diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..1170717
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,136 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+lerna-debug.log*
+.pnpm-debug.log*
+
+# Diagnostic reports (https://nodejs.org/api/report.html)
+report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
+
+# Runtime data
+pids
+*.pid
+*.seed
+*.pid.lock
+
+# Directory for instrumented libs generated by jscoverage/JSCover
+lib-cov
+
+# Coverage directory used by tools like istanbul
+coverage
+*.lcov
+
+# nyc test coverage
+.nyc_output
+
+# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
+.grunt
+
+# Bower dependency directory (https://bower.io/)
+bower_components
+
+# node-waf configuration
+.lock-wscript
+
+# Compiled binary addons (https://nodejs.org/api/addons.html)
+build/Release
+
+# Dependency directories
+node_modules/
+jspm_packages/
+
+# Snowpack dependency directory (https://snowpack.dev/)
+web_modules/
+
+# TypeScript cache
+*.tsbuildinfo
+
+# Optional npm cache directory
+.npm
+
+# Optional eslint cache
+.eslintcache
+
+# Optional stylelint cache
+.stylelintcache
+
+# Microbundle cache
+.rpt2_cache/
+.rts2_cache_cjs/
+.rts2_cache_es/
+.rts2_cache_umd/
+
+# Optional REPL history
+.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
+
+# Yarn Integrity file
+.yarn-integrity
+
+# dotenv environment variable files
+.env
+.env.development.local
+.env.test.local
+.env.production.local
+.env.local
+
+# parcel-bundler cache (https://parceljs.org/)
+.cache
+.parcel-cache
+
+# Next.js build output
+.next
+out
+
+# Nuxt.js build / generate output
+.nuxt
+dist
+
+# Gatsby files
+.cache/
+# Comment in the public line in if your project uses Gatsby and not Next.js
+# https://nextjs.org/blog/next-9-1#public-directory-support
+# public
+
+# vuepress build output
+.vuepress/dist
+
+# vuepress v2.x temp and cache directory
+.temp
+.cache
+
+# vitepress build output
+**/.vitepress/dist
+
+# vitepress cache directory
+**/.vitepress/cache
+
+# Docusaurus cache and generated files
+.docusaurus
+
+# Serverless directories
+.serverless/
+
+# FuseBox cache
+.fusebox/
+
+# DynamoDB Local files
+.dynamodb/
+
+# TernJS port file
+.tern-port
+
+# Stores VSCode versions used for testing VSCode extensions
+.vscode-test
+
+# yarn v2
+.yarn/cache
+.yarn/unplugged
+.yarn/build-state.yml
+.yarn/install-state.gz
+.pnp.*
diff --git a/README.md b/README.md
index 75abb93..552ebb1 100644
--- a/README.md
+++ b/README.md
@@ -1,16 +1,17 @@
# reactor
-Reactor: Advanced Hydraulic Tank & Biological Process Simulator
-
-A comprehensive reactor class for wastewater treatment simulation featuring plug flow hydraulics, ASM1-ASM3 biological modeling, and multi-sectional concentration tracking. Implements hydraulic retention time calculations, dispersion modeling, and real-time biological reaction kinetics for accurate process simulation.
-
-Key Features:
-
-Plug Flow Hydraulics: Multi-section reactor with configurable sectioning factor and dispersion modeling
-ASM1 Integration: Complete biological nutrient removal modeling with 13 state variables (COD, nitrogen, phosphorus)
-Dynamic Volume Control: Automatic section management with overflow handling and retention time calculations
-Oxygen Transfer: Saturation-limited O2 transfer with Fick's law slowdown effects and solubility curves
-Real-time Kinetics: Continuous biological reaction rate calculations with configurable time acceleration
-Weighted Averaging: Volume-based concentration mixing for accurate mass balance calculations
-Child Registration: Integration with diffuser systems and upstream/downstream reactor networks
-Supports complex biological treatment train modeling with temperature compensation, sludge calculations, and comprehensive process monitoring for wastewater treatment plant optimization and regulatory compliance.
\ No newline at end of file
+Reactor: Advanced Hydraulic Tank & Biological Process Simulator
+
+A comprehensive reactor class for wastewater treatment simulation featuring plug flow hydraulics, ASM1-ASM3 biological modeling, and multi-sectional concentration tracking. Implements hydraulic retention time calculations, dispersion modeling, and real-time biological reaction kinetics for accurate process simulation.
+
+Key Features:
+
+Plug Flow Hydraulics: Multi-section reactor with configurable sectioning factor and dispersion modeling
+ASM1 Integration: Complete biological nutrient removal modeling with 13 state variables (COD, nitrogen, phosphorus)
+Dynamic Volume Control: Automatic section management with overflow handling and retention time calculations
+Oxygen Transfer: Saturation-limited O2 transfer with Fick's law slowdown effects and solubility curves
+Real-time Kinetics: Continuous biological reaction rate calculations with configurable time acceleration
+Weighted Averaging: Volume-based concentration mixing for accurate mass balance calculations
+Child Registration: Integration with diffuser systems and upstream/downstream reactor networks
+Supports complex biological treatment train modeling with temperature compensation, sludge calculations, and comprehensive process monitoring for wastewater treatment plant optimization and regulatory compliance.
+
diff --git a/additional_nodes/recirculation-pump.html b/additional_nodes/recirculation-pump.html
new file mode 100644
index 0000000..39a3753
--- /dev/null
+++ b/additional_nodes/recirculation-pump.html
@@ -0,0 +1,57 @@
+
+
+
+
+
diff --git a/additional_nodes/recirculation-pump.js b/additional_nodes/recirculation-pump.js
new file mode 100644
index 0000000..2a02932
--- /dev/null
+++ b/additional_nodes/recirculation-pump.js
@@ -0,0 +1,40 @@
+module.exports = function(RED) {
+ function recirculation(config) {
+ RED.nodes.createNode(this, config);
+ var node = this;
+
+ let name = config.name;
+ let F2 = parseFloat(config.F2);
+ const inlet_F2 = parseInt(config.inlet);
+
+ node.on('input', function(msg, send, done) {
+ switch (msg.topic) {
+ case "Fluent":
+ // conserve volume flow debit
+ let F_in = msg.payload.F;
+ let F1 = Math.max(F_in - F2, 0);
+ let F2_corr = F_in < F2 ? F_in : F2;
+
+ let msg_F1 = structuredClone(msg);
+ msg_F1.payload.F = F1;
+
+ let msg_F2 = {...msg};
+ msg_F2.payload.F = F2_corr;
+ msg_F2.payload.inlet = inlet_F2;
+
+ send([msg_F1, msg_F2]);
+ break;
+ case "clock":
+ break;
+ default:
+ console.log("Unknown topic: " + msg.topic);
+ }
+
+ if (done) {
+ done();
+ }
+ });
+
+ }
+ RED.nodes.registerType("recirculation-pump", recirculation);
+};
diff --git a/additional_nodes/settling-basin.html b/additional_nodes/settling-basin.html
new file mode 100644
index 0000000..e8e8e8d
--- /dev/null
+++ b/additional_nodes/settling-basin.html
@@ -0,0 +1,57 @@
+
+
+
+
+
diff --git a/additional_nodes/settling-basin.js b/additional_nodes/settling-basin.js
new file mode 100644
index 0000000..dd3d697
--- /dev/null
+++ b/additional_nodes/settling-basin.js
@@ -0,0 +1,57 @@
+module.exports = function(RED) {
+ function settler(config) {
+ RED.nodes.createNode(this, config);
+ var node = this;
+
+ let name = config.name;
+ let TS_set = parseFloat(config.TS_set);
+ const inlet_sludge = parseInt(config.inlet);
+
+ node.on('input', function(msg, send, done) {
+ switch (msg.topic) {
+ case "Fluent":
+ // conserve volume flow debit
+ let F_in = msg.payload.F;
+ let C_in = msg.payload.C;
+ let F2 = (F_in * C_in[12]) / TS_set;
+
+ let F1 = Math.max(F_in - F2, 0);
+ let F2_corr = F_in < F2 ? F_in : F2;
+
+ let msg_F1 = structuredClone(msg);
+ msg_F1.payload.F = F1;
+ msg_F1.payload.C[7] = 0;
+ msg_F1.payload.C[8] = 0;
+ msg_F1.payload.C[9] = 0;
+ msg_F1.payload.C[10] = 0;
+ msg_F1.payload.C[11] = 0;
+ msg_F1.payload.C[12] = 0;
+
+ let msg_F2 = {...msg};
+ msg_F2.payload.F = F2_corr;
+ if (F2_corr > 0) {
+ msg_F2.payload.C[7] = F_in * C_in[7] / F2;
+ msg_F2.payload.C[8] = F_in * C_in[8] / F2;
+ msg_F2.payload.C[9] = F_in * C_in[9] / F2;
+ msg_F2.payload.C[10] = F_in * C_in[10] / F2;
+ msg_F2.payload.C[11] = F_in * C_in[11] / F2;
+ msg_F2.payload.C[12] = F_in * C_in[12] / F2;
+ }
+ msg_F2.payload.inlet = inlet_sludge;
+
+ send([msg_F1, msg_F2]);
+ break;
+ case "clock":
+ break;
+ default:
+ console.log("Unknown topic: " + msg.topic);
+ }
+
+ if (done) {
+ done();
+ }
+ });
+
+ }
+ RED.nodes.registerType("settling-basin", settler);
+};
diff --git a/flows/asm3_flows.json b/flows/asm3_flows.json
new file mode 100644
index 0000000..7f6d216
--- /dev/null
+++ b/flows/asm3_flows.json
@@ -0,0 +1,1763 @@
+[
+ {
+ "id": "31bba0914516dd85",
+ "type": "tab",
+ "label": "Flow 2",
+ "disabled": true,
+ "info": "",
+ "env": []
+ },
+ {
+ "id": "0abdac5260d9553e",
+ "type": "tab",
+ "label": "Flow 1",
+ "disabled": false,
+ "info": "",
+ "env": []
+ },
+ {
+ "id": "394f713d4e71366c",
+ "type": "tab",
+ "label": "Flow 4",
+ "disabled": true,
+ "info": "",
+ "env": []
+ },
+ {
+ "id": "2c8bcaa0046b4323",
+ "type": "ui-theme",
+ "name": "Default",
+ "colors": {
+ "surface": "#ffffff",
+ "primary": "#0094ce",
+ "bgPage": "#eeeeee",
+ "groupBg": "#ffffff",
+ "groupOutline": "#cccccc"
+ },
+ "sizes": {
+ "density": "default",
+ "pagePadding": "12px",
+ "groupGap": "12px",
+ "groupBorderRadius": "4px",
+ "widgetGap": "12px"
+ }
+ },
+ {
+ "id": "ac25cd90dc999a5a",
+ "type": "ui-base",
+ "name": "UI Name",
+ "path": "/dashboard",
+ "appIcon": "",
+ "includeClientData": true,
+ "acceptsClientConfig": [
+ "ui-notification",
+ "ui-control"
+ ],
+ "showPathInSidebar": false,
+ "headerContent": "page",
+ "navigationStyle": "default",
+ "titleBarStyle": "default",
+ "showReconnectNotification": true,
+ "notificationDisplayTime": 1,
+ "showDisconnectNotification": true,
+ "allowInstall": true
+ },
+ {
+ "id": "ec4a923c5ead6278",
+ "type": "ui-page",
+ "name": "Dashboard",
+ "ui": "ac25cd90dc999a5a",
+ "path": "/page1",
+ "icon": "home",
+ "layout": "grid",
+ "theme": "2c8bcaa0046b4323",
+ "breakpoints": [
+ {
+ "name": "Default",
+ "px": "0",
+ "cols": "3"
+ },
+ {
+ "name": "Tablet",
+ "px": "576",
+ "cols": "6"
+ },
+ {
+ "name": "Small Desktop",
+ "px": "768",
+ "cols": "9"
+ },
+ {
+ "name": "Desktop",
+ "px": "1024",
+ "cols": "12"
+ }
+ ],
+ "order": -1,
+ "className": "",
+ "visible": "true",
+ "disabled": "false"
+ },
+ {
+ "id": "58b5e9368ec5774b",
+ "type": "ui-group",
+ "name": "Group 1",
+ "page": "ec4a923c5ead6278",
+ "width": 6,
+ "height": 1,
+ "order": -1,
+ "showTitle": true,
+ "className": "",
+ "visible": "true",
+ "disabled": "false",
+ "groupType": "default"
+ },
+ {
+ "id": "14172c57f4c6ff14",
+ "type": "ui-group",
+ "name": "Group 2",
+ "page": "ec4a923c5ead6278",
+ "width": 6,
+ "height": 1,
+ "order": -1,
+ "showTitle": true,
+ "className": "",
+ "visible": "true",
+ "disabled": "false",
+ "groupType": "default"
+ },
+ {
+ "id": "ca564642bfc5606c",
+ "type": "ui-page",
+ "name": "PFR",
+ "ui": "ac25cd90dc999a5a",
+ "path": "/page2",
+ "icon": "home",
+ "layout": "grid",
+ "theme": "2c8bcaa0046b4323",
+ "breakpoints": [
+ {
+ "name": "Default",
+ "px": "0",
+ "cols": "3"
+ },
+ {
+ "name": "Tablet",
+ "px": "576",
+ "cols": "6"
+ },
+ {
+ "name": "Small Desktop",
+ "px": "768",
+ "cols": "9"
+ },
+ {
+ "name": "Desktop",
+ "px": "1024",
+ "cols": "12"
+ }
+ ],
+ "order": -1,
+ "className": "",
+ "visible": "true",
+ "disabled": "false"
+ },
+ {
+ "id": "ae38454098a37db0",
+ "type": "ui-group",
+ "name": "Group 3",
+ "page": "ca564642bfc5606c",
+ "width": 6,
+ "height": 1,
+ "order": -1,
+ "showTitle": true,
+ "className": "",
+ "visible": "true",
+ "disabled": "false",
+ "groupType": "default"
+ },
+ {
+ "id": "de8b029d69f26c0e",
+ "type": "ui-group",
+ "name": "Group 4",
+ "page": "ca564642bfc5606c",
+ "width": 6,
+ "height": 1,
+ "order": -1,
+ "showTitle": true,
+ "className": "",
+ "visible": "true",
+ "disabled": "false",
+ "groupType": "default"
+ },
+ {
+ "id": "f7803caf86a911f6",
+ "type": "inject",
+ "z": "31bba0914516dd85",
+ "name": "",
+ "props": [
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ }
+ ],
+ "repeat": "1",
+ "crontab": "",
+ "once": true,
+ "onceDelay": 0.1,
+ "topic": "clock",
+ "x": 200,
+ "y": 340,
+ "wires": [
+ [
+ "5266f4e09e7b919b"
+ ]
+ ]
+ },
+ {
+ "id": "98f5ffa4bed3b99f",
+ "type": "inject",
+ "z": "31bba0914516dd85",
+ "name": "Influx composition",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ },
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ }
+ ],
+ "repeat": "",
+ "crontab": "",
+ "once": true,
+ "onceDelay": "5",
+ "topic": "Fluent",
+ "payload": "{\"inlet\":0,\"F\":1000,\"C\":[0,30,100,16,0,0,5,25,75,30,0,0,125]}",
+ "payloadType": "json",
+ "x": 170,
+ "y": 260,
+ "wires": [
+ [
+ "5266f4e09e7b919b"
+ ]
+ ]
+ },
+ {
+ "id": "f1b3cffbd2d38473",
+ "type": "ui-chart",
+ "z": "31bba0914516dd85",
+ "group": "14172c57f4c6ff14",
+ "name": "Effluent",
+ "label": "Effluent",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1420,
+ "y": 300,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "fc4aa2928bdbe228",
+ "type": "function",
+ "z": "31bba0914516dd85",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"S_O\", \"Y\": S_O},\n { \"Series\": \"S_I\", \"Y\": S_I},\n { \"Series\": \"S_S\", \"Y\": S_S},\n { \"Series\": \"S_NH\", \"Y\": S_NH},\n { \"Series\": \"S_N2\", \"Y\": S_N2},\n { \"Series\": \"S_NO\", \"Y\": S_NO},\n { \"Series\": \"S_HCO\", \"Y\": S_HCO}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 1220,
+ "y": 300,
+ "wires": [
+ [
+ "f1b3cffbd2d38473"
+ ]
+ ]
+ },
+ {
+ "id": "e955d0c2d3246c4b",
+ "type": "ui-chart",
+ "z": "31bba0914516dd85",
+ "group": "58b5e9368ec5774b",
+ "name": "Anoxic reactor",
+ "label": "Anoxic reactor",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1040,
+ "y": 180,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "59f0787fadf99939",
+ "type": "function",
+ "z": "31bba0914516dd85",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"S_O\", \"Y\": S_O},\n { \"Series\": \"S_I\", \"Y\": S_I},\n { \"Series\": \"S_S\", \"Y\": S_S},\n { \"Series\": \"S_NH\", \"Y\": S_NH},\n { \"Series\": \"S_N2\", \"Y\": S_N2},\n { \"Series\": \"S_NO\", \"Y\": S_NO},\n { \"Series\": \"S_HCO\", \"Y\": S_HCO},\n { \"Series\": \"X_I\", \"Y\": X_I},\n { \"Series\": \"X_S\", \"Y\": X_S},\n { \"Series\": \"X_H\", \"Y\": X_H},\n { \"Series\": \"X_STO\", \"Y\": X_STO},\n { \"Series\": \"X_A\", \"Y\": X_A},\n { \"Series\": \"X_TS\", \"Y\": X_TS}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 820,
+ "y": 180,
+ "wires": [
+ [
+ "e955d0c2d3246c4b"
+ ]
+ ]
+ },
+ {
+ "id": "bb63e864735f963f",
+ "type": "ui-chart",
+ "z": "31bba0914516dd85",
+ "group": "14172c57f4c6ff14",
+ "name": "Sludge composition",
+ "label": "Sludge composition",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1450,
+ "y": 340,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "ca96bcb7f32f011f",
+ "type": "function",
+ "z": "31bba0914516dd85",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"X_I\", \"Y\": X_I},\n { \"Series\": \"X_S\", \"Y\": X_S},\n { \"Series\": \"X_H\", \"Y\": X_H},\n { \"Series\": \"X_STO\", \"Y\": X_STO},\n { \"Series\": \"X_A\", \"Y\": X_A},\n { \"Series\": \"X_TS\", \"Y\": X_TS}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 1220,
+ "y": 340,
+ "wires": [
+ [
+ "bb63e864735f963f"
+ ]
+ ]
+ },
+ {
+ "id": "9327869b411c3063",
+ "type": "ui-chart",
+ "z": "31bba0914516dd85",
+ "group": "58b5e9368ec5774b",
+ "name": "Aerobic reactor",
+ "label": "Aerobic reactor / recirculation",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1440,
+ "y": 260,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "3cb7fec9537ac405",
+ "type": "function",
+ "z": "31bba0914516dd85",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"S_O\", \"Y\": S_O},\n { \"Series\": \"S_I\", \"Y\": S_I},\n { \"Series\": \"S_S\", \"Y\": S_S},\n { \"Series\": \"S_NH\", \"Y\": S_NH},\n { \"Series\": \"S_N2\", \"Y\": S_N2},\n { \"Series\": \"S_NO\", \"Y\": S_NO},\n { \"Series\": \"S_HCO\", \"Y\": S_HCO},\n { \"Series\": \"X_I\", \"Y\": X_I},\n { \"Series\": \"X_S\", \"Y\": X_S},\n { \"Series\": \"X_H\", \"Y\": X_H},\n { \"Series\": \"X_STO\", \"Y\": X_STO},\n { \"Series\": \"X_A\", \"Y\": X_A},\n { \"Series\": \"X_TS\", \"Y\": X_TS}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 1220,
+ "y": 260,
+ "wires": [
+ [
+ "9327869b411c3063"
+ ]
+ ]
+ },
+ {
+ "id": "640ecab878ee623a",
+ "type": "debug",
+ "z": "31bba0914516dd85",
+ "name": "Sludge removal",
+ "active": true,
+ "tosidebar": true,
+ "console": false,
+ "tostatus": false,
+ "complete": "payload",
+ "targetType": "msg",
+ "statusVal": "",
+ "statusType": "auto",
+ "x": 1160,
+ "y": 420,
+ "wires": []
+ },
+ {
+ "id": "8e1117ff307f949b",
+ "type": "debug",
+ "z": "31bba0914516dd85",
+ "name": "Sludge recirculation",
+ "active": true,
+ "tosidebar": true,
+ "console": false,
+ "tostatus": false,
+ "complete": "payload",
+ "targetType": "msg",
+ "statusVal": "",
+ "statusType": "auto",
+ "x": 930,
+ "y": 420,
+ "wires": []
+ },
+ {
+ "id": "d9e3b28718762905",
+ "type": "debug",
+ "z": "31bba0914516dd85",
+ "name": "Effluent",
+ "active": true,
+ "tosidebar": true,
+ "console": false,
+ "tostatus": false,
+ "complete": "payload",
+ "targetType": "msg",
+ "statusVal": "",
+ "statusType": "auto",
+ "x": 680,
+ "y": 420,
+ "wires": []
+ },
+ {
+ "id": "9534da473265bb6a",
+ "type": "recirculation-pump",
+ "z": "31bba0914516dd85",
+ "name": "",
+ "F2": "50",
+ "inlet": "2",
+ "x": 930,
+ "y": 340,
+ "wires": [
+ [
+ "ca96bcb7f32f011f",
+ "640ecab878ee623a"
+ ],
+ [
+ "8e1117ff307f949b",
+ "5266f4e09e7b919b"
+ ]
+ ]
+ },
+ {
+ "id": "038a9d67ce069678",
+ "type": "settling-basin",
+ "z": "31bba0914516dd85",
+ "name": "",
+ "TS_set": "5400",
+ "inlet": "1",
+ "x": 700,
+ "y": 340,
+ "wires": [
+ [
+ "fc4aa2928bdbe228",
+ "d9e3b28718762905"
+ ],
+ [
+ "9534da473265bb6a"
+ ]
+ ]
+ },
+ {
+ "id": "1cb62ce7d6e2b362",
+ "type": "recirculation-pump",
+ "z": "31bba0914516dd85",
+ "name": "",
+ "F2": "3000",
+ "inlet": 1,
+ "x": 470,
+ "y": 340,
+ "wires": [
+ [
+ "038a9d67ce069678"
+ ],
+ [
+ "5266f4e09e7b919b"
+ ]
+ ]
+ },
+ {
+ "id": "2ac1635a77880b09",
+ "type": "advancedReactor",
+ "z": "31bba0914516dd85",
+ "name": "Aerobic 2",
+ "reactor_type": "CSTR",
+ "volume": "400",
+ "length": "",
+ "resolution_L": "",
+ "alpha": "",
+ "n_inlets": 1,
+ "kla": "7500",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": 0.001,
+ "X_TS_init": 125.0009,
+ "x": 960,
+ "y": 260,
+ "wires": [
+ [
+ "3cb7fec9537ac405",
+ "1cb62ce7d6e2b362"
+ ],
+ [],
+ []
+ ]
+ },
+ {
+ "id": "5f39b76fc9528f75",
+ "type": "advancedReactor",
+ "z": "31bba0914516dd85",
+ "name": "Aerobic 1",
+ "reactor_type": "CSTR",
+ "volume": "400",
+ "length": "",
+ "resolution_L": "",
+ "alpha": "",
+ "n_inlets": 1,
+ "kla": "7500",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": "30",
+ "X_TS_init": "132",
+ "x": 780,
+ "y": 260,
+ "wires": [
+ [],
+ [],
+ [
+ "2ac1635a77880b09"
+ ]
+ ]
+ },
+ {
+ "id": "b38f1a7b0ab6a7c7",
+ "type": "advancedReactor",
+ "z": "31bba0914516dd85",
+ "name": "Anoxic 2",
+ "reactor_type": "CSTR",
+ "volume": "400",
+ "length": "",
+ "resolution_L": "",
+ "alpha": "",
+ "n_inlets": 1,
+ "kla": "",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": 0.001,
+ "X_TS_init": 125.0009,
+ "x": 600,
+ "y": 260,
+ "wires": [
+ [
+ "59f0787fadf99939"
+ ],
+ [],
+ [
+ "5f39b76fc9528f75"
+ ]
+ ]
+ },
+ {
+ "id": "5266f4e09e7b919b",
+ "type": "advancedReactor",
+ "z": "31bba0914516dd85",
+ "name": "Anoxic 1",
+ "reactor_type": "CSTR",
+ "volume": "400",
+ "length": "",
+ "resolution_L": "",
+ "alpha": "",
+ "n_inlets": "3",
+ "kla": "",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": 0.001,
+ "X_TS_init": 125.0009,
+ "x": 420,
+ "y": 260,
+ "wires": [
+ [],
+ [],
+ [
+ "b38f1a7b0ab6a7c7"
+ ]
+ ]
+ },
+ {
+ "id": "5865699f68c9aa64",
+ "type": "inject",
+ "z": "0abdac5260d9553e",
+ "name": "",
+ "props": [
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ }
+ ],
+ "repeat": "1",
+ "crontab": "",
+ "once": true,
+ "onceDelay": 0.1,
+ "topic": "clock",
+ "x": 300,
+ "y": 260,
+ "wires": [
+ [
+ "5ba082534d7b491e"
+ ]
+ ]
+ },
+ {
+ "id": "061920b87a45057d",
+ "type": "inject",
+ "z": "0abdac5260d9553e",
+ "name": "Influx composition 1",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ },
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ }
+ ],
+ "repeat": "1440",
+ "crontab": "",
+ "once": true,
+ "onceDelay": "5",
+ "topic": "Fluent",
+ "payload": "{\"inlet\":0,\"F\":6600,\"C\":[0,30,100,16,0,0,5,25,75,30,0,0,125]}",
+ "payloadType": "json",
+ "x": 260,
+ "y": 180,
+ "wires": [
+ [
+ "5ba082534d7b491e"
+ ]
+ ]
+ },
+ {
+ "id": "c2338b164df519f6",
+ "type": "debug",
+ "z": "0abdac5260d9553e",
+ "name": "Sludge removal",
+ "active": false,
+ "tosidebar": true,
+ "console": false,
+ "tostatus": false,
+ "complete": "payload",
+ "targetType": "msg",
+ "statusVal": "",
+ "statusType": "auto",
+ "x": 1260,
+ "y": 340,
+ "wires": []
+ },
+ {
+ "id": "724aa3442b6fc5fc",
+ "type": "debug",
+ "z": "0abdac5260d9553e",
+ "name": "Sludge recirculation",
+ "active": false,
+ "tosidebar": true,
+ "console": false,
+ "tostatus": false,
+ "complete": "payload",
+ "targetType": "msg",
+ "statusVal": "",
+ "statusType": "auto",
+ "x": 1030,
+ "y": 340,
+ "wires": []
+ },
+ {
+ "id": "fd2e755a96891ec3",
+ "type": "debug",
+ "z": "0abdac5260d9553e",
+ "name": "Effluent",
+ "active": false,
+ "tosidebar": true,
+ "console": false,
+ "tostatus": false,
+ "complete": "payload",
+ "targetType": "msg",
+ "statusVal": "",
+ "statusType": "auto",
+ "x": 780,
+ "y": 340,
+ "wires": []
+ },
+ {
+ "id": "c509ace161289789",
+ "type": "recirculation-pump",
+ "z": "0abdac5260d9553e",
+ "name": "",
+ "F2": "1000",
+ "inlet": "2",
+ "x": 1030,
+ "y": 260,
+ "wires": [
+ [
+ "c2338b164df519f6",
+ "c2fd7710c8b22ffa"
+ ],
+ [
+ "724aa3442b6fc5fc",
+ "5ba082534d7b491e",
+ "edbda618f142adfa"
+ ]
+ ]
+ },
+ {
+ "id": "b914e9abe9d60945",
+ "type": "settling-basin",
+ "z": "0abdac5260d9553e",
+ "name": "",
+ "TS_set": "5400",
+ "inlet": "1",
+ "x": 800,
+ "y": 260,
+ "wires": [
+ [
+ "fd2e755a96891ec3"
+ ],
+ [
+ "c509ace161289789"
+ ]
+ ]
+ },
+ {
+ "id": "dc2d2c985e2fdff6",
+ "type": "recirculation-pump",
+ "z": "0abdac5260d9553e",
+ "name": "",
+ "F2": "1100",
+ "inlet": 1,
+ "x": 570,
+ "y": 260,
+ "wires": [
+ [
+ "b914e9abe9d60945"
+ ],
+ [
+ "5ba082534d7b491e"
+ ]
+ ]
+ },
+ {
+ "id": "7f94060aa59d6c3a",
+ "type": "advancedReactor",
+ "z": "0abdac5260d9553e",
+ "name": "Aerobic 1",
+ "reactor_type": "PFR",
+ "volume": "1470",
+ "length": "20",
+ "resolution_L": "20",
+ "alpha": "0",
+ "n_inlets": 1,
+ "kla": "7500",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": "30",
+ "X_TS_init": "132",
+ "enableLog": true,
+ "logLevel": "debug",
+ "positionVsParent": "atEquipment",
+ "x": 1060,
+ "y": 180,
+ "wires": [
+ [
+ "dc2d2c985e2fdff6",
+ "a5d1282993a362c9",
+ "368215b8dd484211"
+ ],
+ [],
+ []
+ ]
+ },
+ {
+ "id": "5ba082534d7b491e",
+ "type": "advancedReactor",
+ "z": "0abdac5260d9553e",
+ "name": "Anoxic 1",
+ "reactor_type": "PFR",
+ "volume": "730",
+ "length": "10",
+ "resolution_L": "10",
+ "alpha": "0",
+ "n_inlets": "3",
+ "kla": "",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": 0.001,
+ "X_TS_init": 125.0009,
+ "enableLog": true,
+ "logLevel": "debug",
+ "positionVsParent": "atEquipment",
+ "x": 540,
+ "y": 180,
+ "wires": [
+ [
+ "4874a8564327e7ab"
+ ],
+ [],
+ [
+ "7f94060aa59d6c3a"
+ ]
+ ]
+ },
+ {
+ "id": "4874a8564327e7ab",
+ "type": "function",
+ "z": "0abdac5260d9553e",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"S_O\", \"Y\": S_O},\n { \"Series\": \"S_I\", \"Y\": S_I},\n { \"Series\": \"S_S\", \"Y\": S_S},\n { \"Series\": \"S_NH\", \"Y\": S_NH},\n { \"Series\": \"S_N2\", \"Y\": S_N2},\n { \"Series\": \"S_NO\", \"Y\": S_NO},\n { \"Series\": \"S_HCO\", \"Y\": S_HCO},\n { \"Series\": \"X_I\", \"Y\": X_I},\n { \"Series\": \"X_S\", \"Y\": X_S},\n { \"Series\": \"X_H\", \"Y\": X_H},\n { \"Series\": \"X_STO\", \"Y\": X_STO},\n { \"Series\": \"X_A\", \"Y\": X_A},\n { \"Series\": \"X_TS\", \"Y\": X_TS}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 800,
+ "y": 120,
+ "wires": [
+ [
+ "ac91a2c6413414f8"
+ ]
+ ]
+ },
+ {
+ "id": "ac91a2c6413414f8",
+ "type": "ui-chart",
+ "z": "0abdac5260d9553e",
+ "group": "ae38454098a37db0",
+ "name": "Anoxic reactor",
+ "label": "Anoxic reactor",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1020,
+ "y": 120,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "a5d1282993a362c9",
+ "type": "function",
+ "z": "0abdac5260d9553e",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"S_O\", \"Y\": S_O},\n { \"Series\": \"S_I\", \"Y\": S_I},\n { \"Series\": \"S_S\", \"Y\": S_S},\n { \"Series\": \"S_NH\", \"Y\": S_NH},\n { \"Series\": \"S_N2\", \"Y\": S_N2},\n { \"Series\": \"S_NO\", \"Y\": S_NO},\n { \"Series\": \"S_HCO\", \"Y\": S_HCO},\n { \"Series\": \"X_I\", \"Y\": X_I},\n { \"Series\": \"X_S\", \"Y\": X_S},\n { \"Series\": \"X_H\", \"Y\": X_H},\n { \"Series\": \"X_STO\", \"Y\": X_STO},\n { \"Series\": \"X_A\", \"Y\": X_A},\n { \"Series\": \"X_TS\", \"Y\": X_TS}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 1260,
+ "y": 180,
+ "wires": [
+ [
+ "e61130eff38ee89a"
+ ]
+ ]
+ },
+ {
+ "id": "e61130eff38ee89a",
+ "type": "ui-chart",
+ "z": "0abdac5260d9553e",
+ "group": "ae38454098a37db0",
+ "name": "Aerobic reactor",
+ "label": "Aerobic reactor / recirculation",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1480,
+ "y": 180,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "c2fd7710c8b22ffa",
+ "type": "function",
+ "z": "0abdac5260d9553e",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"S_O\", \"Y\": S_O},\n { \"Series\": \"S_I\", \"Y\": S_I},\n { \"Series\": \"S_S\", \"Y\": S_S},\n { \"Series\": \"S_NH\", \"Y\": S_NH},\n { \"Series\": \"S_N2\", \"Y\": S_N2},\n { \"Series\": \"S_NO\", \"Y\": S_NO},\n { \"Series\": \"S_HCO\", \"Y\": S_HCO}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 1260,
+ "y": 240,
+ "wires": [
+ [
+ "6cfb58885cf36b74"
+ ]
+ ]
+ },
+ {
+ "id": "6cfb58885cf36b74",
+ "type": "ui-chart",
+ "z": "0abdac5260d9553e",
+ "group": "de8b029d69f26c0e",
+ "name": "Effluent",
+ "label": "Effluent",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1460,
+ "y": 240,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "edbda618f142adfa",
+ "type": "function",
+ "z": "0abdac5260d9553e",
+ "name": "Data_converter",
+ "func": "if (msg.topic != \"Fluent\") {\n return;\n}\n\nconst [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = msg.payload.C;\n\nmsg = {payload: [\n { \"Series\": \"X_I\", \"Y\": X_I},\n { \"Series\": \"X_S\", \"Y\": X_S},\n { \"Series\": \"X_H\", \"Y\": X_H},\n { \"Series\": \"X_STO\", \"Y\": X_STO},\n { \"Series\": \"X_A\", \"Y\": X_A},\n { \"Series\": \"X_TS\", \"Y\": X_TS}\n ]};\n\nreturn msg;",
+ "outputs": 1,
+ "timeout": 0,
+ "noerr": 0,
+ "initialize": "",
+ "finalize": "",
+ "libs": [],
+ "x": 1260,
+ "y": 280,
+ "wires": [
+ [
+ "95dc5302c82d6bcb"
+ ]
+ ]
+ },
+ {
+ "id": "95dc5302c82d6bcb",
+ "type": "ui-chart",
+ "z": "0abdac5260d9553e",
+ "group": "de8b029d69f26c0e",
+ "name": "Sludge composition",
+ "label": "Sludge composition",
+ "order": 9007199254740991,
+ "chartType": "line",
+ "category": "Series",
+ "categoryType": "property",
+ "xAxisLabel": "",
+ "xAxisProperty": "",
+ "xAxisPropertyType": "timestamp",
+ "xAxisType": "time",
+ "xAxisFormat": "",
+ "xAxisFormatType": "auto",
+ "xmin": "",
+ "xmax": "",
+ "yAxisLabel": "",
+ "yAxisProperty": "Y",
+ "yAxisPropertyType": "property",
+ "ymin": "",
+ "ymax": "",
+ "bins": 10,
+ "action": "append",
+ "stackSeries": false,
+ "pointShape": "circle",
+ "pointRadius": 4,
+ "showLegend": true,
+ "removeOlder": "8",
+ "removeOlderUnit": "3600",
+ "removeOlderPoints": "2000",
+ "colors": [
+ "#0095ff",
+ "#ff0000",
+ "#ff7f0e",
+ "#2ca02c",
+ "#a347e1",
+ "#d62728",
+ "#ff9896",
+ "#9467bd",
+ "#c5b0d5"
+ ],
+ "textColor": [
+ "#666666"
+ ],
+ "textColorDefault": true,
+ "gridColor": [
+ "#e5e5e5"
+ ],
+ "gridColorDefault": true,
+ "width": 6,
+ "height": 8,
+ "className": "",
+ "interpolation": "linear",
+ "x": 1490,
+ "y": 280,
+ "wires": [
+ []
+ ]
+ },
+ {
+ "id": "cb4329d4882d3b10",
+ "type": "inject",
+ "z": "0abdac5260d9553e",
+ "name": "",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ }
+ ],
+ "repeat": "",
+ "crontab": "",
+ "once": true,
+ "onceDelay": 0.1,
+ "topic": "Dispersion",
+ "payload": "10000",
+ "payloadType": "num",
+ "x": 290,
+ "y": 340,
+ "wires": [
+ [
+ "5ba082534d7b491e",
+ "7f94060aa59d6c3a"
+ ]
+ ]
+ },
+ {
+ "id": "4b5a1cb582ce04a5",
+ "type": "inject",
+ "z": "0abdac5260d9553e",
+ "name": "Influx composition 2",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ },
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ }
+ ],
+ "repeat": "1440",
+ "crontab": "",
+ "once": true,
+ "onceDelay": "480",
+ "topic": "Fluent",
+ "payload": "{\"inlet\":0,\"F\":8000,\"C\":[0,50,125,20,0,0,6.25,10,50,11,0,0,81.5]}",
+ "payloadType": "json",
+ "x": 260,
+ "y": 140,
+ "wires": [
+ [
+ "5ba082534d7b491e"
+ ]
+ ]
+ },
+ {
+ "id": "68ba512b76ed980a",
+ "type": "inject",
+ "z": "0abdac5260d9553e",
+ "name": "Influx composition 3",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ },
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ }
+ ],
+ "repeat": "1440",
+ "crontab": "",
+ "once": true,
+ "onceDelay": "960",
+ "topic": "Fluent",
+ "payload": "{\"inlet\":0,\"F\":6600,\"C\":[0,25,95,12.8,0,0,4,25,75,40,0,0,134]}",
+ "payloadType": "json",
+ "x": 260,
+ "y": 100,
+ "wires": [
+ [
+ "5ba082534d7b491e"
+ ]
+ ]
+ },
+ {
+ "id": "368215b8dd484211",
+ "type": "debug",
+ "z": "0abdac5260d9553e",
+ "name": "debug 1",
+ "active": true,
+ "tosidebar": true,
+ "console": false,
+ "tostatus": false,
+ "complete": "false",
+ "statusVal": "",
+ "statusType": "auto",
+ "x": 1280,
+ "y": 100,
+ "wires": []
+ },
+ {
+ "id": "b5dde0cd3e3b7a9e",
+ "type": "inject",
+ "z": "394f713d4e71366c",
+ "name": "Influx composition 3",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ },
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ }
+ ],
+ "repeat": "1440",
+ "crontab": "",
+ "once": true,
+ "onceDelay": "960",
+ "topic": "Fluent",
+ "payload": "{\"inlet\":0,\"F\":1000,\"C\":[0,25,95,12.8,0,0,4,25,75,40,0,0,134]}",
+ "payloadType": "json",
+ "x": 220,
+ "y": 140,
+ "wires": [
+ [
+ "818dbe32cad9fa42"
+ ]
+ ]
+ },
+ {
+ "id": "74fa10e5ad6ac925",
+ "type": "inject",
+ "z": "394f713d4e71366c",
+ "name": "Influx composition 2",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ },
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ }
+ ],
+ "repeat": "1440",
+ "crontab": "",
+ "once": true,
+ "onceDelay": "480",
+ "topic": "Fluent",
+ "payload": "{\"inlet\":0,\"F\":1200,\"C\":[0,50,125,20,0,0,6.25,10,50,11,0,0,81.5]}",
+ "payloadType": "json",
+ "x": 220,
+ "y": 180,
+ "wires": [
+ [
+ "818dbe32cad9fa42"
+ ]
+ ]
+ },
+ {
+ "id": "ad54f09b8bb12e39",
+ "type": "inject",
+ "z": "394f713d4e71366c",
+ "name": "Influx composition 1",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ },
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ }
+ ],
+ "repeat": "1440",
+ "crontab": "",
+ "once": true,
+ "onceDelay": "5",
+ "topic": "Fluent",
+ "payload": "{\"inlet\":0,\"F\":1000,\"C\":[0,30,100,16,0,0,5,25,75,30,0,0,125]}",
+ "payloadType": "json",
+ "x": 220,
+ "y": 220,
+ "wires": [
+ [
+ "818dbe32cad9fa42"
+ ]
+ ]
+ },
+ {
+ "id": "2776f6ebd3205e51",
+ "type": "inject",
+ "z": "394f713d4e71366c",
+ "name": "",
+ "props": [
+ {
+ "p": "timestamp",
+ "v": "",
+ "vt": "date"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ }
+ ],
+ "repeat": "1",
+ "crontab": "",
+ "once": true,
+ "onceDelay": 0.1,
+ "topic": "clock",
+ "x": 260,
+ "y": 300,
+ "wires": [
+ [
+ "818dbe32cad9fa42"
+ ]
+ ]
+ },
+ {
+ "id": "8538c18935bee1bf",
+ "type": "inject",
+ "z": "394f713d4e71366c",
+ "name": "",
+ "props": [
+ {
+ "p": "payload"
+ },
+ {
+ "p": "topic",
+ "vt": "str"
+ }
+ ],
+ "repeat": "",
+ "crontab": "",
+ "once": true,
+ "onceDelay": 0.1,
+ "topic": "Dispersion",
+ "payload": "3000",
+ "payloadType": "num",
+ "x": 240,
+ "y": 380,
+ "wires": [
+ [
+ "818dbe32cad9fa42",
+ "c3d507ed7b05c089"
+ ]
+ ]
+ },
+ {
+ "id": "818dbe32cad9fa42",
+ "type": "advancedReactor",
+ "z": "394f713d4e71366c",
+ "name": "Anoxic 1",
+ "reactor_type": "PFR",
+ "volume": "800",
+ "length": "30",
+ "resolution_L": "20",
+ "alpha": "0",
+ "n_inlets": "3",
+ "kla": "",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": 0.001,
+ "X_TS_init": 125.0009,
+ "enableLog": false,
+ "logLevel": "info",
+ "positionVsParent": "downstream",
+ "x": 600,
+ "y": 220,
+ "wires": [
+ [],
+ [],
+ [
+ "c3d507ed7b05c089"
+ ]
+ ]
+ },
+ {
+ "id": "c3d507ed7b05c089",
+ "type": "advancedReactor",
+ "z": "394f713d4e71366c",
+ "name": "Aerobic 1",
+ "reactor_type": "PFR",
+ "volume": "800",
+ "length": "30",
+ "resolution_L": "20",
+ "alpha": "0",
+ "n_inlets": 1,
+ "kla": "7500",
+ "S_O_init": 0,
+ "S_I_init": 30,
+ "S_S_init": 100,
+ "S_NH_init": 16,
+ "S_N2_init": 0,
+ "S_NO_init": 0,
+ "S_HCO_init": 5,
+ "X_I_init": 25,
+ "X_S_init": 75,
+ "X_H_init": 30,
+ "X_STO_init": 0,
+ "X_A_init": "30",
+ "X_TS_init": "132",
+ "enableLog": false,
+ "logLevel": "info",
+ "positionVsParent": "upstream",
+ "x": 1020,
+ "y": 220,
+ "wires": [
+ [],
+ [],
+ []
+ ]
+ }
+]
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
new file mode 100644
index 0000000..a738306
--- /dev/null
+++ b/package-lock.json
@@ -0,0 +1,119 @@
+{
+ "name": "asm3",
+ "version": "0.0.1",
+ "lockfileVersion": 3,
+ "requires": true,
+ "packages": {
+ "": {
+ "name": "asm3",
+ "version": "0.0.1",
+ "license": "SEE LICENSE",
+ "dependencies": {
+ "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/p.vanderwilt/generalFunctions.git#fix-missing-references",
+ "mathjs": "^14.5.2"
+ }
+ },
+ "node_modules/@babel/runtime": {
+ "version": "7.28.3",
+ "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.3.tgz",
+ "integrity": "sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">=6.9.0"
+ }
+ },
+ "node_modules/complex.js": {
+ "version": "2.4.2",
+ "resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.4.2.tgz",
+ "integrity": "sha512-qtx7HRhPGSCBtGiST4/WGHuW+zeaND/6Ld+db6PbrulIB1i2Ev/2UPiqcmpQNPSyfBKraC0EOvOKCB5dGZKt3g==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/decimal.js": {
+ "version": "10.6.0",
+ "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz",
+ "integrity": "sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==",
+ "license": "MIT"
+ },
+ "node_modules/escape-latex": {
+ "version": "1.2.0",
+ "resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
+ "integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw==",
+ "license": "MIT"
+ },
+ "node_modules/fraction.js": {
+ "version": "5.3.4",
+ "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
+ "integrity": "sha512-1X1NTtiJphryn/uLQz3whtY6jK3fTqoE3ohKs0tT+Ujr1W59oopxmoEh7Lu5p6vBaPbgoM0bzveAW4Qi5RyWDQ==",
+ "license": "MIT",
+ "engines": {
+ "node": "*"
+ },
+ "funding": {
+ "type": "github",
+ "url": "https://github.com/sponsors/rawify"
+ }
+ },
+ "node_modules/generalFunctions": {
+ "version": "1.0.0",
+ "resolved": "git+https://gitea.centraal.wbd-rd.nl/p.vanderwilt/generalFunctions.git#302e12238745766a679ef11ca6ed5f4ea1548f87",
+ "license": "SEE LICENSE"
+ },
+ "node_modules/javascript-natural-sort": {
+ "version": "0.7.1",
+ "resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
+ "integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw==",
+ "license": "MIT"
+ },
+ "node_modules/mathjs": {
+ "version": "14.7.0",
+ "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.7.0.tgz",
+ "integrity": "sha512-RaMhb+9MSESjDZNox/FzzuFpIUI+oxGLyOy1t3BMoW53pGWnTzZtlucJ5cvbit0dIMYlCq00gNbW1giZX4/1Rg==",
+ "license": "Apache-2.0",
+ "dependencies": {
+ "@babel/runtime": "^7.26.10",
+ "complex.js": "^2.2.5",
+ "decimal.js": "^10.4.3",
+ "escape-latex": "^1.2.0",
+ "fraction.js": "^5.2.1",
+ "javascript-natural-sort": "^0.7.1",
+ "seedrandom": "^3.0.5",
+ "tiny-emitter": "^2.1.0",
+ "typed-function": "^4.2.1"
+ },
+ "bin": {
+ "mathjs": "bin/cli.js"
+ },
+ "engines": {
+ "node": ">= 18"
+ }
+ },
+ "node_modules/seedrandom": {
+ "version": "3.0.5",
+ "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
+ "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==",
+ "license": "MIT"
+ },
+ "node_modules/tiny-emitter": {
+ "version": "2.1.0",
+ "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
+ "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==",
+ "license": "MIT"
+ },
+ "node_modules/typed-function": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz",
+ "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==",
+ "license": "MIT",
+ "engines": {
+ "node": ">= 18"
+ }
+ }
+ }
+}
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..493dd5f
--- /dev/null
+++ b/package.json
@@ -0,0 +1,33 @@
+{
+ "name": "asm3",
+ "version": "0.0.1",
+ "description": "Implementation of the asm3 model for Node-Red",
+ "repository": {
+ "type": "git",
+ "url": "https://gitea.centraal.wbd-rd.nl/RnD/reactor.git"
+ },
+ "keywords": [
+ "asm3",
+ "activated sludge",
+ "wastewater",
+ "biological model",
+ "node-red"
+ ],
+ "license": "SEE LICENSE",
+ "author": "P.R. van der Wilt",
+ "main": "reactor.js",
+ "scripts": {
+ "test": "node reactor.js"
+ },
+ "node-red": {
+ "nodes": {
+ "reactor": "reactor.js",
+ "recirculation-pump": "additional_nodes/recirculation-pump.js",
+ "settling-basin": "additional_nodes/settling-basin.js"
+ }
+ },
+ "dependencies": {
+ "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/p.vanderwilt/generalFunctions.git#fix-missing-references",
+ "mathjs": "^14.5.2"
+ }
+}
diff --git a/reactor.html b/reactor.html
new file mode 100644
index 0000000..d3d9f2c
--- /dev/null
+++ b/reactor.html
@@ -0,0 +1,248 @@
+
+
+
+
+
+
+
diff --git a/reactor.js b/reactor.js
new file mode 100644
index 0000000..311698e
--- /dev/null
+++ b/reactor.js
@@ -0,0 +1,26 @@
+const nameOfNode = "advancedReactor"; // name of the node, should match file name and node type in Node-RED
+const nodeClass = require('./src/nodeClass.js'); // node class
+const { MenuManager } = require('generalFunctions');
+
+
+module.exports = function (RED) {
+ // Register the node type
+ RED.nodes.registerType(nameOfNode, function (config) {
+ // Initialize the Node-RED node first
+ RED.nodes.createNode(this, config);
+ // Then create your custom class and attach it
+ this.nodeClass = new nodeClass(config, RED, this, nameOfNode);
+ });
+
+ const menuMgr = new MenuManager();
+
+ // Serve /advancedReactor/menu.js
+ RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => {
+ try {
+ const script = menuMgr.createEndpoint(nameOfNode, ['logger', 'position']);
+ res.type('application/javascript').send(script);
+ } catch (err) {
+ res.status(500).send(`// Error generating menu: ${err.message}`);
+ }
+ });
+};
diff --git a/src/nodeClass.js b/src/nodeClass.js
new file mode 100644
index 0000000..a9e53d3
--- /dev/null
+++ b/src/nodeClass.js
@@ -0,0 +1,165 @@
+const { Reactor_CSTR, Reactor_PFR } = require('./specificClass.js');
+
+
+class nodeClass {
+ /**
+ * Node-RED node class for advanced-reactor.
+ * @param {object} uiConfig - Node-RED node configuration
+ * @param {object} RED - Node-RED runtime API
+ * @param {object} nodeInstance - Node-RED node instance
+ * @param {string} nameOfNode - Name of the node
+ */
+ constructor(uiConfig, RED, nodeInstance, nameOfNode) {
+ // Preserve RED reference for HTTP endpoints if needed
+ this.node = nodeInstance;
+ this.RED = RED;
+ this.name = nameOfNode;
+ this.source = null;
+
+ this._loadConfig(uiConfig)
+ this._setupClass();
+
+ this._attachInputHandler();
+ this._registerChild();
+ this._startTickLoop();
+ this._attachCloseHandler();
+ }
+
+ /**
+ * Handle node-red input messages
+ */
+ _attachInputHandler() {
+ this.node.on('input', (msg, send, done) => {
+
+ switch (msg.topic) {
+ case "clock":
+ this.source.updateState(msg.timestamp);
+ send([msg, null, null]);
+ break;
+ case "Fluent":
+ this.source.setInfluent = msg;
+ break;
+ case "OTR":
+ this.source.setOTR = msg;
+ break;
+ case "Temperature":
+ this.source.setTemperature = msg;
+ break;
+ case "Dispersion":
+ this.source.setDispersion = msg;
+ break;
+ case 'registerChild':
+ // Register this node as a parent of the child node
+ const childId = msg.payload;
+ const childObj = this.RED.nodes.getNode(childId);
+ this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent);
+ break;
+ default:
+ console.log("Unknown topic: " + msg.topic);
+ }
+
+ if (done) {
+ done();
+ }
+ });
+ }
+
+ /**
+ * Parse node configuration
+ * @param {object} uiConfig Config set in UI in node-red
+ */
+ _loadConfig(uiConfig) {
+ this.config = {
+ general: {
+ name: uiConfig.name || this.name,
+ id: this.node.id,
+ unit: null,
+ logging: {
+ enabled: uiConfig.enableLog,
+ logLevel: uiConfig.logLevel
+ }
+ },
+ functionality: {
+ positionVsParent: uiConfig.positionVsParent || 'atEquipment', // Default to 'atEquipment' if not specified
+ softwareType: "reactor" // should be set in config manager
+ },
+ reactor_type: uiConfig.reactor_type,
+ volume: parseFloat(uiConfig.volume),
+ length: parseFloat(uiConfig.length),
+ resolution_L: parseInt(uiConfig.resolution_L),
+ alpha: parseFloat(uiConfig.alpha),
+ n_inlets: parseInt(uiConfig.n_inlets),
+ kla: parseFloat(uiConfig.kla),
+ initialState: [
+ parseFloat(uiConfig.S_O_init),
+ parseFloat(uiConfig.S_I_init),
+ parseFloat(uiConfig.S_S_init),
+ parseFloat(uiConfig.S_NH_init),
+ parseFloat(uiConfig.S_N2_init),
+ parseFloat(uiConfig.S_NO_init),
+ parseFloat(uiConfig.S_HCO_init),
+ parseFloat(uiConfig.X_I_init),
+ parseFloat(uiConfig.X_S_init),
+ parseFloat(uiConfig.X_H_init),
+ parseFloat(uiConfig.X_STO_init),
+ parseFloat(uiConfig.X_A_init),
+ parseFloat(uiConfig.X_TS_init)
+ ],
+ timeStep: parseFloat(uiConfig.timeStep)
+ }
+ }
+
+ /**
+ * Register this node as a child upstream and downstream.
+ * Delayed to avoid Node-RED startup race conditions.
+ */
+ _registerChild() {
+ setTimeout(() => {
+ this.node.send([
+ null,
+ null,
+ { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }
+ ]);
+ }, 100);
+ }
+
+ /**
+ * Setup reactor class based on config
+ */
+ _setupClass() {
+ let new_reactor;
+
+ switch (this.config.reactor_type) {
+ case "CSTR":
+ new_reactor = new Reactor_CSTR(this.config);
+ break;
+ case "PFR":
+ new_reactor = new Reactor_PFR(this.config);
+ break;
+ default:
+ console.warn("Unknown reactor type: " + uiConfig.reactor_type);
+ }
+
+ this.source = new_reactor; // protect from reassignment
+ this.node.source = this.source;
+ }
+
+ _startTickLoop() {
+ setTimeout(() => {
+ this._tickInterval = setInterval(() => this._tick(), 1000);
+ }, 1000);
+ }
+
+ _tick(){
+ this.node.send([this.source.getEffluent, null, null]);
+ }
+
+ _attachCloseHandler() {
+ this.node.on('close', (done) => {
+ clearInterval(this._tickInterval);
+ done();
+ });
+ }
+}
+
+module.exports = nodeClass;
\ No newline at end of file
diff --git a/src/reaction_modules/asm3_class Koch.js b/src/reaction_modules/asm3_class Koch.js
new file mode 100644
index 0000000..e5e98b3
--- /dev/null
+++ b/src/reaction_modules/asm3_class Koch.js
@@ -0,0 +1,211 @@
+const math = require('mathjs')
+
+/**
+ * ASM3 class for the Activated Sludge Model No. 3 (ASM3). Using Koch et al. 2000 parameters.
+ */
+class ASM3 {
+
+ constructor() {
+ /**
+ * Kinetic parameters for ASM3 at 20 C. Using Koch et al. 2000 parameters.
+ * @property {Object} kin_params - Kinetic parameters
+ */
+ this.kin_params = {
+ // Hydrolysis
+ k_H: 9., // hydrolysis rate constant [g X_S g-1 X_H d-1]
+ K_X: 1., // hydrolysis saturation constant [g X_S g-1 X_H]
+ // Heterotrophs
+ k_STO: 12., // storage rate constant [g S_S g-1 X_H d-1]
+ nu_NO: 0.5, // anoxic reduction factor [-]
+ K_O: 0.2, // saturation constant S_0 [g O2 m-3]
+ K_NO: 0.5, // saturation constant S_NO [g NO3-N m-3]
+ K_S: 10., // saturation constant S_s [g COD m-3]
+ K_STO: 0.1, // saturation constant X_STO [g X_STO g-1 X_H]
+ mu_H_max: 3., // maximum specific growth rate [d-1]
+ K_NH: 0.01, // saturation constant S_NH3 [g NH3-N m-3]
+ K_HCO: 0.1, // saturation constant S_HCO [mole HCO3 m-3]
+ b_H_O: 0.3, // aerobic respiration rate [d-1]
+ b_H_NO: 0.15, // anoxic respiration rate [d-1]
+ b_STO_O: 0.3, // aerobic respitation rate X_STO [d-1]
+ b_STO_NO: 0.15, // anoxic respitation rate X_STO [d-1]
+ // Autotrophs
+ mu_A_max: 1.3, // maximum specific growth rate [d-1]
+ K_A_NH: 1.4, // saturation constant S_NH3 [g NH3-N m-3]
+ K_A_O: 0.5, // saturation constant S_0 [g O2 m-3]
+ K_A_HCO: 0.5, // saturation constant S_HCO [mole HCO3 m-3]
+ b_A_O: 0.20, // aerobic respiration rate [d-1]
+ b_A_NO: 0.10 // anoxic respiration rate [d-1]
+ };
+
+ /**
+ * Stoichiometric and composition parameters for ASM3. Using Koch et al. 2000 parameters.
+ * @property {Object} stoi_params - Stoichiometric parameters
+ */
+ this.stoi_params = {
+ // Fractions
+ f_SI: 0., // fraction S_I from hydrolysis [g S_I g-1 X_S]
+ f_XI: 0.2, // fraction X_I from decomp X_H [g X_I g-1 X_H]
+ // Yields
+ Y_STO_O: 0.80, // aerobic yield X_STO per S_S [g X_STO g-1 S_S]
+ Y_STO_NO: 0.70, // anoxic yield X_STO per S_S [g X_STO g-1 S_S]
+ Y_H_O: 0.80, // aerobic yield X_H per X_STO [g X_H g-1 X_STO]
+ Y_H_NO: 0.65, // anoxic yield X_H per X_STO [g X_H g-1 X_STO]
+ Y_A: 0.24, // anoxic yield X_A per S_NO [g X_A g-1 NO3-N]
+ // Composition (COD via DoR)
+ i_CODN: -1.71, // COD content (DoR) [g COD g-1 N2-N]
+ i_CODNO: -4.57, // COD content (DoR) [g COD g-1 NO3-N]
+ // Composition (nitrogen)
+ i_NSI: 0.01, // nitrogen content S_I [g N g-1 S_I]
+ i_NSS: 0.03, // nitrogen content S_S [g N g-1 S_S]
+ i_NXI: 0.04, // nitrogen content X_I [g N g-1 X_I]
+ i_NXS: 0.03, // nitrogen content X_S [g N g-1 X_S]
+ i_NBM: 0.07, // nitrogen content X_H / X_A [g N g-1 X_H / X_A]
+ // Composition (TSS)
+ i_TSXI: 0.75, // TSS content X_I [g TS g-1 X_I]
+ i_TSXS: 0.75, // TSS content X_S [g TS g-1 X_S]
+ i_TSBM: 0.90, // TSS content X_H / X_A [g TS g-1 X_H / X_A]
+ i_TSSTO: 0.60, // TSS content X_STO (PHB based) [g TS g-1 X_STO]
+ // Composition (charge)
+ i_cNH: 1/14, // charge per S_NH [mole H+ g-1 NH3-N]
+ i_cNO: -1/14 // charge per S_NO [mole H+ g-1 NO3-N]
+ };
+
+ /**
+ * Temperature theta parameters for ASM3. Using Koch et al. 2000 parameters.
+ * These parameters are used to adjust reaction rates based on temperature.
+ * @property {Object} temp_params - Temperature theta parameters
+ */
+ this.temp_params = {
+ // Hydrolysis
+ theta_H: 0.04,
+ // Heterotrophs
+ theta_STO: 0.07,
+ theta_mu_H: 0.07,
+ theta_b_H_O: 0.07,
+ theta_b_H_NO: 0.07,
+ theta_b_STO_O: this._compute_theta(0.1, 0.3, 10, 20),
+ theta_b_STO_NO: this._compute_theta(0.05, 0.15, 10, 20),
+ // Autotrophs
+ theta_mu_A: 0.105,
+ theta_b_A_O: 0.105,
+ theta_b_A_NO: 0.105
+ };
+
+ this.stoi_matrix = this._initialise_stoi_matrix();
+ }
+
+ /**
+ * Initialises the stoichiometric matrix for ASM3.
+ * @returns {Array} - The stoichiometric matrix for ASM3. (2D array)
+ */
+ _initialise_stoi_matrix() { // initialise stoichiometric matrix
+ const { f_SI, f_XI, Y_STO_O, Y_STO_NO, Y_H_O, Y_H_NO, Y_A, i_CODN, i_CODNO, i_NSI, i_NSS, i_NXI, i_NXS, i_NBM, i_TSXI, i_TSXS, i_TSBM, i_TSSTO, i_cNH, i_cNO } = this.stoi_params;
+
+ const stoi_matrix = Array(12);
+ // S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS
+ stoi_matrix[0] = [0., f_SI, 1.-f_SI, i_NXS-(1.-f_SI)*i_NSS-f_SI*i_NSI, 0., 0., (i_NXS-(1.-f_SI)*i_NSS-f_SI*i_NSI)*i_cNH, 0., -1., 0., 0., 0., -i_TSXS];
+ stoi_matrix[1] = [-(1.-Y_STO_O), 0, -1., i_NSS, 0., 0., i_NSS*i_cNH, 0., 0., 0., Y_STO_O, 0., Y_STO_O*i_TSSTO];
+ stoi_matrix[2] = [0., 0., -1., i_NSS, -(1.-Y_STO_NO)/(i_CODNO-i_CODN), (1.-Y_STO_NO)/(i_CODNO-i_CODN), i_NSS*i_cNH + (1.-Y_STO_NO)/(i_CODNO-i_CODN)*i_cNO, 0., 0., 0., Y_STO_NO, 0., Y_STO_NO*i_TSSTO];
+ stoi_matrix[3] = [-(1.-Y_H_O)/Y_H_O, 0., 0., -i_NBM, 0., 0., -i_NBM*i_cNH, 0., 0., 1., -1./Y_H_O, 0., i_TSBM-i_TSSTO/Y_H_O];
+ stoi_matrix[4] = [0., 0., 0., -i_NBM, -(1.-Y_H_NO)/(Y_H_NO*(i_CODNO-i_CODN)), (1.-Y_H_NO)/(Y_H_NO*(i_CODNO-i_CODN)), -i_NBM*i_cNH+(1.-Y_H_NO)/(Y_H_NO*(i_CODNO-i_CODN))*i_cNO, 0., 0., 1., -1./Y_H_NO, 0., i_TSBM-i_TSSTO/Y_H_NO];
+ stoi_matrix[5] = [f_XI-1., 0., 0., i_NBM-f_XI*i_NXI, 0., 0., (i_NBM-f_XI*i_NXI)*i_cNH, f_XI, 0., -1., 0., 0., f_XI*i_TSXI-i_TSBM];
+ stoi_matrix[6] = [0., 0., 0., i_NBM-f_XI*i_NXI, -(1.-f_XI)/(i_CODNO-i_CODN), (1.-f_XI)/(i_CODNO-i_CODN), (i_NBM-f_XI*i_NXI)*i_cNH+(1-f_XI)/(i_CODNO-i_CODN)*i_cNO, f_XI, 0., -1., 0., 0., f_XI*i_TSXI-i_TSBM];
+ stoi_matrix[7] = [-1., 0., 0., 0., 0., 0., 0., 0., 0., 0., -1., 0., -i_TSSTO];
+ stoi_matrix[8] = [0., 0., 0., 0., -1./(i_CODNO-i_CODN), 1./(i_CODNO-i_CODN), i_cNO/(i_CODNO-i_CODN), 0., 0., 0., -1., 0., -i_TSSTO];
+ stoi_matrix[9] = [1.+i_CODNO/Y_A, 0., 0., -1./Y_A-i_NBM, 0., 1./Y_A, (-1./Y_A-i_NBM)*i_cNH+i_cNO/Y_A, 0., 0., 0., 0., 1., i_TSBM];
+ stoi_matrix[10] = [f_XI-1., 0., 0., i_NBM-f_XI*i_NXI, 0., 0., (i_NBM-f_XI*i_NXI)*i_cNH, f_XI, 0., 0., 0., -1., f_XI*i_TSXI-i_TSBM];
+ stoi_matrix[11] = [0., 0., 0., i_NBM-f_XI*i_NXI, -(1.-f_XI)/(i_CODNO-i_CODN), (1.-f_XI)/(i_CODNO-i_CODN), (i_NBM-f_XI*i_NXI)*i_cNH+(1-f_XI)/(i_CODNO-i_CODN)*i_cNO, 0., 0., 0., 0., -1., f_XI*i_TSXI-i_TSBM];
+
+ return stoi_matrix[0].map((col, i) => stoi_matrix.map(row => row[i])); // transpose matrix
+ }
+
+ /**
+ * Computes the Monod equation rate value for a given concentration and half-saturation constant.
+ * @param {number} c - Concentration of reaction species.
+ * @param {number} K - Half-saturation constant for the reaction species.
+ * @returns {number} - Monod equation rate value for the given concentration and half-saturation constant.
+ */
+ _monod(c, K) {
+ return c / (K + c);
+ }
+
+ /**
+ * Computes the inverse Monod equation rate value for a given concentration and half-saturation constant. Used for inhibition.
+ * @param {number} c - Concentration of reaction species.
+ * @param {number} K - Half-saturation constant for the reaction species.
+ * @returns {number} - Inverse Monod equation rate value for the given concentration and half-saturation constant.
+ */
+ _inv_monod(c, K) {
+ return K / (K + c);
+ }
+
+ /**
+ * Adjust the rate parameter for temperature T using simplied Arrhenius equation based on rate constant at 20 degrees Celsius and theta parameter.
+ * @param {number} k - Rate constant at 20 degrees Celcius.
+ * @param {number} theta - Theta parameter.
+ * @param {number} T - Temperature in Celcius.
+ * @returns {number} - Adjusted rate parameter at temperature T based on the Arrhenius equation.
+ */
+ _arrhenius(k, theta, T) {
+ return k * Math.exp(theta*(T-20));
+ }
+
+ /**
+ * Computes the temperature theta parameter based on two rate constants and their corresponding temperatures.
+ * @param {number} k1 - Rate constant at temperature T1.
+ * @param {number} k2 - Rate constant at temperature T2.
+ * @param {number} T1 - Temperature T1 in Celcius.
+ * @param {number} T2 - Temperature T2 in Celcius.
+ * @returns {number} - Theta parameter.
+ */
+ _compute_theta(k1, k2, T1, T2) {
+ return Math.log(k1/k2)/(T1-T2);
+ }
+
+ /**
+ * Computes the reaction rates for each process reaction based on the current state and temperature.
+ * @param {Array} state - State vector containing concentrations of reaction species.
+ * @param {number} [T=20] - Temperature in degrees Celsius (default is 20).
+ * @returns {Array} - Reaction rates for each process reaction.
+ */
+ compute_rates(state, T = 20) {
+ // state: S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS
+ const rates = Array(12);
+ const [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = state;
+ const { k_H, K_X, k_STO, nu_NO, K_O, K_NO, K_S, K_STO, mu_H_max, K_NH, K_HCO, b_H_O, b_H_NO, b_STO_O, b_STO_NO, mu_A_max, K_A_NH, K_A_O, K_A_HCO, b_A_O, b_A_NO } = this.kin_params;
+ const { theta_H, theta_STO, theta_mu_H, theta_b_H_O, theta_b_H_NO, theta_b_STO_O, theta_b_STO_NO, theta_mu_A, theta_b_A_O, theta_b_A_NO } = this.temp_params;
+
+ // Hydrolysis
+ rates[0] = X_H == 0 ? 0 : this._arrhenius(k_H, theta_H, T) * this._monod(X_S / X_H, K_X) * X_H;
+
+ // Heterotrophs
+ rates[1] = this._arrhenius(k_STO, theta_STO, T) * this._monod(S_O, K_O) * this._monod(S_S, K_S) * X_H;
+ rates[2] = this._arrhenius(k_STO, theta_STO, T) * nu_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * this._monod(S_S, K_S) * X_H;
+ rates[3] = X_H == 0 ? 0 : this._arrhenius(mu_H_max, theta_mu_H, T) * this._monod(S_O, K_O) * this._monod(S_NH, K_NH) * this._monod(S_HCO, K_HCO) * this._monod(X_STO/X_H, K_STO) * X_H;
+ rates[4] = X_H == 0 ? 0 : this._arrhenius(mu_H_max, theta_mu_H, T) * nu_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * this._monod(S_NH, K_NH) * this._monod(S_HCO, K_HCO) * this._monod(X_STO/X_H, K_STO) * X_H;
+ rates[5] = this._arrhenius(b_H_O, theta_b_H_O, T) * this._monod(S_O, K_O) * X_H;
+ rates[6] = this._arrhenius(b_H_NO, theta_b_H_NO, T) * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_H;
+ rates[7] = this._arrhenius(b_STO_O, theta_b_STO_O, T) * this._monod(S_O, K_O) * X_H;
+ rates[8] = this._arrhenius(b_STO_NO, theta_b_STO_NO, T) * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_STO;
+
+ // Autotrophs
+ rates[9] = this._arrhenius(mu_A_max, theta_mu_A, T) * this._monod(S_O, K_A_O) * this._monod(S_NH, K_A_NH) * this._monod(S_HCO, K_A_HCO) * X_A;
+ rates[10] = this._arrhenius(b_A_O, theta_b_A_O, T) * this._monod(S_O, K_O) * X_A;
+ rates[11] = this._arrhenius(b_A_NO, theta_b_A_NO, T) * this._inv_monod(S_O, K_A_O) * this._monod(S_NO, K_NO) * X_A;
+
+ return rates;
+ }
+
+ /**
+ * Computes the change in concentrations of reaction species based on the current state and temperature.
+ * @param {Array} state - State vector containing concentrations of reaction species.
+ * @param {number} [T=20] - Temperature in degrees Celsius (default is 20).
+ * @returns {Array} - Change in reaction species concentrations.
+ */
+ compute_dC(state, T = 20) { // compute changes in concentrations
+ // state: S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS
+ return math.multiply(this.stoi_matrix, this.compute_rates(state, T));
+ }
+}
+
+module.exports = ASM3;
\ No newline at end of file
diff --git a/src/reaction_modules/asm3_class.js b/src/reaction_modules/asm3_class.js
new file mode 100644
index 0000000..d228619
--- /dev/null
+++ b/src/reaction_modules/asm3_class.js
@@ -0,0 +1,211 @@
+const math = require('mathjs')
+
+/**
+ * ASM3 class for the Activated Sludge Model No. 3 (ASM3).
+ */
+class ASM3 {
+
+ constructor() {
+ /**
+ * Kinetic parameters for ASM3 at 20 C.
+ * @property {Object} kin_params - Kinetic parameters
+ */
+ this.kin_params = {
+ // Hydrolysis
+ k_H: 3., // hydrolysis rate constant [g X_S g-1 X_H d-1]
+ K_X: 1., // hydrolysis saturation constant [g X_S g-1 X_H]
+ // Heterotrophs
+ k_STO: 5., // storage rate constant [g S_S g-1 X_H d-1]
+ nu_NO: 0.6, // anoxic reduction factor [-]
+ K_O: 0.2, // saturation constant S_0 [g O2 m-3]
+ K_NO: 0.5, // saturation constant S_NO [g NO3-N m-3]
+ K_S: 2., // saturation constant S_s [g COD m-3]
+ K_STO: 1., // saturation constant X_STO [g X_STO g-1 X_H]
+ mu_H_max: 2., // maximum specific growth rate [d-1]
+ K_NH: 0.01, // saturation constant S_NH3 [g NH3-N m-3]
+ K_HCO: 0.1, // saturation constant S_HCO [mole HCO3 m-3]
+ b_H_O: 0.2, // aerobic respiration rate [d-1]
+ b_H_NO: 0.1, // anoxic respiration rate [d-1]
+ b_STO_O: 0.2, // aerobic respitation rate X_STO [d-1]
+ b_STO_NO: 0.1, // anoxic respitation rate X_STO [d-1]
+ // Autotrophs
+ mu_A_max: 1.0, // maximum specific growth rate [d-1]
+ K_A_NH: 1., // saturation constant S_NH3 [g NH3-N m-3]
+ K_A_O: 0.5, // saturation constant S_0 [g O2 m-3]
+ K_A_HCO: 0.5, // saturation constant S_HCO [mole HCO3 m-3]
+ b_A_O: 0.15, // aerobic respiration rate [d-1]
+ b_A_NO: 0.05 // anoxic respiration rate [d-1]
+ };
+
+ /**
+ * Stoichiometric and composition parameters for ASM3.
+ * @property {Object} stoi_params - Stoichiometric parameters
+ */
+ this.stoi_params = {
+ // Fractions
+ f_SI: 0., // fraction S_I from hydrolysis [g S_I g-1 X_S]
+ f_XI: 0.2, // fraction X_I from decomp X_H [g X_I g-1 X_H]
+ // Yields
+ Y_STO_O: 0.85, // aerobic yield X_STO per S_S [g X_STO g-1 S_S]
+ Y_STO_NO: 0.80, // anoxic yield X_STO per S_S [g X_STO g-1 S_S]
+ Y_H_O: 0.63, // aerobic yield X_H per X_STO [g X_H g-1 X_STO]
+ Y_H_NO: 0.54, // anoxic yield X_H per X_STO [g X_H g-1 X_STO]
+ Y_A: 0.24, // anoxic yield X_A per S_NO [g X_A g-1 NO3-N]
+ // Composition (COD via DoR)
+ i_CODN: -1.71, // COD content (DoR) [g COD g-1 N2-N]
+ i_CODNO: -4.57, // COD content (DoR) [g COD g-1 NO3-N]
+ // Composition (nitrogen)
+ i_NSI: 0.01, // nitrogen content S_I [g N g-1 S_I]
+ i_NSS: 0.03, // nitrogen content S_S [g N g-1 S_S]
+ i_NXI: 0.02, // nitrogen content X_I [g N g-1 X_I]
+ i_NXS: 0.04, // nitrogen content X_S [g N g-1 X_S]
+ i_NBM: 0.07, // nitrogen content X_H / X_A [g N g-1 X_H / X_A]
+ // Composition (TSS)
+ i_TSXI: 0.75, // TSS content X_I [g TS g-1 X_I]
+ i_TSXS: 0.75, // TSS content X_S [g TS g-1 X_S]
+ i_TSBM: 0.90, // TSS content X_H / X_A [g TS g-1 X_H / X_A]
+ i_TSSTO: 0.60, // TSS content X_STO (PHB based) [g TS g-1 X_STO]
+ // Composition (charge)
+ i_cNH: 1/14, // charge per S_NH [mole H+ g-1 NH3-N]
+ i_cNO: -1/14 // charge per S_NO [mole H+ g-1 NO3-N]
+ };
+
+ /**
+ * Temperature theta parameters for ASM3.
+ * These parameters are used to adjust reaction rates based on temperature.
+ * @property {Object} temp_params - Temperature theta parameters
+ */
+ this.temp_params = {
+ // Hydrolysis
+ theta_H: this._compute_theta(2, 3, 10, 20),
+ // Heterotrophs
+ theta_STO: this._compute_theta(2.5, 5, 10, 20),
+ theta_mu_H: this._compute_theta(1, 2, 10, 20),
+ theta_b_H_O: this._compute_theta(0.1, 0.2, 10, 20),
+ theta_b_H_NO: this._compute_theta(0.05, 0.1, 10, 20),
+ theta_b_STO_O: this._compute_theta(0.1, 0.2, 10, 20),
+ theta_b_STO_NO: this._compute_theta(0.05, 0.1, 10, 20),
+ // Autotrophs
+ theta_mu_A: this._compute_theta(0.35, 1, 10, 20),
+ theta_b_A_O: this._compute_theta(0.05, 0.15, 10, 20),
+ theta_b_A_NO: this._compute_theta(0.02, 0.05, 10, 20)
+ };
+
+ this.stoi_matrix = this._initialise_stoi_matrix();
+ }
+
+ /**
+ * Initialises the stoichiometric matrix for ASM3.
+ * @returns {Array} - The stoichiometric matrix for ASM3. (2D array)
+ */
+ _initialise_stoi_matrix() { // initialise stoichiometric matrix
+ const { f_SI, f_XI, Y_STO_O, Y_STO_NO, Y_H_O, Y_H_NO, Y_A, i_CODN, i_CODNO, i_NSI, i_NSS, i_NXI, i_NXS, i_NBM, i_TSXI, i_TSXS, i_TSBM, i_TSSTO, i_cNH, i_cNO } = this.stoi_params;
+
+ const stoi_matrix = Array(12);
+ // S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS
+ stoi_matrix[0] = [0., f_SI, 1.-f_SI, i_NXS-(1.-f_SI)*i_NSS-f_SI*i_NSI, 0., 0., (i_NXS-(1.-f_SI)*i_NSS-f_SI*i_NSI)*i_cNH, 0., -1., 0., 0., 0., -i_TSXS];
+ stoi_matrix[1] = [-(1.-Y_STO_O), 0, -1., i_NSS, 0., 0., i_NSS*i_cNH, 0., 0., 0., Y_STO_O, 0., Y_STO_O*i_TSSTO];
+ stoi_matrix[2] = [0., 0., -1., i_NSS, -(1.-Y_STO_NO)/(i_CODNO-i_CODN), (1.-Y_STO_NO)/(i_CODNO-i_CODN), i_NSS*i_cNH + (1.-Y_STO_NO)/(i_CODNO-i_CODN)*i_cNO, 0., 0., 0., Y_STO_NO, 0., Y_STO_NO*i_TSSTO];
+ stoi_matrix[3] = [-(1.-Y_H_O)/Y_H_O, 0., 0., -i_NBM, 0., 0., -i_NBM*i_cNH, 0., 0., 1., -1./Y_H_O, 0., i_TSBM-i_TSSTO/Y_H_O];
+ stoi_matrix[4] = [0., 0., 0., -i_NBM, -(1.-Y_H_NO)/(Y_H_NO*(i_CODNO-i_CODN)), (1.-Y_H_NO)/(Y_H_NO*(i_CODNO-i_CODN)), -i_NBM*i_cNH+(1.-Y_H_NO)/(Y_H_NO*(i_CODNO-i_CODN))*i_cNO, 0., 0., 1., -1./Y_H_NO, 0., i_TSBM-i_TSSTO/Y_H_NO];
+ stoi_matrix[5] = [f_XI-1., 0., 0., i_NBM-f_XI*i_NXI, 0., 0., (i_NBM-f_XI*i_NXI)*i_cNH, f_XI, 0., -1., 0., 0., f_XI*i_TSXI-i_TSBM];
+ stoi_matrix[6] = [0., 0., 0., i_NBM-f_XI*i_NXI, -(1.-f_XI)/(i_CODNO-i_CODN), (1.-f_XI)/(i_CODNO-i_CODN), (i_NBM-f_XI*i_NXI)*i_cNH+(1-f_XI)/(i_CODNO-i_CODN)*i_cNO, f_XI, 0., -1., 0., 0., f_XI*i_TSXI-i_TSBM];
+ stoi_matrix[7] = [-1., 0., 0., 0., 0., 0., 0., 0., 0., 0., -1., 0., -i_TSSTO];
+ stoi_matrix[8] = [0., 0., 0., 0., -1./(i_CODNO-i_CODN), 1./(i_CODNO-i_CODN), i_cNO/(i_CODNO-i_CODN), 0., 0., 0., -1., 0., -i_TSSTO];
+ stoi_matrix[9] = [1.+i_CODNO/Y_A, 0., 0., -1./Y_A-i_NBM, 0., 1./Y_A, (-1./Y_A-i_NBM)*i_cNH+i_cNO/Y_A, 0., 0., 0., 0., 1., i_TSBM];
+ stoi_matrix[10] = [f_XI-1., 0., 0., i_NBM-f_XI*i_NXI, 0., 0., (i_NBM-f_XI*i_NXI)*i_cNH, f_XI, 0., 0., 0., -1., f_XI*i_TSXI-i_TSBM];
+ stoi_matrix[11] = [0., 0., 0., i_NBM-f_XI*i_NXI, -(1.-f_XI)/(i_CODNO-i_CODN), (1.-f_XI)/(i_CODNO-i_CODN), (i_NBM-f_XI*i_NXI)*i_cNH+(1-f_XI)/(i_CODNO-i_CODN)*i_cNO, 0., 0., 0., 0., -1., f_XI*i_TSXI-i_TSBM];
+
+ return stoi_matrix[0].map((col, i) => stoi_matrix.map(row => row[i])); // transpose matrix
+ }
+
+ /**
+ * Computes the Monod equation rate value for a given concentration and half-saturation constant.
+ * @param {number} c - Concentration of reaction species.
+ * @param {number} K - Half-saturation constant for the reaction species.
+ * @returns {number} - Monod equation rate value for the given concentration and half-saturation constant.
+ */
+ _monod(c, K) {
+ return c / (K + c);
+ }
+
+ /**
+ * Computes the inverse Monod equation rate value for a given concentration and half-saturation constant. Used for inhibition.
+ * @param {number} c - Concentration of reaction species.
+ * @param {number} K - Half-saturation constant for the reaction species.
+ * @returns {number} - Inverse Monod equation rate value for the given concentration and half-saturation constant.
+ */
+ _inv_monod(c, K) {
+ return K / (K + c);
+ }
+
+ /**
+ * Adjust the rate parameter for temperature T using simplied Arrhenius equation based on rate constant at 20 degrees Celsius and theta parameter.
+ * @param {number} k - Rate constant at 20 degrees Celcius.
+ * @param {number} theta - Theta parameter.
+ * @param {number} T - Temperature in Celcius.
+ * @returns {number} - Adjusted rate parameter at temperature T based on the Arrhenius equation.
+ */
+ _arrhenius(k, theta, T) {
+ return k * Math.exp(theta*(T-20));
+ }
+
+ /**
+ * Computes the temperature theta parameter based on two rate constants and their corresponding temperatures.
+ * @param {number} k1 - Rate constant at temperature T1.
+ * @param {number} k2 - Rate constant at temperature T2.
+ * @param {number} T1 - Temperature T1 in Celcius.
+ * @param {number} T2 - Temperature T2 in Celcius.
+ * @returns {number} - Theta parameter.
+ */
+ _compute_theta(k1, k2, T1, T2) {
+ return Math.log(k1/k2)/(T1-T2);
+ }
+
+ /**
+ * Computes the reaction rates for each process reaction based on the current state and temperature.
+ * @param {Array} state - State vector containing concentrations of reaction species.
+ * @param {number} [T=20] - Temperature in degrees Celsius (default is 20).
+ * @returns {Array} - Reaction rates for each process reaction.
+ */
+ compute_rates(state, T = 20) {
+ // state: S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS
+ const rates = Array(12);
+ const [S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS] = state;
+ const { k_H, K_X, k_STO, nu_NO, K_O, K_NO, K_S, K_STO, mu_H_max, K_NH, K_HCO, b_H_O, b_H_NO, b_STO_O, b_STO_NO, mu_A_max, K_A_NH, K_A_O, K_A_HCO, b_A_O, b_A_NO } = this.kin_params;
+ const { theta_H, theta_STO, theta_mu_H, theta_b_H_O, theta_b_H_NO, theta_b_STO_O, theta_b_STO_NO, theta_mu_A, theta_b_A_O, theta_b_A_NO } = this.temp_params;
+
+ // Hydrolysis
+ rates[0] = X_H == 0 ? 0 : this._arrhenius(k_H, theta_H, T) * this._monod(X_S / X_H, K_X) * X_H;
+
+ // Heterotrophs
+ rates[1] = this._arrhenius(k_STO, theta_STO, T) * this._monod(S_O, K_O) * this._monod(S_S, K_S) * X_H;
+ rates[2] = this._arrhenius(k_STO, theta_STO, T) * nu_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * this._monod(S_S, K_S) * X_H;
+ rates[3] = X_H == 0 ? 0 : this._arrhenius(mu_H_max, theta_mu_H, T) * this._monod(S_O, K_O) * this._monod(S_NH, K_NH) * this._monod(S_HCO, K_HCO) * this._monod(X_STO/X_H, K_STO) * X_H;
+ rates[4] = X_H == 0 ? 0 : this._arrhenius(mu_H_max, theta_mu_H, T) * nu_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * this._monod(S_NH, K_NH) * this._monod(S_HCO, K_HCO) * this._monod(X_STO/X_H, K_STO) * X_H;
+ rates[5] = this._arrhenius(b_H_O, theta_b_H_O, T) * this._monod(S_O, K_O) * X_H;
+ rates[6] = this._arrhenius(b_H_NO, theta_b_H_NO, T) * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_H;
+ rates[7] = this._arrhenius(b_STO_O, theta_b_STO_O, T) * this._monod(S_O, K_O) * X_H;
+ rates[8] = this._arrhenius(b_STO_NO, theta_b_STO_NO, T) * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_STO;
+
+ // Autotrophs
+ rates[9] = this._arrhenius(mu_A_max, theta_mu_A, T) * this._monod(S_O, K_A_O) * this._monod(S_NH, K_A_NH) * this._monod(S_HCO, K_A_HCO) * X_A;
+ rates[10] = this._arrhenius(b_A_O, theta_b_A_O, T) * this._monod(S_O, K_O) * X_A;
+ rates[11] = this._arrhenius(b_A_NO, theta_b_A_NO, T) * this._inv_monod(S_O, K_A_O) * this._monod(S_NO, K_NO) * X_A;
+
+ return rates;
+ }
+
+ /**
+ * Computes the change in concentrations of reaction species based on the current state and temperature.
+ * @param {Array} state - State vector containing concentrations of reaction species.
+ * @param {number} [T=20] - Temperature in degrees Celsius (default is 20).
+ * @returns {Array} - Change in reaction species concentrations.
+ */
+ compute_dC(state, T = 20) { // compute changes in concentrations
+ // state: S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS
+ return math.multiply(this.stoi_matrix, this.compute_rates(state, T));
+ }
+}
+
+module.exports = ASM3;
\ No newline at end of file
diff --git a/src/specificClass.js b/src/specificClass.js
new file mode 100644
index 0000000..afddc2c
--- /dev/null
+++ b/src/specificClass.js
@@ -0,0 +1,413 @@
+const ASM3 = require('./reaction_modules/asm3_class.js');
+const { create, all, isArray } = require('mathjs');
+const { assertNoNaN } = require('./utils.js');
+const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions');
+const EventEmitter = require('events');
+
+const mathConfig = {
+ matrix: 'Array' // use Array as the matrix type
+};
+
+const math = create(all, mathConfig);
+
+const S_O_INDEX = 0;
+const NUM_SPECIES = 13;
+const DEBUG = false;
+
+class Reactor {
+ /**
+ * Reactor base class.
+ * @param {object} config - Configuration object containing reactor parameters.
+ */
+ constructor(config) {
+ this.config = config;
+ // EVOLV stuff
+ this.logger = new logger(this.config.general.logging.enabled, this.config.general.logging.logLevel, config.general.name);
+ this.emitter = new EventEmitter();
+ this.measurements = new MeasurementContainer();
+ this.upstreamReactor = null;
+ this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility
+
+ this.asm = new ASM3();
+
+ this.volume = config.volume; // fluid volume reactor [m3]
+
+ this.Fs = Array(config.n_inlets).fill(0); // fluid debits per inlet [m3 d-1]
+ this.Cs_in = Array.from(Array(config.n_inlets), () => new Array(NUM_SPECIES).fill(0)); // composition influents
+ this.OTR = 0.0; // oxygen transfer rate [g O2 d-1 m-3]
+ this.temperature = 20; // temperature [C]
+
+ this.kla = config.kla; // if NaN, use externaly provided OTR [d-1]
+
+ this.currentTime = Date.now(); // milliseconds since epoch [ms]
+ this.timeStep = 1 / (24*60*60) * this.config.timeStep; // time step [d]
+ this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second
+ }
+
+ /**
+ * Setter for influent data.
+ * @param {object} input - Input object (msg) containing payload with inlet index, flow rate, and concentrations.
+ */
+ set setInfluent(input) {
+ let index_in = input.payload.inlet;
+ this.Fs[index_in] = input.payload.F;
+ this.Cs_in[index_in] = input.payload.C;
+ }
+
+ /**
+ * Setter for OTR (Oxygen Transfer Rate).
+ * @param {object} input - Input object (msg) containing payload with OTR value [g O2 d-1 m-3].
+ */
+ set setOTR(input) {
+ this.OTR = input.payload;
+ }
+
+ /**
+ * Getter for effluent data.
+ * @returns {object} Effluent data object (msg), defaults to inlet 0.
+ */
+ get getEffluent() { // getter for Effluent, defaults to inlet 0
+ if (isArray(this.state.at(-1))) {
+ return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime };
+ }
+ return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime };
+ }
+
+ /**
+ * Calculate the oxygen transfer rate (OTR) based on the dissolved oxygen concentration and temperature.
+ * @param {number} S_O - Dissolved oxygen concentration [g O2 m-3].
+ * @param {number} T - Temperature in Celsius, default to 20 C.
+ * @returns {number} - Calculated OTR [g O2 d-1 m-3].
+ */
+ _calcOTR(S_O, T = 20.0) { // caculate the OTR using basic correlation, default to temperature: 20 C
+ let S_O_sat = 14.652 - 4.1022e-1 * T + 7.9910e-3 * T*T + 7.7774e-5 * T*T*T;
+ return this.kla * (S_O_sat - S_O);
+ }
+
+ /**
+ * Clip values in an array to zero.
+ * @param {Array} arr - Array of values to clip.
+ * @returns {Array} - New array with values clipped to zero.
+ */
+ _arrayClip2Zero(arr) {
+ if (Array.isArray(arr)) {
+ return arr.map(x => this._arrayClip2Zero(x));
+ } else {
+ return arr < 0 ? 0 : arr;
+ }
+ }
+
+ registerChild(child, softwareType) {
+ switch (softwareType) {
+ case "measurement":
+ this.logger.debug(`Registering measurement child.`);
+ this._connectMeasurement(child);
+ break;
+ case "reactor":
+ this.logger.debug(`Registering reactor child.`);
+ this._connectReactor(child);
+ break;
+
+ default:
+ this.logger.error(`Unrecognized softwareType: ${softwareType}`);
+ }
+ }
+
+ _connectMeasurement(measurement) {
+ if (!measurement) {
+ this.logger.warn("Invalid measurement provided.");
+ return;
+ }
+
+ const position = measurement.config.functionality.positionVsParent;
+ const measurementType = measurement.config.asset.type;
+ const key = `${measurementType}_${position}`;
+ const eventName = `${measurementType}.measured.${position}`;
+
+ // Register event listener for measurement updates
+ this.measurements.emitter.on(eventName, (eventData) => {
+ this.logger.debug(`${position} ${measurementType} from ${eventData.childName}: ${eventData.value} ${eventData.unit}`);
+
+ // Store directly in parent's measurement container
+ this.measurements
+ .type(measurementType)
+ .variant("measured")
+ .position(position)
+ .value(eventData.value, eventData.timestamp, eventData.unit);
+
+ this._updateMeasurement(measurementType, eventData.value, position, eventData);
+ });
+ }
+
+
+ _connectReactor(reactor) {
+ if (!reactor) {
+ this.logger.warn("Invalid reactor provided.");
+ return;
+ }
+
+ this.upstreamReactor = reactor;
+
+ reactor.emitter.on("stateChange", (data) => {
+ this.logger.debug(`State change of upstream reactor detected.`);
+ this.updateState(data);
+ });
+ }
+
+
+ _updateMeasurement(measurementType, value, position, context) {
+ this.logger.debug(`---------------------- updating ${measurementType} ------------------ `);
+ switch (measurementType) {
+ case "temperature":
+ if (position == "atEquipment") {
+ this.temperature = value;
+ }
+ break;
+ default:
+ this.logger.error(`Type '${measurementType}' not recognized for measured update.`);
+ return;
+ }
+ }
+
+ /**
+ * Update the reactor state based on the new time.
+ * @param {number} newTime - New time to update reactor state to, in milliseconds since epoch.
+ */
+ updateState(newTime = Date.now()) { // expect update with timestamp
+ const day2ms = 1000 * 60 * 60 * 24;
+
+ if (this.upstreamReactor) {
+ this.setInfluent = this.upstreamReactor.getEffluent;
+ }
+
+ let n_iter = Math.floor(this.speedUpFactor * (newTime-this.currentTime) / (this.timeStep*day2ms));
+ if (n_iter) {
+ let n = 0;
+ while (n < n_iter) {
+ this.tick(this.timeStep);
+ n += 1;
+ }
+ this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor;
+ this.emitter.emit("stateChange", this.currentTime);
+ }
+ }
+}
+
+class Reactor_CSTR extends Reactor {
+ /**
+ * Reactor_CSTR class for Continuous Stirred Tank Reactor.
+ * @param {object} config - Configuration object containing reactor parameters.
+ */
+ constructor(config) {
+ super(config);
+ this.state = config.initialState;
+ }
+
+ /**
+ * Tick the reactor state using the forward Euler method.
+ * @param {number} time_step - Time step for the simulation [d].
+ * @returns {Array} - New reactor state.
+ */
+ tick(time_step) { // tick reactor state using forward Euler method
+ const inflow = math.multiply(math.divide([this.Fs], this.volume), this.Cs_in)[0];
+ const outflow = math.multiply(-1 * math.sum(this.Fs) / this.volume, this.state);
+ const reaction = this.asm.compute_dC(this.state, this.temperature);
+ const transfer = Array(NUM_SPECIES).fill(0.0);
+ transfer[S_O_INDEX] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[S_O_INDEX], this.temperature); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR
+
+ const dC_total = math.multiply(math.add(inflow, outflow, reaction, transfer), time_step)
+ this.state = this._arrayClip2Zero(math.add(this.state, dC_total)); // clip value element-wise to avoid negative concentrations
+ if(DEBUG){
+ assertNoNaN(dC_total, "change in state");
+ assertNoNaN(this.state, "new state");
+ }
+ return this.state;
+ }
+}
+
+class Reactor_PFR extends Reactor {
+ /**
+ * Reactor_PFR class for Plug Flow Reactor.
+ * @param {object} config - Configuration object containing reactor parameters.
+ */
+ constructor(config) {
+ super(config);
+
+ this.length = config.length; // reactor length [m]
+ this.n_x = config.resolution_L; // number of slices
+
+ this.d_x = this.length / this.n_x;
+ this.A = this.volume / this.length; // crosssectional area [m2]
+
+ this.alpha = config.alpha;
+
+ this.state = Array.from(Array(this.n_x), () => config.initialState.slice())
+
+ this.D = 0.0; // axial dispersion [m2 d-1]
+
+ this.D_op = this._makeDoperator(true, true);
+ assertNoNaN(this.D_op, "Derivative operator");
+
+ this.D2_op = this._makeD2operator();
+ assertNoNaN(this.D2_op, "Second derivative operator");
+ }
+
+ /**
+ * Setter for axial dispersion.
+ * @param {object} input - Input object (msg) containing payload with dispersion value [m2 d-1].
+ */
+ set setDispersion(input) {
+ this.D = input.payload;
+ }
+
+ updateState(newTime) {
+ super.updateState(newTime);
+ let Pe_local = this.d_x*math.sum(this.Fs)/(this.D*this.A)
+ let Co_D = this.D*this.timeStep/(this.d_x*this.d_x);
+
+ (Pe_local >= 2) && this.logger.warn(`Local Péclet number (${Pe_local}) is too high! Increase reactor resolution.`);
+ (Co_D >= 0.5) && this.logger.warn(`Courant number (${Co_D}) is too high! Reduce time step size.`);
+
+ if(DEBUG) {
+ console.log("Inlet state max " + math.max(this.state[0]))
+ console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A));
+ console.log("Pe local " + Pe_local);
+ console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x));
+ console.log("Co D " + Co_D);
+ }
+ }
+
+ /**
+ * Tick the reactor state using explicit finite difference method.
+ * @param {number} time_step - Time step for the simulation [d].
+ * @returns {Array} - New reactor state.
+ */
+ tick(time_step) {
+ const dispersion = math.multiply(this.D / (this.d_x*this.d_x), this.D2_op, this.state);
+ const advection = math.multiply(-1 * math.sum(this.Fs) / (this.A*this.d_x), this.D_op, this.state);
+ const reaction = this.state.map((state_slice) => this.asm.compute_dC(state_slice, this.temperature));
+ const transfer = Array.from(Array(this.n_x), () => new Array(NUM_SPECIES).fill(0));
+
+ if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR
+ for (let i = 1; i < this.n_x - 1; i++) {
+ transfer[i][S_O_INDEX] = this.OTR * this.n_x/(this.n_x-2);
+ }
+ } else {
+ for (let i = 1; i < this.n_x - 1; i++) {
+ transfer[i][S_O_INDEX] = this._calcOTR(this.state[i][S_O_INDEX], this.temperature) * this.n_x/(this.n_x-2);
+ }
+ }
+
+ const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step);
+
+ const stateNew = math.add(this.state, dC_total);
+ this._applyBoundaryConditions(stateNew);
+
+ if (DEBUG) {
+ assertNoNaN(dispersion, "dispersion");
+ assertNoNaN(advection, "advection");
+ assertNoNaN(reaction, "reaction");
+ assertNoNaN(dC_total, "change in state");
+ assertNoNaN(stateNew, "new state post BC");
+ }
+
+ this.state = this._arrayClip2Zero(stateNew);
+ return stateNew;
+ }
+
+ _updateMeasurement(measurementType, value, position, context) {
+ switch(measurementType) {
+ case "oxygen":
+ grid_pos = Math.round(position * this.n_x);
+ this.state[grid_pos][S_O_INDEX] = value; // naive approach for reconciling measurements and simulation
+ break;
+ }
+ super._updateMeasurement(measurementType, value, position, context);
+ }
+
+ /**
+ * Apply boundary conditions to the reactor state.
+ * for inlet, apply generalised Danckwerts BC, if there is not flow, apply Neumann BC with no flux
+ * for outlet, apply regular Danckwerts BC (Neumann BC with no flux)
+ * @param {Array} state - Current reactor state without enforced BCs.
+ */
+ _applyBoundaryConditions(state) {
+ if (math.sum(this.Fs) > 0) { // Danckwerts BC
+ const BC_C_in = math.multiply(1 / math.sum(this.Fs), [this.Fs], this.Cs_in)[0];
+ const BC_dispersion_term = (1-this.alpha)*this.D*this.A/(math.sum(this.Fs)*this.d_x);
+ state[0] = math.multiply(1/(1+BC_dispersion_term), math.add(BC_C_in, math.multiply(BC_dispersion_term, state[1])));
+ } else {
+ state[0] = state[1];
+ }
+ // Neumann BC (no flux)
+ state[this.n_x-1] = state[this.n_x-2];
+ }
+
+ /**
+ * Create finite difference first derivative operator.
+ * @param {boolean} central - Use central difference scheme if true, otherwise use upwind scheme.
+ * @param {boolean} higher_order - Use higher order scheme if true, otherwise use first order scheme.
+ * @returns {Array} - First derivative operator matrix.
+ */
+ _makeDoperator(central = false, higher_order = false) { // create gradient operator
+ if (higher_order) {
+ if (central) {
+ const I = math.resize(math.diag(Array(this.n_x).fill(1/12), -2), [this.n_x, this.n_x]);
+ const A = math.resize(math.diag(Array(this.n_x).fill(-2/3), -1), [this.n_x, this.n_x]);
+ const B = math.resize(math.diag(Array(this.n_x).fill(2/3), 1), [this.n_x, this.n_x]);
+ const C = math.resize(math.diag(Array(this.n_x).fill(-1/12), 2), [this.n_x, this.n_x]);
+ const D = math.add(I, A, B, C);
+ const NearBoundary = Array(this.n_x).fill(0.0);
+ NearBoundary[0] = -1/4;
+ NearBoundary[1] = -5/6;
+ NearBoundary[2] = 3/2;
+ NearBoundary[3] = -1/2;
+ NearBoundary[4] = 1/12;
+ D[1] = NearBoundary;
+ NearBoundary.reverse();
+ D[this.n_x-2] = math.multiply(-1, NearBoundary);
+ D[0] = Array(this.n_x).fill(0); // set by BCs elsewhere
+ D[this.n_x-1] = Array(this.n_x).fill(0);
+ return D;
+ } else {
+ throw new Error("Upwind higher order method not implemented! Use central scheme instead.");
+ }
+ } else {
+ const I = math.resize(math.diag(Array(this.n_x).fill(1 / (1+central)), central), [this.n_x, this.n_x]);
+ const A = math.resize(math.diag(Array(this.n_x).fill(-1 / (1+central)), -1), [this.n_x, this.n_x]);
+ const D = math.add(I, A);
+ D[0] = Array(this.n_x).fill(0); // set by BCs elsewhere
+ D[this.n_x-1] = Array(this.n_x).fill(0);
+ return D;
+ }
+ }
+
+ /**
+ * Create central finite difference second derivative operator.
+ * @returns {Array} - Second derivative operator matrix.
+ */
+ _makeD2operator() { // create the central second derivative operator
+ const I = math.diag(Array(this.n_x).fill(-2), 0);
+ const A = math.resize(math.diag(Array(this.n_x).fill(1), 1), [this.n_x, this.n_x]);
+ const B = math.resize(math.diag(Array(this.n_x).fill(1), -1), [this.n_x, this.n_x]);
+ const D2 = math.add(I, A, B);
+ D2[0] = Array(this.n_x).fill(0); // set by BCs elsewhere
+ D2[this.n_x - 1] = Array(this.n_x).fill(0);
+ return D2;
+ }
+}
+
+module.exports = { Reactor_CSTR, Reactor_PFR };
+
+// DEBUG
+// state: S_O, S_I, S_S, S_NH, S_N2, S_NO, S_HCO, X_I, X_S, X_H, X_STO, X_A, X_TS
+// let initial_state = [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1];
+// const Reactor = new Reactor_PFR(200, 10, 10, 1, 100, initial_state);
+// Reactor.Cs_in[0] = [0.0, 30., 100., 16., 0., 0., 5., 25., 75., 30., 0., 0., 125.];
+// Reactor.Fs[0] = 10;
+// Reactor.D = 0.01;
+// let N = 0;
+// while (N < 5000) {
+// console.log(Reactor.tick(0.001));
+// N += 1;
+// }
\ No newline at end of file
diff --git a/src/utils.js b/src/utils.js
new file mode 100644
index 0000000..2ca1896
--- /dev/null
+++ b/src/utils.js
@@ -0,0 +1,18 @@
+/**
+ * Assert that no NaN values are present in an array.
+ * @param {Array} arr
+ * @param {string} label
+ */
+function assertNoNaN(arr, label = "array") {
+ if (Array.isArray(arr)) {
+ for (const el of arr) {
+ assertNoNaN(el, label);
+ }
+ } else {
+ if (Number.isNaN(arr)) {
+ throw new Error(`NaN detected in ${label}!`);
+ }
+ }
+}
+
+module.exports = { assertNoNaN };
\ No newline at end of file