'use strict'; const PA_PER_MBAR = 100; const SECONDS_PER_HOUR = 3600; function withinTolerance(observed, expected, absTol, relTol) { if (!Number.isFinite(observed) || !Number.isFinite(expected)) return false; const absErr = Math.abs(observed - expected); if (absErr <= absTol) return true; if (Math.abs(expected) > 0) return absErr / Math.abs(expected) <= relTol; return false; } function assertMassBalance({ inflowKgPerS, outflowKgPerS, accumulationKgPerS = 0, label = 'mass', absTol = 1e-6, relTol = 1e-3 } = {}) { const expected = inflowKgPerS - accumulationKgPerS; const ok = withinTolerance(outflowKgPerS, expected, absTol, relTol); return { ok, label, inflowKgPerS, outflowKgPerS, accumulationKgPerS, residualKgPerS: inflowKgPerS - outflowKgPerS - accumulationKgPerS, relErr: expected === 0 ? null : (outflowKgPerS - expected) / expected, }; } function assertHydraulicBalance({ headerSuctionPa, headerDischargePa, pumpHeadPa, frictionPa = 0, staticHeadPa = 0, label = 'hydraulic', absTol = 50, relTol = 1e-3 } = {}) { const lhs = headerDischargePa - headerSuctionPa; const rhs = pumpHeadPa - frictionPa - staticHeadPa; const ok = withinTolerance(lhs, rhs, absTol, relTol); return { ok, label, lhsPa: lhs, rhsPa: rhs, residualPa: lhs - rhs, residualMbar: (lhs - rhs) / PA_PER_MBAR, }; } function assertHydraulicPower({ flowM3PerS, headPa, shaftPowerW, efficiency, label = 'hydraulic-power', absTol = 1, relTol = 5e-3 } = {}) { if (!Number.isFinite(efficiency) || efficiency <= 0 || efficiency > 1.0) { return { ok: false, label, msg: `efficiency=${efficiency} outside (0,1]` }; } const expectedShaftPowerW = (flowM3PerS * headPa) / efficiency; const ok = withinTolerance(shaftPowerW, expectedShaftPowerW, absTol, relTol); return { ok, label, flowM3PerS, headPa, efficiency, expectedShaftPowerW, observedShaftPowerW: shaftPowerW, residualW: shaftPowerW - expectedShaftPowerW, }; } function assertEnergyBalance({ heatInW = 0, workInW = 0, heatOutW = 0, workOutW = 0, accumulationW = 0, label = 'energy', absTol = 1, relTol = 1e-3 } = {}) { const inputs = heatInW + workInW; const outputs = heatOutW + workOutW + accumulationW; const ok = withinTolerance(inputs, outputs, absTol, relTol); return { ok, label, inputsW: inputs, outputsW: outputs, residualW: inputs - outputs, }; } function assertOxygenTransfer({ klaPerS, csMgPerL, cMgPerL, otrKgPerS, volumeM3, label = 'OTR', absTol = 1e-4, relTol = 5e-3 } = {}) { if (!Number.isFinite(klaPerS) || klaPerS < 0) return { ok: false, label, msg: `KLa=${klaPerS} invalid` }; if (!Number.isFinite(volumeM3) || volumeM3 <= 0) return { ok: false, label, msg: `volume=${volumeM3} invalid` }; const driveMgPerL = csMgPerL - cMgPerL; const expectedKgPerS = klaPerS * driveMgPerL * volumeM3 * 1e-3 / SECONDS_PER_HOUR * SECONDS_PER_HOUR / 1000; const expectedKgPerS_corrected = klaPerS * driveMgPerL * volumeM3 / 1e6; const ok = withinTolerance(otrKgPerS, expectedKgPerS_corrected, absTol, relTol); return { ok, label, klaPerS, csMgPerL, cMgPerL, driveMgPerL, volumeM3, expectedKgPerS: expectedKgPerS_corrected, observedKgPerS: otrKgPerS, residualKgPerS: otrKgPerS - expectedKgPerS_corrected, }; } function reportToString(r) { if (r.ok) return `OK ${r.label}`; const fields = Object.entries(r) .filter(([k]) => !['ok', 'label'].includes(k)) .map(([k, v]) => `${k}=${typeof v === 'number' ? v.toExponential(3) : v}`) .join(' '); return `FAIL ${r.label} ${fields}`; } module.exports = { assertMassBalance, assertHydraulicBalance, assertHydraulicPower, assertEnergyBalance, assertOxygenTransfer, reportToString, PA_PER_MBAR, };