### P1 — match diagram naming (labels only, no schema change)
- "Inlet Elevation" → "Inlet (bottom of pipe, m)"
- "Outlet Elevation" → "Outlet (top of pipe, m)"
- "Overflow Level" → "Overflow (weir crest, m)"
- "Basin Bottom (m Refheight)" → "Basin floor above datum (m)"
- Added a one-line banner at the top of Basin Geometry:
"All heights measured from the basin floor (0 m)."
These map directly to the clarifications added to basin-model.drawio.svg
so editor and diagram speak the same vocabulary.
### P3 — live derived safety levels next to the % fields
Low/High Volume Threshold fields now show the resulting trip level
live as the operator types:
Low Volume Threshold (%) [ 2 ] → dryRunLevel ≈ 0.21 m
High Volume Threshold (%) [ 98 ] → overfillLevel ≈ 4.41 m
Recomputed on every input/change of outflowLevel, inflowLevel,
overflowLevel, minHeightBasedOn, or either %. Pure UI feedback —
no schema change, no save-side change, same formulas as
specificClass._validateThresholdOrdering().
Also includes the user's latest basin-model.drawio.svg update
(inlet=bottom/outlet=top labels + datum annotation).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per discussion: "test" and "eval" overlap in meaning; "simulations"
is more honest about what's actually happening — scripted plant
inputs driving a physics sim, then recorded for analysis.
Rename scope:
- eval/ → simulations/ (tracked as git renames)
- Internal references in run.js and README.md updated
- wiki/modes/mpc.md link updated
Also fixes a log-write bug noticed during the rename:
- run.js didn't mkdir simulations/logs/ before createWriteStream,
so the stream opened into a potentially non-existent dir and the
file never materialised. Added fs.mkdirSync(..., recursive:true).
- end() wasn't awaited, so the process could exit before the stream
flushed. Now awaits the 'finish' event. Confirmed: 1200 records
actually land in simulations/logs/<scenario>.jsonl.
- Added simulations/logs/.gitignore so future JSONL artefacts stay
out of the repo but the dir remains tracked.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
### eval/ (scenario-based evaluation)
Complements the unit tests under test/basic. Scenarios fluctuate inputs
over simulated time, record every tick to JSONL, print a summary
table + event log, and check expectations. Complementary to unit
tests — these answer "how does the system respond to this input
profile" rather than "is this function correct".
- eval/run.js — driver; monkey-patches Date.now so the
volume integrator ticks at 1 s/iter
regardless of wall-clock
- eval/scenarios/ — one file per scenario
- levelbased-steady.js — constant inflow, demand converges
- levelbased-storm.js — inflow surge, demand saturates
- safety-dry-run-trip.js — manual mode, empty basin, safety trips
- eval/formatters/table.js — ASCII summary of sampled ticks
- eval/logs/ — per-scenario JSONL output (one line per tick)
- eval/README.md — usage + scenario file shape + how to pipe
into InfluxDB/Grafana
All three starter scenarios PASS with their expectations.
### wiki/modes/ (tier template pages)
The levelbased page templated Tier-1 modes (static transfer function).
Added worked examples for the other two tiers so all mode pages share
a common skeleton and new modes have something concrete to imitate:
- flowbased.md — Tier 2 (PID on measured outflow)
- powerbased.md — Tier 2 (levelbased curve clipped by grid power budget)
- mpc.md — Tier 3 (optimisation + forecast; block diagram +
scenario time-series instead of a fixed curve)
- modes/README.md — updated with the three-tier classification table
and diagram-type-per-tier guidance
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
### Guardrails (specificClass.js)
New _validateThresholdOrdering() runs in the constructor. Checks every
ordered pair of basin + control + derived-safety levels and logs a
warning for each violation; returns the list as this.thresholdIssues
so tests and the eval harness can inspect. Non-fatal — we prefer a
running-but-warned station to a refusal-to-start (availability-first).
Strict invariants (bottom → top):
0 < outflowLevel < inflowLevel < overflowLevel ≤ basinHeight
dryRunLevel ≤ minLevel ≤ startLevel < maxLevel ≤ overfillLevel
Uses a list-of-checks pattern rather than a switch — easier to add new
invariants without reflowing cases, and the list itself is readable
documentation.
### Bug fix (specificClass.js)
calibratePredictedLevel was writing the volume value into the LEVEL
slot. Root cause: MeasurementContainer is stateful — its type()/
variant()/position() calls mutate the container's own cursor, so
caching chain references (const levelChain = ...; const volumeChain
= ...) doesn't isolate them. The second cached chain ended up sharing
the state of the last type() call. Rebuilt chains fresh each time,
matching the calibratePredictedVolume pattern that already worked.
### Tests (test/basic/specificClass.test.js)
Ported from Jest to node:test + node:assert — the project's standard
per .claude/rules/testing.md. Deleted the stale test/specificClass.test.js
(tests referenced methods that no longer exist post-rename).
New coverage, 42 passing subtests:
- Basin geometry derivations + minHeightBasedOn
- Level/volume roundtrip
- Threshold guardrails (5 violation cases)
- Direction derivation
- Mode change accept/reject
- Calibration (volume and level paths — catches the bug above)
- Levelbased control zones (STOP / DEAD ZONE / RAMP / saturate)
- getOutput flattening
- setManualInflow
Run with: node --test test/basic/*.test.js
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Aligns the code with the 5-threshold convention used throughout the
wiki (basin model + per-mode transfer-function diagrams):
heightInlet → inflowLevel
heightOutlet → outflowLevel
heightOverflow → overflowLevel
stopLevel → minLevel
maxFlowLevel → maxLevel
minFlowLevel → removed (collapsed into startLevel; they were
always supposed to hold the same value)
minVolIn → minVolAtInflow
minVolOut → minVolAtOutflow
maxVolOverflow → maxVolAtOverflow
startLevel → unchanged
Config schema (generalFunctions/src/configs/pumpingStation.json) is
updated in a parallel commit in that submodule.
Also:
- Stripped the ~150-line ASCII basin diagram from initBasinProperties
JSDoc; it now points at wiki/functional-description.md#basin-model.
- Trimmed the top-of-class JSDoc — the config-sections breakdown was
drifting from the schema anyway; wiki is now the source of truth.
- Tidied inline comments in _controlLevelBased, _scaleLevelToFlowPercent.
- Editor order reshuffled to match the bottom→top basin order:
minLevel, startLevel, maxLevel.
Breaking change for saved flows: existing pumpingStation nodes in
production flows reference the old field names and will need to be
re-entered in the editor. No compat shim — node is RnD/trial.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Introduces the pattern: basin model is the shared canvas (mode-agnostic
physics); each control mode is its own page under wiki/modes/ plus a
demand-vs-level transfer-function diagram under wiki/diagrams/modes/.
- wiki/modes/README.md — index + per-mode page template (inputs,
threshold policy, demand formula, edge cases, related)
- wiki/modes/levelbased.md — first worked example using the new naming
convention (dryRunLevel / minLevel / startLevel / maxLevel /
overflowLevel). Forward-looking — the code still uses the old names
until the pending rename refactor.
- wiki/diagrams/modes/levelbased.drawio.svg — transfer-function plot
(zones: STOP / DEAD ZONE / RAMP / SATURATE, safety trips outside the
plot). Round-trippable via embedded drawio XML.
- functional-description.md — replaced the inline levelbased/manual
subsection with a table pointing at the modes/ pages. Removed the
old control-zones ASCII diagram reference (superseded by the
per-mode transfer function).
- wiki/README.md — added Control modes entry + diagrams/modes/ pointer.
The remaining placeholder modes (flowbased, pressureBased,
percentageBased, powerBased, hybrid, manual) can each fill in the
template independently.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each <name>.drawio.svg now has the corresponding <name>.drawio XML
embedded as content="..." on the root <svg> element. Opening the
SVG in draw.io (File → Open, or drag-drop) loads the full editable
model — no need to keep the .drawio file around for editing.
Updated diagrams/README.md to reflect that both file formats are
now round-trippable from the start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Moves documentation into the code repo so code, docs, and diagrams
version-lock and review together. Previous location was
pumpingStation.wiki.git; that will shrink to a pointer.
Contents:
- wiki/README.md — doc index
- wiki/functional-description.md — operator-facing reference derived
from src/specificClass.js: basin model, net-flow selection,
level-based control zones, safety interlocks, registration topology
- wiki/diagrams/ — editable draw.io sources paired with SVG exports
(basin-model, control-zones, safety-rules) + README with the
open/edit/export/commit workflow
The .drawio files are rough starters; iterate in draw.io and re-export.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two hard rules for the safety controller, matching sewer PS design:
1. BELOW stopLevel (dry-run): pumps CANNOT start.
All downstream equipment shut down. safetyControllerActive=true
blocks _controlLogic so level control can't restart pumps.
Only manual override or emergency can change this.
2. ABOVE overflow level (overfill): pumps CANNOT stop.
Only UPSTREAM equipment is shut down (stop more water coming in).
Machine groups (downstream pumps) are NOT shut down — they must
keep draining. safetyControllerActive is NOT set, so _controlLogic
continues commanding pumps at the demand dictated by the level
curve (which is >100% near overflow = all pumps at maximum).
Only manual override or emergency stop can shut pumps during
an overfill event.
Previously the overfill branch called turnOffAllMachines() on machine
groups AND set safetyControllerActive=true, which shut down the pumps
and blocked level control from restarting them — exactly backwards
for a sewer pumping station where the sewage keeps coming.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Previously PS only sent demand to MGC when level > startLevel AND
direction === 'filling'. Between startLevel and stopLevel (the 'dead
zone'), pumps kept running at their last commanded setpoint with no
updates. Basin drained uncontrolled until hitting stopLevel.
Fix: send percControl on every tick when level > stopLevel. The
_scaleLevelToFlowPercent math naturally gives:
- Positive % above startLevel (pumps ramp up)
- 0% at exactly startLevel (pumps at minimum)
- Negative % below startLevel → clamped to 0 → MGC scales to 0
→ pumps ramp down gracefully
This creates smooth visible ramp-up and ramp-down as the basin fills
and drains, instead of a sudden jump at startLevel and stuck ctrl in
the dead zone.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Three bugs in registerChild caused multi-counted outflow in _updatePredictedVolume:
1. machinegroup registered twice (line 66 + line 70 both called
_registerPredictedFlowChild). Fixed: only register in the
machinegroup branch.
2. Individual machines registered alongside their machinegroup parent.
Each pump's predicted flow is already included in MGC's aggregated
total — subscribing to both triple-counts. Fixed: only register
individual machines when no machinegroup is present (direct-wired
pumps without MGC).
3. _registerPredictedFlowChild subscribed to BOTH flow.predicted.downstream
AND flow.predicted.atequipment events. These carry the same total flow
on two event names — the handler wrote the value twice per tick.
Fixed: subscribe to ONE event per child (downstream for outflow,
upstream for inflow).
These are generalizable patterns:
- When a group aggregator exists, subscribe to IT, not its children.
- One event per measurement type per child — pick the most specific.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Two additions to pumpingStation:
1. _controlLevelBased now calls _applyMachineGroupLevelControl in
addition to _applyMachineLevelControl when the basin is filling
above startLevel. Previously only direct-child machines received
the level-based percent-control signal; in a hierarchical topology
(PS → MGC → pumps) the machines sit under MGC and PS.machines is
empty, so the level control never reached them.
2. New 'Qd' input topic + forwardDemandToChildren() method. When PS
is in 'manual' mode (matching the pattern from rotatingMachine's
virtualControl), operator demand from a dashboard slider is forwarded
to all child machine groups and direct machines. When PS is in any
other mode (levelbased, flowbased, etc.), the Qd msg is silently
dropped with a debug log so the automatic control isn't overridden.
No breaking changes — existing flows that don't send 'Qd' are unaffected,
and _controlLevelBased's additional call to machineGroupLevelControl
is a no-op when no machine groups are registered.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
References the flow-layout rule set in the EVOLV superproject
(.claude/rules/node-red-flow-layout.md) so Claude Code sessions working
in this repo know the S88 level, colour, and placement lane for this node.
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Prevents demo code from executing when module is required by Node-RED,
which caused crashes due to missing measurement data.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Fix method name mismatch in tick() that called non-existent _calcTimeRemaining
instead of _calcRemainingTime. Add 27 unit tests for specificClass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replace hardcoded position strings with POSITIONS.* constants.
Prefix unused variables with _ to resolve no-unused-vars warnings.
Fix no-prototype-builtins where applicable.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Replaces manual base config construction with shared buildConfig() method.
Node now only specifies domain-specific config sections.
Part of #1: Extract base config schema
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>