Compare commits
1 Commits
bb2f3bea82
...
045a941ab4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
045a941ab4 |
@@ -4,7 +4,10 @@
|
|||||||
"description": "Control module machineGroupControl",
|
"description": "Control module machineGroupControl",
|
||||||
"main": "mgc.js",
|
"main": "mgc.js",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js"
|
"test": "node --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.js",
|
||||||
|
"wiki:contract": "node ../generalFunctions/scripts/wikiGen.js contract ./src/commands/index.js --write ./wiki/Home.md",
|
||||||
|
"wiki:datamodel": "node ../generalFunctions/scripts/wikiGen.js datamodel ./src/specificClass.js --write ./wiki/Home.md",
|
||||||
|
"wiki:all": "npm run wiki:contract && npm run wiki:datamodel"
|
||||||
},
|
},
|
||||||
"repository": {
|
"repository": {
|
||||||
"type": "git",
|
"type": "git",
|
||||||
|
|||||||
277
wiki/Home.md
Normal file
277
wiki/Home.md
Normal file
@@ -0,0 +1,277 @@
|
|||||||
|
# machineGroupControl
|
||||||
|
|
||||||
|
> **Reflects code as of `afc304b` · regenerated `2026-05-11` via `npm run wiki:all`**
|
||||||
|
> If this banner is stale, the page may be out of date. Treat as informative, not authoritative.
|
||||||
|
|
||||||
|
## 1. What this node is
|
||||||
|
|
||||||
|
**machineGroupControl (MGC)** is an S88 Unit orchestrator that coordinates multiple `rotatingMachine` children sharing a common header. It receives a demand setpoint, evaluates valid pump combinations against the group's totals and curves, picks the best operating point (BEP-Gravitation or NCog), and dispatches per-machine flow setpoints + start/stop commands.
|
||||||
|
|
||||||
|
## 2. Position in the platform
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
parent[pumpingStation<br/>Process Cell]:::pc -->|set.demand| mgc[machineGroupControl<br/>Unit]:::unit
|
||||||
|
header[measurement<br/>header pressure]:::ctrl -.data.-> 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: Process Cell `#0c99d9`, Unit `#50a8d9`, Equipment `#86bbdd`, Control Module `#a9daee`. Source of truth: `.claude/rules/node-red-flow-layout.md`.
|
||||||
|
|
||||||
|
## 3. Capability matrix
|
||||||
|
|
||||||
|
| Capability | Status | Notes |
|
||||||
|
|---|---|---|
|
||||||
|
| Aggregate group flow / power totals | ✅ | `TotalsCalculator` — absolute and dynamic. |
|
||||||
|
| Valid-combination enumeration | ✅ | `combinatorics/pumpCombinations`. |
|
||||||
|
| Best-combination optimiser (BEP-Gravitation) | ✅ | Directional or symmetric variant. |
|
||||||
|
| Best-combination optimiser (NCog) | ✅ | Normalised cost-of-goods score. |
|
||||||
|
| Priority / equal-flow control | ✅ | `mode='prioritycontrol'`. |
|
||||||
|
| Priority percentage control | ✅ | Requires `scaling='normalized'`. |
|
||||||
|
| Optimal control | ✅ | `mode='optimalcontrol'`. |
|
||||||
|
| Group efficiency + BEP distance | ✅ | `GroupEfficiency`. |
|
||||||
|
| Header-pressure equalisation | ✅ | `operatingPoint.equalize()`. |
|
||||||
|
| Demand serialisation (latest-wins) | ✅ | Inline gate; deferred call drains on completion. |
|
||||||
|
| Forced shutdown on `Qd ≤ 0` | ✅ | `turnOffAllMachines()`. |
|
||||||
|
|
||||||
|
## 4. Code map
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph nodeRED["nodeClass.js — adapter (BaseNodeAdapter)"]
|
||||||
|
nc["buildDomainConfig()<br/>static DomainClass, commands"]
|
||||||
|
end
|
||||||
|
subgraph domain["specificClass.js — orchestrator (BaseDomain)"]
|
||||||
|
sc["MachineGroup.configure()<br/>ChildRouter rules<br/>handleInput() dispatch gate"]
|
||||||
|
end
|
||||||
|
subgraph concerns["src/ concern modules"]
|
||||||
|
groupOps["groupOps/<br/>GroupOperatingPoint + curves"]
|
||||||
|
totals["totals/<br/>TotalsCalculator"]
|
||||||
|
combi["combinatorics/<br/>validPumpCombinations"]
|
||||||
|
opt["optimizer/<br/>BEP-Grav / NCog selectors"]
|
||||||
|
efficiency["efficiency/<br/>GroupEfficiency + BEP dist"]
|
||||||
|
dispatch["control/<br/>strategies (equalFlow / prioPct)"]
|
||||||
|
io["io/<br/>output + status"]
|
||||||
|
commands["commands/<br/>topic registry + handlers"]
|
||||||
|
end
|
||||||
|
nc --> sc
|
||||||
|
sc --> groupOps
|
||||||
|
sc --> totals
|
||||||
|
sc --> combi
|
||||||
|
sc --> opt
|
||||||
|
sc --> efficiency
|
||||||
|
sc --> dispatch
|
||||||
|
sc --> io
|
||||||
|
nc --> commands
|
||||||
|
```
|
||||||
|
|
||||||
|
| Module | Owns | Read first if you're changing… |
|
||||||
|
|---|---|---|
|
||||||
|
| `groupOps/` | Group operating point + child read helpers | Header pressure handling, child measurement plumbing. |
|
||||||
|
| `totals/` | Absolute + dynamic flow/power totals | Demand clamping, totals math. |
|
||||||
|
| `combinatorics/` | Enumeration of valid pump subsets | Which combinations are considered eligible. |
|
||||||
|
| `optimizer/` | Best-combination selectors | Optimiser selection method, scoring math. |
|
||||||
|
| `efficiency/` | Group efficiency, BEP distance | BEP gravitation tuning, peak math. |
|
||||||
|
| `control/strategies.js` | Per-mode dispatch (priority, prioPct) | Mode behaviour, priorityList usage. |
|
||||||
|
| `dispatch/` | Demand fan-out helpers (legacy alongside inline gate) | Serialisation, mid-flight overrides. |
|
||||||
|
| `commands/` | Input-topic registry and handlers | New input topics, payload validation. |
|
||||||
|
| `io/` | `getOutput`, `getStatusBadge` | Output shape, dashboard badge. |
|
||||||
|
|
||||||
|
## 5. Topic contract
|
||||||
|
|
||||||
|
> **Auto-generated** from `src/commands/index.js`. Do NOT hand-edit between the markers. Re-run `npm run wiki:contract`.
|
||||||
|
|
||||||
|
<!-- BEGIN AUTOGEN: topic-contract -->
|
||||||
|
|
||||||
|
| Canonical topic | Aliases | Payload | Effect |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `set.mode` | `setMode` | `string` | Replaces the named state value with the supplied payload. |
|
||||||
|
| `set.scaling` | `setScaling` | `string` | Replaces the named state value with the supplied payload. |
|
||||||
|
| `child.register` | `registerChild` | `string` | Parent/child plumbing — registers or unregisters a child node. |
|
||||||
|
| `set.demand` | `Qd` | `any` | Replaces the named state value with the supplied payload. |
|
||||||
|
|
||||||
|
<!-- END AUTOGEN: topic-contract -->
|
||||||
|
|
||||||
|
## 6. Child registration
|
||||||
|
|
||||||
|
`ChildRouter` declarations in `specificClass.js → configure()`.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
subgraph kids["accepted children (softwareType)"]
|
||||||
|
mach["machine<br/>(rotatingMachine)"]:::equip
|
||||||
|
m["measurement<br/>(header pressure)"]:::ctrl
|
||||||
|
end
|
||||||
|
mach -->|"pressure.measured.downstream<br/>pressure.measured.differential<br/>flow.predicted.downstream"| eq[operatingPoint.equalize<br/>+ totals refresh]
|
||||||
|
m -->|"<type>.measured.<position>"| mirror[mirror into own<br/>MeasurementContainer]
|
||||||
|
mirror -->|"if type === 'pressure'"| eq
|
||||||
|
eq --> emit[notifyOutputChanged]
|
||||||
|
classDef equip fill:#86bbdd,color:#000
|
||||||
|
classDef ctrl fill:#a9daee,color:#000
|
||||||
|
```
|
||||||
|
|
||||||
|
| softwareType | filter / subscribed events | Side-effect |
|
||||||
|
|---|---|---|
|
||||||
|
| `machine` | onRegister stores in `this.machines[id]`; subscribes to `pressure.measured.downstream`, `pressure.measured.differential`, `flow.predicted.downstream` | `handlePressureChange()` — equalise + recompute totals + recompute group efficiency. |
|
||||||
|
| `measurement` | onRegister attaches listener for `<asset.type>.measured.<positionVsParent>` | Mirror value into MGC's own MeasurementContainer; pressure also triggers `handlePressureChange()`. |
|
||||||
|
|
||||||
|
## 7. Lifecycle — what one event does
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant parent as pumpingStation
|
||||||
|
participant mgc as MGC
|
||||||
|
participant op as GroupOperatingPoint
|
||||||
|
participant tot as TotalsCalculator
|
||||||
|
participant opt as optimizer
|
||||||
|
participant kids as rotatingMachine[]
|
||||||
|
|
||||||
|
parent->>mgc: set.demand (Qd)
|
||||||
|
Note over mgc: dispatch gate — latest-wins
|
||||||
|
mgc->>mgc: abortActiveMovements('new demand')
|
||||||
|
mgc->>tot: calcDynamicTotals()
|
||||||
|
mgc->>mgc: clamp Qd to [minFlow, maxFlow]
|
||||||
|
alt mode=optimalcontrol
|
||||||
|
mgc->>mgc: validPumpCombinations(Qd)
|
||||||
|
mgc->>opt: pick best (BEP-Grav | NCog)
|
||||||
|
opt-->>mgc: bestCombination + bestFlow/Power
|
||||||
|
mgc->>kids: flowmovement (per-pump flow)
|
||||||
|
mgc->>kids: execsequence (startup / shutdown)
|
||||||
|
else mode=prioritycontrol
|
||||||
|
mgc->>mgc: equalFlowControl(Qd, powerCap, priorityList)
|
||||||
|
end
|
||||||
|
mgc->>op: writeOwn flow/power predicted (AT_EQUIPMENT + DOWNSTREAM)
|
||||||
|
mgc->>mgc: notifyOutputChanged()
|
||||||
|
```
|
||||||
|
|
||||||
|
## 8. Data model — `getOutput()`
|
||||||
|
|
||||||
|
What lands on Port 0. Composed in `io/output.js → getOutput(this)` and delta-compressed by `outputUtils.formatMsg`.
|
||||||
|
|
||||||
|
<!-- BEGIN AUTOGEN: data-model -->
|
||||||
|
|
||||||
|
| Key | Type | Unit | Sample |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `absDistFromPeak` | number | — | `0` |
|
||||||
|
| `mode` | string | — | `"optimalcontrol"` |
|
||||||
|
| `relDistFromPeak` | number | — | `0` |
|
||||||
|
| `scaling` | string | — | `"normalized"` |
|
||||||
|
|
||||||
|
<!-- END AUTOGEN: data-model -->
|
||||||
|
|
||||||
|
**Concrete sample** (excerpt — see live test output for the canonical shape):
|
||||||
|
|
||||||
|
~~~json
|
||||||
|
{
|
||||||
|
"mode": "optimalcontrol",
|
||||||
|
"scaling": "normalized",
|
||||||
|
"flow.predicted.atequipment.<nodeId>": 0.0125,
|
||||||
|
"flow.predicted.downstream.<nodeId>": 0.0125,
|
||||||
|
"power.predicted.atequipment.<nodeId>": 1800,
|
||||||
|
"efficiency.predicted.atequipment.<nodeId>": 0.65,
|
||||||
|
"absDistFromPeak": 0.02,
|
||||||
|
"relDistFromPeak": 0.10
|
||||||
|
}
|
||||||
|
~~~
|
||||||
|
|
||||||
|
The `<nodeId>` segment is the Node-RED node id assigned at deploy time.
|
||||||
|
|
||||||
|
## 9. Configuration — editor form ↔ config keys
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
subgraph editor["Node-RED editor form"]
|
||||||
|
f1[Control mode dropdown]
|
||||||
|
f2[Scaling dropdown]
|
||||||
|
f3[Optimisation method]
|
||||||
|
f4[Output unit (flow)]
|
||||||
|
f5[Position vs parent]
|
||||||
|
f6[Allowed sources / actions per mode]
|
||||||
|
end
|
||||||
|
subgraph cfg["Domain config slice"]
|
||||||
|
c1[mode.current]
|
||||||
|
c2[scaling.current]
|
||||||
|
c3[optimization.method]
|
||||||
|
c4[general.unit]
|
||||||
|
c5[functionality.positionVsParent]
|
||||||
|
c6[mode.allowedSources<br/>mode.allowedActions]
|
||||||
|
end
|
||||||
|
f1 --> c1
|
||||||
|
f2 --> c2
|
||||||
|
f3 --> c3
|
||||||
|
f4 --> c4
|
||||||
|
f5 --> c5
|
||||||
|
f6 --> c6
|
||||||
|
```
|
||||||
|
|
||||||
|
| Form field | Config key | Default | Range | Where used |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Control mode | `mode.current` | `optimalControl` | enum (`prioritycontrol`, `prioritypercentagecontrol`, `optimalcontrol`) | dispatch switch in `_runDispatch` |
|
||||||
|
| Scaling | `scaling.current` | `normalized` | enum (`absolute`, `normalized`) | demand mapping in `_runDispatch` |
|
||||||
|
| Optimisation method | `optimization.method` | `BEP-Gravitation-Directional` | enum (`NCog`, `BEP-Gravitation`, `BEP-Gravitation-Directional`) | `_optimalControl` selector |
|
||||||
|
| Output unit (flow) | `general.unit` | `m3/h` | unit string | unit policy `output.flow` |
|
||||||
|
| Position vs parent | `functionality.positionVsParent` | `atEquipment` | enum | event suffix for parent subscription |
|
||||||
|
|
||||||
|
## 10. State chart
|
||||||
|
|
||||||
|
MGC is **event-driven and stateless** with respect to operating modes — there is no FSM. The closest thing to "state" is the dispatch gate. Diagram for that single state vector:
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> idle_disp: configure()
|
||||||
|
idle_disp --> dispatching: handleInput(Qd)
|
||||||
|
dispatching --> idle_disp: dispatch complete
|
||||||
|
dispatching --> dispatching: handleInput(Qd) — deferred and re-fired on completion
|
||||||
|
dispatching --> turning_off: Qd <= 0
|
||||||
|
turning_off --> idle_disp: all machines acknowledged shutdown
|
||||||
|
```
|
||||||
|
|
||||||
|
While `dispatching`, additional `handleInput` calls overwrite `_delayedCall` (latest-wins); the gate drains the latest one on completion. `turnOffAllMachines()` clears `_delayedCall` to make turn-off the final intent.
|
||||||
|
|
||||||
|
## 11. Examples
|
||||||
|
|
||||||
|
| Tier | File | What it shows | Status |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Basic | `examples/basic.flow.json` | Single MGC + 2 pumps, manual setDemand | ⚠️ legacy shape, pre-refactor |
|
||||||
|
| Integration | `examples/integration.flow.json` | MGC wired under pumpingStation | ⚠️ legacy shape, pre-refactor |
|
||||||
|
| Edge | `examples/edge.flow.json` | Mid-flight demand override + abort | ⚠️ legacy shape, pre-refactor |
|
||||||
|
|
||||||
|
Tier 1/2/3 visual-first example flows are still TODO (see `MEMORY.md` "TODO: Example Flows"). Screenshots will land under `wiki/_partial-screenshots/machineGroupControl/` when the new flows ship.
|
||||||
|
|
||||||
|
## 12. Debug recipes
|
||||||
|
|
||||||
|
| Symptom | First thing to check | Where to look |
|
||||||
|
|---|---|---|
|
||||||
|
| No combination selected | Demand outside `[dynamicTotals.flow.min, max]` — clamped on entry; `_optimalControl` returns early if combinations empty. | `validPumpCombinations` + warn log. |
|
||||||
|
| Group flow stuck at zero | Machines never reach an `ACTIVE_STATE` — check per-pump startup logs. | `isMachineActive`. |
|
||||||
|
| Priority-percentage mode warns and exits | Mode requires `scaling='normalized'`. Set both. | `_runDispatch` switch. |
|
||||||
|
| Stale flow setpoints on chained calls | Dispatch gate may have collapsed multiple calls — confirm `_delayedCall` was honoured. | `handleInput` finally block. |
|
||||||
|
| Header pressure not equalising | Pressure children must register with `asset.type='pressure'` and a matching position. | `operatingPoint.equalize`. |
|
||||||
|
| Optimiser picks unexpected combo | Verify `optimization.method` and per-method scoring (NCog vs BEP-Grav). | `optimizer/`. |
|
||||||
|
|
||||||
|
> Never ship `enableLog: 'debug'` in a demo — fills the container log within seconds and obscures real errors.
|
||||||
|
|
||||||
|
## 13. When you would NOT use this node
|
||||||
|
|
||||||
|
- Don't use MGC for a **single pump** — wire `rotatingMachine` directly. MGC's combinatorics + totals add no value below N=2.
|
||||||
|
- Don't use MGC for **valves** — use `valveGroupControl`. MGC's optimiser assumes a flow-vs-pressure characteristic curve.
|
||||||
|
- Don't use MGC when the pumps live behind **independent headers** — combinations assume a shared discharge / suction pressure.
|
||||||
|
|
||||||
|
## 14. Known limitations / current issues
|
||||||
|
|
||||||
|
| # | Issue | Tracked in |
|
||||||
|
|---|---|---|
|
||||||
|
| 1 | `optimalControl` requires every machine to expose a curve — null-curve members silently exclude themselves from combinations. | `combinatorics/pumpCombinations`. |
|
||||||
|
| 2 | Mid-flight setpoint overrides on `accelerating` / `decelerating` rely on `abortActiveMovements` per dispatch — a sequence with no awaitable `abortMovement` will warn but proceed. | `abortActiveMovements`. |
|
||||||
|
| 3 | Power-cap parameter exposed but not surfaced as a topic input — only programmatic via `handleInput(source, demand, powerCap)`. | `commands/index.js` — no canonical topic. |
|
||||||
|
| 4 | Tier 1/2/3 visual-first example flows not yet written. | P9 follow-up. |
|
||||||
Reference in New Issue
Block a user