370 lines
12 KiB
Rust
370 lines
12 KiB
Rust
//! Epic 1 Integration Tests - Extensible Component Framework
|
|
//!
|
|
//! Tests for User Stories:
|
|
//! - 1-1: Component Trait Definition
|
|
//! - 1-2: Physical Types (NewType Pattern)
|
|
//! - 1-3: Port and Connection System
|
|
//! - 1-4: Compressor Component (AHRI 540)
|
|
//! - 1-5: Generic Heat Exchanger Framework
|
|
//! - 1-6: Expansion Valve Component
|
|
//! - 1-7: Component State Machine (ON/OFF/BYPASS)
|
|
//! - 1-8: Auxiliary and Transport Components
|
|
//! - 1-11: Flow Junctions (Splitter/Merger)
|
|
//! - 1-12: Boundary Conditions (Source/Sink)
|
|
|
|
use approx::assert_relative_eq;
|
|
use entropyk_components::{
|
|
Ahri540Coefficients, Compressor, CompressorModel, ConnectedPort, EpsNtuModel, ExchangerType,
|
|
ExpansionValve, FluidId, OperationalState, Port, Pump, SstSdtCoefficients,
|
|
StateManageable,
|
|
};
|
|
use entropyk_components::heat_exchanger::HeatTransferModel;
|
|
use entropyk_core::{Enthalpy, MassFlow, Pressure, Temperature};
|
|
|
|
// =============================================================================
|
|
// Story 1-2: Physical Types (NewType Pattern)
|
|
// =============================================================================
|
|
|
|
mod story_1_2_types {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_pressure_conversions() {
|
|
let p_bar = Pressure::from_bar(1.0);
|
|
assert_relative_eq!(p_bar.to_pascals(), 100_000.0, epsilon = 1e-6);
|
|
assert_relative_eq!(p_bar.to_bar(), 1.0, epsilon = 1e-10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_temperature_conversions() {
|
|
let t_c = Temperature::from_celsius(0.0);
|
|
assert_relative_eq!(t_c.to_kelvin(), 273.15, epsilon = 1e-10);
|
|
assert_relative_eq!(t_c.to_celsius(), 0.0, epsilon = 1e-10);
|
|
}
|
|
|
|
#[test]
|
|
fn test_enthalpy_conversions() {
|
|
let h_kj = Enthalpy::from_kilojoules_per_kg(100.0);
|
|
assert_relative_eq!(h_kj.to_joules_per_kg(), 100_000.0, epsilon = 1e-6);
|
|
}
|
|
|
|
#[test]
|
|
fn test_mass_flow_regularization() {
|
|
let zero = MassFlow::from_kg_per_s(0.0);
|
|
let regularized = zero.regularized();
|
|
assert!(regularized.to_kg_per_s() > 0.0);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Story 1-3: Port and Connection System
|
|
// =============================================================================
|
|
|
|
mod story_1_3_ports {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_port_creation() {
|
|
let port = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(10.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0),
|
|
);
|
|
assert_eq!(port.fluid_id().as_str(), "R134a");
|
|
}
|
|
|
|
#[test]
|
|
fn test_port_connection_success() {
|
|
let p1 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(10.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0),
|
|
);
|
|
let p2 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(10.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0),
|
|
);
|
|
|
|
let result = p1.connect(p2);
|
|
assert!(result.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_port_connection_fluid_mismatch() {
|
|
let p1 = Port::new(
|
|
FluidId::new("R134a"),
|
|
Pressure::from_bar(10.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0),
|
|
);
|
|
let p2 = Port::new(
|
|
FluidId::new("R410A"),
|
|
Pressure::from_bar(10.0),
|
|
Enthalpy::from_joules_per_kg(400_000.0),
|
|
);
|
|
|
|
let result = p1.connect(p2);
|
|
assert!(result.is_err());
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Story 1-4: Compressor Component (AHRI 540)
|
|
// =============================================================================
|
|
|
|
mod story_1_4_compressor {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_ahri540_coefficients_creation() {
|
|
let coeffs = Ahri540Coefficients::new(
|
|
0.85, 2.5, // M1, M2 (flow)
|
|
500.0, 1500.0, -2.5, 1.8, // M3-M6 (cooling)
|
|
600.0, 1600.0, -3.0, 2.0, // M7-M10 (heating)
|
|
);
|
|
|
|
assert!(coeffs.validate().is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_ahri540_invalid_m2() {
|
|
let coeffs = Ahri540Coefficients::new(
|
|
0.85, -1.0, // M2 must be positive
|
|
500.0, 1500.0, -2.5, 1.8, 600.0, 1600.0, -3.0, 2.0,
|
|
);
|
|
|
|
assert!(coeffs.validate().is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn test_sst_sdt_coefficients() {
|
|
// Use bilinear constructor instead of removed ::default()
|
|
let coeffs = SstSdtCoefficients::bilinear(
|
|
0.05, 0.001, 0.0005, 0.00001, // mass flow coefficients
|
|
1000.0, 50.0, 30.0, 0.5, // power coefficients
|
|
);
|
|
// Verify evaluation works (bilinear model)
|
|
let mass_flow = coeffs.mass_flow_at(263.15, 313.15); // -10°C SST, 40°C SDT
|
|
assert!(mass_flow > 0.0);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Story 1-5: Generic Heat Exchanger Framework
|
|
// =============================================================================
|
|
|
|
mod story_1_5_heat_exchanger {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_eps_ntu_counter_flow() {
|
|
let model = EpsNtuModel::counter_flow(5000.0);
|
|
// ua() is accessed through the HeatTransferModel trait
|
|
assert_eq!(model.ua(), 5000.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_eps_ntu_effectiveness_counter_flow() {
|
|
let model = EpsNtuModel::counter_flow(5000.0);
|
|
|
|
// Test effectiveness calculation
|
|
let ntu = 1.0;
|
|
let c_r = 0.5;
|
|
let eps = model.effectiveness(ntu, c_r);
|
|
|
|
// For counter-flow with NTU=1, C_r=0.5: ε ≈ 0.63
|
|
assert!(eps > 0.0 && eps < 1.0);
|
|
}
|
|
|
|
#[test]
|
|
fn test_eps_ntu_phase_change() {
|
|
let model = EpsNtuModel::counter_flow(5000.0);
|
|
|
|
// Phase change: C_r → 0
|
|
let ntu = 2.0;
|
|
let c_r = 1e-12; // Effectively zero
|
|
let eps = model.effectiveness(ntu, c_r);
|
|
|
|
// For phase change: ε = 1 - exp(-NTU)
|
|
let expected = 1.0 - (-ntu).exp();
|
|
assert_relative_eq!(eps, expected, epsilon = 1e-6);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Story 1-6: Expansion Valve Component
|
|
// =============================================================================
|
|
|
|
mod story_1_6_expansion_valve {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_valve_creation() {
|
|
let inlet = Port::new(
|
|
FluidId::new("R410A"),
|
|
Pressure::from_bar(25.0),
|
|
Enthalpy::from_joules_per_kg(250_000.0),
|
|
);
|
|
let outlet = Port::new(
|
|
FluidId::new("R410A"),
|
|
Pressure::from_bar(25.0),
|
|
Enthalpy::from_joules_per_kg(250_000.0),
|
|
);
|
|
|
|
let valve = ExpansionValve::new(inlet, outlet, Some(1.0));
|
|
assert!(valve.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_valve_invalid_opening() {
|
|
let inlet = Port::new(
|
|
FluidId::new("R410A"),
|
|
Pressure::from_bar(25.0),
|
|
Enthalpy::from_joules_per_kg(250_000.0),
|
|
);
|
|
let outlet = Port::new(
|
|
FluidId::new("R410A"),
|
|
Pressure::from_bar(25.0),
|
|
Enthalpy::from_joules_per_kg(250_000.0),
|
|
);
|
|
|
|
// Opening > 1.0 should fail
|
|
let valve = ExpansionValve::new(inlet, outlet, Some(1.5));
|
|
assert!(valve.is_err());
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Story 1-7: Component State Machine
|
|
// =============================================================================
|
|
|
|
mod story_1_7_state_machine {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_operational_state_transitions() {
|
|
let on = OperationalState::On;
|
|
let off = OperationalState::Off;
|
|
let bypass = OperationalState::Bypass;
|
|
|
|
// On can transition to any state
|
|
assert!(on.can_transition_to(OperationalState::Off));
|
|
assert!(on.can_transition_to(OperationalState::Bypass));
|
|
|
|
// Off can transition to On
|
|
assert!(off.can_transition_to(OperationalState::On));
|
|
}
|
|
|
|
#[test]
|
|
fn test_circuit_id_creation() {
|
|
let circuit = entropyk_components::CircuitId::from_number(5);
|
|
assert_eq!(circuit.as_number(), 5);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Story 1-8: Auxiliary Components (Pipe, Pump)
|
|
// =============================================================================
|
|
|
|
mod story_1_8_auxiliary {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn test_pipe_creation() {
|
|
use entropyk_components::PipeGeometry;
|
|
|
|
let geometry = PipeGeometry {
|
|
length_m: 10.0,
|
|
diameter_m: 0.022,
|
|
roughness_m: 1.5e-6,
|
|
};
|
|
|
|
assert_relative_eq!(geometry.length_m, 10.0);
|
|
assert_relative_eq!(geometry.diameter_m, 0.022);
|
|
}
|
|
|
|
#[test]
|
|
fn test_pump_curves() {
|
|
use entropyk_components::PumpCurves;
|
|
|
|
// Use PumpCurves::quadratic constructor (fields are no longer public)
|
|
let curves = PumpCurves::quadratic(
|
|
30.0, -10.0, -50.0, // head: H = 30 - 10Q - 50Q²
|
|
0.5, 0.3, -0.5, // efficiency: η = 0.5 + 0.3Q - 0.5Q²
|
|
)
|
|
.unwrap();
|
|
|
|
// At Q=0, H should be H0 = 30
|
|
let h_at_zero = curves.head_at_flow(0.0);
|
|
assert_relative_eq!(h_at_zero, 30.0, epsilon = 1e-6);
|
|
}
|
|
}
|
|
|
|
// =============================================================================
|
|
// Story 1-11: Flow Junctions
|
|
// =============================================================================
|
|
|
|
mod story_1_11_junctions {
|
|
use super::*;
|
|
use entropyk_components::FlowSplitter;
|
|
|
|
fn make_connected_port(fluid: &str, p_pa: f64, h_jkg: f64) -> ConnectedPort {
|
|
let a = Port::new(
|
|
FluidId::new(fluid),
|
|
Pressure::from_pascals(p_pa),
|
|
Enthalpy::from_joules_per_kg(h_jkg),
|
|
);
|
|
let b = Port::new(
|
|
FluidId::new(fluid),
|
|
Pressure::from_pascals(p_pa),
|
|
Enthalpy::from_joules_per_kg(h_jkg),
|
|
);
|
|
a.connect(b).unwrap().0
|
|
}
|
|
|
|
#[test]
|
|
fn test_flow_splitter_creation() {
|
|
// FlowSplitter::new() is removed; use ::incompressible()
|
|
let inlet = make_connected_port("Water", 100_000.0, 42_000.0);
|
|
let outlet1 = make_connected_port("Water", 100_000.0, 42_000.0);
|
|
let outlet2 = make_connected_port("Water", 100_000.0, 42_000.0);
|
|
|
|
let splitter =
|
|
FlowSplitter::incompressible("Water", inlet, vec![outlet1, outlet2]);
|
|
assert!(splitter.is_ok());
|
|
}
|
|
|
|
#[test]
|
|
fn test_flow_source_creation() {
|
|
// FlowSource::new() is removed; use ::incompressible() (deprecated but still functional)
|
|
#[allow(deprecated)]
|
|
{
|
|
use entropyk_components::FlowSource;
|
|
let port = make_connected_port("Water", 100_000.0, 42_000.0);
|
|
let source = FlowSource::incompressible(
|
|
"Water",
|
|
100_000.0,
|
|
42_000.0,
|
|
port,
|
|
);
|
|
assert!(source.is_ok());
|
|
let s = source.unwrap();
|
|
assert_eq!(s.fluid_id(), "Water");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn test_flow_sink_creation() {
|
|
// FlowSink::new() is removed; use ::incompressible() (deprecated but still functional)
|
|
#[allow(deprecated)]
|
|
{
|
|
use entropyk_components::FlowSink;
|
|
let port = make_connected_port("Water", 100_000.0, 42_000.0);
|
|
let sink = FlowSink::incompressible("Water", 100_000.0, None, port);
|
|
assert!(sink.is_ok());
|
|
let s = sink.unwrap();
|
|
assert_eq!(s.fluid_id(), "Water");
|
|
}
|
|
}
|
|
}
|