Files
machineGroupControl/wiki/Home.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

machineGroupControl

code-ref s88 status

A machineGroupControl (MGC) coordinates two or more rotatingMachine children that share a common header. It accepts an operator demand setpoint, enumerates the valid pump combinations against the group's live flow/power envelope, picks the best operating point (BEP-Gravitation by default), and schedules per-machine flow setpoints + start/stop commands with timing-aware rendezvous so the running aggregate stays close to demand during transitions.


At a glance

Thing Value
What it represents A pump group sharing one suction + one discharge header
S88 level Unit
Use it when You have 2 + pumps that can substitute for each other on the same header and you want efficient load-sharing
Don't use it for A single pump (wire rotatingMachine directly), valves (use valveGroupControl), or pumps living behind independent headers
Children it accepts machine (rotatingMachine), measurement (pressure / others)
Parent it talks to pumpingStation (typical), or any node that issues set.demand

How it fits

flowchart LR
    parent[pumpingStation<br/>Process Cell]:::pc -->|set.demand| mgc[machineGroupControl<br/>Unit]:::unit
    header[measurement<br/>header pressure]:::ctrl -.measured.-> mgc
    mgc -->|flowmovement / execsequence| m_a[rotatingMachine A]:::equip
    mgc -->|flowmovement / execsequence| m_b[rotatingMachine B]:::equip
    mgc -->|flowmovement / execsequence| m_c[rotatingMachine C]:::equip
    mgc -->|child.register| parent
    m_a -->|child.register| mgc
    m_b -->|child.register| mgc
    m_c -->|child.register| mgc
    classDef pc fill:#0c99d9,color:#fff
    classDef unit fill:#50a8d9,color:#000
    classDef equip fill:#86bbdd,color:#000
    classDef ctrl fill:#a9daee,color:#000

S88 colours are anchored in .claude/rules/node-red-flow-layout.md.


Try it — 3-minute demo

Import the basic example flow, deploy, and watch three pumps come online together when demand rises.

curl -X POST -H 'Content-Type: application/json' \
  --data @nodes/machineGroupControl/examples/01-Basic.json \
  http://localhost:1880/flow

What to click in the dashboard after deploy:

  1. The Setup group auto-fires virtualControl + cmd.startup on each child pump after ~1.5 s.
  2. set.demand = 50 (bare number = percent of group capacity) → MGC picks the best 1- or 2-pump combination by BEP-Gravitation.
  3. set.demand = { value: 80, unit: "m3/h" } → absolute-flow setpoint.
  4. set.mode = priorityControl → equal-flow distribution by priority order.
  5. set.demand = -1 → operator stop-all; turnOffAllMachines cancels any pending dispatch and shuts every active pump down.

Important

GIF needed. Demo recording of demand 50 % → 100 % → -1 with the live status panel. Save as wiki/_partial-gifs/machineGroupControl/01-basic-demo.gif, target ≤ 1 MB after gifsicle -O3 --lossy=80.


The three things you'll send

set.demand is unit-self-describing — the payload itself decides how the value is interpreted. There is no persistent scaling state on the orchestrator.

Topic Aliases Payload What it does
set.mode setMode "optimalControl" | "priorityControl" | "maintenance" Switches dispatch strategy. maintenance is monitoring-only.
set.demand Qd bare number = %; {value, unit} for absolute units (m3/h, l/s, m3/s, …); negative = stop all Operator demand setpoint. Resolves to canonical m³/s before dispatch.
child.register registerChild child node id (string) Manually register a child (Port 2 wiring does this automatically in most flows).

What you'll see come out

Sample Port 0 message (delta-compressed — only changed fields each tick):

{
  "topic": "machineGroupControl#MGC1",
  "payload": {
    "mode": "optimalControl",
    "atEquipment_predicted_flow": 42.5,
    "downstream_predicted_flow": 42.5,
    "atEquipment_predicted_power": 18.0,
    "headerDiffPa": 145000,
    "headerDiffMbar": 1450,
    "flowCapacityMax": 90,
    "flowCapacityMin": 6,
    "machineCount": 3,
    "machineCountActive": 2,
    "absDistFromPeak": 0.02,
    "relDistFromPeak": 0.10
  }
}
Field Meaning
mode Current dispatch mode.
atEquipment_predicted_flow / _power Group aggregate at the pump shafts. The optimizer writes intent here; handlePressureChange keeps it in sync with the live totals.
downstream_predicted_flow Live aggregate mirrored onto DOWNSTREAM — pumpingStation parents subscribe here.
headerDiffPa / headerDiffMbar Last header differential the equalizer resolved. Dashboards use it for Q-H plots without re-reading every child.
flowCapacityMax / flowCapacityMin The group's dynamic envelope at the current header pressure. Defines where set.demand (as %) maps to.
machineCount / machineCountActive All registered children, and how many are in a state other than off / maintenance.
absDistFromPeak / relDistFromPeak Distance from group BEP. relDistFromPeak is undefined when the η spread collapses (homogeneous pump group).

The key shape is <position>_<variant>_<type> — the inverse of rotatingMachine's <type>.<variant>.<position>.<childId> key shape, because MGC's output is the group aggregate, not a per-child snapshot.


The new bit — the movement planner

When MGC computes a new optimal combination it doesn't fan the commands out instantly. It builds a schedule that times each command so the running aggregate stays close to demand during the transition.

flowchart LR
    demand[set.demand] --> dispatch[_runDispatch<br/>latest-wins]
    dispatch --> abort[abortActiveMovements]
    abort --> opt[optimizer.calcBestCombination*]
    opt --> profiles[buildProfile<br/>x children]
    profiles --> plan[movementScheduler.plan<br/>rendezvous t* = max&#40;eta_i&#41;]
    plan --> exec[movementExecutor.replan<br/>+ await tick&#40;&#41;]
    exec --> kids[rotatingMachine x N<br/>flowmovement / execsequence]

The planner classifies each pump's required move (startup / flowmove / shutdown / noop), computes an ETA per move via MoveTrajectory, sets the rendezvous time t* = max(eta_i) over flow-INCREASING moves, and delays flow-DECREASING moves so they FINISH at t*. Net effect: the sum of flows tracks the demand smoothly during the transition; on overshoot the header pressure rises and self-corrects.

This path is exercised in optimalControl mode. priorityControl mode still uses the legacy direct-dispatch path (control.equalFlowControl) — the planner has not been wired through there yet.


Need more?

Page What you'll find
Reference — Contracts Topic registry, config schema, child registration filters
Reference — Architecture Code map, dispatch lifecycle, planner internals, output ports
Reference — Examples Shipped flows, debug recipes
Reference — Limitations When not to use, known issues, open questions

EVOLV master wiki · Topology Patterns · Topic Conventions