### 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>
6.9 KiB
title, mode, tier, status, updated
| title | mode | tier | status | updated |
|---|---|---|---|---|
| MPC (Model-Predictive Control) | mpc | 3 | placeholder | 2026-04-22 |
MPC mode — Tier 3 template
Status — not yet implemented. Not even in the schema today. This page reserves the shape for when the time comes.
Why this is Tier 3
The levelbased/flowbased/powerBased modes are all memoryless or near-memoryless transfer functions. You give them the current state; they give you a demand. You can draw them as 2D plots.
MPC is different. At each tick the controller solves an optimisation over a prediction horizon:
minimise Σ cost(state(t+k), command(t+k)) for k = 0 .. N
subject to forecast, physical limits, power budget, spill penalty, ...
The command that's emitted at time t is merely the first step of that plan; next tick the forecast shifts and the optimiser re-runs. There's no fixed demand = f(level) curve — the curve is remade every tick.
That's why Tier-3 modes get block diagrams + scenario time-series, not transfer functions.
At a glance
| Item | Value |
|---|---|
| Tier | 3 — optimisation-based |
| Signal driving demand | full state (level, flow, power) + forecasts (inflow, grid price, weather) |
| Secondary inputs | cost weights, horizon length, solver config |
| Output | demand + planned trajectory over horizon |
| Thresholds adjusted at runtime? | Effectively yes — the optimiser treats them as soft constraints |
| Use when | Available forecasts beat reactive control, or multi-objective optimisation is needed |
Diagram 1 — signal flow (block diagram)
Placeholder image — replace with:
diagrams/modes/mpc-block.drawio.svg
Blocks:
[sensors] [inflow forecast] [grid price] [weather API]
│ │ │ │
└─────────────┴──────────────────┴──────────────┘
│
┌─────▼──────┐
│ state + │
│ forecast │
│ bundle │
└─────┬──────┘
│
┌─────▼───────────────────┐
│ MPC solver │
│ • horizon N │
│ • cost weights w │
│ • constraints C │
│ • linearised model │
└─────┬───────────────────┘
│
┌─────▼───────┐
│ command[0] │ ── the step we act on now
│ command[1] │
│ ... │
│ command[N] │ ── re-planned next tick
└─────┬───────┘
│
┌─────────▼─────────┐
│ safety layer clip │ ← dryRun / overflow always apply
└─────────┬─────────┘
│
demand → MGC
Diagram 2 — scenario time-series
A much more useful way to evaluate MPC is to plot what it did over a simulated scenario: level, planned vs actual demand, the cost function breakdown, the active constraints. The eval harness is built for exactly this — MPC will need a dedicated scenario like mpc-storm-with-forecast.js.
Placeholder — replace with:
diagrams/modes/mpc-scenario.drawio.svg
Stacked time-series showing:
1. basin level over time (with forecast shadow and horizon)
2. demand over time (with the re-planning edges visible)
3. cost breakdown: energy vs spill-penalty vs ramp-penalty
4. active constraints over time (colored bands)
Inputs
| Signal | Where from | Role |
|---|---|---|
| current state | measurements container |
initial condition for optimiser |
| inflow forecast | external — sewer model / weather API | drives the cost integral |
| grid-price forecast | external — market feed / schedule | weights energy cost |
cost weights w |
config | trades off spill vs energy vs ramp |
horizon N |
config | 15–60 minutes typical |
| model parameters | config / learned | basin dynamics, pump curves |
Threshold policy
Levels appear in the optimiser as soft constraints (penalties in the cost function):
| Threshold | Role in MPC |
|---|---|
dryRunLevel, overflowLevel |
hard constraints — if the optimiser's plan crosses them, safety layer clips |
minLevel, maxLevel |
soft constraints — penalty weight w_level applied to excursions |
startLevel |
advisory only — optimiser doesn't inherently care, but may be used in cost weights for rule-of-thumb alignment with human expectations |
So unlike Tier-1/2 where thresholds directly gate the action, here they shape the objective.
Demand formula
Not a formula — an optimisation problem:
state, forecast, constraints = gather_inputs()
plan = mpc_solver.solve(
state0 = state,
forecast = forecast,
horizon = N,
model = basin_dynamics + pump_curves,
cost = w_energy × Σ power(k)
+ w_spill × Σ max(0, level(k) − overflowLevel)²
+ w_undercut × Σ max(0, minLevel − level(k))²
+ w_ramp × Σ (command(k) − command(k-1))²,
constraints = pump_limits + power_budget + rate_limits,
)
demand = plan.command[0]
Edge cases
- Solver timeout. Fall back to the previous plan's step, or to a levelbased curve as a safe default. Log.
- Bad forecast (persistent bias). Optimiser can chase a wrong prediction for many ticks. Adaptive forecast bias correction, or a watchdog comparing forecast-vs-realised, is essential.
- Infeasibility. If constraints can't be satisfied (e.g. power budget and maxLevel simultaneously during a severe storm), relax soft constraints in priority order (ramp first, then maxLevel, then energy) — never relax dryRun/overflow.
- Safety takeover. The safety layer still overrides. MPC should anticipate safety trips in its cost function (big penalty for trajectories that invoke them), not hit them.
Related
- Functional description — basin model + safety layer
- modes/levelbased.md — Tier 1 — the "default" MPC falls back to
- modes/powerbased.md — Tier 2 — MPC generalises the clip idea into full optimisation
- eval/README.md — where MPC evaluation scenarios will live