Commit Graph

9 Commits

Author SHA1 Message Date
znetsixe
d238270530 test(mgc): drop denormalized asset fields from integration fixtures
Each fixture's machineConfig() now passes asset: { model, unit } only —
the supplier / category / type strings are derived at runtime via
assetResolver in rotatingMachine's _setupCurves. Six integration tests
updated. No behaviour change.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-12 17:13:02 +02:00
znetsixe
31324ae82d B2.3: migrate MGC to LatestWinsGate.fireAndWait
specificClass.js 319 → 311 lines. Removed inline _dispatchInFlight +
_delayedCall + finally block. handleInput is now a 1-line delegate
to DemandDispatcher.fireAndWait({source, demand, ...}).
turnOffAllMachines calls _demandDispatcher.cancelPending().
DemandDispatcher 39 → 53 lines. One integration test rewritten to
use the new sentinel-resolution semantics. 77/77 tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:29:18 +02:00
znetsixe
619b1311d2 P4 wave 1: extract MGC concerns into focused modules
src/groupOps/        groupOperatingPoint + groupCurves (pure functions)
  src/totals/          totalsCalculator (dynamic + absolute + active)
  src/combinatorics/   pumpCombinations (validPumpCombinations + checkSpecialCases)
  src/optimizer/       bestCombination (CoG) + bepGravitation (BEP-G + marginal-cost)
  src/efficiency/      groupEfficiency (calc + distance helpers)
  src/dispatch/        demandDispatcher (LatestWinsGate-based; replaces
                       _dispatchInFlight + _delayedCall)
  src/commands/        canonical names from start (set.mode/scaling/demand,
                       child.register) + legacy aliases
  CONTRACT.md          inputs/outputs/events surface

53 basic tests pass (52 new + 1 pre-existing).
specificClass.js / nodeClass.js untouched — integration in P4 wave 2.

Findings flagged via agents (TODO append to OPEN_QUESTIONS.md):
  - calcGroupEfficiency.maxEfficiency is actually the mean (misleading name)
  - checkSpecialCases has a no-op `return false` inside forEach
  - MGC doesn't route cmd.startup/shutdown/estop — confirm if station broadcasts need it

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 20:45:23 +02:00
Rene De Ren
ea2857fb25 fix: serialize per-pump shutdown + cancel deferred dispatch in turnOffAllMachines
PS calls turnOffAllMachines on every tick once level < stopLevel. Two
ways the pump could re-engage after we shut it down:

1. _delayedCall: a 1% dead-zone keep-alive parked in MGC's deferred
   dispatch fires from the in-flight handleInput's finally block AFTER
   the shutdown completes, dispatching flow + startup to a fresh pump.
   Clear _delayedCall at the top of turnOff.

2. Concurrent shutdown calls on the same pump interrupt each other
   before the sequence can transition past stopping. Track shutdown-
   in-flight per pump and skip if one is already underway.

Together with the rotatingMachine delayedMove-clearing fix, this lets
the level-based hysteresis cycle complete: pumps shut off cleanly at
stopLevel, basin reverses direction, refills to startLevel, repeat.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 18:17:55 +02:00
Rene De Ren
8e684203a8 Test: add full-cycle and up/down-sweep regression scenarios
Scenario 5 covers 100% → 0% → 100% with the second 100% landing
mid-shutdown (stopping/coolingdown) — exercises the path where
delayedMove must NOT be saved on a non-idle non-residue state without
a follow-up startup, since transitionToState('idle') doesn't fire it.

Scenario 6 walks 10%→100%→10% monotonically and asserts the down-sweep's
final demand is honoured (catches the user's observed "stuck around
60% going up, no reaction going down" symptom — where pumps would
otherwise freeze at a stale setpoint from the up-sweep).

Both pass with the current MGC dispatch fix.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:33:56 +02:00
Rene De Ren
9916527790 optimalControl: dispatch setpoint to non-operational pumps too
Previously the dispatch loop only fired flowmovement for pumps in
'operational' or transitioned 'idle' pumps via execsequence-startup-then-flowmovement.
Pumps mid-startup (starting/warmingup) were silently skipped. With PS
sending demand every tick, intermediate setpoints during the startup
window never reached the pump — it locked onto the very first
snapshot's flowmovement and froze there.

Now flowmovement is sent regardless of state and rotatingMachine's
state.moveTo handles the queueing (delayedMove for transients, unpark
for residue, immediate for operational). Crucially, flowmovement runs
BEFORE execsequence-startup so the FIRST call's stale setpoint can't
land on an already-operational pump and overwrite the latest
delayedMove that fires at end of startup.

Adds three integration tests:
- demand-cycle-walkthrough: 0..100% sweep with clean per-step table
- idle-startup-deadlock: four scenarios that pin the dispatch behaviour
  including the regression guard for varying-demand-during-startup
- optimizer-combination-choice: physical-validity invariants

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:19:47 +02:00
znetsixe
9c79dac4e3 Fix stale flow cache on MGC shutdown; correct NCog physics tests
### Bug fix — stale flow cache on shutdown (specificClass.js)

When turnOffAllMachines() fires (negative demand, zero flow demand, or
safety trip), the MGC was only shutting pumps down. The pumps' last
emitted predicted flow / power stayed in the MeasurementContainer,
so the parent pumpingStation kept computing net flow from cached
non-zero values — reading the MGC as "still draining" when it wasn't.
Net: net-flow direction and safety triggers misfired during and
shortly after an MGC shutdown.

Fix: after shutting down all machines, write 0 to the predicted
flow (downstream + atEquipment) and predicted power (atEquipment)
slots so the cache reflects reality immediately.

### Correctness — async/await on shutdown (specificClass.js)

Two call sites invoked turnOffAllMachines() without awaiting it, so
the subsequent `return` raced the shutdown promises. Now awaited.
Also DRY'd one inline shutdown loop into a call to
turnOffAllMachines().

### Physics correction — NCog for centrifugal pumps (integration tests)

The previous tests asserted NCog > 0 for centrifugal pumps. That's
physically wrong: for variable-speed centrifugal pumps P ∝ n³ and
Q ∝ n, so Q/P ∝ 1/n² is monotonically decreasing with speed. Peak
efficiency (peak Q/P) is always at minimum speed → cogIndex = 0 →
NCog = 0 by the current formula.

Tests now:
- Assert NCog == 0 for all centrifugal configurations
- Assert distributeByNCog() falls back to equal distribution when
  NCog == 0 (confirmed by the existing tests 4-6 that slope-based
  redistribution is what actually differentiates pumps with different
  BEPs — not NCog)

This matches the actual implementation; the previous tests were
asserting an idealised COG model that doesn't apply here.

### Editor hygiene (mgc.html, nodeClass.js)

- mgc.html: add missing asset-menu defaults (uuid, supplier, category,
  assetType, model, unit) — brings MGC in line with rotatingMachine
  and pumpingStation editor shapes.
- nodeClass.js: clear node status badge on close.

All 13 tests (basic + integration) pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-22 17:51:10 +02:00
znetsixe
d55f401ab3 fix: production hardening — unit mismatch, safety guards, marginal-cost refinement
- Fix flowmovement unit mismatch: MGC computed flow in canonical (m³/s)
  but rotatingMachine expects output units (m³/h). All flowmovement calls
  now convert via _canonicalToOutputFlow(). Without this fix, every pump
  stayed at minimum flow regardless of demand.
- Fix absolute scaling: demandQout vs demandQ comparison bug, reorder
  conditions so <= 0 is checked first, add else branch for valid demand.
- Fix empty Qd <= 0 block: now calls turnOffAllMachines().
- Add empty-machines guards on optimalControl and equalizePressure.
- Add null fallback (|| 0) on pressure measurement reads.
- Fix division-by-zero in calcRelativeDistanceFromPeak.
- Fix missing flowmovement after startup in equalFlowControl.
- Add marginal-cost refinement loop in BEP-Gravitation: after slope-based
  redistribution, iteratively shifts flow from highest actual dP/dQ to
  lowest using real power evaluations. Closes gap to brute-force optimum
  from 2.1% to <0.1% without affecting combination selection stability.
- Add NCog distribution comparison tests and brute-force power table test.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-07 13:40:45 +02:00
znetsixe
ee38c8b581 before functional changes by codex 2026-02-19 17:38:05 +01:00