# Topology Patterns ![code-ref](https://img.shields.io/badge/code--ref-9ab9f6b-blue) ![verified](https://img.shields.io/badge/edges-verified_against_configure()-brightgreen) > [!NOTE] > Five canonical plant configurations and one worked example that combines them. Every edge in every diagram was checked against the parent's `configure()` declaration in source. Use these as templates when wiring your own plant. --- ## Pattern index | Pattern | When to use it | |:---|:---| | [1. Pumping station with grouped pumps](#1-pumping-station-with-grouped-pumps) | Lift station, single basin, N pumps load-shared | | [2. Reactor + diffuser + settler train](#2-reactor--diffuser--settler-train) | Biological treatment line | | [3. Valve group on a distribution manifold](#3-valve-group-on-a-distribution-manifold) | Multi-valve flow split with upstream flow context | | [4. Composite sampling](#4-composite-sampling) | Flow-proportional grab samples for lab analysis | | [5. Dashboard provisioning](#5-dashboard-provisioning) | Auto-generated Grafana dashboards | | [Worked example — small WWTP](#worked-example--small-wwtp) | All five patterns combined | --- ## 1. Pumping station with grouped pumps The canonical wet-well lift station: one basin model, one demand controller (`pumpingStation`), one load-sharing coordinator (`machineGroupControl`), N pumps (`rotatingMachine` × N), measurements for level + flow + per-pump pressure. ```mermaid flowchart TB subgraph PC["Process Cell"] ps[pumpingStation] end subgraph UN["Unit"] mgc[machineGroupControl] end subgraph EM["Equipment Module"] rmA[rotatingMachine A] rmB[rotatingMachine B] rmC[rotatingMachine C] end subgraph CM["Control Module"] ml["measurement — level"] mfin["measurement — inflow"] mpA["measurement — pressure A"] mpB["measurement — pressure B"] mpC["measurement — pressure C"] end ps --> mgc mgc --> rmA mgc --> rmB mgc --> rmC ml -. data .-> ps mfin -. data .-> ps mpA -. data .-> rmA mpB -. data .-> rmB mpC -. data .-> rmC class ps pc class mgc unit class rmA,rmB,rmC equip class ml,mfin,mpA,mpB,mpC ctrl classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px classDef ctrl fill:#a9daee,color:#000,stroke:#76b7d4,stroke-width:2px ``` ### Data flow | Stage | What happens | |:---|:---| | Basin integration | `pumpingStation` integrates basin volume from inflow / outflow rates | | Demand computation | `pumpingStation` computes a demand setpoint and dispatches it to `machineGroupControl` | | Per-pump operating point | `machineGroupControl` solves a per-pump operating point using each pump's characteristic curve plus measured upstream pressure | | Pump dispatch | Each `rotatingMachine` runs its own FSM (`idle` → `warmingup` → `operational` → `coolingdown` → `emergencystop`, plus `accelerating` / `decelerating`) and predicts flow + power from speed + pressure | ### Variants | Variant | How to wire | |:---|:---| | Single pump (no MGC) | `pumpingStation.configure()` accepts `machine` directly — skip the MGC and parent the `rotatingMachine` under `pumpingStation` | | Cascaded stations | `pumpingStation.configure()` accepts `pumpingstation` as a child — downstream PS registers upstream PS to read its predicted outflow | --- ## 2. Reactor + diffuser + settler train Biological treatment line. `reactor` runs ASM kinetics (CSTR or PFR engine, set via `config.reactor_type`). `diffuser` injects OTR. `settler` clarifies the effluent and drives a return pump. ```mermaid flowchart TB subgraph UN["Unit"] reactor[reactor] settler[settler] end subgraph EM["Equipment Module"] diff[diffuser] rp["rotatingMachine — return pump"] end subgraph CM["Control Module"] mt["measurement — temperature"] mdo["measurement — dissolved O2"] mts["measurement — TSS"] end reactor ==stateChange==> settler diff -. OTR data .-> reactor settler -->|return pump| rp mt -. data .-> reactor mdo -. data .-> reactor mts -. data .-> settler mdo -. data .-> diff class reactor,settler unit class diff,rp equip class mt,mdo,mts ctrl classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px classDef ctrl fill:#a9daee,color:#000,stroke:#76b7d4,stroke-width:2px ``` ### Two non-standard wirings > [!IMPORTANT] > `diffuser` → `reactor` is data-only. Diffuser fires `data.otr` on its emitter; reactor subscribes via `emitter.on('otr', ...)`. There is no `child.register` handshake between them. See `nodes/reactor/src/specificClass.js` `configure()`. > [!IMPORTANT] > `reactor` → `settler` is a `stateChange` subscription, not a parent / child edge. Settler's `_connectReactor` attaches `emitter.on('stateChange', ...)` to pull effluent composition from the upstream reactor. The `reactor` softwareType is registered as a child of settler even though the reactor is semantically upstream. > [!CAUTION] > DO setpoint feedback is not automatic. A measured-DO → diffuser-airflow loop must be closed externally (a function node) or via a `valveGroupControl` upstream of an airflow valve. --- ## 3. Valve group on a distribution manifold Multi-valve flow distribution. `valveGroupControl` computes per-valve K_v shares to satisfy a target split while respecting upstream flow availability. ```mermaid flowchart TB subgraph PC["Process Cell"] ps["pumpingStation — upstream flow source"] end subgraph UN["Unit"] vgc[valveGroupControl] end subgraph EM["Equipment Module"] vA[valve A] vB[valve B] vC[valve C] end ps -. flow source .-> vgc vgc --> vA vgc --> vB vgc --> vC class ps pc class vgc unit class vA,vB,vC equip classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px ``` > [!IMPORTANT] > VGC's child types are unusual. `valveGroupControl.configure()` registers five softwareTypes: > - `valve` — actual S88 child relationship (VGC controls these) > - `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol` — flow sources. VGC reads upstream flow availability when computing splits. Semantic is "VGC knows about this flow producer", not "VGC controls it". > > See `nodes/valveGroupControl/src/specificClass.js` lines 13–49. --- ## 4. Composite sampling Virtual sensor for downstream lab analysis. `monster` accumulates samples in a bucket based on integrated flow — a flow-proportional grab sample. ```mermaid flowchart TB subgraph UN["Unit"] monster[monster] end subgraph CM["Control Module"] mflow["measurement — flow (assetType MUST be 'flow')"] mq["measurement — any quality (e.g. NH4, COD)"] end mflow -. data .-> monster mq -. data .-> monster class monster unit class mflow,mq ctrl classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px classDef ctrl fill:#a9daee,color:#000,stroke:#76b7d4,stroke-width:2px ``` > [!WARNING] > Two gotchas: > 1. `measurement.config.asset.type` must be exactly `"flow"`. A value like `"flow-electromagnetic"` is silently ignored by monster's child router. > 2. `monster.config.constraints.flowmeter` exists in the schema but is not forwarded by `buildDomainConfig`. Toggling proportional-vs-time mode has no runtime effect. Tracked in `.claude/refactor/OPEN_QUESTIONS.md`. --- ## 5. Dashboard provisioning `dashboardAPI` doesn't operate on data — it generates Grafana dashboards. Any node registers via `child.register`; dashboardAPI composes a dashboard JSON from softwareType plus measurements and POSTs to Grafana's HTTP API. ```mermaid flowchart LR subgraph EVOLV["EVOLV process nodes (any softwareType)"] direction TB ps[pumpingStation] mgc[machineGroupControl] rm[rotatingMachine] end subgraph UT["Utility"] dash[dashboardAPI] end grafana[("Grafana HTTP API")] ps -. child.register .-> dash mgc -. child.register .-> dash rm -. child.register .-> dash dash ==>|POST /api/dashboards/db| grafana class ps pc class mgc unit class rm equip class dash util class grafana ext classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px classDef util fill:#dddddd,color:#000,stroke:#a8a8a8,stroke-width:2px classDef ext fill:#fff2cc,color:#000,stroke:#aa8400,stroke-width:2px ``` | Behaviour | Detail | |:---|:---| | What it accepts | Any softwareType on `child.register` | | What it emits | One HTTP POST per registered child, payload from `nodes/dashboardAPI/src/config/templates/.json` | | Auth | Bearer token in `config.grafanaConnector.bearerToken` (when set) | | `meta` envelope | `{nodeId, softwareType, uid, title}` for correlating responses | | Architecture variance | The one node in the platform that does not extend `BaseDomain`. Documented in `.claude/refactor/OPEN_QUESTIONS.md` | --- ## Worked example — small WWTP All five patterns combined. ```mermaid flowchart TB subgraph PC["Process Cell"] ps1["pumpingStation — inlet lift"] ps2["pumpingStation — RAS pumping"] end subgraph UN["Unit"] mgc1["MGC inlet"] mgc2["MGC RAS"] vgc["VGC effluent split"] r1["reactor aerobic"] s1["settler"] mon["monster — composite sampler"] end subgraph EM["Equipment Module"] rm1["pump A"] rm2["pump B"] rm3["RAS pump"] d1["diffuser"] v1["valve 1"] v2["valve 2"] end ps1 --> mgc1 mgc1 --> rm1 mgc1 --> rm2 ps2 --> mgc2 mgc2 --> rm3 r1 ==stateChange==> s1 s1 -->|return pump| rm3 d1 -. OTR .-> r1 ps2 -. flow source .-> vgc vgc --> v1 vgc --> v2 class ps1,ps2 pc class mgc1,mgc2,vgc,r1,s1,mon unit class rm1,rm2,rm3,d1,v1,v2 equip classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px classDef equip fill:#86bbdd,color:#000,stroke:#5a90b2,stroke-width:2px ``` | Sub-pattern | Recognise it | |:---|:---| | Pumping station with grouped pumps (×2) | `ps1 -> mgc1 -> {rm1, rm2}` and `ps2 -> mgc2 -> rm3` | | Reactor + settler train | `r1 ==stateChange==> s1` plus `d1 -. OTR .-> r1` | | Valve group on flow source | `ps2 -. flow source .-> vgc -> {v1, v2}` | | Settler return pump | `s1 -> rm3` | | Composite sampling | `mon` (would be wired to inflow + quality measurements not drawn) | Every edge here is reproducible from one of the patterns above. --- ## Anti-patterns > [!CAUTION] > `pumpingStation` → `valveGroupControl` as a parent / child edge. PS does not register VGC. VGC registers PS as a flow source — the edge goes the other way semantically. > [!CAUTION] > `diffuser` → `reactor` as a child registration. Diffuser emits OTR via its emitter; reactor subscribes via `emitter.on`. No `child.register` handshake. > [!CAUTION] > `measurement` parented under `dashboardAPI`. dashboardAPI accepts any node for Grafana provisioning, but `measurement` should register with the process node it is monitoring, not with dashboardAPI. --- ## Related pages | Page | Why | |:---|:---| | [Home](Home) | Top-level node map | | [Architecture](Architecture) | Three-tier code + generalFunctions API | | [Topic Conventions](Topic-Conventions) | What topics flow on each edge | | [Telemetry](Telemetry) | Port 0 / 1 / 2 InfluxDB layout |