From 2e3ba8a9bf6b1421165264760c1f886828d13c87 Mon Sep 17 00:00:00 2001 From: root Date: Tue, 31 Mar 2026 14:26:10 +0200 Subject: [PATCH] Expand reactor demo telemetry and stability handling --- .gitignore | 272 +- LICENSE | 380 +- README.md | 34 +- additional_nodes/recirculation-pump.html | 114 +- additional_nodes/recirculation-pump.js | 80 +- additional_nodes/settling-basin.html | 114 +- additional_nodes/settling-basin.js | 114 +- flows/asm3_flows.json | 3524 ++++++++--------- package-lock.json | 238 +- package.json | 60 +- reactor.html | 534 +-- reactor.js | 52 +- src/nodeClass.js | 362 +- src/reaction_modules/asm3_class Koch.js | 420 +- src/reaction_modules/asm3_class.js | 420 +- src/specificClass.js | 913 ++--- src/utils.js | 34 +- test/basic/grid-profile.basic.test.js | 90 +- test/basic/speedup-factor.basic.test.js | 136 +- test/integration/otr-kla.integration.test.js | 10 +- .../integration/tick-loop.integration.test.js | 45 + 21 files changed, 4030 insertions(+), 3916 deletions(-) diff --git a/.gitignore b/.gitignore index 1170717..8f5a1e3 100644 --- a/.gitignore +++ b/.gitignore @@ -1,136 +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.* +# 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/LICENSE b/LICENSE index 6d8cea4..0fe7849 100644 --- a/LICENSE +++ b/LICENSE @@ -1,190 +1,190 @@ -EUROPEAN UNION PUBLIC LICENCE v. 1.2 -EUPL © the European Union 2007, 2016 - -This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the -terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such -use is covered by a right of the copyright holder of the Work). -The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following -notice immediately following the copyright notice for the Work: - Licensed under the EUPL -or has expressed by any other means his willingness to license under the EUPL. - -1.Definitions -In this Licence, the following terms have the following meaning: -— ‘The Licence’:this Licence. -— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available -as Source Code and also as Executable Code as the case may be. -— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or -modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work -required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in -the country mentioned in Article 15. -— ‘The Work’:the Original Work or its Derivative Works. -— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and -modify. -— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by -a computer as a program. -— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence. -— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to -the creation of a Derivative Work. -— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the -Licence. -— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating, -transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential -functionalities at the disposal of any other natural or legal person. - -2.Scope of the rights granted by the Licence -The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for -the duration of copyright vested in the Original Work: -— use the Work in any circumstance and for all usage, -— reproduce the Work, -— modify the Work, and make Derivative Works based upon the Work, -— communicate to the public, including the right to make available or display the Work or copies thereof to the public -and perform publicly, as the case may be, the Work, -— distribute the Work or copies thereof, -— lend and rent the Work or copies thereof, -— sublicense rights in the Work or copies thereof. -Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the -applicable law permits so. -In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed -by law in order to make effective the licence of the economic rights here above listed. -The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the -extent necessary to make use of the rights granted on the Work under this Licence. - -3.Communication of the Source Code -The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as -Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with -each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to -the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to -distribute or communicate the Work. - -4.Limitations on copyright -Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the -exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations -thereto. - -5.Obligations of the Licensee -The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those -obligations are the following: - -Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to -the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the -Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work -to carry prominent notices stating that the Work has been modified and the date of modification. - -Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this -Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless -the Original Work is expressly distributed only under this version of the Licence — for example by communicating -‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the -Work or Derivative Work that alter or restrict the terms of the Licence. - -Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both -the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done -under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed -in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with -his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. - -Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide -a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available -for as long as the Licensee continues to distribute or communicate the Work. -Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names -of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and -reproducing the content of the copyright notice. - -6.Chain of Authorship -The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or -licensed to him/her and that he/she has the power and authority to grant the Licence. -Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or -licensed to him/her and that he/she has the power and authority to grant the Licence. -Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions -to the Work, under the terms of this Licence. - -7.Disclaimer of Warranty -The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work -and may therefore contain defects or ‘bugs’ inherent to this type of development. -For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind -concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or -errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this -Licence. -This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. - -8.Disclaimer of Liability -Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be -liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the -Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss -of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, -the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. - -9.Additional agreements -While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services -consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole -responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, -defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by -the fact You have accepted any warranty or additional liability. - -10.Acceptance of the Licence -The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window -displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of -applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms -and conditions. -Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You -by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution -or Communication by You of the Work or copies thereof. - -11.Information to the public -In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, -by offering to download the Work from a remote location) the distribution channel or media (for example, a website) -must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence -and the way it may be accessible, concluded, stored and reproduced by the Licensee. - -12.Termination of the Licence -The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms -of the Licence. -Such a termination will not terminate the licences of any person who has received the Work from the Licensee under -the Licence, provided such persons remain in full compliance with the Licence. - -13.Miscellaneous -Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the -Work. -If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or -enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid -and enforceable. -The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of -the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. -New versions of the Licence will be published with a unique version number. -All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take -advantage of the linguistic version of their choice. - -14.Jurisdiction -Without prejudice to specific agreement between parties, -— any litigation resulting from the interpretation of this License, arising between the European Union institutions, -bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice -of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, -— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to -the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. - -15.Applicable Law -Without prejudice to specific agreement between parties, -— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, -resides or has his registered office, -— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside -a European Union Member State. - - - Appendix - -‘Compatible Licences’ according to Article 5 EUPL are: -— GNU General Public License (GPL) v. 2, v. 3 -— GNU Affero General Public License (AGPL) v. 3 -— Open Software License (OSL) v. 2.1, v. 3.0 -— Eclipse Public License (EPL) v. 1.0 -— CeCILL v. 2.0, v. 2.1 -— Mozilla Public Licence (MPL) v. 2 -— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 -— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software -— European Union Public Licence (EUPL) v. 1.1, v. 1.2 -— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+). - -The European Commission may update this Appendix to later versions of the above licences without producing -a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the -covered Source Code from exclusive appropriation. -All other changes or additions to this Appendix require the production of a new EUPL version. +EUROPEAN UNION PUBLIC LICENCE v. 1.2 +EUPL © the European Union 2007, 2016 + +This European Union Public Licence (the ‘EUPL’) applies to the Work (as defined below) which is provided under the +terms of this Licence. Any use of the Work, other than as authorised under this Licence is prohibited (to the extent such +use is covered by a right of the copyright holder of the Work). +The Work is provided under the terms of this Licence when the Licensor (as defined below) has placed the following +notice immediately following the copyright notice for the Work: + Licensed under the EUPL +or has expressed by any other means his willingness to license under the EUPL. + +1.Definitions +In this Licence, the following terms have the following meaning: +— ‘The Licence’:this Licence. +— ‘The Original Work’:the work or software distributed or communicated by the Licensor under this Licence, available +as Source Code and also as Executable Code as the case may be. +— ‘Derivative Works’:the works or software that could be created by the Licensee, based upon the Original Work or +modifications thereof. This Licence does not define the extent of modification or dependence on the Original Work +required in order to classify a work as a Derivative Work; this extent is determined by copyright law applicable in +the country mentioned in Article 15. +— ‘The Work’:the Original Work or its Derivative Works. +— ‘The Source Code’:the human-readable form of the Work which is the most convenient for people to study and +modify. +— ‘The Executable Code’:any code which has generally been compiled and which is meant to be interpreted by +a computer as a program. +— ‘The Licensor’:the natural or legal person that distributes or communicates the Work under the Licence. +— ‘Contributor(s)’:any natural or legal person who modifies the Work under the Licence, or otherwise contributes to +the creation of a Derivative Work. +— ‘The Licensee’ or ‘You’:any natural or legal person who makes any usage of the Work under the terms of the +Licence. +— ‘Distribution’ or ‘Communication’:any act of selling, giving, lending, renting, distributing, communicating, +transmitting, or otherwise making available, online or offline, copies of the Work or providing access to its essential +functionalities at the disposal of any other natural or legal person. + +2.Scope of the rights granted by the Licence +The Licensor hereby grants You a worldwide, royalty-free, non-exclusive, sublicensable licence to do the following, for +the duration of copyright vested in the Original Work: +— use the Work in any circumstance and for all usage, +— reproduce the Work, +— modify the Work, and make Derivative Works based upon the Work, +— communicate to the public, including the right to make available or display the Work or copies thereof to the public +and perform publicly, as the case may be, the Work, +— distribute the Work or copies thereof, +— lend and rent the Work or copies thereof, +— sublicense rights in the Work or copies thereof. +Those rights can be exercised on any media, supports and formats, whether now known or later invented, as far as the +applicable law permits so. +In the countries where moral rights apply, the Licensor waives his right to exercise his moral right to the extent allowed +by law in order to make effective the licence of the economic rights here above listed. +The Licensor grants to the Licensee royalty-free, non-exclusive usage rights to any patents held by the Licensor, to the +extent necessary to make use of the rights granted on the Work under this Licence. + +3.Communication of the Source Code +The Licensor may provide the Work either in its Source Code form, or as Executable Code. If the Work is provided as +Executable Code, the Licensor provides in addition a machine-readable copy of the Source Code of the Work along with +each copy of the Work that the Licensor distributes or indicates, in a notice following the copyright notice attached to +the Work, a repository where the Source Code is easily and freely accessible for as long as the Licensor continues to +distribute or communicate the Work. + +4.Limitations on copyright +Nothing in this Licence is intended to deprive the Licensee of the benefits from any exception or limitation to the +exclusive rights of the rights owners in the Work, of the exhaustion of those rights or of other applicable limitations +thereto. + +5.Obligations of the Licensee +The grant of the rights mentioned above is subject to some restrictions and obligations imposed on the Licensee. Those +obligations are the following: + +Attribution right: The Licensee shall keep intact all copyright, patent or trademarks notices and all notices that refer to +the Licence and to the disclaimer of warranties. The Licensee must include a copy of such notices and a copy of the +Licence with every copy of the Work he/she distributes or communicates. The Licensee must cause any Derivative Work +to carry prominent notices stating that the Work has been modified and the date of modification. + +Copyleft clause: If the Licensee distributes or communicates copies of the Original Works or Derivative Works, this +Distribution or Communication will be done under the terms of this Licence or of a later version of this Licence unless +the Original Work is expressly distributed only under this version of the Licence — for example by communicating +‘EUPL v. 1.2 only’. The Licensee (becoming Licensor) cannot offer or impose any additional terms or conditions on the +Work or Derivative Work that alter or restrict the terms of the Licence. + +Compatibility clause: If the Licensee Distributes or Communicates Derivative Works or copies thereof based upon both +the Work and another work licensed under a Compatible Licence, this Distribution or Communication can be done +under the terms of this Compatible Licence. For the sake of this clause, ‘Compatible Licence’ refers to the licences listed +in the appendix attached to this Licence. Should the Licensee's obligations under the Compatible Licence conflict with +his/her obligations under this Licence, the obligations of the Compatible Licence shall prevail. + +Provision of Source Code: When distributing or communicating copies of the Work, the Licensee will provide +a machine-readable copy of the Source Code or indicate a repository where this Source will be easily and freely available +for as long as the Licensee continues to distribute or communicate the Work. +Legal Protection: This Licence does not grant permission to use the trade names, trademarks, service marks, or names +of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and +reproducing the content of the copyright notice. + +6.Chain of Authorship +The original Licensor warrants that the copyright in the Original Work granted hereunder is owned by him/her or +licensed to him/her and that he/she has the power and authority to grant the Licence. +Each Contributor warrants that the copyright in the modifications he/she brings to the Work are owned by him/her or +licensed to him/her and that he/she has the power and authority to grant the Licence. +Each time You accept the Licence, the original Licensor and subsequent Contributors grant You a licence to their contributions +to the Work, under the terms of this Licence. + +7.Disclaimer of Warranty +The Work is a work in progress, which is continuously improved by numerous Contributors. It is not a finished work +and may therefore contain defects or ‘bugs’ inherent to this type of development. +For the above reason, the Work is provided under the Licence on an ‘as is’ basis and without warranties of any kind +concerning the Work, including without limitation merchantability, fitness for a particular purpose, absence of defects or +errors, accuracy, non-infringement of intellectual property rights other than copyright as stated in Article 6 of this +Licence. +This disclaimer of warranty is an essential part of the Licence and a condition for the grant of any rights to the Work. + +8.Disclaimer of Liability +Except in the cases of wilful misconduct or damages directly caused to natural persons, the Licensor will in no event be +liable for any direct or indirect, material or moral, damages of any kind, arising out of the Licence or of the use of the +Work, including without limitation, damages for loss of goodwill, work stoppage, computer failure or malfunction, loss +of data or any commercial damage, even if the Licensor has been advised of the possibility of such damage. However, +the Licensor will be liable under statutory product liability laws as far such laws apply to the Work. + +9.Additional agreements +While distributing the Work, You may choose to conclude an additional agreement, defining obligations or services +consistent with this Licence. However, if accepting obligations, You may act only on your own behalf and on your sole +responsibility, not on behalf of the original Licensor or any other Contributor, and only if You agree to indemnify, +defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against such Contributor by +the fact You have accepted any warranty or additional liability. + +10.Acceptance of the Licence +The provisions of this Licence can be accepted by clicking on an icon ‘I agree’ placed under the bottom of a window +displaying the text of this Licence or by affirming consent in any other similar way, in accordance with the rules of +applicable law. Clicking on that icon indicates your clear and irrevocable acceptance of this Licence and all of its terms +and conditions. +Similarly, you irrevocably accept this Licence and all of its terms and conditions by exercising any rights granted to You +by Article 2 of this Licence, such as the use of the Work, the creation by You of a Derivative Work or the Distribution +or Communication by You of the Work or copies thereof. + +11.Information to the public +In case of any Distribution or Communication of the Work by means of electronic communication by You (for example, +by offering to download the Work from a remote location) the distribution channel or media (for example, a website) +must at least provide to the public the information requested by the applicable law regarding the Licensor, the Licence +and the way it may be accessible, concluded, stored and reproduced by the Licensee. + +12.Termination of the Licence +The Licence and the rights granted hereunder will terminate automatically upon any breach by the Licensee of the terms +of the Licence. +Such a termination will not terminate the licences of any person who has received the Work from the Licensee under +the Licence, provided such persons remain in full compliance with the Licence. + +13.Miscellaneous +Without prejudice of Article 9 above, the Licence represents the complete agreement between the Parties as to the +Work. +If any provision of the Licence is invalid or unenforceable under applicable law, this will not affect the validity or +enforceability of the Licence as a whole. Such provision will be construed or reformed so as necessary to make it valid +and enforceable. +The European Commission may publish other linguistic versions or new versions of this Licence or updated versions of +the Appendix, so far this is required and reasonable, without reducing the scope of the rights granted by the Licence. +New versions of the Licence will be published with a unique version number. +All linguistic versions of this Licence, approved by the European Commission, have identical value. Parties can take +advantage of the linguistic version of their choice. + +14.Jurisdiction +Without prejudice to specific agreement between parties, +— any litigation resulting from the interpretation of this License, arising between the European Union institutions, +bodies, offices or agencies, as a Licensor, and any Licensee, will be subject to the jurisdiction of the Court of Justice +of the European Union, as laid down in article 272 of the Treaty on the Functioning of the European Union, +— any litigation arising between other parties and resulting from the interpretation of this License, will be subject to +the exclusive jurisdiction of the competent court where the Licensor resides or conducts its primary business. + +15.Applicable Law +Without prejudice to specific agreement between parties, +— this Licence shall be governed by the law of the European Union Member State where the Licensor has his seat, +resides or has his registered office, +— this licence shall be governed by Belgian law if the Licensor has no seat, residence or registered office inside +a European Union Member State. + + + Appendix + +‘Compatible Licences’ according to Article 5 EUPL are: +— GNU General Public License (GPL) v. 2, v. 3 +— GNU Affero General Public License (AGPL) v. 3 +— Open Software License (OSL) v. 2.1, v. 3.0 +— Eclipse Public License (EPL) v. 1.0 +— CeCILL v. 2.0, v. 2.1 +— Mozilla Public Licence (MPL) v. 2 +— GNU Lesser General Public Licence (LGPL) v. 2.1, v. 3 +— Creative Commons Attribution-ShareAlike v. 3.0 Unported (CC BY-SA 3.0) for works other than software +— European Union Public Licence (EUPL) v. 1.1, v. 1.2 +— Québec Free and Open-Source Licence — Reciprocity (LiLiQ-R) or Strong Reciprocity (LiLiQ-R+). + +The European Commission may update this Appendix to later versions of the above licences without producing +a new version of the EUPL, as long as they provide the rights granted in Article 2 of this Licence and protect the +covered Source Code from exclusive appropriation. +All other changes or additions to this Appendix require the production of a new EUPL version. diff --git a/README.md b/README.md index 552ebb1..3566f1b 100644 --- a/README.md +++ b/README.md @@ -1,17 +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. - +# 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. + diff --git a/additional_nodes/recirculation-pump.html b/additional_nodes/recirculation-pump.html index 39a3753..597c837 100644 --- a/additional_nodes/recirculation-pump.html +++ b/additional_nodes/recirculation-pump.html @@ -1,57 +1,57 @@ - - - - - + + + + + diff --git a/additional_nodes/recirculation-pump.js b/additional_nodes/recirculation-pump.js index 2a02932..5e6d50f 100644 --- a/additional_nodes/recirculation-pump.js +++ b/additional_nodes/recirculation-pump.js @@ -1,40 +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); -}; +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 index e8e8e8d..8aef9bf 100644 --- a/additional_nodes/settling-basin.html +++ b/additional_nodes/settling-basin.html @@ -1,57 +1,57 @@ - - - - - + + + + + diff --git a/additional_nodes/settling-basin.js b/additional_nodes/settling-basin.js index dd3d697..66bc57c 100644 --- a/additional_nodes/settling-basin.js +++ b/additional_nodes/settling-basin.js @@ -1,57 +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); -}; +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 index 7f6d216..5915015 100644 --- a/flows/asm3_flows.json +++ b/flows/asm3_flows.json @@ -1,1763 +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": [ - [], - [], - [] - ] - } +[ + { + "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 index b8efbf7..f917472 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,119 +1,119 @@ -{ - "name": "reactor", - "version": "0.0.1", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "reactor", - "version": "0.0.1", - "license": "SEE LICENSE", - "dependencies": { - "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git", - "mathjs": "^14.5.2" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "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/RnD/generalFunctions.git#efc97d6cd17399391b011298e47e8c1b1599592d", - "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.8.0", - "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.8.0.tgz", - "integrity": "sha512-DN4wmAjNzFVJ9vHqpAJ3vX0UF306u/1DgGKh7iVPuAFH19JDRd9NAaQS764MsKbSwDB6uBSkQEmgVmKdgYaCoQ==", - "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" - } - } - } -} +{ + "name": "reactor", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "reactor", + "version": "0.0.1", + "license": "SEE LICENSE", + "dependencies": { + "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/RnD/generalFunctions.git", + "mathjs": "^14.5.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", + "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", + "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/RnD/generalFunctions.git#efc97d6cd17399391b011298e47e8c1b1599592d", + "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.8.0", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.8.0.tgz", + "integrity": "sha512-DN4wmAjNzFVJ9vHqpAJ3vX0UF306u/1DgGKh7iVPuAFH19JDRd9NAaQS764MsKbSwDB6uBSkQEmgVmKdgYaCoQ==", + "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 index 60aa193..b67938f 100644 --- a/package.json +++ b/package.json @@ -1,33 +1,33 @@ -{ - "name": "reactor", - "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", +{ + "name": "reactor", + "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 --test test/basic/*.test.js test/integration/*.test.js test/edge/*.test.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/RnD/generalFunctions.git", - "mathjs": "^14.5.2" - } -} + "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/RnD/generalFunctions.git", + "mathjs": "^14.5.2" + } +} diff --git a/reactor.html b/reactor.html index b34d2ec..82734f3 100644 --- a/reactor.html +++ b/reactor.html @@ -1,267 +1,267 @@ - - - - - - - - + + + + + + + + diff --git a/reactor.js b/reactor.js index 04b185e..4b43998 100644 --- a/reactor.js +++ b/reactor.js @@ -1,26 +1,26 @@ -const nameOfNode = "reactor"; // 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}`); - } - }); -}; +const nameOfNode = "reactor"; // 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 index d03fa43..a6a7701 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -1,178 +1,218 @@ const { Reactor_CSTR, Reactor_PFR } = require('./specificClass.js'); +const { outputUtils } = require('generalFunctions'); - -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; - +const REACTOR_SPECIES = [ + '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' +]; + + +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._output = new outputUtils(); this._attachInputHandler(); this._registerChild(); this._startTickLoop(); - this._attachCloseHandler(); - } - - /** - * Handle node-red input messages - */ - _attachInputHandler() { - this.node.on('input', (msg, send, done) => { - try { - 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': { - const childId = msg.payload; - const childObj = this.RED.nodes.getNode(childId); - if (!childObj || !childObj.source) { - this.source?.logger?.warn(`registerChild skipped: missing child/source for id=${childId}`); - break; - } - this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); - break; - } - default: - this.source?.logger?.warn(`Unknown topic: ${msg.topic}`); - } - } catch (error) { - this.source?.logger?.error(`Input handler failure: ${error.message}`); - } - - if (typeof done === 'function') { - 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), - speedUpFactor: Number(uiConfig.speedUpFactor) || 1 - } - } - - /** - * 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: - this.node.warn("Unknown reactor type: " + this.config.reactor_type + ". Falling back to CSTR."); - new_reactor = new Reactor_CSTR(this.config); - } - - this.source = new_reactor; // protect from reassignment - this.node.source = this.source; - } - - _startTickLoop() { - setTimeout(() => { - this._tickInterval = setInterval(() => this._tick(), 1000); - }, 1000); - } - + this._attachCloseHandler(); + } + + /** + * Handle node-red input messages + */ + _attachInputHandler() { + this.node.on('input', (msg, send, done) => { + try { + 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': { + const childId = msg.payload; + const childObj = this.RED.nodes.getNode(childId); + if (!childObj || !childObj.source) { + this.source?.logger?.warn(`registerChild skipped: missing child/source for id=${childId}`); + break; + } + this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); + break; + } + default: + this.source?.logger?.warn(`Unknown topic: ${msg.topic}`); + } + } catch (error) { + this.source?.logger?.error(`Input handler failure: ${error.message}`); + } + + if (typeof done === 'function') { + 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), + speedUpFactor: Number(uiConfig.speedUpFactor) || 1 + } + } + + /** + * 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: + this.node.warn("Unknown reactor type: " + this.config.reactor_type + ". Falling back to CSTR."); + new_reactor = new Reactor_CSTR(this.config); + } + + this.source = new_reactor; // protect from reassignment + this.node.source = this.source; + } + + _startTickLoop() { + setTimeout(() => { + this._tickInterval = setInterval(() => this._tick(), 1000); + }, 1000); + } + _tick(){ const gridProfile = this.source.getGridProfile; if (gridProfile) { this.node.send([{ topic: "GridProfile", payload: gridProfile }, null, null]); } - this.node.send([this.source.getEffluent, null, null]); + this.node.send([this.source.getEffluent, this._buildTelemetryMessage(), null]); + } + + _buildTelemetryMessage() { + const effluent = this.source?.getEffluent; + const concentrations = effluent?.payload?.C; + if (!Array.isArray(concentrations)) { + return null; + } + + const telemetry = { + flow_total: Number(effluent.payload.F), + temperature: Number(this.source?.temperature), + }; + + for (let i = 0; i < Math.min(REACTOR_SPECIES.length, concentrations.length); i += 1) { + const value = Number(concentrations[i]); + if (Number.isFinite(value)) { + telemetry[REACTOR_SPECIES[i]] = value; + } + } + + return this._output.formatMsg(telemetry, this.config, 'influxdb'); } _attachCloseHandler() { this.node.on('close', (done) => { - clearInterval(this._tickInterval); - if (typeof done === 'function') done(); - }); - } -} - -module.exports = nodeClass; + clearInterval(this._tickInterval); + if (typeof done === 'function') done(); + }); + } +} + +module.exports = nodeClass; diff --git a/src/reaction_modules/asm3_class Koch.js b/src/reaction_modules/asm3_class Koch.js index e5e98b3..11cedb2 100644 --- a/src/reaction_modules/asm3_class Koch.js +++ b/src/reaction_modules/asm3_class Koch.js @@ -1,211 +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)); - } -} - +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 index d228619..c10c00c 100644 --- a/src/reaction_modules/asm3_class.js +++ b/src/reaction_modules/asm3_class.js @@ -1,211 +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)); - } -} - +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 index 5e16745..fb4eab6 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,459 +1,482 @@ -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 in seconds, converted to days. - this.speedUpFactor = config.speedUpFactor ?? 1; // speed up factor for simulation - } - - /** - * 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; - } - - /** - * Setter for reactor temperature [C]. - * Accepts either a direct numeric payload or { value } object payload. - * @param {object} input - Input object (msg) - */ - set setTemperature(input) { - const payload = input?.payload; - const rawValue = (payload && typeof payload === 'object' && payload.value !== undefined) - ? payload.value - : payload; - const parsedValue = Number(rawValue); - if (!Number.isFinite(parsedValue)) { - this.logger.warn(`Invalid temperature input: ${rawValue}`); - return; - } - this.temperature = parsedValue; - } - - /** - * 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 }; - } - - get getGridProfile() { return null; } - - /** - * 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]. - */ +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 in seconds, converted to days. + this.speedUpFactor = config.speedUpFactor ?? 1; // speed up factor for simulation + } + + /** + * 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; + } + + /** + * Setter for reactor temperature [C]. + * Accepts either a direct numeric payload or { value } object payload. + * @param {object} input - Input object (msg) + */ + set setTemperature(input) { + const payload = input?.payload; + const rawValue = (payload && typeof payload === 'object' && payload.value !== undefined) + ? payload.value + : payload; + const parsedValue = Number(rawValue); + if (!Number.isFinite(parsedValue)) { + this.logger.warn(`Invalid temperature input: ${rawValue}`); + return; + } + this.temperature = parsedValue; + } + + /** + * 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 }; + } + + get getGridProfile() { return null; } + + /** + * 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; - } + _calcOxygenSaturation(T = 20.0) { + return 14.652 - 4.1022e-1 * T + 7.9910e-3 * T*T + 7.7774e-5 * T*T*T; } - 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; - } - - let position; - if (measurement.config.functionality.distance !== 'undefined') { - position = measurement.config.functionality.distance; - } else { - 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 - measurement.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; + _capDissolvedOxygen(state) { + const saturation = this._calcOxygenSaturation(this.temperature); + const capRow = (row) => { + if (!Array.isArray(row)) { + return row; } - 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"); - } - - get getGridProfile() { - return { - grid: this.state.map(row => row.slice()), - n_x: this.n_x, - d_x: this.d_x, - length: this.length, - species: ['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'], - timestamp: this.currentTime + const next = row.slice(); + if (Number.isFinite(next[S_O_INDEX])) { + next[S_O_INDEX] = Math.max(0, Math.min(next[S_O_INDEX], saturation)); + } + return next; }; - } - /** - * 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); + if (Array.isArray(state) && Array.isArray(state[0])) { + return state.map(capRow); } + return capRow(state); } - - /** - * 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); + + /** + * 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; + } + + let position; + if (measurement.config.functionality.distance !== 'undefined') { + position = measurement.config.functionality.distance; + } else { + 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 + measurement.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._capDissolvedOxygen(this._arrayClip2Zero(math.add(this.state, dC_total))); // clip concentrations and enforce physical DO saturation + 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"); + } + + get getGridProfile() { + return { + grid: this.state.map(row => row.slice()), + n_x: this.n_x, + d_x: this.d_x, + length: this.length, + species: ['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'], + timestamp: this.currentTime + }; + } + + /** + * 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._capDissolvedOxygen(this._arrayClip2Zero(stateNew)); return stateNew; } - - _updateMeasurement(measurementType, value, position, context) { - switch(measurementType) { - case "quantity (oxygen)": - if (!Number.isFinite(position) || !Number.isFinite(value) || this.config.length <= 0) { - this.logger.warn(`Ignoring oxygen measurement update with invalid data (position=${position}, value=${value}).`); - break; - } - { - // Clamp sensor-derived position to valid PFR grid bounds. - const rawIndex = Math.round(position / this.config.length * this.n_x); - const grid_pos = Math.max(0, Math.min(this.n_x - 1, rawIndex)); - this.state[grid_pos][S_O_INDEX] = value; // reconcile measured oxygen concentration into nearest grid cell - } - break; - default: - 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; -// } + + _updateMeasurement(measurementType, value, position, context) { + switch(measurementType) { + case "quantity (oxygen)": + if (!Number.isFinite(position) || !Number.isFinite(value) || this.config.length <= 0) { + this.logger.warn(`Ignoring oxygen measurement update with invalid data (position=${position}, value=${value}).`); + break; + } + { + // Clamp sensor-derived position to valid PFR grid bounds. + const rawIndex = Math.round(position / this.config.length * this.n_x); + const grid_pos = Math.max(0, Math.min(this.n_x - 1, rawIndex)); + this.state[grid_pos][S_O_INDEX] = value; // reconcile measured oxygen concentration into nearest grid cell + } + break; + default: + 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; +// } diff --git a/src/utils.js b/src/utils.js index 2ca1896..6836a2c 100644 --- a/src/utils.js +++ b/src/utils.js @@ -1,18 +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}!`); - } - } -} - +/** + * 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 diff --git a/test/basic/grid-profile.basic.test.js b/test/basic/grid-profile.basic.test.js index 4efbfcd..404a9f1 100644 --- a/test/basic/grid-profile.basic.test.js +++ b/test/basic/grid-profile.basic.test.js @@ -1,45 +1,45 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); - -const { Reactor_CSTR, Reactor_PFR } = require('../../src/specificClass'); -const { makeReactorConfig } = require('../helpers/factories'); - -test('CSTR getGridProfile returns null', () => { - const reactor = new Reactor_CSTR(makeReactorConfig({ reactor_type: 'CSTR' })); - assert.equal(reactor.getGridProfile, null); -}); - -test('PFR getGridProfile returns state matrix with correct dimensions', () => { - const n_x = 8; - const length = 40; - const reactor = new Reactor_PFR( - makeReactorConfig({ reactor_type: 'PFR', resolution_L: n_x, length }), - ); - - const profile = reactor.getGridProfile; - assert.notEqual(profile, null); - assert.equal(profile.n_x, n_x); - assert.equal(profile.d_x, length / n_x); - assert.equal(profile.length, length); - assert.equal(profile.grid.length, n_x, 'grid should have n_x rows'); - assert.equal(profile.grid[0].length, 13, 'each row should have 13 species'); - assert.ok(Array.isArray(profile.species), 'species list should be an array'); - assert.equal(profile.species.length, 13); - assert.equal(profile.species[3], 'S_NH'); - assert.equal(typeof profile.timestamp, 'number'); -}); - -test('PFR getGridProfile is mutation-safe', () => { - const reactor = new Reactor_PFR( - makeReactorConfig({ reactor_type: 'PFR', resolution_L: 5, length: 10 }), - ); - - const profile = reactor.getGridProfile; - const originalValue = reactor.state[0][3]; // S_NH at cell 0 - - // Mutate the returned grid - profile.grid[0][3] = 999; - - // Reactor internal state should be unchanged - assert.equal(reactor.state[0][3], originalValue, 'mutating grid copy must not affect reactor state'); -}); +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { Reactor_CSTR, Reactor_PFR } = require('../../src/specificClass'); +const { makeReactorConfig } = require('../helpers/factories'); + +test('CSTR getGridProfile returns null', () => { + const reactor = new Reactor_CSTR(makeReactorConfig({ reactor_type: 'CSTR' })); + assert.equal(reactor.getGridProfile, null); +}); + +test('PFR getGridProfile returns state matrix with correct dimensions', () => { + const n_x = 8; + const length = 40; + const reactor = new Reactor_PFR( + makeReactorConfig({ reactor_type: 'PFR', resolution_L: n_x, length }), + ); + + const profile = reactor.getGridProfile; + assert.notEqual(profile, null); + assert.equal(profile.n_x, n_x); + assert.equal(profile.d_x, length / n_x); + assert.equal(profile.length, length); + assert.equal(profile.grid.length, n_x, 'grid should have n_x rows'); + assert.equal(profile.grid[0].length, 13, 'each row should have 13 species'); + assert.ok(Array.isArray(profile.species), 'species list should be an array'); + assert.equal(profile.species.length, 13); + assert.equal(profile.species[3], 'S_NH'); + assert.equal(typeof profile.timestamp, 'number'); +}); + +test('PFR getGridProfile is mutation-safe', () => { + const reactor = new Reactor_PFR( + makeReactorConfig({ reactor_type: 'PFR', resolution_L: 5, length: 10 }), + ); + + const profile = reactor.getGridProfile; + const originalValue = reactor.state[0][3]; // S_NH at cell 0 + + // Mutate the returned grid + profile.grid[0][3] = 999; + + // Reactor internal state should be unchanged + assert.equal(reactor.state[0][3], originalValue, 'mutating grid copy must not affect reactor state'); +}); diff --git a/test/basic/speedup-factor.basic.test.js b/test/basic/speedup-factor.basic.test.js index f7a7c93..cc35b0e 100644 --- a/test/basic/speedup-factor.basic.test.js +++ b/test/basic/speedup-factor.basic.test.js @@ -1,68 +1,68 @@ -const test = require('node:test'); -const assert = require('node:assert/strict'); - -const { Reactor_CSTR } = require('../../src/specificClass'); -const nodeClass = require('../../src/nodeClass'); -const { makeReactorConfig, makeUiConfig, makeNodeStub, makeREDStub } = require('../helpers/factories'); - -/** - * Smoke tests for Fix 3: configurable speedUpFactor on Reactor. - */ - -test('specificClass defaults speedUpFactor to 1 when not in config', () => { - const config = makeReactorConfig(); - const reactor = new Reactor_CSTR(config); - assert.equal(reactor.speedUpFactor, 1, 'speedUpFactor should default to 1'); -}); - -test('specificClass accepts speedUpFactor from config', () => { - const config = makeReactorConfig(); - config.speedUpFactor = 10; - const reactor = new Reactor_CSTR(config); - assert.equal(reactor.speedUpFactor, 10, 'speedUpFactor should be read from config'); -}); - -test('specificClass accepts speedUpFactor = 60 for accelerated simulation', () => { - const config = makeReactorConfig(); - config.speedUpFactor = 60; - const reactor = new Reactor_CSTR(config); - assert.equal(reactor.speedUpFactor, 60, 'speedUpFactor=60 should be accepted'); -}); - -test('nodeClass passes speedUpFactor from uiConfig to reactor config', () => { - const uiConfig = makeUiConfig({ speedUpFactor: 5 }); - const node = makeNodeStub(); - const RED = makeREDStub(); - - const nc = new nodeClass(uiConfig, RED, node, 'test-reactor'); - assert.equal(nc.source.speedUpFactor, 5, 'nodeClass should pass speedUpFactor=5 to specificClass'); -}); - -test('nodeClass defaults speedUpFactor to 1 when not in uiConfig', () => { - const uiConfig = makeUiConfig(); - // Ensure speedUpFactor is not set - delete uiConfig.speedUpFactor; - - const node = makeNodeStub(); - const RED = makeREDStub(); - - const nc = new nodeClass(uiConfig, RED, node, 'test-reactor'); - assert.equal(nc.source.speedUpFactor, 1, 'nodeClass should default speedUpFactor to 1'); -}); - -test('updateState with speedUpFactor=1 advances roughly real-time', () => { - const config = makeReactorConfig(); - config.speedUpFactor = 1; - config.n_inlets = 1; - const reactor = new Reactor_CSTR(config); - - // Set a known start time - const t0 = reactor.currentTime; - // Advance by 2 seconds real time - reactor.updateState(t0 + 2000); - - // With speedUpFactor=1, simulation should have advanced ~2 seconds worth - // (not 120 seconds like with the old hardcoded 60x factor) - const elapsed = reactor.currentTime - t0; - assert.ok(elapsed < 5000, `Elapsed ${elapsed}ms should be close to 2000ms, not 120000ms (old 60x factor)`); -}); +const test = require('node:test'); +const assert = require('node:assert/strict'); + +const { Reactor_CSTR } = require('../../src/specificClass'); +const nodeClass = require('../../src/nodeClass'); +const { makeReactorConfig, makeUiConfig, makeNodeStub, makeREDStub } = require('../helpers/factories'); + +/** + * Smoke tests for Fix 3: configurable speedUpFactor on Reactor. + */ + +test('specificClass defaults speedUpFactor to 1 when not in config', () => { + const config = makeReactorConfig(); + const reactor = new Reactor_CSTR(config); + assert.equal(reactor.speedUpFactor, 1, 'speedUpFactor should default to 1'); +}); + +test('specificClass accepts speedUpFactor from config', () => { + const config = makeReactorConfig(); + config.speedUpFactor = 10; + const reactor = new Reactor_CSTR(config); + assert.equal(reactor.speedUpFactor, 10, 'speedUpFactor should be read from config'); +}); + +test('specificClass accepts speedUpFactor = 60 for accelerated simulation', () => { + const config = makeReactorConfig(); + config.speedUpFactor = 60; + const reactor = new Reactor_CSTR(config); + assert.equal(reactor.speedUpFactor, 60, 'speedUpFactor=60 should be accepted'); +}); + +test('nodeClass passes speedUpFactor from uiConfig to reactor config', () => { + const uiConfig = makeUiConfig({ speedUpFactor: 5 }); + const node = makeNodeStub(); + const RED = makeREDStub(); + + const nc = new nodeClass(uiConfig, RED, node, 'test-reactor'); + assert.equal(nc.source.speedUpFactor, 5, 'nodeClass should pass speedUpFactor=5 to specificClass'); +}); + +test('nodeClass defaults speedUpFactor to 1 when not in uiConfig', () => { + const uiConfig = makeUiConfig(); + // Ensure speedUpFactor is not set + delete uiConfig.speedUpFactor; + + const node = makeNodeStub(); + const RED = makeREDStub(); + + const nc = new nodeClass(uiConfig, RED, node, 'test-reactor'); + assert.equal(nc.source.speedUpFactor, 1, 'nodeClass should default speedUpFactor to 1'); +}); + +test('updateState with speedUpFactor=1 advances roughly real-time', () => { + const config = makeReactorConfig(); + config.speedUpFactor = 1; + config.n_inlets = 1; + const reactor = new Reactor_CSTR(config); + + // Set a known start time + const t0 = reactor.currentTime; + // Advance by 2 seconds real time + reactor.updateState(t0 + 2000); + + // With speedUpFactor=1, simulation should have advanced ~2 seconds worth + // (not 120 seconds like with the old hardcoded 60x factor) + const elapsed = reactor.currentTime - t0; + assert.ok(elapsed < 5000, `Elapsed ${elapsed}ms should be close to 2000ms, not 120000ms (old 60x factor)`); +}); diff --git a/test/integration/otr-kla.integration.test.js b/test/integration/otr-kla.integration.test.js index 8855a30..6066f5e 100644 --- a/test/integration/otr-kla.integration.test.js +++ b/test/integration/otr-kla.integration.test.js @@ -35,7 +35,10 @@ test('CSTR uses kla-based oxygen transfer when kla is finite', () => { reactor.OTR = 1; reactor.state = Array(NUM_SPECIES).fill(0); - const expected = reactor._calcOTR(0, reactor.temperature); + const expected = Math.min( + reactor._calcOTR(0, reactor.temperature), + reactor._calcOxygenSaturation(reactor.temperature), + ); reactor.tick(1); assert.ok(Math.abs(reactor.state[0] - expected) < 1e-9); @@ -75,7 +78,10 @@ test('PFR uses kla-based transfer branch when kla is finite', () => { reactor.OTR = 0; reactor.state = Array.from({ length: reactor.n_x }, () => Array(NUM_SPECIES).fill(0)); - const expected = reactor._calcOTR(0, reactor.temperature) * (reactor.n_x / (reactor.n_x - 2)); + const expected = Math.min( + reactor._calcOTR(0, reactor.temperature) * (reactor.n_x / (reactor.n_x - 2)), + reactor._calcOxygenSaturation(reactor.temperature), + ); reactor.tick(1); assert.ok(Math.abs(reactor.state[1][0] - expected) < 1e-9); diff --git a/test/integration/tick-loop.integration.test.js b/test/integration/tick-loop.integration.test.js index f57f926..c7bd80d 100644 --- a/test/integration/tick-loop.integration.test.js +++ b/test/integration/tick-loop.integration.test.js @@ -9,6 +9,7 @@ test('_tick emits source effluent on process output', () => { const node = makeNodeStub(); inst.node = node; + inst._output = { formatMsg() { return null; } }; inst.source = { get getEffluent() { return { topic: 'Fluent', payload: { inlet: 0, F: 1, C: [] }, timestamp: 1 }; @@ -23,6 +24,50 @@ test('_tick emits source effluent on process output', () => { assert.equal(node._sent[0][2], null); }); +test('_tick emits reactor telemetry on influx output', () => { + const inst = Object.create(NodeClass.prototype); + const node = makeNodeStub(); + let captured = null; + + inst.node = node; + inst.config = { functionality: { softwareType: 'reactor' }, general: { id: 'reactor-node-1' } }; + inst._output = { + formatMsg(output, config, format) { + captured = { output, config, format }; + return { topic: 'reactor_reactor-node-1', payload: { measurement: 'reactor_reactor-node-1', fields: output } }; + } + }; + inst.source = { + temperature: 19.5, + get getGridProfile() { + return null; + }, + get getEffluent() { + return { + topic: 'Fluent', + payload: { + inlet: 0, + F: 42, + C: [2.1, 30, 100, 16, 0, 1, 8, 25, 75, 1500, 0, 15, 2500] + }, + timestamp: 1 + }; + }, + }; + + inst._tick(); + + assert.equal(node._sent.length, 1); + assert.equal(node._sent[0][0].topic, 'Fluent'); + assert.equal(node._sent[0][1].topic, 'reactor_reactor-node-1'); + assert.equal(captured.format, 'influxdb'); + assert.equal(captured.output.flow_total, 42); + assert.equal(captured.output.temperature, 19.5); + assert.equal(captured.output.S_O, 2.1); + assert.equal(captured.output.S_NH, 16); + assert.equal(captured.output.X_TS, 2500); +}); + test('_startTickLoop schedules periodic tick after startup delay', () => { const inst = Object.create(NodeClass.prototype); const delays = [];