754 lines
22 KiB
Rust
754 lines
22 KiB
Rust
//! 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 { .. })
|
|
));
|
|
}
|
|
}
|