Files
pumpingStation/wiki/modes/mpc.md
znetsixe 3e13512a83 Rename eval/ → simulations/ and fix log-write bug
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>
2026-04-22 17:46:10 +02:00

7.0 KiB
Raw Blame History

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 1560 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.