### 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>
67 lines
2.7 KiB
JavaScript
67 lines
2.7 KiB
JavaScript
// Dry-run safety trip — manual mode, fixed high demand, zero inflow.
|
|
// Levelbased control would taper demand as the level drops (its ramp),
|
|
// stalling drainage before safety fires. Manual mode holds demand
|
|
// constant so the level actually reaches the dry-run threshold.
|
|
|
|
module.exports = {
|
|
name: 'safety-dry-run-trip',
|
|
description: 'Manual mode, constant 100 % demand, zero inflow; expect safety to force-stop downstream pumps when level reaches the dry-run threshold.',
|
|
durationSec: 600,
|
|
|
|
config: {
|
|
general: { name: 'EvalDryRun', id: 'eval-dry-run', 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: 'manual',
|
|
allowedModes: new Set(['levelbased', 'manual']),
|
|
levelbased: { minLevel: 0.5, startLevel: 2, maxLevel: 4 },
|
|
},
|
|
safety: {
|
|
enableDryRunProtection: true,
|
|
dryRunThresholdPercent: 50,
|
|
enableOverfillProtection: false,
|
|
overfillThresholdPercent: 98,
|
|
timeleftToFullOrEmptyThresholdSeconds: 0,
|
|
},
|
|
},
|
|
|
|
setup: async (ps) => {
|
|
const MAX_OUTFLOW = 0.04;
|
|
let mgcRunning = true; // gets toggled by safety's shutdown call
|
|
ps.machineGroups['mgc1'] = {
|
|
config: { general: { name: 'mgc1', id: 'mgc1' }, functionality: { positionVsParent: 'downstream' } },
|
|
turnOffAllMachines: () => {
|
|
mgcRunning = false;
|
|
ps.measurements.type('flow').variant('predicted').position('out').child('mgc1').value(0, Date.now(), 'm3/s');
|
|
},
|
|
handleInput: async (_src, demand) => {
|
|
if (!mgcRunning) return;
|
|
const d = Math.max(0, Math.min(100, Number(demand) || 0));
|
|
ps.measurements.type('flow').variant('predicted').position('out').child('mgc1').value((d / 100) * MAX_OUTFLOW, Date.now(), 'm3/s');
|
|
},
|
|
};
|
|
// Need a downstream machine for safety to shut down
|
|
ps.machines['pump1'] = {
|
|
config: { general: { name: 'pump1', id: 'pump1' }, functionality: { positionVsParent: 'downstream' } },
|
|
_isOperationalState: () => mgcRunning,
|
|
handleInput: async (_src, action) => {
|
|
if (action === 'execSequence') mgcRunning = false;
|
|
},
|
|
};
|
|
ps.calibratePredictedLevel(2.5);
|
|
},
|
|
|
|
inputs: (t, ps) => {
|
|
ps.setManualInflow(0, Date.now(), 'm3/s');
|
|
if (ps.mode === 'manual') ps.forwardDemandToChildren(100);
|
|
},
|
|
|
|
expectations: [
|
|
{ name: 'safety engages at some point', type: 'safety_trips_gt', value: 0 },
|
|
{ name: 'level never goes below outflow pipe', type: 'min_level_bounded', value: 0.2 },
|
|
],
|
|
};
|