2026-04-22 16:38:41 +02:00
|
|
|
|
// Basic unit tests for PumpingStation (domain logic, no Node-RED).
|
|
|
|
|
|
// Run with: node --test test/basic/specificClass.test.js
|
|
|
|
|
|
|
|
|
|
|
|
const test = require('node:test');
|
|
|
|
|
|
const assert = require('node:assert/strict');
|
|
|
|
|
|
|
|
|
|
|
|
const PumpingStation = require('../../src/specificClass');
|
|
|
|
|
|
|
|
|
|
|
|
// Standard config shape. Override any section by passing { section: {...} }.
|
|
|
|
|
|
function makeConfig(overrides = {}) {
|
|
|
|
|
|
const base = {
|
|
|
|
|
|
general: {
|
|
|
|
|
|
name: 'TestStation',
|
|
|
|
|
|
id: 'ps-test',
|
|
|
|
|
|
unit: 'm3/h',
|
|
|
|
|
|
logging: { enabled: false, logLevel: 'error' },
|
|
|
|
|
|
flowThreshold: 1e-4,
|
|
|
|
|
|
},
|
|
|
|
|
|
functionality: {
|
|
|
|
|
|
softwareType: 'pumpingStation',
|
|
|
|
|
|
role: 'stationcontroller',
|
|
|
|
|
|
positionVsParent: 'atEquipment',
|
|
|
|
|
|
},
|
|
|
|
|
|
basin: {
|
|
|
|
|
|
volume: 50,
|
|
|
|
|
|
height: 5,
|
|
|
|
|
|
inflowLevel: 3,
|
|
|
|
|
|
outflowLevel: 0.2,
|
|
|
|
|
|
overflowLevel: 4.5,
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
inletPipeDiameter: 0.4,
|
|
|
|
|
|
outletPipeDiameter: 0.3,
|
2026-04-22 16:38:41 +02:00
|
|
|
|
},
|
|
|
|
|
|
hydraulics: {
|
|
|
|
|
|
refHeight: 'NAP',
|
|
|
|
|
|
basinBottomRef: 0,
|
|
|
|
|
|
minHeightBasedOn: 'outlet',
|
|
|
|
|
|
},
|
|
|
|
|
|
control: {
|
|
|
|
|
|
mode: 'levelbased',
|
|
|
|
|
|
allowedModes: new Set(['levelbased', 'manual']),
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
levelbased: { minLevel: 1, startLevel: 2, maxLevel: 4, curveType: 'linear', logCurveFactor: 9 },
|
2026-04-22 16:38:41 +02:00
|
|
|
|
},
|
|
|
|
|
|
safety: {
|
|
|
|
|
|
enableDryRunProtection: false,
|
|
|
|
|
|
enableOverfillProtection: false,
|
|
|
|
|
|
dryRunThresholdPercent: 2,
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
highVolumeSafetyThresholdPercent: 98,
|
2026-04-22 16:38:41 +02:00
|
|
|
|
overfillThresholdPercent: 98,
|
|
|
|
|
|
timeleftToFullOrEmptyThresholdSeconds: 0,
|
|
|
|
|
|
},
|
|
|
|
|
|
};
|
|
|
|
|
|
for (const k of Object.keys(overrides)) {
|
|
|
|
|
|
base[k] = typeof overrides[k] === 'object' && !Array.isArray(overrides[k])
|
|
|
|
|
|
? { ...base[k], ...overrides[k] }
|
|
|
|
|
|
: overrides[k];
|
|
|
|
|
|
}
|
|
|
|
|
|
return base;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
test('Basin geometry — derived values', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('surfaceArea = volume / height', () => {
|
|
|
|
|
|
assert.equal(ps.basin.surfaceArea, 10); // 50 / 5
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('maxVol = height × area ≡ volEmptyBasin', () => {
|
|
|
|
|
|
assert.equal(ps.basin.maxVol, 50);
|
|
|
|
|
|
assert.equal(ps.basin.maxVol, ps.basin.volEmptyBasin);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('maxVolAtOverflow = overflowLevel × area', () => {
|
|
|
|
|
|
assert.equal(ps.basin.maxVolAtOverflow, 45); // 4.5 × 10
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('minVolAtInflow = inflowLevel × area', () => {
|
|
|
|
|
|
assert.equal(ps.basin.minVolAtInflow, 30); // 3 × 10
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('minVolAtOutflow = outflowLevel × area', () => {
|
|
|
|
|
|
assert.ok(Math.abs(ps.basin.minVolAtOutflow - 2) < 1e-9); // 0.2 × 10
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('minVol honours minHeightBasedOn=outlet', () => {
|
|
|
|
|
|
assert.ok(Math.abs(ps.basin.minVol - 2) < 1e-9);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('minVol honours minHeightBasedOn=inlet', () => {
|
|
|
|
|
|
const ps2 = new PumpingStation(makeConfig({ hydraulics: { minHeightBasedOn: 'inlet' } }));
|
|
|
|
|
|
assert.equal(ps2.basin.minVol, 30);
|
|
|
|
|
|
});
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
await t.test('pipe diameters are part of basin contract', () => {
|
|
|
|
|
|
assert.equal(ps.basin.inletPipeDiameter, 0.4);
|
|
|
|
|
|
assert.equal(ps.basin.outletPipeDiameter, 0.3);
|
|
|
|
|
|
});
|
2026-04-22 16:38:41 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Level ↔ volume roundtrip', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('_calcVolumeFromLevel multiplies by area', () => {
|
|
|
|
|
|
assert.equal(ps._calcVolumeFromLevel(2), 20);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('_calcVolumeFromLevel clamps negatives to 0', () => {
|
|
|
|
|
|
assert.equal(ps._calcVolumeFromLevel(-3), 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('_calcLevelFromVolume divides by area', () => {
|
|
|
|
|
|
assert.equal(ps._calcLevelFromVolume(20), 2);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('_calcLevelFromVolume clamps negatives to 0', () => {
|
|
|
|
|
|
assert.equal(ps._calcLevelFromVolume(-10), 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('roundtrip preserves level', () => {
|
|
|
|
|
|
const v = ps._calcVolumeFromLevel(2.7);
|
|
|
|
|
|
assert.ok(Math.abs(ps._calcLevelFromVolume(v) - 2.7) < 1e-10);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Threshold guardrails — _validateThresholdOrdering', async (t) => {
|
|
|
|
|
|
await t.test('valid config returns no issues', () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
assert.equal(ps.thresholdIssues.length, 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('minLevel > startLevel flagged', () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
control: {
|
|
|
|
|
|
mode: 'levelbased',
|
|
|
|
|
|
allowedModes: new Set(['levelbased']),
|
|
|
|
|
|
levelbased: { minLevel: 3, startLevel: 2, maxLevel: 4 },
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
assert.ok(ps.thresholdIssues.some((i) => i.aName === 'minLevel'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('startLevel == maxLevel flagged (must be strict <)', () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
control: {
|
|
|
|
|
|
mode: 'levelbased',
|
|
|
|
|
|
allowedModes: new Set(['levelbased']),
|
|
|
|
|
|
levelbased: { minLevel: 1, startLevel: 4, maxLevel: 4 },
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
assert.ok(ps.thresholdIssues.some((i) => i.aName === 'startLevel'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
await t.test('startLevel > inflowLevel flagged for levelbased rising hold zone', () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
control: {
|
|
|
|
|
|
mode: 'levelbased',
|
|
|
|
|
|
allowedModes: new Set(['levelbased']),
|
|
|
|
|
|
levelbased: { minLevel: 1, startLevel: 3.5, maxLevel: 4, curveType: 'linear' },
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
assert.ok(ps.thresholdIssues.some((i) => i.aName === 'startLevel' && i.bName === 'inflowLevel'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-22 16:38:41 +02:00
|
|
|
|
await t.test('outflowLevel >= inflowLevel flagged', () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
basin: { volume: 50, height: 5, inflowLevel: 0.1, outflowLevel: 0.5, overflowLevel: 4.5 },
|
|
|
|
|
|
}));
|
|
|
|
|
|
assert.ok(ps.thresholdIssues.some((i) => i.aName === 'outflowLevel'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('overflowLevel > basinHeight flagged', () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
basin: { volume: 50, height: 5, inflowLevel: 3, outflowLevel: 0.2, overflowLevel: 6 },
|
|
|
|
|
|
}));
|
|
|
|
|
|
assert.ok(ps.thresholdIssues.some((i) => i.aName === 'overflowLevel'));
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('dryRunLevel > minLevel flagged (safety band inverted)', () => {
|
|
|
|
|
|
// With minHeightBasedOn=inlet, refLowLevel=inflowLevel=3.
|
|
|
|
|
|
// dryRunLevel = 3 × (1 + 100/100) = 6; minLevel=1 → 6 ≤ 1 fails.
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
hydraulics: { minHeightBasedOn: 'inlet' },
|
|
|
|
|
|
safety: { enableDryRunProtection: true, dryRunThresholdPercent: 100 },
|
|
|
|
|
|
}));
|
|
|
|
|
|
assert.ok(ps.thresholdIssues.some((i) => i.aName === 'dryRunLevel'));
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Direction derivation — _deriveDirection', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('positive flow above dead-band → filling', () => {
|
|
|
|
|
|
assert.equal(ps._deriveDirection(0.01), 'filling');
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('negative flow below dead-band → draining', () => {
|
|
|
|
|
|
assert.equal(ps._deriveDirection(-0.01), 'draining');
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('flow inside dead-band → steady', () => {
|
|
|
|
|
|
assert.equal(ps._deriveDirection(0), 'steady');
|
|
|
|
|
|
assert.equal(ps._deriveDirection(1e-5), 'steady');
|
|
|
|
|
|
assert.equal(ps._deriveDirection(-1e-5), 'steady');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Mode change — changeMode', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('valid mode swap updates this.mode', () => {
|
|
|
|
|
|
ps.changeMode('manual');
|
|
|
|
|
|
assert.equal(ps.mode, 'manual');
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('rejected mode leaves this.mode unchanged', () => {
|
|
|
|
|
|
ps.changeMode('manual');
|
|
|
|
|
|
ps.changeMode('notamode');
|
|
|
|
|
|
assert.equal(ps.mode, 'manual');
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Calibration — predicted volume and level', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('calibratePredictedVolume rewrites volume series', () => {
|
|
|
|
|
|
ps.calibratePredictedVolume(25);
|
|
|
|
|
|
const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.ok(Math.abs(vol - 25) < 1e-9);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('calibratePredictedVolume also writes level (= vol / area)', () => {
|
|
|
|
|
|
ps.calibratePredictedVolume(30);
|
|
|
|
|
|
const lvl = ps.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m');
|
|
|
|
|
|
assert.ok(Math.abs(lvl - 3) < 1e-9); // 30 / 10
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('calibratePredictedLevel writes level + volume = level × area', () => {
|
|
|
|
|
|
ps.calibratePredictedLevel(2.5);
|
|
|
|
|
|
const lvl = ps.measurements.type('level').variant('predicted').position('atequipment').getCurrentValue('m');
|
|
|
|
|
|
const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.ok(Math.abs(lvl - 2.5) < 1e-9);
|
|
|
|
|
|
assert.ok(Math.abs(vol - 25) < 1e-9); // 2.5 × 10
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Levelbased control zones — _controlLevelBased', async (t) => {
|
|
|
|
|
|
await t.test('level < minLevel → percControl=0 and MGC turnOff called', async () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
let turnOffCalls = 0;
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => { turnOffCalls++; },
|
|
|
|
|
|
handleInput: async () => {},
|
|
|
|
|
|
};
|
|
|
|
|
|
ps.calibratePredictedLevel(0.5); // below minLevel=1
|
|
|
|
|
|
await ps._controlLevelBased();
|
|
|
|
|
|
assert.equal(ps.percControl, 0);
|
|
|
|
|
|
assert.equal(turnOffCalls, 1);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
await t.test('minLevel ≤ level < active ramp start → commands 0% without shutdown', async () => {
|
2026-04-22 16:38:41 +02:00
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
ps.percControl = 42; // simulated previous demand
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
const demands = [];
|
2026-04-22 16:38:41 +02:00
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
handleInput: async (_src, d) => { demands.push(d); },
|
2026-04-22 16:38:41 +02:00
|
|
|
|
};
|
|
|
|
|
|
ps.calibratePredictedLevel(1.5); // between minLevel=1 and startLevel=2
|
|
|
|
|
|
await ps._controlLevelBased();
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
assert.equal(ps.percControl, 0);
|
|
|
|
|
|
assert.equal(demands[0], 0);
|
2026-04-22 16:38:41 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
await t.test('filling: level between startLevel and inflowLevel commands 0%', async () => {
|
2026-04-22 16:38:41 +02:00
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
const demands = [];
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
|
|
|
|
|
handleInput: async (_src, d) => { demands.push(d); },
|
|
|
|
|
|
};
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
ps.calibratePredictedLevel(2.5); // startLevel=2, inflowLevel=3
|
|
|
|
|
|
await ps._controlLevelBased('filling');
|
|
|
|
|
|
assert.equal(ps.percControl, 0);
|
|
|
|
|
|
assert.equal(demands[0], 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('filling: level ≥ inflowLevel → percControl linearly scaled to [0,100]', async () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
const demands = [];
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
|
|
|
|
|
handleInput: async (_src, d) => { demands.push(d); },
|
|
|
|
|
|
};
|
|
|
|
|
|
ps.calibratePredictedLevel(3.5); // midpoint of inflowLevel=3 and maxLevel=4
|
|
|
|
|
|
await ps._controlLevelBased('filling');
|
|
|
|
|
|
// lerp(3.5, [3,4], [0,100]) = 50
|
2026-04-22 16:38:41 +02:00
|
|
|
|
assert.ok(Math.abs(ps.percControl - 50) < 1e-9);
|
|
|
|
|
|
assert.equal(demands.length, 1);
|
|
|
|
|
|
assert.ok(Math.abs(demands[0] - 50) < 1e-9);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
await t.test('shift disabled (default): foot stays at inflowLevel even after fall', async () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
|
|
|
|
|
handleInput: async () => {},
|
|
|
|
|
|
};
|
|
|
|
|
|
// Climb past inflowLevel and beyond, then fall to a level inside [start..inflow].
|
|
|
|
|
|
ps.calibratePredictedLevel(3.8);
|
|
|
|
|
|
await ps._controlLevelBased();
|
|
|
|
|
|
assert.ok(ps.percControl > 0);
|
|
|
|
|
|
ps.calibratePredictedLevel(2.5); // between startLevel=2 and inflowLevel=3
|
|
|
|
|
|
await ps._controlLevelBased();
|
|
|
|
|
|
// Without shift the foot is inflowLevel → 0% in the hold zone.
|
|
|
|
|
|
assert.equal(ps.percControl, 0);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
await t.test('shift enabled: arming on % threshold + hold-then-ramp on draining', async () => {
|
|
|
|
|
|
// Geometry: inflow=3, max=4 → up curve goes 0%@3 to 100%@4.
|
|
|
|
|
|
// shiftArmPercent=80 ⇒ arms when up curve ≥ 80 % i.e. level ≥ 3.8.
|
|
|
|
|
|
// shiftLevel=3.5 ⇒ held output starts ramping down at this level.
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
control: {
|
|
|
|
|
|
mode: 'levelbased',
|
|
|
|
|
|
allowedModes: new Set(['levelbased']),
|
|
|
|
|
|
levelbased: {
|
|
|
|
|
|
minLevel: 1, startLevel: 2, maxLevel: 4, curveType: 'linear', logCurveFactor: 9,
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
enableShiftedRamp: true, shiftLevel: 3.5, shiftArmPercent: 80,
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
|
|
|
|
|
handleInput: async () => {},
|
|
|
|
|
|
};
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
// Filling at level=3.5 ⇒ up curve = 50 %, below arm threshold ⇒ not armed.
|
|
|
|
|
|
ps.calibratePredictedLevel(3.5);
|
|
|
|
|
|
await ps._controlLevelBased('filling');
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
assert.equal(ps._shiftArmed, false);
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
assert.ok(Math.abs(ps.percControl - 50) < 1e-9);
|
|
|
|
|
|
// Filling at level=3.85 ⇒ up curve = 85 % ≥ arm threshold ⇒ ARM.
|
|
|
|
|
|
ps.calibratePredictedLevel(3.85);
|
|
|
|
|
|
await ps._controlLevelBased('filling');
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
assert.equal(ps._shiftArmed, true);
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
assert.ok(Math.abs(ps.percControl - 85) < 1e-9); // still up curve while filling
|
|
|
|
|
|
// Direction flips to draining at the same level ⇒ capture hold ≈ 85 %.
|
|
|
|
|
|
await ps._controlLevelBased('draining');
|
|
|
|
|
|
assert.ok(Math.abs(ps._shiftHoldValue - 85) < 1e-6);
|
|
|
|
|
|
// While draining and level ≥ shiftLevel ⇒ output stays at hold (≈85 %).
|
|
|
|
|
|
ps.calibratePredictedLevel(3.6);
|
|
|
|
|
|
await ps._controlLevelBased('draining');
|
|
|
|
|
|
assert.ok(Math.abs(ps.percControl - 85) < 1e-6);
|
|
|
|
|
|
// Below shiftLevel: ramp [shift, hold] → [start, 0]. At level=2.75
|
|
|
|
|
|
// (midpoint of [2, 3.5]), x=0.5, output ≈ 85 × 0.5 = 42.5 %.
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
ps.calibratePredictedLevel(2.75);
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
await ps._controlLevelBased('draining');
|
|
|
|
|
|
assert.ok(Math.abs(ps.percControl - 42.5) < 1e-6);
|
|
|
|
|
|
// Below startLevel ⇒ output 0 % AND disarm.
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
ps.calibratePredictedLevel(1.9);
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
await ps._controlLevelBased('draining');
|
|
|
|
|
|
assert.equal(ps.percControl, 0);
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
assert.equal(ps._shiftArmed, false);
|
Hold-then-ramp shift semantics + shiftArmPercent + e2e tests
Runtime (specificClass.js):
- Replace the "shift left both ramp ends" geometry with a true
hold-then-ramp hysteresis driven by output %, not level:
• Up-curve % crosses shiftArmPercent on the way up → ARM.
• Filling→draining transition while armed → capture the up-curve %
at that moment as _shiftHoldValue.
• Draining + level ≥ shiftLevel → output stays at _shiftHoldValue
(horizontal hold, matching the dashed segment in the SVG).
• Draining + level in [start, shift] → output ramps holdValue → 0 %
along the same curve shape (linear or log) as the up curve.
• Draining + level < startLevel → 0 % AND disarm.
• Returning to filling clears holdValue, stays armed; next drain
transition captures a fresh hold so bouncing fills rearm cleanly.
• Disarm only when level ≤ startLevel.
- New _curveShape(x) helper for shared linear/log shaping.
- Removed legacy _levelBasedRampStart / _levelBasedRampTop /
_updateShiftArmed in favour of the inline state machine.
Adapter (nodeClass.js):
- Pipe shiftArmPercent through to control.levelbased.
Editor (pumpingStation.html + src/editor/):
- Add shiftArmPercent input row (% with unit) to the mode side panel
(only shown when shifted ramp is enabled). Default 95 %.
- Add the horizontal arming-% line + label inside the mode SVG —
this is the "% Threshold triggering shifted ramp down" line from
the original drawing that had been missing.
- Redraw the shifted-down curve to match the SVG geometry literally:
100 % flat from maxLevel → shiftLevel, then ramp shiftLevel →
startLevel down to 0 %, OFF below startLevel. Preview shows the
worst-case envelope (hold = 100 %); runtime hold is captured live.
- Validation extended: 0 < shiftArmPercent ≤ 100; ordering rules
preserved (start < shift ≤ max etc.).
- Auto-default shiftArmPercent to 95 when shift is enabled and the
current value is missing or out of range.
Dashboard example (examples/basic-dashboard.flow.json):
- Parser now reads `level.predicted.atequipment.default` etc. The
MeasurementContainer flatten format includes the implicit 'default'
childId; consumers must include it. Comment in the parser points
at the documenting source in generalFunctions.
Tests:
- test/basic: replace old level-armed-shift tests with two new ones
that exercise the hold-then-ramp arming, capture, hold, ramp-down,
disarm, and the bounce case (filling→draining→filling→draining
captures a fresh hold each time).
- test/integration/shifted-ramp-end-to-end.test.js: new file. Drives
Q_IN/Q_OUT through the full runtime tick with a controllable clock,
asserting the same hysteresis path the dashboard exercises.
- test/integration/basic-dashboard-flow.test.js: fixture keys updated
to the .default-suffixed form so they match the real flatten output.
56/56 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 11:46:46 +02:00
|
|
|
|
assert.equal(ps._shiftHoldValue, null);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('shift enabled: returning to filling clears hold; new hold captured on next drain', async () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
control: {
|
|
|
|
|
|
mode: 'levelbased',
|
|
|
|
|
|
allowedModes: new Set(['levelbased']),
|
|
|
|
|
|
levelbased: {
|
|
|
|
|
|
minLevel: 1, startLevel: 2, maxLevel: 4, curveType: 'linear', logCurveFactor: 9,
|
|
|
|
|
|
enableShiftedRamp: true, shiftLevel: 3.5, shiftArmPercent: 80,
|
|
|
|
|
|
},
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
|
|
|
|
|
handleInput: async () => {},
|
|
|
|
|
|
};
|
|
|
|
|
|
ps.calibratePredictedLevel(3.85);
|
|
|
|
|
|
await ps._controlLevelBased('filling');
|
|
|
|
|
|
await ps._controlLevelBased('draining');
|
|
|
|
|
|
assert.ok(Math.abs(ps._shiftHoldValue - 85) < 1e-6);
|
|
|
|
|
|
// Direction back to filling ⇒ up curve, hold cleared, still armed.
|
|
|
|
|
|
ps.calibratePredictedLevel(3.9);
|
|
|
|
|
|
await ps._controlLevelBased('filling');
|
|
|
|
|
|
assert.equal(ps._shiftHoldValue, null);
|
|
|
|
|
|
assert.equal(ps._shiftArmed, true);
|
|
|
|
|
|
assert.ok(Math.abs(ps.percControl - 90) < 1e-6); // up curve at 3.9 = 90 %
|
|
|
|
|
|
// Flip to draining again at higher level ⇒ new hold ≈ 90 %.
|
|
|
|
|
|
await ps._controlLevelBased('draining');
|
|
|
|
|
|
assert.ok(Math.abs(ps._shiftHoldValue - 90) < 1e-6);
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('log curve has fast early response', async () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
control: {
|
|
|
|
|
|
mode: 'levelbased',
|
|
|
|
|
|
allowedModes: new Set(['levelbased']),
|
|
|
|
|
|
levelbased: { minLevel: 1, startLevel: 2, maxLevel: 4, curveType: 'log', logCurveFactor: 9 },
|
|
|
|
|
|
},
|
|
|
|
|
|
}));
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
|
|
|
|
|
handleInput: async () => {},
|
|
|
|
|
|
};
|
|
|
|
|
|
ps.calibratePredictedLevel(3.5); // x=0.5 on filling ramp [3,4]
|
|
|
|
|
|
await ps._controlLevelBased('filling');
|
|
|
|
|
|
assert.ok(ps.percControl > 50);
|
|
|
|
|
|
assert.ok(ps.percControl < 100);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
2026-04-22 16:38:41 +02:00
|
|
|
|
await t.test('level > maxLevel → percControl ≥ 100 (MGC clamps internally)', async () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
ps.machineGroups['mgc1'] = {
|
|
|
|
|
|
config: { general: { name: 'mgc1' } },
|
|
|
|
|
|
turnOffAllMachines: () => {},
|
|
|
|
|
|
handleInput: async () => {},
|
|
|
|
|
|
};
|
|
|
|
|
|
ps.calibratePredictedLevel(4.5); // above maxLevel=4
|
|
|
|
|
|
await ps._controlLevelBased();
|
|
|
|
|
|
assert.ok(ps.percControl >= 100);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('getOutput — flattens basin + state + demand', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
ps.percControl = 37;
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('includes basin geometry fields', () => {
|
|
|
|
|
|
const out = ps.getOutput();
|
|
|
|
|
|
assert.equal(out.volEmptyBasin, 50);
|
|
|
|
|
|
assert.equal(out.maxVolAtOverflow, 45);
|
|
|
|
|
|
assert.equal(out.minVolAtInflow, 30);
|
|
|
|
|
|
assert.ok(Math.abs(out.minVolAtOutflow - 2) < 1e-9);
|
Level-armed shift, derived dryRunLevel, side-panel editor + manual q_out
Runtime (specificClass.js):
- Replace direction-based hysteresis with level-armed _shiftArmed state.
Arms when level rises past shiftLevel; disarms when level drops below
startLevel. While armed, ramp foot moves to startLevel and ramp top
to shiftLevel — both ends shift left, then saturate at 100 % up to
maxLevel.
- _scaleLevelToFlowPercent now takes (rampStartLevel, rampTopLevel) so
the saturation point follows the shift state.
- New setManualOutflow mirroring setManualInflow.
Adapter (nodeClass.js):
- Pipe enableShiftedRamp / shiftLevel through to control.levelbased.
- New q_out topic handler.
Editor (pumpingStation.html + new src/editor/ modules):
- Split monolithic <script> into modules: index.js (helpers),
basin-diagram.js, mode-preview.js, hover-couple.js, oneditprepare.js,
oneditsave.js — served via /pumpingStation/editor/:file.
- Mode preview redrawn per the SVG diagrams: OFF tier below 0 %, 0 %
flat from start→inlet, ramp inlet→max, optional shifted-down curve
start→shift with 100 % saturation past shift.
- Mode preview gains zone bands (dryRun / safetyLow / safe / safetyHigh /
overflow), level markers (dryRun derived, start, inlet, max, shift,
overflow), validation ribbon that blocks save on bad ordering.
- Auto-default shiftLevel to 0.9 × maxLevel on enable so the marker is
always visible.
- All level inputs moved to a side panel left of each diagram, color-
coded to match line strokes; hover-couple highlights the paired SVG
line on input focus / mouseover.
- Removed UI for non-static parameters: minHeightBasedOn,
pipelineLength, maxDischargeHead, staticHead, defaultFluid,
maxInflowRate, temperatureReferenceDegC,
timeleftToFullOrEmptyThresholdSeconds, inletPipeDiameter,
outletPipeDiameter, minLevel (now derived = dryRunLevel).
- foreignObject inputs in basin SVG removed (single source of truth in
side panel).
Dashboard example (examples/basic-dashboard.flow.json):
- Add manual Q_OUT slider + q_out builder mirroring the existing q_in
trio so the basin can be exercised end-to-end without a connected
rotating-machine downstream.
Tests (test/basic/specificClass.test.js):
- Replace direction-shift test with two new cases covering shift-disabled
hold-zone behaviour and shift-armed/disarmed transitions through
shiftLevel and startLevel boundaries. 53/53 tests pass.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-05 19:29:34 +02:00
|
|
|
|
assert.equal(out.inletPipeDiameter, 0.4);
|
|
|
|
|
|
assert.equal(out.outletPipeDiameter, 0.3);
|
|
|
|
|
|
assert.ok(Math.abs(out.highVolumeSafetyLevel - 4.41) < 1e-9);
|
|
|
|
|
|
assert.ok(Math.abs(out.dryRunLevel - 0.204) < 1e-9);
|
2026-04-22 16:38:41 +02:00
|
|
|
|
});
|
|
|
|
|
|
await t.test('includes state fields (direction, flowSource, timeleft)', () => {
|
|
|
|
|
|
const out = ps.getOutput();
|
|
|
|
|
|
assert.ok('direction' in out);
|
|
|
|
|
|
assert.ok('flowSource' in out);
|
|
|
|
|
|
assert.ok('timeleft' in out);
|
|
|
|
|
|
});
|
|
|
|
|
|
await t.test('includes percControl', () => {
|
|
|
|
|
|
assert.equal(ps.getOutput().percControl, 37);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Manual inflow — setManualInflow stores predicted inflow', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
ps.setManualInflow(0.05, Date.now(), 'm3/s'); // 0.05 m³/s
|
|
|
|
|
|
const v = ps.measurements.type('flow').variant('predicted').position('in').child('manual-qin').getCurrentValue('m3/s');
|
|
|
|
|
|
assert.ok(Math.abs(v - 0.05) < 1e-9);
|
|
|
|
|
|
});
|
Predicted-volume overflow clamp + spill tracking
Predicted volume is now clamped to [dryRunSafetyVol, maxVolAtOverflow]
in _updatePredictedVolume — the integrator can no longer drift above
the weir crest (only a real measurement can show level > overflow,
e.g. inflow exceeding pump+weir capacity). Excess is recorded as:
- overflowVolume.predicted.atequipment.default — cumulative spill (m3)
- flow.predicted.out.overflow — instantaneous spill rate (m3/s),
registered as a synthetic outflow so net-flow balance reads ~0
while pinned. The integrator subtracts the prior tick's synthetic
flow before integrating so it never feeds back into volume math.
Lower clamp at dryRunSafetyVol fires only on the transition — a low
seed/calibration is left alone; inflow is what brings it back up.
_selectBestNetFlow holds the last non-zero level-rate net flow when
level pins at overflowLevel and dL/dt collapses to 0, so dashboards
keep showing roughly what's coming in. Auto-refreshes once level
drops.
getOutput() exposes predictedOverflowVolume + predictedOverflowRate
as top-level convenience keys; the underlying measurements flow to
InfluxDB via the standard MeasurementContainer flatten path.
9 new test assertions cover the upper-clamp + spill increment, stable
spill across ticks, net-flow ~0 while pinned, spill clearing when
inflow stops, low-seed left alone, drain-across-threshold clamp, and
the new top-level output keys.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-06 14:47:46 +02:00
|
|
|
|
|
|
|
|
|
|
// _updatePredictedVolume now clamps [dryRunSafetyVol, maxVolAtOverflow] and
|
|
|
|
|
|
// tracks any excess as cumulative `overflowVolume` plus a synthetic
|
|
|
|
|
|
// `flow.predicted.out.overflow` rate so net-flow balance stays at ~0 while
|
|
|
|
|
|
// pinned. We drive ticks manually with monotonic timestamps to keep tests
|
|
|
|
|
|
// deterministic (Date.now() in the integrator can step by 0 ms in fast loops).
|
|
|
|
|
|
test('Predicted volume — overflow clamp and spill tracking', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
safety: { enableDryRunProtection: false, enableHighVolumeSafety: false, dryRunThresholdPercent: 0 },
|
|
|
|
|
|
}));
|
|
|
|
|
|
// Seed predicted volume just below the spill point.
|
|
|
|
|
|
// maxVolAtOverflow = overflowLevel × area = 4.5 × 10 = 45 m³.
|
|
|
|
|
|
const t0 = 1_700_000_000_000;
|
|
|
|
|
|
ps.calibratePredictedVolume(44, t0);
|
|
|
|
|
|
// Heavy inflow, no real outflow (no pumps wired).
|
|
|
|
|
|
ps.setManualInflow(2, t0, 'm3/s'); // 2 m³/s, dt=1s → 2 m³/tick
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('first overflow tick clamps volume and records spill increment', () => {
|
|
|
|
|
|
ps._predictedFlowState = { inflow: 2, outflow: 0, lastTimestamp: t0 };
|
|
|
|
|
|
Date.now = () => t0 + 1000;
|
|
|
|
|
|
ps._updatePredictedVolume();
|
|
|
|
|
|
const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.equal(vol, 45); // pinned at overflow
|
|
|
|
|
|
const cumulative = ps.measurements.type('overflowVolume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.equal(cumulative, 1); // proposed=44+2=46, excess=1 m³ this tick
|
|
|
|
|
|
const spill = ps.measurements.type('flow').variant('predicted').position('out').child('overflow').getCurrentValue('m3/s');
|
|
|
|
|
|
assert.equal(spill, 2); // instantaneous balance: inflow − outflowReal
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('subsequent ticks accumulate full inflow as spill (stable)', () => {
|
|
|
|
|
|
Date.now = () => t0 + 2000;
|
|
|
|
|
|
ps._updatePredictedVolume();
|
|
|
|
|
|
const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.equal(vol, 45);
|
|
|
|
|
|
const cumulative = ps.measurements.type('overflowVolume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.equal(cumulative, 3); // 1 + 2
|
|
|
|
|
|
const spill = ps.measurements.type('flow').variant('predicted').position('out').child('overflow').getCurrentValue('m3/s');
|
|
|
|
|
|
assert.equal(spill, 2);
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('predicted net flow reads ~0 while pinned at overflow', () => {
|
|
|
|
|
|
const net = ps._selectBestNetFlow();
|
|
|
|
|
|
// inflow=2, outflow_total=2 (synthetic spill), net = 0
|
|
|
|
|
|
assert.ok(Math.abs(net.value) < 1e-9);
|
|
|
|
|
|
assert.equal(net.source, 'predicted');
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('once inflow stops, spill flow clears and clamp releases', () => {
|
|
|
|
|
|
ps.setManualInflow(0, t0 + 2000, 'm3/s');
|
|
|
|
|
|
ps._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: t0 + 2000 };
|
|
|
|
|
|
Date.now = () => t0 + 3000;
|
|
|
|
|
|
ps._updatePredictedVolume();
|
|
|
|
|
|
const spill = ps.measurements.type('flow').variant('predicted').position('out').child('overflow').getCurrentValue('m3/s');
|
|
|
|
|
|
assert.equal(spill, 0);
|
|
|
|
|
|
// Volume stays at 45 (no draining force) but is no longer "pinned".
|
|
|
|
|
|
const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.equal(vol, 45);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('Predicted volume — dry-run lower clamp', async (t) => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig({
|
|
|
|
|
|
// dryRunSafetyVol = minVolAtOutflow × (1 + 5/100) = 2 × 1.05 = 2.1 m³
|
|
|
|
|
|
safety: { enableDryRunProtection: true, dryRunThresholdPercent: 5 },
|
|
|
|
|
|
}));
|
|
|
|
|
|
const t0 = 1_700_000_000_000;
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('initial seed below dryRunSafetyVol is left alone (no upward bump)', () => {
|
|
|
|
|
|
// Seed defaults to minVol=2 (below dryRunSafetyVol=2.1).
|
|
|
|
|
|
ps._predictedFlowState = { inflow: 0, outflow: 0, lastTimestamp: t0 };
|
|
|
|
|
|
Date.now = () => t0 + 1000;
|
|
|
|
|
|
ps._updatePredictedVolume();
|
|
|
|
|
|
const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.equal(vol, 2); // unchanged — clamp doesn't fire because we started below it
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
await t.test('drain across dryRunSafetyVol clamps at the threshold', () => {
|
|
|
|
|
|
// Calibrate well above, then push outflow that would cross the threshold.
|
|
|
|
|
|
ps.calibratePredictedVolume(3, t0 + 1000);
|
|
|
|
|
|
// outflow=2 m³/s for 1s → would drop to 1; clamp catches at 2.1.
|
|
|
|
|
|
ps.setManualOutflow(2, t0 + 1000, 'm3/s');
|
|
|
|
|
|
ps._predictedFlowState = { inflow: 0, outflow: 2, lastTimestamp: t0 + 1000 };
|
|
|
|
|
|
Date.now = () => t0 + 2000;
|
|
|
|
|
|
ps._updatePredictedVolume();
|
|
|
|
|
|
const vol = ps.measurements.type('volume').variant('predicted').position('atequipment').getCurrentValue('m3');
|
|
|
|
|
|
assert.ok(Math.abs(vol - 2.1) < 1e-9);
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
test('getOutput — exposes predictedOverflowVolume / predictedOverflowRate', () => {
|
|
|
|
|
|
const ps = new PumpingStation(makeConfig());
|
|
|
|
|
|
// Seed an overflow scenario.
|
|
|
|
|
|
const t0 = 1_700_000_000_000;
|
|
|
|
|
|
ps.calibratePredictedVolume(44, t0);
|
|
|
|
|
|
ps.setManualInflow(2, t0, 'm3/s');
|
|
|
|
|
|
ps._predictedFlowState = { inflow: 2, outflow: 0, lastTimestamp: t0 };
|
|
|
|
|
|
Date.now = () => t0 + 1000;
|
|
|
|
|
|
ps._updatePredictedVolume();
|
|
|
|
|
|
const out = ps.getOutput();
|
|
|
|
|
|
assert.equal(out.predictedOverflowVolume, 1);
|
|
|
|
|
|
assert.equal(out.predictedOverflowRate, 2);
|
|
|
|
|
|
});
|