From 37a85690d1110b139c50286f732726fad1cada36 Mon Sep 17 00:00:00 2001 From: znetsixe Date: Tue, 12 May 2026 18:16:49 +0200 Subject: [PATCH] =?UTF-8?q?refactor(diffuser):=20pass=20canonical=20specif?= =?UTF-8?q?ic=20flux=20Nm=C2=B3/(h=C2=B7m=C2=B2)=20to=20curve=20lookup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/nodeClass.js | 3 +++ src/specificClass.js | 32 ++++++++++++++++++++++++++------ 2 files changed, 29 insertions(+), 6 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 87e9475..ad01ada 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -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), diff --git a/src/specificClass.js b/src/specificClass.js index 42ea754..a4d1e7d 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -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,