diff --git a/index.js b/index.js index 0c437c8..3ca63cb 100644 --- a/index.js +++ b/index.js @@ -14,6 +14,7 @@ const validation = require('./src/helper/validationUtils.js'); const configUtils = require('./src/helper/configUtils.js'); const assertions = require('./src/helper/assertionUtils.js') const coolprop = require('./src/coolprop-node/src/index.js'); +const gravity = require('./src/helper/gravity.js') // Domain-specific modules const { MeasurementContainer } = require('./src/measurements/index.js'); @@ -44,5 +45,6 @@ module.exports = { convert, MenuManager, childRegistrationUtils, - loadCurve + loadCurve, + gravity }; diff --git a/src/configs/pumpingStation.json b/src/configs/pumpingStation.json index 44a05ba..d8ea505 100644 --- a/src/configs/pumpingStation.json +++ b/src/configs/pumpingStation.json @@ -409,7 +409,7 @@ } }, "timeThresholdSeconds": { - "default": 120, + "default": 5, "rules": { "type": "number", "min": 0, diff --git a/src/coolprop-node/src/index.js b/src/coolprop-node/src/index.js index c5b98ed..e5b2890 100644 --- a/src/coolprop-node/src/index.js +++ b/src/coolprop-node/src/index.js @@ -3,11 +3,61 @@ const customRefs = require('./refData.js'); class CoolPropWrapper { constructor() { + this.initialized = false; this.defaultRefrigerant = null; this.defaultTempUnit = 'K'; // K, C, F this.defaultPressureUnit = 'Pa' // Pa, kPa, bar, psi this.customRef = false; + this.PropsSI = this._propsSI.bind(this); + + + // 🔹 Wastewater correction options (defaults) + this._ww = { + enabled: true, + tss_g_per_L: 3.5, // default MLSS / TSS + density_k: 2e-4, // +0.02% per g/L + viscosity_k: 0.07, // +7% per g/L (clamped) + viscosity_max_gpl: 4 // cap effect at 4 g/L + }; + + this._initPromise = null; + this._autoInit({ refrigerant: 'Water' }); + + } + + _isWastewaterFluid(fluidRaw) { + if (!fluidRaw) return false; + const token = String(fluidRaw).trim().toLowerCase(); + return token === 'wastewater' || token.startsWith('wastewater:'); + } + + _parseWastewaterFluid(fluidRaw) { + if (!this._isWastewaterFluid(fluidRaw)) return null; + const ww = { ...this._ww }; + const [, tail] = String(fluidRaw).split(':'); + if (tail) { + tail.split(',').forEach(pair => { + const [key, value] = pair.split('=').map(s => s.trim().toLowerCase()); + if (key === 'tss' && !Number.isNaN(Number(value))) { + ww.tss_g_per_L = Number(value); + } + }); + } + return ww; + } + + _applyWastewaterCorrection(outputKey, baseValue, ww) { + if (!Number.isFinite(baseValue) || !ww || !ww.enabled) return baseValue; + switch (outputKey.toUpperCase()) { + case 'D': // density + return baseValue * (1 + ww.density_k * ww.tss_g_per_L); + case 'V': // viscosity + const effTss = Math.min(ww.tss_g_per_L, ww.viscosity_max_gpl); + return baseValue * (1 + ww.viscosity_k * effTss); + default: + return baseValue; + } } // Temperature conversion helpers @@ -407,13 +457,31 @@ class CoolPropWrapper { } } - // Direct access to CoolProp functions - async getPropsSI() { - if(!this.initialized) { - await coolprop.init(); + _autoInit(defaults) { + if (!this._initPromise) { + this._initPromise = this.init(defaults); } - return coolprop.PropsSI; + return this._initPromise; } + + _propsSI(outputKey, inKey1, inVal1, inKey2, inVal2, fluidRaw) { + if (!this.initialized) { + // Start init if no one else asked yet + this._autoInit({ refrigerant: this.defaultRefrigerant || 'Water' }); + throw new Error('CoolProp is still warming up, retry PropsSI in a moment'); + } + const ww = this._parseWastewaterFluid(fluidRaw); + const fluid = ww ? 'Water' : (this.customRefString || fluidRaw); + const baseValue = coolprop.PropsSI(outputKey, inKey1, inVal1, inKey2, inVal2, fluid); + return ww ? this._applyWastewaterCorrection(outputKey, baseValue, ww) : baseValue; + } + + //Access to coolprop + async getPropsSI() { + await this._ensureInit({ refrigerant: this.defaultRefrigerant || 'Water' }); + return this.PropsSI; + } + } module.exports = new CoolPropWrapper(); diff --git a/src/helper/gravity.js b/src/helper/gravity.js new file mode 100644 index 0000000..b6fb568 --- /dev/null +++ b/src/helper/gravity.js @@ -0,0 +1,90 @@ +/** + * Gravity calculations based on WGS-84 ellipsoid model. + * Author: Rene de Ren (Waterschap Brabantse Delta) + * License: EUPL-1.2 + */ + +class Gravity { + constructor() { + // Standard (conventional) gravity at 45° latitude, sea level + this.g0 = 9.80665; // m/s² + } + + /** + * Returns standard gravity (constant) + * @returns {number} gravity in m/s² + */ + getStandardGravity() { + return this.g0; + } + + /** + * Computes local gravity based on latitude and elevation. + * Formula: WGS-84 normal gravity (Somigliana) + * @param {number} latitudeDeg Latitude in degrees (−90 → +90) + * @param {number} elevationM Elevation above sea level [m] + * @returns {number} gravity in m/s² + */ + getLocalGravity(latitudeDeg, elevationM = 0) { + const phi = (latitudeDeg * Math.PI) / 180; + const sinPhi = Math.sin(phi); + const sin2 = sinPhi * sinPhi; + const sin2_2phi = Math.sin(2 * phi) ** 2; + + // WGS-84 normal gravity on the ellipsoid + const gSurface = + 9.780327 * (1 + 0.0053024 * sin2 - 0.0000058 * sin2_2phi); + + // Free-air correction for elevation (~ −3.086×10⁻⁶ m/s² per m) + const gLocal = gSurface - 3.086e-6 * elevationM; + return gLocal; + } + + /** + * Calculates hydrostatic pressure difference (ΔP = ρ g h) + * @param {number} density Fluid density [kg/m³] + * @param {number} heightM Height difference [m] + * @param {number} latitudeDeg Latitude (for local g) + * @param {number} elevationM Elevation (for local g) + * @returns {number} Pressure difference [Pa] + */ + pressureHead(density, heightM, latitudeDeg = 45, elevationM = 0) { + const g = this.getLocalGravity(latitudeDeg, elevationM); + return density * g * heightM; + } + + /** + * Calculates weight force (F = m g) + * @param {number} massKg Mass [kg] + * @param {number} latitudeDeg Latitude (for local g) + * @param {number} elevationM Elevation (for local g) + * @returns {number} Force [N] + */ + weightForce(massKg, latitudeDeg = 45, elevationM = 0) { + const g = this.getLocalGravity(latitudeDeg, elevationM); + return massKg * g; + } +} + +module.exports = new Gravity(); + + +/* +const gravity = gravity; + +// Standard gravity +console.log('g₀ =', gravity.getStandardGravity(), 'm/s²'); + +// Local gravity (Breda ≈ 51.6° N, 3 m elevation) +console.log('g @ Breda =', gravity.getLocalGravity(51.6, 3).toFixed(6), 'm/s²'); + +// Head pressure for 5 m water column at Breda +console.log( + 'ΔP =', + gravity.pressureHead(1000, 5, 51.6, 3).toFixed(1), + 'Pa' +); + +// Weight of 1 kg mass at Breda +console.log('Weight =', gravity.weightForce(1, 51.6, 3).toFixed(6), 'N'); +*/ \ No newline at end of file diff --git a/src/menu/physicalPosition.js b/src/menu/physicalPosition.js index fe167e5..aab35e9 100644 --- a/src/menu/physicalPosition.js +++ b/src/menu/physicalPosition.js @@ -6,9 +6,9 @@ class PhysicalPositionMenu { return { positionGroups: [ { group: 'Positional', options: [ - { value: 'upstream', label: '→ Upstream', icon: '←'}, //flow is then typically left to right + { value: 'upstream', label: '→ Upstream', icon: '→'}, //flow is then typically left to right { value: 'atEquipment', label: '⊥ in place' , icon: '⊥' }, - { value: 'downstream', label: '← Downstream' , icon: '→' } + { value: 'downstream', label: '← Downstream' , icon: '←' } ] } ],