61 lines
2.5 KiB
JavaScript
61 lines
2.5 KiB
JavaScript
|
|
// 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 },
|
|||
|
|
],
|
|||
|
|
};
|