# 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 |