feat(components): add ThermoState generators and Eurovent backend demo
This commit is contained in:
239
crates/solver/tests/multi_circuit.rs
Normal file
239
crates/solver/tests/multi_circuit.rs
Normal file
@@ -0,0 +1,239 @@
|
||||
//! Integration tests for multi-circuit machine definition (Story 3.3, FR9).
|
||||
//!
|
||||
//! Verifies multi-circuit heat pump topology (refrigerant + water) without thermal coupling.
|
||||
//! Tests circuits from 2 up to the maximum of 5 circuits (circuit IDs 0-4).
|
||||
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, ResidualVector, SystemState,
|
||||
};
|
||||
use entropyk_solver::{CircuitId, System, ThermalCoupling, TopologyError};
|
||||
use entropyk_core::ThermalConductance;
|
||||
|
||||
/// Mock refrigerant component (e.g. compressor, condenser refrigerant side).
|
||||
struct RefrigerantMock {
|
||||
n_equations: usize,
|
||||
}
|
||||
|
||||
impl Component for RefrigerantMock {
|
||||
fn compute_residuals(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
residuals: &mut ResidualVector,
|
||||
) -> Result<(), ComponentError> {
|
||||
for r in residuals.iter_mut().take(self.n_equations) {
|
||||
*r = 0.0;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn jacobian_entries(
|
||||
&self,
|
||||
_state: &SystemState,
|
||||
_jacobian: &mut JacobianBuilder,
|
||||
) -> Result<(), ComponentError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn n_equations(&self) -> usize {
|
||||
self.n_equations
|
||||
}
|
||||
|
||||
fn get_ports(&self) -> &[ConnectedPort] {
|
||||
&[]
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_two_circuit_heat_pump_topology() {
|
||||
let mut sys = System::new();
|
||||
|
||||
// Circuit 0: refrigerant (compressor -> condenser -> valve -> evaporator)
|
||||
let comp = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 2 }),
|
||||
CircuitId::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let cond = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 2 }),
|
||||
CircuitId::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let valve = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 2 }),
|
||||
CircuitId::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let evap = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 2 }),
|
||||
CircuitId::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
sys.add_edge(comp, cond).unwrap();
|
||||
sys.add_edge(cond, valve).unwrap();
|
||||
sys.add_edge(valve, evap).unwrap();
|
||||
sys.add_edge(evap, comp).unwrap();
|
||||
|
||||
// Circuit 1: water (pump -> condenser water side -> evaporator water side)
|
||||
let pump = sys
|
||||
.add_component_to_circuit(Box::new(RefrigerantMock { n_equations: 1 }), CircuitId(1))
|
||||
.unwrap();
|
||||
let cond_w = sys
|
||||
.add_component_to_circuit(Box::new(RefrigerantMock { n_equations: 1 }), CircuitId(1))
|
||||
.unwrap();
|
||||
let evap_w = sys
|
||||
.add_component_to_circuit(Box::new(RefrigerantMock { n_equations: 1 }), CircuitId(1))
|
||||
.unwrap();
|
||||
|
||||
sys.add_edge(pump, cond_w).unwrap();
|
||||
sys.add_edge(cond_w, evap_w).unwrap();
|
||||
sys.add_edge(evap_w, pump).unwrap();
|
||||
|
||||
assert_eq!(sys.circuit_count(), 2);
|
||||
assert_eq!(sys.circuit_nodes(CircuitId::ZERO).count(), 4);
|
||||
assert_eq!(sys.circuit_nodes(CircuitId(1)).count(), 3);
|
||||
assert_eq!(sys.circuit_edges(CircuitId::ZERO).count(), 4);
|
||||
assert_eq!(sys.circuit_edges(CircuitId(1)).count(), 3);
|
||||
|
||||
let result = sys.finalize();
|
||||
assert!(
|
||||
result.is_ok(),
|
||||
"finalize should succeed: {:?}",
|
||||
result.err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_cross_circuit_rejected_integration() {
|
||||
let mut sys = System::new();
|
||||
let n0 = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 0 }),
|
||||
CircuitId::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let n1 = sys
|
||||
.add_component_to_circuit(Box::new(RefrigerantMock { n_equations: 0 }), CircuitId(1))
|
||||
.unwrap();
|
||||
|
||||
let result = sys.add_edge(n0, n1);
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(TopologyError::CrossCircuitConnection { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_maximum_five_circuits_integration() {
|
||||
// Integration test: Verify maximum of 5 circuits (IDs 0-4) is supported
|
||||
let mut sys = System::new();
|
||||
|
||||
// Create 5 separate circuits, each with 2 nodes forming a cycle
|
||||
for circuit_id in 0..=4 {
|
||||
let n0 = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 1 }),
|
||||
CircuitId(circuit_id),
|
||||
)
|
||||
.unwrap();
|
||||
let n1 = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 1 }),
|
||||
CircuitId(circuit_id),
|
||||
)
|
||||
.unwrap();
|
||||
sys.add_edge(n0, n1).unwrap();
|
||||
sys.add_edge(n1, n0).unwrap();
|
||||
}
|
||||
|
||||
assert_eq!(sys.circuit_count(), 5, "should have exactly 5 circuits");
|
||||
|
||||
// Verify each circuit has its own nodes and edges
|
||||
for circuit_id in 0..=4 {
|
||||
assert_eq!(
|
||||
sys.circuit_nodes(CircuitId(circuit_id)).count(),
|
||||
2,
|
||||
"circuit {} should have 2 nodes",
|
||||
circuit_id
|
||||
);
|
||||
assert_eq!(
|
||||
sys.circuit_edges(CircuitId(circuit_id)).count(),
|
||||
2,
|
||||
"circuit {} should have 2 edges",
|
||||
circuit_id
|
||||
);
|
||||
}
|
||||
|
||||
// Verify 6th circuit is rejected
|
||||
let result =
|
||||
sys.add_component_to_circuit(Box::new(RefrigerantMock { n_equations: 1 }), CircuitId(5));
|
||||
assert!(
|
||||
result.is_err(),
|
||||
"circuit 5 should be rejected (exceeds max of 4)"
|
||||
);
|
||||
assert!(matches!(
|
||||
result,
|
||||
Err(TopologyError::TooManyCircuits { requested: 5 })
|
||||
));
|
||||
|
||||
// Verify system can still be finalized with 5 circuits
|
||||
sys.finalize().unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_coupling_residuals_basic() {
|
||||
// Two circuits with one thermal coupling; verify coupling_residual_count and coupling_residuals.
|
||||
let mut sys = System::new();
|
||||
let n0 = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 1 }),
|
||||
CircuitId::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
let n1 = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 1 }),
|
||||
CircuitId::ZERO,
|
||||
)
|
||||
.unwrap();
|
||||
sys.add_edge(n0, n1).unwrap();
|
||||
sys.add_edge(n1, n0).unwrap();
|
||||
|
||||
let n2 = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 1 }),
|
||||
CircuitId(1),
|
||||
)
|
||||
.unwrap();
|
||||
let n3 = sys
|
||||
.add_component_to_circuit(
|
||||
Box::new(RefrigerantMock { n_equations: 1 }),
|
||||
CircuitId(1),
|
||||
)
|
||||
.unwrap();
|
||||
sys.add_edge(n2, n3).unwrap();
|
||||
sys.add_edge(n3, n2).unwrap();
|
||||
|
||||
let coupling = ThermalCoupling::new(
|
||||
CircuitId::ZERO,
|
||||
CircuitId(1),
|
||||
ThermalConductance::from_watts_per_kelvin(1000.0),
|
||||
);
|
||||
sys.add_thermal_coupling(coupling).unwrap();
|
||||
|
||||
sys.finalize().unwrap();
|
||||
|
||||
assert_eq!(sys.coupling_residual_count(), 1);
|
||||
|
||||
let temperatures = [(350.0_f64, 300.0_f64)]; // T_hot, T_cold in K
|
||||
let mut out = [0.0_f64; 4];
|
||||
sys.coupling_residuals(&temperatures, &mut out);
|
||||
// Q = UA * (T_hot - T_cold) = 1000 * 50 = 50000 W into cold circuit
|
||||
assert!(out[0] > 0.0);
|
||||
assert!((out[0] - 50000.0).abs() < 1.0);
|
||||
}
|
||||
Reference in New Issue
Block a user