Files
pumpingStation/eval/scenarios/levelbased-storm.js
znetsixe 66fd3feff8 Add eval harness + Tier 2/3 mode template pages
### 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>
2026-04-22 16:49:41 +02:00

61 lines
2.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Storm surge — inflow triples briefly, pumps should saturate at 100%,
// level rises toward overflow then recedes.
//
// Expectation: during the surge (t=300..600), demand reaches 100% and
// level may transiently climb above maxLevel. Overflow safety should
// fire if the surge overwhelms pump capacity; dry-run should not fire.
module.exports = {
name: 'levelbased-storm',
description: 'Sewer inflow triples from 0.008 → 0.024 m³/s for 5 minutes then returns to baseline. Overfill safety may engage.',
durationSec: 1500,
config: {
general: { name: 'EvalStorm', id: 'eval-storm', unit: 'm3/h',
logging: { enabled: false, logLevel: 'error' } },
functionality: { softwareType: 'pumpingStation', role: 'stationcontroller', positionVsParent: 'atEquipment' },
basin: { volume: 50, height: 5, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 4.5 },
hydraulics: { refHeight: 'NAP', basinBottomRef: 0, minHeightBasedOn: 'outlet' },
control: {
mode: 'levelbased',
allowedModes: new Set(['levelbased']),
levelbased: { minLevel: 1, startLevel: 2, maxLevel: 4 },
},
safety: {
enableDryRunProtection: true,
dryRunThresholdPercent: 2,
enableOverfillProtection: true,
overfillThresholdPercent: 95,
timeleftToFullOrEmptyThresholdSeconds: 0,
},
},
setup: async (ps) => {
const MAX_OUTFLOW = 0.012; // m³/s pumps cannot keep up with 3× surge
ps.machineGroups['mgc1'] = {
config: { general: { name: 'mgc1' } },
turnOffAllMachines: () => {
ps.measurements.type('flow').variant('predicted').position('out').child('mgc1').value(0, Date.now(), 'm3/s');
},
handleInput: async (_src, demand) => {
const d = Math.max(0, Math.min(100, Number(demand) || 0));
const outflow = (d / 100) * MAX_OUTFLOW;
ps.measurements.type('flow').variant('predicted').position('out').child('mgc1').value(outflow, Date.now(), 'm3/s');
},
};
ps.calibratePredictedLevel(2.5);
},
inputs: (t, ps) => {
const surge = (t >= 300 && t < 600) ? 0.024 : 0.008;
ps.setManualInflow(surge, Date.now(), 'm3/s');
},
expectations: [
{ name: 'dry-run never trips', type: 'end_state_eq', field: 'safetyActive', value: false },
// Level may exceed maxLevel transiently but must stay under basinHeight
{ name: 'level never breaches physical basin', type: 'max_level_bounded', value: 5.0 },
{ name: 'demand saturates at 100% during surge', type: 'max_demand_bounded', value: 100 },
],
};