Fix BPHX mass flux per-side channels so pressure drop is visible.
Some checks failed
CI / check (push) Has been cancelled
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:
@@ -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
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -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">
|
||||||
|
|||||||
@@ -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`);
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
@@ -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]
|
||||||
|
|||||||
Reference in New Issue
Block a user