### 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>
3.8 KiB
3.8 KiB
title, mode, tier, status, updated
| title | mode | tier | status | updated |
|---|---|---|---|---|
| Flow-based mode | flowbased | 2 | placeholder | 2026-04-22 |
Flow-based mode — Tier 2 template
Status — not yet implemented. The
flowbasedentry is a placeholder in_controlLogic. This page reserves the shape and documents the intended design so all Tier-2 modes share the same layout.
At a glance
| Item | Value |
|---|---|
| Tier | 2 — parameterised transfer function |
| Signal driving demand | measured outflow (actual pumps) |
| Secondary inputs | integrator + derivative state (for PID) |
| Output | demand 0–100 % via PID correction |
| Thresholds adjusted at runtime? | No (but the demand can move independently of level) |
| Use when | The station has a flow sensor on the outlet and you want to hold a target outflow rate regardless of basin level |
Diagram
Primary plot. Demand vs outflow-error (not level!) is the meaningful transfer function for flow-based control. The curve is a classic PID surface — proportional slope times error, plus integral + derivative terms.
Secondary plot. Level still enters as gates (STOP below minLevel, don't overfill above maxLevel) — same thresholds as levelbased, but the mode doesn't use level to pick demand.
Placeholder image — replace with:
diagrams/modes/flowbased.drawio.svg (demand vs outflow-error, showing Kp slope)
Inputs
| Signal | Where from | Role |
|---|---|---|
| measured outflow | sum of flow.measured.* at outflow positions |
error = (flowSetpoint − measuredOutflow) |
config.control.flowBased.flowSetpoint |
editor, static | target outflow in m³/h |
config.control.flowBased.flowDeadband |
editor, static | zone around setpoint where PID output holds |
config.control.flowBased.pid.{kp, ki, kd, ...} |
editor / schema | PID gains + rate limits |
| current level | fallback → threshold gates | only used for minLevel/maxLevel bounds |
Threshold policy
The control thresholds (minLevel, startLevel, maxLevel) are still enforced but for different reasons than levelbased:
| Threshold | Role in flowbased |
|---|---|
minLevel |
If level drops below, force demand=0 regardless of PID output (prevents pump undercut) |
startLevel |
unused — demand is driven by error, not level |
maxLevel |
If level climbs above, force demand=100 regardless of PID output (prevents spill) |
Demand formula
error = flowSetpoint − measuredOutflow
if level < minLevel:
demand = 0 # pump-undercut guard
elif level > maxLevel:
demand = 100 # anti-spill guard
else:
# normal PID branch
P = Kp × error
I += Ki × error × dt # with anti-windup clamp
D = Kd × d(error)/dt # with low-pass filter
demand = clamp(P + I + D, 0, 100) # with rate limits Δup/Δdown
Edge cases
- Cold start, no prior outflow measurement. PID state starts at 0; first error is
flowSetpoint. Integral term will build up — rate-limit the demand ramp to avoid over-shoot. - Sensor dropout on the outflow meter. Fall back to predicted outflow (sum of pump curve predictions). Log a warning — PID on predicted-only is unreliable.
- Setpoint step change. PID with derivative filter + rate limits handles this gracefully; without filter, the D-kick would saturate output.
- Safety layer interaction. Same as levelbased —
dryRunLevelandoverflowLeveloverride the PID output. See functional description § Safety.
Related
- Functional description — basin model + shared safety layer
- modes/README.md — mode index + page template
- modes/levelbased.md — Tier 1 reference implementation