From 558a9fdbab790f5fdb1ec855f14d96e37e8a16e9 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 4 Jun 2025 12:09:32 +0000 Subject: [PATCH 01/97] Initial commit --- LICENSE | 9 +++++++++ README.md | 3 +++ 2 files changed, 12 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..bd82348 --- /dev/null +++ b/LICENSE @@ -0,0 +1,9 @@ +MIT License + +Copyright (c) 2025 p.vanderwilt + +Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 0000000..2035a18 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# asm3 + +Implementation of the asm3 model (doi:10.1016/S0273-1223(98)00785-9) \ No newline at end of file From 087acf8395de615f94157c4ab71ff210b3fda0ae Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 4 Jun 2025 14:24:12 +0200 Subject: [PATCH 02/97] added initial file (unfinised) Add ASM3 class with kinetic parameters and rate computation method --- dependencies/asm3_class.js | 41 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 dependencies/asm3_class.js diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js new file mode 100644 index 0000000..6215671 --- /dev/null +++ b/dependencies/asm3_class.js @@ -0,0 +1,41 @@ +class ASM3 { + + kin_params = { + // kinetic parameters (20 C for now) + + // 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: 2., + K_NH: 0.01, + K_HCO: 0.1, + b_H_O2: 0.2, + b_H_NO: 0.1, + b_STO_O2: 0.2, + b_STO_NO: 0.1, + // Autotrophs + mu_A: 1.0, + K_A_NH: 1., + K_A_O: 0.5, + K_A_HCO: 0.5, + b_A_O2: 0.15, + b_A_NO: 0.05 + } + + + constructor() { + } + + compute_rates(state) { + const rates = new Array(12); + rates[0] = this.parameters[] + return rates; + } +} \ No newline at end of file From 6ae7b5bf53e15f052518d60e40801f79038cfea3 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 4 Jun 2025 15:19:17 +0200 Subject: [PATCH 03/97] Finished parameter objects --- dependencies/asm3_class.js | 60 ++++++++++++++++++++++++++------------ 1 file changed, 41 insertions(+), 19 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 6215671..2b85fe5 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -1,34 +1,56 @@ class ASM3 { kin_params = { - // kinetic parameters (20 C for now) + // Kinetic parameters (20 C for now) // 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: 2., - K_NH: 0.01, - K_HCO: 0.1, - b_H_O2: 0.2, - b_H_NO: 0.1, - b_STO_O2: 0.2, - b_STO_NO: 0.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_O2: 0.2, // aerobic respiration rate [d-1] + b_H_NO: 0.1, // anoxic respiration rate [d-1] + b_STO_O2: 0.2, // aerobic respitation rate X_STO [d-1] + b_STO_NO: 0.1, // anoxic respitation rate X_STO [d-1] // Autotrophs - mu_A: 1.0, - K_A_NH: 1., - K_A_O: 0.5, - K_A_HCO: 0.5, - b_A_O2: 0.15, - b_A_NO: 0.05 + 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_O2: 0.15, // aerobic respiration rate [d-1] + b_A_NO: 0.05 // anoxic respiration rate [d-1] } + stoi_params = { + // Stoichiometric and composition parameters + + f_S_I: 0., // fraction S_I from hydrolysis [g S_I g-1 X_S] + // 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 (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_TS: 0.75, // TSS content X_I [g TS g-1 X_I] + i_TS: 0.75, // TSS content X_S [g TS g-1 X_S] + i_TS: 0.90, // TSS content X_H / X_A [g TS g-1 X_H / X_A] + i_TS: 0.60 // TSS content X_STO (PHB based) [g TS g-1 X_STO] + } constructor() { } From c2122e95372bb268f2902d42bf5323e306b4346a Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 4 Jun 2025 17:28:52 +0200 Subject: [PATCH 04/97] Implemented rates in rates function --- dependencies/asm3_class.js | 42 ++++++++++++++++++++++++++++++++------ 1 file changed, 36 insertions(+), 6 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 2b85fe5..1b214db 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -5,7 +5,7 @@ class ASM3 { // 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] + 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 [-] @@ -16,16 +16,16 @@ class ASM3 { 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_O2: 0.2, // aerobic respiration rate [d-1] + b_H_O: 0.2, // aerobic respiration rate [d-1] b_H_NO: 0.1, // anoxic respiration rate [d-1] - b_STO_O2: 0.2, // aerobic respitation rate X_STO [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_O2: 0.15, // aerobic respiration rate [d-1] + b_A_O: 0.15, // aerobic respiration rate [d-1] b_A_NO: 0.05 // anoxic respiration rate [d-1] } @@ -51,13 +51,43 @@ class ASM3 { i_TS: 0.90, // TSS content X_H / X_A [g TS g-1 X_H / X_A] i_TS: 0.60 // TSS content X_STO (PHB based) [g TS g-1 X_STO] } + // 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 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] - constructor() { + constructor(state) { + this.state = state; + } + + _monod(c, K){ + return c / (K + c); + } + + _inv_monod(c, K){ + return K / (K + c); } compute_rates(state) { const rates = new Array(12); - rates[0] = this.parameters[] + 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; + + // Hydrolysis + rates[0] = this.kin_params.k_H * this._monod(X_S / X_H, this.kin_params.K_X) * X_H; + + // Heterotrophs + rates[1] = this.kin_params.k_STO * this._monod(S_O, this.kin_params.K_O) * this._monod(S_S, this.kin_params.K_S) * X_H; + rates[2] = this.kin_params.k_STO * this.kin_params.nu_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.K_NO) * this._monod(S_S, this.kin_params.K_S) * X_H; + rates[3] = this.kin_params.mu_H_max * this._monod(S_O, this.kin_params.K_O) * this._monod(S_NH, this.kin_params.K_NH) * this._monod(S_HCO, this.kin_params.K_HCO) * this._monod(X_STO/X_H, K_STO) * X_H; + rates[4] = this.kin_params.mu_H_max * this.kin_params.nu_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.NO) * this._monod(S_NH, this.kin_params.K_NH) * this._monod(S_HCO, this.kin_params.K_HCO) * this._monod(X_STO/X_H, this.kin_params.K_STO) * X_H; + rates[5] = this.kin_params.b_H_O * this._monod(S_O, this.kin_params.K_O) * X_H; + rates[6] = this.kin_params.b_H_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.K_NO) * X_H; + rates[7] = this.kin_params.b_STO_O * this._monod(S_O, this.kin_params.K_O) * X_H; + rates[8] = this.kin_params.b_STO_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.K_NO) * X_STO; + + // Autotrophs + rates[9] = this.kin_params.mu_A_max * this._monod(S_O, this.kin_params.K_A_O) * this._monod(S_NH, this.kin_params.K_A_NH) * this._monod(S_HCO, this.kin_params.K_A_HCO) * X_A; + rates[10] = this.kin_params.b_A_O * this._monod(S_O, this.kin_params.K_O) * X_A; + rates[11] = this.kin_params.b_A_NO * this._inv_monod(S_O, this.kin_params.K_A_O) * this._monod(S_NO, this.kin_params.K_NO) * X_A; + return rates; } } \ No newline at end of file From c5a9a2e6109d6bbaaa300ee945c6e6df503ddd23 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 5 Jun 2025 14:52:23 +0200 Subject: [PATCH 05/97] Implemented first five stoichiometric equations --- dependencies/asm3_class.js | 86 ++++++++++++++++++++++++++------------ 1 file changed, 59 insertions(+), 27 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 1b214db..c7aa16f 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -32,13 +32,17 @@ class ASM3 { stoi_params = { // Stoichiometric and composition parameters - f_S_I: 0., // fraction S_I from hydrolysis [g S_I g-1 X_S] + 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_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] @@ -46,16 +50,40 @@ class ASM3 { 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_TS: 0.75, // TSS content X_I [g TS g-1 X_I] - i_TS: 0.75, // TSS content X_S [g TS g-1 X_S] - i_TS: 0.90, // TSS content X_H / X_A [g TS g-1 X_H / X_A] - i_TS: 0.60 // TSS content X_STO (PHB based) [g TS g-1 X_STO] + 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] } - // 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 = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] constructor(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 this.state = state; + this.stoi_matrix = this._initialise_stoi_matrix() + } + + _initialise_stoi_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, (1.-f_SI)*i_NSS-f_SI*i_NSI+i_NXS, 0., 0., ((1.-f_SI)*i_NSS-f_SI*i_NSI+i_NXS)*i_cNH, 0., 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] = [-(1-f_XI), 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., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + stoi_matrix[7] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + stoi_matrix[8] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + stoi_matrix[9] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + stoi_matrix[10] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + stoi_matrix[11] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + + return _.zip(...stoi_matrix); // transpose matrix } _monod(c, K){ @@ -66,28 +94,32 @@ class ASM3 { return K / (K + c); } - compute_rates(state) { - const rates = new 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; + compute_rates() { + 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] = this.state; + const { k_H, 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; // Hydrolysis - rates[0] = this.kin_params.k_H * this._monod(X_S / X_H, this.kin_params.K_X) * X_H; + rates[0] = k_H * this._monod(X_S / X_H, K_X) * X_H; // Heterotrophs - rates[1] = this.kin_params.k_STO * this._monod(S_O, this.kin_params.K_O) * this._monod(S_S, this.kin_params.K_S) * X_H; - rates[2] = this.kin_params.k_STO * this.kin_params.nu_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.K_NO) * this._monod(S_S, this.kin_params.K_S) * X_H; - rates[3] = this.kin_params.mu_H_max * this._monod(S_O, this.kin_params.K_O) * this._monod(S_NH, this.kin_params.K_NH) * this._monod(S_HCO, this.kin_params.K_HCO) * this._monod(X_STO/X_H, K_STO) * X_H; - rates[4] = this.kin_params.mu_H_max * this.kin_params.nu_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.NO) * this._monod(S_NH, this.kin_params.K_NH) * this._monod(S_HCO, this.kin_params.K_HCO) * this._monod(X_STO/X_H, this.kin_params.K_STO) * X_H; - rates[5] = this.kin_params.b_H_O * this._monod(S_O, this.kin_params.K_O) * X_H; - rates[6] = this.kin_params.b_H_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.K_NO) * X_H; - rates[7] = this.kin_params.b_STO_O * this._monod(S_O, this.kin_params.K_O) * X_H; - rates[8] = this.kin_params.b_STO_NO * this._inv_monod(S_O, this.kin_params.K_O) * this._monod(S_NO, this.kin_params.K_NO) * X_STO; + rates[1] = k_STO * this._monod(S_O, K_O) * this._monod(S_S, K_S) * X_H; + rates[2] = k_STO * 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] = mu_H_max * 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] = mu_H_max * 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] = b_H_O * this._monod(S_O, K_O) * X_H; + rates[6] = b_H_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_H; + rates[7] = b_STO_O * this._monod(S_O, K_O) * X_H; + rates[8] = b_STO_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_STO; // Autotrophs - rates[9] = this.kin_params.mu_A_max * this._monod(S_O, this.kin_params.K_A_O) * this._monod(S_NH, this.kin_params.K_A_NH) * this._monod(S_HCO, this.kin_params.K_A_HCO) * X_A; - rates[10] = this.kin_params.b_A_O * this._monod(S_O, this.kin_params.K_O) * X_A; - rates[11] = this.kin_params.b_A_NO * this._inv_monod(S_O, this.kin_params.K_A_O) * this._monod(S_NO, this.kin_params.K_NO) * X_A; + rates[9] = mu_A_max * 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] = b_A_O * this._monod(S_O, K_O) * X_A; + rates[11] = b_A_NO * this._inv_monod(S_O, K_A_O) * this._monod(S_NO, K_NO) * X_A; return rates; } -} \ No newline at end of file +} + +const asm3 = new ASM3([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]) +console.log(asm3.compute_rates()) \ No newline at end of file From 6261ae9c47d558f5dd0c06f126039a19ef4b6b25 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 5 Jun 2025 16:31:28 +0200 Subject: [PATCH 06/97] Finish stoichiometric matrix calculations and include missing kinetic parameter in compute_rates --- dependencies/asm3_class.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index c7aa16f..499583c 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -70,20 +70,20 @@ class ASM3 { 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, (1.-f_SI)*i_NSS-f_SI*i_NSI+i_NXS, 0., 0., ((1.-f_SI)*i_NSS-f_SI*i_NSI+i_NXS)*i_cNH, 0., 0., -1., 0., 0., 0., -i_TSXS]; + 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] = [-(1-f_XI), 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., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - stoi_matrix[7] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - stoi_matrix[8] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - stoi_matrix[9] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - stoi_matrix[10] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; - stoi_matrix[11] = [0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0., 0.]; + 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 _.zip(...stoi_matrix); // transpose matrix + return stoi_matrix[0].map((col, i) => stoi_matrix.map(row => row[i])); // transpose matrix } _monod(c, K){ @@ -97,7 +97,7 @@ class ASM3 { compute_rates() { 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] = this.state; - const { k_H, 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 { 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; // Hydrolysis rates[0] = k_H * this._monod(X_S / X_H, K_X) * X_H; From f9bd4279aa8f04deb599c4d7cf4df64cc3d1f0e1 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 5 Jun 2025 16:57:31 +0200 Subject: [PATCH 07/97] Add project files including package.json, package-lock.json, .gitignore --- .gitignore | 136 +++++++++++++++++++++++++++++++++++++ dependencies/asm3_class.js | 1 + package-lock.json | 113 ++++++++++++++++++++++++++++++ package.json | 30 ++++++++ 4 files changed, 280 insertions(+) create mode 100644 .gitignore create mode 100644 package-lock.json create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..1170717 --- /dev/null +++ b/.gitignore @@ -0,0 +1,136 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# vitepress build output +**/.vitepress/dist + +# vitepress cache directory +**/.vitepress/cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 499583c..6aea036 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -121,5 +121,6 @@ class ASM3 { } } +// testing stuff const asm3 = new ASM3([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]) console.log(asm3.compute_rates()) \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..e2e89fc --- /dev/null +++ b/package-lock.json @@ -0,0 +1,113 @@ +{ + "name": "asm3", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "asm3", + "version": "0.0.1", + "license": "SEE LICENSE", + "dependencies": { + "mathjs": "^14.5.2" + } + }, + "node_modules/@babel/runtime": { + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.6.tgz", + "integrity": "sha512-vbavdySgbTTrmFE+EsiqUTzlOr5bzlnJtUv9PynGCAKvfQqjIXbvFdumPM/GxMDfyuGMJaJAU6TO4zc1Jf1i8Q==", + "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.5.0", + "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.5.0.tgz", + "integrity": "sha512-8vDa8Qxvr/+d94hSh5P3IJwI5t8/c0KsMp+g8bNw9cY2icONa5aPfvKeieW1WlG0WQYwwhJ7mjui2xtiePQSXw==", + "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.2.2", + "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.2.2.tgz", + "integrity": "sha512-uXBDv5knpYmv/2gLzWQ5mBHGBRk9wcKTeWu6GLTUEQfjCxO09uM/mHDrojlL+Q1mVGIIFo149Gba7od1XPgSzQ==", + "license": "MIT", + "engines": { + "node": ">= 12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/rawify" + } + }, + "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.5.2", + "resolved": "https://registry.npmjs.org/mathjs/-/mathjs-14.5.2.tgz", + "integrity": "sha512-51U6hp7j4M4Rj+l+q2KbmXAV9EhQVQzUdw1wE67RnUkKKq5ibxdrl9Ky2YkSUEIc2+VU8/IsThZNu6QSHUoyTA==", + "license": "Apache-2.0", + "dependencies": { + "@babel/runtime": "^7.26.10", + "complex.js": "^2.2.5", + "decimal.js": "^10.4.3", + "escape-latex": "^1.2.0", + "fraction.js": "^5.2.1", + "javascript-natural-sort": "^0.7.1", + "seedrandom": "^3.0.5", + "tiny-emitter": "^2.1.0", + "typed-function": "^4.2.1" + }, + "bin": { + "mathjs": "bin/cli.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/seedrandom": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz", + "integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg==", + "license": "MIT" + }, + "node_modules/tiny-emitter": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", + "integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q==", + "license": "MIT" + }, + "node_modules/typed-function": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.2.1.tgz", + "integrity": "sha512-EGjWssW7Tsk4DGfE+5yluuljS1OGYWiI1J6e8puZz9nTMM51Oug8CD5Zo4gWMsOhq5BI+1bF+rWTm4Vbj3ivRA==", + "license": "MIT", + "engines": { + "node": ">= 18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..279c4a8 --- /dev/null +++ b/package.json @@ -0,0 +1,30 @@ +{ + "name": "asm3", + "version": "0.0.1", + "description": "Implementation of the asm3 model for Node-Red", + "repository": { + "type": "git", + "url": "https://gitea.centraal.wbd-rd.nl/p.vanderwilt/asm3.git" + }, + "keywords": [ + "asm3", + "activated sludge", + "wastewater", + "biological model", + "node-red" + ], + "license": "SEE LICENSE", + "author": "P.R. van der Wilt", + "main": "asm3.js", + "scripts": { + "test": "node asm3.js" + }, + "node-red": { + "nodes": { + "asm3": "asm3.js" + } + }, + "dependencies": { + "mathjs": "^14.5.2" + } +} From 333efcda5299d9520e7d68baedfed0fda6092bba Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 5 Jun 2025 17:23:51 +0200 Subject: [PATCH 08/97] Add optional state parameter to compute_rates, improve comments, and implement compute_dC method --- dependencies/asm3_class.js | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 6aea036..1cf4ec0 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -1,3 +1,5 @@ +const math = require('mathjs') + class ASM3 { kin_params = { @@ -65,7 +67,7 @@ class ASM3 { this.stoi_matrix = this._initialise_stoi_matrix() } - _initialise_stoi_matrix(){ + _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); @@ -94,9 +96,10 @@ class ASM3 { return K / (K + c); } - compute_rates() { + compute_rates(state) { // computes reaction rates. state is optional, if not provided, use class state + 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] = this.state; + 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 || this.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; // Hydrolysis @@ -119,8 +122,13 @@ class ASM3 { return rates; } + + compute_dC(dt){ // compute change in concentration over time step dt + return math.multiply(math.multiply(this.stoi_matrix, this.compute_rates()), dt); + } } // testing stuff const asm3 = new ASM3([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]) -console.log(asm3.compute_rates()) \ No newline at end of file +console.log(asm3.compute_rates()) +console.log(asm3.compute_dC(0.001)) \ No newline at end of file From 341af6db4dd8899f9492ef16784009a0e3f50d19 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 11 Jun 2025 15:17:09 +0200 Subject: [PATCH 09/97] Refactor ASM3 constructor to remove state parameter and update compute_dC method to use state directly; add Reactor_CSTR class for reactor simulation with forward Euler method --- dependencies/asm3_class.js | 16 ++++++---------- dependencies/reactor_class.js | 21 +++++++++++++++++++++ 2 files changed, 27 insertions(+), 10 deletions(-) create mode 100644 dependencies/reactor_class.js diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 1cf4ec0..93c7364 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -61,9 +61,8 @@ class ASM3 { i_cNO: -1/14 // charge per S_NO [mole H+ g-1 NO3-N] } - constructor(state) { + constructor() { // 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 - this.state = state; this.stoi_matrix = this._initialise_stoi_matrix() } @@ -96,10 +95,10 @@ class ASM3 { return K / (K + c); } - compute_rates(state) { // computes reaction rates. state is optional, if not provided, use class state + compute_rates(state) { // computes reaction rates. state is optional 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 || this.state; + 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; // Hydrolysis @@ -123,12 +122,9 @@ class ASM3 { return rates; } - compute_dC(dt){ // compute change in concentration over time step dt - return math.multiply(math.multiply(this.stoi_matrix, this.compute_rates()), dt); + compute_dC(state){ // compute changes in concentrations + return math.multiply(this.stoi_matrix, this.compute_rates(state)); } } -// testing stuff -const asm3 = new ASM3([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]) -console.log(asm3.compute_rates()) -console.log(asm3.compute_dC(0.001)) \ No newline at end of file +module.exports = ASM3; \ No newline at end of file diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js new file mode 100644 index 0000000..04527e8 --- /dev/null +++ b/dependencies/reactor_class.js @@ -0,0 +1,21 @@ +const ASM3 = require('./asm3_class') +const math = require('mathjs') + +class Reactor_CSTR { + + constructor(initial_state){ + this.state = initial_state; + this.asm = new ASM3(); + } + + tick_fe(time_step){ // tick reactor state using forward Euler method + const delta = this.asm.compute_dC(this.state); + this.state = math.add(this.state, math.multiply(delta, time_step)); + return this.state; + } +} + +// testing stuff +let 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_CSTR(state); +console.log(Reactor.tick_fe(0.001)); \ No newline at end of file From 603a1d228322f861c4a2cb54f7d4170777ccf9f2 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 11 Jun 2025 16:24:27 +0200 Subject: [PATCH 10/97] Expand reactor class to build a simple CSTR model. Moved some functionality from asm3_class to reactor. --- dependencies/asm3_class.js | 4 ++-- dependencies/reactor_class.js | 28 +++++++++++++++++++++++----- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 93c7364..ce44f96 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -62,7 +62,6 @@ class ASM3 { } constructor() { - // 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 this.stoi_matrix = this._initialise_stoi_matrix() } @@ -96,7 +95,7 @@ class ASM3 { } compute_rates(state) { // computes reaction rates. state is optional - + // 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; @@ -123,6 +122,7 @@ class ASM3 { } compute_dC(state){ // 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)); } } diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 04527e8..5863dd0 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -6,16 +6,34 @@ class Reactor_CSTR { constructor(initial_state){ this.state = initial_state; this.asm = new ASM3(); + + this.Vl = 10.0; // fluid volume reactor [m3] + this.F = 1.0; // fluid debit [m3 d-1] + this.C_in = Array(13).fill(0.0); // composition influent + this.OTR = 1000.0; // oxygen transfer rate [g O2 d-1] } tick_fe(time_step){ // tick reactor state using forward Euler method - const delta = this.asm.compute_dC(this.state); - this.state = math.add(this.state, math.multiply(delta, time_step)); + const r = this.asm.compute_dC(this.state); + const dC_in = math.multiply(this.C_in, this.F/this.Vl); + const dC_out = math.multiply(this.state, this.F/this.Vl); + const T_O = Array(13).fill(0.0); + T_O[0] = this.OTR; + + const dC_total = math.multiply(math.add(dC_in, dC_out, r, T_O), time_step); + + this.state = math.add(this.state, dC_total); return this.state; } } // testing stuff -let 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_CSTR(state); -console.log(Reactor.tick_fe(0.001)); \ No newline at end of file +// 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_CSTR(initial_state); +Reactor.C_in = [0.0, 30., 100., 16., 0., 0., 5., 25., 75., 30., 0., 0., 125.]; +N = 0; +while (N < 15) { + console.log(Reactor.tick_fe(0.001)); + N += 1; +} \ No newline at end of file From b210a71657d93c0815b2af907ea9bba3b8a51d4c Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 11 Jun 2025 17:18:27 +0200 Subject: [PATCH 11/97] Fix structure and improve comments in ASM3 and Reactor_CSTR classes --- dependencies/asm3_class.js | 4 ++-- dependencies/reactor_class.js | 22 ++++++++++++++++++---- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index ce44f96..b5c795e 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -65,7 +65,7 @@ class ASM3 { this.stoi_matrix = this._initialise_stoi_matrix() } - _initialise_stoi_matrix(){ // initialise stoichiometric matrix + _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); @@ -121,7 +121,7 @@ class ASM3 { return rates; } - compute_dC(state){ // compute changes in concentrations + compute_dC(state) { // 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)); } diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 5863dd0..4db5522 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -3,17 +3,31 @@ const math = require('mathjs') class Reactor_CSTR { - constructor(initial_state){ + constructor(initial_state) { this.state = initial_state; this.asm = new ASM3(); this.Vl = 10.0; // fluid volume reactor [m3] this.F = 1.0; // fluid debit [m3 d-1] this.C_in = Array(13).fill(0.0); // composition influent - this.OTR = 1000.0; // oxygen transfer rate [g O2 d-1] + this.OTR = 100.0; // oxygen transfer rate [g O2 d-1] + + this.currentTime = Date.now(); // milliseconds since epoch [ms] + this.timeStep = 1/(24*60) // time step [d] } - tick_fe(time_step){ // tick reactor state using forward Euler method + // expect update with timestamp + updateState(input) { + throw new Error("Not implemented yet"); + + let newTime = input.payload; + + const day2ms = 1000 * 60 * 60 * 24; + + let n_iter = (newTime - this.currentTime) % (this.timeStep * day2ms); + } + + tick_fe(time_step) { // tick reactor state using forward Euler method const r = this.asm.compute_dC(this.state); const dC_in = math.multiply(this.C_in, this.F/this.Vl); const dC_out = math.multiply(this.state, this.F/this.Vl); @@ -33,7 +47,7 @@ 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, const Reactor = new Reactor_CSTR(initial_state); Reactor.C_in = [0.0, 30., 100., 16., 0., 0., 5., 25., 75., 30., 0., 0., 125.]; N = 0; -while (N < 15) { +while (N < 500) { console.log(Reactor.tick_fe(0.001)); N += 1; } \ No newline at end of file From 49334f59e99a376cd6e513c1f6642b1579309746 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 12 Jun 2025 11:55:17 +0200 Subject: [PATCH 12/97] Add advanced-reactor node-red implementation and update package.json references --- advanced-reactor.js | 9 +++++++++ package.json | 6 +++--- reactor.html | 26 ++++++++++++++++++++++++++ 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 advanced-reactor.js create mode 100644 reactor.html diff --git a/advanced-reactor.js b/advanced-reactor.js new file mode 100644 index 0000000..b8942b4 --- /dev/null +++ b/advanced-reactor.js @@ -0,0 +1,9 @@ +module.exports = function(RED) { + function reactor(config) { + RED.nodes.createNode(this, config); + var node = this; + + let name = config.name; + } + RED.nodes.registerType("advanced-reactor", reactor); +} diff --git a/package.json b/package.json index 279c4a8..b4a086b 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,13 @@ ], "license": "SEE LICENSE", "author": "P.R. van der Wilt", - "main": "asm3.js", + "main": "advanced-reactor.js", "scripts": { - "test": "node asm3.js" + "test": "node advanced-reactor.js" }, "node-red": { "nodes": { - "asm3": "asm3.js" + "advanced-reactor": "advanced-reactor.js" } }, "dependencies": { diff --git a/reactor.html b/reactor.html new file mode 100644 index 0000000..f9ad921 --- /dev/null +++ b/reactor.html @@ -0,0 +1,26 @@ + + + + + From 2182bed343da891632d0a5166a0ed1ca2e7e8661 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 12 Jun 2025 12:52:32 +0200 Subject: [PATCH 13/97] Fixed node not showing up in pallete. --- reactor.html => advanced-reactor.html | 4 ++-- advanced-reactor.js | 7 ++++++- 2 files changed, 8 insertions(+), 3 deletions(-) rename reactor.html => advanced-reactor.html (89%) diff --git a/reactor.html b/advanced-reactor.html similarity index 89% rename from reactor.html rename to advanced-reactor.html index f9ad921..eade6d7 100644 --- a/reactor.html +++ b/advanced-reactor.html @@ -1,6 +1,6 @@ @@ -19,6 +45,65 @@ +

Reactor properties

+
+ + +
+

Dissolved components

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+

Particulate components

+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
diff --git a/advanced-reactor.js b/advanced-reactor.js index 20d6081..bd364c8 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -10,25 +10,35 @@ module.exports = function(RED) { const reactor = new Reactor( config.volume, [ - config.S_O_init, - config.S_I_init, - config.S_S_init, - config.S_NH_init, - config.S_N2_init, - config.S_NO_init, - config.S_HCO_init, - config.X_I_init, - config.X_S_init, - config.X_H_init, - config.X_STO_init, - config.X_A_init, - config.X_TS_init + parseFloat(config.S_O_init), + parseFloat(config.S_I_init), + parseFloat(config.S_S_init), + parseFloat(config.S_NH_init), + parseFloat(config.S_N2_init), + parseFloat(config.S_NO_init), + parseFloat(config.S_HCO_init), + parseFloat(config.X_I_init), + parseFloat(config.X_S_init), + parseFloat(config.X_H_init), + parseFloat(config.X_STO_init), + parseFloat(config.X_A_init), + parseFloat(config.X_TS_init) ] ); node.on('input', function(msg, send, done) { - if (msg.topic == "clock") { - reactor.updateState(msg); + switch (msg.topic) { + case "clock": + reactor.updateState(msg); + break; + case "Influx": + reactor.setInflux = msg; + break; + case "OTR": + reactor.setOTR = msg; + break; + default: + console.log("Unknown topic: " + msg.topic) } if (done) { diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 80aa3b6..a50ca23 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -9,14 +9,23 @@ class Reactor_CSTR { this.asm = new ASM3(); this.Vl = volume; // fluid volume reactor [m3] - this.F = 1.0; // fluid debit [m3 d-1] - this.C_in = [0., 30., 100., 16., 0., 0., 5., 25., 75., 30., 0., 0., 125.]; // composition influent - this.OTR = 100.0; // oxygen transfer rate [g O2 d-1] + this.F = 0.0; // fluid debit [m3 d-1] + this.C_in = Array(13).fill(0.0); // composition influent + this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1/(24*60*15) // time step [d] } + set setInflux(input) { // setter for C_in (WIP) + this.F = input.payload.F; + this.C_in = input.payload.C_in; + } + + set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] + this.OTR = input.payload; + } + // expect update with timestamp updateState(input) { let newTime = input.payload; From 5b7f7a3cef78960a0d2462df03f8ded0491f906b Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 13 Jun 2025 15:31:31 +0200 Subject: [PATCH 17/97] Add effluent output handling --- advanced-reactor.html | 1 + advanced-reactor.js | 9 +++++++-- dependencies/reactor_class.js | 4 ++++ 3 files changed, 12 insertions(+), 2 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index d63f71b..a18e6a3 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -21,6 +21,7 @@ }, inputs: 1, outputs: 1, + outputLabels: "Effluent", icon: "font-awesome/fa-recycle", label: function() { return this.name || "advanced-reactor"; diff --git a/advanced-reactor.js b/advanced-reactor.js index bd364c8..0a0bbcd 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -31,16 +31,21 @@ module.exports = function(RED) { case "clock": reactor.updateState(msg); break; - case "Influx": + case "Influent": + reactor.setInflux = msg; + break; + case "Effluent": reactor.setInflux = msg; break; case "OTR": reactor.setOTR = msg; break; default: - console.log("Unknown topic: " + msg.topic) + console.log("Unknown topic: " + msg.topic); } + send(reactor.getEffluent); + if (done) { done(); } diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index a50ca23..29a3111 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -26,6 +26,10 @@ class Reactor_CSTR { this.OTR = input.payload; } + get getEffluent() { + return {topic: "Effluent", payload: {F: this.F, C_in:this.state}}; + } + // expect update with timestamp updateState(input) { let newTime = input.payload; From d0f8ada144b1bd09f2df450689519ef1b8fb933f Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 16 Jun 2025 14:01:19 +0200 Subject: [PATCH 18/97] Add number of inlets input handling to advanced-reactor node --- advanced-reactor.html | 13 +++++++++++++ advanced-reactor.js | 13 ++++++------- dependencies/reactor_class.js | 25 +++++++++++++------------ 3 files changed, 32 insertions(+), 19 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index a18e6a3..a1a7ea7 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -5,6 +5,7 @@ defaults: { name: { value: "" }, volume: { value: 0., required: true}, + n_inlets: { value: 1, required: true}, S_O_init: { value: 0., required: true }, S_I_init: { value: 30., required: true }, S_S_init: { value: 100., required: true }, @@ -31,6 +32,10 @@ type:"num", types:["num"] }); + $("#node-input-n_inlets").typedInput({ + type:"num", + types:["num"] + }); $(".concentrations").typedInput({ type:"num", types:["num"] @@ -41,6 +46,10 @@ if (isNaN(volume) || volume <= 0) { RED.notify("Fluid volume not set correctly", {type: "error"}); } + let n_inlets = parseInt($("#node-input-n_inlets").typedInput("value")); + if (isNaN(n_inlets) || n_inlets < 1) { + RED.notify("Number of inlets not set correctly", {type: "error"}); + } } }); @@ -55,6 +64,10 @@ +
+ + +

Dissolved components

diff --git a/advanced-reactor.js b/advanced-reactor.js index 0a0bbcd..15369ec 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -8,7 +8,8 @@ module.exports = function(RED) { const Reactor = require('./dependencies/reactor_class'); const reactor = new Reactor( - config.volume, + parseFloat(config.volume), + parseInt(config.n_inlets), [ parseFloat(config.S_O_init), parseFloat(config.S_I_init), @@ -29,13 +30,11 @@ module.exports = function(RED) { node.on('input', function(msg, send, done) { switch (msg.topic) { case "clock": - reactor.updateState(msg); + reactor.updateState(msg.timestamp); break; - case "Influent": - reactor.setInflux = msg; - break; - case "Effluent": - reactor.setInflux = msg; + case "Fluent": + reactor.setInfluent = msg; + reactor.updateState(msg.timestamp); break; case "OTR": reactor.setOTR = msg; diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 29a3111..fd459d2 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -3,36 +3,37 @@ const math = require('mathjs') class Reactor_CSTR { - constructor(volume, initial_state) { + constructor(volume, n_inlets, initial_state) { this.state = initial_state; console.log(this.state); this.asm = new ASM3(); this.Vl = volume; // fluid volume reactor [m3] - this.F = 0.0; // fluid debit [m3 d-1] - this.C_in = Array(13).fill(0.0); // composition influent + this.Fs = Array(n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] + this.Cs_in = Array.from(Array(n_inlets), () => new Array(13)).fill(0.0); // composition influent this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1/(24*60*15) // time step [d] } - set setInflux(input) { // setter for C_in (WIP) - this.F = input.payload.F; - this.C_in = input.payload.C_in; + set setInfluent(input) { // setter for C_in (WIP) + let index_in = input.payload.inlet; + this.Fs[index_in] = input.payload.F; + this.Cs_in[index_in] = input.payload.C; } set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] this.OTR = input.payload; } - get getEffluent() { - return {topic: "Effluent", payload: {F: this.F, C_in:this.state}}; + get getEffluent() { // getter for Effluent, defaults to inlet 0 + return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; } // expect update with timestamp - updateState(input) { - let newTime = input.payload; + updateState(timestamp) { + let newTime = timestamp; const day2ms = 1000 * 60 * 60 * 24; @@ -50,8 +51,8 @@ class Reactor_CSTR { tick_fe(time_step) { // tick reactor state using forward Euler method const r = this.asm.compute_dC(this.state); - const dC_in = math.multiply(this.C_in, this.F/this.Vl); - const dC_out = math.multiply(this.state, this.F/this.Vl); + const dC_in = math.multiply(this.Cs_in, this.Fs/this.Vl)[0]; + const dC_out = math.multiply(this.state, math.sum(this.Fs)/this.Vl); const T_O = Array(13).fill(0.0); T_O[0] = this.OTR; From 5281696a21da0ec279d25746dc8e7290b0ab3aaa Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 16 Jun 2025 16:53:07 +0200 Subject: [PATCH 19/97] Add recirculation pump node with input handling and flow management --- additional_nodes/recirculation-pump.html | 57 ++++++++++++++++++++++++ additional_nodes/recirculation-pump.js | 38 ++++++++++++++++ advanced-reactor.html | 4 +- advanced-reactor.js | 4 +- dependencies/reactor_class.js | 6 +-- package.json | 3 +- 6 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 additional_nodes/recirculation-pump.html create mode 100644 additional_nodes/recirculation-pump.js diff --git a/additional_nodes/recirculation-pump.html b/additional_nodes/recirculation-pump.html new file mode 100644 index 0000000..2027a17 --- /dev/null +++ b/additional_nodes/recirculation-pump.html @@ -0,0 +1,57 @@ + + + + + diff --git a/additional_nodes/recirculation-pump.js b/additional_nodes/recirculation-pump.js new file mode 100644 index 0000000..ddf0602 --- /dev/null +++ b/additional_nodes/recirculation-pump.js @@ -0,0 +1,38 @@ +module.exports = function(RED) { + function recirculation(config) { + RED.nodes.createNode(this, config); + var node = this; + + let name = config.name; + let F2 = parseFloat(config.F2); + let inlet = parseInt(config.inlet); + + node.on('input', function(msg, send, done) { + switch (msg.topic) { + case "Fluent": + // conserve volume flow debit + let F1 = msg.payload.F; + let F_diff = Math.max(F1 - F2, 0); + let F2_corr = F1 < F2 ? F1 : F2; + + let msg_F1 = structuredClone(msg); + msg_F1.payload.F = F_diff; + + let msg_F2 = structuredClone(msg); + msg_F2.payload.F = F2_corr; + msg_F2.payload.inlet = inlet; + + send([msg_F1, msg_F2]); + break; + default: + console.log("Unknown topic: " + msg.topic); + } + + if (done) { + done(); + } + }); + + } + RED.nodes.registerType("recirculation-pump", recirculation); +}; diff --git a/advanced-reactor.html b/advanced-reactor.html index a1a7ea7..6db1abe 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -1,7 +1,7 @@ + + + + diff --git a/additional_nodes/settling-basin.js b/additional_nodes/settling-basin.js new file mode 100644 index 0000000..f012b81 --- /dev/null +++ b/additional_nodes/settling-basin.js @@ -0,0 +1,53 @@ +module.exports = function(RED) { + function settler(config) { + RED.nodes.createNode(this, config); + var node = this; + + let name = config.name; + let SVI = parseFloat(config.SVI); + 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 X_in = (C_in[7] + C_in[8] + C_in[9] + C_in[10] + C_in[11] + C_in[12]); + let F2 = (F_in * X_in) / (SVI*1000*1000); + + let msg_F1 = structuredClone(msg); + msg_F1.payload.F = F_in - F2; + 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; + if (F2 != 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; + default: + console.log("Unknown topic: " + msg.topic); + } + + if (done) { + done(); + } + }); + + } + RED.nodes.registerType("settling-basin", settler); +}; diff --git a/package.json b/package.json index 59c69a7..e60fa97 100644 --- a/package.json +++ b/package.json @@ -22,7 +22,8 @@ "node-red": { "nodes": { "advanced-reactor": "advanced-reactor.js", - "recirculation-pump": "additional_nodes/recirculation-pump.js" + "recirculation-pump": "additional_nodes/recirculation-pump.js", + "settling-basin": "additional_nodes/settling-basin.js" } }, "dependencies": { From 288cf905d1acb096ca9b1a8919158b0ade930601 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 18 Jun 2025 10:25:40 +0200 Subject: [PATCH 24/97] Close volume balance and minor fixes --- additional_nodes/recirculation-pump.html | 2 +- additional_nodes/recirculation-pump.js | 8 ++++---- additional_nodes/settling-basin.html | 14 +++++++------- additional_nodes/settling-basin.js | 14 ++++++++------ 4 files changed, 20 insertions(+), 18 deletions(-) diff --git a/additional_nodes/recirculation-pump.html b/additional_nodes/recirculation-pump.html index 52f94a6..39a3753 100644 --- a/additional_nodes/recirculation-pump.html +++ b/additional_nodes/recirculation-pump.html @@ -43,7 +43,7 @@
- +
diff --git a/additional_nodes/recirculation-pump.js b/additional_nodes/recirculation-pump.js index 489df01..cd9fc21 100644 --- a/additional_nodes/recirculation-pump.js +++ b/additional_nodes/recirculation-pump.js @@ -11,12 +11,12 @@ module.exports = function(RED) { switch (msg.topic) { case "Fluent": // conserve volume flow debit - let F1 = msg.payload.F; - let F_diff = Math.max(F1 - F2, 0); - let F2_corr = F1 < F2 ? F1 : F2; + 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 = F_diff; + msg_F1.payload.F = F1; let msg_F2 = {...msg}; msg_F2.payload.F = F2_corr; diff --git a/additional_nodes/settling-basin.html b/additional_nodes/settling-basin.html index da614bc..e8e8e8d 100644 --- a/additional_nodes/settling-basin.html +++ b/additional_nodes/settling-basin.html @@ -4,7 +4,7 @@ color: "#e4a363", defaults: { name: { value: "" }, - SVI: { value: 0.1, required: true }, + TS_set: { value: 0.1, required: true }, inlet: { value: 1, required: true } }, inputs: 1, @@ -15,7 +15,7 @@ return this.name || "Settling basin"; }, oneditprepare: function() { - $("#node-input-SVI").typedInput({ + $("#node-input-TS_set").typedInput({ type:"num", types:["num"] }); @@ -25,9 +25,9 @@ }); }, oneditsave: function() { - let SVI = parseFloat($("#node-input-SVI").typedInput("value")); - if (isNaN(SVI) || SVI < 0) { - RED.notify("SVI is not set correctly", {type: "error"}); + let TS_set = parseFloat($("#node-input-TS_set").typedInput("value")); + if (isNaN(TS_set) || TS_set < 0) { + RED.notify("TS is not set correctly", {type: "error"}); } let inlet = parseInt($("#node-input-n_inlets").typedInput("value")); if (inlet < 1) { @@ -43,8 +43,8 @@
- - + +
diff --git a/additional_nodes/settling-basin.js b/additional_nodes/settling-basin.js index f012b81..e19457a 100644 --- a/additional_nodes/settling-basin.js +++ b/additional_nodes/settling-basin.js @@ -4,7 +4,7 @@ module.exports = function(RED) { var node = this; let name = config.name; - let SVI = parseFloat(config.SVI); + let TS_set = parseFloat(config.TS_set); const inlet_sludge = parseInt(config.inlet); node.on('input', function(msg, send, done) { @@ -13,11 +13,13 @@ module.exports = function(RED) { // conserve volume flow debit let F_in = msg.payload.F; let C_in = msg.payload.C; - let X_in = (C_in[7] + C_in[8] + C_in[9] + C_in[10] + C_in[11] + C_in[12]); - let F2 = (F_in * X_in) / (SVI*1000*1000); + 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 = F_in - F2; + msg_F1.payload.F = F1; msg_F1.payload.C[7] = 0; msg_F1.payload.C[8] = 0; msg_F1.payload.C[9] = 0; @@ -26,8 +28,8 @@ module.exports = function(RED) { msg_F1.payload.C[12] = 0; let msg_F2 = {...msg}; - msg_F2.payload.F = F2; - if (F2 != 0) { + 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; From 5bd094f4a695c586e37b0fbe5f2abc852341b362 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 18 Jun 2025 12:34:19 +0200 Subject: [PATCH 25/97] Prevent negative values in reactor state --- advanced-reactor.html | 2 +- dependencies/reactor_class.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index ff12949..73d66c2 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -19,7 +19,7 @@ X_H_init: { value: 30., required: true }, X_STO_init: { value: 0., required: true }, X_A_init: { value: 0.001, required: true }, - X_TS_init: { value: 125., required: true } + X_TS_init: { value: 125.0009, required: true } }, inputs: 1, outputs: 1, diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index dcc5cfd..f1695e7 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -65,7 +65,7 @@ class Reactor_CSTR { const dC_total = math.multiply(math.add(dC_in, dC_out, r, t_O), time_step); - this.state = math.add(this.state, dC_total); + this.state = math.abs(math.add(this.state, dC_total)); // make sure that concentrations do not go negative return this.state; } } From 85df04e2156f2e35625557354c2924d2e7938fc3 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 19 Jun 2025 00:16:54 +0200 Subject: [PATCH 26/97] Fix major bug in calculation of dC_out in tick_fe method to account for outflow --- dependencies/reactor_class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index f1695e7..db8b3bb 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -59,7 +59,7 @@ class Reactor_CSTR { tick_fe(time_step) { // tick reactor state using forward Euler method const r = this.asm.compute_dC(this.state); const dC_in = math.multiply(math.divide([this.Fs], this.Vl), this.Cs_in)[0]; - const dC_out = math.multiply(math.sum(this.Fs)/this.Vl, this.state); + const dC_out = math.multiply(-1*math.sum(this.Fs)/this.Vl, this.state); const t_O = Array(13).fill(0.0); t_O[0] = isNaN(this.kla) ? this.OTR : this.calcOTR(this.state[0]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR From 62b034fb76421acd37e0b89474bb28216b2f8455 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 19 Jun 2025 20:55:42 +0200 Subject: [PATCH 27/97] Added speed-up factor --- dependencies/reactor_class.js | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index db8b3bb..1b2e3d9 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -16,7 +16,8 @@ class Reactor_CSTR { this.kla = kla; // if NaN, use external OTR [d-1] this.currentTime = Date.now(); // milliseconds since epoch [ms] - this.timeStep = 1/(24*60*15) // time step [d] + this.timeStep = 1/(24*60*15); // time step [d] + this.speedUpFactor = 30; } set setInfluent(input) { // setter for C_in (WIP) @@ -39,20 +40,18 @@ class Reactor_CSTR { } // expect update with timestamp - updateState(timestamp) { - let newTime = timestamp; + updateState(newTime) { const day2ms = 1000 * 60 * 60 * 24; - let n_iter = Math.floor((newTime - this.currentTime) / (this.timeStep * day2ms)); - if (n_iter > 0) { + let n_iter = Math.floor(this.speedUpFactor*(newTime - this.currentTime) / (this.timeStep * day2ms)); + if (n_iter) { let n = 0; while (n < n_iter) { - console.log(this.tick_fe(this.timeStep)); + this.tick_fe(this.timeStep); n += 1; } - this.currentTime += n_iter * this.timeStep * day2ms; - n_iter = 0; + this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; } } From 70531a3a597b6d68514b62f79621c5bc2ebb0933 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 23 Jun 2025 16:58:02 +0200 Subject: [PATCH 28/97] Add support for multiple reactor types (CSTR and PFR) with corresponding properties (Dichelet BC for now) --- advanced-reactor.html | 43 ++++++++++++- advanced-reactor.js | 74 ++++++++++++++++------- dependencies/reactor_class.js | 110 +++++++++++++++++++++++++++++++++- 3 files changed, 204 insertions(+), 23 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index 73d66c2..5b4d41b 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -4,7 +4,10 @@ color: "#c4cce0", defaults: { name: { value: "" }, - volume: { value: 0., required: true}, + reactor_type: { value: "CSTR", required: true }, + volume: { value: 0., required: true }, + length: { value: 0.}, + resolution_L: { value: 0.}, n_inlets: { value: 1, required: true}, kla: { value: null }, S_O_init: { value: 0., required: true }, @@ -45,6 +48,32 @@ type:"num", types:["num"] }); + $("#node-input-reactor_type").typedInput({ + types: [ + { + value: "CSTR", + options: [ + { value: "CSTR", label: "CSTR"}, + { value: "PFR", label: "PFR"} + ] + } + ] + }) + $("#node-input-reactor_type").on("change", function() { + const type = $("#node-input-reactor_type").typedInput("value"); + if (type === "CSTR") { + $(".PFR").hide(); + } else { + $(".PFR").show(); + } + }); + // Set initial visibility on dialog open + const initialType = $("#node-input-reactor_type").typedInput("value"); + if (initialType === "CSTR") { + $(".PFR").hide(); + } else { + $(".PFR").show(); + } }, oneditsave: function() { let volume = parseFloat($("#node-input-volume").typedInput("value")); @@ -65,10 +94,22 @@

Reactor properties

+
+ + +
+
+ + +
+
+ + +
diff --git a/advanced-reactor.js b/advanced-reactor.js index edfad14..68e9674 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -7,26 +7,60 @@ module.exports = function(RED) { const Reactor = require('./dependencies/reactor_class'); - const reactor = new Reactor( - parseFloat(config.volume), - parseInt(config.n_inlets), - parseFloat(config.kla), - [ - parseFloat(config.S_O_init), - parseFloat(config.S_I_init), - parseFloat(config.S_S_init), - parseFloat(config.S_NH_init), - parseFloat(config.S_N2_init), - parseFloat(config.S_NO_init), - parseFloat(config.S_HCO_init), - parseFloat(config.X_I_init), - parseFloat(config.X_S_init), - parseFloat(config.X_H_init), - parseFloat(config.X_STO_init), - parseFloat(config.X_A_init), - parseFloat(config.X_TS_init) - ] - ); + let new_reactor; + + switch (config.reactor_type) { + case "CSTR": + new_reactor = new Reactor( + parseFloat(config.volume), + parseInt(config.n_inlets), + parseFloat(config.kla), + [ + parseFloat(config.S_O_init), + parseFloat(config.S_I_init), + parseFloat(config.S_S_init), + parseFloat(config.S_NH_init), + parseFloat(config.S_N2_init), + parseFloat(config.S_NO_init), + parseFloat(config.S_HCO_init), + parseFloat(config.X_I_init), + parseFloat(config.X_S_init), + parseFloat(config.X_H_init), + parseFloat(config.X_STO_init), + parseFloat(config.X_A_init), + parseFloat(config.X_TS_init) + ] + ); + break; + case "PFR": + new_reactor = new Reactor( + parseFloat(config.volume), + parseFloat(config.L), + parseInt(config.resolution_L), + parseInt(config.n_inlets), + parseFloat(config.kla), + [ + parseFloat(config.S_O_init), + parseFloat(config.S_I_init), + parseFloat(config.S_S_init), + parseFloat(config.S_NH_init), + parseFloat(config.S_N2_init), + parseFloat(config.S_NO_init), + parseFloat(config.S_HCO_init), + parseFloat(config.X_I_init), + parseFloat(config.X_S_init), + parseFloat(config.X_H_init), + parseFloat(config.X_STO_init), + parseFloat(config.X_A_init), + parseFloat(config.X_TS_init) + ] + ); + break; + default: + console.warn("Unknown reactor type: " + config.reactor_type); + } + + const reactor = new_reactor; // protect from reassignment node.on('input', function(msg, send, done) { let toggleUpdate = false; diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 1b2e3d9..aa7f9dd 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -5,7 +5,6 @@ class Reactor_CSTR { constructor(volume, n_inlets, kla, initial_state) { this.state = initial_state; - console.log(this.state); this.asm = new ASM3(); this.Vl = volume; // fluid volume reactor [m3] @@ -17,7 +16,7 @@ class Reactor_CSTR { this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1/(24*60*15); // time step [d] - this.speedUpFactor = 30; + this.speedUpFactor = 1; } set setInfluent(input) { // setter for C_in (WIP) @@ -69,6 +68,113 @@ class Reactor_CSTR { } } +class Reactor_PFR { + + constructor(volume, length, resolution_L, n_inlets, kla, initial_state) { + this.asm = new ASM3(); + + this.Vl = volume; // fluid volume reactor [m3] + this.length = length; // reactor length [m] + this.n_x = resolution_L; // number of slices + this.d_x = length / resolution_L; + + this.A = volume / length; // crosssectional area [m2] + + this.state = Array.from(Array(this.n_x), () => initial_state.slice()) + + this.Fs = Array(n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] + this.Cs_in = Array.from(Array(n_inlets), () => new Array(13).fill(0.0)); // composition influents + this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] + this.D = 0.0; // axial dispersion [m2 d-1] + + this.kla = kla; // if NaN, use external OTR [d-1] + + this.currentTime = Date.now(); // milliseconds since epoch [ms] + this.timeStep = 1/(24*60*15); // time step [d] + this.speedUpFactor = 1; + + this.D_op = makeDoperator(); + this.D2_op = makeD2operator(); + } + + set setInfluent(input) { // setter for C_in (WIP) + let index_in = input.payload.inlet; + this.Fs[index_in] = input.payload.F; + this.Cs_in[index_in] = input.payload.C; + } + + set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] + this.OTR = input.payload; + } + + set setDispersion(input) { // setter for Axial dispersion [m2 d-1] + this.D = input.payload; + } + + get getEffluent() { // getter for Effluent, defaults to inlet 0 + return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; + } + + 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); + } + + // expect update with timestamp + updateState(newTime) { + + const day2ms = 1000 * 60 * 60 * 24; + + 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_fe(this.timeStep); + n += 1; + } + this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; + } + } + + tick_fe(time_step) { // tick reactor state using forward Euler method + if (math.sum(this.Fs) > 0) { + this.state[0] = math.multiply(math.divide([this.Fs], this.A), this.Cs_in)[0] // Dichelet boundary condition + } + + const dispersion = math.multiply(this.D / (this.d_x*this.d_x), this.D2_op, this.state); + const advection = math.multiply(math.sum(this.Fs)/(this.A*this.d_x), this.D_op, this.state); + const reaction = this.state.map(this.asm.compute_dC); + const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)) + + if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR + transfer.forEach((x) => { x[0] = this.OTR; }); + } else { + transfer.forEach((x, i) => { x[0] = this.calcOTR(this.state[i][0]); }); + } + + const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); + + this.state = math.abs(math.add(this.state, dC_total)); // make sure that concentrations do not go negative + return this.state; + } + + makeDoperator() { // create the upwind scheme gradient operator + const I = math.identity(this.n_x); + const A = math.diag(Array(this.n_x).fill(-1), 1).resize([this.n_x, this.n_x]); + I[this.n_x-1, this.n_x-1] = 0; // Neumann boundary condition at x=L + return math.add(I, A); + } + + makeD2operator() { // create the upwind scheme second derivative operator + const I = math.diag(Array(this.n_x).fill(2), 0); + const A = math.diag(Array(this.n_x).fill(-1), 1).resize([this.n_x, this.n_x]); + const B = math.diag(Array(this.n_x).fill(-1), -1).resize([this.n_x, this.n_x]); + I[0, 0] = 1; + return math.add(I, A, B); + } +} + + // testing stuff // 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]; From e6c1e21c166ac90751e8e320344bf741b9b4bfc8 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 23 Jun 2025 17:46:55 +0200 Subject: [PATCH 29/97] Implement Danckwerts boundary condition in tick_fe method for Reactor_PFR --- dependencies/reactor_class.js | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index aa7f9dd..bb5a96c 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -137,14 +137,11 @@ class Reactor_PFR { } tick_fe(time_step) { // tick reactor state using forward Euler method - if (math.sum(this.Fs) > 0) { - this.state[0] = math.multiply(math.divide([this.Fs], this.A), this.Cs_in)[0] // Dichelet boundary condition - } - const dispersion = math.multiply(this.D / (this.d_x*this.d_x), this.D2_op, this.state); const advection = math.multiply(math.sum(this.Fs)/(this.A*this.d_x), this.D_op, this.state); const reaction = this.state.map(this.asm.compute_dC); - const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)) + reaction[0] = Array(13).fill(0.0); + const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR transfer.forEach((x) => { x[0] = this.OTR; }); @@ -152,6 +149,15 @@ class Reactor_PFR { transfer.forEach((x, i) => { x[0] = this.calcOTR(this.state[i][0]); }); } + if (math.sum(this.Fs) > 0) { // Danckwerts BC + const BC_influx = math.multiply(math.divide([this.Fs], this.A), this.Cs_in)[0]; + const BC_gradient = Array(this.n_x).fill(0.0); + BC_gradient[0] = 1; + BC_gradient[1] = -1; + const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], this.state); + this.state[0] = math.add(BC_influx, BC_dispersion); + } + const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); this.state = math.abs(math.add(this.state, dC_total)); // make sure that concentrations do not go negative @@ -161,6 +167,8 @@ class Reactor_PFR { makeDoperator() { // create the upwind scheme gradient operator const I = math.identity(this.n_x); const A = math.diag(Array(this.n_x).fill(-1), 1).resize([this.n_x, this.n_x]); + I[0, 0] = 0; + I[0, 1] = 0; I[this.n_x-1, this.n_x-1] = 0; // Neumann boundary condition at x=L return math.add(I, A); } @@ -169,7 +177,8 @@ class Reactor_PFR { const I = math.diag(Array(this.n_x).fill(2), 0); const A = math.diag(Array(this.n_x).fill(-1), 1).resize([this.n_x, this.n_x]); const B = math.diag(Array(this.n_x).fill(-1), -1).resize([this.n_x, this.n_x]); - I[0, 0] = 1; + I[0, 0] = 0; + I[0, 1] = 0; return math.add(I, A, B); } } From e5c9010093e2cf8df67cead7d4ded041b1e758d1 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 24 Jun 2025 11:20:28 +0200 Subject: [PATCH 30/97] Fixed various bugs --- dependencies/asm3_class.js | 118 +++++++++++++++++----------------- dependencies/reactor_class.js | 41 +++++++----- 2 files changed, 82 insertions(+), 77 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 9ad03d9..9b7762a 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -2,67 +2,65 @@ const math = require('mathjs') class ASM3 { - kin_params = { - // Kinetic parameters (20 C for now) - - // 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] - } - - stoi_params = { - // Stoichiometric and composition parameters - - 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] - } - constructor() { - this.stoi_matrix = this._initialise_stoi_matrix() + this.kin_params = { + // Kinetic parameters (20 C for now) + + // 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] + }; + this.stoi_params = { + // Stoichiometric and composition parameters + + 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] + }; + this.stoi_matrix = this._initialise_stoi_matrix(); } _initialise_stoi_matrix() { // initialise stoichiometric matrix diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index bb5a96c..af3289e 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -1,5 +1,11 @@ const ASM3 = require('./asm3_class') -const math = require('mathjs') +const { create, all } = require('mathjs') + +const config = { + matrix: 'Array' // Choose 'Matrix' (default) or 'Array' +} + +const math = create(all, config) class Reactor_CSTR { @@ -93,8 +99,8 @@ class Reactor_PFR { this.timeStep = 1/(24*60*15); // time step [d] this.speedUpFactor = 1; - this.D_op = makeDoperator(); - this.D2_op = makeD2operator(); + this.D_op = this.makeDoperator(); + this.D2_op = this.makeD2operator(); } set setInfluent(input) { // setter for C_in (WIP) @@ -139,7 +145,7 @@ class Reactor_PFR { tick_fe(time_step) { // tick reactor state using forward Euler method const dispersion = math.multiply(this.D / (this.d_x*this.d_x), this.D2_op, this.state); const advection = math.multiply(math.sum(this.Fs)/(this.A*this.d_x), this.D_op, this.state); - const reaction = this.state.map(this.asm.compute_dC); + const reaction = this.state.map((row) => this.asm.compute_dC(row)); reaction[0] = Array(13).fill(0.0); const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); @@ -154,7 +160,7 @@ class Reactor_PFR { const BC_gradient = Array(this.n_x).fill(0.0); BC_gradient[0] = 1; BC_gradient[1] = -1; - const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], this.state); + const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], this.state)[0]; this.state[0] = math.add(BC_influx, BC_dispersion); } @@ -166,19 +172,19 @@ class Reactor_PFR { makeDoperator() { // create the upwind scheme gradient operator const I = math.identity(this.n_x); - const A = math.diag(Array(this.n_x).fill(-1), 1).resize([this.n_x, this.n_x]); - I[0, 0] = 0; - I[0, 1] = 0; - I[this.n_x-1, this.n_x-1] = 0; // Neumann boundary condition at x=L + const A = math.resize(math.diag(Array(this.n_x).fill(-1), 1), [this.n_x, this.n_x]); + I[0][0] = 0; + I[0][1] = 1; + I[this.n_x-1][this.n_x-1] = 0; // Neumann boundary condition at x=L return math.add(I, A); } makeD2operator() { // create the upwind scheme second derivative operator - const I = math.diag(Array(this.n_x).fill(2), 0); - const A = math.diag(Array(this.n_x).fill(-1), 1).resize([this.n_x, this.n_x]); - const B = math.diag(Array(this.n_x).fill(-1), -1).resize([this.n_x, this.n_x]); - I[0, 0] = 0; - I[0, 1] = 0; + const I = math.identity(this.n_x); + 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]); + I[0][0] = 0; + I[0][1] = 1; return math.add(I, A, B); } } @@ -187,9 +193,10 @@ class Reactor_PFR { // testing stuff // 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_CSTR(initial_state); -// Reactor.C_in = [0.0, 30., 100., 16., 0., 0., 5., 25., 75., 30., 0., 0., 125.]; -// N = 0; +// 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; +// let N = 0; // while (N < 500) { // console.log(Reactor.tick_fe(0.001)); // N += 1; From 6b57a46aabd54511c7f3db31b9d480062bb18272 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 24 Jun 2025 12:32:11 +0200 Subject: [PATCH 31/97] Add typed input fields for reactor length and resolution in advanced-reactor, fixed NaN bug in reactor length --- advanced-reactor.html | 8 ++++++++ advanced-reactor.js | 8 ++++---- dependencies/reactor_class.js | 15 +++++++++------ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index 5b4d41b..512f3dd 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -40,6 +40,14 @@ type:"num", types:["num"] }); + $("#node-input-length").typedInput({ + type:"num", + types:["num"] + }); + $("#node-input-resolution_L").typedInput({ + type:"num", + types:["num"] + }); $("#node-input-kla").typedInput({ type:"num", types:["num"] diff --git a/advanced-reactor.js b/advanced-reactor.js index 68e9674..8348137 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -5,13 +5,13 @@ module.exports = function(RED) { let name = config.name; - const Reactor = require('./dependencies/reactor_class'); + const { Reactor_CSTR, Reactor_PFR } = require('./dependencies/reactor_class'); let new_reactor; switch (config.reactor_type) { case "CSTR": - new_reactor = new Reactor( + new_reactor = new Reactor_CSTR( parseFloat(config.volume), parseInt(config.n_inlets), parseFloat(config.kla), @@ -33,9 +33,9 @@ module.exports = function(RED) { ); break; case "PFR": - new_reactor = new Reactor( + new_reactor = new Reactor_PFR( parseFloat(config.volume), - parseFloat(config.L), + parseFloat(config.length), parseInt(config.resolution_L), parseInt(config.n_inlets), parseFloat(config.kla), diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index af3289e..4027690 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -87,16 +87,19 @@ class Reactor_PFR { this.A = volume / length; // crosssectional area [m2] this.state = Array.from(Array(this.n_x), () => initial_state.slice()) + + // console.log("Initial State: ") + // console.log(this.state) this.Fs = Array(n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] this.Cs_in = Array.from(Array(n_inlets), () => new Array(13).fill(0.0)); // composition influents this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] - this.D = 0.0; // axial dispersion [m2 d-1] + this.D = 0.1; // axial dispersion [m2 d-1] this.kla = kla; // if NaN, use external OTR [d-1] this.currentTime = Date.now(); // milliseconds since epoch [ms] - this.timeStep = 1/(24*60*15); // time step [d] + this.timeStep = 1/(24*60*60); // time step [d] this.speedUpFactor = 1; this.D_op = this.makeDoperator(); @@ -118,7 +121,7 @@ class Reactor_PFR { } get getEffluent() { // getter for Effluent, defaults to inlet 0 - return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; + return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state.at(-1)}, timestamp: this.currentTime}; } calcOTR(S_O, T=20.0) { // caculate the OTR using basic correlation, default to temperature: 20 C @@ -145,10 +148,10 @@ class Reactor_PFR { tick_fe(time_step) { // tick reactor state using forward Euler method const dispersion = math.multiply(this.D / (this.d_x*this.d_x), this.D2_op, this.state); const advection = math.multiply(math.sum(this.Fs)/(this.A*this.d_x), this.D_op, this.state); - const reaction = this.state.map((row) => this.asm.compute_dC(row)); + const reaction = this.state.map((state_slice) => this.asm.compute_dC(state_slice)); reaction[0] = Array(13).fill(0.0); const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); - + if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR transfer.forEach((x) => { x[0] = this.OTR; }); } else { @@ -202,4 +205,4 @@ class Reactor_PFR { // N += 1; // } -module.exports = Reactor_CSTR; \ No newline at end of file +module.exports = {Reactor_CSTR, Reactor_PFR}; \ No newline at end of file From f2d94b26c5141d976dac3e3cf22405fb8703cbd3 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 24 Jun 2025 13:28:45 +0200 Subject: [PATCH 32/97] Add dispersion setting in advanced-reactor and initialize axial dispersion to zero in Reactor_PFR --- advanced-reactor.js | 3 +++ dependencies/reactor_class.js | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/advanced-reactor.js b/advanced-reactor.js index 8348137..e2eca51 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -78,6 +78,9 @@ module.exports = function(RED) { case "OTR": reactor.setOTR = msg; break; + case "Dispersion": + reactor.setDispersion = msg; + break; default: console.log("Unknown topic: " + msg.topic); } diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 4027690..e030637 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -94,7 +94,7 @@ class Reactor_PFR { this.Fs = Array(n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] this.Cs_in = Array.from(Array(n_inlets), () => new Array(13).fill(0.0)); // composition influents this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] - this.D = 0.1; // axial dispersion [m2 d-1] + this.D = 0.0; // axial dispersion [m2 d-1] this.kla = kla; // if NaN, use external OTR [d-1] From 2e76f733a8fed39a17f3f699a2c6b0506017a315 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 24 Jun 2025 16:23:33 +0200 Subject: [PATCH 33/97] Working on fixing the Derivative operators and BCs --- dependencies/reactor_class.js | 40 ++++++++++++++++++++--------------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index e030637..9e02fca 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -69,7 +69,8 @@ class Reactor_CSTR { const dC_total = math.multiply(math.add(dC_in, dC_out, r, t_O), time_step); - this.state = math.abs(math.add(this.state, dC_total)); // make sure that concentrations do not go negative + // clip value element-wise to each subarray to avoid negative concentrations + this.state = math.add(this.state, dC_total).map(val => val < 0 ? 0 : val); return this.state; } } @@ -99,8 +100,8 @@ class Reactor_PFR { this.kla = kla; // if NaN, use external OTR [d-1] this.currentTime = Date.now(); // milliseconds since epoch [ms] - this.timeStep = 1/(24*60*60); // time step [d] - this.speedUpFactor = 1; + this.timeStep = 1/(24*60*15); // time step [d] + this.speedUpFactor = 60; this.D_op = this.makeDoperator(); this.D2_op = this.makeD2operator(); @@ -147,7 +148,7 @@ class Reactor_PFR { tick_fe(time_step) { // tick reactor state using forward Euler method const dispersion = math.multiply(this.D / (this.d_x*this.d_x), this.D2_op, this.state); - const advection = math.multiply(math.sum(this.Fs)/(this.A*this.d_x), this.D_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)); reaction[0] = Array(13).fill(0.0); const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); @@ -158,36 +159,40 @@ class Reactor_PFR { transfer.forEach((x, i) => { x[0] = this.calcOTR(this.state[i][0]); }); } + transfer[0][0] = 0; + if (math.sum(this.Fs) > 0) { // Danckwerts BC - const BC_influx = math.multiply(math.divide([this.Fs], this.A), this.Cs_in)[0]; + const BC_C_in = math.multiply(1/math.sum(this.Fs),[this.Fs], this.Cs_in)[0]; const BC_gradient = Array(this.n_x).fill(0.0); - BC_gradient[0] = 1; - BC_gradient[1] = -1; + BC_gradient[0] = -1; + BC_gradient[1] = 1; const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], this.state)[0]; - this.state[0] = math.add(BC_influx, BC_dispersion); + this.state[0] = math.add(BC_C_in, BC_dispersion); + console.log(BC_dispersion); } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); - this.state = math.abs(math.add(this.state, dC_total)); // make sure that concentrations do not go negative + // clip value element-wise to each subarray to avoid negative concentrations + this.state = math.add(this.state, dC_total).map(row => row.map(val => val < 0 ? 0 : val)); return this.state; } makeDoperator() { // create the upwind scheme gradient operator - const I = math.identity(this.n_x); - const A = math.resize(math.diag(Array(this.n_x).fill(-1), 1), [this.n_x, this.n_x]); + const I = math.diag(Array(this.n_x).fill(-1), 0); + const A = math.resize(math.diag(Array(this.n_x).fill(1), 1), [this.n_x, this.n_x]); I[0][0] = 0; - I[0][1] = 1; + I[0][1] = -1; I[this.n_x-1][this.n_x-1] = 0; // Neumann boundary condition at x=L return math.add(I, A); } makeD2operator() { // create the upwind scheme second derivative operator - const I = math.identity(this.n_x); - 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 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]); I[0][0] = 0; - I[0][1] = 1; + I[0][1] = -1; return math.add(I, A, B); } } @@ -199,8 +204,9 @@ class Reactor_PFR { // 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 < 500) { +// while (N < 5000) { // console.log(Reactor.tick_fe(0.001)); // N += 1; // } From 9f1322978535e10c57eb83872d50cb416ce713bf Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 24 Jun 2025 16:38:07 +0200 Subject: [PATCH 34/97] Fix boundary conditions in gradient and second derivative operators for Reactor_PFR --- dependencies/reactor_class.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 9e02fca..adf744f 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -179,20 +179,20 @@ class Reactor_PFR { } makeDoperator() { // create the upwind scheme gradient operator - const I = math.diag(Array(this.n_x).fill(-1), 0); - const A = math.resize(math.diag(Array(this.n_x).fill(1), 1), [this.n_x, this.n_x]); + const I = math.diag(Array(this.n_x).fill(1), 0); + const A = math.resize(math.diag(Array(this.n_x).fill(-1), -1), [this.n_x, this.n_x]); I[0][0] = 0; I[0][1] = -1; - I[this.n_x-1][this.n_x-1] = 0; // Neumann boundary condition at x=L return math.add(I, A); } - makeD2operator() { // create the upwind scheme second derivative operator + 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]); I[0][0] = 0; I[0][1] = -1; + I[0][0] = -1; // Dichelet boundary condition at outlet return math.add(I, A, B); } } From bb74fc86c212e8545a79e485146c1beaae0174d5 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 27 Jun 2025 16:56:37 +0200 Subject: [PATCH 35/97] Refactor dispersion and boundary condition handling in Reactor_PFR --- dependencies/reactor_class.js | 49 +++++++++++++++++++---------------- 1 file changed, 27 insertions(+), 22 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index adf744f..94844ee 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -103,7 +103,7 @@ class Reactor_PFR { this.timeStep = 1/(24*60*15); // time step [d] this.speedUpFactor = 60; - this.D_op = this.makeDoperator(); + this.D_op = this.makeDoperator(false); this.D2_op = this.makeD2operator(); } @@ -111,6 +111,8 @@ class Reactor_PFR { let index_in = input.payload.inlet; this.Fs[index_in] = input.payload.F; this.Cs_in[index_in] = input.payload.C; + // console.log("Pe " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); + // console.log("Co " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); } set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] @@ -150,7 +152,6 @@ class Reactor_PFR { 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)); - reaction[0] = Array(13).fill(0.0); const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR @@ -159,41 +160,45 @@ class Reactor_PFR { transfer.forEach((x, i) => { x[0] = this.calcOTR(this.state[i][0]); }); } - transfer[0][0] = 0; + const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); + const new_state = math.add(this.state, dC_total); + // apply boundary conditions 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_C_in = math.multiply(1/math.sum(this.Fs), [this.Fs], this.Cs_in)[0]; const BC_gradient = Array(this.n_x).fill(0.0); BC_gradient[0] = -1; BC_gradient[1] = 1; - const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], this.state)[0]; - this.state[0] = math.add(BC_C_in, BC_dispersion); - console.log(BC_dispersion); + const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], new_state)[0]; + new_state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); + console.log(new_state[0]) + } else { // Neumann BC (no flux) + new_state[0] = new_state[1]; } + // Neumann BC (no flux) + new_state[this.n_x-1] = new_state[this.n_x-2] - const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); - - // clip value element-wise to each subarray to avoid negative concentrations - this.state = math.add(this.state, dC_total).map(row => row.map(val => val < 0 ? 0 : val)); - return this.state; + this.state = new_state.map(row => row.map(val => val < 0 ? 0 : val)); // apply the new state + return new_state; } - makeDoperator() { // create the upwind scheme gradient operator - const I = math.diag(Array(this.n_x).fill(1), 0); + makeDoperator(central=false) { // create the upwind scheme gradient operator + const I = math.resize(math.diag(Array(this.n_x).fill(1), central), [this.n_x, this.n_x]); const A = math.resize(math.diag(Array(this.n_x).fill(-1), -1), [this.n_x, this.n_x]); - I[0][0] = 0; - I[0][1] = -1; - return math.add(I, A); + 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; } 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]); - I[0][0] = 0; - I[0][1] = -1; - I[0][0] = -1; // Dichelet boundary condition at outlet - return math.add(I, A, B); + 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; } } @@ -211,4 +216,4 @@ class Reactor_PFR { // N += 1; // } -module.exports = {Reactor_CSTR, Reactor_PFR}; \ No newline at end of file +module.exports = { Reactor_CSTR, Reactor_PFR }; \ No newline at end of file From 0cc653800324a0990508ebe38799c2f6a67ddf26 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 27 Jun 2025 17:29:20 +0200 Subject: [PATCH 36/97] Handle division by zero in rate calculations for ASM3 --- dependencies/asm3_class.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 9b7762a..6f1e5fe 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -104,8 +104,8 @@ class ASM3 { // Heterotrophs rates[1] = k_STO * this._monod(S_O, K_O) * this._monod(S_S, K_S) * X_H; rates[2] = k_STO * 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] = mu_H_max * 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] = mu_H_max * 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[3] = X_H == 0 ? 0 : mu_H_max * 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 : mu_H_max * 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] = b_H_O * this._monod(S_O, K_O) * X_H; rates[6] = b_H_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_H; rates[7] = b_STO_O * this._monod(S_O, K_O) * X_H; From 8215c5ed9a6a736eed45a47dd8bbd6dd1772e888 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Sat, 28 Jun 2025 19:19:38 +0200 Subject: [PATCH 37/97] Add checks for NaN values in Reactor_PFR calculations and update hydrolysis rate calculation to handle division by zero --- dependencies/asm3_class.js | 2 +- dependencies/reactor_class.js | 33 ++++++++++++++++++++++++++------- 2 files changed, 27 insertions(+), 8 deletions(-) diff --git a/dependencies/asm3_class.js b/dependencies/asm3_class.js index 6f1e5fe..f5ac1df 100644 --- a/dependencies/asm3_class.js +++ b/dependencies/asm3_class.js @@ -99,7 +99,7 @@ class ASM3 { 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; // Hydrolysis - rates[0] = k_H * this._monod(X_S / X_H, K_X) * X_H; + rates[0] = X_H == 0 ? 0 : k_H * this._monod(X_S / X_H, K_X) * X_H; // Heterotrophs rates[1] = k_STO * this._monod(S_O, K_O) * this._monod(S_S, K_S) * X_H; diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 94844ee..8212aaf 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -103,7 +103,7 @@ class Reactor_PFR { this.timeStep = 1/(24*60*15); // time step [d] this.speedUpFactor = 60; - this.D_op = this.makeDoperator(false); + this.D_op = this.makeDoperator(true); this.D2_op = this.makeD2operator(); } @@ -111,8 +111,10 @@ class Reactor_PFR { let index_in = input.payload.inlet; this.Fs[index_in] = input.payload.F; this.Cs_in[index_in] = input.payload.C; - // console.log("Pe " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); - // console.log("Co " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); + console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); + console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); + console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); + console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); } set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] @@ -134,7 +136,6 @@ class Reactor_PFR { // expect update with timestamp updateState(newTime) { - const day2ms = 1000 * 60 * 60 * 24; let n_iter = Math.floor(this.speedUpFactor*(newTime - this.currentTime) / (this.timeStep * day2ms)); @@ -153,6 +154,16 @@ class Reactor_PFR { 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)); const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); + + if (dispersion.some(row => row.some(Number.isNaN))) { + throw new Error("NaN detected in dispersion!"); + } + if (advection.some(row => row.some(Number.isNaN))) { + throw new Error("NaN detected in advection!"); + } + if (reaction.some(row => row.some(Number.isNaN))) { + throw new Error("NaN detected in reaction!"); + } if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR transfer.forEach((x) => { x[0] = this.OTR; }); @@ -162,6 +173,9 @@ class Reactor_PFR { const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); const new_state = math.add(this.state, dC_total); + if (new_state.some(row => row.some(Number.isNaN))) { + throw new Error("NaN detected in new_state after dC_total update!"); + } // apply boundary conditions if (math.sum(this.Fs) > 0) { // Danckwerts BC @@ -170,21 +184,26 @@ class Reactor_PFR { BC_gradient[0] = -1; BC_gradient[1] = 1; const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], new_state)[0]; + console.log(math.add(BC_C_in, BC_dispersion)); new_state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); - console.log(new_state[0]) + } else { // Neumann BC (no flux) new_state[0] = new_state[1]; } // Neumann BC (no flux) new_state[this.n_x-1] = new_state[this.n_x-2] + if (new_state.some(row => row.some(Number.isNaN))) { + throw new Error("NaN detected in new_state after enforcing boundary conditions!"); + } + this.state = new_state.map(row => row.map(val => val < 0 ? 0 : val)); // apply the new state return new_state; } makeDoperator(central=false) { // create the upwind scheme gradient operator - const I = math.resize(math.diag(Array(this.n_x).fill(1), central), [this.n_x, this.n_x]); - const A = math.resize(math.diag(Array(this.n_x).fill(-1), -1), [this.n_x, this.n_x]); + 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); From b2d32ba9f2011bbc878a0aa46db1925e5ed69ad4 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 30 Jun 2025 12:50:02 +0200 Subject: [PATCH 38/97] Enhance makeDoperator to support higher-order central gradient schemes and improve boundary handling --- dependencies/reactor_class.js | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 8212aaf..e80f88b 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -201,10 +201,32 @@ class Reactor_PFR { return new_state; } - makeDoperator(central=false) { // create the upwind scheme gradient operator - 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); + 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); + const NearBoundary = Array(this.n_x).fill(0.0); + NearBoundary[1] = -25/12; + NearBoundary[2] = 4; + NearBoundary[3] = -3; + NearBoundary[4] = 4/3; + NearBoundary[5] = -1/4; + D[1] = NearBoundary; + NearBoundary.reverse(); + D[this.n_x-2] = math.multiply(-1, NearBoundary) + } 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; From 3cc876533c64a2ac5b4dd0043af0aaf6b43e01f2 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 30 Jun 2025 15:46:13 +0200 Subject: [PATCH 39/97] Changed the upper boundary to lower order scheme for now --- dependencies/reactor_class.js | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index e80f88b..0d73705 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -103,8 +103,10 @@ class Reactor_PFR { this.timeStep = 1/(24*60*15); // time step [d] this.speedUpFactor = 60; - this.D_op = this.makeDoperator(true); + this.D_op = this.makeDoperator(true, true); this.D2_op = this.makeD2operator(); + + this.alpha = 0.001; // boundary condition modifier } set setInfluent(input) { // setter for C_in (WIP) @@ -183,7 +185,7 @@ class Reactor_PFR { const BC_gradient = Array(this.n_x).fill(0.0); BC_gradient[0] = -1; BC_gradient[1] = 1; - const BC_dispersion = math.multiply(this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], new_state)[0]; + const BC_dispersion = math.multiply(this.alpha*this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], new_state)[0]; console.log(math.add(BC_C_in, BC_dispersion)); new_state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); @@ -208,16 +210,22 @@ class Reactor_PFR { 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); + const D = math.add(I, A, B, C); + D[1][0] = -1; + D[1][1] = 0; + D[1][2] = 1; + D[1][3] = 0; const NearBoundary = Array(this.n_x).fill(0.0); NearBoundary[1] = -25/12; NearBoundary[2] = 4; NearBoundary[3] = -3; NearBoundary[4] = 4/3; NearBoundary[5] = -1/4; - D[1] = NearBoundary; NearBoundary.reverse(); - D[this.n_x-2] = math.multiply(-1, NearBoundary) + D[this.n_x-2] = 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."); } @@ -225,11 +233,10 @@ class Reactor_PFR { 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; } - - 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; } makeD2operator() { // create the central second derivative operator From f4824b822cac7208cd87bd5fbb12a9c182f674a8 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 1 Jul 2025 13:04:32 +0200 Subject: [PATCH 40/97] Improved wieghted finite differencing --- dependencies/reactor_class.js | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 0d73705..2c8d3b8 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -211,18 +211,15 @@ class Reactor_PFR { 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); - D[1][0] = -1; - D[1][1] = 0; - D[1][2] = 1; - D[1][3] = 0; const NearBoundary = Array(this.n_x).fill(0.0); - NearBoundary[1] = -25/12; - NearBoundary[2] = 4; - NearBoundary[3] = -3; - NearBoundary[4] = 4/3; - NearBoundary[5] = -1/4; + 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] = NearBoundary; + 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; From e9847607e85d8f6776dae255337dfb04879f753d Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 1 Jul 2025 16:08:35 +0200 Subject: [PATCH 41/97] Use Generalized boundary condition by Nauman and Mallikarjun 1983 --- dependencies/reactor_class.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 2c8d3b8..6dd6168 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -2,7 +2,7 @@ const ASM3 = require('./asm3_class') const { create, all } = require('mathjs') const config = { - matrix: 'Array' // Choose 'Matrix' (default) or 'Array' + matrix: 'Array' // choose 'Matrix' (default) or 'Array' } const math = create(all, config) @@ -185,7 +185,8 @@ class Reactor_PFR { const BC_gradient = Array(this.n_x).fill(0.0); BC_gradient[0] = -1; BC_gradient[1] = 1; - const BC_dispersion = math.multiply(this.alpha*this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], new_state)[0]; + let Pe = this.length*math.sum(this.Fs)/(this.D*this.A) + const BC_dispersion = math.multiply((1-(1+4*this.volume/math.sum(this.Fs)/Pe)^0.5)/Pe, this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], new_state)[0]; console.log(math.add(BC_C_in, BC_dispersion)); new_state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); From dcc8562dbf5ac2800d6ce4bc10a2b6ff8f88cc1b Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 2 Jul 2025 10:37:02 +0200 Subject: [PATCH 42/97] Remove depreciated variable --- dependencies/reactor_class.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 6dd6168..652ee26 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -105,8 +105,6 @@ class Reactor_PFR { this.D_op = this.makeDoperator(true, true); this.D2_op = this.makeD2operator(); - - this.alpha = 0.001; // boundary condition modifier } set setInfluent(input) { // setter for C_in (WIP) From f517b7764d083b49f8fac052a2f81e29da0325ab Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 3 Jul 2025 22:28:34 +0200 Subject: [PATCH 43/97] Remove mistake boundary condition --- dependencies/reactor_class.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 652ee26..76a6340 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -184,7 +184,7 @@ class Reactor_PFR { BC_gradient[0] = -1; BC_gradient[1] = 1; let Pe = this.length*math.sum(this.Fs)/(this.D*this.A) - const BC_dispersion = math.multiply((1-(1+4*this.volume/math.sum(this.Fs)/Pe)^0.5)/Pe, this.D * this.A / (math.sum(this.Fs)*this.d_x), [BC_gradient], new_state)[0]; + const BC_dispersion = math.multiply((1-(1+4*this.volume/math.sum(this.Fs)/Pe)^0.5)/Pe, [BC_gradient], new_state)[0]; console.log(math.add(BC_C_in, BC_dispersion)); new_state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); From d0db1b416c36bf9b73238770dd269a6d15ac794a Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 10:01:46 +0200 Subject: [PATCH 44/97] Remove debug messages --- dependencies/reactor_class.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/dependencies/reactor_class.js b/dependencies/reactor_class.js index 76a6340..c1e8989 100644 --- a/dependencies/reactor_class.js +++ b/dependencies/reactor_class.js @@ -111,10 +111,10 @@ class Reactor_PFR { let index_in = input.payload.inlet; this.Fs[index_in] = input.payload.F; this.Cs_in[index_in] = input.payload.C; - console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); - console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); - console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); - console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); + // console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); + // console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); + // console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); + // console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); } set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] @@ -185,7 +185,6 @@ class Reactor_PFR { BC_gradient[1] = 1; let Pe = this.length*math.sum(this.Fs)/(this.D*this.A) const BC_dispersion = math.multiply((1-(1+4*this.volume/math.sum(this.Fs)/Pe)^0.5)/Pe, [BC_gradient], new_state)[0]; - console.log(math.add(BC_C_in, BC_dispersion)); new_state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); } else { // Neumann BC (no flux) From 25cd728b68e04a3ceca70d0cf4288bffd8ca3d00 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 10:44:54 +0200 Subject: [PATCH 45/97] Refactor reactor node registration --- advanced-reactor.js | 105 ++---------------------- {dependencies => src}/asm3_class.js | 0 src/nodeClass.js | 109 +++++++++++++++++++++++++ {dependencies => src}/reactor_class.js | 0 4 files changed, 118 insertions(+), 96 deletions(-) rename {dependencies => src}/asm3_class.js (100%) create mode 100644 src/nodeClass.js rename {dependencies => src}/reactor_class.js (100%) diff --git a/advanced-reactor.js b/advanced-reactor.js index e2eca51..ee954cd 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -1,99 +1,12 @@ +const nameOfNode = "advanced-reactor"; // name of the node, should match file name and node type in Node-RED +const nodeClass = require('./src/nodeClass.js'); // node class + module.exports = function(RED) { - function reactor(config) { + // Register the node type + RED.nodes.registerType(nameOfNode, function(config) { + // Initialize the Node-RED node first RED.nodes.createNode(this, config); - var node = this; - - let name = config.name; - - const { Reactor_CSTR, Reactor_PFR } = require('./dependencies/reactor_class'); - - let new_reactor; - - switch (config.reactor_type) { - case "CSTR": - new_reactor = new Reactor_CSTR( - parseFloat(config.volume), - parseInt(config.n_inlets), - parseFloat(config.kla), - [ - parseFloat(config.S_O_init), - parseFloat(config.S_I_init), - parseFloat(config.S_S_init), - parseFloat(config.S_NH_init), - parseFloat(config.S_N2_init), - parseFloat(config.S_NO_init), - parseFloat(config.S_HCO_init), - parseFloat(config.X_I_init), - parseFloat(config.X_S_init), - parseFloat(config.X_H_init), - parseFloat(config.X_STO_init), - parseFloat(config.X_A_init), - parseFloat(config.X_TS_init) - ] - ); - break; - case "PFR": - new_reactor = new Reactor_PFR( - parseFloat(config.volume), - parseFloat(config.length), - parseInt(config.resolution_L), - parseInt(config.n_inlets), - parseFloat(config.kla), - [ - parseFloat(config.S_O_init), - parseFloat(config.S_I_init), - parseFloat(config.S_S_init), - parseFloat(config.S_NH_init), - parseFloat(config.S_N2_init), - parseFloat(config.S_NO_init), - parseFloat(config.S_HCO_init), - parseFloat(config.X_I_init), - parseFloat(config.X_S_init), - parseFloat(config.X_H_init), - parseFloat(config.X_STO_init), - parseFloat(config.X_A_init), - parseFloat(config.X_TS_init) - ] - ); - break; - default: - console.warn("Unknown reactor type: " + config.reactor_type); - } - - const reactor = new_reactor; // protect from reassignment - - node.on('input', function(msg, send, done) { - let toggleUpdate = false; - - switch (msg.topic) { - case "clock": - toggleUpdate = true; - break; - case "Fluent": - reactor.setInfluent = msg; - if (msg.payload.inlet == 0) { - toggleUpdate = true; - } - break; - case "OTR": - reactor.setOTR = msg; - break; - case "Dispersion": - reactor.setDispersion = msg; - break; - default: - console.log("Unknown topic: " + msg.topic); - } - - if (toggleUpdate) { - reactor.updateState(msg.timestamp); - send(reactor.getEffluent); - } - - if (done) { - done(); - } - }); - } - RED.nodes.registerType("advanced-reactor", reactor); + // Then create your custom class and attach it + this.nodeClass = new nodeClass(config, RED, this, nameOfNode); + }); }; diff --git a/dependencies/asm3_class.js b/src/asm3_class.js similarity index 100% rename from dependencies/asm3_class.js rename to src/asm3_class.js diff --git a/src/nodeClass.js b/src/nodeClass.js new file mode 100644 index 0000000..9281a45 --- /dev/null +++ b/src/nodeClass.js @@ -0,0 +1,109 @@ +const { Reactor_CSTR, Reactor_PFR } = require('./reactor_class.js'); + + +class nodeClass { + /** + * Create a ReactorNode. + * @param {object} uiConfig - Node-RED node configuration. + * @param {object} RED - Node-RED runtime API. + * @param {object} nodeInstance - The Node-RED node instance. + * @param {string} nameOfNode - The name of the node, used for + */ + constructor(uiConfig, RED, nodeInstance, nameOfNode) { + // Preserve RED reference for HTTP endpoints if needed + this.node = nodeInstance; + this.RED = RED; + this.name = nameOfNode; + + let new_reactor; + + switch (uiConfig.reactor_type) { + case "CSTR": + new_reactor = new Reactor_CSTR( + parseFloat(uiConfig.volume), + parseInt(uiConfig.n_inlets), + parseFloat(uiConfig.kla), + [ + 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) + ] + ); + break; + case "PFR": + new_reactor = new Reactor_PFR( + parseFloat(uiConfig.volume), + parseFloat(uiConfig.length), + parseInt(uiConfig.resolution_L), + parseInt(uiConfig.n_inlets), + parseFloat(uiConfig.kla), + [ + 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) + ] + ); + break; + default: + console.warn("Unknown reactor type: " + uiConfig.reactor_type); + } + + const reactor = new_reactor; // protect from reassignment + + this.node.on('input', function(msg, send, done) { + let toggleUpdate = false; + + switch (msg.topic) { + case "clock": + toggleUpdate = true; + break; + case "Fluent": + reactor.setInfluent = msg; + if (msg.payload.inlet == 0) { + toggleUpdate = true; + } + break; + case "OTR": + reactor.setOTR = msg; + break; + case "Dispersion": + reactor.setDispersion = msg; + break; + default: + console.log("Unknown topic: " + msg.topic); + } + + if (toggleUpdate) { + reactor.updateState(msg.timestamp); + send(reactor.getEffluent); + } + + if (done) { + done(); + } + }); + } + +} + +module.exports = nodeClass; \ No newline at end of file diff --git a/dependencies/reactor_class.js b/src/reactor_class.js similarity index 100% rename from dependencies/reactor_class.js rename to src/reactor_class.js From 1cda956d832cecc2a079cdc14941601c0f15e170 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 11:42:34 +0200 Subject: [PATCH 46/97] Refactor Reactor class structure and include inheritance for CSTR and PFR --- src/reactor_class.js | 66 +++++++++++++++++--------------------------- 1 file changed, 25 insertions(+), 41 deletions(-) diff --git a/src/reactor_class.js b/src/reactor_class.js index c1e8989..a81ff7f 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -7,43 +7,56 @@ const config = { const math = create(all, config) -class Reactor_CSTR { - - constructor(volume, n_inlets, kla, initial_state) { - this.state = initial_state; +class Reactor { + + constructor(volume, n_inlets, kla){ this.asm = new ASM3(); this.Vl = volume; // fluid volume reactor [m3] + this.Fs = Array(n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] this.Cs_in = Array.from(Array(n_inlets), () => new Array(13).fill(0.0)); // composition influents this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] - + this.kla = kla; // if NaN, use external OTR [d-1] this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1/(24*60*15); // time step [d] - this.speedUpFactor = 1; + this.speedUpFactor = 60; } set setInfluent(input) { // setter for C_in (WIP) let index_in = input.payload.inlet; this.Fs[index_in] = input.payload.F; this.Cs_in[index_in] = input.payload.C; + // console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); + // console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); + // console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); + // console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); } set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] this.OTR = input.payload; } - get getEffluent() { // getter for Effluent, defaults to inlet 0 - return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; - } - 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); } +} + +class Reactor_CSTR extends Reactor { + + constructor(volume, n_inlets, kla, initial_state) { + super(volume, n_inlets, kla); + this.state = initial_state; + } + + get getEffluent() { // getter for Effluent, defaults to inlet 0 + return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; + } + // expect update with timestamp updateState(newTime) { @@ -75,12 +88,11 @@ class Reactor_CSTR { } } -class Reactor_PFR { +class Reactor_PFR extends Reactor { constructor(volume, length, resolution_L, n_inlets, kla, initial_state) { - this.asm = new ASM3(); + super(volume, n_inlets, kla); - this.Vl = volume; // fluid volume reactor [m3] this.length = length; // reactor length [m] this.n_x = resolution_L; // number of slices this.d_x = length / resolution_L; @@ -92,35 +104,12 @@ class Reactor_PFR { // console.log("Initial State: ") // console.log(this.state) - this.Fs = Array(n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] - this.Cs_in = Array.from(Array(n_inlets), () => new Array(13).fill(0.0)); // composition influents - this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] this.D = 0.0; // axial dispersion [m2 d-1] - - this.kla = kla; // if NaN, use external OTR [d-1] - - this.currentTime = Date.now(); // milliseconds since epoch [ms] - this.timeStep = 1/(24*60*15); // time step [d] - this.speedUpFactor = 60; this.D_op = this.makeDoperator(true, true); this.D2_op = this.makeD2operator(); } - set setInfluent(input) { // setter for C_in (WIP) - let index_in = input.payload.inlet; - this.Fs[index_in] = input.payload.F; - this.Cs_in[index_in] = input.payload.C; - // console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); - // console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); - // console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); - // console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); - } - - set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] - this.OTR = input.payload; - } - set setDispersion(input) { // setter for Axial dispersion [m2 d-1] this.D = input.payload; } @@ -129,11 +118,6 @@ class Reactor_PFR { return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state.at(-1)}, timestamp: this.currentTime}; } - 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); - } - // expect update with timestamp updateState(newTime) { const day2ms = 1000 * 60 * 60 * 24; From fee6881f1b9847ca68602022871ccfcf2141ea39 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 12:06:58 +0200 Subject: [PATCH 47/97] Refactor nodeClass for to mostly allign with the standard EVOLV structure --- src/nodeClass.js | 78 ++++++++++++++++++++++++++---------------------- 1 file changed, 43 insertions(+), 35 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 9281a45..ad11f69 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -15,6 +15,48 @@ class nodeClass { this.RED = RED; this.name = nameOfNode; + this._setupClass(uiConfig, this.node); + + this._attachInputHandler(); + + } + + _attachInputHandler() { // Handle input messages + this.node.on('input', function(msg, send, done) { + let toggleUpdate = false; + + switch (msg.topic) { + case "clock": + toggleUpdate = true; + break; + case "Fluent": + this.reactor.setInfluent = msg; + if (msg.payload.inlet == 0) { + toggleUpdate = true; + } + break; + case "OTR": + this.reactor.setOTR = msg; + break; + case "Dispersion": + this.reactor.setDispersion = msg; + break; + default: + console.log("Unknown topic: " + msg.topic); + } + + if (toggleUpdate) { + this.reactor.updateState(msg.timestamp); + send(this.reactor.getEffluent); + } + + if (done) { + done(); + } + }); + } + + _setupClass(uiConfig, node) { let new_reactor; switch (uiConfig.reactor_type) { @@ -68,42 +110,8 @@ class nodeClass { console.warn("Unknown reactor type: " + uiConfig.reactor_type); } - const reactor = new_reactor; // protect from reassignment - - this.node.on('input', function(msg, send, done) { - let toggleUpdate = false; - - switch (msg.topic) { - case "clock": - toggleUpdate = true; - break; - case "Fluent": - reactor.setInfluent = msg; - if (msg.payload.inlet == 0) { - toggleUpdate = true; - } - break; - case "OTR": - reactor.setOTR = msg; - break; - case "Dispersion": - reactor.setDispersion = msg; - break; - default: - console.log("Unknown topic: " + msg.topic); - } - - if (toggleUpdate) { - reactor.updateState(msg.timestamp); - send(reactor.getEffluent); - } - - if (done) { - done(); - } - }); + node.reactor = new_reactor; // protect from reassignment } - } module.exports = nodeClass; \ No newline at end of file From c23818c10805bf60cdd7386c68e2ed7bc0e6c0ac Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 12:16:08 +0200 Subject: [PATCH 48/97] Remove unnecessary node parameter _setupClass --- src/nodeClass.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index ad11f69..4cbb4bf 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -15,7 +15,7 @@ class nodeClass { this.RED = RED; this.name = nameOfNode; - this._setupClass(uiConfig, this.node); + this._setupClass(uiConfig); this._attachInputHandler(); @@ -56,7 +56,7 @@ class nodeClass { }); } - _setupClass(uiConfig, node) { + _setupClass(uiConfig) { let new_reactor; switch (uiConfig.reactor_type) { @@ -110,7 +110,7 @@ class nodeClass { console.warn("Unknown reactor type: " + uiConfig.reactor_type); } - node.reactor = new_reactor; // protect from reassignment + this.reactor = new_reactor; // protect from reassignment } } From 530dac5c771b7dd6110c64965f3cc96cf52cd1aa Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 12:51:37 +0200 Subject: [PATCH 49/97] Refactor nodeClass to streamline configuration loading and reactor setup --- src/nodeClass.js | 94 ++++++++++++++++++++++++------------------------ 1 file changed, 47 insertions(+), 47 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 4cbb4bf..917f1c0 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -3,11 +3,11 @@ const { Reactor_CSTR, Reactor_PFR } = require('./reactor_class.js'); class nodeClass { /** - * Create a ReactorNode. - * @param {object} uiConfig - Node-RED node configuration. - * @param {object} RED - Node-RED runtime API. - * @param {object} nodeInstance - The Node-RED node instance. - * @param {string} nameOfNode - The name of the node, used for + * Create ReactorNode. + * @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 @@ -15,14 +15,16 @@ class nodeClass { this.RED = RED; this.name = nameOfNode; - this._setupClass(uiConfig); + this._loadConfig(uiConfig) + + this._setupClass(); this._attachInputHandler(); } _attachInputHandler() { // Handle input messages - this.node.on('input', function(msg, send, done) { + this.node.on('input', (msg, send, done) => { let toggleUpdate = false; switch (msg.topic) { @@ -56,54 +58,52 @@ class nodeClass { }); } - _setupClass(uiConfig) { + _loadConfig(uiConfig) { + this.config = { + reactor_type: uiConfig.reactor_type, + volume: parseFloat(uiConfig.volume), + length: parseFloat(uiConfig.length), + resolution_L: parseInt(uiConfig.resolution_L), + 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) + ] + } + } + + _setupClass() { let new_reactor; - switch (uiConfig.reactor_type) { + switch (this.config.reactor_type) { case "CSTR": new_reactor = new Reactor_CSTR( - parseFloat(uiConfig.volume), - parseInt(uiConfig.n_inlets), - parseFloat(uiConfig.kla), - [ - 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) - ] + this.config.volume, + this.config.n_inlets, + this.config.kla, + this.config.initialState ); break; case "PFR": new_reactor = new Reactor_PFR( - parseFloat(uiConfig.volume), - parseFloat(uiConfig.length), - parseInt(uiConfig.resolution_L), - parseInt(uiConfig.n_inlets), - parseFloat(uiConfig.kla), - [ - 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) - ] + this.config.volume, + this.config.length, + this.config.resolution_L, + this.config.n_inlets, + this.config.kla, + this.config.initialState ); break; default: From 348307d99919688cebae08e457d6a099e65ba539 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 13:09:20 +0200 Subject: [PATCH 50/97] Add documentation --- src/nodeClass.js | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 917f1c0..62a6226 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -3,7 +3,7 @@ const { Reactor_CSTR, Reactor_PFR } = require('./reactor_class.js'); class nodeClass { /** - * Create ReactorNode. + * Construct ReactorNode. * @param {object} uiConfig - Node-RED node configuration * @param {object} RED - Node-RED runtime API * @param {object} nodeInstance - Node-RED node instance @@ -23,7 +23,10 @@ class nodeClass { } - _attachInputHandler() { // Handle input messages + /** + * Handle node-red input messages + */ + _attachInputHandler() { this.node.on('input', (msg, send, done) => { let toggleUpdate = false; @@ -58,6 +61,10 @@ class nodeClass { }); } + /** + * Parse node configuration + * @param {object} uiConfig Config set in UI in node-red + */ _loadConfig(uiConfig) { this.config = { reactor_type: uiConfig.reactor_type, @@ -84,6 +91,9 @@ class nodeClass { } } + /** + * Setup reactor class based on config + */ _setupClass() { let new_reactor; From c239b71ad87b9ee9243a858ab4483e0a803cb19d Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 13:16:49 +0200 Subject: [PATCH 51/97] Refactor reactor constructors to accept a config object for improved clarity and maintainability --- src/nodeClass.js | 16 ++-------------- src/reactor_class.js | 30 +++++++++++++++--------------- 2 files changed, 17 insertions(+), 29 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 62a6226..15a959b 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -99,22 +99,10 @@ class nodeClass { switch (this.config.reactor_type) { case "CSTR": - new_reactor = new Reactor_CSTR( - this.config.volume, - this.config.n_inlets, - this.config.kla, - this.config.initialState - ); + new_reactor = new Reactor_CSTR(this.config); break; case "PFR": - new_reactor = new Reactor_PFR( - this.config.volume, - this.config.length, - this.config.resolution_L, - this.config.n_inlets, - this.config.kla, - this.config.initialState - ); + new_reactor = new Reactor_PFR(this.config); break; default: console.warn("Unknown reactor type: " + uiConfig.reactor_type); diff --git a/src/reactor_class.js b/src/reactor_class.js index a81ff7f..220fbf9 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -9,16 +9,16 @@ const math = create(all, config) class Reactor { - constructor(volume, n_inlets, kla){ + constructor(config){ this.asm = new ASM3(); - this.Vl = volume; // fluid volume reactor [m3] + this.Vl = config.volume; // fluid volume reactor [m3] - this.Fs = Array(n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] - this.Cs_in = Array.from(Array(n_inlets), () => new Array(13).fill(0.0)); // composition influents + this.Fs = Array(config.n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] + this.Cs_in = Array.from(Array(config.n_inlets), () => new Array(13).fill(0.0)); // composition influents this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] - this.kla = kla; // if NaN, use external OTR [d-1] + this.kla = config.kla; // if NaN, use external OTR [d-1] this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1/(24*60*15); // time step [d] @@ -48,9 +48,9 @@ class Reactor { class Reactor_CSTR extends Reactor { - constructor(volume, n_inlets, kla, initial_state) { - super(volume, n_inlets, kla); - this.state = initial_state; + constructor(config) { + super(config); + this.state = config.initialState; } get getEffluent() { // getter for Effluent, defaults to inlet 0 @@ -90,16 +90,16 @@ class Reactor_CSTR extends Reactor { class Reactor_PFR extends Reactor { - constructor(volume, length, resolution_L, n_inlets, kla, initial_state) { - super(volume, n_inlets, kla); + constructor(config) { + super(config); - this.length = length; // reactor length [m] - this.n_x = resolution_L; // number of slices - this.d_x = length / resolution_L; + this.length = config.length; // reactor length [m] + this.n_x = config.resolution_L; // number of slices - this.A = volume / length; // crosssectional area [m2] + this.d_x = this.length / this.n_x; + this.A = this.Vl / this.length; // crosssectional area [m2] - this.state = Array.from(Array(this.n_x), () => initial_state.slice()) + this.state = Array.from(Array(this.n_x), () => config.initialState.slice()) // console.log("Initial State: ") // console.log(this.state) From 09e7072d16e64b38e2fd6ff11cd77f961705289b Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 13:52:28 +0200 Subject: [PATCH 52/97] Refactor documentation in nodeClass and reactor_class for clarity and consistency --- src/nodeClass.js | 4 +- src/reactor_class.js | 133 ++++++++++++++++++++++++++++--------------- 2 files changed, 90 insertions(+), 47 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 15a959b..b9485be 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -3,9 +3,9 @@ const { Reactor_CSTR, Reactor_PFR } = require('./reactor_class.js'); class nodeClass { /** - * Construct ReactorNode. + * Node-RED node class for advanced-reactor. * @param {object} uiConfig - Node-RED node configuration - * @param {object} RED - Node-RED runtime API + * @param {object} RED - Node-RED runtime API * @param {object} nodeInstance - Node-RED node instance * @param {string} nameOfNode - Name of the node */ diff --git a/src/reactor_class.js b/src/reactor_class.js index 220fbf9..245f194 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -8,7 +8,10 @@ const config = { const math = create(all, config) class Reactor { - + /** + * Reactor base class. + * @param {object} config - Configuration object containing reactor parameters. + */ constructor(config){ this.asm = new ASM3(); @@ -18,62 +21,91 @@ class Reactor { this.Cs_in = Array.from(Array(config.n_inlets), () => new Array(13).fill(0.0)); // composition influents this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] - this.kla = config.kla; // if NaN, use external OTR [d-1] + 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*15); // time step [d] - this.speedUpFactor = 60; + this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second } - set setInfluent(input) { // setter for C_in (WIP) + /** + * 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; + // DEBUG // console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); // console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); // console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); // console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); } - set setOTR(input) { // setter for OTR (WIP) [g O2 d-1] + /** + * Setter for OTR (Oxygen Transfer Rate). + * @param {object} input - Input object (msg) containing payload with OTR value [g O2 d-1]. + */ + set setOTR(input) { this.OTR = input.payload; } + /** + * + * @param {number} S_O - Dissolved oxygen concentration [g O2 m-3]. + * @param {number} T - Temperature in Celsius, default to 20 C. + * @returns + */ 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); } -} - -class Reactor_CSTR extends Reactor { - - constructor(config) { - super(config); - this.state = config.initialState; - } - - get getEffluent() { // getter for Effluent, defaults to inlet 0 - return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; - } - - // expect update with timestamp - updateState(newTime) { - + /** + * 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) { // expect update with timestamp const day2ms = 1000 * 60 * 60 * 24; 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_fe(this.timeStep); + this.tick(this.timeStep); n += 1; } this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; } } - tick_fe(time_step) { // tick reactor state using forward Euler method +} + +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; + } + + /** + * Getter for effluent data. + * @returns {object} Effluent data object (msg), defaults to inlet 0. + */ + get getEffluent() { // getter for Effluent, defaults to inlet 0 + return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; + } + + /** + * 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 r = this.asm.compute_dC(this.state); const dC_in = math.multiply(math.divide([this.Fs], this.Vl), this.Cs_in)[0]; const dC_out = math.multiply(-1*math.sum(this.Fs)/this.Vl, this.state); @@ -89,7 +121,10 @@ class Reactor_CSTR extends Reactor { } class Reactor_PFR extends Reactor { - + /** + * Reactor_PFR class for Plug Flow Reactor. + * @param {object} config - Configuration object containing reactor parameters. + */ constructor(config) { super(config); @@ -109,31 +144,29 @@ class Reactor_PFR extends Reactor { this.D_op = this.makeDoperator(true, true); this.D2_op = this.makeD2operator(); } - - set setDispersion(input) { // setter for Axial dispersion [m2 d-1] + + /** + * 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; } - get getEffluent() { // getter for Effluent, defaults to inlet 0 + /** + * Getter for effluent data. + * @returns {object} Effluent data object (msg), defaults to inlet 0. + */ + get getEffluent() { return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state.at(-1)}, timestamp: this.currentTime}; } - // expect update with timestamp - updateState(newTime) { - const day2ms = 1000 * 60 * 60 * 24; - - 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_fe(this.timeStep); - n += 1; - } - this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; - } - } - - tick_fe(time_step) { // tick reactor state using forward Euler method + /** + * 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)); @@ -185,6 +218,12 @@ class Reactor_PFR extends Reactor { return new_state; } + /** + * 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) { @@ -218,6 +257,10 @@ class Reactor_PFR extends Reactor { } } + /** + * 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]); @@ -230,7 +273,7 @@ class Reactor_PFR extends Reactor { } -// testing stuff +// 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); @@ -239,7 +282,7 @@ class Reactor_PFR extends Reactor { // Reactor.D = 0.01; // let N = 0; // while (N < 5000) { -// console.log(Reactor.tick_fe(0.001)); +// console.log(Reactor.tick(0.001)); // N += 1; // } From 3f5b0eea32e055a3f3ff518ee92474642ea4c987 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 14:58:38 +0200 Subject: [PATCH 53/97] Enhance reactor class with NaN checks and refactor methods for clarity --- src/reactor_class.js | 66 +++++++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 29 deletions(-) diff --git a/src/reactor_class.js b/src/reactor_class.js index 245f194..8473fe8 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -1,4 +1,4 @@ -const ASM3 = require('./asm3_class') +const ASM3 = require('./asm3_class.js') const { create, all } = require('mathjs') const config = { @@ -7,6 +7,12 @@ const config = { const math = create(all, config) +function assertNoNaN(arr, label="array") { + if (math.isNaN(arr)) { + throw new Error("NaN detected in ${label}!"); + } +} + class Reactor { /** * Reactor base class. @@ -57,11 +63,24 @@ class Reactor { * @param {number} T - Temperature in Celsius, default to 20 C. * @returns */ - calcOTR(S_O, T=20.0) { // caculate the OTR using basic correlation, default to temperature: 20 C + _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(clipToZero); + } else { + return arr < 0 ? 0 : arr; + } + } + /** * Update the reactor state based on the new time. * @param {number} newTime - New time to update reactor state to, in milliseconds since epoch. @@ -110,12 +129,11 @@ class Reactor_CSTR extends Reactor { const dC_in = math.multiply(math.divide([this.Fs], this.Vl), this.Cs_in)[0]; const dC_out = math.multiply(-1*math.sum(this.Fs)/this.Vl, this.state); const t_O = Array(13).fill(0.0); - t_O[0] = isNaN(this.kla) ? this.OTR : this.calcOTR(this.state[0]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR + t_O[0] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[0]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR const dC_total = math.multiply(math.add(dC_in, dC_out, r, t_O), time_step); - // clip value element-wise to each subarray to avoid negative concentrations - this.state = math.add(this.state, dC_total).map(val => val < 0 ? 0 : val); + this.state = this_.arrayClip2Zero(math.add(this.state, dC_total)); // clip value element-wise to avoid negative concentrations return this.state; } } @@ -141,8 +159,8 @@ class Reactor_PFR extends Reactor { this.D = 0.0; // axial dispersion [m2 d-1] - this.D_op = this.makeDoperator(true, true); - this.D2_op = this.makeD2operator(); + this.D_op = this._makeDoperator(true, true); + this.D2_op = this._makeD2operator(); } /** @@ -172,27 +190,20 @@ class Reactor_PFR extends Reactor { const reaction = this.state.map((state_slice) => this.asm.compute_dC(state_slice)); const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); - if (dispersion.some(row => row.some(Number.isNaN))) { - throw new Error("NaN detected in dispersion!"); - } - if (advection.some(row => row.some(Number.isNaN))) { - throw new Error("NaN detected in advection!"); - } - if (reaction.some(row => row.some(Number.isNaN))) { - throw new Error("NaN detected in reaction!"); - } + assertNoNaN(dispersion, "dispersion"); + assertNoNaN(advection, "advection"); + assertNoNaN(reaction, "reaction"); if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR transfer.forEach((x) => { x[0] = this.OTR; }); } else { - transfer.forEach((x, i) => { x[0] = this.calcOTR(this.state[i][0]); }); + transfer.forEach((x, i) => { x[0] = this._calcOTR(this.state[i][0]); }); } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); const new_state = math.add(this.state, dC_total); - if (new_state.some(row => row.some(Number.isNaN))) { - throw new Error("NaN detected in new_state after dC_total update!"); - } + + assertNoNaN(new_state, "new state"); // apply boundary conditions if (math.sum(this.Fs) > 0) { // Danckwerts BC @@ -210,11 +221,9 @@ class Reactor_PFR extends Reactor { // Neumann BC (no flux) new_state[this.n_x-1] = new_state[this.n_x-2] - if (new_state.some(row => row.some(Number.isNaN))) { - throw new Error("NaN detected in new_state after enforcing boundary conditions!"); - } + assertNoNaN(new_state, "new state post BC"); - this.state = new_state.map(row => row.map(val => val < 0 ? 0 : val)); // apply the new state + this.state = this._arrayClip2Zero(new_state); return new_state; } @@ -224,7 +233,7 @@ class Reactor_PFR extends Reactor { * @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 + _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]); @@ -261,7 +270,7 @@ class Reactor_PFR extends Reactor { * Create central finite difference second derivative operator. * @returns {Array} - Second derivative operator matrix. */ - makeD2operator() { // create the central second derivative operator + _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]); @@ -272,6 +281,7 @@ class Reactor_PFR extends Reactor { } } +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 @@ -284,6 +294,4 @@ class Reactor_PFR extends Reactor { // while (N < 5000) { // console.log(Reactor.tick(0.001)); // N += 1; -// } - -module.exports = { Reactor_CSTR, Reactor_PFR }; \ No newline at end of file +// } \ No newline at end of file From c6b0cab06785efb050e4a978728d374321c17c0e Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 15:14:03 +0200 Subject: [PATCH 54/97] Refactor advanced-reactor and nodeClass for improved readability and consistency --- advanced-reactor.js | 4 +- src/nodeClass.js | 1 - src/reactor_class.js | 115 +++++++++++++++++++++++-------------------- 3 files changed, 64 insertions(+), 56 deletions(-) diff --git a/advanced-reactor.js b/advanced-reactor.js index ee954cd..9ca6959 100644 --- a/advanced-reactor.js +++ b/advanced-reactor.js @@ -1,9 +1,9 @@ const nameOfNode = "advanced-reactor"; // name of the node, should match file name and node type in Node-RED const nodeClass = require('./src/nodeClass.js'); // node class -module.exports = function(RED) { +module.exports = function (RED) { // Register the node type - RED.nodes.registerType(nameOfNode, function(config) { + 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 diff --git a/src/nodeClass.js b/src/nodeClass.js index b9485be..a873284 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -20,7 +20,6 @@ class nodeClass { this._setupClass(); this._attachInputHandler(); - } /** diff --git a/src/reactor_class.js b/src/reactor_class.js index 8473fe8..d483201 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -2,12 +2,17 @@ const ASM3 = require('./asm3_class.js') const { create, all } = require('mathjs') const config = { - matrix: 'Array' // choose 'Matrix' (default) or 'Array' + matrix: 'Array' // use Array as the matrix type } const math = create(all, config) -function assertNoNaN(arr, label="array") { +/** + * Assert that no NaN values are present in an array. + * @param {Array} arr + * @param {string} label + */ +function assertNoNaN(arr, label = "array") { if (math.isNaN(arr)) { throw new Error("NaN detected in ${label}!"); } @@ -18,7 +23,7 @@ class Reactor { * Reactor base class. * @param {object} config - Configuration object containing reactor parameters. */ - constructor(config){ + constructor(config) { this.asm = new ASM3(); this.Vl = config.volume; // fluid volume reactor [m3] @@ -30,7 +35,7 @@ class Reactor { 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*15); // time step [d] + this.timeStep = 1 / (24 * 60 * 15); // time step [d] this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second } @@ -63,8 +68,8 @@ class Reactor { * @param {number} T - Temperature in Celsius, default to 20 C. * @returns */ - _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; + _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); } @@ -86,9 +91,9 @@ class Reactor { * @param {number} newTime - New time to update reactor state to, in milliseconds since epoch. */ updateState(newTime) { // expect update with timestamp - const day2ms = 1000 * 60 * 60 * 24; + const day2ms = 1000 * 60 * 60 * 24; - let n_iter = Math.floor(this.speedUpFactor*(newTime - this.currentTime) / (this.timeStep * day2ms)); + let n_iter = Math.floor(this.speedUpFactor * (newTime - this.currentTime) / (this.timeStep * day2ms)); if (n_iter) { let n = 0; while (n < n_iter) { @@ -116,7 +121,7 @@ class Reactor_CSTR extends Reactor { * @returns {object} Effluent data object (msg), defaults to inlet 0. */ get getEffluent() { // getter for Effluent, defaults to inlet 0 - return {topic: "Fluent", payload: {inlet: 0, F: math.sum(this.Fs), C:this.state}, timestamp: this.currentTime}; + return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; } /** @@ -127,13 +132,13 @@ class Reactor_CSTR extends Reactor { tick(time_step) { // tick reactor state using forward Euler method const r = this.asm.compute_dC(this.state); const dC_in = math.multiply(math.divide([this.Fs], this.Vl), this.Cs_in)[0]; - const dC_out = math.multiply(-1*math.sum(this.Fs)/this.Vl, this.state); + const dC_out = math.multiply(-1 * math.sum(this.Fs) / this.Vl, this.state); const t_O = Array(13).fill(0.0); t_O[0] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[0]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR const dC_total = math.multiply(math.add(dC_in, dC_out, r, t_O), time_step); - this.state = this_.arrayClip2Zero(math.add(this.state, dC_total)); // clip value element-wise to avoid negative concentrations + this.state = this._arrayClip2Zero(math.add(this.state, dC_total)); // clip value element-wise to avoid negative concentrations return this.state; } } @@ -153,7 +158,7 @@ class Reactor_PFR extends Reactor { this.A = this.Vl / this.length; // crosssectional area [m2] this.state = Array.from(Array(this.n_x), () => config.initialState.slice()) - + // console.log("Initial State: ") // console.log(this.state) @@ -162,7 +167,7 @@ class Reactor_PFR extends Reactor { this.D_op = this._makeDoperator(true, true); this.D2_op = this._makeD2operator(); } - + /** * Setter for axial dispersion. * @param {object} input - Input object (msg) containing payload with dispersion value [m2 d-1]. @@ -176,7 +181,25 @@ class Reactor_PFR extends Reactor { * @returns {object} Effluent data object (msg), defaults to inlet 0. */ get getEffluent() { - 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.at(-1) }, timestamp: this.currentTime }; + } + + _applyBoundaryConditions(newState) { + // apply boundary conditions + 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_gradient = Array(this.n_x).fill(0.0); + BC_gradient[0] = -1; + BC_gradient[1] = 1; + let Pe = this.length * math.sum(this.Fs) / (this.D * this.A) + const BC_dispersion = math.multiply((1 - (1 + 4 * this.volume / math.sum(this.Fs) / Pe) ^ 0.5) / Pe, [BC_gradient], stateNew)[0]; + newState[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); + } else { // Neumann BC (no flux) + newState[0] = newState[1]; + } + // Neumann BC (no flux) + newState[this.n_x - 1] = newState[this.n_x - 2] + return newState } /** @@ -185,15 +208,15 @@ class Reactor_PFR extends Reactor { * @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 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)); const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); assertNoNaN(dispersion, "dispersion"); assertNoNaN(advection, "advection"); assertNoNaN(reaction, "reaction"); - + if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR transfer.forEach((x) => { x[0] = this.OTR; }); } else { @@ -201,30 +224,16 @@ class Reactor_PFR extends Reactor { } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); - const new_state = math.add(this.state, dC_total); + let stateNew = math.add(this.state, dC_total); - assertNoNaN(new_state, "new state"); + assertNoNaN(stateNew, "new state"); - // apply boundary conditions - 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_gradient = Array(this.n_x).fill(0.0); - BC_gradient[0] = -1; - BC_gradient[1] = 1; - let Pe = this.length*math.sum(this.Fs)/(this.D*this.A) - const BC_dispersion = math.multiply((1-(1+4*this.volume/math.sum(this.Fs)/Pe)^0.5)/Pe, [BC_gradient], new_state)[0]; - new_state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); - - } else { // Neumann BC (no flux) - new_state[0] = new_state[1]; - } - // Neumann BC (no flux) - new_state[this.n_x-1] = new_state[this.n_x-2] + stateNew = this._applyBoundaryConditions(stateNew); - assertNoNaN(new_state, "new state post BC"); + assertNoNaN(stateNew, "new state post BC"); - this.state = this._arrayClip2Zero(new_state); - return new_state; + this.state = this._arrayClip2Zero(stateNew); + return stateNew; } /** @@ -233,35 +242,35 @@ class Reactor_PFR extends Reactor { * @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 + _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 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; + 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[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); + 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 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); + D[this.n_x - 1] = Array(this.n_x).fill(0); return D; } } @@ -276,7 +285,7 @@ class Reactor_PFR extends Reactor { 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); + D2[this.n_x - 1] = Array(this.n_x).fill(0); return D2; } } From 4b49d1076368015a85b2eab2b38cee006aba4475 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 15:48:05 +0200 Subject: [PATCH 55/97] Fixed bug in NaN assertion --- src/reactor_class.js | 52 +++++++++++++++++++++++++------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/src/reactor_class.js b/src/reactor_class.js index d483201..d41fd39 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -13,8 +13,14 @@ const math = create(all, config) * @param {string} label */ function assertNoNaN(arr, label = "array") { - if (math.isNaN(arr)) { - throw new Error("NaN detected in ${label}!"); + if (Array.isArray(arr)) { + for (const el of arr) { + assertNoNaN(el, label); + } + } else { + if (Number.isNaN(arr)) { + throw new Error(`NaN detected in ${label}!`); + } } } @@ -80,7 +86,7 @@ class Reactor { */ _arrayClip2Zero(arr) { if (Array.isArray(arr)) { - return arr.map(clipToZero); + return arr.map(x => this._arrayClip2Zero(x)); } else { return arr < 0 ? 0 : arr; } @@ -166,6 +172,9 @@ class Reactor_PFR extends Reactor { this.D_op = this._makeDoperator(true, true); this.D2_op = this._makeD2operator(); + + assertNoNaN(this.D_op, "Derivative operator"); + assertNoNaN(this.D2_op, "Second derivative operator"); } /** @@ -184,7 +193,7 @@ class Reactor_PFR extends Reactor { return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; } - _applyBoundaryConditions(newState) { + _applyBoundaryConditions(state) { // apply boundary conditions 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]; @@ -192,14 +201,13 @@ class Reactor_PFR extends Reactor { BC_gradient[0] = -1; BC_gradient[1] = 1; let Pe = this.length * math.sum(this.Fs) / (this.D * this.A) - const BC_dispersion = math.multiply((1 - (1 + 4 * this.volume / math.sum(this.Fs) / Pe) ^ 0.5) / Pe, [BC_gradient], stateNew)[0]; - newState[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); + const BC_dispersion = math.multiply((1 - (1 + 4 * this.volume / math.sum(this.Fs) / Pe) ^ 0.5) / Pe, [BC_gradient], state)[0]; + state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); } else { // Neumann BC (no flux) - newState[0] = newState[1]; + state[0] = state[1]; } // Neumann BC (no flux) - newState[this.n_x - 1] = newState[this.n_x - 2] - return newState + state[this.n_x - 1] = state[this.n_x - 2] } /** @@ -224,11 +232,11 @@ class Reactor_PFR extends Reactor { } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); - let stateNew = math.add(this.state, dC_total); + const stateNew = math.add(this.state, dC_total); assertNoNaN(stateNew, "new state"); - stateNew = this._applyBoundaryConditions(stateNew); + this._applyBoundaryConditions(stateNew); assertNoNaN(stateNew, "new state post BC"); @@ -245,22 +253,22 @@ class Reactor_PFR extends Reactor { _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 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; + 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[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); + 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."); From 6755f2bd28b98dfd26f0107650bf950d0a9d94f5 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 16:03:42 +0200 Subject: [PATCH 56/97] Refactor reactor class to improve NaN handling and removed magic numbers --- src/reactor_class.js | 41 +++++++++++++++++++++++------------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/reactor_class.js b/src/reactor_class.js index d41fd39..2062d8f 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -1,11 +1,14 @@ -const ASM3 = require('./asm3_class.js') -const { create, all } = require('mathjs') +const ASM3 = require('./asm3_class.js'); +const { create, all } = require('mathjs'); const config = { matrix: 'Array' // use Array as the matrix type -} +}; -const math = create(all, config) +const math = create(all, config); + +const OXYGEN_INDEX = 0; +const NUM_SPECIES = 13; /** * Assert that no NaN values are present in an array. @@ -35,7 +38,7 @@ class Reactor { this.Vl = config.volume; // fluid volume reactor [m3] this.Fs = Array(config.n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] - this.Cs_in = Array.from(Array(config.n_inlets), () => new Array(13).fill(0.0)); // composition influents + this.Cs_in = Array.from(Array(config.n_inlets), () => new Array(NUM_SPECIES).fill(0.0)); // composition influents this.OTR = 0.0; // oxygen transfer rate [g O2 d-1] this.kla = config.kla; // if NaN, use externaly provided OTR [d-1] @@ -136,15 +139,17 @@ class Reactor_CSTR extends Reactor { * @returns {Array} - New reactor state. */ tick(time_step) { // tick reactor state using forward Euler method - const r = this.asm.compute_dC(this.state); - const dC_in = math.multiply(math.divide([this.Fs], this.Vl), this.Cs_in)[0]; - const dC_out = math.multiply(-1 * math.sum(this.Fs) / this.Vl, this.state); - const t_O = Array(13).fill(0.0); - t_O[0] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[0]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR + const inflow = math.multiply(math.divide([this.Fs], this.Vl), this.Cs_in)[0]; + const outflow = math.multiply(-1 * math.sum(this.Fs) / this.Vl, this.state); + const reaction = this.asm.compute_dC(this.state); + const transfer = Array(NUM_SPECIES).fill(0.0); + transfer[OXYGEN_INDEX] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[OXYGEN_INDEX]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR - const dC_total = math.multiply(math.add(dC_in, dC_out, r, t_O), time_step); + const dC_total = math.multiply(math.add(inflow, outflow, reaction, transfer), time_step); + assertNoNaN(dC_total, "change in state"); this.state = this._arrayClip2Zero(math.add(this.state, dC_total)); // clip value element-wise to avoid negative concentrations + assertNoNaN(this.state, "new state"); return this.state; } } @@ -171,9 +176,9 @@ class Reactor_PFR extends Reactor { this.D = 0.0; // axial dispersion [m2 d-1] this.D_op = this._makeDoperator(true, true); - this.D2_op = this._makeD2operator(); - assertNoNaN(this.D_op, "Derivative operator"); + + this.D2_op = this._makeD2operator(); assertNoNaN(this.D2_op, "Second derivative operator"); } @@ -219,25 +224,25 @@ class Reactor_PFR extends Reactor { 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)); - const transfer = Array.from(Array(this.n_x), () => new Array(13).fill(0.0)); + const transfer = Array.from(Array(this.n_x), () => new Array(NUM_SPECIES).fill(0.0)); assertNoNaN(dispersion, "dispersion"); assertNoNaN(advection, "advection"); assertNoNaN(reaction, "reaction"); if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR - transfer.forEach((x) => { x[0] = this.OTR; }); + transfer.forEach((x) => { x[OXYGEN_INDEX] = this.OTR; }); } else { - transfer.forEach((x, i) => { x[0] = this._calcOTR(this.state[i][0]); }); + transfer.forEach((x, i) => { x[OXYGEN_INDEX] = this._calcOTR(this.state[i][OXYGEN_INDEX]); }); } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); - const stateNew = math.add(this.state, dC_total); + assertNoNaN(dC_total, "change in state"); + const stateNew = math.add(this.state, dC_total); assertNoNaN(stateNew, "new state"); this._applyBoundaryConditions(stateNew); - assertNoNaN(stateNew, "new state post BC"); this.state = this._arrayClip2Zero(stateNew); From a2cfb20e2ca405671ca86b99c6b3a92b93ee6038 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 16:28:35 +0200 Subject: [PATCH 57/97] Refactor reactor class to improve NaN handling and add utility function for NaN assertions --- src/reactor_class.js | 71 +++++++++++++++++++------------------------- src/utils.js | 18 +++++++++++ 2 files changed, 48 insertions(+), 41 deletions(-) create mode 100644 src/utils.js diff --git a/src/reactor_class.js b/src/reactor_class.js index 2062d8f..dd5daf7 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -1,5 +1,6 @@ const ASM3 = require('./asm3_class.js'); const { create, all } = require('mathjs'); +const { assertNoNaN } = require('./utils.js'); const config = { matrix: 'Array' // use Array as the matrix type @@ -7,26 +8,9 @@ const config = { const math = create(all, config); -const OXYGEN_INDEX = 0; +const S_O_INDEX = 0; const NUM_SPECIES = 13; -/** - * 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}!`); - } - } -} - class Reactor { /** * Reactor base class. @@ -35,16 +19,16 @@ class Reactor { constructor(config) { this.asm = new ASM3(); - this.Vl = config.volume; // fluid volume reactor [m3] + this.volume = config.volume; // fluid volume reactor [m3] - this.Fs = Array(config.n_inlets).fill(0.0); // fluid debits per inlet [m3 d-1] - this.Cs_in = Array.from(Array(config.n_inlets), () => new Array(NUM_SPECIES).fill(0.0)); // composition influents + 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] 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 * 15); // time step [d] + this.timeStep = 1 / (24*60*15); // time step [d] this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second } @@ -78,7 +62,7 @@ class Reactor { * @returns */ _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; + 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); } @@ -102,7 +86,7 @@ class Reactor { updateState(newTime) { // expect update with timestamp const day2ms = 1000 * 60 * 60 * 24; - let n_iter = Math.floor(this.speedUpFactor * (newTime - this.currentTime) / (this.timeStep * day2ms)); + let n_iter = Math.floor(this.speedUpFactor * (newTime-this.currentTime) / (this.timeStep*day2ms)); if (n_iter) { let n = 0; while (n < n_iter) { @@ -139,11 +123,11 @@ class Reactor_CSTR extends Reactor { * @returns {Array} - New reactor state. */ tick(time_step) { // tick reactor state using forward Euler method - const inflow = math.multiply(math.divide([this.Fs], this.Vl), this.Cs_in)[0]; - const outflow = math.multiply(-1 * math.sum(this.Fs) / this.Vl, this.state); + 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); const transfer = Array(NUM_SPECIES).fill(0.0); - transfer[OXYGEN_INDEX] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[OXYGEN_INDEX]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR + transfer[S_O_INDEX] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[S_O_INDEX]); // 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); assertNoNaN(dC_total, "change in state"); @@ -166,7 +150,7 @@ class Reactor_PFR extends Reactor { this.n_x = config.resolution_L; // number of slices this.d_x = this.length / this.n_x; - this.A = this.Vl / this.length; // crosssectional area [m2] + this.A = this.volume / this.length; // crosssectional area [m2] this.state = Array.from(Array(this.n_x), () => config.initialState.slice()) @@ -177,7 +161,7 @@ class Reactor_PFR extends Reactor { 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"); } @@ -198,21 +182,26 @@ class Reactor_PFR extends Reactor { return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; } + /** + * 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) { - // apply boundary conditions 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_gradient = Array(this.n_x).fill(0.0); + const BC_gradient = Array(this.n_x).fill(0); BC_gradient[0] = -1; BC_gradient[1] = 1; let Pe = this.length * math.sum(this.Fs) / (this.D * this.A) - const BC_dispersion = math.multiply((1 - (1 + 4 * this.volume / math.sum(this.Fs) / Pe) ^ 0.5) / Pe, [BC_gradient], state)[0]; + const BC_dispersion = math.multiply((1 - (1 + 4*this.volume/math.sum(this.Fs)/Pe)^0.5) / Pe, [BC_gradient], state)[0]; state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); } else { // Neumann BC (no flux) state[0] = state[1]; } // Neumann BC (no flux) - state[this.n_x - 1] = state[this.n_x - 2] + state[this.n_x-1] = state[this.n_x-2] } /** @@ -221,19 +210,19 @@ class Reactor_PFR extends Reactor { * @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 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)); - const transfer = Array.from(Array(this.n_x), () => new Array(NUM_SPECIES).fill(0.0)); + const transfer = Array.from(Array(this.n_x), () => new Array(NUM_SPECIES).fill(0)); assertNoNaN(dispersion, "dispersion"); assertNoNaN(advection, "advection"); assertNoNaN(reaction, "reaction"); if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR - transfer.forEach((x) => { x[OXYGEN_INDEX] = this.OTR; }); + transfer.forEach((x) => { x[S_O_INDEX] = this.OTR; }); } else { - transfer.forEach((x, i) => { x[OXYGEN_INDEX] = this._calcOTR(this.state[i][OXYGEN_INDEX]); }); + transfer.forEach((x, i) => { x[S_O_INDEX] = this._calcOTR(this.state[i][S_O_INDEX]); }); } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); @@ -279,11 +268,11 @@ class Reactor_PFR extends Reactor { 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 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); + D[this.n_x-1] = Array(this.n_x).fill(0); return D; } } diff --git a/src/utils.js b/src/utils.js new file mode 100644 index 0000000..2ca1896 --- /dev/null +++ b/src/utils.js @@ -0,0 +1,18 @@ +/** + * Assert that no NaN values are present in an array. + * @param {Array} arr + * @param {string} label + */ +function assertNoNaN(arr, label = "array") { + if (Array.isArray(arr)) { + for (const el of arr) { + assertNoNaN(el, label); + } + } else { + if (Number.isNaN(arr)) { + throw new Error(`NaN detected in ${label}!`); + } + } +} + +module.exports = { assertNoNaN }; \ No newline at end of file From fe3add40071e9ecb1c9f02cedd9061a4e7139c12 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 4 Jul 2025 17:42:31 +0200 Subject: [PATCH 58/97] Add debug assertions for state changes in Reactor classes --- src/reactor_class.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/src/reactor_class.js b/src/reactor_class.js index dd5daf7..d12e158 100644 --- a/src/reactor_class.js +++ b/src/reactor_class.js @@ -10,6 +10,7 @@ const math = create(all, config); const S_O_INDEX = 0; const NUM_SPECIES = 13; +const DEBUG = false; class Reactor { /** @@ -129,11 +130,12 @@ class Reactor_CSTR extends Reactor { const transfer = Array(NUM_SPECIES).fill(0.0); transfer[S_O_INDEX] = isNaN(this.kla) ? this.OTR : this._calcOTR(this.state[S_O_INDEX]); // 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); - assertNoNaN(dC_total, "change in state"); - + 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 - assertNoNaN(this.state, "new state"); + if(DEBUG){ + assertNoNaN(dC_total, "change in state"); + assertNoNaN(this.state, "new state"); + } return this.state; } } @@ -194,8 +196,9 @@ class Reactor_PFR extends Reactor { const BC_gradient = Array(this.n_x).fill(0); BC_gradient[0] = -1; BC_gradient[1] = 1; - let Pe = this.length * math.sum(this.Fs) / (this.D * this.A) - const BC_dispersion = math.multiply((1 - (1 + 4*this.volume/math.sum(this.Fs)/Pe)^0.5) / Pe, [BC_gradient], state)[0]; + let Pe = this.length * math.sum(this.Fs) / (this.D * this.A); + let residence_time = this.volume/math.sum(this.Fs); + const BC_dispersion = math.multiply((1 - (1 + 4*residence_time/Pe)^0.5) / (Pe*this.d_x), [BC_gradient], state)[0]; state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); } else { // Neumann BC (no flux) state[0] = state[1]; @@ -215,10 +218,6 @@ class Reactor_PFR extends Reactor { const reaction = this.state.map((state_slice) => this.asm.compute_dC(state_slice)); const transfer = Array.from(Array(this.n_x), () => new Array(NUM_SPECIES).fill(0)); - assertNoNaN(dispersion, "dispersion"); - assertNoNaN(advection, "advection"); - assertNoNaN(reaction, "reaction"); - if (isNaN(this.kla)) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR transfer.forEach((x) => { x[S_O_INDEX] = this.OTR; }); } else { @@ -226,13 +225,17 @@ class Reactor_PFR extends Reactor { } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); - assertNoNaN(dC_total, "change in state"); const stateNew = math.add(this.state, dC_total); - assertNoNaN(stateNew, "new state"); - this._applyBoundaryConditions(stateNew); - assertNoNaN(stateNew, "new state post BC"); + + if (DEBUG) { + assertNoNaN(dispersion, "dispersion"); + assertNoNaN(advection, "advection"); + assertNoNaN(reaction, "reaction"); + assertNoNaN(dC_total, "change in state"); + assertNoNaN(stateNew, "new state post BC"); + } this.state = this._arrayClip2Zero(stateNew); return stateNew; From b4ddb6b8dfbb431bf23f9e07840f39018a400144 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 7 Jul 2025 11:08:11 +0200 Subject: [PATCH 59/97] Enhance documentation for ASM3 class and its parameters --- src/asm3_class.js | 45 ++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 40 insertions(+), 5 deletions(-) diff --git a/src/asm3_class.js b/src/asm3_class.js index f5ac1df..ab272dc 100644 --- a/src/asm3_class.js +++ b/src/asm3_class.js @@ -1,11 +1,16 @@ 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 = { - // Kinetic parameters (20 C for now) - // 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] @@ -31,9 +36,13 @@ class ASM3 { b_A_O: 0.15, // aerobic respiration rate [d-1] b_A_NO: 0.05 // anoxic respiration rate [d-1] }; - this.stoi_params = { - // Stoichiometric and composition parameters + /** + * 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 @@ -63,6 +72,10 @@ class ASM3 { 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; @@ -84,15 +97,32 @@ class ASM3 { 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); } - compute_rates(state) { // computes reaction rates. state is optional + /** + * Computes the reaction rates for each process reaction based on the current state. + * @param {Array} state - State vector containing concentrations of reaction species. + * @returns {Array} - Reaction rates for each process reaction. + */ + compute_rates(state) { // 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; @@ -119,6 +149,11 @@ class ASM3 { return rates; } + /** + * Computes the change in concentrations of reaction species based on the current state. + * @param {Array} state - State vector containing concentrations of reaction species. + * @returns {Array} - Change in reaction species concentrations. + */ compute_dC(state) { // 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)); From deb5269d1a59d7eeb14e19ad7aed9c6514d08537 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 7 Jul 2025 11:59:11 +0200 Subject: [PATCH 60/97] Change file structure to align with project --- src/nodeClass.js | 2 +- src/{ => reaction_modules}/asm3_class.js | 0 src/{reactor_class.js => specificClass.js} | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename src/{ => reaction_modules}/asm3_class.js (100%) rename src/{reactor_class.js => specificClass.js} (99%) diff --git a/src/nodeClass.js b/src/nodeClass.js index a873284..7919df2 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -1,4 +1,4 @@ -const { Reactor_CSTR, Reactor_PFR } = require('./reactor_class.js'); +const { Reactor_CSTR, Reactor_PFR } = require('./specificClass.js'); class nodeClass { diff --git a/src/asm3_class.js b/src/reaction_modules/asm3_class.js similarity index 100% rename from src/asm3_class.js rename to src/reaction_modules/asm3_class.js diff --git a/src/reactor_class.js b/src/specificClass.js similarity index 99% rename from src/reactor_class.js rename to src/specificClass.js index d12e158..ce5c786 100644 --- a/src/reactor_class.js +++ b/src/specificClass.js @@ -1,4 +1,4 @@ -const ASM3 = require('./asm3_class.js'); +const ASM3 = require('./reaction_modules/asm3_class.js'); const { create, all } = require('mathjs'); const { assertNoNaN } = require('./utils.js'); From 302780726a8b6d850f7dd4b64440c83d63dcbd41 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 7 Jul 2025 12:24:15 +0200 Subject: [PATCH 61/97] Refactor Reactor class to remove debug logs and enhance setter for influent data with conditional logging --- src/specificClass.js | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index ce5c786..384aaf0 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -41,11 +41,6 @@ class Reactor { let index_in = input.payload.inlet; this.Fs[index_in] = input.payload.F; this.Cs_in[index_in] = input.payload.C; - // DEBUG - // console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); - // console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); - // console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); - // console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); } /** @@ -57,10 +52,10 @@ class Reactor { } /** - * + * 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 + * @returns {number} - Calculated OTR [g O2 d-1]. */ _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; @@ -176,6 +171,20 @@ class Reactor_PFR extends Reactor { this.D = input.payload; } + /** + * Setter for influent data. + * @param {object} input - Input object (msg) containing payload with inlet index, flow rate, and concentrations. + */ + set setInfluent(input) { + super.setInfluent = input; + if(DEBUG) { + console.log("Pe total " + this.length*math.sum(this.Fs)/(this.D*this.A)); + console.log("Pe local " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); + console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); + console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); + } + } + /** * Getter for effluent data. * @returns {object} Effluent data object (msg), defaults to inlet 0. From 01380c309f757622c5ad194f42ee938cedde2bc7 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 7 Jul 2025 14:47:50 +0200 Subject: [PATCH 62/97] Add boundary condition input and update reactor configuration handling --- advanced-reactor.html | 17 +++++++++++++++++ src/nodeClass.js | 1 + src/specificClass.js | 27 ++++++++++++++++++++++----- 3 files changed, 40 insertions(+), 5 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index 512f3dd..6594947 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -8,6 +8,7 @@ volume: { value: 0., required: true }, length: { value: 0.}, resolution_L: { value: 0.}, + boundary_condition: {value: "Generalised"}, n_inlets: { value: 1, required: true}, kla: { value: null }, S_O_init: { value: 0., required: true }, @@ -75,6 +76,18 @@ $(".PFR").show(); } }); + $("#node-input-boundary_condition").typedInput({ + types: [ + { + value: "Generalised", + options: [ + { value: "Generalised", label: "Generalised"}, + { value: "Diriclet", label: "Diriclet"}, + { value: "Danckwerts", label: "Danckwerts"} + ] + } + ] + }) // Set initial visibility on dialog open const initialType = $("#node-input-reactor_type").typedInput("value"); if (initialType === "CSTR") { @@ -117,6 +130,10 @@
+
+
+ +
diff --git a/src/nodeClass.js b/src/nodeClass.js index 7919df2..e3c3fd6 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -70,6 +70,7 @@ class nodeClass { volume: parseFloat(uiConfig.volume), length: parseFloat(uiConfig.length), resolution_L: parseInt(uiConfig.resolution_L), + boundary_condition: uiConfig.boundary_condition, n_inlets: parseInt(uiConfig.n_inlets), kla: parseFloat(uiConfig.kla), initialState: [ diff --git a/src/specificClass.js b/src/specificClass.js index 384aaf0..e670eb5 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -10,7 +10,7 @@ const math = create(all, config); const S_O_INDEX = 0; const NUM_SPECIES = 13; -const DEBUG = false; +const DEBUG = true; class Reactor { /** @@ -30,7 +30,7 @@ class Reactor { this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1 / (24*60*15); // time step [d] - this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second + this.speedUpFactor = 1; // speed up factor for simulation, 60 means 1 minute per simulated second } /** @@ -149,6 +149,8 @@ class Reactor_PFR extends Reactor { this.d_x = this.length / this.n_x; this.A = this.volume / this.length; // crosssectional area [m2] + this.BC = config.boundary_condition; + this.state = Array.from(Array(this.n_x), () => config.initialState.slice()) // console.log("Initial State: ") @@ -178,6 +180,7 @@ class Reactor_PFR extends Reactor { set setInfluent(input) { super.setInfluent = input; 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 " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); @@ -205,9 +208,23 @@ class Reactor_PFR extends Reactor { const BC_gradient = Array(this.n_x).fill(0); BC_gradient[0] = -1; BC_gradient[1] = 1; - let Pe = this.length * math.sum(this.Fs) / (this.D * this.A); - let residence_time = this.volume/math.sum(this.Fs); - const BC_dispersion = math.multiply((1 - (1 + 4*residence_time/Pe)^0.5) / (Pe*this.d_x), [BC_gradient], state)[0]; + let BC_term = 0; + switch(this.BC) { + case "Diriclet": + BC_term = 0; + break; + case "Danckwerts": + BC_term = this.D*this.A / math.sum(this.Fs); + break; + case "Generalised": + let Pe = this.length * math.sum(this.Fs) / (this.D * this.A); + let residence_time = this.volume/math.sum(this.Fs); + BC_term = (1 - (1 + 4*residence_time/Pe)^0.5) / Pe + break; + default: + console.warn("Unknown boundary condition: " + this.BC); + } + const BC_dispersion = math.multiply(BC_term/this.d_x, [BC_gradient], state)[0]; state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); } else { // Neumann BC (no flux) state[0] = state[1]; From 01318a2d3b2d968f2efed840e6dfe2dd08d73971 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 7 Jul 2025 14:58:52 +0200 Subject: [PATCH 63/97] Fix spelling of "Dirichlet" in advanced-reactor.html and specificClass.js --- advanced-reactor.html | 2 +- src/specificClass.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index 6594947..9ea7e23 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -82,7 +82,7 @@ value: "Generalised", options: [ { value: "Generalised", label: "Generalised"}, - { value: "Diriclet", label: "Diriclet"}, + { value: "Dirichlet", label: "Dirichlet"}, { value: "Danckwerts", label: "Danckwerts"} ] } diff --git a/src/specificClass.js b/src/specificClass.js index e670eb5..1124b0d 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -210,7 +210,7 @@ class Reactor_PFR extends Reactor { BC_gradient[1] = 1; let BC_term = 0; switch(this.BC) { - case "Diriclet": + case "Dirichlet": BC_term = 0; break; case "Danckwerts": From 5c03dddb7923066f705edbd490afd49388c511bf Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 8 Jul 2025 10:03:03 +0200 Subject: [PATCH 64/97] Refactor boundary condition handling to use adjustable parameter alpha in advanced-reactor and specificClass --- advanced-reactor.html | 25 ++++++++++--------------- src/nodeClass.js | 2 +- src/specificClass.js | 20 ++------------------ 3 files changed, 13 insertions(+), 34 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index 9ea7e23..40ea4f6 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -8,7 +8,7 @@ volume: { value: 0., required: true }, length: { value: 0.}, resolution_L: { value: 0.}, - boundary_condition: {value: "Generalised"}, + alpha: {value: 0}, n_inlets: { value: 1, required: true}, kla: { value: null }, S_O_init: { value: 0., required: true }, @@ -76,17 +76,9 @@ $(".PFR").show(); } }); - $("#node-input-boundary_condition").typedInput({ - types: [ - { - value: "Generalised", - options: [ - { value: "Generalised", label: "Generalised"}, - { value: "Dirichlet", label: "Dirichlet"}, - { value: "Danckwerts", label: "Danckwerts"} - ] - } - ] + $("#node-input-alpha").typedInput({ + type:"num", + types:["num"] }) // Set initial visibility on dialog open const initialType = $("#node-input-reactor_type").typedInput("value"); @@ -131,9 +123,12 @@
-
- - +
+

Inlet boundary condition parameter α (α = 0: Danckwerts BC / α = 1: Dirichlet BC)

+
+ + +
diff --git a/src/nodeClass.js b/src/nodeClass.js index e3c3fd6..7ec81d3 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -70,7 +70,7 @@ class nodeClass { volume: parseFloat(uiConfig.volume), length: parseFloat(uiConfig.length), resolution_L: parseInt(uiConfig.resolution_L), - boundary_condition: uiConfig.boundary_condition, + alpha: parseFloat(uiConfig.alpha), n_inlets: parseInt(uiConfig.n_inlets), kla: parseFloat(uiConfig.kla), initialState: [ diff --git a/src/specificClass.js b/src/specificClass.js index 1124b0d..18af512 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -149,7 +149,7 @@ class Reactor_PFR extends Reactor { this.d_x = this.length / this.n_x; this.A = this.volume / this.length; // crosssectional area [m2] - this.BC = config.boundary_condition; + this.alpha = config.alpha; this.state = Array.from(Array(this.n_x), () => config.initialState.slice()) @@ -208,23 +208,7 @@ class Reactor_PFR extends Reactor { const BC_gradient = Array(this.n_x).fill(0); BC_gradient[0] = -1; BC_gradient[1] = 1; - let BC_term = 0; - switch(this.BC) { - case "Dirichlet": - BC_term = 0; - break; - case "Danckwerts": - BC_term = this.D*this.A / math.sum(this.Fs); - break; - case "Generalised": - let Pe = this.length * math.sum(this.Fs) / (this.D * this.A); - let residence_time = this.volume/math.sum(this.Fs); - BC_term = (1 - (1 + 4*residence_time/Pe)^0.5) / Pe - break; - default: - console.warn("Unknown boundary condition: " + this.BC); - } - const BC_dispersion = math.multiply(BC_term/this.d_x, [BC_gradient], state)[0]; + const BC_dispersion = math.multiply((1 - this.alpha) * this.D*this.A / (math.sum(this.Fs) * this.d_x), [BC_gradient], state)[0]; state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); } else { // Neumann BC (no flux) state[0] = state[1]; From c1e331b5f0134070b51d6a3f2762442d1aa87543 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 8 Jul 2025 11:02:39 +0200 Subject: [PATCH 65/97] Add temperature theta parameters and adjust reaction rate calculations in ASM3 class --- src/reaction_modules/asm3_class.js | 88 +++++++++++++++++++++++------- 1 file changed, 68 insertions(+), 20 deletions(-) diff --git a/src/reaction_modules/asm3_class.js b/src/reaction_modules/asm3_class.js index ab272dc..d228619 100644 --- a/src/reaction_modules/asm3_class.js +++ b/src/reaction_modules/asm3_class.js @@ -69,6 +69,28 @@ class ASM3 { 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(); } @@ -103,7 +125,7 @@ class ASM3 { * @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){ + _monod(c, K) { return c / (K + c); } @@ -113,50 +135,76 @@ class ASM3 { * @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){ + _inv_monod(c, K) { return K / (K + c); } /** - * Computes the reaction rates for each process reaction based on the current state. + * 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) { + 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 : k_H * this._monod(X_S / X_H, K_X) * X_H; + 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] = k_STO * this._monod(S_O, K_O) * this._monod(S_S, K_S) * X_H; - rates[2] = k_STO * 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 : mu_H_max * 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 : mu_H_max * 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] = b_H_O * this._monod(S_O, K_O) * X_H; - rates[6] = b_H_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_H; - rates[7] = b_STO_O * this._monod(S_O, K_O) * X_H; - rates[8] = b_STO_NO * this._inv_monod(S_O, K_O) * this._monod(S_NO, K_NO) * X_STO; + 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] = mu_A_max * 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] = b_A_O * this._monod(S_O, K_O) * X_A; - rates[11] = b_A_NO * this._inv_monod(S_O, K_A_O) * this._monod(S_NO, K_NO) * X_A; + 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. + * 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) { // compute changes in 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)); + return math.multiply(this.stoi_matrix, this.compute_rates(state, T)); } } From c566766c4de982f6416ca87fd48d9855f7c19390 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 8 Jul 2025 15:29:55 +0200 Subject: [PATCH 66/97] Refactor boundary condition application in Reactor_PFR class for improved clarity and efficiency --- src/specificClass.js | 39 ++++++++++++++++++--------------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index 18af512..1d715a7 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -196,27 +196,6 @@ class Reactor_PFR extends Reactor { return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; } - /** - * 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_gradient = Array(this.n_x).fill(0); - BC_gradient[0] = -1; - BC_gradient[1] = 1; - const BC_dispersion = math.multiply((1 - this.alpha) * this.D*this.A / (math.sum(this.Fs) * this.d_x), [BC_gradient], state)[0]; - state[0] = math.add(BC_C_in, BC_dispersion).map(val => val < 0 ? 0 : val); - } else { // Neumann BC (no flux) - state[0] = state[1]; - } - // Neumann BC (no flux) - state[this.n_x-1] = state[this.n_x-2] - } - /** * Tick the reactor state using explicit finite difference method. * @param {number} time_step - Time step for the simulation [d]. @@ -251,6 +230,24 @@ class Reactor_PFR extends Reactor { return stateNew; } + /** + * 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. From 0efa76fa6ac360ee950f01ec70267e586ed7be5a Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 8 Jul 2025 15:41:41 +0200 Subject: [PATCH 67/97] Reset speedUpFactor in Reactor class for simulation acceleration, disable debug --- src/specificClass.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index 1d715a7..a41acec 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -10,7 +10,7 @@ const math = create(all, config); const S_O_INDEX = 0; const NUM_SPECIES = 13; -const DEBUG = true; +const DEBUG = false; class Reactor { /** @@ -30,7 +30,7 @@ class Reactor { this.currentTime = Date.now(); // milliseconds since epoch [ms] this.timeStep = 1 / (24*60*15); // time step [d] - this.speedUpFactor = 1; // speed up factor for simulation, 60 means 1 minute per simulated second + this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second } /** From 0f912b05e4060029afa53a90e1c223186588f707 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 9 Jul 2025 10:29:54 +0200 Subject: [PATCH 68/97] Add temperature handling --- src/nodeClass.js | 3 +++ src/specificClass.js | 17 +++++++++++++---- 2 files changed, 16 insertions(+), 4 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 7ec81d3..2e00ced 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -42,6 +42,9 @@ class nodeClass { case "OTR": this.reactor.setOTR = msg; break; + case "Temperature": + this.reactor.setTemperature = msg; + break; case "Dispersion": this.reactor.setDispersion = msg; break; diff --git a/src/specificClass.js b/src/specificClass.js index a41acec..efe0f1d 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -25,6 +25,7 @@ class Reactor { 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] + this.temperature = 20; // temperature [C] this.kla = config.kla; // if NaN, use externaly provided OTR [d-1] @@ -51,6 +52,14 @@ class Reactor { this.OTR = input.payload; } + /** + * Setter for temperature. + * @param {object} input - Input object (msg) containing payload with temperature value [C]. + */ + set setTemperature(input) { + this.temperature = input.payload; + } + /** * 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]. @@ -121,9 +130,9 @@ class Reactor_CSTR extends Reactor { 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); + 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]); // calculate OTR if kla is not NaN, otherwise use externaly calculated OTR + 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 @@ -204,13 +213,13 @@ class Reactor_PFR extends Reactor { 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)); + 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 transfer.forEach((x) => { x[S_O_INDEX] = this.OTR; }); } else { - transfer.forEach((x, i) => { x[S_O_INDEX] = this._calcOTR(this.state[i][S_O_INDEX]); }); + transfer.forEach((x, i) => { x[S_O_INDEX] = this._calcOTR(this.state[i][S_O_INDEX], this.temperature); }); } const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); From 6fd86f71c8c26efcc4b8b4fe9e14ebec3a905ab2 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 9 Jul 2025 15:37:29 +0200 Subject: [PATCH 69/97] Switched around Debug point and added checks for Pe and Co_D --- src/specificClass.js | 32 +++++++++++++++++--------------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index efe0f1d..62236d1 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -182,21 +182,6 @@ class Reactor_PFR extends Reactor { this.D = input.payload; } - /** - * Setter for influent data. - * @param {object} input - Input object (msg) containing payload with inlet index, flow rate, and concentrations. - */ - set setInfluent(input) { - super.setInfluent = input; - 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 " + this.d_x*math.sum(this.Fs)/(this.D*this.A)); - console.log("Co ad " + math.sum(this.Fs)*this.timeStep/(this.A*this.d_x)); - console.log("Co D " + this.D*this.timeStep/(this.d_x*this.d_x)); - } - } - /** * Getter for effluent data. * @returns {object} Effluent data object (msg), defaults to inlet 0. @@ -205,6 +190,23 @@ class Reactor_PFR extends Reactor { return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; } + 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) && console.warn(`Local Péclet number (${Pe_local}) is too high! Increase reactor resolution.`); + (Co_D >= 0.5) && console.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]. From 24de5a4c9f2129e1443a5f90ad4ca438c98ae79e Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 11 Jul 2025 12:22:36 +0200 Subject: [PATCH 70/97] Add generalFunctions dependency and implement basic measurement child registration in nodeClass --- package-lock.json | 6 + package.json | 1 + src/nodeClass.js | 6 + src/specificClass.js | 545 ++++++++++++++++++++++--------------------- 4 files changed, 294 insertions(+), 264 deletions(-) diff --git a/package-lock.json b/package-lock.json index e2e89fc..19b3188 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,6 +9,7 @@ "version": "0.0.1", "license": "SEE LICENSE", "dependencies": { + "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/p.vanderwilt/generalFunctions.git", "mathjs": "^14.5.2" } }, @@ -59,6 +60,11 @@ "url": "https://github.com/sponsors/rawify" } }, + "node_modules/generalFunctions": { + "version": "1.0.0", + "resolved": "git+https://gitea.centraal.wbd-rd.nl/p.vanderwilt/generalFunctions.git#950ca2b6b4e91b37479aee90bff74b02c16f130e", + "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", diff --git a/package.json b/package.json index e60fa97..b6d04c5 100644 --- a/package.json +++ b/package.json @@ -27,6 +27,7 @@ } }, "dependencies": { + "generalFunctions": "git+https://gitea.centraal.wbd-rd.nl/p.vanderwilt/generalFunctions.git", "mathjs": "^14.5.2" } } diff --git a/src/nodeClass.js b/src/nodeClass.js index 2e00ced..f4bbe25 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -48,6 +48,12 @@ class nodeClass { case "Dispersion": this.reactor.setDispersion = msg; break; + case 'registerChild': + // Register this node as a child of the parent node + const childId = msg.payload; + const childObj = this.RED.nodes.getNode(childId); + this.reactor.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); + break; default: console.log("Unknown topic: " + msg.topic); } diff --git a/src/specificClass.js b/src/specificClass.js index 62236d1..509f460 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,9 +1,10 @@ const ASM3 = require('./reaction_modules/asm3_class.js'); const { create, all } = require('mathjs'); const { assertNoNaN } = require('./utils.js'); +const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions'); const config = { - matrix: 'Array' // use Array as the matrix type + matrix: 'Array' // use Array as the matrix type }; const math = create(all, config); @@ -13,304 +14,320 @@ const NUM_SPECIES = 13; const DEBUG = false; class Reactor { - /** - * Reactor base class. - * @param {object} config - Configuration object containing reactor parameters. - */ - constructor(config) { - this.asm = new ASM3(); + /** + * Reactor base class. + * @param {object} config - Configuration object containing reactor parameters. + */ + constructor(config) { + // EVOLV stuff + this.logger = new logger(); //TODO: attach config + this.measurements = new MeasurementContainer(); + this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility - this.volume = config.volume; // fluid volume reactor [m3] + this.asm = new ASM3(); - 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] - this.temperature = 20; // temperature [C] + this.volume = config.volume; // fluid volume reactor [m3] - this.kla = config.kla; // if NaN, use externaly provided OTR [d-1] + 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] + this.temperature = 20; // temperature [C] - this.currentTime = Date.now(); // milliseconds since epoch [ms] - this.timeStep = 1 / (24*60*15); // time step [d] - this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second + 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*15); // time step [d] + this.speedUpFactor = 60; // speed up factor for simulation, 60 means 1 minute per simulated second + } + + updateMeasurement(variant, subType, value, position) { + this.logger.debug(`---------------------- updating ${subType} ------------------ `); + switch (subType) { + case "temperature": + this.logger.debug(`no nothing`); + break; + default: + this.logger.error(`Type '${subType}' not recognized for measured update.`); + return; } + } - /** - * 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 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]. + */ + set setOTR(input) { + this.OTR = input.payload; + } + + /** + * Setter for temperature. + * @param {object} input - Input object (msg) containing payload with temperature value [C]. + */ + set setTemperature(input) { + this.temperature = input.payload; + } + + /** + * 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]. + */ + _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; } + } - /** - * Setter for OTR (Oxygen Transfer Rate). - * @param {object} input - Input object (msg) containing payload with OTR value [g O2 d-1]. - */ - set setOTR(input) { - this.OTR = input.payload; + /** + * 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) { // expect update with timestamp + const day2ms = 1000 * 60 * 60 * 24; + + 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; } - - /** - * Setter for temperature. - * @param {object} input - Input object (msg) containing payload with temperature value [C]. - */ - set setTemperature(input) { - this.temperature = input.payload; + this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; } - - /** - * 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]. - */ - _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; - } - } - - /** - * 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) { // expect update with timestamp - const day2ms = 1000 * 60 * 60 * 24; - - 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; - } - } - + } } 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; - } + /** + * Reactor_CSTR class for Continuous Stirred Tank Reactor. + * @param {object} config - Configuration object containing reactor parameters. + */ + constructor(config) { + super(config); + this.state = config.initialState; + } - /** - * Getter for effluent data. - * @returns {object} Effluent data object (msg), defaults to inlet 0. - */ - get getEffluent() { // getter for Effluent, defaults to inlet 0 - return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; - } + /** + * Getter for effluent data. + * @returns {object} Effluent data object (msg), defaults to inlet 0. + */ + get getEffluent() { // getter for Effluent, defaults to inlet 0 + return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; + } - /** - * 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 + /** + * 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; - } + 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); + /** + * 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.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.d_x = this.length / this.n_x; + this.A = this.volume / this.length; // crosssectional area [m2] - this.alpha = config.alpha; + this.alpha = config.alpha; - this.state = Array.from(Array(this.n_x), () => config.initialState.slice()) + this.state = Array.from(Array(this.n_x), () => config.initialState.slice()) - // console.log("Initial State: ") - // console.log(this.state) + // console.log("Initial State: ") + // console.log(this.state) - this.D = 0.0; // axial dispersion [m2 d-1] + this.D = 0.0; // axial dispersion [m2 d-1] - this.D_op = this._makeDoperator(true, true); - assertNoNaN(this.D_op, "Derivative operator"); + 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"); + this.D2_op = this._makeD2operator(); + assertNoNaN(this.D2_op, "Second derivative operator"); + } + + /** + * Setter for axial dispersion. + * @param {object} input - Input object (msg) containing payload with dispersion value [m2 d-1]. + */ + set setDispersion(input) { + this.D = input.payload; + } + + /** + * Getter for effluent data. + * @returns {object} Effluent data object (msg), defaults to inlet 0. + */ + get getEffluent() { + return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; + } + + 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) && console.warn(`Local Péclet number (${Pe_local}) is too high! Increase reactor resolution.`); + (Co_D >= 0.5) && console.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 + transfer.forEach((x) => { x[S_O_INDEX] = this.OTR; }); + } else { + transfer.forEach((x, i) => { x[S_O_INDEX] = this._calcOTR(this.state[i][S_O_INDEX], this.temperature); }); } - /** - * 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; + 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"); } - /** - * Getter for effluent data. - * @returns {object} Effluent data object (msg), defaults to inlet 0. - */ - get getEffluent() { - return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; + this.state = this._arrayClip2Zero(stateNew); + return stateNew; + } + + /** + * 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]; + } - 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) && console.warn(`Local Péclet number (${Pe_local}) is too high! Increase reactor resolution.`); - (Co_D >= 0.5) && console.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); - } + /** + * 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; } + } - /** - * 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 - transfer.forEach((x) => { x[S_O_INDEX] = this.OTR; }); - } else { - transfer.forEach((x, i) => { x[S_O_INDEX] = this._calcOTR(this.state[i][S_O_INDEX], this.temperature); }); - } - - const dC_total = math.multiply(math.add(dispersion, advection, reaction, transfer), time_step); - - const stateNew = math.add(this.state, dC_total); - this._applyBoundaryConditions(stateNew); - - if (DEBUG) { - assertNoNaN(dispersion, "dispersion"); - assertNoNaN(advection, "advection"); - assertNoNaN(reaction, "reaction"); - assertNoNaN(dC_total, "change in state"); - assertNoNaN(stateNew, "new state post BC"); - } - - this.state = this._arrayClip2Zero(stateNew); - return stateNew; - } - - /** - * 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; - } + /** + * 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 }; From 6227bbe2566bac8db43f06e9862a9940efd99373 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Fri, 11 Jul 2025 12:27:06 +0200 Subject: [PATCH 71/97] Update temperature handling in Reactor class and remove redundant setter --- src/specificClass.js | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/specificClass.js b/src/specificClass.js index 509f460..8fb1448 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -44,7 +44,7 @@ class Reactor { this.logger.debug(`---------------------- updating ${subType} ------------------ `); switch (subType) { case "temperature": - this.logger.debug(`no nothing`); + this.temperature = value; break; default: this.logger.error(`Type '${subType}' not recognized for measured update.`); @@ -70,14 +70,6 @@ class Reactor { this.OTR = input.payload; } - /** - * Setter for temperature. - * @param {object} input - Input object (msg) containing payload with temperature value [C]. - */ - set setTemperature(input) { - this.temperature = input.payload; - } - /** * 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]. From d5db1ae0a0fc4c00c4fb71761e29c4bbe8f07734 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 16 Jul 2025 10:57:35 +0200 Subject: [PATCH 72/97] Added seperate process, DB and parent outputs --- advanced-reactor.html | 5 +++-- src/nodeClass.js | 18 ++++++++++++++++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/advanced-reactor.html b/advanced-reactor.html index 40ea4f6..91df690 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -26,8 +26,9 @@ X_TS_init: { value: 125.0009, required: true } }, inputs: 1, - outputs: 1, - outputLabels: "Effluent", + outputs: 3, + inputLabels: ["input"], + outputLabels: ["process", "dbase", "parent"], icon: "font-awesome/fa-recycle", label: function() { return this.name || "advanced-reactor"; diff --git a/src/nodeClass.js b/src/nodeClass.js index f4bbe25..9eba995 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -16,7 +16,7 @@ class nodeClass { this.name = nameOfNode; this._loadConfig(uiConfig) - + this._registerChild(); this._setupClass(); this._attachInputHandler(); @@ -60,7 +60,7 @@ class nodeClass { if (toggleUpdate) { this.reactor.updateState(msg.timestamp); - send(this.reactor.getEffluent); + send([this.reactor.getEffluent, null, null]); } if (done) { @@ -100,6 +100,20 @@ class nodeClass { } } + /** + * 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 */ From 633a0884835c69095aa2696779371bbefafc8900 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 16 Jul 2025 16:08:14 +0200 Subject: [PATCH 73/97] Added handles for influent change emitter --- src/nodeClass.js | 20 ++++++++++--------- src/specificClass.js | 46 +++++++++++++++++++++----------------------- 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 9eba995..c36152d 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -32,12 +32,11 @@ class nodeClass { switch (msg.topic) { case "clock": toggleUpdate = true; + this.reactor.updateState(msg.timestamp); + send([this.reactor.getEffluent, null, null]); break; case "Fluent": this.reactor.setInfluent = msg; - if (msg.payload.inlet == 0) { - toggleUpdate = true; - } break; case "OTR": this.reactor.setOTR = msg; @@ -58,11 +57,6 @@ class nodeClass { console.log("Unknown topic: " + msg.topic); } - if (toggleUpdate) { - this.reactor.updateState(msg.timestamp); - send([this.reactor.getEffluent, null, null]); - } - if (done) { done(); } @@ -75,6 +69,14 @@ class nodeClass { */ _loadConfig(uiConfig) { this.config = { + general: { + name: uiConfig.name || this.name, + id: this.node.id, + unit: null + }, + functionality: { + softwareType: null // should be set in config manager + }, reactor_type: uiConfig.reactor_type, volume: parseFloat(uiConfig.volume), length: parseFloat(uiConfig.length), @@ -109,7 +111,7 @@ class nodeClass { this.node.send([ null, null, - { topic: 'registerChild', payload: this.node.id , positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, + { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, ]); }, 100); } diff --git a/src/specificClass.js b/src/specificClass.js index 8fb1448..474ebf7 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -1,13 +1,14 @@ const ASM3 = require('./reaction_modules/asm3_class.js'); -const { create, all } = require('mathjs'); +const { create, all, isArray } = require('mathjs'); const { assertNoNaN } = require('./utils.js'); const { childRegistrationUtils, logger, MeasurementContainer } = require('generalFunctions'); +const EventEmitter = require('events'); -const config = { +const mathConfig = { matrix: 'Array' // use Array as the matrix type }; -const math = create(all, config); +const math = create(all, mathConfig); const S_O_INDEX = 0; const NUM_SPECIES = 13; @@ -20,7 +21,8 @@ class Reactor { */ constructor(config) { // EVOLV stuff - this.logger = new logger(); //TODO: attach config + this.logger = new logger(undefined, undefined, config.general.name); + this.emitter = new EventEmitter(); this.measurements = new MeasurementContainer(); this.childRegistrationUtils = new childRegistrationUtils(this); // Child registration utility @@ -70,6 +72,17 @@ class Reactor { this.OTR = input.payload; } + /** + * Getter for effluent data. + * @returns {object} Effluent data object (msg), defaults to inlet 0. + */ + get getEffluent() { // getter for Effluent, defaults to inlet 0 + if (isArray(this.state.at(-1))) { + return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; + } + return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; + } + /** * Calculate the oxygen transfer rate (OTR) based on the dissolved oxygen concentration and temperature. * @param {number} S_O - Dissolved oxygen concentration [g O2 m-3]. @@ -104,11 +117,12 @@ class Reactor { 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; - } + while (n < n_iter) { + this.tick(this.timeStep); + n += 1; + } this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; + this.emitter.emit("stateChanges", this.getEffluent); } } } @@ -123,14 +137,6 @@ class Reactor_CSTR extends Reactor { this.state = config.initialState; } - /** - * Getter for effluent data. - * @returns {object} Effluent data object (msg), defaults to inlet 0. - */ - get getEffluent() { // getter for Effluent, defaults to inlet 0 - return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state }, timestamp: this.currentTime }; - } - /** * Tick the reactor state using the forward Euler method. * @param {number} time_step - Time step for the simulation [d]. @@ -191,14 +197,6 @@ class Reactor_PFR extends Reactor { this.D = input.payload; } - /** - * Getter for effluent data. - * @returns {object} Effluent data object (msg), defaults to inlet 0. - */ - get getEffluent() { - return { topic: "Fluent", payload: { inlet: 0, F: math.sum(this.Fs), C: this.state.at(-1) }, timestamp: this.currentTime }; - } - updateState(newTime) { super.updateState(newTime); let Pe_local = this.d_x*math.sum(this.Fs)/(this.D*this.A) From 73c2b654e145e7c8ec3f96282f49fb54b6e8a1aa Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 16 Jul 2025 16:54:30 +0200 Subject: [PATCH 74/97] Deal with clock singal --- additional_nodes/recirculation-pump.js | 2 ++ additional_nodes/settling-basin.js | 2 ++ src/nodeClass.js | 1 + 3 files changed, 5 insertions(+) diff --git a/additional_nodes/recirculation-pump.js b/additional_nodes/recirculation-pump.js index cd9fc21..2a02932 100644 --- a/additional_nodes/recirculation-pump.js +++ b/additional_nodes/recirculation-pump.js @@ -24,6 +24,8 @@ module.exports = function(RED) { send([msg_F1, msg_F2]); break; + case "clock": + break; default: console.log("Unknown topic: " + msg.topic); } diff --git a/additional_nodes/settling-basin.js b/additional_nodes/settling-basin.js index e19457a..dd3d697 100644 --- a/additional_nodes/settling-basin.js +++ b/additional_nodes/settling-basin.js @@ -41,6 +41,8 @@ module.exports = function(RED) { send([msg_F1, msg_F2]); break; + case "clock": + break; default: console.log("Unknown topic: " + msg.topic); } diff --git a/src/nodeClass.js b/src/nodeClass.js index c36152d..94b3965 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -34,6 +34,7 @@ class nodeClass { toggleUpdate = true; this.reactor.updateState(msg.timestamp); send([this.reactor.getEffluent, null, null]); + send([msg, null, null]) break; case "Fluent": this.reactor.setInfluent = msg; From b1719376cffcd0e32d96dae20e438262b528851d Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 21 Jul 2025 12:44:07 +0200 Subject: [PATCH 75/97] Rewrite reactor to source and register it properly to node object --- src/nodeClass.js | 25 ++++++++++++++----------- src/specificClass.js | 3 ++- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 94b3965..989b806 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -14,12 +14,13 @@ class nodeClass { this.node = nodeInstance; this.RED = RED; this.name = nameOfNode; + this.source = null; this._loadConfig(uiConfig) - this._registerChild(); this._setupClass(); this._attachInputHandler(); + this._registerChild(); } /** @@ -32,27 +33,28 @@ class nodeClass { switch (msg.topic) { case "clock": toggleUpdate = true; - this.reactor.updateState(msg.timestamp); - send([this.reactor.getEffluent, null, null]); + this.source.updateState(msg.timestamp); + send([this.source.getEffluent, null, null]); send([msg, null, null]) break; case "Fluent": - this.reactor.setInfluent = msg; + this.source.setInfluent = msg; break; case "OTR": - this.reactor.setOTR = msg; + this.source.setOTR = msg; break; case "Temperature": - this.reactor.setTemperature = msg; + this.source.setTemperature = msg; break; case "Dispersion": - this.reactor.setDispersion = msg; + this.source.setDispersion = msg; break; case 'registerChild': // Register this node as a child of the parent node const childId = msg.payload; const childObj = this.RED.nodes.getNode(childId); - this.reactor.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); + console.log(childObj.source); + this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); break; default: console.log("Unknown topic: " + msg.topic); @@ -76,7 +78,7 @@ class nodeClass { unit: null }, functionality: { - softwareType: null // should be set in config manager + softwareType: "reactor" // should be set in config manager }, reactor_type: uiConfig.reactor_type, volume: parseFloat(uiConfig.volume), @@ -112,7 +114,7 @@ class nodeClass { this.node.send([ null, null, - { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' }, + { topic: 'registerChild', payload: this.node.id, positionVsParent: this.config?.functionality?.positionVsParent || 'atEquipment' } ]); }, 100); } @@ -134,7 +136,8 @@ class nodeClass { console.warn("Unknown reactor type: " + uiConfig.reactor_type); } - this.reactor = new_reactor; // protect from reassignment + this.source = new_reactor; // protect from reassignment + this.node.source = this.source; } } diff --git a/src/specificClass.js b/src/specificClass.js index 474ebf7..99ed988 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -20,6 +20,7 @@ class Reactor { * @param {object} config - Configuration object containing reactor parameters. */ constructor(config) { + this.config = config; // EVOLV stuff this.logger = new logger(undefined, undefined, config.general.name); this.emitter = new EventEmitter(); @@ -122,7 +123,7 @@ class Reactor { n += 1; } this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; - this.emitter.emit("stateChanges", this.getEffluent); + this.emitter.emit("stateChange", this.getEffluent); } } } From 57aafe3e0b8ae4ebaf3784dc24497b21be791592 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Mon, 21 Jul 2025 17:28:09 +0200 Subject: [PATCH 76/97] Minor optimisations --- src/nodeClass.js | 6 ++---- src/specificClass.js | 7 ++++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 989b806..97ec545 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -28,14 +28,12 @@ class nodeClass { */ _attachInputHandler() { this.node.on('input', (msg, send, done) => { - let toggleUpdate = false; switch (msg.topic) { case "clock": - toggleUpdate = true; this.source.updateState(msg.timestamp); send([this.source.getEffluent, null, null]); - send([msg, null, null]) + send([msg, null, null]); break; case "Fluent": this.source.setInfluent = msg; @@ -53,7 +51,6 @@ class nodeClass { // Register this node as a child of the parent node const childId = msg.payload; const childObj = this.RED.nodes.getNode(childId); - console.log(childObj.source); this.source.childRegistrationUtils.registerChild(childObj.source, msg.positionVsParent); break; default: @@ -139,6 +136,7 @@ class nodeClass { this.source = new_reactor; // protect from reassignment this.node.source = this.source; } + } module.exports = nodeClass; \ No newline at end of file diff --git a/src/specificClass.js b/src/specificClass.js index 99ed988..a240275 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -25,6 +25,7 @@ class Reactor { this.logger = new logger(undefined, undefined, 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(); @@ -115,6 +116,10 @@ class Reactor { updateState(newTime) { // 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; @@ -123,7 +128,7 @@ class Reactor { n += 1; } this.currentTime += n_iter * this.timeStep * day2ms / this.speedUpFactor; - this.emitter.emit("stateChange", this.getEffluent); + this.emitter.emit("stateChange", newTime); } } } From f81161b2d571b881fe291ec15401528796e1e9ab Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 22 Jul 2025 12:20:29 +0200 Subject: [PATCH 77/97] Process output using tick function rather than clock message --- src/nodeClass.js | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/nodeClass.js b/src/nodeClass.js index 97ec545..637ec5b 100644 --- a/src/nodeClass.js +++ b/src/nodeClass.js @@ -21,6 +21,8 @@ class nodeClass { this._attachInputHandler(); this._registerChild(); + this._startTickLoop(); + this._attachCloseHandler(); } /** @@ -32,7 +34,6 @@ class nodeClass { switch (msg.topic) { case "clock": this.source.updateState(msg.timestamp); - send([this.source.getEffluent, null, null]); send([msg, null, null]); break; case "Fluent": @@ -137,6 +138,22 @@ class nodeClass { this.node.source = this.source; } + _startTickLoop() { + setTimeout(() => { + this._tickInterval = setInterval(() => this._tick(), 1000); + }, 1000); + } + + _tick(){ + this.node.send([this.source.getEffluent, null, null]); + } + + _attachCloseHandler() { + this.node.on('close', (done) => { + clearInterval(this._tickInterval); + done(); + }); + } } module.exports = nodeClass; \ No newline at end of file From ef02c47cff9999fb8b010c915f307a5e508c4b87 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Tue, 22 Jul 2025 14:36:52 +0200 Subject: [PATCH 78/97] Add WIP oxygen measurement child relation --- src/specificClass.js | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/specificClass.js b/src/specificClass.js index a240275..81ba36b 100644 --- a/src/specificClass.js +++ b/src/specificClass.js @@ -254,6 +254,16 @@ class Reactor_PFR extends Reactor { return stateNew; } + updateMeasurement(variant, subType, value, position) { + switch(subType) { + case "oxygen": + grid_pos = Math.round(position * this.n_x); + this.state[grid_pos][S_O_INDEX] = value; // naive approach for reconciling measurements and simulation + return; + } + super.updateMeasurement(variant, subType, value, position); + } + /** * Apply boundary conditions to the reactor state. * for inlet, apply generalised Danckwerts BC, if there is not flow, apply Neumann BC with no flux From da90224d3f4537b77dc2aa09bf9cb0fae30ce236 Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Wed, 23 Jul 2025 17:16:35 +0200 Subject: [PATCH 79/97] Added loggin to advanced-reactor. Currently broken for some reason? --- advanced-reactor.html | 21 +++++++++++++++++++++ advanced-reactor.js | 14 ++++++++++++++ 2 files changed, 35 insertions(+) diff --git a/advanced-reactor.html b/advanced-reactor.html index 91df690..7f2e55a 100644 --- a/advanced-reactor.html +++ b/advanced-reactor.html @@ -1,3 +1,5 @@ + + + - - diff --git a/advanced-reactor.js b/advancedReactor.js similarity index 85% rename from advanced-reactor.js rename to advancedReactor.js index 8740f71..b904029 100644 --- a/advanced-reactor.js +++ b/advancedReactor.js @@ -1,4 +1,4 @@ -const nameOfNode = "advanced-reactor"; // name of the node, should match file name and node type in Node-RED +const nameOfNode = "advancedReactor"; // name of the node, should match file name and node type in Node-RED const nodeClass = require('./src/nodeClass.js'); // node class const { MenuManager } = require('generalFunctions'); @@ -14,7 +14,7 @@ module.exports = function (RED) { const menuMgr = new MenuManager(); - // Serve /advanced-reactor/menu.js + // Serve /advancedReactor/menu.js RED.httpAdmin.get(`/${nameOfNode}/menu.js`, (req, res) => { try { const script = menuMgr.createEndpoint(nameOfNode, ['logger']); diff --git a/package.json b/package.json index b6d04c5..6bc14c8 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,13 @@ ], "license": "SEE LICENSE", "author": "P.R. van der Wilt", - "main": "advanced-reactor.js", + "main": "advancedReactor.js", "scripts": { - "test": "node advanced-reactor.js" + "test": "node advancedReactor.js" }, "node-red": { "nodes": { - "advanced-reactor": "advanced-reactor.js", + "advancedReactor": "advancedReactor.js", "recirculation-pump": "additional_nodes/recirculation-pump.js", "settling-basin": "additional_nodes/settling-basin.js" } From 5ec9319b3f20e03d8a6822a44b1ab91069c227ff Mon Sep 17 00:00:00 2001 From: "p.vanderwilt" Date: Thu, 24 Jul 2025 12:13:16 +0200 Subject: [PATCH 81/97] Add position field and proper logging configuration --- advancedReactor.html | 9 +++++++++ advancedReactor.js | 2 +- src/nodeClass.js | 7 ++++++- src/specificClass.js | 2 +- 4 files changed, 17 insertions(+), 3 deletions(-) diff --git a/advancedReactor.html b/advancedReactor.html index 3bdbe31..7d1f09f 100644 --- a/advancedReactor.html +++ b/advancedReactor.html @@ -107,6 +107,11 @@ window.EVOLV.nodes['advancedReactor'].loggerMenu.saveEditor(this); } + // save position field + if (window.EVOLV?.nodes?.measurement?.positionMenu?.saveEditor) { + window.EVOLV.nodes.rotatingMachine.positionMenu.saveEditor(this); + } + let volume = parseFloat($("#node-input-volume").typedInput("value")); if (isNaN(volume) || volume <= 0) { RED.notify("Fluid volume not set correctly", {type: "error"}); @@ -215,6 +220,10 @@
+ +
+ +