Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled

Capture uncommitted solver robustness work (regularization, domain errors, linear solver lifecycle, tube DP/MSH), web workbench updates, and synced BMAD skills across IDE agent folders before starting BPHX pressure-drop.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 16:35:31 +02:00
parent 88620790d6
commit 5bd180b5b8
1363 changed files with 101041 additions and 58547 deletions

View File

@@ -1,4 +1,4 @@
//! Integration tests for MacroComponent (Story 3.6).
//! Integration tests for MacroComponent (Story 3.6).
//!
//! Tests cover:
//! - AC #1: MacroComponent implements Component trait
@@ -73,13 +73,15 @@ fn make_port(fluid: &str, p: f64, h: f64) -> ConnectedPort {
}
/// Build a 4-component refrigerant cycle: A→B→C→D→A (4 edges).
/// Each component contributes 3 equations (2 thermo + 1 mass-flow) per CM1.3.
/// CM1.4: 4-edge series cycle → 1 branch + 8 P,h = 9 internal unknowns.
/// One 3-eq component (mass-flow reference) + three 2-eq components keeps the
/// macro square internally: 3 + 3×2 = 9 equations.
fn build_4_component_cycle() -> System {
let mut sys = System::new();
let a = sys.add_component(pass(3)); // compressor
let b = sys.add_component(pass(3)); // condenser
let c = sys.add_component(pass(3)); // valve
let d = sys.add_component(pass(3)); // evaporator
let a = sys.add_component(pass(3)); // compressor (mass-flow reference)
let b = sys.add_component(pass(2)); // condenser
let c = sys.add_component(pass(2)); // valve
let d = sys.add_component(pass(2)); // evaporator
sys.add_edge(a, b).unwrap();
sys.add_edge(b, c).unwrap();
sys.add_edge(c, d).unwrap();
@@ -97,11 +99,11 @@ fn test_4_component_cycle_macro_creation() {
let internal = build_4_component_cycle();
let mc = MacroComponent::new(internal);
// 4 components × 3 equations = 12 internal equations (pass(3)×4), 0 exposed ports
// 1 component × 3 eqs + 3 components × 2 eqs = 9 internal equations, 0 exposed ports
assert_eq!(
mc.n_equations(),
12,
"should have 12 internal equations (4 components × 3 eqs) with no exposed ports"
9,
"should have 9 internal equations (1×3 + 3×2) with no exposed ports"
);
// CM1.4: 4-edge series cycle → 1 branch + 4×2 P,h = 9 internal state vars
assert_eq!(mc.internal_state_len(), 9);
@@ -117,11 +119,11 @@ fn test_4_component_cycle_expose_two_ports() {
mc.expose_port(0, "refrig_in", make_port("R134a", 1e5, 4e5));
mc.expose_port(2, "refrig_out", make_port("R134a", 5e5, 4.5e5));
// 12 internal (4 components × 3 eqs) + 4 coupling (2 per port × 2 ports) = 16
// 9 internal (1×3 + 3×2) + 4 coupling (2 per port × 2 ports) = 13
assert_eq!(
mc.n_equations(),
16,
"should have 16 equations with 2 exposed ports"
13,
"should have 13 equations with 2 exposed ports"
);
assert_eq!(mc.get_ports().len(), 2);
assert_eq!(mc.port_mappings()[0].name, "refrig_in");
@@ -186,20 +188,20 @@ fn test_coupling_residuals_are_zero_at_consistent_state() {
state[4] = 1.0e5; // P_int_e0 (consistent with port: offset 3 + 1 = 4)
state[5] = 4.0e5; // h_int_e0 (consistent with port: offset 3 + 2 = 5)
let n_eqs = mc.n_equations(); // 12 internal + 2 coupling = 14
let n_eqs = mc.n_equations(); // 9 internal + 2 coupling = 11
let mut residuals = vec![0.0; n_eqs];
mc.compute_residuals(&state, &mut residuals).unwrap();
// Coupling residuals at indices 12, 13 should be zero (consistent state)
// Coupling residuals at indices 9, 10 should be zero (consistent state)
assert!(
residuals[12].abs() < 1e-10,
residuals[9].abs() < 1e-10,
"P coupling residual should be 0, got {}",
residuals[12]
residuals[9]
);
assert!(
residuals[13].abs() < 1e-10,
residuals[10].abs() < 1e-10,
"h coupling residual should be 0, got {}",
residuals[13]
residuals[10]
);
}
@@ -212,7 +214,7 @@ fn test_coupling_residuals_nonzero_at_inconsistent_state() {
mc.set_global_state_offset(3);
mc.set_system_context(3, &[(0, 1, 2)]);
let mut state = vec![0.0; 15];
let mut state = vec![0.0; 12]; // 3 parent + 9 internal
state[1] = 2.0e5; // P_ext (different from internal, p_ext=1)
state[2] = 5.0e5; // h_ext (h_ext=2)
state[4] = 1.0e5; // P_int_e0 (offset 3+1=4)
@@ -222,16 +224,16 @@ fn test_coupling_residuals_nonzero_at_inconsistent_state() {
let mut residuals = vec![0.0; n_eqs];
mc.compute_residuals(&state, &mut residuals).unwrap();
// Coupling: r[12] = P_ext - P_int = 2e5 - 1e5 = 1e5
// Coupling: r[9] = P_ext - P_int = 2e5 - 1e5 = 1e5
assert!(
(residuals[12] - 1.0e5).abs() < 1.0,
(residuals[9] - 1.0e5).abs() < 1.0,
"P coupling residual mismatch: {}",
residuals[12]
residuals[9]
);
assert!(
(residuals[13] - 1.0e5).abs() < 1.0,
(residuals[10] - 1.0e5).abs() < 1.0,
"h coupling residual mismatch: {}",
residuals[13]
residuals[10]
);
}
@@ -245,7 +247,7 @@ fn test_jacobian_coupling_entries_correct() {
mc.set_global_state_offset(3);
mc.set_system_context(3, &[(0, 1, 2)]);
let state = vec![0.0; 15];
let state = vec![0.0; 12]; // 3 parent + 9 internal
let mut jac = JacobianBuilder::new();
mc.jacobian_entries(&state, &mut jac).unwrap();
@@ -257,11 +259,11 @@ fn test_jacobian_coupling_entries_correct() {
.map(|&(_, _, v)| v)
};
// Coupling rows 12 (P) and 13 (h); internal edge0 (P@offset+1=4, h@offset+2=5)
assert_eq!(find(12, 1), Some(1.0), "∂r_P/∂p_ext should be +1");
assert_eq!(find(12, 4), Some(-1.0), "∂r_P/∂int_p should be -1");
assert_eq!(find(13, 2), Some(1.0), "∂r_h/∂h_ext should be +1");
assert_eq!(find(13, 5), Some(-1.0), "∂r_h/∂int_h should be -1");
// Coupling rows 9 (P) and 10 (h); internal edge0 (P@offset+1=4, h@offset+2=5)
assert_eq!(find(9, 1), Some(1.0), "∂r_P/∂p_ext should be +1");
assert_eq!(find(9, 4), Some(-1.0), "∂r_P/∂int_p should be -1");
assert_eq!(find(10, 2), Some(1.0), "∂r_h/∂h_ext should be +1");
assert_eq!(find(10, 5), Some(-1.0), "∂r_h/∂int_h should be -1");
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -302,7 +304,7 @@ fn test_snapshot_fails_on_short_state() {
let mut mc = MacroComponent::new(internal);
mc.set_global_state_offset(0);
// Only 4 values, but internal needs 12
// Only 4 values, but internal needs 9
let short_state = vec![0.0; 4];
let snap = mc.to_snapshot(&short_state, None);
assert!(snap.is_none(), "should return None for short state vector");
@@ -362,18 +364,18 @@ fn test_two_macro_chillers_in_parallel_topology() {
// 4 edges
assert_eq!(parent.edge_count(), 4);
// Total component equations (CM1.3):
// chiller_a: 12 internal (4 components × 3 eqs) + 4 coupling (2 ports × 2) = 16
// chiller_b: 12 internal + 4 coupling = 16
// Total component equations (CM1.3 / CM1.4):
// chiller_a: 9 internal (1×3 + 3×2) + 4 coupling (2 ports × 2) = 13
// chiller_b: 9 internal + 4 coupling = 13
// splitter: 1
// merger: 1
// total: 34
// total: 28
let total_eqs: usize = parent
.traverse_for_jacobian()
.map(|(_, c, _)| c.n_equations())
.sum();
assert_eq!(
total_eqs, 34,
total_eqs, 28,
"total equation count mismatch: {}",
total_eqs
);