### 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>
84 lines
3.8 KiB
Markdown
84 lines
3.8 KiB
Markdown
---
|
||
title: Flow-based mode
|
||
mode: flowbased
|
||
tier: 2
|
||
status: placeholder
|
||
updated: 2026-04-22
|
||
---
|
||
|
||
# Flow-based mode — *Tier 2 template*
|
||
|
||
> **Status — not yet implemented.** The `flowbased` entry 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
|
||
|
||
```text
|
||
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 — `dryRunLevel` and `overflowLevel` override the PID output. See [functional description § Safety](../functional-description.md#safety-controller).
|
||
|
||
## Related
|
||
|
||
- [Functional description](../functional-description.md) — basin model + shared safety layer
|
||
- [modes/README.md](README.md) — mode index + page template
|
||
- [modes/levelbased.md](levelbased.md) — Tier 1 reference implementation
|