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 = [];