feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
753
crates/components/src/port.rs
Normal file
753
crates/components/src/port.rs
Normal file
@@ -0,0 +1,753 @@
|
||||
//! Port and Connection System
|
||||
//!
|
||||
//! This module provides the foundation for connecting thermodynamic components
|
||||
//! using the Type-State pattern for compile-time connection safety.
|
||||
//!
|
||||
//! ## Type-State Pattern
|
||||
//!
|
||||
//! Ports have two states:
|
||||
//! - `Disconnected`: Initial state, cannot be used in solver
|
||||
//! - `Connected`: Linked to another port, ready for simulation
|
||||
//!
|
||||
//! State transitions are enforced at compile time:
|
||||
//! ```text
|
||||
//! Port<Disconnected> --connect()--> Port<Connected>
|
||||
//! ↑ │
|
||||
//! └───────── (no way back) ────────────┘
|
||||
//! ```
|
||||
//!
|
||||
//! ## Connection Semantics
|
||||
//!
|
||||
//! Connected ports validate continuity (pressure/enthalpy match) at connection time,
|
||||
//! but track values independently afterward. This allows the solver to update port
|
||||
//! states during iteration without requiring synchronization.
|
||||
//!
|
||||
//! ## Example
|
||||
//!
|
||||
//! ```rust
|
||||
//! use entropyk_components::port::{Port, Disconnected, Connected, FluidId, ConnectionError};
|
||||
//! use entropyk_core::{Pressure, Enthalpy};
|
||||
//!
|
||||
//! // Create two disconnected ports
|
||||
//! let port1 = Port::new(FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400000.0));
|
||||
//! let port2 = Port::new(FluidId::new("R134a"), Pressure::from_bar(1.0), Enthalpy::from_joules_per_kg(400000.0));
|
||||
//!
|
||||
//! // Connect them
|
||||
//! let (connected1, connected2) = port1.connect(port2)?;
|
||||
//!
|
||||
//! // Ports track values independently for solver flexibility
|
||||
//! assert_eq!(connected1.pressure().to_bar(), 1.0);
|
||||
//! # Ok::<(), ConnectionError>(())
|
||||
//! ```
|
||||
|
||||
use entropyk_core::{Enthalpy, Pressure};
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use thiserror::Error;
|
||||
|
||||
/// Default relative tolerance for pressure matching (0.01% = 100 ppm).
|
||||
/// For 1 bar = 100,000 Pa, this allows 10 Pa difference.
|
||||
const PRESSURE_TOLERANCE_FRACTION: f64 = 1e-4;
|
||||
|
||||
/// Default absolute tolerance for enthalpy matching (100 J/kg).
|
||||
/// This is approximately 0.024 kJ/kg, reasonable for HVAC calculations.
|
||||
const ENTHALPY_TOLERANCE_J_KG: f64 = 100.0;
|
||||
|
||||
/// Minimum absolute pressure tolerance (1 Pa) to avoid issues near zero.
|
||||
const MIN_PRESSURE_TOLERANCE_PA: f64 = 1.0;
|
||||
|
||||
/// Errors that can occur during port operations.
|
||||
#[derive(Error, Debug, Clone, PartialEq)]
|
||||
pub enum ConnectionError {
|
||||
/// Attempted to connect ports with incompatible fluids.
|
||||
#[error("Incompatible fluids: cannot connect {from} to {to}")]
|
||||
IncompatibleFluid {
|
||||
/// Source fluid identifier
|
||||
from: String,
|
||||
/// Target fluid identifier
|
||||
to: String,
|
||||
},
|
||||
|
||||
/// Pressure mismatch at connection point.
|
||||
#[error(
|
||||
"Pressure mismatch: {from_pressure} Pa vs {to_pressure} Pa (tolerance: {tolerance} Pa)"
|
||||
)]
|
||||
PressureMismatch {
|
||||
/// Pressure at source port (Pa)
|
||||
from_pressure: f64,
|
||||
/// Pressure at target port (Pa)
|
||||
to_pressure: f64,
|
||||
/// Tolerance used for comparison (Pa)
|
||||
tolerance: f64,
|
||||
},
|
||||
|
||||
/// Enthalpy mismatch at connection point.
|
||||
#[error("Enthalpy mismatch: {from_enthalpy} J/kg vs {to_enthalpy} J/kg (tolerance: {tolerance} J/kg)")]
|
||||
EnthalpyMismatch {
|
||||
/// Enthalpy at source port (J/kg)
|
||||
from_enthalpy: f64,
|
||||
/// Enthalpy at target port (J/kg)
|
||||
to_enthalpy: f64,
|
||||
/// Tolerance used for comparison (J/kg)
|
||||
tolerance: f64,
|
||||
},
|
||||
|
||||
/// Attempted to connect a port that is already connected.
|
||||
#[error("Port is already connected and cannot be reconnected")]
|
||||
AlreadyConnected,
|
||||
|
||||
/// Detected a cycle in the connection graph.
|
||||
#[error("Connection would create a cycle in the system topology")]
|
||||
CycleDetected,
|
||||
|
||||
/// Invalid port index.
|
||||
#[error(
|
||||
"Invalid port index {index}: component has {port_count} ports (valid: 0..{max_index})"
|
||||
)]
|
||||
InvalidPortIndex {
|
||||
/// The invalid port index that was requested
|
||||
index: usize,
|
||||
/// Number of ports on the component
|
||||
port_count: usize,
|
||||
/// Maximum valid index (port_count - 1, or 0 if no ports)
|
||||
max_index: usize,
|
||||
},
|
||||
|
||||
/// Invalid node index.
|
||||
#[error("Invalid node index: {0}")]
|
||||
InvalidNodeIndex(usize),
|
||||
}
|
||||
|
||||
/// Type-state marker for disconnected ports.
|
||||
///
|
||||
/// Ports in this state cannot be used in the solver until connected.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Disconnected;
|
||||
|
||||
/// Type-state marker for connected ports.
|
||||
///
|
||||
/// Ports in this state are linked to another port and ready for simulation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Connected;
|
||||
|
||||
/// Identifier for thermodynamic fluids.
|
||||
///
|
||||
/// Used to ensure only compatible fluids are connected.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct FluidId(String);
|
||||
|
||||
impl FluidId {
|
||||
/// Creates a new fluid identifier.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `id` - Unique identifier for the fluid (e.g., "R134a", "Water")
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_components::port::FluidId;
|
||||
///
|
||||
/// let fluid = FluidId::new("R134a");
|
||||
/// ```
|
||||
pub fn new(id: impl Into<String>) -> Self {
|
||||
FluidId(id.into())
|
||||
}
|
||||
|
||||
/// Returns the fluid identifier as a string slice.
|
||||
pub fn as_str(&self) -> &str {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FluidId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// A thermodynamic port for connecting components.
|
||||
///
|
||||
/// Ports use the Type-State pattern to enforce connection safety at compile time.
|
||||
/// A `Port<Disconnected>` must be connected before it can be used in simulations.
|
||||
///
|
||||
/// # Type Parameters
|
||||
///
|
||||
/// * `State` - Either `Disconnected` or `Connected`, tracking the port's state
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_components::port::{Port, Disconnected, FluidId};
|
||||
/// use entropyk_core::{Pressure, Enthalpy};
|
||||
///
|
||||
/// // Create a disconnected port
|
||||
/// let port: Port<Disconnected> = Port::new(
|
||||
/// FluidId::new("R134a"),
|
||||
/// Pressure::from_bar(1.0),
|
||||
/// Enthalpy::from_joules_per_kg(400000.0)
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct Port<State> {
|
||||
fluid_id: FluidId,
|
||||
pressure: Pressure,
|
||||
enthalpy: Enthalpy,
|
||||
_state: PhantomData<State>,
|
||||
}
|
||||
|
||||
/// Helper to validate connection parameters.
|
||||
fn validate_connection_params(
|
||||
from_fluid: &FluidId,
|
||||
from_p: Pressure,
|
||||
from_h: Enthalpy,
|
||||
to_fluid: &FluidId,
|
||||
to_p: Pressure,
|
||||
to_h: Enthalpy,
|
||||
) -> Result<(), ConnectionError> {
|
||||
if from_fluid != to_fluid {
|
||||
return Err(ConnectionError::IncompatibleFluid {
|
||||
from: from_fluid.to_string(),
|
||||
to: to_fluid.to_string(),
|
||||
});
|
||||
}
|
||||
|
||||
let pressure_tol =
|
||||
(from_p.to_pascals().abs() * PRESSURE_TOLERANCE_FRACTION).max(MIN_PRESSURE_TOLERANCE_PA);
|
||||
let pressure_diff = (from_p.to_pascals() - to_p.to_pascals()).abs();
|
||||
if pressure_diff > pressure_tol {
|
||||
return Err(ConnectionError::PressureMismatch {
|
||||
from_pressure: from_p.to_pascals(),
|
||||
to_pressure: to_p.to_pascals(),
|
||||
tolerance: pressure_tol,
|
||||
});
|
||||
}
|
||||
|
||||
let enthalpy_diff = (from_h.to_joules_per_kg() - to_h.to_joules_per_kg()).abs();
|
||||
if enthalpy_diff > ENTHALPY_TOLERANCE_J_KG {
|
||||
return Err(ConnectionError::EnthalpyMismatch {
|
||||
from_enthalpy: from_h.to_joules_per_kg(),
|
||||
to_enthalpy: to_h.to_joules_per_kg(),
|
||||
tolerance: ENTHALPY_TOLERANCE_J_KG,
|
||||
});
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
impl Port<Disconnected> {
|
||||
/// Creates a new disconnected port.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `fluid_id` - Identifier for the fluid flowing through this port
|
||||
/// * `pressure` - Initial pressure at the port
|
||||
/// * `enthalpy` - Initial specific enthalpy at the port
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_components::port::{Port, FluidId};
|
||||
/// use entropyk_core::{Pressure, Enthalpy};
|
||||
///
|
||||
/// let port = Port::new(
|
||||
/// FluidId::new("R134a"),
|
||||
/// Pressure::from_bar(1.0),
|
||||
/// Enthalpy::from_joules_per_kg(400000.0)
|
||||
/// );
|
||||
/// ```
|
||||
pub fn new(fluid_id: FluidId, pressure: Pressure, enthalpy: Enthalpy) -> Self {
|
||||
Self {
|
||||
fluid_id,
|
||||
pressure,
|
||||
enthalpy,
|
||||
_state: PhantomData,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the fluid identifier.
|
||||
pub fn fluid_id(&self) -> &FluidId {
|
||||
&self.fluid_id
|
||||
}
|
||||
|
||||
/// Returns the current pressure.
|
||||
pub fn pressure(&self) -> Pressure {
|
||||
self.pressure
|
||||
}
|
||||
|
||||
/// Returns the current enthalpy.
|
||||
pub fn enthalpy(&self) -> Enthalpy {
|
||||
self.enthalpy
|
||||
}
|
||||
|
||||
/// Connects two disconnected ports.
|
||||
///
|
||||
/// Validates that:
|
||||
/// - Both ports have the same fluid type
|
||||
/// - Pressures match within relative tolerance
|
||||
/// - Enthalpies match within absolute tolerance
|
||||
///
|
||||
/// After connection, ports track values independently, allowing the solver
|
||||
/// to update states during iteration.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `other` - The port to connect to
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// Returns a tuple of `(Port<Connected>, Port<Connected>)` on success,
|
||||
/// or a `ConnectionError` if validation fails.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use entropyk_components::port::{Port, FluidId, ConnectionError};
|
||||
/// use entropyk_core::{Pressure, Enthalpy};
|
||||
///
|
||||
/// let port1 = Port::new(
|
||||
/// FluidId::new("R134a"),
|
||||
/// Pressure::from_pascals(100000.0),
|
||||
/// Enthalpy::from_joules_per_kg(400000.0)
|
||||
/// );
|
||||
/// let port2 = Port::new(
|
||||
/// FluidId::new("R134a"),
|
||||
/// Pressure::from_pascals(100000.0),
|
||||
/// Enthalpy::from_joules_per_kg(400000.0)
|
||||
/// );
|
||||
///
|
||||
/// let (connected1, connected2) = port1.connect(port2)?;
|
||||
/// # Ok::<(), ConnectionError>(())
|
||||
/// ```
|
||||
pub fn connect(
|
||||
self,
|
||||
other: Port<Disconnected>,
|
||||
) -> Result<(Port<Connected>, Port<Connected>), ConnectionError> {
|
||||
validate_connection_params(
|
||||
&self.fluid_id,
|
||||
self.pressure,
|
||||
self.enthalpy,
|
||||
&other.fluid_id,
|
||||
other.pressure,
|
||||
other.enthalpy,
|
||||
)?;
|
||||
|
||||
let avg_pressure = Pressure::from_pascals(
|
||||
(self.pressure.to_pascals() + other.pressure.to_pascals()) / 2.0,
|
||||
);
|
||||
let avg_enthalpy = Enthalpy::from_joules_per_kg(
|
||||
(self.enthalpy.to_joules_per_kg() + other.enthalpy.to_joules_per_kg()) / 2.0,
|
||||
);
|
||||
|
||||
let connected1 = Port {
|
||||
fluid_id: self.fluid_id,
|
||||
pressure: avg_pressure,
|
||||
enthalpy: avg_enthalpy,
|
||||
_state: PhantomData,
|
||||
};
|
||||
|
||||
let connected2 = Port {
|
||||
fluid_id: other.fluid_id,
|
||||
pressure: avg_pressure,
|
||||
enthalpy: avg_enthalpy,
|
||||
_state: PhantomData,
|
||||
};
|
||||
|
||||
Ok((connected1, connected2))
|
||||
}
|
||||
}
|
||||
|
||||
impl Port<Connected> {
|
||||
/// Returns the fluid identifier.
|
||||
pub fn fluid_id(&self) -> &FluidId {
|
||||
&self.fluid_id
|
||||
}
|
||||
|
||||
/// Returns the current pressure.
|
||||
pub fn pressure(&self) -> Pressure {
|
||||
self.pressure
|
||||
}
|
||||
|
||||
/// Returns the current enthalpy.
|
||||
pub fn enthalpy(&self) -> Enthalpy {
|
||||
self.enthalpy
|
||||
}
|
||||
|
||||
/// Updates the pressure at this port.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `pressure` - The new pressure value
|
||||
pub fn set_pressure(&mut self, pressure: Pressure) {
|
||||
self.pressure = pressure;
|
||||
}
|
||||
|
||||
/// Updates the enthalpy at this port.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `enthalpy` - The new enthalpy value
|
||||
pub fn set_enthalpy(&mut self, enthalpy: Enthalpy) {
|
||||
self.enthalpy = enthalpy;
|
||||
}
|
||||
}
|
||||
|
||||
/// A connected port reference that can be stored in components.
|
||||
///
|
||||
/// This type is object-safe and can be used in trait objects.
|
||||
pub type ConnectedPort = Port<Connected>;
|
||||
|
||||
/// Validates that two connected ports are compatible for a flow connection.
|
||||
///
|
||||
/// Uses the same tolerance constants as [`Port::connect`](Port::connect):
|
||||
/// - Pressure: `max(P * 1e-4, 1 Pa)`
|
||||
/// - Enthalpy: 100 J/kg
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `outlet` - Source port (flow direction: outlet → inlet)
|
||||
/// * `inlet` - Target port
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// `Ok(())` if ports are compatible, `Err(ConnectionError)` otherwise.
|
||||
pub fn validate_port_continuity(
|
||||
outlet: &ConnectedPort,
|
||||
inlet: &ConnectedPort,
|
||||
) -> Result<(), ConnectionError> {
|
||||
validate_connection_params(
|
||||
&outlet.fluid_id,
|
||||
outlet.pressure,
|
||||
outlet.enthalpy,
|
||||
&inlet.fluid_id,
|
||||
inlet.pressure,
|
||||
inlet.enthalpy,
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
#[test]
|
||||
fn test_port_creation() {
|
||||
let port = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_bar(1.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
assert_eq!(port.fluid_id().as_str(), "R134a");
|
||||
assert_relative_eq!(port.pressure().to_bar(), 1.0, epsilon = 1e-10);
|
||||
assert_relative_eq!(
|
||||
port.enthalpy().to_joules_per_kg(),
|
||||
400000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_successful_connection() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
let (connected1, connected2) = port1.connect(port2).unwrap();
|
||||
|
||||
assert_eq!(connected1.fluid_id().as_str(), "R134a");
|
||||
assert_eq!(connected2.fluid_id().as_str(), "R134a");
|
||||
assert_relative_eq!(
|
||||
connected1.pressure().to_pascals(),
|
||||
100000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
connected2.pressure().to_pascals(),
|
||||
100000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_incompatible_fluid_error() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("Water"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
let result = port1.connect(port2);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ConnectionError::IncompatibleFluid { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pressure_mismatch_error() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(200000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
let result = port1.connect(port2);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ConnectionError::PressureMismatch { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_enthalpy_mismatch_error() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(500000.0),
|
||||
);
|
||||
|
||||
let result = port1.connect(port2);
|
||||
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(ConnectionError::EnthalpyMismatch { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connected_port_setters() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
let (mut connected1, _) = port1.connect(port2).unwrap();
|
||||
|
||||
connected1.set_pressure(Pressure::from_pascals(150000.0));
|
||||
connected1.set_enthalpy(Enthalpy::from_joules_per_kg(450000.0));
|
||||
|
||||
assert_relative_eq!(
|
||||
connected1.pressure().to_pascals(),
|
||||
150000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
assert_relative_eq!(
|
||||
connected1.enthalpy().to_joules_per_kg(),
|
||||
450000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_ports_track_independently() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
let (mut connected1, connected2) = port1.connect(port2).unwrap();
|
||||
|
||||
connected1.set_pressure(Pressure::from_pascals(150000.0));
|
||||
|
||||
// connected2 should NOT see the change - ports are independent
|
||||
assert_relative_eq!(
|
||||
connected2.pressure().to_pascals(),
|
||||
100000.0,
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fluid_id_creation() {
|
||||
let fluid1 = FluidId::new("R134a");
|
||||
let fluid2 = FluidId::new(String::from("Water"));
|
||||
|
||||
assert_eq!(fluid1.as_str(), "R134a");
|
||||
assert_eq!(fluid2.as_str(), "Water");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_connection_error_display() {
|
||||
let err = ConnectionError::IncompatibleFluid {
|
||||
from: "R134a".to_string(),
|
||||
to: "Water".to_string(),
|
||||
};
|
||||
let msg = format!("{}", err);
|
||||
assert!(msg.contains("Incompatible fluids"));
|
||||
assert!(msg.contains("R134a"));
|
||||
assert!(msg.contains("Water"));
|
||||
|
||||
let err = ConnectionError::PressureMismatch {
|
||||
from_pressure: 100000.0,
|
||||
to_pressure: 200000.0,
|
||||
tolerance: 10.0,
|
||||
};
|
||||
let msg = format!("{}", err);
|
||||
assert!(msg.contains("100000 Pa"));
|
||||
assert!(msg.contains("200000 Pa"));
|
||||
assert!(msg.contains("tolerance"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pressure_averaging_on_connection() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
let (connected1, connected2) = port1.connect(port2).unwrap();
|
||||
|
||||
assert_relative_eq!(
|
||||
connected1.pressure().to_pascals(),
|
||||
connected2.pressure().to_pascals(),
|
||||
epsilon = 1e-10
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pressure_tolerance_with_small_difference() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100005.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
|
||||
let result = port1.connect(port2);
|
||||
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"5 Pa difference should be within tolerance for 100 kPa pressure"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_clone_disconnected_port() {
|
||||
let port1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100000.0),
|
||||
Enthalpy::from_joules_per_kg(400000.0),
|
||||
);
|
||||
let port2 = port1.clone();
|
||||
|
||||
assert_eq!(port1, port2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fluid_id_equality() {
|
||||
let f1 = FluidId::new("R134a");
|
||||
let f2 = FluidId::new("R134a");
|
||||
let f3 = FluidId::new("Water");
|
||||
|
||||
assert_eq!(f1, f2);
|
||||
assert_ne!(f1, f3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_already_connected_error() {
|
||||
let err = ConnectionError::AlreadyConnected;
|
||||
let msg = format!("{}", err);
|
||||
assert!(msg.contains("already connected"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cycle_detected_error() {
|
||||
let err = ConnectionError::CycleDetected;
|
||||
let msg = format!("{}", err);
|
||||
assert!(msg.contains("cycle"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_port_continuity_ok() {
|
||||
let p1 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100_000.0),
|
||||
Enthalpy::from_joules_per_kg(400_000.0),
|
||||
);
|
||||
let p2 = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100_000.0),
|
||||
Enthalpy::from_joules_per_kg(400_000.0),
|
||||
);
|
||||
let (c1, c2) = p1.connect(p2).unwrap();
|
||||
assert!(validate_port_continuity(&c1, &c2).is_ok());
|
||||
assert!(validate_port_continuity(&c2, &c1).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_validate_port_continuity_incompatible_fluid() {
|
||||
let (r134a, _) = Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100_000.0),
|
||||
Enthalpy::from_joules_per_kg(400_000.0),
|
||||
)
|
||||
.connect(Port::new(
|
||||
FluidId::new("R134a"),
|
||||
Pressure::from_pascals(100_000.0),
|
||||
Enthalpy::from_joules_per_kg(400_000.0),
|
||||
))
|
||||
.unwrap();
|
||||
let (water, _) = Port::new(
|
||||
FluidId::new("Water"),
|
||||
Pressure::from_pascals(100_000.0),
|
||||
Enthalpy::from_joules_per_kg(400_000.0),
|
||||
)
|
||||
.connect(Port::new(
|
||||
FluidId::new("Water"),
|
||||
Pressure::from_pascals(100_000.0),
|
||||
Enthalpy::from_joules_per_kg(400_000.0),
|
||||
))
|
||||
.unwrap();
|
||||
assert!(matches!(
|
||||
validate_port_continuity(&r134a, &water),
|
||||
Err(ConnectionError::IncompatibleFluid { .. })
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user