This commit is contained in:
znetsixe
2026-03-11 11:13:26 +01:00
parent 33f3c2ef61
commit 6b2a8239f2
16 changed files with 2850 additions and 146 deletions

View File

@@ -19,6 +19,21 @@ test('calcEfficiency runs through coolprop path without mocks', () => {
const eff = machine.measurements.type('efficiency').variant('predicted').position('atEquipment').getCurrentValue();
assert.equal(typeof eff, 'number');
assert.ok(eff > 0);
const pressureDiffPa = (1200 - 800) * 100; // mbar -> Pa
const flowM3s = 120 / 3600; // m3/h -> m3/s
const expectedHydraulicPower = pressureDiffPa * flowM3s;
const expectedHydraulicEfficiency = expectedHydraulicPower / 12000; // 12kW -> W
const hydraulicPower = machine.measurements.type('hydraulicPower').variant('predicted').position('atEquipment').getCurrentValue('W');
const hydraulicEfficiency = machine.measurements.type('nHydraulicEfficiency').variant('predicted').position('atEquipment').getCurrentValue();
const head = machine.measurements.type('pumpHead').variant('predicted').position('atEquipment').getCurrentValue('m');
assert.ok(Number.isFinite(hydraulicPower));
assert.ok(Number.isFinite(hydraulicEfficiency));
assert.ok(Number.isFinite(head));
assert.ok(Math.abs(hydraulicPower - expectedHydraulicPower) < 1);
assert.ok(Math.abs(hydraulicEfficiency - expectedHydraulicEfficiency) < 0.01);
});
test('predictions use initialized medium pressure and not the minimum-pressure fallback', () => {
@@ -33,7 +48,7 @@ test('predictions use initialized medium pressure and not the minimum-pressure f
assert.equal(pressureStatus.initialized, true);
assert.equal(pressureStatus.hasDifferential, true);
const expectedDiff = mediumDownstreamMbar - mediumUpstreamMbar;
const expectedDiff = (mediumDownstreamMbar - mediumUpstreamMbar) * 100; // mbar -> Pa canonical
assert.equal(Math.round(machine.predictFlow.fDimension), expectedDiff);
assert.ok(machine.predictFlow.fDimension > 0);
});

View File

@@ -0,0 +1,75 @@
const test = require('node:test');
const assert = require('node:assert/strict');
const Machine = require('../../src/specificClass');
const { makeMachineConfig, makeStateConfig } = require('../helpers/factories');
test('flow drift is assessed with NRMSE and exposed in output', () => {
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
machine.updateMeasuredPressure(700, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-up' });
machine.updateMeasuredPressure(1100, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
machine.updatePosition();
const predictedFlow = machine.measurements
.type('flow')
.variant('predicted')
.position('downstream')
.getCurrentValue('m3/h');
for (let i = 0; i < 10; i += 1) {
machine.updateMeasuredFlow(predictedFlow * 0.92, 'downstream', {
timestamp: Date.now() + i,
unit: 'm3/h',
childName: 'ft-down',
});
}
const output = machine.getOutput();
assert.ok(Number.isFinite(output.flowNrmse));
assert.equal(typeof output.flowImmediateLevel, 'number');
assert.equal(typeof output.flowLongTermLevel, 'number');
assert.ok(['high', 'medium', 'low', 'invalid'].includes(output.predictionQuality));
assert.ok(Number.isFinite(output.predictionConfidence));
assert.equal(output.predictionPressureSource, 'differential');
assert.ok(Array.isArray(output.predictionFlags));
});
test('power drift is assessed when measured power is provided', () => {
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
machine.updateMeasuredPressure(700, 'upstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-up' });
machine.updateMeasuredPressure(1100, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
machine.updatePosition();
const predictedPower = machine.measurements
.type('power')
.variant('predicted')
.position('atEquipment')
.getCurrentValue('kW');
for (let i = 0; i < 10; i += 1) {
machine.updateMeasuredPower(predictedPower * 1.08, 'atEquipment', {
timestamp: Date.now() + i,
unit: 'kW',
childName: 'power-meter',
});
}
const output = machine.getOutput();
assert.ok(Number.isFinite(output.powerNrmse));
assert.equal(typeof output.powerImmediateLevel, 'number');
assert.equal(typeof output.powerLongTermLevel, 'number');
});
test('single-side pressure lowers prediction confidence category', () => {
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
machine.updateMeasuredPressure(950, 'downstream', { timestamp: Date.now(), unit: 'mbar', childName: 'pt-down' });
const output = machine.getOutput();
assert.equal(output.predictionPressureSource, 'downstream');
assert.ok(output.predictionConfidence < 0.9);
assert.equal(output.pressureDriftLevel, 1);
assert.ok(Array.isArray(output.predictionFlags));
assert.ok(output.predictionFlags.includes('single_side_pressure'));
});

View File

@@ -27,8 +27,8 @@ test('pressure initialization combinations are handled explicitly', () => {
assert.equal(status.hasDifferential, false);
assert.equal(status.source, 'upstream');
const upstreamValue = machine.getMeasuredPressure();
assert.equal(Math.round(upstreamValue), upstreamOnly);
assert.equal(Math.round(machine.predictFlow.fDimension), upstreamOnly);
assert.equal(Math.round(upstreamValue), upstreamOnly * 100);
assert.equal(Math.round(machine.predictFlow.fDimension), upstreamOnly * 100);
// downstream only
machine = createMachine();
@@ -41,8 +41,8 @@ test('pressure initialization combinations are handled explicitly', () => {
assert.equal(status.hasDifferential, false);
assert.equal(status.source, 'downstream');
const downstreamValue = machine.getMeasuredPressure();
assert.equal(Math.round(downstreamValue), downstreamOnly);
assert.equal(Math.round(machine.predictFlow.fDimension), downstreamOnly);
assert.equal(Math.round(downstreamValue), downstreamOnly * 100);
assert.equal(Math.round(machine.predictFlow.fDimension), downstreamOnly * 100);
// downstream and upstream
machine = createMachine();
@@ -57,8 +57,8 @@ test('pressure initialization combinations are handled explicitly', () => {
assert.equal(status.hasDifferential, true);
assert.equal(status.source, 'differential');
const differentialValue = machine.getMeasuredPressure();
assert.equal(Math.round(differentialValue), downstream - upstream);
assert.equal(Math.round(machine.predictFlow.fDimension), downstream - upstream);
assert.equal(Math.round(differentialValue), (downstream - upstream) * 100);
assert.equal(Math.round(machine.predictFlow.fDimension), (downstream - upstream) * 100);
});
test('real pressure child data has priority over simulated dashboard pressure', async () => {
@@ -66,7 +66,7 @@ test('real pressure child data has priority over simulated dashboard pressure',
machine.updateSimulatedMeasurement('pressure', 'upstream', 900, { unit: 'mbar', timestamp: Date.now() });
machine.updateSimulatedMeasurement('pressure', 'downstream', 1200, { unit: 'mbar', timestamp: Date.now() });
assert.equal(Math.round(machine.getMeasuredPressure()), 300);
assert.equal(Math.round(machine.getMeasuredPressure()), 30000);
const upstreamChild = makeChildMeasurement({ id: 'pt-up-real', name: 'PT Up', positionVsParent: 'upstream', type: 'pressure', unit: 'mbar' });
const downstreamChild = makeChildMeasurement({ id: 'pt-down-real', name: 'PT Down', positionVsParent: 'downstream', type: 'pressure', unit: 'mbar' });
@@ -77,7 +77,7 @@ test('real pressure child data has priority over simulated dashboard pressure',
upstreamChild.measurements.type('pressure').variant('measured').position('upstream').value(700, Date.now(), 'mbar');
downstreamChild.measurements.type('pressure').variant('measured').position('downstream').value(1300, Date.now(), 'mbar');
assert.equal(Math.round(machine.getMeasuredPressure()), 600);
assert.equal(Math.round(machine.getMeasuredPressure()), 60000);
const status = machine.getPressureInitializationStatus();
assert.equal(status.source, 'differential');
assert.equal(status.initialized, true);

View File

@@ -25,3 +25,29 @@ test('registerChild listens to measurement events and stores measured pressure',
assert.equal(typeof stored, 'number');
assert.equal(Math.round(stored), 123);
});
test('registerChild deduplicates listeners on re-registration', async () => {
const machine = new Machine(makeMachineConfig(), makeStateConfig());
const child = makeChildMeasurement({ id: 'pt-dup', positionVsParent: 'downstream', type: 'pressure', unit: 'mbar' });
const eventName = 'pressure.measured.downstream';
let handlerCalls = 0;
const originalUpdatePressure = machine.updateMeasuredPressure.bind(machine);
machine.updateMeasuredPressure = (...args) => {
handlerCalls += 1;
return originalUpdatePressure(...args);
};
machine.registerChild(child, 'measurement');
machine.registerChild(child, 'measurement');
assert.equal(child.measurements.emitter.listenerCount(eventName), 1);
child.measurements
.type('pressure')
.variant('measured')
.position('downstream')
.value(321, Date.now(), 'mbar');
assert.equal(handlerCalls, 1);
});

View File

@@ -12,11 +12,13 @@ test('execSequence startup reaches operational with zero transition times', asyn
assert.equal(machine.state.getCurrentState(), 'operational');
});
test('execMovement updates controller position in operational state', async () => {
test('execMovement constrains controller position to safe bounds in operational state', async () => {
const machine = new Machine(makeMachineConfig(), makeStateConfig({ state: { current: 'operational' } }));
const { max } = machine._resolveSetpointBounds();
await machine.handleInput('parent', 'execMovement', 10);
const pos = machine.state.getCurrentPosition();
assert.ok(pos >= 9.9 && pos <= 10);
assert.ok(pos <= max);
assert.equal(pos, max);
});