- wiki/functional-description.md: rename Overfill Protection → High-volume
Safety; tighten basin-ordering chain; relocate level-based mode
diagrams under wiki/diagrams/modes/level-based/; document the new
flow.predicted.overflow.default position (replaces the previous
child='overflow' under position 'out'); add underflowVolume +
predictedUnderflowVolume entries.
- wiki/modes/{levelbased,powerbased}.md: paragraph cleanups.
- wiki/diagrams: move level-linear basin diagram under modes/level-based/
alongside a new level-log variant.
- simulations/run.js: add max_demand_gt expectation.
- simulations/scenarios/*: minor fixture updates.
- test/basic/nodeClass-config.test.js: new config-shape coverage.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5.7 KiB
title, mode, status, updated
| title | mode | status | updated |
|---|---|---|---|
| Level-based mode | levelbased | implemented | 2026-04-22 |
Level-based mode
The simplest and most widely deployed control strategy. Demand is a direct, static function of basin level — no feedback loop, no predictions beyond the level measurement itself. This page uses the shared basin model; see modes/README.md for the template other mode pages follow.
At a glance
| Item | Value |
|---|---|
| Signal driving demand | basin level (measured, predicted fallback) |
| Output | demand 0–100 % forwarded to every MGC child |
| Thresholds adjusted at runtime? | No — static from editor config |
| Use when | Inflow is sewer-gravity (no smart metering) and operator wants a predictable, inspectable response |
Diagram
Editable sources: ../diagrams/modes/level-based/basin-mode-level-linear.drawio.svg and ../diagrams/modes/level-based/basin-mode-level-log.drawio.svg (drag into draw.io — they round-trip).
Inputs
| Signal | Where from | Role |
|---|---|---|
| current level | measurement child (measured) → predicted from volume integrator (fallback) |
X-axis of the transfer function |
config.control.levelbased.minLevel |
editor, static | below → pumps hard OFF |
config.control.levelbased.startLevel |
editor, static | falling ramp reaches 0 % here; rising demand holds 0 % until the inlet level |
config.control.levelbased.maxLevel |
editor, static | where demand saturates at 100 % |
config.control.levelbased.curveType |
editor, static | linear or log; log is fast early response |
The three control thresholds plus curve type are the mode-specific configuration. Nothing here is recomputed at runtime.
Threshold policy
| Threshold | Source | Adjustable at runtime? |
|---|---|---|
minLevel |
config.control.levelbased.minLevel |
No |
startLevel |
config.control.levelbased.startLevel |
No |
maxLevel |
config.control.levelbased.maxLevel |
No |
curveType |
config.control.levelbased.curveType |
No |
That this policy is trivial (all static) is the defining simplicity of this mode. Modes like powerBased or future weather-aware variants will recompute these thresholds on the fly.
Demand formula
if level < minLevel:
demand = 0
MGC → turnOffAllMachines() # explicit shutdown, not just "0 %"
elif direction == filling:
demand = curve(level, [inflowLevel, maxLevel], [0 %, 100 %])
elif direction == draining:
demand = curve(level, [startLevel, maxLevel], [0 %, 100 %])
else:
demand = previous demand
Below the active lower ramp point, demand is 0 %. Above maxLevel, demand is 100 %. curve is either linear or logarithmic; the log variant rises faster early in the ramp. The MGC is free to distribute the demand across its pumps however its own policy dictates (equal split, lead-lag, staging — that's the MGC's business).
Edge cases
- Cold start with level in the dead zone.
demandhas no prior value; it defaults to0. Pumps stay OFF until the level first crossesstartLevelupward. Once it does, normal ramp-and-hold behaviour engages. - Level sensor drops out mid-run.
_selectBestNetFlowfalls back to predicted level (computed from the volume integrator) — the mode doesn't care which variant wins, it just reads the chosen level. - Both sensor and predictor unavailable. The mode's preconditions fail;
_controlLogiclogs a warning and exits without issuing a command. The last-known demand is held, which is safe. - Level crosses
maxLevelupward. Demand saturates at 100 %. Level may still continue rising if inflow > station capacity — this is the scenario that trips the overflow-safety layer (see below). - Level crosses
dryRunLeveldownward. The safety layer (not this mode) force-shuts all downstream pumps regardless of what demand the mode is commanding. The mode's demand is effectively overridden until level climbs back abovedryRunLevel + hysteresis_margin. - Level crosses
overflowLevelupward. The safety layer logs the spill event and raises an alarm. The mode continues commanding at 100 % — which is what you want, because the pumps should keep draining as fast as physically possible. (See functional description § Safety controller for the gravity-sewer caveat.)
Why this is worth migrating off of
Level-based is fine for steady-state sewer inflows. It has two known weaknesses:
- Predictable, not proactive. It can't pre-empty the basin ahead of a forecasted storm or a power-price peak. Modes like
weather-awareorpowerBasedcan — by movingstartLeveldown or up at runtime. - Thresholds assume pump capacity is fixed. If you add or remove pumps, the
startLevel ↔ maxLevelband that gave smooth 0-100 % coverage no longer matches the new capacity. Flow-based and percentage-based modes are less brittle to capacity changes because they close the loop on what you actually measure (outflow or fill %) rather than what you assume the level→capacity map is.
Related
- Functional description — basin model, net-flow selection, safety layer (shared across all modes)
- modes/README.md — mode index + template
- Other mode pages: to be written (
flowbased.md,pressurebased.md,percentagebased.md,powerbased.md,hybrid.md,manual.md)