Add diagram workbench UI with Modelica DoF coaching and ISO glyphs.
Ship the Next.js cycle editor with CAD chrome, technical HX symbols, Fixed/Free boundary guidance, and secondary water/air pressure drop support in the solver stack. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,16 +1,14 @@
|
||||
//! Integration tests for structured simulation result extraction.
|
||||
|
||||
use entropyk::{
|
||||
extract_simulation_result, SimulationOutcome, SimulationResult, SystemBuilder,
|
||||
};
|
||||
use entropyk::{extract_simulation_result, SimulationOutcome, SimulationResult, SystemBuilder};
|
||||
use entropyk_components::expansion_valve::ExpansionValve;
|
||||
use entropyk_components::heat_exchanger::{Condenser, Evaporator};
|
||||
use entropyk_components::heat_exchanger::Evaporator;
|
||||
use entropyk_components::port::{Disconnected, FluidId, Port};
|
||||
use entropyk_components::{
|
||||
Component, ComponentError, ConnectedPort, JacobianBuilder, MchxCondenserCoil, Polynomial2D,
|
||||
ResidualVector, ScrewEconomizerCompressor, ScrewPerformanceCurves, StateSlice,
|
||||
ResidualVector, ScrewEconomizerCompressor, ScrewPerformanceCurves,
|
||||
};
|
||||
use entropyk_core::{Enthalpy, MassFlow, Power, Pressure};
|
||||
use entropyk_core::{Enthalpy, Power, Pressure};
|
||||
use entropyk_solver::{ConvergedState, ConvergenceStatus, SimulationMetadata};
|
||||
|
||||
use approx::assert_relative_eq;
|
||||
@@ -53,8 +51,9 @@ fn build_real_r134a_cycle() -> (entropyk_solver::System, ConvergedState) {
|
||||
let suc = make_connected_port("R134a", 2.93, 405.0);
|
||||
let dis = make_connected_port("R134a", 10.17, 440.0);
|
||||
let eco = make_connected_port("R134a", 5.5, 250.0);
|
||||
let comp = ScrewEconomizerCompressor::new(make_screw_curves(), "R134a", 50.0, 0.92, suc, dis, eco)
|
||||
.expect("compressor");
|
||||
let comp =
|
||||
ScrewEconomizerCompressor::new(make_screw_curves(), "R134a", 50.0, 0.92, suc, dis, eco)
|
||||
.expect("compressor");
|
||||
|
||||
// --- Condenser (air-cooled coil at 35°C ambient) ---
|
||||
let condenser = MchxCondenserCoil::for_35c_ambient(15_000.0, 0);
|
||||
@@ -62,19 +61,31 @@ fn build_real_r134a_cycle() -> (entropyk_solver::System, ConvergedState) {
|
||||
// --- Expansion valve (fully open) ---
|
||||
let exv_in = make_disconnected_port("R134a", 10.17, 253.4);
|
||||
let exv_out = make_disconnected_port("R134a", 2.93, 253.4);
|
||||
let exv_disconnected = ExpansionValve::new(exv_in, exv_out, Some(1.0)).expect("exv disconnected");
|
||||
let exv_disconnected =
|
||||
ExpansionValve::new(exv_in, exv_out, Some(1.0)).expect("exv disconnected");
|
||||
let exv = exv_disconnected
|
||||
.connect(make_disconnected_port("R134a", 10.17, 253.4), make_disconnected_port("R134a", 2.93, 253.4))
|
||||
.connect(
|
||||
make_disconnected_port("R134a", 10.17, 253.4),
|
||||
make_disconnected_port("R134a", 2.93, 253.4),
|
||||
)
|
||||
.expect("exv connect");
|
||||
|
||||
// --- Evaporator (BPHE, T_sat=278.15K, SH=5K) ---
|
||||
let evaporator = Evaporator::with_superheat(8000.0, 278.15, 5.0);
|
||||
|
||||
// Add to circuit 0
|
||||
let n_comp = sys.add_component_to_circuit(Box::new(comp), CircuitId::ZERO).unwrap();
|
||||
let n_cond = sys.add_component_to_circuit(Box::new(condenser), CircuitId::ZERO).unwrap();
|
||||
let n_exv = sys.add_component_to_circuit(Box::new(exv), CircuitId::ZERO).unwrap();
|
||||
let n_evap = sys.add_component_to_circuit(Box::new(evaporator), CircuitId::ZERO).unwrap();
|
||||
let n_comp = sys
|
||||
.add_component_to_circuit(Box::new(comp), CircuitId::ZERO)
|
||||
.unwrap();
|
||||
let n_cond = sys
|
||||
.add_component_to_circuit(Box::new(condenser), CircuitId::ZERO)
|
||||
.unwrap();
|
||||
let n_exv = sys
|
||||
.add_component_to_circuit(Box::new(exv), CircuitId::ZERO)
|
||||
.unwrap();
|
||||
let n_evap = sys
|
||||
.add_component_to_circuit(Box::new(evaporator), CircuitId::ZERO)
|
||||
.unwrap();
|
||||
|
||||
// Register names for extract_simulation_result
|
||||
sys.register_component_name("compressor", n_comp);
|
||||
@@ -88,6 +99,12 @@ fn build_real_r134a_cycle() -> (entropyk_solver::System, ConvergedState) {
|
||||
sys.add_edge(n_exv, n_evap).unwrap();
|
||||
sys.add_edge(n_evap, n_comp).unwrap();
|
||||
|
||||
// DoF gate escape hatch: the real components contribute 6+2+2+2 = 12 equations
|
||||
// vs 10 unknowns (1 branch ṁ + 4×(P,h) + compressor internal W_shaft) because no
|
||||
// free actuators (compressor speed, EXV opening) are registered here. This test
|
||||
// exercises result extraction/JSON serialization, not DoF balancing, so the
|
||||
// over-constrained system is accepted deliberately (test-only escape hatch).
|
||||
sys.set_enforce_dof_gate(false);
|
||||
sys.finalize().expect("system finalize");
|
||||
|
||||
// ConvergedState from NIST R134a reference data:
|
||||
@@ -95,11 +112,14 @@ fn build_real_r134a_cycle() -> (entropyk_solver::System, ConvergedState) {
|
||||
// T_cond_sat = 40°C → P_sat ≈ 1017000 Pa (10.17 bar)
|
||||
// h_g(0°C) ≈ 398600 J/kg, h_f(40°C) ≈ 256400 J/kg
|
||||
// With SH=5K and SC=3K
|
||||
// CM1.4: 1 series branch + 4 × (P, h) = 9 elements.
|
||||
// [ṁ_branch, P_e0, h_e0, P_e1, h_e1, P_e2, h_e2, P_e3, h_e3]
|
||||
let state = vec![
|
||||
0.05, // ṁ branch (shared, ~0.05 kg/s)
|
||||
1017000.0, 440000.0, // edge 0: comp→cond (discharge, superheated ~440 kJ/kg)
|
||||
1000000.0, 250000.0, // edge 1: cond→exv (subcooled liquid ~250 kJ/kg, ~3K SC)
|
||||
292800.0, 250000.0, // edge 2: exv→evap (isenthalpic expansion, same h)
|
||||
285000.0, 405000.0, // edge 3: evap→comp (superheated ~5K above sat)
|
||||
292800.0, 250000.0, // edge 2: exv→evap (isenthalpic expansion, same h)
|
||||
285000.0, 405000.0, // edge 3: evap→comp (superheated ~5K above sat)
|
||||
];
|
||||
|
||||
let converged = ConvergedState::new(
|
||||
@@ -217,6 +237,7 @@ impl Component for MockPipe {
|
||||
// ─────────────────────────────────────────────────────────────────────────────
|
||||
|
||||
/// Helper: build a realistic 4-component vapor compression cycle with mock components.
|
||||
#[allow(dead_code)] // Reusable cycle fixture for result-serialization tests.
|
||||
fn build_realistic_cycle() -> (entropyk_solver::System, ConvergedState) {
|
||||
let system = SystemBuilder::new()
|
||||
.component("compressor", Box::new(MockCompressor))
|
||||
@@ -238,13 +259,16 @@ fn build_realistic_cycle() -> (entropyk_solver::System, ConvergedState) {
|
||||
.build()
|
||||
.expect("build system");
|
||||
|
||||
// R410A-like state vector: 4 edges × 2 (P, h)
|
||||
// Realistic values: high side ~24 bar, low side ~8 bar
|
||||
// CM1.4 layout: 1 series branch + 4 × (P, h) = 9 elements
|
||||
// [ṁ_branch, P_e0, h_e0, P_e1, h_e1, P_e2, h_e2, P_e3, h_e3]
|
||||
// Realistic R410A values: high side ~24 bar, low side ~8 bar
|
||||
let state = vec![
|
||||
2400000.0, 440000.0, // edge 0: compressor → condenser (discharge, high P, superheated)
|
||||
0.05, // ṁ branch (shared)
|
||||
2400000.0,
|
||||
440000.0, // edge 0: compressor → condenser (discharge, high P, superheated)
|
||||
2350000.0, 280000.0, // edge 1: condenser → expansion (subcooled liquid)
|
||||
800000.0, 260000.0, // edge 2: expansion → evaporator (two-phase, low P)
|
||||
780000.0, 400000.0, // edge 3: evaporator → compressor (superheated vapor, low P)
|
||||
800000.0, 260000.0, // edge 2: expansion → evaporator (two-phase, low P)
|
||||
780000.0, 400000.0, // edge 3: evaporator → compressor (superheated vapor, low P)
|
||||
];
|
||||
|
||||
let converged = ConvergedState::new(
|
||||
@@ -280,9 +304,10 @@ fn build_test_system() -> (entropyk_solver::System, ConvergedState) {
|
||||
.build()
|
||||
.expect("build system");
|
||||
|
||||
// Create a fake converged state with 4 edges = 8 state variables
|
||||
// [P0, h0, P1, h1, P2, h2, P3, h3]
|
||||
// CM1.4 layout: 1 series branch + 4 × (P, h) = 9 elements
|
||||
// [ṁ_branch, P_e0, h_e0, P_e1, h_e1, P_e2, h_e2, P_e3, h_e3]
|
||||
let state = vec![
|
||||
0.05, // ṁ branch (shared)
|
||||
500000.0, 450000.0, // edge 0: comp -> pipe1 (high pressure)
|
||||
490000.0, 440000.0, // edge 1: pipe1 -> evap
|
||||
200000.0, 250000.0, // edge 2: evap -> pipe2 (low pressure)
|
||||
@@ -360,14 +385,22 @@ fn test_extract_per_edge_results() {
|
||||
let result = extract_simulation_result(&system, &converged);
|
||||
|
||||
// Edge 0: comp -> pipe1 (high pressure side)
|
||||
let edge0 = result.edges.iter().find(|e| e.edge_id == 0).expect("edge 0");
|
||||
let edge0 = result
|
||||
.edges
|
||||
.iter()
|
||||
.find(|e| e.edge_id == 0)
|
||||
.expect("edge 0");
|
||||
assert_relative_eq!(edge0.pressure_pa, 500000.0);
|
||||
assert_relative_eq!(edge0.enthalpy_j_kg, 450000.0);
|
||||
assert_eq!(edge0.source.as_deref(), Some("comp"));
|
||||
assert_eq!(edge0.target.as_deref(), Some("pipe1"));
|
||||
|
||||
// Edge 2: evap -> pipe2 (low pressure side)
|
||||
let edge2 = result.edges.iter().find(|e| e.edge_id == 2).expect("edge 2");
|
||||
let edge2 = result
|
||||
.edges
|
||||
.iter()
|
||||
.find(|e| e.edge_id == 2)
|
||||
.expect("edge 2");
|
||||
assert_relative_eq!(edge2.pressure_pa, 200000.0);
|
||||
assert_relative_eq!(edge2.enthalpy_j_kg, 250000.0);
|
||||
assert_eq!(edge2.source.as_deref(), Some("evap"));
|
||||
@@ -381,15 +414,9 @@ fn test_system_summary() {
|
||||
|
||||
// Evaporator absorbs 10000W (cooling), compressor uses 3000W
|
||||
assert!(result.summary.total_cooling_capacity_w.is_some());
|
||||
assert_relative_eq!(
|
||||
result.summary.total_cooling_capacity_w.unwrap(),
|
||||
10000.0
|
||||
);
|
||||
assert_relative_eq!(result.summary.total_cooling_capacity_w.unwrap(), 10000.0);
|
||||
assert!(result.summary.total_compressor_power_w.is_some());
|
||||
assert_relative_eq!(
|
||||
result.summary.total_compressor_power_w.unwrap(),
|
||||
3000.0
|
||||
);
|
||||
assert_relative_eq!(result.summary.total_compressor_power_w.unwrap(), 3000.0);
|
||||
|
||||
// COP_cooling = 10000 / 3000
|
||||
assert!(result.summary.cop_cooling.is_some());
|
||||
@@ -415,13 +442,19 @@ fn test_simulation_result_json_roundtrip() {
|
||||
let deserialized: SimulationResult =
|
||||
serde_json::from_str(&json).expect("deserialize should succeed");
|
||||
assert_eq!(result.status, deserialized.status);
|
||||
assert_eq!(result.convergence.iterations, deserialized.convergence.iterations);
|
||||
assert_eq!(
|
||||
result.convergence.iterations,
|
||||
deserialized.convergence.iterations
|
||||
);
|
||||
assert_relative_eq!(
|
||||
result.convergence.final_residual,
|
||||
deserialized.convergence.final_residual,
|
||||
epsilon = 1e-15
|
||||
);
|
||||
assert_eq!(result.convergence.converged, deserialized.convergence.converged);
|
||||
assert_eq!(
|
||||
result.convergence.converged,
|
||||
deserialized.convergence.converged
|
||||
);
|
||||
assert_eq!(result.convergence.status, deserialized.convergence.status);
|
||||
assert_eq!(result.components.len(), deserialized.components.len());
|
||||
assert_eq!(result.edges.len(), deserialized.edges.len());
|
||||
@@ -485,20 +518,52 @@ fn test_realistic_cycle_json_output() {
|
||||
assert!(json.contains("\"expansion_valve\""));
|
||||
|
||||
// Compressor should have real component type (not Mock)
|
||||
let comp = result.components.iter().find(|c| c.name == "compressor").expect("comp");
|
||||
assert!(comp.component_type.contains("Screw"), "expected ScrewEconomizer, got {}", comp.component_type);
|
||||
let comp = result
|
||||
.components
|
||||
.iter()
|
||||
.find(|c| c.name == "compressor")
|
||||
.expect("comp");
|
||||
assert!(
|
||||
comp.component_type.contains("Screw"),
|
||||
"expected ScrewEconomizer, got {}",
|
||||
comp.component_type
|
||||
);
|
||||
|
||||
// Condenser should be MchxCondenserCoil
|
||||
let cond = result.components.iter().find(|c| c.name == "condenser").expect("cond");
|
||||
assert!(cond.component_type.contains("Mchx"), "expected MchxCondenserCoil, got {}", cond.component_type);
|
||||
let cond = result
|
||||
.components
|
||||
.iter()
|
||||
.find(|c| c.name == "condenser")
|
||||
.expect("cond");
|
||||
assert!(
|
||||
cond.component_type.contains("Mchx"),
|
||||
"expected MchxCondenserCoil, got {}",
|
||||
cond.component_type
|
||||
);
|
||||
|
||||
// Expansion valve should have real type
|
||||
let exv = result.components.iter().find(|c| c.name == "expansion_valve").expect("exv");
|
||||
assert!(exv.component_type.contains("ExpansionValve"), "expected ExpansionValve, got {}", exv.component_type);
|
||||
let exv = result
|
||||
.components
|
||||
.iter()
|
||||
.find(|c| c.name == "expansion_valve")
|
||||
.expect("exv");
|
||||
assert!(
|
||||
exv.component_type.contains("ExpansionValve"),
|
||||
"expected ExpansionValve, got {}",
|
||||
exv.component_type
|
||||
);
|
||||
|
||||
// Evaporator
|
||||
let evap = result.components.iter().find(|c| c.name == "evaporator").expect("evap");
|
||||
assert!(evap.component_type.contains("Evaporator"), "expected Evaporator, got {}", evap.component_type);
|
||||
let evap = result
|
||||
.components
|
||||
.iter()
|
||||
.find(|c| c.name == "evaporator")
|
||||
.expect("evap");
|
||||
assert!(
|
||||
evap.component_type.contains("Evaporator"),
|
||||
"expected Evaporator, got {}",
|
||||
evap.component_type
|
||||
);
|
||||
|
||||
// Check edge pressures are from NIST data
|
||||
let edge0 = result.edges.iter().find(|e| e.edge_id == 0).unwrap();
|
||||
|
||||
Reference in New Issue
Block a user