Clone
2
Topology Patterns
znetsixe edited this page 2026-05-11 22:24:29 +02:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Topology Patterns

code-ref verified

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 Lift station, single basin, N pumps load-shared
2. Reactor + diffuser + settler train Biological treatment line
3. Valve group on a distribution manifold Multi-valve flow split with upstream flow context
4. Composite sampling Flow-proportional grab samples for lab analysis
5. Dashboard provisioning Auto-generated Grafana dashboards
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.

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 (idlewarmingupoperationalcoolingdownemergencystop, 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.

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

diffuserreactor 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

reactorsettler 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.

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 1349.


4. Composite sampling

Virtual sensor for downstream lab analysis. monster accumulates samples in a bucket based on integrated flow — a flow-proportional grab sample.

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.

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 — small WWTP

All five patterns combined.

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 (×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

pumpingStationvalveGroupControl 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

diffuserreactor 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.


Page Why
Home Top-level node map
Architecture Three-tier code + generalFunctions API
Topic Conventions What topics flow on each edge
Telemetry Port 0 / 1 / 2 InfluxDB layout