diff --git a/apps/web/public/examples/bphx_evaporator_condenser.json b/apps/web/public/examples/bphx_evaporator_condenser.json index a907bca..f428145 100644 --- a/apps/web/public/examples/bphx_evaporator_condenser.json +++ b/apps/web/public/examples/bphx_evaporator_condenser.json @@ -33,6 +33,7 @@ "target_subcooling_k": 5.0, "emergent_pressure": true, "correlation": "Longo2004", + "dp_correlation": "SimplifiedChannel", "ua": 2500.0 }, { @@ -52,6 +53,7 @@ "target_superheat_k": 5.0, "emergent_pressure": true, "correlation": "Longo2004", + "dp_correlation": "SimplifiedChannel", "ua": 2000.0 }, { diff --git a/apps/web/src/components/panels/PropertiesPanel.tsx b/apps/web/src/components/panels/PropertiesPanel.tsx index 14e3e37..962c17c 100644 --- a/apps/web/src/components/panels/PropertiesPanel.tsx +++ b/apps/web/src/components/panels/PropertiesPanel.tsx @@ -903,10 +903,24 @@ function ModelicaResultsView({
{inspector.delta_p_bar != null && ( - + = 0.05 + ? `${(inspector.delta_p_bar * 100).toFixed(2)} kPa` + : `${(inspector.delta_p_bar * 1e5).toFixed(1)} Pa` + } + /> )} {inspector.delta_p_sec_bar != null && ( - + = 0.05 + ? `${(inspector.delta_p_sec_bar * 100).toFixed(2)} kPa` + : `${(inspector.delta_p_sec_bar * 1e5).toFixed(1)} Pa` + } + /> )} {inspector.q_primary_kw != null && ( @@ -977,43 +991,6 @@ function ModelicaResultsView({ ); } -function SolvedVariablesBlock({ - items, - title, -}: { - items: SolvedVariable[]; - title: string; -}) { - if (items.length === 0) return null; - return ( -
-
- {title} -
- - - - - - - - - - - {items.map((sv) => ( - - - - - - - ))} - -
VarValueMinMax
{sv.variable}{fmtOpt(sv.value, 4)}{fmtOpt(sv.min, 3)}{fmtOpt(sv.max, 3)}
-
- ); -} - function MiniMetric({ label, value }: { label: string; value: string }) { return (
diff --git a/apps/web/src/lib/componentInspector.ts b/apps/web/src/lib/componentInspector.ts index 8a010f1..43be94a 100644 --- a/apps/web/src/lib/componentInspector.ts +++ b/apps/web/src/lib/componentInspector.ts @@ -182,8 +182,22 @@ export function buildComponentInspector( } const summaryLines: string[] = []; - if (delta_p_bar != null) summaryLines.push(`ΔP = ${fmt(delta_p_bar, 3)} bar`); - if (delta_p_sec_bar != null) summaryLines.push(`ΔP sec = ${fmt(delta_p_sec_bar, 3)} bar`); + if (delta_p_bar != null) { + const dp_kpa = delta_p_bar * 100.0; + summaryLines.push( + Math.abs(dp_kpa) >= 0.05 + ? `ΔP = ${fmt(dp_kpa, 2)} kPa` + : `ΔP = ${fmt(delta_p_bar * 1e5, 1)} Pa`, + ); + } + if (delta_p_sec_bar != null) { + const dp_kpa = delta_p_sec_bar * 100.0; + summaryLines.push( + Math.abs(dp_kpa) >= 0.05 + ? `ΔP sec = ${fmt(dp_kpa, 2)} kPa` + : `ΔP sec = ${fmt(delta_p_sec_bar * 1e5, 1)} Pa`, + ); + } if (q_primary_kw != null) summaryLines.push(`Q̇ = ${fmt(Math.abs(q_primary_kw), 2)} kW`); if (q_secondary_kw != null) summaryLines.push(`Q̇ sec = ${fmt(Math.abs(q_secondary_kw), 2)} kW`); if (work_kw != null) summaryLines.push(`Ẇ = ${fmt(work_kw, 2)} kW`); diff --git a/crates/components/src/heat_exchanger/bphx_exchanger.rs b/crates/components/src/heat_exchanger/bphx_exchanger.rs index 0effc09..e67bbd9 100644 --- a/crates/components/src/heat_exchanger/bphx_exchanger.rs +++ b/crates/components/src/heat_exchanger/bphx_exchanger.rs @@ -498,7 +498,8 @@ impl BphxExchanger { let dp_hot = self.side_channel_dp(state[m_h], rho_hot, z_dp)?; let dp_cold = self.side_channel_dp(state[m_c], rho_cold, z_dp)?; - let a_flow = self.geometry.channel_flow_area() * self.geometry.n_channels() as f64; + let a_flow = + self.geometry.channel_flow_area() * self.geometry.n_channels_per_side() as f64; if a_flow <= 1e-30 { return Err(ComponentError::InvalidState( "BPHX channel flow area too small for pressure-drop Jacobian".into(), diff --git a/crates/components/src/heat_exchanger/bphx_geometry.rs b/crates/components/src/heat_exchanger/bphx_geometry.rs index 8138d09..d0f5144 100644 --- a/crates/components/src/heat_exchanger/bphx_geometry.rs +++ b/crates/components/src/heat_exchanger/bphx_geometry.rs @@ -170,20 +170,29 @@ impl BphxGeometry { self.channel_spacing * self.plate_width } - /// Returns the total number of channels (n_plates - 1). + /// Returns the total number of inter-plate gaps (n_plates − 1). pub fn n_channels(&self) -> u32 { self.n_plates.saturating_sub(1) } - /// Returns the mass flux for a given mass flow rate. + /// Channels available to **one** fluid in a single-pass counterflow pack. /// - /// G = m_dot / (A_channel × n_channels) + /// Alternating hot/cold channels split the gaps roughly in half: + /// `⌊n_plates / 2⌋` (at least 1). Using the full `n_channels` for mass flux + /// underestimates G by ~2× and ΔP by ~4×. + pub fn n_channels_per_side(&self) -> u32 { + (self.n_plates / 2).max(1) + } + + /// Returns the mass flux for a given mass flow rate on one fluid side. + /// + /// G = m_dot / (A_channel × n_channels_per_side) /// /// # Arguments /// - /// * `mass_flow` - Total mass flow rate (kg/s) + /// * `mass_flow` - Total mass flow rate on that side (kg/s) pub fn mass_flux(&self, mass_flow: f64) -> f64 { - let n_channels = self.n_channels() as f64; + let n_channels = self.n_channels_per_side() as f64; if n_channels < 1e-10 { return 0.0; } @@ -460,8 +469,12 @@ mod tests { .unwrap(); let mass_flow = 0.1; + // 10 plates → 5 channels per side; A_ch = 0.002 × 0.1 = 2e-4 m² + // G = 0.1 / (2e-4 × 5) = 100 kg/(m²·s) let g = geo.mass_flux(mass_flow); - assert!(g > 0.0); + assert!((g - 100.0).abs() < 1e-9, "got G={g}"); + assert_eq!(geo.n_channels_per_side(), 5); + assert_eq!(geo.n_channels(), 9); } #[test]