const { logger, interpolation, gravity, convert } = require('generalFunctions'); class Diffuser { constructor(config = {}) { this.config = config; this.logger = new logger( this.config.general?.logging?.enabled, this.config.general?.logging?.logLevel, this.config.general?.name, ); this.interpolation = new interpolation({ type: 'linear' }); this.fysics = gravity.fysics; this.convert = convert; this.specs = this.loadSpecs(); this.idle = true; this.warning = { state: false, text: [], flow: { min: { hyst: 2 }, max: { hyst: 2 } } }; this.alarm = { state: false, text: [], flow: { min: { hyst: 10 }, max: { hyst: 10 } } }; this.i_pressure = this.config.diffuser?.headerPressure || 0; this.i_local_atm_pressure = this.config.diffuser?.localAtmPressure || 1013.25; this.i_water_density = this.config.diffuser?.waterDensity || 997; this.i_alfa_factor = this.config.diffuser?.alfaFactor || 0.7; this.i_n_elements = this.normalizePositiveInteger(this.config.diffuser?.elements, 1); this.i_diff_density = this.normalizeNumber(this.config.diffuser?.density, 2.4); this.i_m_water = this.normalizeNumber(this.config.diffuser?.waterHeight, 0); this.i_flow = 0; this.n_kg = this.fysics.calc_air_dens(1013.25, 0, 20); this.n_flow = 0; this.o_otr = 0; this.o_p_flow = 0; this.o_p_water = this.fysics.heigth_to_pressure(this.i_water_density, this.i_m_water); 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_kgo2_h_min = 0; this.o_kgo2_h_max = 0; this.o_flow_element = 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; this.o_slope = 0; } normalizeNumber(value, fallback = 0) { const parsed = Number(value); return Number.isFinite(parsed) ? parsed : fallback; } normalizePositiveInteger(value, fallback = 1) { const parsed = Math.round(Number(value)); return Number.isFinite(parsed) && parsed > 0 ? parsed : fallback; } setDensity(value) { this.i_diff_density = this.normalizeNumber(value, this.i_diff_density); this.recalculate(); } setFlow(value) { this.i_flow = Math.max(0, this.normalizeNumber(value, 0)); this.recalculate(); } setWaterHeight(value) { this.i_m_water = Math.max(0, this.normalizeNumber(value, this.i_m_water)); this.o_p_water = this.fysics.heigth_to_pressure(this.i_water_density, this.i_m_water); this.recalculate(); } setHeaderPressure(value) { this.i_pressure = this.normalizeNumber(value, this.i_pressure); this.recalculate(); } setElementCount(value) { this.i_n_elements = this.normalizePositiveInteger(value, this.i_n_elements); this.recalculate(); } setAlfaFactor(value) { this.i_alfa_factor = this.normalizeNumber(value, this.i_alfa_factor); this.recalculate(); } 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.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; this.warning.text = []; this.warning.state = false; this.alarm.text = []; this.alarm.state = false; return; } this.idle = false; this.calcOtrPressure(this.i_flow); } getCurveKeys(curve) { return Object.keys(curve) .map(Number) .sort((a, b) => a - b); } interpolateSeries(points, x) { this.interpolation.load_spline(points.x, points.y, 'linear'); return this.interpolation.interpolate(x); } interpolateCurveByDensity(curve, density, x) { const keys = this.getCurveKeys(curve); if (keys.length === 1) { const only = curve[keys[0]]; return { value: this.interpolateSeries(only, x), minY: Math.min(...only.y), maxY: Math.max(...only.y), minX: Math.min(...only.x), maxX: Math.max(...only.x), slope: this.getSegmentSlope(only, x), }; } const lowerKey = keys.reduce((acc, key) => (key <= density ? key : acc), keys[0]); const upperKey = keys.find((key) => key >= density) ?? keys[keys.length - 1]; const lowerCurve = curve[lowerKey]; const upperCurve = curve[upperKey]; if (lowerKey === upperKey) { return { value: this.interpolateSeries(lowerCurve, x), minY: Math.min(...lowerCurve.y), maxY: Math.max(...lowerCurve.y), minX: Math.min(...lowerCurve.x), maxX: Math.max(...lowerCurve.x), slope: this.getSegmentSlope(lowerCurve, x), }; } const lowerValue = this.interpolateSeries(lowerCurve, x); const upperValue = this.interpolateSeries(upperCurve, x); const ratio = (density - lowerKey) / (upperKey - lowerKey); return { value: lowerValue + (upperValue - lowerValue) * ratio, minY: Math.min(...lowerCurve.y) + (Math.min(...upperCurve.y) - Math.min(...lowerCurve.y)) * ratio, maxY: Math.max(...lowerCurve.y) + (Math.max(...upperCurve.y) - Math.max(...lowerCurve.y)) * ratio, minX: Math.min(...lowerCurve.x), maxX: Math.max(...lowerCurve.x), slope: this.getSegmentSlope(lowerCurve, x), }; } getSegmentSlope(curvePoints, x) { const xs = curvePoints.x; const ys = curvePoints.y; for (let i = 0; i < xs.length - 1; i += 1) { if (x <= xs[i + 1]) { return (ys[i + 1] - ys[i]) / (xs[i + 1] - xs[i]); } } const last = xs.length - 1; return (ys[last] - ys[last - 1]) / (xs[last] - xs[last - 1]); } combineEff(oOtr, oOtrMin, oOtrMax, oPFlow, oPMin, oPMax) { const otrSpan = oOtrMax - oOtrMin; const pSpan = oPMax - oPMin; const eff1 = otrSpan > 0 ? (oOtr - oOtrMin) / otrSpan : 0; const eff2 = pSpan > 0 ? 1 - ((oPFlow - oPMin) / pSpan) : 0; return Math.max(0, eff1 * eff2 * 100); } calcOtrPressure(flow) { const totalInputPressureMbar = this.i_local_atm_pressure + this.i_pressure; this.o_kg = this.fysics.calc_air_dens(totalInputPressureMbar, 0, 20); 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; 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); this.o_otr_min = otr.minY; this.o_otr_max = otr.maxY; this.o_p_min = pressure.minY; this.o_p_max = pressure.maxY; this.o_otr = Math.round(otr.value * 100) / 100; this.o_p_flow = Math.round(pressure.value * 100) / 100; this.o_p_total = Math.round((this.o_p_water + this.o_p_flow) * 100) / 100; this.o_kgo2_h = Math.round(this.convert(this.o_otr * this.n_flow * this.i_m_water * this.i_alfa_factor).from('g').to('kg') * 100) / 100; this.o_kgo2_h_min = Math.round(this.convert(this.o_otr_min * this.n_flow * this.i_m_water * this.i_alfa_factor).from('g').to('kg') * 100) / 100; this.o_kgo2_h_max = Math.round(this.convert(this.o_otr_max * this.n_flow * this.i_m_water * this.i_alfa_factor).from('g').to('kg') * 100) / 100; this.o_kgo2 = this.o_kgo2_h / 3600; this.o_combined_eff = Math.round(this.combineEff( this.o_otr, this.o_otr_min, this.o_otr_max, this.o_p_flow, this.o_p_min, this.o_p_max, ) * 100) / 100; this.o_slope = Math.round(otr.slope * 1000) / 1000; this.warningCheck(pressure.minX, pressure.maxX); this.alarmCheck(pressure.minX, pressure.maxX); } warningCheck(minFlow, maxFlow) { this.warning.text = []; this.warning.state = false; const minHyst = minFlow * (this.warning.flow.min.hyst / 100); const maxHyst = maxFlow * (this.warning.flow.max.hyst / 100); if (this.o_flow_element < minFlow - minHyst) { this.warning.state = true; this.warning.text.push(`Warning: flow per element ${this.o_flow_element} is below ${Math.round((minFlow - minHyst) * 100) / 100}`); } if (this.o_flow_element > maxFlow + maxHyst) { this.warning.state = true; this.warning.text.push(`Warning: flow per element ${this.o_flow_element} exceeds ${Math.round((maxFlow + maxHyst) * 100) / 100}`); } } alarmCheck(minFlow, maxFlow) { this.alarm.text = []; this.alarm.state = false; const minHyst = minFlow * (this.alarm.flow.min.hyst / 100); const maxHyst = maxFlow * (this.alarm.flow.max.hyst / 100); if (this.o_flow_element < minFlow - minHyst) { this.alarm.state = true; this.alarm.text.push(`Alarm: flow per element ${this.o_flow_element} is below ${Math.round((minFlow - minHyst) * 100) / 100}`); } if (this.o_flow_element > maxFlow + maxHyst) { this.alarm.state = true; this.alarm.text.push(`Alarm: flow per element ${this.o_flow_element} exceeds ${Math.round((maxFlow + maxHyst) * 100) / 100}`); } } getStatus() { if (this.alarm.state) { return { fill: 'red', shape: 'dot', text: this.alarm.text[0] }; } if (this.warning.state) { return { fill: 'yellow', shape: 'dot', text: this.warning.text[0] }; } if (this.idle) { return { fill: 'grey', shape: 'dot', text: `${this.o_kgo2_h} kg o2 / h` }; } return { fill: 'green', shape: 'dot', text: `${this.o_kgo2_h} kg o2 / h` }; } getOutput() { return { iPressure: this.i_pressure, iMWater: this.i_m_water, iFlow: this.i_flow, nFlow: Math.round(this.n_flow * 100) / 100, oOtr: this.o_otr, oPLoss: this.o_p_total, oKgo2H: this.o_kgo2_h, oFlowElement: this.o_flow_element, efficiency: this.o_combined_eff, slope: this.o_slope, idle: this.idle, warning: [...this.warning.text], alarm: [...this.alarm.text], }; } loadSpecs() { return { supplier: 'GVA', type: 'ELASTOX-R', units: { Nm3: { temp: 20, pressure: 1.01325, RH: 0 }, }, otr_curve: { 2.4: { x: [2, 3, 4, 5, 6, 7, 8, 9, 10], y: [26, 25, 24, 23.5, 23, 22.75, 22.5, 22.25, 22], }, }, p_curve: { 0: { x: [2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], y: [40, 42.5, 45, 47.5, 50, 51.5, 53, 54.5, 56, 57.5, 59], }, }, }; } } module.exports = Diffuser;