--- title: MPC (Model-Predictive Control) mode: mpc tier: 3 status: placeholder updated: 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](../../simulations/README.md) 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: ```text 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](../functional-description.md) — basin model + safety layer - [modes/levelbased.md](levelbased.md) — Tier 1 — the "default" MPC falls back to - [modes/powerbased.md](powerbased.md) — Tier 2 — MPC generalises the clip idea into full optimisation - [simulations/README.md](../../simulations/README.md) — where MPC simulation scenarios will live