Files
machineGroupControl/wiki/Reference-Limitations.md
znetsixe 472402c62d feat(mgc): rendezvous planner — same-time landing across all modes
Routes every dispatch through a tick-aware planner so all pumps reach
their setpoint at the same wall-clock instant t* = max(eta_i),
regardless of control strategy or per-pump reaction speed.

Architecture (src/movement/):
- machineProfile.js   – pure snapshot of a registered child (state,
                        position, velocityPctPerS, ladder timings,
                        flowAt / positionForFlow). Reads timings from
                        child.state.config.time (the actual storage
                        location — previous fallback paths silently
                        produced 0 s, collapsing every eta to ramp-only).
- moveTrajectory.js   – seconds-to-target per machine; handles
                        idle / starting / warmingup / operational / cooling.
- movementScheduler.js – t* = max eta over ALL non-noop moves. Every
                        command is delayed so its move finishes at t*.
                        Startup execsequence fires at 0; its flowmovement
                        is gated by max(ladderS, t* − rampS) so a fast
                        pump waits before ramping rather than landing
                        early. useRendezvous=false collapses to all
                        fireAtTickN=0 (legacy fire-and-forget).
- movementExecutor.js – wall-clock virtual cursor: each tick fires
                        every command whose fireAtTickN ≤ floor(elapsed/tickS).
                        tick() no longer awaits pending fireCommand
                        promises — the synchronous prologue of
                        handleInput claims the latest-wins gate, which
                        is what race-favouring relies on.

Shared dispatch path (src/specificClass.js):
- _dispatchFlowDistribution(distribution) — extracted from
  _optimalControl. Builds profiles, calls movementScheduler.plan,
  replans the executor, ticks once. Reads
  config.planner.useRendezvous (default true).
- _optimalControl computes its bestCombination and hands off.
- equalFlowControl (priorityControl mode) computes its
  flowDistribution and hands off via ctx.mgc._dispatchFlowDistribution.
  Same-time landing now applies in BOTH modes.

Editor toggle (mgc.html + src/nodeClass.js):
- New "Same-time landing" checkbox under Control Strategy.
- nodeClass.buildDomainConfig bridges uiConfig.useRendezvous →
  config.planner.useRendezvous. Default ON.

Tests:
- New: planner-convergence.integration.test.js (real-time end-to-end
  diagnostic — drives a 3-pump mixed-state dispatch and asserts both
  convergence to the demand setpoint AND same-time landing within
  one tick).
- New: planner-rendezvous.integration.test.js (schedule-shape
  assertions against real pump objects).
- New: movementScheduler.basic.test.js — includes a mixed-speed
  multi-startup case proving the fast pumps wait so all three land
  together (the regression that prompted this work).
- New: movementExecutor.basic.test.js + moveTrajectory.basic.test.js.
- Updated executor contract test: tick() must NOT await pending fires.

Commands + wiki:
- handlers.js: source/mode allow-list gate moved into a shared _gate()
  helper; every command now checks isValidActionForMode +
  isValidSourceForMode before dispatching. Status-level commands
  (set.mode, set.scaling) are allowed in every mode.
- commands.basic.test.js: coverage for the new gate behaviour.
- wiki regen: Home.md visual-first rewrite + Reference-{Architecture,
  Contracts,Examples,Limitations}.md split with _Sidebar.md index.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:43:55 +02:00

7.6 KiB
Raw Permalink Blame History

Reference — Limitations

code-ref

Note

What machineGroupControl does not do, current rough edges, and open questions. The planner-decline question is tracked as Gitea issue RnD/machineGroupControl#1; other open items live in .agents/improvements/IMPROVEMENTS_BACKLOG.md in the superproject.


When you would not use this node

Scenario Use instead
A single pump Wire rotatingMachine directly under your parent. MGC's combinatorics + totals add no value below N=2.
Valves (no curve, no FSM-driven motor) valveGroupControl. MGC's optimizer assumes a flow-vs-pressure characteristic.
Pumps behind independent headers Multiple MGCs (one per header), each parented to its own logical aggregator. The equaliser assumes a shared discharge / suction pressure.
Curve-less assets Without a curve, optimalControl excludes the machine from every combination; the dispatch loop falls into the empty-set branch and warns each tick.
Mixed compressor + pump groups The optimizer is curve-agnostic in principle, but the η = (Q·ΔP)/P_shaft identity used in _optimalControl assumes an incompressible-flow head. Use separate MGCs per phase.

Known limitations

maintenance mode is in the schema but not in the dispatch switch

config.mode.current accepts maintenance as a valid value (per the schema enum), but _runDispatch's mode switch only handles optimalcontrol and prioritycontrol. Picking maintenance will log 'maintenance' is not a valid mode. on every demand. Treated as schema-vs-code drift, not a runtime bug.

priorityControl bypasses the movement planner

equalFlowControl (the priority-mode strategy) still uses the legacy direct-dispatch path:

await Promise.all(flowDistribution.map(async ({ machineId, flow }) => {
    if (flow > 0) {
        await machine.handleInput('parent', 'flowmovement', ...);
        if (currentState === 'idle') await machine.handleInput('parent', 'execsequence', 'startup');
    } else { ... shutdown ... }
}));

The planner is only wired through optimalControl. Consequence: priority-mode transitions can show a flow dip while one pump warms up and another keeps spinning. Tracked for a future pass; the planner's API is mode-agnostic so the surgery is straightforward when priorities allow.

mgc.scaling is undefined

The orchestrator no longer carries a scaling field — set.demand is unit-self-describing per message. The io/output.js formatter still references mgc.scaling, which always reads undefined. The status-badge cosmetically displays norm. This is a leftover artifact of the pre-refactor design; harmless, scheduled for removal.

Group efficiency naming — maxEfficiency is the mean, not the peak

GroupEfficiency.calcGroupEfficiency returns { maxEfficiency, lowestEfficiency }. maxEfficiency is the mean cog across all machines, not the maximum. The name is preserved for behavioural parity with the pre-refactor code; callers using it as "the peak" will over-estimate the BEP target. Tracked — rename is a follow-up.

calcAbsoluteTotals implicit pressure coupling

TotalsCalculator.calcAbsoluteTotals iterates a machine's predictFlow.inputCurve and re-indexes the SAME pressure key into predictPower.inputCurve. If the two curves were sampled at different pressures the lookup is undefined and the call throws. Mitigation deferred to the rotatingMachine curveLoader pass (P5).

Power-cap parameter has no canonical topic

handleInput(source, demand, powerCap) accepts a powerCap argument and threads it to validPumpCombinations, but there is no set.power-cap topic in commands/index.js. Only programmatic callers can set it. Tracked.

Per-pump fan-out not on Port 0

MGC's Port 0 carries the group aggregate only (atEquipment_predicted_flow, headerDiffPa, etc.). If you want per-pump trends on a dashboard you must wire each rotatingMachine's Port 0 separately. By design — the alternative would put N × M fields on the MGC payload.

Curve-less members silently drop out

combinatorics/pumpCombinations.validPumpCombinations filters by FSM state and mode but not by curve presence. A machine with predictFlow === null (because its curve loader failed at startup) has currentFxyYMin / Max = 0, so its contribution to subset envelopes is zero. It can still appear in subsets — the optimizer just gives it zero flow. The drop-out is silent; the only signal is the curve-loader's error log at startup.


Open questions (tracked)

Question Where it lives
Should the planner ever decline a combination when the slowest startup exceeds an SLA on demand spikes? machineGroupControl#1
Wire the movement planner through priorityControl Internal — not yet ticketed
Remove the mgc.scaling artifact + the scaling badge field Internal
Rename maxEfficiencymeanGroupCog in GroupEfficiency Internal
Decline-and-fall-back vs always-commit on planner level Same as the Gitea issue above

Migration notes

From pre-planner

The MGC's _optimalControl used to fan commands out inline (lines 226239 in 26e92b5^):

await Promise.all(Object.entries(this.machines).map(async ([id, machine]) => {
    if (flow > 0) {
        await machine.handleInput('parent', 'flowmovement', ...);
        if (state === 'idle') await machine.handleInput('parent', 'execsequence', 'startup');
    } else if (ACTIVE_STATES.has(state)) {
        await machine.handleInput('parent', 'execsequence', 'shutdown');
    }
}));

That code is gone. The new path: build profiles → scheduler.planexecutor.replanawait executor.tick() (synchronous first tick) → setInterval(1000) for the rest. The flow / power numbers and the optimizer's pick are unchanged; only the timing of the per-pump commands changed.

If your test fixture relied on commands firing inline during _runDispatch, the new behaviour fires fireAtTickN=0 commands synchronously inside the first await executor.tick() and later ones on the wall-clock interval. Tests that asserted exact timing should use the executor.schedule() introspection getter.

From pre-unit-self-describing demand

The old set.scaling topic and its persistent scaling.current config field have been removed. Each set.demand now carries its own unit context:

Pre Post
set.scaling = "absolute"; set.demand = 80 set.demand = {value: 80, unit: "m3/h"}
set.scaling = "normalized"; set.demand = 50 set.demand = 50 (bare number = %)
set.scaling = "absolute"; set.demand = 0.022 (m³/s) set.demand = {value: 0.022, unit: "m3/s"}

Old flows that still send set.scaling will silently ignore it; the topic is no longer registered.

From prioritypercentagecontrol

The mode prioritypercentagecontrol was retired with the unit-self-describing refactor. Use priorityControl with absolute-unit set.demand payloads, or optimalControl with the same.


Page Why
Home Intuitive overview
Reference — Contracts Topic + config + child filters
Reference — Architecture Code map, dispatch lifecycle, planner internals
Reference — Examples Shipped flows + debug recipes
rotatingMachine — Limitations The child's own limitations (drift, multi-parent, virtual-child stale data)