Files
EVOLV/wiki/Topology-Patterns.md

340 lines
12 KiB
Markdown
Raw Permalink Normal View History

# 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/<softwareType>.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 &mdash; small WWTP
All five patterns combined.
```mermaid
flowchart TB
subgraph PC["Process Cell"]
ps1["pumpingStation &mdash; inlet lift"]
ps2["pumpingStation &mdash; RAS pumping"]
end
subgraph UN["Unit"]
mgc1["MGC inlet"]
mgc2["MGC RAS"]
vgc["VGC effluent split"]
r1["reactor aerobic"]
s1["settler"]
mon["monster &mdash; 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 (&times;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` &rarr; `valveGroupControl` as a parent / child edge. PS does not register VGC. VGC registers PS as a flow source &mdash; the edge goes the other way semantically.
> [!CAUTION]
> `diffuser` &rarr; `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 |