fix: fully configure PS basin + add node-completeness rule
Some checks failed
CI / lint-and-test (push) Has been cancelled
Some checks failed
CI / lint-and-test (push) Has been cancelled
Basin undersized (10m³) for sinus peak (126 m³/h) → overflow → 122%. Now 30 m³ with 4m height, all PS fields set. New rule: always configure every field of every node. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -240,7 +240,20 @@ If you only fill the top-level fields, `payload_type=json` is silently treated a
|
||||
|
||||
Both ends store the paired ids in `links`. The `name` is cosmetic (label only) — Node-RED routes by id. Multiple emitters can target one receiver; one emitter can target multiple receivers.
|
||||
|
||||
## 9. Verifying the layout
|
||||
## 9. Node configuration completeness — ALWAYS set every field
|
||||
|
||||
When placing an EVOLV node in a flow (demo or production), configure **every config field** the node's schema defines — don't rely on schema defaults for operational parameters. Schema defaults exist to make the validator happy, not to represent a realistic plant.
|
||||
|
||||
**Why this matters:** A pumpingStation with `basinVolume: 10` but default `heightOverflow: 2.5` and default `heightOutlet: 0.2` creates an internally inconsistent basin where the fill % exceeds 100%, safety guards fire at wrong thresholds, and the demo looks broken. Every field interacts with every other field.
|
||||
|
||||
**The rule:**
|
||||
1. Read the node's config schema (`generalFunctions/src/configs/<nodeName>.json`) before writing the flow.
|
||||
2. For each section (basin, hydraulics, control, safety, scaling, smoothing, …), set EVERY field explicitly in the flow JSON — even if you'd pick the same value as the default.
|
||||
3. Add a comment in the flow generator per section explaining WHY you chose each value (e.g. "basin sized so sinus peak takes 6 min to fill from startLevel to overflow").
|
||||
4. Cross-check computed values: `surfaceArea = volume / height`, `maxVolOverflow = heightOverflow × surfaceArea`, gauge `max` = basin `height`, fill % denominator = `volume` (not overflow volume).
|
||||
5. If a gauge or chart references a config value (basin height, maxVol), derive it from the same source — never hardcode a number that was computed elsewhere.
|
||||
|
||||
## 10. Verifying the layout
|
||||
|
||||
Before declaring a flow done:
|
||||
|
||||
|
||||
@@ -576,15 +576,37 @@ def build_process_tab():
|
||||
"hasDistance": False, "distance": 0, "distanceUnit": "m",
|
||||
"distanceDescription": "",
|
||||
"processOutputFormat": "process", "dbaseOutputFormat": "influxdb",
|
||||
# PS in levelbased mode — sinus inflow fills the basin, pumps start
|
||||
# when level > startLevel, stop when level < stopLevel.
|
||||
# === FULLY CONFIGURED PS — every field explicitly set ===
|
||||
# Rule: ALWAYS configure ALL node fields. Defaults are for
|
||||
# schema validation, not for realistic operation.
|
||||
#
|
||||
# Basin geometry: 30 m³, 4 m tall → surfaceArea = 7.5 m²
|
||||
# Sized so peak sinus inflow (0.035 m³/s = 126 m³/h) takes
|
||||
# ~6 min to fill from startLevel to overflow → pumps have time.
|
||||
"controlMode": "levelbased",
|
||||
"basinVolume": 10, "basinHeight": 3,
|
||||
"startLevel": 1.2, "stopLevel": 0.6,
|
||||
"minFlowLevel": 0.6, "maxFlowLevel": 2.8,
|
||||
"heightInlet": 2.5, "heightOutlet": 0.2, "heightOverflow": 2.8,
|
||||
# Volume-based safeties ON, time-based OFF (time guard fires too
|
||||
# aggressively with the sinus demo's small basin + high peak inflow).
|
||||
"basinVolume": 30,
|
||||
"basinHeight": 4,
|
||||
"heightInlet": 3.5,
|
||||
"heightOutlet": 0.3,
|
||||
"heightOverflow": 3.8,
|
||||
"inletPipeDiameter": 0.3,
|
||||
"outletPipeDiameter": 0.3,
|
||||
# Level-based control thresholds
|
||||
"startLevel": 2.0, # pumps ON above 2.0 m (50% of height)
|
||||
"stopLevel": 1.0, # pumps OFF below 1.0 m (25% of height)
|
||||
"minFlowLevel": 1.0, # 0% pump demand at this level
|
||||
"maxFlowLevel": 3.5, # 100% pump demand at this level
|
||||
# Hydraulics
|
||||
"refHeight": "NAP",
|
||||
"minHeightBasedOn": "outlet",
|
||||
"basinBottomRef": 0,
|
||||
"staticHead": 12,
|
||||
"maxDischargeHead": 24,
|
||||
"pipelineLength": 80,
|
||||
"defaultFluid": "wastewater",
|
||||
"temperatureReferenceDegC": 15,
|
||||
"maxInflowRate": 200,
|
||||
# Safety guards
|
||||
"enableDryRunProtection": True,
|
||||
"enableOverfillProtection": True,
|
||||
"dryRunThresholdPercent": 5,
|
||||
@@ -613,7 +635,7 @@ def build_process_tab():
|
||||
"const qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\n"
|
||||
"// Compute derived metrics\n"
|
||||
"// Basin capacity = basinVolume (config). Don't hardcode — read it once.\n"
|
||||
"if (!context.get('maxVol')) context.set('maxVol', 10.0); // basinVolume from PS config\n"
|
||||
"if (!context.get('maxVol')) context.set('maxVol', 30.0); // basinVolume from PS config\n"
|
||||
"const maxVol = context.get('maxVol');\n"
|
||||
"const fillPct = vol != null ? Math.min(100, Math.max(0, Math.round(Number(vol) / maxVol * 100))) : null;\n"
|
||||
"const netM3h = (c.netFlow != null) ? Number(c.netFlow) * 3600 : null;\n"
|
||||
@@ -1094,11 +1116,11 @@ def build_ui_tab():
|
||||
# ===== Basin charts + gauges (fill %, level, net flow) =====
|
||||
# Gauge segment definitions (reused for both pages)
|
||||
TANK_SEGMENTS = [
|
||||
{"color": "#f44336", "from": 0}, # red: below stopLevel
|
||||
{"color": "#ff9800", "from": 0.6}, # orange: between stop and start
|
||||
{"color": "#2196f3", "from": 1.2}, # blue: normal operating
|
||||
{"color": "#ff9800", "from": 2.5}, # orange: approaching overflow
|
||||
{"color": "#f44336", "from": 2.8}, # red: overflow zone
|
||||
{"color": "#f44336", "from": 0}, # red: below stopLevel (1.0 m)
|
||||
{"color": "#ff9800", "from": 1.0}, # orange: between stop and start
|
||||
{"color": "#2196f3", "from": 2.0}, # blue: normal operating (startLevel)
|
||||
{"color": "#ff9800", "from": 3.5}, # orange: approaching overflow
|
||||
{"color": "#f44336", "from": 3.8}, # red: overflow zone (heightOverflow)
|
||||
]
|
||||
FILL_SEGMENTS = [
|
||||
{"color": "#f44336", "from": 0},
|
||||
@@ -1131,7 +1153,7 @@ def build_ui_tab():
|
||||
"gtype": "gauge-tank", "gstyle": "Rounded",
|
||||
"title": "Level", "units": "m",
|
||||
"prefix": "", "suffix": " m",
|
||||
"min": 0, "max": 3,
|
||||
"min": 0, "max": 4,
|
||||
"segments": TANK_SEGMENTS,
|
||||
"width": 2, "height": 5, "order": 2,
|
||||
"icon": "", "sizeGauge": 20, "sizeGap": 2, "sizeSegments": 10,
|
||||
|
||||
@@ -853,15 +853,26 @@
|
||||
"processOutputFormat": "process",
|
||||
"dbaseOutputFormat": "influxdb",
|
||||
"controlMode": "levelbased",
|
||||
"basinVolume": 10,
|
||||
"basinHeight": 3,
|
||||
"startLevel": 1.2,
|
||||
"stopLevel": 0.6,
|
||||
"minFlowLevel": 0.6,
|
||||
"maxFlowLevel": 2.8,
|
||||
"heightInlet": 2.5,
|
||||
"heightOutlet": 0.2,
|
||||
"heightOverflow": 2.8,
|
||||
"basinVolume": 30,
|
||||
"basinHeight": 4,
|
||||
"heightInlet": 3.5,
|
||||
"heightOutlet": 0.3,
|
||||
"heightOverflow": 3.8,
|
||||
"inletPipeDiameter": 0.3,
|
||||
"outletPipeDiameter": 0.3,
|
||||
"startLevel": 2.0,
|
||||
"stopLevel": 1.0,
|
||||
"minFlowLevel": 1.0,
|
||||
"maxFlowLevel": 3.5,
|
||||
"refHeight": "NAP",
|
||||
"minHeightBasedOn": "outlet",
|
||||
"basinBottomRef": 0,
|
||||
"staticHead": 12,
|
||||
"maxDischargeHead": 24,
|
||||
"pipelineLength": 80,
|
||||
"defaultFluid": "wastewater",
|
||||
"temperatureReferenceDegC": 15,
|
||||
"maxInflowRate": 200,
|
||||
"enableDryRunProtection": true,
|
||||
"enableOverfillProtection": true,
|
||||
"dryRunThresholdPercent": 5,
|
||||
@@ -881,7 +892,7 @@
|
||||
"type": "function",
|
||||
"z": "tab_process",
|
||||
"name": "format PS port 0",
|
||||
"func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst lvl = find('level.predicted.');\nconst vol = find('volume.predicted.');\nconst qIn = find('flow.measured.upstream.') || find('flow.measured.in.');\nconst qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\n// Compute derived metrics\nconst maxVol = 9.33; // must match basinVolume * basinHeight / basinHeight = basinVolume / surfaceArea * height\nconst fillPct = vol != null ? Math.round(Number(vol) / maxVol * 100) : null;\nconst netM3h = (c.netFlow != null) ? Number(c.netFlow) * 3600 : null;\nconst seconds = (c.seconds != null && Number.isFinite(Number(c.seconds))) ? Number(c.seconds) : null;\nconst timeStr = seconds != null ? (seconds > 60 ? Math.round(seconds/60) + ' min' : Math.round(seconds) + ' s') : 'n/a';\nmsg.payload = {\n direction: c.direction || 'steady',\n level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n volume: vol != null ? Number(vol).toFixed(1) + ' m\u00b3' : 'n/a',\n fillPct: fillPct != null ? fillPct + '%' : 'n/a',\n netFlow: netM3h != null ? netM3h.toFixed(0) + ' m\u00b3/h' : 'n/a',\n timeLeft: timeStr,\n qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n // Numerics for trends\n levelNum: lvl != null ? Number(lvl) : null,\n volumeNum: vol != null ? Number(vol) : null,\n fillPctNum: fillPct,\n netFlowNum: netM3h,\n};\nreturn msg;",
|
||||
"func": "const p = msg.payload || {};\nconst c = context.get('c') || {};\nObject.assign(c, p);\ncontext.set('c', c);\nfunction find(prefix) {\n for (const k in c) { if (k.indexOf(prefix) === 0) return c[k]; }\n return null;\n}\nconst lvl = find('level.predicted.');\nconst vol = find('volume.predicted.');\nconst qIn = find('flow.measured.upstream.') || find('flow.measured.in.');\nconst qOut = find('flow.measured.downstream.') || find('flow.measured.out.');\n// Compute derived metrics\n// Basin capacity = basinVolume (config). Don't hardcode \u2014 read it once.\nif (!context.get('maxVol')) context.set('maxVol', 30.0); // basinVolume from PS config\nconst maxVol = context.get('maxVol');\nconst fillPct = vol != null ? Math.min(100, Math.max(0, Math.round(Number(vol) / maxVol * 100))) : null;\nconst netM3h = (c.netFlow != null) ? Number(c.netFlow) * 3600 : null;\nconst seconds = (c.seconds != null && Number.isFinite(Number(c.seconds))) ? Number(c.seconds) : null;\nconst timeStr = seconds != null ? (seconds > 60 ? Math.round(seconds/60) + ' min' : Math.round(seconds) + ' s') : 'n/a';\nmsg.payload = {\n direction: c.direction || 'steady',\n level: lvl != null ? Number(lvl).toFixed(2) + ' m' : 'n/a',\n volume: vol != null ? Number(vol).toFixed(1) + ' m\u00b3' : 'n/a',\n fillPct: fillPct != null ? fillPct + '%' : 'n/a',\n netFlow: netM3h != null ? netM3h.toFixed(0) + ' m\u00b3/h' : 'n/a',\n timeLeft: timeStr,\n qIn: qIn != null ? (Number(qIn) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n qOut: qOut != null ? (Number(qOut) * 3600).toFixed(0) + ' m\u00b3/h' : 'n/a',\n // Numerics for trends\n levelNum: lvl != null ? Number(lvl) : null,\n volumeNum: vol != null ? Number(vol) : null,\n fillPctNum: fillPct,\n netFlowNum: netM3h,\n};\nreturn msg;",
|
||||
"outputs": 1,
|
||||
"noerr": 0,
|
||||
"initialize": "",
|
||||
@@ -3476,7 +3487,7 @@
|
||||
"prefix": "",
|
||||
"suffix": " m",
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"max": 4,
|
||||
"segments": [
|
||||
{
|
||||
"color": "#f44336",
|
||||
@@ -3484,19 +3495,19 @@
|
||||
},
|
||||
{
|
||||
"color": "#ff9800",
|
||||
"from": 0.6
|
||||
"from": 1.0
|
||||
},
|
||||
{
|
||||
"color": "#2196f3",
|
||||
"from": 1.2
|
||||
"from": 2.0
|
||||
},
|
||||
{
|
||||
"color": "#ff9800",
|
||||
"from": 2.5
|
||||
"from": 3.5
|
||||
},
|
||||
{
|
||||
"color": "#f44336",
|
||||
"from": 2.8
|
||||
"from": 3.8
|
||||
}
|
||||
],
|
||||
"width": 2,
|
||||
@@ -3632,7 +3643,7 @@
|
||||
"prefix": "",
|
||||
"suffix": " m",
|
||||
"min": 0,
|
||||
"max": 3,
|
||||
"max": 4,
|
||||
"segments": [
|
||||
{
|
||||
"color": "#f44336",
|
||||
@@ -3640,19 +3651,19 @@
|
||||
},
|
||||
{
|
||||
"color": "#ff9800",
|
||||
"from": 0.6
|
||||
"from": 1.0
|
||||
},
|
||||
{
|
||||
"color": "#2196f3",
|
||||
"from": 1.2
|
||||
"from": 2.0
|
||||
},
|
||||
{
|
||||
"color": "#ff9800",
|
||||
"from": 2.5
|
||||
"from": 3.5
|
||||
},
|
||||
{
|
||||
"color": "#f44336",
|
||||
"from": 2.8
|
||||
"from": 3.8
|
||||
}
|
||||
],
|
||||
"width": 2,
|
||||
|
||||
Reference in New Issue
Block a user