150 lines
6.9 KiB
Markdown
150 lines
6.9 KiB
Markdown
|
|
---
|
|||
|
|
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 [eval harness](../../eval/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
|
|||
|
|
- [eval/README.md](../../eval/README.md) — where MPC evaluation scenarios will live
|