Fix BPHX mass flux per-side channels so pressure drop is visible.
Some checks failed
CI / check (push) Has been cancelled

Mass flux used all plate gaps instead of half the pack per fluid, underestimating G and making ΔP look like zero in the UI (0.000 bar). Also show ΔP in kPa/Pa and sync the web example.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-19 17:06:44 +02:00
parent 81abd5a591
commit 2f1f7ecb80
5 changed files with 55 additions and 48 deletions

View File

@@ -33,6 +33,7 @@
"target_subcooling_k": 5.0, "target_subcooling_k": 5.0,
"emergent_pressure": true, "emergent_pressure": true,
"correlation": "Longo2004", "correlation": "Longo2004",
"dp_correlation": "SimplifiedChannel",
"ua": 2500.0 "ua": 2500.0
}, },
{ {
@@ -52,6 +53,7 @@
"target_superheat_k": 5.0, "target_superheat_k": 5.0,
"emergent_pressure": true, "emergent_pressure": true,
"correlation": "Longo2004", "correlation": "Longo2004",
"dp_correlation": "SimplifiedChannel",
"ua": 2000.0 "ua": 2000.0
}, },
{ {

View File

@@ -903,10 +903,24 @@ function ModelicaResultsView({
<div className="grid grid-cols-2 gap-1.5"> <div className="grid grid-cols-2 gap-1.5">
{inspector.delta_p_bar != null && ( {inspector.delta_p_bar != null && (
<MiniMetric label="ΔP" value={`${inspector.delta_p_bar.toFixed(3)} bar`} /> <MiniMetric
label="ΔP"
value={
Math.abs(inspector.delta_p_bar) * 100 >= 0.05
? `${(inspector.delta_p_bar * 100).toFixed(2)} kPa`
: `${(inspector.delta_p_bar * 1e5).toFixed(1)} Pa`
}
/>
)} )}
{inspector.delta_p_sec_bar != null && ( {inspector.delta_p_sec_bar != null && (
<MiniMetric label="ΔP sec" value={`${inspector.delta_p_sec_bar.toFixed(3)} bar`} /> <MiniMetric
label="ΔP sec"
value={
Math.abs(inspector.delta_p_sec_bar) * 100 >= 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 && ( {inspector.q_primary_kw != null && (
<MiniMetric label="Q̇" value={`${Math.abs(inspector.q_primary_kw).toFixed(2)} kW`} /> <MiniMetric label="Q̇" value={`${Math.abs(inspector.q_primary_kw).toFixed(2)} kW`} />
@@ -977,43 +991,6 @@ function ModelicaResultsView({
); );
} }
function SolvedVariablesBlock({
items,
title,
}: {
items: SolvedVariable[];
title: string;
}) {
if (items.length === 0) return null;
return (
<div className="overflow-hidden rounded-md border border-[var(--line)]">
<div className="border-b border-[var(--line)] bg-[var(--chrome-2)] px-2 py-1 text-[9px] font-semibold uppercase tracking-wide text-[var(--ink-faint)]">
{title}
</div>
<table className="w-full text-[10px]">
<thead>
<tr className="text-left text-[9px] text-[var(--ink-faint)]">
<th className="px-1.5 py-1 font-medium">Var</th>
<th className="px-1 py-1 font-medium">Value</th>
<th className="px-1 py-1 font-medium">Min</th>
<th className="px-1 py-1 font-medium">Max</th>
</tr>
</thead>
<tbody>
{items.map((sv) => (
<tr key={sv.id} className="border-t border-[var(--line)]">
<td className="mono px-1.5 py-1 text-[var(--ink)]">{sv.variable}</td>
<td className="mono px-1 py-1 text-[var(--ink)]">{fmtOpt(sv.value, 4)}</td>
<td className="mono px-1 py-1 text-[var(--ink-dim)]">{fmtOpt(sv.min, 3)}</td>
<td className="mono px-1 py-1 text-[var(--ink-dim)]">{fmtOpt(sv.max, 3)}</td>
</tr>
))}
</tbody>
</table>
</div>
);
}
function MiniMetric({ label, value }: { label: string; value: string }) { function MiniMetric({ label, value }: { label: string; value: string }) {
return ( return (
<div className="rounded border border-[var(--line)] bg-[var(--chrome-2)] px-1.5 py-1"> <div className="rounded border border-[var(--line)] bg-[var(--chrome-2)] px-1.5 py-1">

View File

@@ -182,8 +182,22 @@ export function buildComponentInspector(
} }
const summaryLines: string[] = []; const summaryLines: string[] = [];
if (delta_p_bar != null) summaryLines.push(`ΔP = ${fmt(delta_p_bar, 3)} bar`); if (delta_p_bar != null) {
if (delta_p_sec_bar != null) summaryLines.push(`ΔP sec = ${fmt(delta_p_sec_bar, 3)} bar`); 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_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 (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`); if (work_kw != null) summaryLines.push(`Ẇ = ${fmt(work_kw, 2)} kW`);

View File

@@ -498,7 +498,8 @@ impl BphxExchanger {
let dp_hot = self.side_channel_dp(state[m_h], rho_hot, z_dp)?; 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 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 { if a_flow <= 1e-30 {
return Err(ComponentError::InvalidState( return Err(ComponentError::InvalidState(
"BPHX channel flow area too small for pressure-drop Jacobian".into(), "BPHX channel flow area too small for pressure-drop Jacobian".into(),

View File

@@ -170,20 +170,29 @@ impl BphxGeometry {
self.channel_spacing * self.plate_width 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 { pub fn n_channels(&self) -> u32 {
self.n_plates.saturating_sub(1) 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 /// # 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 { 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 { if n_channels < 1e-10 {
return 0.0; return 0.0;
} }
@@ -460,8 +469,12 @@ mod tests {
.unwrap(); .unwrap();
let mass_flow = 0.1; 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); 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] #[test]