794 lines
24 KiB
Rust
794 lines
24 KiB
Rust
//! Pump Component Implementation
|
||
//!
|
||
//! This module provides a pump component for hydraulic systems using
|
||
//! polynomial performance curves and affinity laws for variable speed operation.
|
||
//!
|
||
//! ## Performance Curves
|
||
//!
|
||
//! **Head Curve:** H = a₀ + a₁Q + a₂Q² + a₃Q³
|
||
//!
|
||
//! **Efficiency Curve:** η = b₀ + b₁Q + b₂Q²
|
||
//!
|
||
//! **Hydraulic Power:** P_hydraulic = ρ × g × Q × H / η
|
||
//!
|
||
//! ## Affinity Laws (Variable Speed)
|
||
//!
|
||
//! When operating at reduced speed (VFD):
|
||
//! - Q₂/Q₁ = N₂/N₁
|
||
//! - H₂/H₁ = (N₂/N₁)²
|
||
//! - P₂/P₁ = (N₂/N₁)³
|
||
|
||
use crate::polynomials::{AffinityLaws, PerformanceCurves, Polynomial1D};
|
||
use crate::port::{Connected, Disconnected, FluidId, Port};
|
||
use crate::state_machine::StateManageable;
|
||
use crate::{
|
||
CircuitId, Component, ComponentError, ConnectedPort, JacobianBuilder, OperationalState,
|
||
ResidualVector, SystemState,
|
||
};
|
||
use entropyk_core::{MassFlow, Power};
|
||
use serde::{Deserialize, Serialize};
|
||
use std::marker::PhantomData;
|
||
|
||
/// Pump performance curve coefficients.
|
||
///
|
||
/// Defines the polynomial coefficients for the pump's head-flow curve
|
||
/// and efficiency curve.
|
||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||
pub struct PumpCurves {
|
||
/// Performance curves (head, efficiency, optional power)
|
||
curves: PerformanceCurves,
|
||
}
|
||
|
||
impl PumpCurves {
|
||
/// Creates pump curves from performance curves.
|
||
pub fn new(curves: PerformanceCurves) -> Result<Self, ComponentError> {
|
||
curves.validate()?;
|
||
Ok(Self { curves })
|
||
}
|
||
|
||
/// Creates pump curves from polynomial coefficients.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `head_coeffs` - Head curve coefficients [a0, a1, a2, ...] for H = a0 + a1*Q + a2*Q²
|
||
/// * `eff_coeffs` - Efficiency coefficients [b0, b1, b2, ...] for η = b0 + b1*Q + b2*Q²
|
||
///
|
||
/// # Units
|
||
///
|
||
/// * Q (flow) in m³/s
|
||
/// * H (head) in meters
|
||
/// * η (efficiency) as decimal (0.0 to 1.0)
|
||
pub fn from_coefficients(
|
||
head_coeffs: Vec<f64>,
|
||
eff_coeffs: Vec<f64>,
|
||
) -> Result<Self, ComponentError> {
|
||
let head_curve = Polynomial1D::new(head_coeffs);
|
||
let eff_curve = Polynomial1D::new(eff_coeffs);
|
||
let curves = PerformanceCurves::simple(head_curve, eff_curve);
|
||
Self::new(curves)
|
||
}
|
||
|
||
/// Creates a quadratic pump curve.
|
||
///
|
||
/// H = a0 + a1*Q + a2*Q²
|
||
/// η = b0 + b1*Q + b2*Q²
|
||
pub fn quadratic(
|
||
h0: f64,
|
||
h1: f64,
|
||
h2: f64,
|
||
e0: f64,
|
||
e1: f64,
|
||
e2: f64,
|
||
) -> Result<Self, ComponentError> {
|
||
Self::from_coefficients(vec![h0, h1, h2], vec![e0, e1, e2])
|
||
}
|
||
|
||
/// Creates a cubic pump curve (3rd-order polynomial for head).
|
||
///
|
||
/// H = a0 + a1*Q + a2*Q² + a3*Q³
|
||
/// η = b0 + b1*Q + b2*Q²
|
||
pub fn cubic(
|
||
h0: f64,
|
||
h1: f64,
|
||
h2: f64,
|
||
h3: f64,
|
||
e0: f64,
|
||
e1: f64,
|
||
e2: f64,
|
||
) -> Result<Self, ComponentError> {
|
||
Self::from_coefficients(vec![h0, h1, h2, h3], vec![e0, e1, e2])
|
||
}
|
||
|
||
/// Returns the head at the given flow rate (at full speed).
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `flow_m3_per_s` - Volumetric flow rate in m³/s
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// Head in meters
|
||
pub fn head_at_flow(&self, flow_m3_per_s: f64) -> f64 {
|
||
self.curves.head_curve.evaluate(flow_m3_per_s)
|
||
}
|
||
|
||
/// Returns the efficiency at the given flow rate (at full speed).
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `flow_m3_per_s` - Volumetric flow rate in m³/s
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// Efficiency as decimal (0.0 to 1.0)
|
||
pub fn efficiency_at_flow(&self, flow_m3_per_s: f64) -> f64 {
|
||
let eta = self.curves.efficiency_curve.evaluate(flow_m3_per_s);
|
||
// Clamp efficiency to valid range
|
||
eta.clamp(0.0, 1.0)
|
||
}
|
||
|
||
/// Returns reference to the performance curves.
|
||
pub fn curves(&self) -> &PerformanceCurves {
|
||
&self.curves
|
||
}
|
||
}
|
||
|
||
impl Default for PumpCurves {
|
||
fn default() -> Self {
|
||
Self::quadratic(30.0, 0.0, 0.0, 0.7, 0.0, 0.0).unwrap()
|
||
}
|
||
}
|
||
|
||
/// A pump component with polynomial performance curves.
|
||
///
|
||
/// The pump uses the Type-State pattern to ensure ports are connected
|
||
/// before use in simulations.
|
||
///
|
||
/// # Example
|
||
///
|
||
/// ```ignore
|
||
/// use entropyk_components::pump::{Pump, PumpCurves};
|
||
/// use entropyk_components::port::{FluidId, Port};
|
||
/// use entropyk_core::{Pressure, Enthalpy};
|
||
///
|
||
/// // Create pump curves: H = 30 - 10*Q - 50*Q² (in m and m³/s)
|
||
/// let curves = PumpCurves::quadratic(30.0, -10.0, -50.0, 0.5, 0.3, -0.5).unwrap();
|
||
///
|
||
/// let inlet = Port::new(
|
||
/// FluidId::new("Water"),
|
||
/// Pressure::from_bar(1.0),
|
||
/// Enthalpy::from_joules_per_kg(100000.0),
|
||
/// );
|
||
/// let outlet = Port::new(
|
||
/// FluidId::new("Water"),
|
||
/// Pressure::from_bar(1.0),
|
||
/// Enthalpy::from_joules_per_kg(100000.0),
|
||
/// );
|
||
///
|
||
/// let pump = Pump::new(curves, inlet, outlet, 1000.0).unwrap();
|
||
/// ```
|
||
#[derive(Debug, Clone)]
|
||
pub struct Pump<State> {
|
||
/// Performance curves
|
||
curves: PumpCurves,
|
||
/// Inlet port
|
||
port_inlet: Port<State>,
|
||
/// Outlet port
|
||
port_outlet: Port<State>,
|
||
/// Fluid density in kg/m³
|
||
fluid_density_kg_per_m3: f64,
|
||
/// Speed ratio (0.0 to 1.0), default 1.0 (full speed)
|
||
speed_ratio: f64,
|
||
/// Circuit identifier
|
||
circuit_id: CircuitId,
|
||
/// Operational state
|
||
operational_state: OperationalState,
|
||
/// Phantom data for type state
|
||
_state: PhantomData<State>,
|
||
}
|
||
|
||
impl Pump<Disconnected> {
|
||
/// Creates a new disconnected pump.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `curves` - Pump performance curves
|
||
/// * `port_inlet` - Inlet port (disconnected)
|
||
/// * `port_outlet` - Outlet port (disconnected)
|
||
/// * `fluid_density` - Fluid density in kg/m³
|
||
///
|
||
/// # Errors
|
||
///
|
||
/// Returns an error if:
|
||
/// - Ports have different fluid types
|
||
/// - Fluid density is not positive
|
||
pub fn new(
|
||
curves: PumpCurves,
|
||
port_inlet: Port<Disconnected>,
|
||
port_outlet: Port<Disconnected>,
|
||
fluid_density: f64,
|
||
) -> Result<Self, ComponentError> {
|
||
if port_inlet.fluid_id() != port_outlet.fluid_id() {
|
||
return Err(ComponentError::InvalidState(
|
||
"Inlet and outlet ports must have the same fluid type".to_string(),
|
||
));
|
||
}
|
||
|
||
if fluid_density <= 0.0 {
|
||
return Err(ComponentError::InvalidState(
|
||
"Fluid density must be positive".to_string(),
|
||
));
|
||
}
|
||
|
||
Ok(Self {
|
||
curves,
|
||
port_inlet,
|
||
port_outlet,
|
||
fluid_density_kg_per_m3: fluid_density,
|
||
speed_ratio: 1.0,
|
||
circuit_id: CircuitId::default(),
|
||
operational_state: OperationalState::default(),
|
||
_state: PhantomData,
|
||
})
|
||
}
|
||
|
||
/// Returns the fluid identifier.
|
||
pub fn fluid_id(&self) -> &FluidId {
|
||
self.port_inlet.fluid_id()
|
||
}
|
||
|
||
/// Returns the inlet port.
|
||
pub fn port_inlet(&self) -> &Port<Disconnected> {
|
||
&self.port_inlet
|
||
}
|
||
|
||
/// Returns the outlet port.
|
||
pub fn port_outlet(&self) -> &Port<Disconnected> {
|
||
&self.port_outlet
|
||
}
|
||
|
||
/// Returns the fluid density.
|
||
pub fn fluid_density(&self) -> f64 {
|
||
self.fluid_density_kg_per_m3
|
||
}
|
||
|
||
/// Returns the performance curves.
|
||
pub fn curves(&self) -> &PumpCurves {
|
||
&self.curves
|
||
}
|
||
|
||
/// Returns the speed ratio.
|
||
pub fn speed_ratio(&self) -> f64 {
|
||
self.speed_ratio
|
||
}
|
||
|
||
/// Sets the speed ratio (0.0 to 1.0).
|
||
pub fn set_speed_ratio(&mut self, ratio: f64) -> Result<(), ComponentError> {
|
||
if !(0.0..=1.0).contains(&ratio) {
|
||
return Err(ComponentError::InvalidState(
|
||
"Speed ratio must be between 0.0 and 1.0".to_string(),
|
||
));
|
||
}
|
||
self.speed_ratio = ratio;
|
||
Ok(())
|
||
}
|
||
}
|
||
|
||
impl Pump<Connected> {
|
||
/// Returns the inlet port.
|
||
pub fn port_inlet(&self) -> &Port<Connected> {
|
||
&self.port_inlet
|
||
}
|
||
|
||
/// Returns the outlet port.
|
||
pub fn port_outlet(&self) -> &Port<Connected> {
|
||
&self.port_outlet
|
||
}
|
||
|
||
/// Calculates the pressure rise across the pump.
|
||
///
|
||
/// Uses the head curve and converts to pressure:
|
||
/// ΔP = ρ × g × H
|
||
///
|
||
/// Applies affinity laws for variable speed operation.
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `flow_m3_per_s` - Volumetric flow rate in m³/s
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// Pressure rise in Pascals
|
||
pub fn pressure_rise(&self, flow_m3_per_s: f64) -> f64 {
|
||
// Handle zero speed - pump produces no pressure
|
||
if self.speed_ratio <= 0.0 {
|
||
return 0.0;
|
||
}
|
||
|
||
// Handle negative flow gracefully by using a linear extrapolation from Q=0
|
||
// to prevent polynomial extrapolation issues with quadratic/cubic terms
|
||
if flow_m3_per_s < 0.0 {
|
||
let h0 = self.curves.head_at_flow(0.0);
|
||
let h_eps = self.curves.head_at_flow(1e-6);
|
||
let dh_dq = (h_eps - h0) / 1e-6;
|
||
|
||
let head_m = h0 + dh_dq * flow_m3_per_s;
|
||
let actual_head = AffinityLaws::scale_head(head_m, self.speed_ratio);
|
||
const G: f64 = 9.80665; // m/s²
|
||
return self.fluid_density_kg_per_m3 * G * actual_head;
|
||
}
|
||
|
||
// Handle exactly zero flow
|
||
if flow_m3_per_s == 0.0 {
|
||
// At zero flow, use the shut-off head scaled by speed
|
||
let head_m = self.curves.head_at_flow(0.0);
|
||
let actual_head = AffinityLaws::scale_head(head_m, self.speed_ratio);
|
||
const G: f64 = 9.80665; // m/s²
|
||
return self.fluid_density_kg_per_m3 * G * actual_head;
|
||
}
|
||
|
||
// Apply affinity law to get equivalent flow at full speed
|
||
let equivalent_flow = AffinityLaws::unscale_flow(flow_m3_per_s, self.speed_ratio);
|
||
|
||
// Get head at equivalent flow
|
||
let head_m = self.curves.head_at_flow(equivalent_flow);
|
||
|
||
// Apply affinity law to scale head back to actual speed
|
||
let actual_head = AffinityLaws::scale_head(head_m, self.speed_ratio);
|
||
|
||
// Convert head to pressure: P = ρ × g × H
|
||
const G: f64 = 9.80665; // m/s²
|
||
self.fluid_density_kg_per_m3 * G * actual_head
|
||
}
|
||
|
||
/// Calculates the efficiency at the given flow rate.
|
||
///
|
||
/// Applies affinity laws to find the equivalent operating point.
|
||
pub fn efficiency(&self, flow_m3_per_s: f64) -> f64 {
|
||
// Handle zero speed - pump is not running
|
||
if self.speed_ratio <= 0.0 {
|
||
return 0.0;
|
||
}
|
||
|
||
// Handle zero flow
|
||
if flow_m3_per_s <= 0.0 {
|
||
return self.curves.efficiency_at_flow(0.0);
|
||
}
|
||
|
||
let equivalent_flow = AffinityLaws::unscale_flow(flow_m3_per_s, self.speed_ratio);
|
||
self.curves.efficiency_at_flow(equivalent_flow)
|
||
}
|
||
|
||
/// Calculates the hydraulic power consumption.
|
||
///
|
||
/// P_hydraulic = Q × ΔP / η
|
||
///
|
||
/// # Arguments
|
||
///
|
||
/// * `flow_m3_per_s` - Volumetric flow rate in m³/s
|
||
///
|
||
/// # Returns
|
||
///
|
||
/// Power in Watts
|
||
pub fn hydraulic_power(&self, flow_m3_per_s: f64) -> Power {
|
||
if flow_m3_per_s <= 0.0 || self.speed_ratio <= 0.0 {
|
||
return Power::from_watts(0.0);
|
||
}
|
||
|
||
let delta_p = self.pressure_rise(flow_m3_per_s);
|
||
let eta = self.efficiency(flow_m3_per_s);
|
||
|
||
if eta <= 0.0 {
|
||
return Power::from_watts(0.0);
|
||
}
|
||
|
||
// P = Q × ΔP / η
|
||
let power_w = flow_m3_per_s * delta_p / eta;
|
||
Power::from_watts(power_w)
|
||
}
|
||
|
||
/// Calculates mass flow rate from volumetric flow.
|
||
pub fn mass_flow_from_volumetric(&self, flow_m3_per_s: f64) -> MassFlow {
|
||
MassFlow::from_kg_per_s(flow_m3_per_s * self.fluid_density_kg_per_m3)
|
||
}
|
||
|
||
/// Calculates volumetric flow rate from mass flow.
|
||
pub fn volumetric_from_mass_flow(&self, mass_flow: MassFlow) -> f64 {
|
||
mass_flow.to_kg_per_s() / self.fluid_density_kg_per_m3
|
||
}
|
||
|
||
/// Returns the fluid density.
|
||
pub fn fluid_density(&self) -> f64 {
|
||
self.fluid_density_kg_per_m3
|
||
}
|
||
|
||
/// Returns the performance curves.
|
||
pub fn curves(&self) -> &PumpCurves {
|
||
&self.curves
|
||
}
|
||
|
||
/// Returns the speed ratio.
|
||
pub fn speed_ratio(&self) -> f64 {
|
||
self.speed_ratio
|
||
}
|
||
|
||
/// Sets the speed ratio (0.0 to 1.0).
|
||
pub fn set_speed_ratio(&mut self, ratio: f64) -> Result<(), ComponentError> {
|
||
if !(0.0..=1.0).contains(&ratio) {
|
||
return Err(ComponentError::InvalidState(
|
||
"Speed ratio must be between 0.0 and 1.0".to_string(),
|
||
));
|
||
}
|
||
self.speed_ratio = ratio;
|
||
Ok(())
|
||
}
|
||
|
||
/// Returns both ports as a slice for solver topology.
|
||
pub fn get_ports_slice(&self) -> [&Port<Connected>; 2] {
|
||
[&self.port_inlet, &self.port_outlet]
|
||
}
|
||
}
|
||
|
||
impl Component for Pump<Connected> {
|
||
fn compute_residuals(
|
||
&self,
|
||
state: &SystemState,
|
||
residuals: &mut ResidualVector,
|
||
) -> Result<(), ComponentError> {
|
||
if residuals.len() != self.n_equations() {
|
||
return Err(ComponentError::InvalidResidualDimensions {
|
||
expected: self.n_equations(),
|
||
actual: residuals.len(),
|
||
});
|
||
}
|
||
|
||
// Handle operational states
|
||
match self.operational_state {
|
||
OperationalState::Off => {
|
||
residuals[0] = state[0]; // Mass flow = 0
|
||
residuals[1] = 0.0; // No energy transfer
|
||
return Ok(());
|
||
}
|
||
OperationalState::Bypass => {
|
||
// Behaves as a pipe: no pressure rise, no energy change
|
||
let p_in = self.port_inlet.pressure().to_pascals();
|
||
let p_out = self.port_outlet.pressure().to_pascals();
|
||
let h_in = self.port_inlet.enthalpy().to_joules_per_kg();
|
||
let h_out = self.port_outlet.enthalpy().to_joules_per_kg();
|
||
|
||
residuals[0] = p_in - p_out;
|
||
residuals[1] = h_in - h_out;
|
||
return Ok(());
|
||
}
|
||
OperationalState::On => {}
|
||
}
|
||
|
||
if state.len() < 2 {
|
||
return Err(ComponentError::InvalidStateDimensions {
|
||
expected: 2,
|
||
actual: state.len(),
|
||
});
|
||
}
|
||
|
||
// State: [mass_flow_kg_s, power_w]
|
||
let mass_flow_kg_s = state[0];
|
||
let _power_w = state[1];
|
||
|
||
// Convert to volumetric flow
|
||
let flow_m3_s = mass_flow_kg_s / self.fluid_density_kg_per_m3;
|
||
|
||
// Calculate pressure rise from curves
|
||
let delta_p_calc = self.pressure_rise(flow_m3_s);
|
||
|
||
// Get port pressures
|
||
let p_in = self.port_inlet.pressure().to_pascals();
|
||
let p_out = self.port_outlet.pressure().to_pascals();
|
||
let delta_p_actual = p_out - p_in;
|
||
|
||
// Residual 0: Pressure balance
|
||
residuals[0] = delta_p_calc - delta_p_actual;
|
||
|
||
// Residual 1: Power balance
|
||
let power_calc = self.hydraulic_power(flow_m3_s).to_watts();
|
||
residuals[1] = power_calc - _power_w;
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn jacobian_entries(
|
||
&self,
|
||
state: &SystemState,
|
||
jacobian: &mut JacobianBuilder,
|
||
) -> Result<(), ComponentError> {
|
||
if state.len() < 2 {
|
||
return Err(ComponentError::InvalidStateDimensions {
|
||
expected: 2,
|
||
actual: state.len(),
|
||
});
|
||
}
|
||
|
||
let mass_flow_kg_s = state[0];
|
||
let flow_m3_s = mass_flow_kg_s / self.fluid_density_kg_per_m3;
|
||
|
||
// Numerical derivative of pressure with respect to mass flow
|
||
let h = 0.001;
|
||
let p_plus = self.pressure_rise(flow_m3_s + h / self.fluid_density_kg_per_m3);
|
||
let p_minus = self.pressure_rise(flow_m3_s - h / self.fluid_density_kg_per_m3);
|
||
let dp_dm = (p_plus - p_minus) / (2.0 * h);
|
||
|
||
// ∂r₀/∂ṁ = dΔP/dṁ
|
||
jacobian.add_entry(0, 0, dp_dm);
|
||
|
||
// ∂r₀/∂P = -1 (constant)
|
||
jacobian.add_entry(0, 1, 0.0);
|
||
|
||
// Numerical derivative of power with respect to mass flow
|
||
let pow_plus = self
|
||
.hydraulic_power(flow_m3_s + h / self.fluid_density_kg_per_m3)
|
||
.to_watts();
|
||
let pow_minus = self
|
||
.hydraulic_power(flow_m3_s - h / self.fluid_density_kg_per_m3)
|
||
.to_watts();
|
||
let dpow_dm = (pow_plus - pow_minus) / (2.0 * h);
|
||
|
||
// ∂r₁/∂ṁ
|
||
jacobian.add_entry(1, 0, dpow_dm);
|
||
|
||
// ∂r₁/∂P = -1
|
||
jacobian.add_entry(1, 1, -1.0);
|
||
|
||
Ok(())
|
||
}
|
||
|
||
fn n_equations(&self) -> usize {
|
||
2
|
||
}
|
||
|
||
fn get_ports(&self) -> &[ConnectedPort] {
|
||
&[]
|
||
}
|
||
}
|
||
|
||
impl StateManageable for Pump<Connected> {
|
||
fn state(&self) -> OperationalState {
|
||
self.operational_state
|
||
}
|
||
|
||
fn set_state(&mut self, state: OperationalState) -> Result<(), ComponentError> {
|
||
if self.operational_state.can_transition_to(state) {
|
||
let from = self.operational_state;
|
||
self.operational_state = state;
|
||
self.on_state_change(from, state);
|
||
Ok(())
|
||
} else {
|
||
Err(ComponentError::InvalidStateTransition {
|
||
from: self.operational_state,
|
||
to: state,
|
||
reason: "Transition not allowed".to_string(),
|
||
})
|
||
}
|
||
}
|
||
|
||
fn can_transition_to(&self, target: OperationalState) -> bool {
|
||
self.operational_state.can_transition_to(target)
|
||
}
|
||
|
||
fn circuit_id(&self) -> &CircuitId {
|
||
&self.circuit_id
|
||
}
|
||
|
||
fn set_circuit_id(&mut self, circuit_id: CircuitId) {
|
||
self.circuit_id = circuit_id;
|
||
}
|
||
}
|
||
|
||
#[cfg(test)]
|
||
mod tests {
|
||
use super::*;
|
||
use crate::port::FluidId;
|
||
use approx::assert_relative_eq;
|
||
use entropyk_core::{Enthalpy, Pressure};
|
||
|
||
fn create_test_curves() -> PumpCurves {
|
||
// Typical small pump:
|
||
// H = 30 - 10*Q - 50*Q² (m, Q in m³/s)
|
||
// η = 0.6 + 1.0*Q - 2.0*Q²
|
||
PumpCurves::quadratic(30.0, -10.0, -50.0, 0.6, 1.0, -2.0).unwrap()
|
||
}
|
||
|
||
fn create_test_pump_disconnected() -> Pump<Disconnected> {
|
||
let curves = create_test_curves();
|
||
let inlet = Port::new(
|
||
FluidId::new("Water"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
let outlet = Port::new(
|
||
FluidId::new("Water"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
Pump::new(curves, inlet, outlet, 1000.0).unwrap()
|
||
}
|
||
|
||
fn create_test_pump_connected() -> Pump<Connected> {
|
||
let curves = create_test_curves();
|
||
let inlet = Port::new(
|
||
FluidId::new("Water"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
let outlet = Port::new(
|
||
FluidId::new("Water"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
let (inlet_conn, outlet_conn) = inlet.connect(outlet).unwrap();
|
||
|
||
Pump {
|
||
curves,
|
||
port_inlet: inlet_conn,
|
||
port_outlet: outlet_conn,
|
||
fluid_density_kg_per_m3: 1000.0,
|
||
speed_ratio: 1.0,
|
||
circuit_id: CircuitId::default(),
|
||
operational_state: OperationalState::default(),
|
||
_state: PhantomData,
|
||
}
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_curves_creation() {
|
||
let curves = create_test_curves();
|
||
assert_eq!(curves.head_at_flow(0.0), 30.0);
|
||
assert_relative_eq!(curves.efficiency_at_flow(0.0), 0.6);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_curves_head() {
|
||
let curves = create_test_curves();
|
||
// H = 30 - 10*0.5 - 50*0.25 = 30 - 5 - 12.5 = 12.5 m
|
||
let head = curves.head_at_flow(0.5);
|
||
assert_relative_eq!(head, 12.5, epsilon = 1e-10);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_curves_efficiency_clamped() {
|
||
let curves = create_test_curves();
|
||
// At very high flow, efficiency might go negative
|
||
// Should be clamped to 0
|
||
let eff = curves.efficiency_at_flow(10.0);
|
||
assert!(eff >= 0.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_creation() {
|
||
let pump = create_test_pump_disconnected();
|
||
assert_eq!(pump.fluid_density(), 1000.0);
|
||
assert_eq!(pump.speed_ratio(), 1.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_invalid_density() {
|
||
let curves = create_test_curves();
|
||
let inlet = Port::new(
|
||
FluidId::new("Water"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
let outlet = Port::new(
|
||
FluidId::new("Water"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
|
||
let result = Pump::new(curves, inlet, outlet, -1.0);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_different_fluids() {
|
||
let curves = create_test_curves();
|
||
let inlet = Port::new(
|
||
FluidId::new("Water"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
let outlet = Port::new(
|
||
FluidId::new("Glycol"),
|
||
Pressure::from_bar(1.0),
|
||
Enthalpy::from_joules_per_kg(100000.0),
|
||
);
|
||
|
||
let result = Pump::new(curves, inlet, outlet, 1000.0);
|
||
assert!(result.is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_set_speed_ratio() {
|
||
let mut pump = create_test_pump_connected();
|
||
assert!(pump.set_speed_ratio(0.8).is_ok());
|
||
assert_eq!(pump.speed_ratio(), 0.8);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_set_speed_ratio_invalid() {
|
||
let mut pump = create_test_pump_connected();
|
||
assert!(pump.set_speed_ratio(1.5).is_err());
|
||
assert!(pump.set_speed_ratio(-0.1).is_err());
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_pressure_rise_full_speed() {
|
||
let pump = create_test_pump_connected();
|
||
// At Q=0: H=30m, P = 1000 * 9.8 * 30 ≈ 294200 Pa
|
||
let delta_p = pump.pressure_rise(0.0);
|
||
let expected = 1000.0 * 9.80665 * 30.0;
|
||
assert_relative_eq!(delta_p, expected, epsilon = 100.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_pressure_rise_reduced_speed() {
|
||
let mut pump = create_test_pump_connected();
|
||
pump.set_speed_ratio(0.5).unwrap();
|
||
|
||
// At 50% speed, shut-off head is 25% of full speed
|
||
// H = 0.25 * 30 = 7.5 m
|
||
let delta_p = pump.pressure_rise(0.0);
|
||
let expected = 1000.0 * 9.80665 * 7.5;
|
||
assert_relative_eq!(delta_p, expected, epsilon = 100.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_hydraulic_power() {
|
||
let pump = create_test_pump_connected();
|
||
|
||
// At Q=0.1 m³/s: H ≈ 30 - 1 - 0.5 = 28.5 m
|
||
// η ≈ 0.6 + 0.1 - 0.02 = 0.68
|
||
// P = 1000 * 9.8 * 0.1 * 28.5 / 0.68 ≈ 4110 W
|
||
let power = pump.hydraulic_power(0.1);
|
||
assert!(power.to_watts() > 0.0);
|
||
assert!(power.to_watts() < 50000.0);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_affinity_laws_power() {
|
||
let pump_full = create_test_pump_connected();
|
||
|
||
let mut pump_half = create_test_pump_connected();
|
||
pump_half.set_speed_ratio(0.5).unwrap();
|
||
|
||
// Power at half speed should be ~12.5% of full speed (cube law)
|
||
// At the same equivalent flow point
|
||
let power_full = pump_full.hydraulic_power(0.1);
|
||
let power_half = pump_half.hydraulic_power(0.05); // Half the flow
|
||
|
||
// P_half / P_full ≈ 0.5³ = 0.125
|
||
let ratio = power_half.to_watts() / power_full.to_watts();
|
||
assert_relative_eq!(ratio, 0.125, epsilon = 0.05);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_component_n_equations() {
|
||
let pump = create_test_pump_connected();
|
||
assert_eq!(pump.n_equations(), 2);
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_component_compute_residuals() {
|
||
let pump = create_test_pump_connected();
|
||
let state = vec![50.0, 2000.0]; // mass flow, power
|
||
let mut residuals = vec![0.0; 2];
|
||
|
||
let result = pump.compute_residuals(&state, &mut residuals);
|
||
assert!(result.is_ok());
|
||
}
|
||
|
||
#[test]
|
||
fn test_pump_state_manageable() {
|
||
let pump = create_test_pump_connected();
|
||
assert_eq!(pump.state(), OperationalState::On);
|
||
assert!(pump.can_transition_to(OperationalState::Off));
|
||
}
|
||
}
|