Commit Graph

36 Commits

Author SHA1 Message Date
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
0e8cab5d3f B3.3 follow-up: drop _unitView mirror; use UnitPolicy property bags directly
UnitPolicy now exposes canonical/output/curve as both methods AND
frozen property bags, so this.unitPolicy = this.constructor.unitPolicy
works directly. Removes the 14-line _unitView assembly in configure().
specificClass.js 336→318. 77/77 tests still pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-11 17:13:18 +02:00
znetsixe
bb2f3bea82 P4 wave 2: convert MGC to BaseDomain + extract control/ + io/
specificClass.js: 1808 → 336 lines.
  MachineGroup extends BaseDomain. Configure() wires GroupOperatingPoint,
  TotalsCalculator, GroupEfficiency, DemandDispatcher (built but unused —
  see OPEN_QUESTIONS); ChildRouter handles registration + measurement
  events; tick is event-driven (no setInterval, recomputes on pressure
  events).

  src/control/strategies.js (210 lines, new) — extracted equalFlowControl
  + prioPercentageControl from the orchestrator to fit the line budget.
  src/io/output.js (69 lines, new) — extracted getOutput + getStatusBadge
  composition.

  Public surface preserved: machines / setMode / setScaling / handleInput
  / isMachineActive / handlePressureChange / dynamicTotals / absoluteTotals
  / absDistFromPeak / relDistFromPeak. _delayedCall + _dispatchInFlight
  inline gate kept (tests await handleInput; LatestWinsGate.fire is
  void) — see OPEN_QUESTIONS for the deferred decision.

nodeClass.js: 280 → 20 lines.
  Extends BaseNodeAdapter. tickInterval=null (event-driven), commands
  registry from src/commands/. buildDomainConfig returns {} (MGC has
  no node-specific domain slice).

53 basic + 23 integration + 1 edge tests pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-10 21:32:11 +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
2651aaf409 abortActiveMovements: only WARN when actually aborting an in-flight move
In normal operation the _dispatchInFlight gate (handleInput)
guarantees no pump movement is in flight when a new dispatch starts,
so the per-machine abort call is a no-op. The previous unconditional
WARN flooded the log with one line per pump per tick (~3/s) for what
was actually a normal-path no-op.

Now the WARN fires ONLY when a pump's state is accelerating or
decelerating — i.e. the gate has been bypassed and we're force-
aborting an in-flight ramp. The wording reflects that:

  Force-aborting in-flight movement on pump_a (state=accelerating)
  due to: new demand received — _dispatchInFlight gate bypassed.

If you ever see this in production logs, the gate has a hole and
needs investigating.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:43:12 +02:00
Rene De Ren
df74ea0fac Serialize handleInput dispatches via _dispatchInFlight gate
Mirrors rotatingMachine state.delayedMove. PS ticks demand into MGC at
1 Hz but a real pump ramp takes several seconds; before this gate every
PS tick aborted the in-flight optimalControl and started a new one, so
pumps never reached their setpoint. Live observation: 120 aborts / 2
min, pump_a drifting to 138 m³/h while pump_b stayed clamped at minFlow
60 m³/h ("near_curve_edge").

While a dispatch is in flight, the latest {source, demand, powerCap,
priorityList} is parked in _delayedCall and the new call returns.
The in-flight dispatch's finally block picks up the latest delayed
value when it settles. Latest-wins — intermediate demands are stomped
because they were obsolete by the time the pumps were ready for them.

Regression test in superproject:
test/mgc-overactive-demand-serialization.integration.test.js
30 concurrent demand calls now produce ≤ 5 aborts (was 30).

All existing tests still pass: 21 MGC integration + 7 cross-node
integration (incl. realistic-startup-timing, inflow-overcapacity-
stability, ps-mgc-flow-contract, idle-startup-deadlock).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-09 09:14:59 +02:00
Rene De Ren
96b84d3124 Revert: handleInput unchanged-demand short-circuit
Reverts a14aa0d. The "skip when demand unchanged" optimisation broke
the live demo: in some real conditions (basin transitions, safety
controller activations) PS sends repeated demand=0 and the optimisation
correctly turned pumps off the first time but then declined to re-act
when conditions changed in a way the test suite didn't cover. Live
result: pumps stayed off even when basin filled to overflow.

The original symptom (pumps stuck mid-ramp under saturated demand) needs
a different approach — likely a pump-side guard rather than an MGC-side
demand filter. Investigating in a follow-up.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:55:41 +02:00
Rene De Ren
a14aa0dab8 handleInput: skip abort+redispatch when demand unchanged
Live trace showed PS ticking every 1 s and re-firing the SAME demand
(100% saturated under storm inflow) while the basin level evolved slowly.
Each tick was calling abortActiveMovements + optimalControl, which
aborted in-flight pump moves before they could finish (move duration
~0.4 s vs 1 s tick) and immediately re-issued the same setpoint. Pumps
got stuck ramping from the same starting position toward the same
target indefinitely — moveTimeleft stable at 0.379 s for minutes,
flow.predicted frozen.

Now early-return when |demandQ - prev| < max(0.5, prev*0.005). PS
hysteresis float jitter is filtered, real demand changes still
propagate. Pumps finish their first move and stay at the right
setpoint instead of being aborted forever.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 20:10:50 +02:00
Rene De Ren
69bdf11fc4 DOWNSTREAM is the live aggregate; AT_EQUIPMENT is the optimizer's intent
handlePressureChange writes the live aggregate (sum of every pump's
current predicted-flow measurement) to flow.predicted.downstream — that
is the channel PS subscribes to for its outflow estimate, and it must
reflect what pumps are actually delivering.

optimalControl + equalFlowControl + prioPercentageControl were also
writing to DOWNSTREAM with the optimizer's TARGET (bestFlow / totalFlow).
That's a planned setpoint, not an achieved aggregate, and it was
clobbering the live value every handleInput tick — leaving PS reading
e.g. 105 m³/h while the real aggregate was 681 m³/h. Test
ps-mgc-flow-contract caught this deterministically.

Move all the optimizer-target writes to AT_EQUIPMENT (the "what we
commanded the equipment to do" channel). DOWNSTREAM is now
single-writer (handlePressureChange) and faithfully tracks reality.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 18:32:58 +02:00
Rene De Ren
dc27a569d9 handlePressureChange: mirror aggregate flow onto DOWNSTREAM
PS subscribes to MGC's flow.predicted.downstream and uses it as the
outflow estimate for net-flow computation. MGC was only writing to
DOWNSTREAM inside optimalControl (the optimizer's bestFlow TARGET, not
the achieved aggregate), and to AT_EQUIPMENT in handlePressureChange.

During transients — e.g. demand dropping to dead-band keep-alive while
pumps are still ramping down from full throttle — PS saw a stale 25 m³/h
target on DOWNSTREAM while pumps were physically delivering 500+ m³/h.
NetFlow looked small and stable when the basin was actually draining
fast.

flow.act = sum of every pump's current predicted output = achieved
aggregate. Mirror it onto DOWNSTREAM so PS gets a live signal on every
pump flow/pressure update, not just every MGC.handleInput.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 17:20:21 +02:00
Rene De Ren
b7c40b0ddc equalFlowControl: mirror the optimalControl dispatch reorder
The priority-control codepath had the same stale dispatch shape that
caused the live deadlock in optimalControl: only handling idle and
operational states, and chaining flowmovement after execsequence
startup. Aligns it with the optimalControl fix so a future mode switch
to prioritycontrol doesn't reintroduce the bug.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 11:47:17 +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
ffb2072baa Merge commit '85797b5' into HEAD
# Conflicts:
#	src/nodeClass.js
#	src/specificClass.js
2026-03-31 18:17:41 +02:00
Rene De Ren
85797b5b8b Align machineGroupControl with current architecture 2026-03-12 16:43:29 +01:00
znetsixe
b337bf9eb7 updates 2026-03-11 11:12:52 +01:00
znetsixe
f8012c8bad update 2026-02-23 13:17:39 +01:00
znetsixe
f4cb329597 updates 2025-11-25 15:10:36 +01:00
znetsixe
b49f0c3ed2 attempt to fix flow distribution 2025-11-22 21:09:38 +01:00
znetsixe
edcffade75 Added edge case for when 1 pump cant handle the scope 2025-11-20 22:28:49 +01:00
znetsixe
b6ffefc92b Lots of minor bug fixes to update on architecture choices 2025-11-13 19:39:32 +01:00
znetsixe
ed2cf4c23d fixed outputformats 2025-11-06 11:18:38 +01:00
znetsixe
e0526250c2 changed colours, description based on s88 2025-10-14 13:52:18 +02:00
Rene De ren
15501e8b1d updates from laptop 2025-10-03 15:33:37 +02:00
znetsixe
b4364094c6 Stable version of machinegroup control 2025-10-02 17:08:41 +02:00
znetsixe
a55c6bdbea fixed pressure updates from machines. Everything seems to be working again. 2025-09-23 15:50:40 +02:00
znetsixe
ac9d1b4fdd added test file 2025-09-23 15:12:01 +02:00
znetsixe
cbc0840f0c added testfile fixing bugs 2025-09-23 15:03:57 +02:00
znetsixe
c62071992d working on a stable version 2025-09-23 11:19:22 +02:00
znetsixe
078a0d80dc updated function for registration of machines 2025-09-04 17:07:18 +02:00
znetsixe
dc1fb500c0 license update 2025-08-07 13:52:56 +02:00
znetsixe
de5652b73d small bug fixes 2025-07-31 09:10:34 +02:00
znetsixe
2aeb876c0d bug fixes 2025-07-02 10:52:37 +02:00
znetsixe
35eb965609 not working yet need to fix child registration? 2025-07-01 17:03:36 +02:00