/* Copyright: Year : (c) 2023 Author : Rene De Ren Contact details : zn375ix3@gmail.com Location : The Netherlands 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 author shall be notified of any and all improvements or adaptations this 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, OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ //Diffised Aeration Devices (example fine bubble aeration) class Diffuser{ /*------------------- Construct and set vars -------------------*/ constructor(supplier,type) { this.init = false; /* --------------------load depenencies ------------- */ this.Interpolation = require('../../predict/dependencies/interpolation') ; //load class this.interpolation = new this.Interpolation; //general use of interpolation object this.Fysics = require('./../../convert/dependencies/fysics') ; //load class this.fysics = new this.Fysics; //general use of fysics this.Graph = require('./graph') ; //load class this.graph = new this.Graph; //general use of fysics //load default pressure curve depending on type this.specs = this.load_specs(); //after loading specs load curve builder this.Predict = require('../../predict/dependencies/predict_class') ; //load class this.predict_otr = new this.Predict(); //load otr curve this.predict_otr.i_curve = this.specs.otr_curve; this.predict_p = new this.Predict(); this.predict_p.i_curve = this.specs.p_curve; // load pressure curve this.predict_p.i_f = 0; // set f to dim 0 because there is no other dim for pressure this.predictO2saturation = new this.Predict(); this.predictO2saturation.i_curve = this.fysics.o2Solubility; // load solubility curve for o2 in water //load converter ./dependencies/index this.convert = require('./../../convert/dependencies/index'); /* ------------ static vars ---------------*/ this.name = ""; // user defined name this.number = 0 ; // user defined number this.id = ""; // unique id from node red? this.idle = true; // is this idle (not outputting kg o2 / h) this.desc = "diffuser"; // description of the current object this.supplier = supplier; // supplier of diffuser this.type = type; // type of diffuser /* ---------- Inputs -------------- */ this.i_flow = 0 ; // input actual flow rate expressed in Nm3/h this.i_diff_density = 5; // intput actual density of diffusers this.i_pressure = 0 ; // this is pressure in header expressed in mbar to calculated air density. this.i_local_atm_pressure = 1013,25 ; // local atm pressure in mbar this.i_alfa_factor = 0.7 ; // stnd alfa factor this.i_water_density = 997 ; // this.water_molar_mass * this.num_moles_water // water density in kg/m3; this.i_m_water = 0; // input actual height in meter water ABOVE the diffuser this.i_n_elements = 1; // input for amount of diffusers we need to divide the flow over to get the nm per element /*---------calculated parameters ----------*/ this.n_flow = 0 ; // converted input to normalized conditions of the diffusers flow parameter (x) this.n_kg = this.fysics.calc_air_dens(this.specs.units.Nm3.pressure,this.specs.units.Nm3.RH,this.specs.units.Nm3.temp); // calculated normalized kg for air density depending on specs kg /m3 /* ---------- Outputs -------------- */ this.o_otr = 0; // predicted oxygen transfer rate this.o_p_flow = 0; // predicted pressure loss over flow rate this.o_p_water = 0; // predicted pressure loss over meter water above diffuser this.o_p_total = 0; // predicted total pressure loss this.o_kg = 0; // predicted kg of air this.o_kg_h = 0; // predicted kg per hour this.o_kgo2_h = 0; // predicted oxygen transfer per hour this.o_kgo2_h_min = 0; // predicted min kgo2 / hour for zone controller this.o_kgo2_h_max = 0; // predicted max kgo2 / hour for zone controller this.o_kgo2 = 0 ; // current oxygen input this.o_flow_element = 0 ; // flow per element this.o_otr_max = 0; // store max otr for easy access this.o_otr_min = 0; // store min otr for easy access this.o_combined_eff = 0; // combined efficiency this.o_histogram = 0; // keep track of histogram x % of time for OTR (efficiency) over a max historical value ? or a counter which gets higher over time without. this.o_slope = 0; /*-----------alarms---------------*/ //putting alarms in 1 array always gets all the alarms that are currently active or inactive //an alarm is a trigger to stop any process feeding the diffusers this.alarm = { text:[], state:false, flow:{ min:{state:false,hyst:10}, //when there isnt enough flow to ensure the correct distribution of air max:{state:false,hyst:10},//when there is to much flow per diffuser and exceeds the suppliers limits }, pressure:{ min:{state:false,hyst:10}, // see min flow max:{state:false,hyst:10}, // see max flow }, }; /*-----------warnings---------------*/ //putting alarms in 1 array always gets all the alarms that are currently active or inactive //a warning is a trigger to alert users on eradic behavior or some other activity that might cause damage in the future so users can investigate what is going on this.warning = { text:[], // fill this with the warnings we want to display state:false, deviation:{ pressure:{state:false,hyst:2},//when there is a deviation versus expected pressures of the baseline that exceeds a hyst of x % of deviation before becomming true }, flow:{ min:{state:false,hyst:2}, //when there isnt enough flow to ensure the correct distribution of air max:{state:false,hyst:2},//when there is to much flow per diffuser and exceeds the suppliers limits }, pressure:{ min:{state:false,hyst:2}, // see min flow max:{state:false,hyst:2}, // see max flow }, }; /*-------------error handling -----------*/ this.error = { text:[], state:false, } this.init = true; } /*------------------- GETTER/SETTERS -------------------*/ set i_n_elements(x){ //check if this input is a number if(Number.isNaN(x)){ this.error.state = true; this.error.text.push("number of elements not of type number"); } // you cant have partial elements x = Math.round(x); //check if init has allready been exectued if ( x <= 0 ) { this.error.state = true; this.error.text.push("0 elements input"); } //set densi3ty to value this._i_n_elements = x; } get i_n_elements(){ return this._i_n_elements; } get i_diff_density(){ return this._i_diff_density; } set i_diff_density(value){ //set densi3ty to value this._i_diff_density = value; //check if init has allready been exectued if(this.init == true){ //submit new value to prediction this.predict_otr.i_f = value; //refresh predictions this.o_otr = this.predict_otr.o_y; } } set o_otr(value){ //set density to value this._o_otr = Math.round( value * 100 ) / 100; //check if init has allready been exectued if(this.init == true){ //current output in kg o2 / h this.o_kgo2_h = this.convert ( this.o_otr * this.n_flow * this.i_m_water ).from('g').to('kg') ; /*make max and min calculations to use them in a kg load zone controller*/ this.o_kgo2_h_min = this.convert ( this.o_otr_min * this.n_flow * this.i_m_water ).from('g').to('kg') ; this.o_kgo2_h_max = this.convert ( this.o_otr_max * this.n_flow * this.i_m_water ).from('g').to('kg') ; //divide by 3600 to get the current ouput in kg this.o_kgo2 = this.o_kgo2_h / 3600 ; } } get o_otr(){ return this._o_otr; } //set meter water column above the diffuser in meters set i_m_water(value){ //set density to value this._i_m_water = value; //convert height to pressure in mbar this.o_p_water = this.fysics.heigth_to_pressure(this.i_water_density,value); this.o_p_total = this.o_p_water + this.o_p_flow ; //recalc values this.i_flow = this.i_flow; } get i_m_water(){ return this._i_m_water } set o_kgo2_h(value){ this._o_kgo2_h = Math.round( value * 100 ) / 100; } get o_kgo2_h(){ return this._o_kgo2_h; } set o_p_total(value){ this._o_p_total = Math.round( value * 100 ) / 100; } get o_p_total(){ return this._o_p_total; } set o_p_flow(value){ //set density to value this._o_p_flow = Math.round( value * 100 ) / 100; //recalc total this.o_p_total = this.o_p_water + value ; } get o_p_flow(){ return this._o_p_flow; } set i_flow(value){ //set density to value this._i_flow = value; if(this.init == true){ //any flow smaller or equal to zero means diffusers are not being supplied with air and are not active. if(value > 0){ // idle to off this.idle = false; //calc otr and p this.calc_otr_p(value); } else{ this.idle = true; this.o_otr = 0; this.o_p_flow = 0; this.o_flow_element = 0; this.o_p_total = 0; } } } get i_flow(){ return this._i_flow; } /*------------------------- functions ----------------------------*/ //a diffuser has a combined efficiency combine_eff(o_otr,o_otr_min,o_otr_max,o_p_flow,o_p_min,o_p_max){ //highest otr is best efficiency possible let eff1 = this.interpolation.interpolate_lin_single_point(o_otr,o_otr_min,o_otr_max,0,1); //lowest pressure is best pressure possible let eff2 = this.interpolation.interpolate_lin_single_point(o_p_flow,o_p_min,o_p_max,1,0); let result = eff1 * eff2 * 100 ; return result; } // do all actions in order to calculate the outputs for flow and pressure related stuf calc_otr_p(flow){ //convert to kg using pressure,rh,temperature we need to get actual data to do this else we assume that normalized equals site //total input pressure equals atm pressure + measured pressure in system let tot_i_pressure = this.convert(this.i_local_atm_pressure + this.i_pressure).from('mbar').to('bar'); //calculated output in kg air this.o_kg = this.fysics.calc_air_dens(tot_i_pressure,0,20); this.o_kg_h = this.o_kg * flow ; //convert this to the normal m3 of diffuser density data this.n_flow = ( this.o_kg / this.n_kg ) * flow; //calculate how much flow per element this.o_flow_element = Math.round( ( this.n_flow / this.i_n_elements ) * 100 ) / 100 ; // input x values in predictors (we could make functions and return outputs dunno whats better) this.predict_otr.i_x = this.o_flow_element; this.predict_p.i_x = this.o_flow_element; //store otr max and min this.o_otr_min = this.predict_otr.c_fxy_y_min; this.o_otr_max = this.predict_otr.c_fxy_y_max; //store min and max pressure this.o_p_min = this.predict_p.c_fxy_y_min; this.o_p_max = this.predict_p.c_fxy_y_max; // predict oxygen transfer rate this.o_otr = this.predict_otr.o_y; // predict pressure output of diffuser this.o_p_flow = this.predict_p.o_y; //calculated combined efficiency in % where 100 % is ideal efficieny and 0 is worst this.o_combined_eff = Math.round(this.combine_eff(this.o_otr,this.o_otr_min,this.o_otr_max,this.o_p_flow,this.o_p_min,this.o_p_max) * 100 ) /100; //slope otr curve this.graph.calc(this.predict_otr.i_x,this.predict_otr.o_y); this.o_slope = this.graph.slope; //go through functions this.warning_check(); this.alarm_check(); } // calculate average saturation over the water depth of the diffuser calcAvgSolubility(temp){ //calculate average pressure in water column let avgPMbar = ( this.o_p_water + this.i_local_atm_pressure ) / 2; let avgPBar = this.convert(avgPMbar).from('mbar').to('bar'); // input x values in predictors (we could make functions and return outputs dunno whats better) this.predictO2saturation.i_f = avgPBar; // set f to temperature this.predictO2saturation.i_x = temp; // set temperature // predict oxygen transfer rate return this.predictO2saturation.o_y; //return average saturation value } warning_check(){ //warnings do not require to be resetted. we need to log them somewhere in the database so its easy to check if these values are being exceeded //empty tekst before filling again and always reset state this.warning.text = []; this.warning.state = false; /* ------------------ Flow warnings ------------------*/ //define hyst for flow let min_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.warning.flow.min.hyst / 100); let max_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.warning.flow.max.hyst / 100); //min flow if(this.o_flow_element < this.predict_p.c_fxy_x_min - min_flow_hyst ){ this.warning.flow.min.state = true; this.warning.state = true; this.warning.text.push(" Warning : input flow " + this.o_flow_element + " is less then minimum allowed flow of " + ( this.predict_p.c_fxy_x_min - min_flow_hyst )); } else{ //auto reset this.warning.flow.min.state = false; } //max flow if(this.o_flow_element > this.predict_p.c_fxy_x_max + max_flow_hyst ){ this.warning.flow.max.state = true; this.warning.state = true; this.warning.text.push("Warning input flow " + this.o_flow_element + " is exceeding nominal value of " + ( this.predict_p.c_fxy_x_max + max_flow_hyst )); } else{ //auto reset this.warning.flow.max.state = false; } } alarm_check(){ //warnings do not require to be resetted. we need to log them somewhere in the database so its easy to check if these values are being exceeded //empty tekst before filling again and reset general state this.alarm.text = []; this.alarm.state = false; /* ------------------ Flow warnings ------------------*/ //define hyst for flow let min_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.alarm.flow.min.hyst / 100); let max_flow_hyst = this.predict_p.c_fxy_x_min * 1 * ( this.alarm.flow.max.hyst / 100); //min flow if(this.o_flow_element < this.predict_p.c_fxy_x_min - min_flow_hyst ){ this.alarm.flow.min.state = true; this.alarm.state = true; this.alarm.text.push("Alarm input flow " + this.o_flow_element + " is less then minimum allowed flow of " + ( this.predict_p.c_fxy_x_min - min_flow_hyst )); } else{ //auto reset this.warning.flow.min.state = false; } //max flow if(this.o_flow_element > this.predict_p.c_fxy_x_max + max_flow_hyst ){ this.alarm.flow.max.state = true; this.alarm.state = true; this.alarm.text.push("Alarm input flow " + this.o_flow_element + " is exceeding an absolute max value of " + ( this.predict_p.c_fxy_x_max + max_flow_hyst )); } else{ //auto reset this.alarm.flow.max.state = false; } } //fetch curve and load it into object var load_specs(){ //for now hardcoded will do as example from sulzer let Sulzerspecs = { supplier : "sulzer", type : "pik300", units:{ Nm3: { "temp": 20, "pressure" : 1.01325 , "RH" : 0 }, t_otr_curve : { f : "diffuser density in % ", x : "Nm3/h", y: { o2_weight : "g", flow : "Nm3/h" , height: "m" } } , // according to DIN t_p_curve : { x : "flow", y : "mbar" } }, otr_curve: { 5: // diffuser density expressed in % { x:[1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10], // flow expressed in normal m3/h y:[28, 27, 26, 25, 24.5, 24, 23.5, 23.2, 23, 22.9], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m }, 10: // diffuser density expressed in % { x:[1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10], // flow expressed in normal m3/h y:[31, 29.5, 28.5, 27, 26, 25.5, 25, 24.7, 24.2, 24], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m } }, p_curve:{ //difuser pressure loss curve 0: // if curve doesnt have more than 1 dimension just fill in zero here or whatever { x:[1.5, 2, 3, 4, 5, 6, 7, 8, 9, 10], // Flow expressed in nm3/h y:[25,26,28,30,35,38,42,48,55,68] //pressure expressed in mbar } }, solubility_curve:{ 1: // abs bar { x:[0,5,10,15,20,25,30,35,40,45,50], // temp in degrees celcius y:[14.6,12.8,11.3,10.1,9.1,8.3,7.6,7,6.5,6,5.6], // mg/l }, 2: // abs bar { x:[0,5,10,15,20,25,30,35,40,45,50], // temp in degrees celcius y:[29.2,25.5,22.6,20.2,18.2,16.5,15.2,14,12.9,12,11.3], // mg/l }, 4: // abs bar { x:[0,5,10,15,20,25,30,35,40,45,50], // temp in degrees celcius y:[58.4,51.1,45.1,40.3,36.4,33.1,30.3,27.9,25.9,24,22.7], // mg/l }, } } //for now hardcoded will do as example from sulzer let specs = { supplier : "GVA", type : "ELASTOX-R", units:{ Nm3: { "temp": 20, "pressure" : 1.01325 , "RH" : 0 }, t_otr_curve : { f : "diffuser density in % ", x : "Nm3/h", y: { o2_weight : "g", flow : "Nm3/h" , height: "m" } } , // according to DIN t_p_curve : { x : "flow", y : "mbar" } }, otr_curve: { 2.4: // diffuser density expressed in % { x:[2,3,4,5,6,7,8,9,10], // flow expressed in normal m3/h y:[26,25,24,23.5,23,22.75,22.5,22.25,22], //oxygen transfer rate expressed in gram o2 / normal m3/h / per m } }, p_curve:{ //difuser pressure loss curve 0: // if curve doesnt have more than 1 dimension just fill in zero here or whatever { x:[2,3,4,5,6,7,8,9,10,11,12], // Flow expressed in nm3/h y:[40,42.5,45,47.5,50,51.5,53,54.5,56,57.5,59] //pressure expressed in mbar } } } return specs; } //testing converter function converttest(){ let test = this.convert(1).from('l').to('ml'); return test ; } } // end of class /* var diffuser = new Diffuser("sulzer","PIK300"); // define inputs for diffuser diffuser.i_m_water = 5; // set water column above diffuser diffuser.i_n_elements = 1; diffuser.i_diff_density = 5; // set density in % diffuser.i_flow = 4; // set flow rate in m3/h // outputs are console.log("--------------Inputs--------------") console.log("Diffuser density : "+ diffuser.i_diff_density + " %"); console.log("Flow : " + diffuser.i_flow + " m3/h" ); console.log("Flow rate per element : " + diffuser.o_flow_element + " m3/h"); console.log("Number of elements : " + diffuser.i_n_elements ); console.log("--------------Outputs--------------") console.log("converted input flow to diffuser normalized flow : " + diffuser.n_flow + " m3/h "); console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter"); console.log("Pressure loss over diffusers : " + diffuser.o_p_flow + " mbar" ); console.log("Pressure loss over water column:" + diffuser.o_p_water + " mbar" ); console.log("Total pressure loss : " + diffuser.o_p_total + " mbar"); console.log("predicted diffuser oxygen input for bio : " + diffuser.o_kgo2_h + " kg o2 / h"); console.log("Predicted actual input o2 : " + diffuser.o_kgo2 + " kg o2 / s " ); console.log("max otr " + diffuser.o_otr_max); console.log("displaying warnings : " + JSON.stringify(diffuser.warning.text) ); //change flow diffuser.i_diff_density = 5; // set density in % //diffuser.i_flow = 4; console.log("--------------Inputs--------------") console.log("Diffuser density : "+ diffuser.i_diff_density + " %"); console.log("Flow : " + diffuser.i_flow + " m3/h" ); console.log("--------------Outputs--------------") console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter"); console.log("Pressure loss: " + diffuser.o_p_flow + " mbar" ); console.log("slope: " + diffuser.o_slope ); diffuser.i_flow = 5; console.log("--------------Inputs--------------") console.log("Diffuser density : "+ diffuser.i_diff_density + " %"); console.log("Flow : " + diffuser.i_flow + " m3/h" ); console.log("--------------Outputs--------------") console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter"); console.log("Pressure loss: " + diffuser.o_p_flow + " mbar" ); console.log("slope: " + diffuser.o_slope ); diffuser.i_diff_density = 0; console.log("--------------Inputs--------------") console.log("Diffuser density : "+ diffuser.i_diff_density + " %"); console.log("Flow : " + diffuser.i_flow + " m3/h" ); console.log("--------------Outputs--------------") console.log("Oxygen transfer rate : " + diffuser.o_otr + " o2 / m3/h / meter"); console.log("Pressure loss: " + diffuser.o_p_flow + " mbar" ); // //*/ module.exports = Diffuser;