Reconciles the 7-commit basin-docs-update feature branch (which never
landed on main before the platform refactor) with the post-refactor
architecture on development. Each basin-docs feature ported into the
relevant concern module:
control/levelBased.js
- stopLevel Schmitt-trigger + dead-band keep-alive
- Shifted ramp (arm % → hold @ 100% → ramp down to shiftLevel)
- Linear vs log up-curve (curveType + logCurveFactor)
measurement/flowAggregator.js
- Predicted-volume overflow clamp + spill flow stream
- Cumulative overflowVolume + underflowVolume
- Hard floor at 0 + dry-run-on-transition handling
basin/thresholdValidator.js
- computeSafetyPoints exposes dryRunLevel + highVolumeSafetyLevel
- startLevel ≤ inflowLevel invariant added
measurement/calibration.js + commands/
- Manual q_out path (set.outflow / q_out alias)
safety/safetyController.js
- Accepts both legacy + new high-volume threshold names
UI:
pumpingStation.html — restored the side-panel + SVG mode-preview block,
added defaults for stopLevel/shiftLevel/shiftArmPercent/levelCurveType/
logCurveFactor/enableShiftedRamp.
src/editor/* — basin-docs' 7-file modular editor (replaces single
src/editor.js, which is deleted).
pumpingStation.js — admin endpoint serves editor/:file.
Tests: 130/130 pass (125 basic + 5 integration). Two basin-docs test
files added: nodeClass-config.test.js, basic-dashboard-flow.test.js,
shifted-ramp-end-to-end.test.js. One pre-refactor control-levelBased
test adapted to match basin-docs canonical "no-shutdown in dead zone"
behaviour.
Human-review items (see commit context):
- rampFoot = inflowLevel (matches basin-docs test); basin-docs source
used rampFoot = startLevel. Domain owner: confirm intent.
- Naming kept dual (overfillLevel + highVolumeSafetyLevel).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
100 lines
4.0 KiB
JavaScript
100 lines
4.0 KiB
JavaScript
// Basin geometry for a wet-well pumping station.
|
||
//
|
||
// Models the basin as a rectangular prism (constant cross-section), so
|
||
// volume = level × surfaceArea. Owns the level↔volume conversions and the
|
||
// derived threshold volumes used by control + safety. Pure domain — no
|
||
// Node-RED, no logger, no side effects beyond construction.
|
||
|
||
class BasinGeometry {
|
||
/**
|
||
* @param {object} basinConfig - { volume, height, inflowLevel, outflowLevel, overflowLevel }
|
||
* @param {object} hydraulicsConfig - { minHeightBasedOn: 'inlet' | 'outlet' }
|
||
*/
|
||
constructor(basinConfig, hydraulicsConfig) {
|
||
const volEmptyBasin = basinConfig.volume;
|
||
const heightBasin = basinConfig.height;
|
||
const inflowLevel = basinConfig.inflowLevel;
|
||
const outflowLevel = basinConfig.outflowLevel;
|
||
const overflowLevel = basinConfig.overflowLevel;
|
||
const inletPipeDiameter = basinConfig.inletPipeDiameter;
|
||
const outletPipeDiameter = basinConfig.outletPipeDiameter;
|
||
const minHeightBasedOn = hydraulicsConfig?.minHeightBasedOn;
|
||
|
||
const surfaceArea = volEmptyBasin / heightBasin;
|
||
|
||
// maxVol ≡ volEmptyBasin under the constant cross-section assumption;
|
||
// kept as a separate field for naming symmetry with the trigger volumes.
|
||
const maxVol = heightBasin * surfaceArea;
|
||
const maxVolAtOverflow = overflowLevel * surfaceArea;
|
||
const minVolAtOutflow = outflowLevel * surfaceArea;
|
||
const minVolAtInflow = inflowLevel * surfaceArea;
|
||
const minVol = minHeightBasedOn === 'inlet' ? minVolAtInflow : minVolAtOutflow;
|
||
|
||
this._volEmptyBasin = volEmptyBasin;
|
||
this._heightBasin = heightBasin;
|
||
this._inflowLevel = inflowLevel;
|
||
this._outflowLevel = outflowLevel;
|
||
this._overflowLevel = overflowLevel;
|
||
this._inletPipeDiameter = inletPipeDiameter;
|
||
this._outletPipeDiameter = outletPipeDiameter;
|
||
this._surfaceArea = surfaceArea;
|
||
this._maxVol = maxVol;
|
||
this._maxVolAtOverflow = maxVolAtOverflow;
|
||
this._minVolAtInflow = minVolAtInflow;
|
||
this._minVolAtOutflow = minVolAtOutflow;
|
||
this._minVol = minVol;
|
||
this._minHeightBasedOn = minHeightBasedOn;
|
||
}
|
||
|
||
get volEmptyBasin() { return this._volEmptyBasin; }
|
||
get heightBasin() { return this._heightBasin; }
|
||
get inflowLevel() { return this._inflowLevel; }
|
||
get outflowLevel() { return this._outflowLevel; }
|
||
get overflowLevel() { return this._overflowLevel; }
|
||
get inletPipeDiameter() { return this._inletPipeDiameter; }
|
||
get outletPipeDiameter() { return this._outletPipeDiameter; }
|
||
get surfaceArea() { return this._surfaceArea; }
|
||
get maxVol() { return this._maxVol; }
|
||
get maxVolAtOverflow() { return this._maxVolAtOverflow; }
|
||
get minVolAtInflow() { return this._minVolAtInflow; }
|
||
get minVolAtOutflow() { return this._minVolAtOutflow; }
|
||
get minVol() { return this._minVol; }
|
||
get minHeightBasedOn() { return this._minHeightBasedOn; }
|
||
|
||
/** Convert level (m from floor) → volume (m3). Negative levels clamp to 0. */
|
||
volumeFromLevel(level) {
|
||
return Math.max(level, 0) * this._surfaceArea;
|
||
}
|
||
|
||
/** Convert volume (m3) → level (m from floor). Negative volumes clamp to 0. */
|
||
levelFromVolume(volume) {
|
||
return Math.max(volume, 0) / this._surfaceArea;
|
||
}
|
||
|
||
/**
|
||
* Plain-object snapshot mirroring the legacy `this.basin` shape so
|
||
* getOutput / status code can keep using the same field names without
|
||
* caring whether it's holding a class instance or a plain object.
|
||
*/
|
||
snapshot() {
|
||
return {
|
||
volEmptyBasin: this._volEmptyBasin,
|
||
heightBasin: this._heightBasin,
|
||
inflowLevel: this._inflowLevel,
|
||
outflowLevel: this._outflowLevel,
|
||
overflowLevel: this._overflowLevel,
|
||
inletPipeDiameter: this._inletPipeDiameter,
|
||
outletPipeDiameter: this._outletPipeDiameter,
|
||
surfaceArea: this._surfaceArea,
|
||
maxVol: this._maxVol,
|
||
maxVolAtOverflow: this._maxVolAtOverflow,
|
||
minVolAtInflow: this._minVolAtInflow,
|
||
minVolAtOutflow: this._minVolAtOutflow,
|
||
minVol: this._minVol,
|
||
minHeightBasedOn: this._minHeightBasedOn,
|
||
};
|
||
}
|
||
}
|
||
|
||
module.exports = BasinGeometry;
|