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>
7.0 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 simulations 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
- simulations/README.md — where MPC simulation scenarios will live