refactor(diffuser): pass canonical specific flux Nm³/(h·m²) to curve lookup
Pairs with the generalFunctions curve-file rewrite — all supplier curves now share one X-axis: specific air flux through the membrane, Nm³/(h·m² membrane). specificClass._calcOtrPressure computes the flux from the existing n_flow and i_n_elements plus a new i_membrane_area field, and queries the OTR / DWP curves at that flux instead of at flow per element. i_membrane_area resolution (precedence top-down): 1. config.diffuser.membraneAreaPerElement (new optional override) 2. specs._meta.membraneArea_m2_per_element (curve-file source of truth) 3. 0.18 m² (Jäger TD-65 / GVA placeholder) _loadSpecs now preserves the curve's full _meta block so the area metadata reaches configure(). Output schema gains oFluxPerM2 alongside the existing oFlowElement (kept for user-readability — both numbers are useful on a dashboard). _checkLimits compares specific flux against the curve's minX / maxX (also in canonical units now), so warning / alarm thresholds remain self-consistent. 8/8 tests pass. Verified across all 5 suppliers (gva-elastox-r, jaeger-jetflex, aerostrip-phoenix, pik300, prk300) that the resolved flux + SSOTR + DWP numbers match hand-computed expectations. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -27,6 +27,9 @@ class nodeClass extends BaseNodeAdapter {
|
||||
number: n(uiConfig.number, 1),
|
||||
elements: n(uiConfig.i_elements, 1),
|
||||
density: n(uiConfig.i_diff_density, 15),
|
||||
membraneAreaPerElement: Number.isFinite(Number(uiConfig.membraneAreaPerElement))
|
||||
? Number(uiConfig.membraneAreaPerElement)
|
||||
: null,
|
||||
waterHeight: n(uiConfig.i_m_water, 0),
|
||||
alfaFactor: n(uiConfig.alfaf, 0.7),
|
||||
headerPressure: n(uiConfig.i_pressure, 0),
|
||||
|
||||
@@ -30,6 +30,15 @@ class Diffuser extends BaseDomain {
|
||||
this.i_flow = 0;
|
||||
this.zoneVolume = _num(d.zoneVolume, 0);
|
||||
|
||||
// Membrane area per element. Curves declare it in _meta — that's the
|
||||
// source of truth. Config can override (e.g. for a non-standard build).
|
||||
// Final fallback 0.18 m² matches the Jäger TD-65 / GVA placeholder.
|
||||
const curveArea = Number(this.specs?._meta?.membraneArea_m2_per_element);
|
||||
const cfgArea = Number(d.membraneAreaPerElement);
|
||||
this.i_membrane_area = Number.isFinite(cfgArea) && cfgArea > 0
|
||||
? cfgArea
|
||||
: (Number.isFinite(curveArea) && curveArea > 0 ? curveArea : 0.18);
|
||||
|
||||
this.n_kg = this._calcAirDensityMbar(1013.25, 0, 20);
|
||||
this.n_flow = 0;
|
||||
this.o_otr = 0;
|
||||
@@ -39,6 +48,7 @@ class Diffuser extends BaseDomain {
|
||||
this.o_kg = 0; this.o_kg_h = 0; this.o_kgo2_h = 0; this.o_kgo2 = 0;
|
||||
this.o_kgo2_h_min = 0; this.o_kgo2_h_max = 0;
|
||||
this.o_flow_element = 0;
|
||||
this.o_flux_per_m2 = 0;
|
||||
this.o_otr_min = 0; this.o_otr_max = 0;
|
||||
this.o_p_min = 0; this.o_p_max = 0;
|
||||
this.o_combined_eff = 0;
|
||||
@@ -59,7 +69,8 @@ class Diffuser extends BaseDomain {
|
||||
_recalculate() {
|
||||
if (this.i_flow <= 0) {
|
||||
this.idle = true;
|
||||
this.n_flow = 0; this.o_otr = 0; this.o_p_flow = 0; this.o_flow_element = 0;
|
||||
this.n_flow = 0; this.o_otr = 0; this.o_p_flow = 0;
|
||||
this.o_flow_element = 0; this.o_flux_per_m2 = 0;
|
||||
this.o_p_total = this.o_p_water;
|
||||
this.o_kg = 0; this.o_kg_h = 0; this.o_kgo2_h = 0; this.o_kgo2 = 0;
|
||||
this.o_combined_eff = 0; this.o_slope = 0;
|
||||
@@ -147,9 +158,14 @@ class Diffuser extends BaseDomain {
|
||||
this.o_kg_h = this.o_kg * flow;
|
||||
this.n_flow = (this.o_kg / this.n_kg) * flow;
|
||||
this.o_flow_element = Math.round((this.n_flow / this.i_n_elements) * 100) / 100;
|
||||
// Specific flux through the membrane — the canonical x-axis of every
|
||||
// curve file under datasets/assetData/curves/. Curves are indexed by
|
||||
// bottom coverage % (this.i_diff_density) and queried at this flux.
|
||||
const totalMembraneArea = this.i_n_elements * this.i_membrane_area;
|
||||
this.o_flux_per_m2 = Math.round((this.n_flow / totalMembraneArea) * 100) / 100;
|
||||
|
||||
const otr = this._interpolateCurveByDensity(this.specs.otr_curve, this.i_diff_density, this.o_flow_element);
|
||||
const pressure = this._interpolateCurveByDensity(this.specs.p_curve, 0, this.o_flow_element);
|
||||
const otr = this._interpolateCurveByDensity(this.specs.otr_curve, this.i_diff_density, this.o_flux_per_m2);
|
||||
const pressure = this._interpolateCurveByDensity(this.specs.p_curve, 0, this.o_flux_per_m2);
|
||||
|
||||
this.o_otr_min = otr.minY; this.o_otr_max = otr.maxY;
|
||||
this.o_p_min = pressure.minY; this.o_p_max = pressure.maxY;
|
||||
@@ -173,13 +189,15 @@ class Diffuser extends BaseDomain {
|
||||
_checkLimits(minFlow, maxFlow) {
|
||||
this.warning.text = []; this.warning.state = false;
|
||||
this.alarm.text = []; this.alarm.state = false;
|
||||
const f = this.o_flow_element;
|
||||
// Compare against the canonical flux, since pressure.minX / maxX come
|
||||
// from the per-m²-membrane curve.
|
||||
const f = this.o_flux_per_m2;
|
||||
for (const k of ['warning', 'alarm']) {
|
||||
const band = this[k];
|
||||
const lo = minFlow - minFlow * (band.flow.min.hyst / 100);
|
||||
const hi = maxFlow + maxFlow * (band.flow.max.hyst / 100);
|
||||
if (f < lo) { band.state = true; band.text.push(`${_cap(k)}: flow per element ${f} is below ${Math.round(lo * 100) / 100}`); }
|
||||
if (f > hi) { band.state = true; band.text.push(`${_cap(k)}: flow per element ${f} exceeds ${Math.round(hi * 100) / 100}`); }
|
||||
if (f < lo) { band.state = true; band.text.push(`${_cap(k)}: specific flux ${f} Nm³/(h·m²) is below ${Math.round(lo * 100) / 100}`); }
|
||||
if (f > hi) { band.state = true; band.text.push(`${_cap(k)}: specific flux ${f} Nm³/(h·m²) exceeds ${Math.round(hi * 100) / 100}`); }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,6 +227,7 @@ class Diffuser extends BaseDomain {
|
||||
oPLoss: this.o_p_total,
|
||||
oKgo2H: this.o_kgo2_h,
|
||||
oFlowElement: this.o_flow_element,
|
||||
oFluxPerM2: this.o_flux_per_m2,
|
||||
efficiency: this.o_combined_eff,
|
||||
slope: this.o_slope,
|
||||
oZoneOtr: this.getReactorOtr(this.zoneVolume),
|
||||
@@ -240,6 +259,7 @@ class Diffuser extends BaseDomain {
|
||||
);
|
||||
}
|
||||
return {
|
||||
_meta: raw._meta || {},
|
||||
supplier: raw._meta?.supplier || null,
|
||||
type: raw._meta?.type || null,
|
||||
model: raw._meta?.model || cfgModel,
|
||||
|
||||
Reference in New Issue
Block a user