Snapshot WIP: solver HP epic progress, BPHX/HX physics, BMAD skill refresh.
Some checks failed
CI / check (push) Has been cancelled
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:
@@ -252,12 +252,14 @@ pub struct Pipe<State> {
|
||||
/// Operational state
|
||||
operational_state: OperationalState,
|
||||
/// Design-point pressure drop (Pa) for the edge-coupled (P,h) solver model.
|
||||
/// The (P,h) refrigeration solver has no mass-flow unknown, so the pressure
|
||||
/// drop is imposed as a design-point value rather than computed from Darcy.
|
||||
/// When `> 0`, ΔP is imposed as this constant. When `0`, ΔP is computed from
|
||||
/// Darcy–Weisbach using geometry + live mass flow (`inlet_m_idx`).
|
||||
design_dp_pa: f64,
|
||||
/// When true, the component uses the edge-coupled (P,h) solver model
|
||||
/// (`n_equations() == 2`) instead of the legacy mass-flow model.
|
||||
edge_coupled: bool,
|
||||
/// Inlet edge mass-flow index (edge-coupled Darcy mode).
|
||||
inlet_m_idx: Option<usize>,
|
||||
/// Inlet edge pressure index in the global state vector (edge-coupled mode).
|
||||
inlet_p_idx: Option<usize>,
|
||||
/// Inlet edge enthalpy index in the global state vector (edge-coupled mode).
|
||||
@@ -316,6 +318,7 @@ impl Pipe<Disconnected> {
|
||||
operational_state: OperationalState::default(),
|
||||
design_dp_pa: 0.0,
|
||||
edge_coupled: false,
|
||||
inlet_m_idx: None,
|
||||
inlet_p_idx: None,
|
||||
inlet_h_idx: None,
|
||||
outlet_p_idx: None,
|
||||
@@ -477,6 +480,7 @@ impl Pipe<Disconnected> {
|
||||
operational_state: self.operational_state,
|
||||
design_dp_pa: self.design_dp_pa,
|
||||
edge_coupled: self.edge_coupled,
|
||||
inlet_m_idx: self.inlet_m_idx,
|
||||
inlet_p_idx: self.inlet_p_idx,
|
||||
inlet_h_idx: self.inlet_h_idx,
|
||||
outlet_p_idx: self.outlet_p_idx,
|
||||
@@ -487,12 +491,13 @@ impl Pipe<Disconnected> {
|
||||
}
|
||||
|
||||
impl Pipe<Connected> {
|
||||
/// Enables the edge-coupled (P,h) solver model with a design-point pressure
|
||||
/// drop, so the pipe participates in the refrigeration/hydronic graph solver.
|
||||
/// Enables the edge-coupled (P,h) solver model so the pipe participates in
|
||||
/// the refrigeration/hydronic graph solver.
|
||||
///
|
||||
/// In this mode the pipe owns 2 equations on its outlet edge:
|
||||
/// - `r0 = P_out - (P_in - design_dp_pa)` (imposed pressure drop)
|
||||
/// - `r1 = h_out - h_in` (adiabatic pass-through)
|
||||
/// - `r0 = P_out - (P_in - ΔP)` — if `design_dp_pa > 0`, ΔP is that constant;
|
||||
/// if `design_dp_pa == 0`, ΔP is Darcy–Weisbach from geometry + live ṁ
|
||||
/// - `r1 = h_out - h_in` — adiabatic pass-through
|
||||
pub fn with_design_pressure_drop_pa(mut self, design_dp_pa: f64) -> Self {
|
||||
self.design_dp_pa = design_dp_pa.max(0.0);
|
||||
self.edge_coupled = true;
|
||||
@@ -624,6 +629,7 @@ impl Component for Pipe<Connected> {
|
||||
// Layout: [0] = incoming edge (upstream→pipe), [1] = outgoing edge (pipe→downstream)
|
||||
// Triple: (m_idx, p_idx, h_idx)
|
||||
if !external_edge_state_indices.is_empty() {
|
||||
self.inlet_m_idx = Some(external_edge_state_indices[0].0);
|
||||
self.inlet_p_idx = Some(external_edge_state_indices[0].1);
|
||||
self.inlet_h_idx = Some(external_edge_state_indices[0].2);
|
||||
}
|
||||
@@ -654,9 +660,15 @@ impl Component for Pipe<Connected> {
|
||||
}
|
||||
let dp = match self.operational_state {
|
||||
OperationalState::Bypass => 0.0,
|
||||
_ => self.calib.z_dp * self.design_dp_pa,
|
||||
_ if self.design_dp_pa > 0.0 => self.calib.z_dp * self.design_dp_pa,
|
||||
_ => {
|
||||
// Darcy–Weisbach from geometry + live ṁ (design_dp unset/0).
|
||||
let m = self.inlet_m_idx.map(|i| state[i]).unwrap_or(0.0);
|
||||
let flow_m3 = m / self.fluid_density_kg_per_m3;
|
||||
self.pressure_drop(flow_m3).abs()
|
||||
}
|
||||
};
|
||||
// r0: imposed pressure drop across the pipe
|
||||
// r0: pressure drop across the pipe
|
||||
residuals[0] = state[out_p] - (state[in_p] - dp);
|
||||
// r1: adiabatic pass-through (enthalpy conserved)
|
||||
residuals[1] = state[out_h] - state[in_h];
|
||||
@@ -736,6 +748,19 @@ impl Component for Pipe<Connected> {
|
||||
// r0 = P_out - (P_in - dp) → ∂r0/∂P_out = 1, ∂r0/∂P_in = -1
|
||||
jacobian.add_entry(0, out_p, 1.0);
|
||||
jacobian.add_entry(0, in_p, -1.0);
|
||||
// Darcy: dp depends on ṁ → ∂r0/∂ṁ = +∂(dp)/∂ṁ
|
||||
if self.design_dp_pa <= 0.0 {
|
||||
if let Some(m_idx) = self.inlet_m_idx {
|
||||
let m = state[m_idx];
|
||||
let h = 1e-6_f64.max(m.abs() * 1e-5);
|
||||
let rho = self.fluid_density_kg_per_m3;
|
||||
let dp_plus = self.pressure_drop((m + h) / rho).abs();
|
||||
let dp_minus = self.pressure_drop((m - h) / rho).abs();
|
||||
let ddp_dm = (dp_plus - dp_minus) / (2.0 * h);
|
||||
// r0 = P_out - P_in + dp(m) → ∂r0/∂m = ∂dp/∂m
|
||||
jacobian.add_entry(0, m_idx, ddp_dm);
|
||||
}
|
||||
}
|
||||
// r1 = h_out - h_in → ∂r1/∂h_out = 1, ∂r1/∂h_in = -1
|
||||
jacobian.add_entry(1, out_h, 1.0);
|
||||
jacobian.add_entry(1, in_h, -1.0);
|
||||
@@ -929,6 +954,7 @@ mod tests {
|
||||
operational_state: OperationalState::default(),
|
||||
design_dp_pa: 0.0,
|
||||
edge_coupled: false,
|
||||
inlet_m_idx: None,
|
||||
inlet_p_idx: None,
|
||||
inlet_h_idx: None,
|
||||
outlet_p_idx: None,
|
||||
@@ -1116,6 +1142,34 @@ mod tests {
|
||||
assert!(dp2 > dp1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_edge_coupled_zero_design_dp_uses_darcy() {
|
||||
use crate::Component;
|
||||
let mut pipe = create_test_pipe_connected().with_design_pressure_drop_pa(0.0);
|
||||
// state layout: m, P_in, h_in, m_out, P_out, h_out
|
||||
pipe.set_system_context(0, &[(0, 1, 2), (3, 4, 5)]);
|
||||
let m = 5.0_f64; // kg/s
|
||||
let flow_m3 = m / pipe.fluid_density();
|
||||
let dp_expected = pipe.pressure_drop(flow_m3).abs();
|
||||
assert!(
|
||||
dp_expected > 100.0,
|
||||
"test pipe should have meaningful Darcy ΔP"
|
||||
);
|
||||
|
||||
let p_in = 300_000.0;
|
||||
let h = 100_000.0;
|
||||
let state = vec![m, p_in, h, m, p_in - dp_expected, h];
|
||||
let mut residuals = vec![0.0; 2];
|
||||
pipe.compute_residuals(&state, &mut residuals).unwrap();
|
||||
assert_relative_eq!(residuals[0], 0.0, epsilon = 1e-6);
|
||||
assert_relative_eq!(residuals[1], 0.0, epsilon = 1e-12);
|
||||
|
||||
// Wrong P_out → residual equals ΔP error
|
||||
let state_bad = vec![m, p_in, h, m, p_in, h]; // isobaric — should fail
|
||||
pipe.compute_residuals(&state_bad, &mut residuals).unwrap();
|
||||
assert_relative_eq!(residuals[0], dp_expected, epsilon = 1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_f_dp_scales_pressure_drop() {
|
||||
let mut pipe = create_test_pipe_connected();
|
||||
|
||||
Reference in New Issue
Block a user