# Reference — Limitations ![code-ref](https://img.shields.io/badge/code--ref-26e92b5-blue) > [!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: ```js 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](https://gitea.wbd-rd.nl/RnD/machineGroupControl/issues/1) | | Wire the movement planner through `priorityControl` | Internal — not yet ticketed | | Remove the `mgc.scaling` artifact + the `scaling` badge field | Internal | | Rename `maxEfficiency` → `meanGroupCog` 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 226–239 in `26e92b5^`): ```js 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.plan` → `executor.replan` → `await 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. --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Intuitive overview | | [Reference — Contracts](Reference-Contracts) | Topic + config + child filters | | [Reference — Architecture](Reference-Architecture) | Code map, dispatch lifecycle, planner internals | | [Reference — Examples](Reference-Examples) | Shipped flows + debug recipes | | [rotatingMachine — Limitations](https://gitea.wbd-rd.nl/RnD/rotatingMachine/wiki/Reference-Limitations) | The child's own limitations (drift, multi-parent, virtual-child stale data) |