2025-07-04 16:03:42 +02:00
const ASM3 = require ( './asm3_class.js' ) ;
const { create , all } = require ( 'mathjs' ) ;
2025-07-04 16:28:35 +02:00
const { assertNoNaN } = require ( './utils.js' ) ;
2025-06-24 11:20:28 +02:00
const config = {
2025-07-04 15:14:03 +02:00
matrix : 'Array' // use Array as the matrix type
2025-07-04 16:03:42 +02:00
} ;
const math = create ( all , config ) ;
2025-06-24 11:20:28 +02:00
2025-07-04 16:28:35 +02:00
const S _O _INDEX = 0 ;
2025-07-04 16:03:42 +02:00
const NUM _SPECIES = 13 ;
2025-06-11 15:17:09 +02:00
2025-07-04 11:42:34 +02:00
class Reactor {
2025-07-04 13:52:28 +02:00
/ * *
* Reactor base class .
* @ param { object } config - Configuration object containing reactor parameters .
* /
2025-07-04 15:14:03 +02:00
constructor ( config ) {
2025-06-11 15:17:09 +02:00
this . asm = new ASM3 ( ) ;
2025-06-11 16:24:27 +02:00
2025-07-04 16:28:35 +02:00
this . volume = config . volume ; // fluid volume reactor [m3]
2025-07-04 11:42:34 +02:00
2025-07-04 16:28:35 +02:00
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
2025-06-13 15:10:57 +02:00
this . OTR = 0.0 ; // oxygen transfer rate [g O2 d-1]
2025-07-04 11:42:34 +02:00
2025-07-04 13:52:28 +02:00
this . kla = config . kla ; // if NaN, use externaly provided OTR [d-1]
2025-06-11 17:18:27 +02:00
this . currentTime = Date . now ( ) ; // milliseconds since epoch [ms]
2025-07-04 16:28:35 +02:00
this . timeStep = 1 / ( 24 * 60 * 15 ) ; // time step [d]
2025-07-04 13:52:28 +02:00
this . speedUpFactor = 60 ; // speed up factor for simulation, 60 means 1 minute per simulated second
2025-06-11 17:18:27 +02:00
}
2025-07-04 13:52:28 +02:00
/ * *
* Setter for influent data .
* @ param { object } input - Input object ( msg ) containing payload with inlet index , flow rate , and concentrations .
* /
set setInfluent ( input ) {
2025-06-16 14:01:19 +02:00
let index _in = input . payload . inlet ;
this . Fs [ index _in ] = input . payload . F ;
this . Cs _in [ index _in ] = input . payload . C ;
2025-07-04 13:52:28 +02:00
// DEBUG
2025-07-04 11:42:34 +02:00
// 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));
2025-06-13 15:10:57 +02:00
}
2025-07-04 13:52:28 +02:00
/ * *
* Setter for OTR ( Oxygen Transfer Rate ) .
* @ param { object } input - Input object ( msg ) containing payload with OTR value [ g O2 d - 1 ] .
* /
set setOTR ( input ) {
2025-06-13 15:10:57 +02:00
this . OTR = input . payload ;
}
2025-07-04 13:52:28 +02:00
/ * *
*
* @ param { number } S _O - Dissolved oxygen concentration [ g O2 m - 3 ] .
* @ param { number } T - Temperature in Celsius , default to 20 C .
* @ returns
* /
2025-07-04 15:14:03 +02:00
_calcOTR ( S _O , T = 20.0 ) { // caculate the OTR using basic correlation, default to temperature: 20 C
2025-07-04 16:28:35 +02:00
let S _O _sat = 14.652 - 4.1022 e - 1 * T + 7.9910 e - 3 * T * T + 7.7774 e - 5 * T * T * T ;
2025-06-17 11:13:38 +02:00
return this . kla * ( S _O _sat - S _O ) ;
}
2025-07-04 14:58:38 +02:00
/ * *
* 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 ) ) {
2025-07-04 15:48:05 +02:00
return arr . map ( x => this . _arrayClip2Zero ( x ) ) ;
2025-07-04 14:58:38 +02:00
} else {
return arr < 0 ? 0 : arr ;
}
}
2025-07-04 13:52:28 +02:00
/ * *
* 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
2025-07-04 15:14:03 +02:00
const day2ms = 1000 * 60 * 60 * 24 ;
2025-06-11 17:18:27 +02:00
2025-07-04 16:28:35 +02:00
let n _iter = Math . floor ( this . speedUpFactor * ( newTime - this . currentTime ) / ( this . timeStep * day2ms ) ) ;
2025-06-19 20:55:42 +02:00
if ( n _iter ) {
2025-06-12 16:56:28 +02:00
let n = 0 ;
while ( n < n _iter ) {
2025-07-04 13:52:28 +02:00
this . tick ( this . timeStep ) ;
2025-06-12 16:56:28 +02:00
n += 1 ;
}
2025-06-19 20:55:42 +02:00
this . currentTime += n _iter * this . timeStep * day2ms / this . speedUpFactor ;
2025-06-12 16:56:28 +02:00
}
2025-06-11 15:17:09 +02:00
}
2025-07-04 13:52:28 +02:00
}
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
2025-07-04 15:14:03 +02:00
return { topic : "Fluent" , payload : { inlet : 0 , F : math . sum ( this . Fs ) , C : this . state } , timestamp : this . currentTime } ;
2025-07-04 13:52:28 +02:00
}
/ * *
* 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
2025-07-04 16:28:35 +02:00
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 ) ;
2025-07-04 16:03:42 +02:00
const reaction = this . asm . compute _dC ( this . state ) ;
const transfer = Array ( NUM _SPECIES ) . fill ( 0.0 ) ;
2025-07-04 16:28:35 +02:00
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
2025-06-11 16:24:27 +02:00
2025-07-04 16:03:42 +02:00
const dC _total = math . multiply ( math . add ( inflow , outflow , reaction , transfer ) , time _step ) ;
assertNoNaN ( dC _total , "change in state" ) ;
2025-06-11 16:24:27 +02:00
2025-07-04 15:14:03 +02:00
this . state = this . _arrayClip2Zero ( math . add ( this . state , dC _total ) ) ; // clip value element-wise to avoid negative concentrations
2025-07-04 16:03:42 +02:00
assertNoNaN ( this . state , "new state" ) ;
2025-06-11 15:17:09 +02:00
return this . state ;
}
}
2025-07-04 11:42:34 +02:00
class Reactor _PFR extends Reactor {
2025-07-04 13:52:28 +02:00
/ * *
* Reactor _PFR class for Plug Flow Reactor .
* @ param { object } config - Configuration object containing reactor parameters .
* /
2025-07-04 13:16:49 +02:00
constructor ( config ) {
super ( config ) ;
2025-06-23 16:58:02 +02:00
2025-07-04 13:16:49 +02:00
this . length = config . length ; // reactor length [m]
this . n _x = config . resolution _L ; // number of slices
2025-06-23 16:58:02 +02:00
2025-07-04 13:16:49 +02:00
this . d _x = this . length / this . n _x ;
2025-07-04 16:28:35 +02:00
this . A = this . volume / this . length ; // crosssectional area [m2]
2025-06-23 16:58:02 +02:00
2025-07-04 13:16:49 +02:00
this . state = Array . from ( Array ( this . n _x ) , ( ) => config . initialState . slice ( ) )
2025-07-04 15:14:03 +02:00
2025-06-24 12:32:11 +02:00
// console.log("Initial State: ")
// console.log(this.state)
2025-06-23 16:58:02 +02:00
2025-06-24 13:28:45 +02:00
this . D = 0.0 ; // axial dispersion [m2 d-1]
2025-06-23 16:58:02 +02:00
2025-07-04 14:58:38 +02:00
this . D _op = this . _makeDoperator ( true , true ) ;
2025-07-04 15:48:05 +02:00
assertNoNaN ( this . D _op , "Derivative operator" ) ;
2025-07-04 16:28:35 +02:00
2025-07-04 16:03:42 +02:00
this . D2 _op = this . _makeD2operator ( ) ;
2025-07-04 15:48:05 +02:00
assertNoNaN ( this . D2 _op , "Second derivative operator" ) ;
2025-06-23 16:58:02 +02:00
}
2025-07-04 15:14:03 +02:00
2025-07-04 13:52:28 +02:00
/ * *
* Setter for axial dispersion .
* @ param { object } input - Input object ( msg ) containing payload with dispersion value [ m2 d - 1 ] .
* /
set setDispersion ( input ) {
2025-06-23 16:58:02 +02:00
this . D = input . payload ;
}
2025-07-04 13:52:28 +02:00
/ * *
* Getter for effluent data .
* @ returns { object } Effluent data object ( msg ) , defaults to inlet 0.
* /
get getEffluent ( ) {
2025-07-04 15:14:03 +02:00
return { topic : "Fluent" , payload : { inlet : 0 , F : math . sum ( this . Fs ) , C : this . state . at ( - 1 ) } , timestamp : this . currentTime } ;
}
2025-07-04 16:28:35 +02:00
/ * *
* 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 .
* /
2025-07-04 15:48:05 +02:00
_applyBoundaryConditions ( state ) {
2025-07-04 15:14:03 +02:00
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 ] ;
2025-07-04 16:28:35 +02:00
const BC _gradient = Array ( this . n _x ) . fill ( 0 ) ;
2025-07-04 15:14:03 +02:00
BC _gradient [ 0 ] = - 1 ;
BC _gradient [ 1 ] = 1 ;
let Pe = this . length * math . sum ( this . Fs ) / ( this . D * this . A )
2025-07-04 16:28:35 +02:00
const BC _dispersion = math . multiply ( ( 1 - ( 1 + 4 * this . volume / math . sum ( this . Fs ) / Pe ) ^ 0.5 ) / Pe , [ BC _gradient ] , state ) [ 0 ] ;
2025-07-04 15:48:05 +02:00
state [ 0 ] = math . add ( BC _C _in , BC _dispersion ) . map ( val => val < 0 ? 0 : val ) ;
2025-07-04 15:14:03 +02:00
} else { // Neumann BC (no flux)
2025-07-04 15:48:05 +02:00
state [ 0 ] = state [ 1 ] ;
2025-07-04 15:14:03 +02:00
}
// Neumann BC (no flux)
2025-07-04 16:28:35 +02:00
state [ this . n _x - 1 ] = state [ this . n _x - 2 ]
2025-06-23 16:58:02 +02:00
}
2025-07-04 13:52:28 +02:00
/ * *
* 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 ) {
2025-07-04 16:28:35 +02:00
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 ) ;
2025-06-24 12:32:11 +02:00
const reaction = this . state . map ( ( state _slice ) => this . asm . compute _dC ( state _slice ) ) ;
2025-07-04 16:28:35 +02:00
const transfer = Array . from ( Array ( this . n _x ) , ( ) => new Array ( NUM _SPECIES ) . fill ( 0 ) ) ;
2025-06-28 19:19:38 +02:00
2025-07-04 14:58:38 +02:00
assertNoNaN ( dispersion , "dispersion" ) ;
assertNoNaN ( advection , "advection" ) ;
assertNoNaN ( reaction , "reaction" ) ;
2025-07-04 15:14:03 +02:00
2025-06-23 16:58:02 +02:00
if ( isNaN ( this . kla ) ) { // calculate OTR if kla is not NaN, otherwise use externally calculated OTR
2025-07-04 16:28:35 +02:00
transfer . forEach ( ( x ) => { x [ S _O _INDEX ] = this . OTR ; } ) ;
2025-06-23 16:58:02 +02:00
} else {
2025-07-04 16:28:35 +02:00
transfer . forEach ( ( x , i ) => { x [ S _O _INDEX ] = this . _calcOTR ( this . state [ i ] [ S _O _INDEX ] ) ; } ) ;
2025-06-23 16:58:02 +02:00
}
2025-06-27 16:56:37 +02:00
const dC _total = math . multiply ( math . add ( dispersion , advection , reaction , transfer ) , time _step ) ;
2025-07-04 16:03:42 +02:00
assertNoNaN ( dC _total , "change in state" ) ;
2025-07-04 14:58:38 +02:00
2025-07-04 16:03:42 +02:00
const stateNew = math . add ( this . state , dC _total ) ;
2025-07-04 15:14:03 +02:00
assertNoNaN ( stateNew , "new state" ) ;
2025-06-24 16:23:33 +02:00
2025-07-04 15:48:05 +02:00
this . _applyBoundaryConditions ( stateNew ) ;
2025-07-04 15:14:03 +02:00
assertNoNaN ( stateNew , "new state post BC" ) ;
2025-06-28 19:19:38 +02:00
2025-07-04 15:14:03 +02:00
this . state = this . _arrayClip2Zero ( stateNew ) ;
return stateNew ;
2025-06-23 16:58:02 +02:00
}
2025-07-04 13:52:28 +02:00
/ * *
* 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 .
* /
2025-07-04 15:14:03 +02:00
_makeDoperator ( central = false , higher _order = false ) { // create gradient operator
2025-06-30 12:50:02 +02:00
if ( higher _order ) {
if ( central ) {
2025-07-04 15:48:05 +02:00
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 ] ) ;
2025-06-30 15:46:13 +02:00
const D = math . add ( I , A , B , C ) ;
2025-06-30 12:50:02 +02:00
const NearBoundary = Array ( this . n _x ) . fill ( 0.0 ) ;
2025-07-04 15:48:05 +02:00
NearBoundary [ 0 ] = - 1 / 4 ;
NearBoundary [ 1 ] = - 5 / 6 ;
NearBoundary [ 2 ] = 3 / 2 ;
NearBoundary [ 3 ] = - 1 / 2 ;
NearBoundary [ 4 ] = 1 / 12 ;
2025-07-01 13:04:32 +02:00
D [ 1 ] = NearBoundary ;
2025-06-30 12:50:02 +02:00
NearBoundary . reverse ( ) ;
2025-07-04 15:48:05 +02:00
D [ this . n _x - 2 ] = math . multiply ( - 1 , NearBoundary ) ;
2025-06-30 15:46:13 +02:00
D [ 0 ] = Array ( this . n _x ) . fill ( 0 ) ; // set by BCs elsewhere
2025-07-04 15:48:05 +02:00
D [ this . n _x - 1 ] = Array ( this . n _x ) . fill ( 0 ) ;
2025-06-30 15:46:13 +02:00
return D ;
2025-06-30 12:50:02 +02:00
} else {
throw new Error ( "Upwind higher order method not implemented! Use central scheme instead." ) ;
}
} else {
2025-07-04 16:28:35 +02:00
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 ] ) ;
2025-06-30 12:50:02 +02:00
const D = math . add ( I , A ) ;
2025-06-30 15:46:13 +02:00
D [ 0 ] = Array ( this . n _x ) . fill ( 0 ) ; // set by BCs elsewhere
2025-07-04 16:28:35 +02:00
D [ this . n _x - 1 ] = Array ( this . n _x ) . fill ( 0 ) ;
2025-06-30 15:46:13 +02:00
return D ;
2025-06-30 12:50:02 +02:00
}
2025-06-23 16:58:02 +02:00
}
2025-07-04 13:52:28 +02:00
/ * *
* Create central finite difference second derivative operator .
* @ returns { Array } - Second derivative operator matrix .
* /
2025-07-04 14:58:38 +02:00
_makeD2operator ( ) { // create the central second derivative operator
2025-06-24 16:23:33 +02:00
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 ] ) ;
2025-06-27 16:56:37 +02:00
const D2 = math . add ( I , A , B ) ;
D2 [ 0 ] = Array ( this . n _x ) . fill ( 0 ) ; // set by BCs elsewhere
2025-07-04 15:14:03 +02:00
D2 [ this . n _x - 1 ] = Array ( this . n _x ) . fill ( 0 ) ;
2025-06-27 16:56:37 +02:00
return D2 ;
2025-06-23 16:58:02 +02:00
}
}
2025-07-04 14:58:38 +02:00
module . exports = { Reactor _CSTR , Reactor _PFR } ;
2025-06-23 16:58:02 +02:00
2025-07-04 13:52:28 +02:00
// DEBUG
2025-06-11 16:24:27 +02:00
// 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
2025-06-12 16:56:28 +02:00
// 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];
2025-06-24 11:20:28 +02:00
// 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;
2025-06-24 16:23:33 +02:00
// Reactor.D = 0.01;
2025-06-24 11:20:28 +02:00
// let N = 0;
2025-06-24 16:23:33 +02:00
// while (N < 5000) {
2025-07-04 13:52:28 +02:00
// console.log(Reactor.tick(0.001));
2025-06-12 16:56:28 +02:00
// N += 1;
2025-07-04 14:58:38 +02:00
// }