diff --git a/diffuser.html b/diffuser.html
new file mode 100644
index 0000000..bfbb310
--- /dev/null
+++ b/diffuser.html
@@ -0,0 +1,89 @@
+
+
+
+
+
+
diff --git a/diffuser.js b/diffuser.js
new file mode 100644
index 0000000..bd2f31a
--- /dev/null
+++ b/diffuser.js
@@ -0,0 +1,162 @@
+module.exports = function (RED) {
+ function diffuser(config) {
+ //create node
+ RED.nodes.createNode(this, config);
+
+ //call this => node so whenver you want to call a node function type node and the function behind it
+ var node = this;
+
+ //fetch class
+ var Diffuser = require("./dependencies/diffuser_class");
+
+ //make new class on creation to work with.
+ var diffuser = new Diffuser();
+
+ //fetch name from node into the measurement code
+ diffuser.name = config.name;
+ diffuser.number = config.number;
+ diffuser.id = diffuser.name+diffuser.number;
+ diffuser.i_n_elements = config.i_elements;
+ diffuser.i_diff_density = Number(config.i_diff_density);
+ diffuser.i_m_water = config.i_m_water;
+
+ // create internal vars
+ this.interval_id = null;
+ let internalTickRate = 1;
+ function update_node_state() {
+ //alarm has prio over warning so comes first in the if else statement
+ if (diffuser.alarm.state == true) {
+ //display status
+ node.status({ fill: "red", shape: "dot", text: diffuser.alarm.text[0] });
+ }
+ else if(diffuser.warning.state == true ){
+ //display status
+ node.status({ fill: "yellow", shape: "dot", text: diffuser.warning.text[0] });
+ }
+ else if(diffuser.idle == true){
+ node.status({ fill: "gray", shape: "dot", text: diffuser.o_kgo2_h + " Kg o2 / h"});
+ }
+ else{
+ node.status({fill: "green", shape: "dot", text: diffuser.o_kgo2_h + " Kg o2 / h"});
+ }
+ }
+
+ //update on creation
+ update_node_state();
+
+ function send_output(){
+
+ //define empty msgs
+ let msgs = [];
+
+ let dynList = {
+ iPressure: diffuser.i_pressure,
+ iMWater: diffuser.i_m_water,
+ iFlow: diffuser.i_flow,
+ nFlow: diffuser.n_flow,
+ oOtr: diffuser.o_otr,
+ oPLoss: diffuser.o_p_total,
+ oKgo2H: diffuser.o_kgo2_h,
+ oFlowElement: diffuser.o_flow_element,
+ efficiency: diffuser.o_combined_eff,
+ //threshold: diffuser.threshold //EXPERIMENTAL!
+ }
+
+ let specList = {
+ name: diffuser.name,
+ number: diffuser.number,
+ //supplier: diffuser.supplier,
+ //type: diffuser.type,
+ density: diffuser.i_diff_density,
+ nElements: diffuser.i_n_elements,
+ alfaF: diffuser.i_alfa_factor
+ }
+
+ msgs[0] = {topic: "kgo2/h" , payload: {kgo2h: diffuser.o_kgo2_h , tot_p_loss : diffuser.o_p_total} ,id: diffuser.id };
+ msgs[1] = {topic: "object", payload : diffuser};
+ msgs[2] = {
+ topic:"outputdbase",
+ payload:[
+
+ {
+ measurement: diffuser.name+diffuser.number,
+ fields : dynList,
+ tags: {
+ group: "values",
+ },
+ timestamp: new Date()
+ },
+ {
+ measurement: diffuser.name+diffuser.number,
+ fields : specList,
+ tags: {
+ group: "specs",
+ },
+ timestamp: new Date()
+ },
+ ]
+ } // output to broker
+
+ //send outputs
+ node.send(msgs);
+ }
+
+ //never ending functions
+ function tick(){
+ update_node_state();
+ send_output();
+
+ }
+
+ // register child on first output this timeout is needed because of node - red stuff
+ setTimeout(
+ () => {
+
+ /*---execute code on first start----*/
+ let msgs = [];
+ msgs[3] = { topic : "registerChild" , payload: diffuser };
+ //send msg
+ this.send(msgs);
+ },
+ 100
+ );
+
+ //declare refresh interval internal node
+ setTimeout(
+ () => {
+ /*---execute code on first start----*/
+ this.interval_id = setInterval(function(){ tick() },(internalTickRate * 1000))
+ },
+ 1000
+ );
+
+
+ //-------------------------------------------------------------------->>what to do on input
+ node.on("input", function (msg,send,done) {
+
+ //change density of diffusers
+ if(msg.topic == "density"){
+ diffuser.i_diff_density = Number(msg.payload);
+ }
+
+ //change input flow
+ if(msg.topic == "air_flow"){
+ diffuser.i_flow = Number(msg.payload);
+ }
+
+ //change water height
+ if(msg.topic == "height_water"){
+ diffuser.i_m_water = Number(msg.payload);
+ }
+
+ done();
+
+ });
+
+ // tidy up any async code here - shutdown connections and so on.
+ node.on('close', function() {
+ clearTimeout(this.interval_id);
+ });
+ }
+ RED.nodes.registerType("diffuser", diffuser);
+};
\ No newline at end of file
diff --git a/diffuser_class.js b/diffuser_class.js
new file mode 100644
index 0000000..f240940
--- /dev/null
+++ b/diffuser_class.js
@@ -0,0 +1,589 @@
+/*
+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;
diff --git a/graph.js b/graph.js
new file mode 100644
index 0000000..81864bd
--- /dev/null
+++ b/graph.js
@@ -0,0 +1,152 @@
+/*
+Copyright:
+Year : (c) 2023
+Author/licensor : Rene De Ren
+Contact details : zn375ix3@gmail.com
+Location : The Netherlands
+Licensee : Waterschap brabantse delta
+Address: Bouvignelaan 5, 4836 AA Breda
+
+Permission is hereby granted, to the licensee , to use this software only for the purposes of testing it in its R&D lab.
+
+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.
+
+
+*/
+
+//A class to interact and manipulate machines with a parabolic curve
+class Graph {
+ /*------------------- Construct and set vars -------------------*/
+ constructor() {
+
+ // current x and y
+ this.x = 0;
+ this.y = 0;
+
+ //previous y and x
+ this.y_prev = 0;
+ this.x_prev = 0;
+
+ //slope of current value
+ this.slope = 0;
+
+
+ }
+
+ set slope(value){
+ this._slope = Math.round( value * 100 ) / 100;
+ }
+ get slope(){
+ return this._slope;
+ }
+
+ calc(x,y){
+
+ //store old values
+ this.x_prev = this.x;
+ this.y_prev = this.y;
+
+ //store new values
+ this.x = x;
+ this.y = y;
+
+ //calc slope
+ this.slope = this.calc_slope(this.x,this.y,this.x_prev,this.y_prev);
+
+
+ }
+
+ calc_slope(x,y,x_prev,y_prev){
+
+ let slope = 0;
+ let incline = false;
+ let decline = false;
+
+ if( x_prev > x){
+ decline = true;
+ incline = false;
+ }
+ else if( x_prev < x ){
+ decline = false;
+ incline = true;
+ }
+
+ //determine decline / incl
+ if( (!decline && !incline) ){
+ slope = 0;
+ }
+ else if(decline){
+ slope = -( (y - y_prev) / ( x - x_prev) ) ;
+ }
+ else if(incline){
+ slope = ( (y - y_prev) / ( x - x_prev) ) ;
+ }
+
+ return slope;
+ }
+
+ // 1 a dimensional peak ( THIS WILL FIND ONLY 1 PEAK!! so assuming the array only has one !)
+ one_dim_peak_finder(array){
+
+ //define array length
+ let array_length = array.length;
+ //start position in array is middle
+ let recursive_pos = array_length/2;
+ //define end conditions
+ let max_iterations = 100;
+ let done = false;
+ let iteration = 0;
+ let peak_found = false;
+
+ while(!done){
+
+ //find peak going left
+ if( array[recursive_pos-1] > array[recursive_pos] ){
+ //calc new pos
+ recursive_pos = (recursive_pos-1) / 2;
+ }
+ //find peak going right
+ else if( array[recursive_pos+1] > array[recursive_pos] ){
+ //calc new pos
+ recursive_pos = (recursive_pos+1) / 2;
+ }
+ //found peak!
+ else{
+ done = true;
+ peak_found = true;
+ position_peak = recursive_pos;
+ }
+
+ //end prematurely
+ if(max_iterations >= iteration){
+ done = true;
+ peak_found = false;
+ position_peak = null;
+ }
+ iteration++;
+ }
+ //build response
+ let obj_peak = { found:peak_found , value_of_peak: array[position_peak] , position_peak:position_peak };
+
+ return obj_peak;
+ }
+
+
+} // end of class
+
+
+/*
+var graph = new Graph();
+
+
+//*/
+
+module.exports = Graph;
\ No newline at end of file
diff --git a/package.json b/package.json
new file mode 100644
index 0000000..b316fad
--- /dev/null
+++ b/package.json
@@ -0,0 +1,24 @@
+{
+ "name": "diffuser",
+ "version": "0.0.1",
+ "description": "Control module diffuser",
+ "main": "diffuser",
+ "scripts": {
+ "test": "diffuser"
+ },
+ "repository": {
+ "type": "git",
+ "url": "diffuser"
+ },
+ "keywords": [
+ "diffuser"
+ ],
+ "author": "Rene De Ren",
+ "license": "MIT",
+ "node-red" : {
+ "nodes": {
+ "diffuser": "diffuser.js"
+
+ }
+ }
+}
\ No newline at end of file