diff --git a/nodes/generalFunctions b/nodes/generalFunctions index c7e561e..1b6b433 160000 --- a/nodes/generalFunctions +++ b/nodes/generalFunctions @@ -1 +1 @@ -Subproject commit c7e561e5938f7710d96d0e5f93dbd4cbd7a1d0fa +Subproject commit 1b6b43349fd4cc45860f1bb23d5c1ce0d35fb7fa diff --git a/nodes/monster b/nodes/monster index 53c25f2..59ff4d2 160000 --- a/nodes/monster +++ b/nodes/monster @@ -1 +1 @@ -Subproject commit 53c25f2d10c50a3ab21dffbf5d82d68d4b3652a4 +Subproject commit 59ff4d230eedf90e2578f64515fc5780699b3ebc diff --git a/nodes/pumpingStation b/nodes/pumpingStation index 8507ee4..fe5fa35 160000 --- a/nodes/pumpingStation +++ b/nodes/pumpingStation @@ -1 +1 @@ -Subproject commit 8507ee4e02cece93ae9a449d6ca8be3ead23942b +Subproject commit fe5fa3577b388a12fcc60eef4fab02d7659bcb5c diff --git a/wiki/Functional-Overview.md b/wiki/Functional-Overview.md new file mode 100644 index 0000000..54ea89d --- /dev/null +++ b/wiki/Functional-Overview.md @@ -0,0 +1,626 @@ +# Functional Overview + +![code-ref](https://img.shields.io/badge/code--ref-5ae8788-blue) +![view](https://img.shields.io/badge/view-process_%2F_water_side-orange) + +> [!NOTE] +> What each EVOLV node physically represents and what control objective it serves. Companion to [Architecture](Architecture), which describes the **code** shape; this page describes the **process** the code models. Read this when you want to know "what is actually happening in the water", not "what is happening in the JavaScript". + +--- + +## Plant-level process flow + +Real water flows left to right through real equipment. EVOLV models a subset of that equipment as Node-RED nodes. The coloured nodes below are modelled by EVOLV; the grey ones are upstream / downstream of the EVOLV-modelled section. + +```mermaid +flowchart LR + inf["Influent"]:::extern + scr["Coarse screens"]:::extern + grit["Grit chamber"]:::extern + ps1["Inlet lift station"]:::pc + primary["Primary settler"]:::extern + reactor["Aerobic reactor"]:::unit + secondary["Secondary settler"]:::unit + disinf["Disinfection / UV"]:::extern + eff["Effluent"]:::extern + sludge["Sludge handling"]:::extern + + inf --> scr --> grit --> ps1 --> primary --> reactor --> secondary --> disinf --> eff + secondary -. RAS .-> reactor + secondary -. WAS .-> sludge + + classDef pc fill:#0c99d9,color:#fff,stroke:#075a82,stroke-width:2px + classDef unit fill:#50a8d9,color:#000,stroke:#2c7ba8,stroke-width:2px + classDef extern fill:#f0f0f0,color:#666,stroke:#bbb,stroke-width:1px,stroke-dasharray:4 4 +``` + +### Plant step to EVOLV node mapping + +| Plant step | What happens physically | EVOLV node(s) | +|:---|:---|:---| +| Inlet lift station | Wet-well buffer; pumps raise water to plant elevation | pumpingStation + machineGroupControl + rotatingMachine | +| Aerobic reactor | Bacteria consume NH4, COD under aeration; O2 supplied by diffusers | reactor + diffuser + measurement | +| Secondary settler | Biomass settles by gravity; clean water overflows; sludge returns or is wasted | settler + rotatingMachine (RAS pump) | +| Flow distribution | Multi-valve manifold for effluent split, airflow distribution, RAS proportioning | valveGroupControl + valve | +| Composite sampling | Flow-proportional sample buildup for lab analysis | monster + measurement | +| Telemetry to UI | Time-series storage + dashboard provisioning | dashboardAPI (Grafana) + Port 1 to InfluxDB | + +--- + +## pumpingStation — wet-well lift station + +### Physical view + +A wet-well buffer at low elevation. Inflow arrives by gravity from the upstream sewer; pumps lift water to plant elevation. Level is the controlled variable. + +``` + inflow + | + v + +---------------------+ + | | <-- overflow weir (safety, alarm) + | | + | ~ ~ ~ ~ ~ ~ ~ ~ ~ | <-- startLevel (e.g. 80% of basin) + | | + | | <-- band where 1 pump runs + | | + | ~ ~ ~ ~ ~ ~ ~ ~ ~ | <-- stopLevel (e.g. 30%) + | | + | | <-- band where pumps idle + | | + | ___________________| <-- dryRunLevel (cutoff, alarm) + | | | | | | + | v v v v | + | P1 P2 P3 P4 | + +--|----|----|----|---+ + v v v v + outflow (to next stage) +``` + +### Process variables + +| Variable | Typical range | Unit | Source | +|:---|:---|:---|:---| +| Inflow Q_in | 0 to 2000 | m3/h | gravity sewer; measured by inlet flow meter | +| Outflow Q_out | 0 to 2000 | m3/h | sum of running pump flows | +| Level | 0 to 5 | m | level sensor (radar, ultrasonic, hydrostatic) | +| Volume | 0 to V_max | m3 | basin geometry × level | +| Predicted ETA full / empty | seconds | s | (V_max − V) / (Q_in − Q_out) | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Keep level in operating band | Schmitt-trigger hysteresis: start pumps at `startLevel`, stop at `stopLevel` | +| Avoid rapid pump cycling | `stopLevel` strictly below `startLevel` (deadband) | +| Avoid overflow | High-level alarm; safety controller can override | +| Avoid running dry | Low-level cutoff stops all pumps | +| Stay near pump BEP | Demand is shared by machineGroupControl using each pump's curve | + +### EVOLV implementation + +- `pumpingStation.configure()` builds a `BasinGeometry` from `config.basin` plus a `FlowAggregator` that integrates volume from inflow / outflow measurements per tick. +- Level + inflow + outflow arrive as `measurement` children with positions `inflow`, `outflow`, `atequipment` for the level. +- The `control` strategy module picks one of several modes (level-based, flow-based, time-based). The level-based strategy implements the Schmitt trigger and shifted-ramp behaviour. +- `SafetyController` guards against panic / dry-run / overfill conditions and can block dispatch. + +--- + +## rotatingMachine — single pump or compressor + +### Physical view + +A centrifugal pump (or compressor) characterised by its supplier curves. Speed sets where on the curve the machine operates. The operating point is the intersection of pump curve and system curve. + +``` + Q-H curve (head vs flow) Q-P curve (power vs flow) + ^ ^ + H | * P | * + | * | * + | * <-- BEP | * + | * | * <-- BEP region + | * | * + |________________ Q |____________________ Q + flow [m3/h] flow [m3/h] + + Operating point = (system curve) intersect (pump curve at running speed) +``` + +### Process variables + +| Variable | Typical range | Unit | +|:---|:---|:---| +| Flow Q | 1 to 1000 | m3/h | +| Head H (or differential pressure) | 1 to 100 | m (or bar) | +| Power P | 1 to 500 | kW | +| Efficiency η | 0.3 to 0.85 | — | +| Speed N | 0 to 100 | % of rated | + +### Physical state machine (water-side) + +```mermaid +stateDiagram-v2 + [*] --> off + off --> warmingup: cmd.startup + warmingup --> operational: warmup time elapsed + operational --> accelerating: setpoint changes + accelerating --> operational: target reached + operational --> decelerating: setpoint reduced + decelerating --> operational: target reached + operational --> coolingdown: cmd.shutdown + coolingdown --> off: cooldown time elapsed + operational --> emergencystop: cmd.estop + warmingup --> emergencystop: cmd.estop + emergencystop --> off: cmd.reset +``` + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Deliver commanded flow / speed | Internal FSM ramps speed up / down within configured rates | +| Predict outputs before sensors react | Q + P + η predicted from speed + measured pressure differential via characteristic curves | +| Detect drift | `drift/` module compares predicted vs measured; fires HealthStatus levels 0..3 | +| Protect during warmup / cooldown | Some transitions are non-interruptible (configurable per machine) | + +### EVOLV implementation + +- `curves/` module loads supplier characteristic curves (Q-H, Q-P, Q-η); supports multi-dataset assets. +- `prediction/` module interpolates curve values at current operating point. +- `state/` module owns the FSM with configurable warmup / cooldown / ramp times. +- `drift/` module assesses prediction quality and emits `HealthStatus`. + +--- + +## machineGroupControl — multi-pump load sharing + +### Physical view + +Multiple pumps share a common discharge header. The group operating point is where the **sum of pump curves** intersects the system curve. The load-sharing problem is: which pumps run at what speed to deliver demand at the lowest combined power. + +``` + Pump A curve Pump B curve Pump C curve + ^ ^ ^ + | | | + +-> share Q_A -->+-> share Q_B -->+-> share Q_C + | + sum = Q_demand + v + total power = P_A + P_B + P_C + minimize subject to per-pump curve constraints +``` + +### Process variables + +| Variable | Source | +|:---|:---| +| Group demand Q_d | Inbound from parent (pumpingStation, UI, scheduler) | +| Per-pump setpoint | Computed each tick | +| Pressure (downstream) | Measurement child, position `downstream` | +| Group totals (flow, power, efficiency) | Sum / weighted average of per-pump predictions | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Deliver Q_d at lowest total power | `GroupOperatingPoint` solver picks per-pump shares | +| Avoid frequent pump start / stop | Hysteresis on pump count + `NCog` switching metric | +| Stay near per-pump BEP | Penalise operating points far from BEP in the solver | +| Settle latest demand if upstream fires faster than children absorb | `DemandDispatcher` (LatestWinsGate) keeps only the freshest dispatch in flight | + +### EVOLV implementation + +- `dispatch/DemandDispatcher` wraps a `LatestWinsGate` so a rapid stream of demands collapses to the most recent. +- `efficiency/groupEfficiency` computes mean group efficiency at the current shares. +- `totals/TotalsCalculator` aggregates flow / power across active machines. + +--- + +## valveGroupControl — multi-valve flow distribution + +### Physical view + +Multiple valves on a distribution manifold. Each valve has a flow coefficient K_v that varies with position. The group must split a total available flow between branches. + +``` + upstream pressure P_up + | + v + +---+---+---+ + | | | | + v v v v + Kv1 Kv2 Kv3 Kv4 <-- per-valve K_v + | | | | + Q1 Q2 Q3 Q4 <-- per-branch flow + + Q_i = K_v_i * sqrt(P_up - P_branch_i) + sum(Q_i) = Q_available (from upstream flow source) +``` + +### Process variables + +| Variable | Typical range | Unit | +|:---|:---|:---| +| Valve position | 0 to 100 | % | +| K_v at full open | 1 to 1000 | m3/h / sqrt(bar) | +| Differential pressure across valve | 0.1 to 5 | bar | +| Per-branch flow split | percentage of total | % | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Achieve target per-branch flow split | Solve per-valve K_v from inverse characteristic | +| Respect upstream availability | Read total flow from registered flow source (pumpingStation, MGC, etc.) | +| Honour valve position limits | Clamp K_v to physical valve range | + +### Note on softwareType registration + +`valveGroupControl.configure()` registers five softwareTypes — `valve`, `machine`, `machinegroup`, `pumpingstation`, `valvegroupcontrol`. Only `valve` is a true S88 child. The other four are flow-source registrations: VGC reads their flow output to know how much it has to distribute. + +--- + +## reactor — bioreactor (ASM kinetics) + +### Physical view + +A tank where bacteria consume substrate under aeration. Continuous-flow operation (CSTR or PFR). The state of the reactor is described by 13 ASM state variables (soluble + particulate fractions of organic matter, ammonia, nitrate, biomass, alkalinity, oxygen). + +``` + air from diffuser + | + v bubbles + Q_in +-------+-------+ Q_out + ---->| ~ ~ ~ ~ ~ ~ |----> + | ~ ~ ~ ~ ~ ~ | + C_in | ~ ~ ~ ~ ~ | C_out + | ~ ~ ~ ~ ~ ~ | + | ~ ~ ~ ~ ~ ~ | + +----------------------+ + + Mass balance per ASM component i: + V * dC_i/dt = Q_in * C_in_i - Q_out * C_out_i + V * r_i(C, T, DO, ...) + inflow term outflow term reaction term +``` + +### Process variables + +| Variable | Typical range | Unit | +|:---|:---|:---| +| Volume V | 100 to 10000 | m3 | +| Hydraulic retention time HRT | 4 to 24 | h | +| Sludge retention time SRT | 5 to 30 | d | +| MLSS | 2000 to 5000 | mg/L | +| Temperature | 5 to 30 | degC | +| DO setpoint | 1 to 3 | mg/L | +| NH4 effluent target | < 1 | mg/L | +| NO3 effluent | 0 to 15 | mg/L | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Maintain DO setpoint | DO measurement → diffuser airflow loop (closed externally) | +| Achieve effluent quality | Manage HRT via reactor inflow, SRT via WAS rate | +| Operate stably across temperature | Kinetics are temperature-corrected via Arrhenius factors | + +### Engine choice + +| Engine | When to use | +|:---|:---| +| CSTR | Single well-mixed tank or short reactor | +| PFR | Long, narrow plug-flow reactor; discretised into grid cells along the flow path | + +Set via `config.reactor_type`. + +### EVOLV implementation + +- `kinetics/baseEngine.js` — common state vector + boundary-condition handling. +- `kinetics/cstr.js` — single-zone integrator. +- `kinetics/pfr.js` — multi-zone PFR with a grid. +- Diffuser fires `data.otr` on its emitter; reactor subscribes and treats OTR as an O2 source term. +- Downstream `settler` subscribes to reactor `stateChange`; the settler reads effluent composition each tick. + +--- + +## settler — secondary clarifier + +### Physical view + +A wide, shallow tank where biomass settles by gravity. A sludge blanket forms at the bottom; clean water overflows the rim at the top. A return-pump sucks settled sludge back to the reactor; a smaller waste stream removes excess (WAS). + +``` + Q_in (from reactor) + + biomass C_in + | + v + +------------------------------+ + | clean water |---> overflow Q_out + | | low TSS + | - - - - - - - - - - - - - | <-- top of sludge blanket + | (settling zone) | + | = = = = = = = = = = = = = | + | compacting biomass | + | ########################### | <-- sludge blanket + +-----------+---+--------------+ + | | + v v + underflow to reactor (RAS) + high TSS small bleed (WAS) +``` + +### Process variables + +| Variable | Typical range | Unit | +|:---|:---|:---| +| Surface area | 50 to 2000 | m2 | +| Depth | 3 to 5 | m | +| Surface loading rate (SLR) | 0.5 to 2 | m/h | +| RAS flow | 50 to 150 | % of inflow | +| WAS flow | 1 to 5 | % of inflow | +| Effluent TSS | < 30 | mg/L | +| Underflow TSS | 6000 to 12000 | mg/L | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Keep sludge blanket below overflow weir | RAS pump rate adjusted to inflow | +| Maintain reactor MLSS target | WAS rate set as fraction of inflow | +| Avoid blanket carryover | Limit SLR; alarm on rising blanket level | + +### EVOLV implementation + +- `settler._connectReactor` attaches `emitter.on('stateChange', ...)` to the upstream reactor, pulling effluent composition each tick. +- `settler._connectMachine` registers the RAS pump (a `rotatingMachine`) as a child. +- Settling-velocity model (Takács or Vesilind) is in the settler's `src/`; see [Settling Models](Concept-Settling-Models). + +--- + +## diffuser — aeration device + +### Physical view + +A perforated panel or membrane at the bottom of the reactor that releases fine bubbles. Bubbles rise through the water column; oxygen dissolves into the water across the gas-liquid interface. + +``` + reactor side + ~ ~ ~ ~ ~ dissolved O2 enters water + ~ ~ ~ ~ ~ + o o o o o o <-- bubbles rising + o o o o o o + o o o o o + ooo ooo + ooo oo + +----[========]----+ <-- diffuser membrane / panel + | | | | | | | | | compressed air manifold + +-+--+-+--+-+--+-+-+ + ^ + air inflow + (from blower upstream) +``` + +### Process variables + +| Variable | Typical range | Unit | +|:---|:---|:---| +| Airflow per diffuser | 1 to 10 | Nm3/h | +| Header pressure | 0.3 to 0.7 | bar | +| Water depth (above diffuser) | 4 to 6 | m | +| K_La (volumetric mass-transfer coefficient) | 1 to 20 | 1/h | +| Alpha factor (wastewater vs clean water) | 0.5 to 0.9 | — | +| OTR (oxygen transfer rate) | 1 to 5 | kg-O2/h per diffuser | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Deliver enough OTR to meet reactor DO setpoint | Modulate airflow via upstream blower / valve | +| Distribute air evenly across panels | Manifold sizing; sometimes a valveGroupControl on the air side | +| Avoid over-aeration (energy waste) | DO feedback loop | + +### EVOLV implementation + +- `diffuser` reads `headerPressure`, water depth, airflow as inputs. +- Computes OTR from K_La (configurable, supplier-specific), alpha factor, water properties. +- Emits `data.otr` on its emitter. Reactor subscribes via `emitter.on('otr', ...)` — this is **not** a child-register handshake. + +--- + +## valve — single valve actuator + +### Physical view + +A control valve in a pipe. Position 0..100% maps to K_v via the valve's characteristic curve (linear, equal-percentage, or quick-opening). Flow through the valve obeys `Q = K_v * sqrt(dP)`. + +``` + flow +---+ flow + ----------| |-----------> + | ===== <-- closure element (plug, ball, disc, gate) + | | + +---+ + ^ + position 0..100% + + position --(characteristic curve)--> K_v + Q = K_v * sqrt(P_up - P_down) +``` + +### Process variables + +| Variable | Typical range | Unit | +|:---|:---|:---| +| Position | 0 to 100 | % | +| K_v at full open | 1 to 1000 | m3/h / sqrt(bar) | +| Differential pressure | 0.1 to 5 | bar | +| Stroke time (close to open) | 10 to 60 | s | + +### Physical state machine + +valve shares the rotatingMachine state model. States: `off`, `idle`, `warmingup`, `operational`, `accelerating` (opening / closing), `decelerating`, `coolingdown`, `emergencystop`, `maintenance`. `warmingup` and `coolingdown` are protected (cannot be aborted). + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Reach commanded position | Move at the configured `reactionSpeed` rate | +| Avoid water hammer | Limit how fast position changes | +| Provide flow feedback to upstream | Computed Q from current K_v and measured dP | + +### EVOLV implementation + +- `valve` registers `measurement` children for position / pressure feedback. +- Inherits the shared FSM (`generalFunctions` state config) with rotatingMachine. +- Characteristic curve is supplier-specific and loaded similarly to pump curves. + +--- + +## measurement — sensor signal conditioning + +### Physical view + +A field sensor (level meter, flow meter, pressure transducer, DO probe, ...) outputs a raw signal. EVOLV's `measurement` node wraps that signal: scales it from instrument units (mA, mV, raw counts) to engineering units, smooths it, rejects outliers, and publishes the result to a parent process node. + +``` + Sensor 4-20 mA (or digital, or MQTT) + in the pipe -----------------+ + | + v + +--------------------------------+ + | measurement node | + | | + | raw input | + | | | + | v scaling (mA -> EU) | + | v smoothing | + | v outlier rejection | + | v calibration offset | + | | + +-------+------------------------+ + | + v data. + parent process node + (pumpingStation, reactor, ...) +``` + +### Three input modes + +| Mode | Source | When to use | +|:---|:---|:---| +| analog | 4-20 mA, 0-10 V, raw scaled value | Direct PLC / IO-card analog input | +| digital | Boolean (on / off, ok / fault) | Limit switches, status contacts | +| MQTT | External MQTT broker topic | Field bus, sensor with its own gateway | + +### Process variables (examples) + +| Type | Typical range | Unit | Example sensor | +|:---|:---|:---|:---| +| level | 0 to 5 | m | radar level meter | +| flow | 0 to 2000 | m3/h | electromagnetic flowmeter | +| pressure | 0 to 10 | bar | piezo pressure transmitter | +| temperature | 5 to 40 | degC | PT100 RTD | +| DO | 0 to 10 | mg/L | optical dissolved-O2 probe | +| NH4 | 0 to 50 | mg/L | ion-selective electrode | +| TSS | 0 to 5000 | mg/L | optical turbidity sensor | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Deliver trustworthy values to parent | Pipeline: scaling → smoothing → outlier → publish | +| Survive sensor faults | Outlier rejection + sticky-last-good behaviour | +| Calibrate against reference | `cmd.calibrate` triggers a calibration cycle | + +--- + +## monster — composite sampling + +### Physical view + +A virtual composite sampler: imagine a small bucket beside the pipe. Every time a unit volume of water flows past, a unit dose of that water is added to the bucket. After a sampling period (e.g. 24 h) the bucket contains a flow-proportional composite of every concentration over that period. + +``` + ____________________ + | sampling bucket | <-- accumulated sample + | ~~~~~~ | + | ~~~~~ | + | ~ | + |_____________________| + | + | sampling dose dV at every flow increment + v + -----++------------------------+-----> main pipe + | (flow Q, conc C) flow + v + dV = (Q * dt / total_flow_target) * sample_volume + composite_C(t) = integral( C(s) * dV(s) ) / total_dV +``` + +### Process variables + +| Variable | Typical range | Unit | +|:---|:---|:---| +| Sampling period | 1 to 24 | h | +| Bucket volume target | 2 to 10 | L | +| Sample doses per period | 24 to 96 | — | +| Output composite concentration | as configured per parameter | mg/L, NTU, … | + +### Control objective + +| Goal | Mechanism | +|:---|:---| +| Build a representative composite sample over the period | Flow-proportional dosing | +| Produce reportable averages | Each tick, accumulate flow-weighted concentration | +| Reset for next period | At end of period, emit composite and reset bucket | + +### Gotchas + +| Gotcha | Detail | +|:---|:---| +| `assetType` must be `"flow"` exactly | Sub-types like `"flow-electromagnetic"` are silently ignored by monster's child router | +| `constraints.flowmeter` not forwarded | Toggling proportional-vs-time mode has no runtime effect in current code. Tracked in `.claude/refactor/OPEN_QUESTIONS.md` | + +--- + +## dashboardAPI — Grafana provisioning + +### Physical view + +There is none. `dashboardAPI` does not model any piece of water-treatment equipment. It is a utility that auto-generates Grafana dashboards from a node's softwareType + measurements, so operators see the right panels per equipment without hand-building dashboard JSON. + +### Operational role + +| Trigger | Effect | +|:---|:---| +| Any EVOLV node sends `child.register` to dashboardAPI | dashboardAPI composes a dashboard JSON from the template for that softwareType and POSTs an upsert to Grafana | +| Telemetry arrives in InfluxDB (Port 1 of process nodes) | Grafana panels query InfluxDB and render the trends | + +dashboardAPI is the one node in the platform that does not extend `BaseDomain` (it is a passive HTTP bridge). See `.claude/refactor/OPEN_QUESTIONS.md`. + +--- + +## Where it all fits + +If you imagine a wastewater plant from inlet to outlet, every EVOLV node is a piece of equipment you would see on a P&ID. The software's job is to model that equipment well enough that: + +1. Operators can run the plant without watching the water directly (predictions + telemetry). +2. New plants can be commissioned by composing standard nodes (no bespoke control code). +3. Anomalies surface as `HealthStatus` flags before they become operator problems. + +See [Topology Patterns](Topology-Patterns) for how to assemble these nodes into a working plant. + +--- + +## Related pages + +| Page | Why | +|:---|:---| +| [Home](Home) | Top-level node map and S88 hierarchy | +| [Topology Patterns](Topology-Patterns) | Standard assemblies of these nodes | +| [Architecture](Architecture) | The **code** counterpart of this page | +| [Topic Conventions](Topic-Conventions) | How process variables travel between nodes | +| [Telemetry](Telemetry) | How process variables land in InfluxDB and Grafana | +| [ASM Models](Concept-ASM-Models) | The reactor's biological kinetics in detail | +| [Pump Affinity Laws](Concept-Pump-Affinity-Laws) | Pump curve physics | +| [Settling Models](Concept-Settling-Models) | Settler physics | +| [Signal Processing — Sensors](Concept-Signal-Processing-Sensors) | Measurement node pipeline |