2025-07-07 11:59:11 +02:00
const ASM3 = require ( './reaction_modules/asm3_class.js' ) ;
2025-07-16 16:08:14 +02:00
const { create , all , isArray } = require ( 'mathjs' ) ;
2025-07-04 16:28:35 +02:00
const { assertNoNaN } = require ( './utils.js' ) ;
2025-07-11 12:22:36 +02:00
const { childRegistrationUtils , logger , MeasurementContainer } = require ( 'generalFunctions' ) ;
2025-07-16 16:08:14 +02:00
const EventEmitter = require ( 'events' ) ;
2025-06-24 11:20:28 +02:00
2025-07-16 16:08:14 +02:00
const mathConfig = {
2025-07-11 12:22:36 +02:00
matrix : 'Array' // use Array as the matrix type
2025-07-04 16:03:42 +02:00
} ;
2025-07-16 16:08:14 +02:00
const math = create ( all , mathConfig ) ;
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-07-08 15:41:41 +02:00
const DEBUG = false ;
2025-06-11 15:17:09 +02:00
2025-07-04 11:42:34 +02:00
class Reactor {
2025-07-11 12:22:36 +02:00
/ * *
* Reactor base class .
* @ param { object } config - Configuration object containing reactor parameters .
* /
constructor ( config ) {
2025-07-21 12:44:07 +02:00
this . config = config ;
2025-07-11 12:22:36 +02:00
// EVOLV stuff
2025-07-24 12:13:16 +02:00
this . logger = new logger ( this . config . general . logging . enabled , this . config . general . logging . logLevel , config . general . name ) ;
2025-07-16 16:08:14 +02:00
this . emitter = new EventEmitter ( ) ;
2025-07-11 12:22:36 +02:00
this . measurements = new MeasurementContainer ( ) ;
2025-07-21 17:28:09 +02:00
this . upstreamReactor = null ;
2025-07-11 12:22:36 +02:00
this . childRegistrationUtils = new childRegistrationUtils ( this ) ; // Child registration utility
this . asm = new ASM3 ( ) ;
this . volume = config . volume ; // fluid volume reactor [m3]
this . Fs = Array ( config . n _inlets ) . fill ( 0 ) ; // fluid debits per inlet [m3 d-1]
this . Cs _in = Array . from ( Array ( config . n _inlets ) , ( ) => new Array ( NUM _SPECIES ) . fill ( 0 ) ) ; // composition influents
2025-08-14 11:14:14 +02:00
this . OTR = 0.0 ; // oxygen transfer rate [g O2 d-1 m-3]
2025-07-11 12:22:36 +02:00
this . temperature = 20 ; // temperature [C]
this . kla = config . kla ; // if NaN, use externaly provided OTR [d-1]
this . currentTime = Date . now ( ) ; // milliseconds since epoch [ms]
2025-09-24 15:27:08 +02:00
this . timeStep = 1 / ( 24 * 60 * 60 ) * this . config . timeStep ; // time step in seconds, converted to days.
2025-07-11 12:22:36 +02:00
this . speedUpFactor = 60 ; // speed up factor for simulation, 60 means 1 minute per simulated second
}
/ * *
* Setter for influent data .
* @ param { object } input - Input object ( msg ) containing payload with inlet index , flow rate , and concentrations .
* /
set setInfluent ( input ) {
let index _in = input . payload . inlet ;
this . Fs [ index _in ] = input . payload . F ;
this . Cs _in [ index _in ] = input . payload . C ;
}
/ * *
* Setter for OTR ( Oxygen Transfer Rate ) .
2025-08-14 11:14:14 +02:00
* @ param { object } input - Input object ( msg ) containing payload with OTR value [ g O2 d - 1 m - 3 ] .
2025-07-11 12:22:36 +02:00
* /
set setOTR ( input ) {
this . OTR = input . payload ;
}
2025-07-16 16:08:14 +02:00
/ * *
* 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 } ;
}
2025-07-11 12:22:36 +02:00
/ * *
* 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 .
2025-08-14 11:14:14 +02:00
* @ returns { number } - Calculated OTR [ g O2 d - 1 m - 3 ] .
2025-07-11 12:22:36 +02:00
* /
_calcOTR ( S _O , T = 20.0 ) { // caculate the OTR using basic correlation, default to temperature: 20 C
let S _O _sat = 14.652 - 4.1022 e - 1 * T + 7.9910 e - 3 * T * T + 7.7774 e - 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 ;
2025-06-13 15:10:57 +02:00
}
2025-07-11 12:22:36 +02:00
}
2025-09-05 15:26:00 +02:00
registerChild ( child , softwareType ) {
switch ( softwareType ) {
2025-09-15 12:48:18 +02:00
case "measurement" :
this . logger . debug ( ` Registering measurement child. ` ) ;
2025-09-16 11:44:29 +02:00
this . _connectMeasurement ( child ) ;
2025-09-15 12:48:18 +02:00
break ;
2025-09-05 15:26:00 +02:00
case "reactor" :
this . logger . debug ( ` Registering reactor child. ` ) ;
2025-09-16 11:44:29 +02:00
this . _connectReactor ( child ) ;
2025-09-05 15:26:00 +02:00
break ;
default :
this . logger . error ( ` Unrecognized softwareType: ${ softwareType } ` ) ;
}
2025-09-15 12:48:18 +02:00
}
2025-09-16 11:44:29 +02:00
_connectMeasurement ( measurement ) {
2025-09-15 12:48:18 +02:00
if ( ! measurement ) {
this . logger . warn ( "Invalid measurement provided." ) ;
return ;
}
2025-09-05 15:26:00 +02:00
2025-09-26 14:51:18 +02:00
let position ;
2025-09-26 10:17:00 +02:00
if ( measurement . config . functionality . distance !== 'undefined' ) {
2025-09-26 14:51:18 +02:00
position = measurement . config . functionality . distance ;
2025-09-26 10:17:00 +02:00
} else {
2025-09-26 14:51:18 +02:00
position = measurement . config . functionality . positionVsParent ;
2025-09-26 10:17:00 +02:00
}
2025-09-16 15:54:31 +02:00
const measurementType = measurement . config . asset . type ;
2025-09-15 12:48:18 +02:00
const key = ` ${ measurementType } _ ${ position } ` ;
const eventName = ` ${ measurementType } .measured. ${ position } ` ;
// Register event listener for measurement updates
2025-09-26 16:33:00 +02:00
measurement . measurements . emitter . on ( eventName , ( eventData ) => {
2025-09-19 13:26:45 +02:00
this . logger . debug ( ` ${ position } ${ measurementType } from ${ eventData . childName } : ${ eventData . value } ${ eventData . unit } ` ) ;
2025-09-15 12:48:18 +02:00
// Store directly in parent's measurement container
this . measurements
. type ( measurementType )
. variant ( "measured" )
. position ( position )
. value ( eventData . value , eventData . timestamp , eventData . unit ) ;
2025-09-15 17:39:54 +02:00
this . _updateMeasurement ( measurementType , eventData . value , position , eventData ) ;
2025-09-15 12:48:18 +02:00
} ) ;
2025-09-05 15:26:00 +02:00
}
2025-09-15 12:48:18 +02:00
2025-09-16 11:44:29 +02:00
_connectReactor ( reactor ) {
2025-09-05 15:26:00 +02:00
if ( ! reactor ) {
this . logger . warn ( "Invalid reactor provided." ) ;
return ;
}
this . upstreamReactor = reactor ;
reactor . emitter . on ( "stateChange" , ( data ) => {
this . logger . debug ( ` State change of upstream reactor detected. ` ) ;
this . updateState ( data ) ;
} ) ;
}
2025-09-15 17:39:54 +02:00
_updateMeasurement ( measurementType , value , position , context ) {
2025-09-15 12:48:18 +02:00
this . logger . debug ( ` ---------------------- updating ${ measurementType } ------------------ ` ) ;
switch ( measurementType ) {
2025-09-05 15:26:00 +02:00
case "temperature" :
2025-09-22 15:17:25 +02:00
if ( position == "atEquipment" ) {
this . temperature = value ;
}
2025-09-05 15:26:00 +02:00
break ;
default :
2025-09-15 12:48:18 +02:00
this . logger . error ( ` Type ' ${ measurementType } ' not recognized for measured update. ` ) ;
2025-09-05 15:26:00 +02:00
return ;
}
}
2025-07-11 12:22:36 +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 .
* /
2025-08-04 10:59:11 +02:00
updateState ( newTime = Date . now ( ) ) { // expect update with timestamp
2025-07-11 12:22:36 +02:00
const day2ms = 1000 * 60 * 60 * 24 ;
2025-07-21 17:28:09 +02:00
if ( this . upstreamReactor ) {
this . setInfluent = this . upstreamReactor . getEffluent ;
}
2025-07-11 12:22:36 +02:00
let n _iter = Math . floor ( this . speedUpFactor * ( newTime - this . currentTime ) / ( this . timeStep * day2ms ) ) ;
if ( n _iter ) {
let n = 0 ;
2025-07-16 16:08:14 +02:00
while ( n < n _iter ) {
this . tick ( this . timeStep ) ;
n += 1 ;
}
2025-07-11 12:22:36 +02:00
this . currentTime += n _iter * this . timeStep * day2ms / this . speedUpFactor ;
2025-08-04 10:59:11 +02:00
this . emitter . emit ( "stateChange" , this . currentTime ) ;
2025-06-17 11:13:38 +02:00
}
2025-07-11 12:22:36 +02:00
}
2025-07-04 13:52:28 +02:00
}
class Reactor _CSTR extends Reactor {
2025-07-11 12:22:36 +02:00
/ * *
* Reactor _CSTR class for Continuous Stirred Tank Reactor .
* @ param { object } config - Configuration object containing reactor parameters .
* /
constructor ( config ) {
super ( config ) ;
this . state = config . initialState ;
}
/ * *
* Tick the reactor state using the forward Euler method .
* @ param { number } time _step - Time step for the simulation [ d ] .
* @ returns { Array } - New reactor state .
* /
tick ( time _step ) { // tick reactor state using forward Euler method
2025-08-04 10:59:11 +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 ) ;
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 ;
2025-07-11 12:22:36 +02:00
}
2025-06-11 15:17:09 +02:00
}
2025-07-04 11:42:34 +02:00
class Reactor _PFR extends Reactor {
2025-07-11 12:22:36 +02:00
/ * *
* Reactor _PFR class for Plug Flow Reactor .
* @ param { object } config - Configuration object containing reactor parameters .
* /
constructor ( config ) {
super ( config ) ;
this . length = config . length ; // reactor length [m]
this . n _x = config . resolution _L ; // number of slices
this . d _x = this . length / this . n _x ;
this . A = this . volume / this . length ; // crosssectional area [m2]
this . alpha = config . alpha ;
this . state = Array . from ( Array ( this . n _x ) , ( ) => config . initialState . slice ( ) )
this . D = 0.0 ; // axial dispersion [m2 d-1]
this . D _op = this . _makeDoperator ( true , true ) ;
assertNoNaN ( this . D _op , "Derivative operator" ) ;
this . D2 _op = this . _makeD2operator ( ) ;
assertNoNaN ( this . D2 _op , "Second derivative operator" ) ;
}
/ * *
* Setter for axial dispersion .
* @ param { object } input - Input object ( msg ) containing payload with dispersion value [ m2 d - 1 ] .
* /
set setDispersion ( input ) {
this . D = input . payload ;
}
updateState ( newTime ) {
super . updateState ( newTime ) ;
let Pe _local = this . d _x * math . sum ( this . Fs ) / ( this . D * this . A )
let Co _D = this . D * this . timeStep / ( this . d _x * this . d _x ) ;
2025-08-04 10:59:11 +02:00
( Pe _local >= 2 ) && this . logger . warn ( ` Local Péclet number ( ${ Pe _local } ) is too high! Increase reactor resolution. ` ) ;
( Co _D >= 0.5 ) && this . logger . warn ( ` Courant number ( ${ Co _D } ) is too high! Reduce time step size. ` ) ;
2025-07-11 12:22:36 +02:00
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 ) ;
2025-06-23 16:58:02 +02:00
}
2025-07-11 12:22:36 +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 ) {
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
2025-08-14 11:14:14 +02:00
for ( let i = 1 ; i < this . n _x - 1 ; i ++ ) {
transfer [ i ] [ S _O _INDEX ] = this . OTR * this . n _x / ( this . n _x - 2 ) ;
}
2025-07-11 12:22:36 +02:00
} else {
2025-08-14 11:14:14 +02:00
for ( let i = 1 ; i < this . n _x - 1 ; i ++ ) {
transfer [ i ] [ S _O _INDEX ] = this . _calcOTR ( this . state [ i ] [ S _O _INDEX ] , this . temperature ) * this . n _x / ( this . n _x - 2 ) ;
}
2025-07-04 15:14:03 +02:00
}
2025-07-11 12:22:36 +02:00
const dC _total = math . multiply ( math . add ( dispersion , advection , reaction , transfer ) , time _step ) ;
2025-07-09 15:37:29 +02:00
2025-07-11 12:22:36 +02:00
const stateNew = math . add ( this . state , dC _total ) ;
this . _applyBoundaryConditions ( stateNew ) ;
2025-06-23 16:58:02 +02:00
2025-07-11 12:22:36 +02:00
if ( DEBUG ) {
assertNoNaN ( dispersion , "dispersion" ) ;
assertNoNaN ( advection , "advection" ) ;
assertNoNaN ( reaction , "reaction" ) ;
assertNoNaN ( dC _total , "change in state" ) ;
assertNoNaN ( stateNew , "new state post BC" ) ;
2025-07-08 15:29:55 +02:00
}
2025-07-11 12:22:36 +02:00
this . state = this . _arrayClip2Zero ( stateNew ) ;
return stateNew ;
}
2025-09-15 17:39:54 +02:00
_updateMeasurement ( measurementType , value , position , context ) {
switch ( measurementType ) {
2025-09-29 09:40:17 +02:00
case "quantity (oxygen)" :
2025-10-01 11:50:35 +02:00
let grid _pos = Math . round ( position / this . config . length * this . n _x ) ;
2025-07-22 14:36:52 +02:00
this . state [ grid _pos ] [ S _O _INDEX ] = value ; // naive approach for reconciling measurements and simulation
2025-09-16 11:44:29 +02:00
break ;
2025-09-26 16:36:09 +02:00
default :
super . _updateMeasurement ( measurementType , value , position , context ) ;
2025-07-22 14:36:52 +02:00
}
}
2025-07-11 12:22:36 +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 .
* /
_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 ] ;
2025-06-23 16:58:02 +02:00
}
2025-07-11 12:22:36 +02:00
// 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 ;
2025-06-23 16:58:02 +02:00
}
2025-07-11 12:22:36 +02:00
}
/ * *
* 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 ;
}
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
// }