├── __init__.py ├── Sinusoidal oscillating perturbation.py ├── README.md ├── Rx_Fermentation_Monod-Herbert_Aerobic.py ├── Rx_Fermentation_Monod-Herbert_Anaerobic.py ├── Rx_Fermentation_Scerevisiae_Glucose_Aerobic_Batch.py ├── Rx_Fermentation_Ecoli_Glucose_Aerobic_Batch.py └── Rx_Fermentation_Ecoli_Glucose_Aerobic_Fedbatch.py /__init__.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Wed Sep 5 09:51:37 2018 4 | 5 | @author: bjogut 6 | """ 7 | 8 | -------------------------------------------------------------------------------- /Sinusoidal oscillating perturbation.py: -------------------------------------------------------------------------------- 1 | #This is the function to add perturbations inside a mechanistic model with concentrations 2 | 3 | #import the model 4 | 5 | def disturbances(modelInUse): 6 | modelInUse.solve() 7 | t = modelInUse.solve()[0] 8 | C= modelInUse.solve()[1] 9 | 10 | #Initialization of the vectors for the superposition of the perturbations 11 | PV =[] 12 | PV2 = [] 13 | PV3 = [] 14 | 15 | #Creation of the vector to collect the data with the noise/perturbation 16 | C_noise = np.zeros((C.shape)) 17 | import numpy as np 18 | 19 | #sinusoidal oscillation of each time - Dependance of time 20 | for i in range(len(t)): 21 | PV.append((((math.sin(t[i]*83))/47)+(math.cos(t[i]*11))/23)) 22 | PV2.append(0.6*(((math.sin(t[i] * 61)/31) + (math.cos(t[i] * 43)) / 61))) 23 | PV3.append(0.33*(((math.sin(t[i] * 41))/19) + (math.cos(t[i] * 61)) / 89)) 24 | 25 | #stablishing the perturbation inside the concentration 26 | for i in range(len(C[0])): 27 | C_noise [:, i] = C[:, i] + C[:, i]* PV + C[:, i]*PV2 + C[:, i]*PV3 28 | 29 | return C_noise 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # NyctiDB 3 | Welcome to NyctiDB - the BioVL library that collects some of the models inside FermProc software implemented and solved in Python. 4 | 5 | ## Mechanistics models for Fermentation 6 | 7 | Along their benefits, mechanistic models: 8 | 9 | · Summary of knowledge about the process. 10 | 11 | · Possibility to try different scenarios. 12 | 13 | ## Implementation of a fermentation mechanistic model 14 | 15 | In order to create a template, we follow the next steps: 16 | 1. Create a class. 17 | 18 | 1.1 Initialize variables, constants, and parameters. 19 | 20 | 1.2 Create a process matrix in which it is enclosed the kinetic reaction. 21 | 22 | 1.3 Create a process matrix in which it is enclosed the kinetic reaction. 23 | 24 | 1.4 Solution of the model through a solver such as the "odeint" module of scipy.integrate for ODE. 25 | 26 | ## Models inside this repository 27 | 28 | In this repository, you can find different models for the aerobic or anaerobic growth of microorganisms. 29 | The models are implemented as an object and they are based on determistic principles. 30 | In this library you can find: 31 | 32 | > Herbert-Monod in aerobic conditions 33 | 34 | > Herbert-Monod in anaerobic conditions 35 | 36 | > Aerobic growth of Saccharomyces cerevisiae in glucose (with metabolic switch controlled by the Cabtree Effect/Overflow and glucose inhibition) 37 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /Rx_Fermentation_Monod-Herbert_Aerobic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Sep 6 13:34:32 2018 4 | 5 | @author: simoca 6 | """ 7 | 8 | 9 | from scipy.integrate import odeint 10 | #Package for plotting 11 | import math 12 | #Package for the use of vectors and matrix 13 | import numpy as np 14 | import array as arr 15 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 16 | from matplotlib.figure import Figure 17 | import sys 18 | import os 19 | import matplotlib.pyplot as plt 20 | from matplotlib.ticker import FormatStrFormatter 21 | import glob 22 | from random import sample 23 | import random 24 | import time 25 | 26 | 27 | 28 | class Monod_Herbert: 29 | def __init__(self, Control=False): 30 | self.Y_XS = 0.8 31 | self.Y_OX = 1.05 32 | self.y_x = 0.5 33 | self.mu_max = 2.1 34 | self.Ks = 0.17 35 | self.kd = 0.21 36 | self.kla = 1000 37 | self.O_sat = 0.0755 38 | 39 | self.S0 = 18 40 | self.O0 = 0.0755 41 | self.X0 = 0.01 42 | self.V0 = 10 43 | # #parameters for control, default every 1/24 hours: 44 | self.t_end = 30 45 | self.t_start = 0 46 | self.Control = Control 47 | self.coolingOn = True 48 | self.steps = (self.t_end - self.t_start)*24 49 | self.T0 = 30 50 | self.K_p = 2.31e+01 51 | self.K_i = 3.03e-01 52 | self.K_d = -3.58e-03 53 | self.Tset = 30 54 | self.u_max = 150 55 | self.u_min = 0 56 | def rxn(self, C,t, u): 57 | #when there is no control, k has no effect 58 | k=1 59 | #when cooling is off than u = 0 60 | if self.coolingOn == False: 61 | u = 0 62 | 63 | if self.Control == True : 64 | #Cardinal temperature model with inflection: Salvado et al 2011 "Temperature Adaptation Markedly Determines Evolution within the Genus Saccharomyces" 65 | #Strain S. cerevisiae PE35 M 66 | Topt = 30 67 | Tmax = 45.48 68 | Tmin = 5.04 69 | T = C[5] 70 | if T < Tmin or T > Tmax: 71 | k = 0 72 | else: 73 | D = (T-Tmax)*(T-Tmin)**2 74 | E = (Topt-Tmin)*((Topt-Tmin)*(T-Topt)-(Topt-Tmax)*(Topt+Tmin-2*T)) 75 | k = D/E 76 | 77 | #number of components 78 | n = 3 79 | m = 3 80 | #initialize the stoichiometric matrix, s 81 | s = np.zeros((m,n)) 82 | s[0,0] = -1/self.Y_XS 83 | s[0,1] = -1/self.Y_OX 84 | s[0,2] = 1 85 | 86 | 87 | s[1,0] = 0 88 | s[1,1] = 1/self.y_x 89 | s[1,2] = -1 90 | 91 | s[2,0] = 0 92 | s[2,1] = self.kla 93 | s[2,2] = 0 94 | #initialize the rate vector 95 | rho = np.zeros((m,1)) 96 | ##initialize the overall conversion vector 97 | r=np.zeros((n,1)) 98 | rho[0,0] = self.mu_max*(C[0]/(C[0]+self.Ks))*C[2] 99 | rho[1,0] = self.kd*C[2] 100 | rho[2,0] = self.kla*(self.O_sat - C[1]) 101 | 102 | #Developing the matrix, the overall conversion rate is stoichiometric *rates 103 | r[0,0] = (s[0,0]*rho[0,0])+(s[1,0]*rho[1,0])+(s[2,0]*rho[2,0]) 104 | r[1,0] = (s[0,1]*rho[0,0])+(s[1,1]*rho[1,0])+(s[2,1]*rho[2,0]) 105 | r[2,0] = (s[0,2]*rho[0,0])+(s[1,2]*rho[1,0])+(s[2,2]*rho[2,0]) 106 | 107 | 108 | #Solving the mass balances 109 | dSdt = r[0,0] 110 | dOdt = r[1,0] 111 | dXdt = r[2,0] 112 | dVdt = 0 113 | if self.Control == True : 114 | ''' 115 | dHrxn heat produced by cells estimated by yeast heat combustion coeficcient dhc0 = -21.2 kJ/g 116 | dHrxn = dGdt*V*dhc0(G)-dEdt*V*dhc0(E)-dXdt*V*dhc0(X) 117 | (when cooling is working) Q = - dHrxn -W , 118 | dT = V[L] * 1000 g/L / 4.1868 [J/gK]*dE [kJ]*1000 J/KJ 119 | dhc0(EtOH) = -1366.8 kJ/gmol/46 g/gmol [KJ/g] 120 | dhc0(Glc) = -2805 kJ/gmol/180g/gmol [KJ/g] 121 | 122 | ''' 123 | #Metabolic heat: [W]=[J/s], dhc0 from book "Bioprocess Engineering Principles" (Pauline M. Doran) : Appendix Table C.8 124 | dHrxndt = dXdt*C[4]*(-21200) #[J/s] + dGdt*C[4]*(15580)- dEdt*C[4]*(29710) 125 | #Shaft work 1 W/L1 126 | W = -1*C[4] #[J/S] negative because exothermic 127 | #Cooling just an initial value (constant cooling to see what happens) 128 | #dQdt = -0.03*C[4]*(-21200) #[J/S] 129 | #velocity of cooling water: u [m3/h] -->controlled by PID 130 | 131 | #Mass flow cooling water 132 | M=u/3600*1000 #[kg/s] 133 | #Define Tin = 5 C, Tout=TReactor 134 | #heat capacity water = 4190 J/kgK 135 | Tin = 5 136 | #Estimate water at outlet same as Temp in reactor 137 | Tout = C[5] 138 | cpc = 4190 139 | #Calculate Q from Eq 9.47 140 | Q=-M*cpc*(Tout-Tin) # J/s 141 | #Calculate Temperature change 142 | dTdt = -1*(dHrxndt - Q + W)/(C[4]*1000*4.1868) #[K/s] 143 | else: 144 | dTdt = 0 145 | return [dSdt, dOdt, dXdt, dVdt, dTdt] 146 | 147 | def solve(self): 148 | #solve normal: 149 | t = np.linspace(self.t_start, self.t_end, self.steps) 150 | if self.Control == False : 151 | u = 0 152 | # fc = 1 153 | C0 = [self.S0, self.O0, self.X0,self.V0, self.T0] 154 | C = odeint(self.rxn, C0, t, rtol = 1e-7, mxstep= 500000, args=(u,)) 155 | 156 | #solve for Control 157 | else: 158 | """ 159 | PID Temperature Control: 160 | """ 161 | # storage for recording values 162 | C = np.ones([len(t), 6]) 163 | C0 = [self.S0, self.O0, self.X0,self.V0,self.T0] 164 | C[0] = C0 165 | self.ctrl_output = np.zeros(len(t)) # controller output 166 | e = np.zeros(len(t)) # error 167 | ie = np.zeros(len(t)) # integral of the error 168 | dpv = np.zeros(len(t)) # derivative of the pv 169 | P = np.zeros(len(t)) # proportional 170 | I = np.zeros(len(t)) # integral 171 | D = np.zeros(len(t)) # derivative 172 | 173 | for i in range(len(t)-1): 174 | #print(t[i]) 175 | #PID control of cooling water 176 | dt = t[i+1]-t[i] 177 | #Error 178 | e[i] = C[i,5] - self.Tset 179 | #print(e[i]) 180 | if i >= 1: 181 | dpv[i] = (C[i,5]-C[i-1,5])/dt 182 | ie[i] = ie[i-1] + e[i]*dt 183 | P[i]=self.K_p*e[i] 184 | I[i]=self.K_i*ie[i] 185 | D[i]=self.K_d*dpv[i] 186 | 187 | self.ctrl_output[i]=P[i]+I[i]+D[i] 188 | u=self.ctrl_output[i] 189 | if u>self.u_max: 190 | u=self.u_max 191 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 192 | if u < self.u_min: 193 | u =self.u_min 194 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 195 | #time for solving ODE 196 | ts = [t[i],t[i+1]] 197 | #disturbance 198 | #if self.t[i] > 5 and self.t[i] < 10: 199 | # u = 0 200 | #solve ODE from last timepoint to new timepoint with old values 201 | 202 | y = odeint(self.rxn, C0, ts, rtol = 1e-7, mxstep= 500000, args=(u,)) 203 | #update C0 204 | C0 = y[-1] 205 | #merge y to C 206 | C[i+1]=y[-1] 207 | 208 | return t, C 209 | 210 | 211 | 212 | #f= Monod_Herbert() 213 | #f.solve() 214 | #plt.plot(f.solve()[0], f.solve()[1]) 215 | #plt.show() 216 | 217 | 218 | 219 | -------------------------------------------------------------------------------- /Rx_Fermentation_Monod-Herbert_Anaerobic.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Sep 6 13:34:32 2018 4 | 5 | @author: simoca 6 | """ 7 | 8 | 9 | from scipy.integrate import odeint 10 | #Package for plotting 11 | import math 12 | #Package for the use of vectors and matrix 13 | import numpy as np 14 | import array as arr 15 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 16 | from matplotlib.figure import Figure 17 | import sys 18 | import os 19 | import matplotlib.pyplot as plt 20 | from matplotlib.ticker import FormatStrFormatter 21 | import glob 22 | from random import sample 23 | import random 24 | import time 25 | 26 | 27 | class Monod_Herbert_Anaero: 28 | def __init__(self, Control=False): 29 | self.Y_XS = 0.8 30 | self.Y_OX = 1.05 31 | self.Y_PX = 2 32 | self.y_x = 0.5 33 | self.mu_max = 2.1 34 | self.Ks = 0.17 35 | self.kd = 0.21 36 | self.kla = 1000 37 | self.O_sat = 0.0755 38 | 39 | self.S0 = 18 40 | self.X0 = 0.1 41 | self.V0 = 10 42 | self.P0=0 43 | # #parameters for control, default every 1/24 hours: 44 | self.t_end = 30 45 | self.t_start = 0 46 | self.Control = Control 47 | self.coolingOn = True 48 | self.steps = (self.t_end - self.t_start)*24 49 | self.T0 = 30 50 | self.K_p = 2.31e+01 51 | self.K_i = 3.03e-01 52 | self.K_d = -3.58e-03 53 | self.Tset = 30 54 | self.u_max = 150 55 | self.u_min = 0 56 | # def compounds(self): 57 | # self.compound = ['Substrate','Product','Biomass'] 58 | # return self.compound 59 | def rxn(self, C,t, u): 60 | #when there is no control, k has no effect 61 | k=1 62 | #when cooling is off than u = 0 63 | if self.coolingOn == False: 64 | u = 0 65 | 66 | if self.Control == True : 67 | #Cardinal temperature model with inflection: Salvado et al 2011 "Temperature Adaptation Markedly Determines Evolution within the Genus Saccharomyces" 68 | #Strain S. cerevisiae PE35 M 69 | Topt = 30 70 | Tmax = 45.48 71 | Tmin = 5.04 72 | T = C[5] 73 | if T < Tmin or T > Tmax: 74 | k = 0 75 | else: 76 | D = (T-Tmax)*(T-Tmin)**2 77 | E = (Topt-Tmin)*((Topt-Tmin)*(T-Topt)-(Topt-Tmax)*(Topt+Tmin-2*T)) 78 | k = D/E 79 | 80 | #number of components 81 | self.s = np.zeros((2,3)) 82 | self.rho=np.zeros((2,1)) 83 | self.s[0,2]=1 84 | self.s[0,0]=(-1/self.Y_XS) 85 | self.s[0,1] = (1/self.Y_PX) 86 | self.s[1,2]=-1 87 | 88 | self.rho[0,0]=((self.mu_max*C[0])/(C[0]+self.Ks))*C[2] 89 | self.rho[1,0]=self.kd*C[2] 90 | 91 | # print(self.rho) 92 | # print(self.s) 93 | self.r= np.zeros((3,1)) 94 | self.r[0,0]= self.s[0,0]*self.rho[0,0]+self.s[1,0]*self.rho[1,0] 95 | self.r[1,0]= self.s[0,1]*self.rho[0,0]+self.s[1,1]*self.rho[1,0] 96 | self.r[2,0]= self.s[0,2]*self.rho[0,0]+self.s[1,2]*self.rho[1,0] 97 | dSdt = self.r[0,0] 98 | dPdt = self.r[1,0] 99 | dXdt = self.r[2,0] 100 | dVdt = 0 101 | # 102 | # n = 3 103 | # m = 3 104 | # #initialize the stoichiometric matrix, s 105 | # s = np.zeros((m,n)) 106 | # s[0,0] = -1/self.Y_XS 107 | # s[0,1] = -1/self.Y_OX 108 | # s[0,2] = 1 109 | # 110 | # 111 | # s[1,0] = 0 112 | # s[1,1] = 1/self.y_x 113 | # s[1,2] = -1 114 | # 115 | # s[2,0] = 0 116 | # s[2,1] = self.kla 117 | # s[2,2] = 0 118 | # #initialize the rate vector 119 | # rho = np.zeros((m,1)) 120 | # ##initialize the overall conversion vector 121 | # r=np.zeros((n,1)) 122 | # rho[0,0] = self.mu_max*(C[0]/(C[0]+self.Ks))*C[2] 123 | # rho[1,0] = self.kd*C[2] 124 | # rho[2,0] = self.kla*(self.O_sat - C[1]) 125 | # 126 | # #Developing the matrix, the overall conversion rate is stoichiometric *rates 127 | # r[0,0] = (s[0,0]*rho[0,0])+(s[1,0]*rho[1,0])+(s[2,0]*rho[2,0]) 128 | # r[1,0] = (s[0,1]*rho[0,0])+(s[1,1]*rho[1,0])+(s[2,1]*rho[2,0]) 129 | # r[2,0] = (s[0,2]*rho[0,0])+(s[1,2]*rho[1,0])+(s[2,2]*rho[2,0]) 130 | # 131 | # 132 | # #Solving the mass balances 133 | # dSdt = r[0,0] 134 | # dOdt = r[1,0] 135 | # dXdt = r[2,0] 136 | # dVdt = 0 137 | if self.Control == True : 138 | ''' 139 | dHrxn heat produced by cells estimated by yeast heat combustion coeficcient dhc0 = -21.2 kJ/g 140 | dHrxn = dGdt*V*dhc0(G)-dEdt*V*dhc0(E)-dXdt*V*dhc0(X) 141 | (when cooling is working) Q = - dHrxn -W , 142 | dT = V[L] * 1000 g/L / 4.1868 [J/gK]*dE [kJ]*1000 J/KJ 143 | dhc0(EtOH) = -1366.8 kJ/gmol/46 g/gmol [KJ/g] 144 | dhc0(Glc) = -2805 kJ/gmol/180g/gmol [KJ/g] 145 | 146 | ''' 147 | #Metabolic heat: [W]=[J/s], dhc0 from book "Bioprocess Engineering Principles" (Pauline M. Doran) : Appendix Table C.8 148 | dHrxndt = dXdt*C[4]*(-21200) #[J/s] + dGdt*C[4]*(15580)- dEdt*C[4]*(29710) 149 | #Shaft work 1 W/L1 150 | W = -1*C[4] #[J/S] negative because exothermic 151 | #Cooling just an initial value (constant cooling to see what happens) 152 | #dQdt = -0.03*C[4]*(-21200) #[J/S] 153 | #velocity of cooling water: u [m3/h] -->controlled by PID 154 | 155 | #Mass flow cooling water 156 | M=u/3600*1000 #[kg/s] 157 | #Define Tin = 5 C, Tout=TReactor 158 | #heat capacity water = 4190 J/kgK 159 | Tin = 5 160 | #Estimate water at outlet same as Temp in reactor 161 | Tout = C[5] 162 | cpc = 4190 163 | #Calculate Q from Eq 9.47 164 | Q=-M*cpc*(Tout-Tin) # J/s 165 | #Calculate Temperature change 166 | dTdt = -1*(dHrxndt - Q + W)/(C[4]*1000*4.1868) #[K/s] 167 | else: 168 | dTdt = 0 169 | return [dSdt, dPdt, dXdt, dVdt, dTdt] 170 | 171 | def solve(self): 172 | #solve normal: 173 | t = np.linspace(self.t_start, self.t_end, self.steps) 174 | if self.Control == False : 175 | u = 0 176 | C0 = [self.S0, self.P0, self.X0,self.V0, self.T0] 177 | C = odeint(self.rxn, C0, t, rtol = 1e-7, mxstep= 500000, args=(u,)) 178 | 179 | #solve for Control 180 | else: 181 | """ 182 | PID Temperature Control: 183 | """ 184 | # storage for recording values 185 | C = np.ones([len(t), 6]) 186 | C0 = [self.S0, self.P0, self.X0,self.V0,self.T0] 187 | self.ctrl_output = np.zeros(len(t)) # controller output 188 | e = np.zeros(len(t)) # error 189 | ie = np.zeros(len(t)) # integral of the error 190 | dpv = np.zeros(len(t)) # derivative of the pv 191 | P = np.zeros(len(t)) # proportional 192 | I = np.zeros(len(t)) # integral 193 | D = np.zeros(len(t)) # derivative 194 | 195 | for i in range(len(t)-1): 196 | #print(t[i]) 197 | #PID control of cooling water 198 | dt = t[i+1]-t[i] 199 | #Error 200 | e[i] = C[i,5] - self.Tset 201 | #print(e[i]) 202 | if i >= 1: 203 | dpv[i] = (C[i,5]-C[i-1,5])/dt 204 | ie[i] = ie[i-1] + e[i]*dt 205 | P[i]=self.K_p*e[i] 206 | I[i]=self.K_i*ie[i] 207 | D[i]=self.K_d*dpv[i] 208 | 209 | self.ctrl_output[i]=P[i]+I[i]+D[i] 210 | u=self.ctrl_output[i] 211 | if u>self.u_max: 212 | u=self.u_max 213 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 214 | if u < self.u_min: 215 | u =self.u_min 216 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 217 | #time for solving ODE 218 | ts = [t[i],t[i+1]] 219 | #disturbance 220 | #if self.t[i] > 5 and self.t[i] < 10: 221 | # u = 0 222 | #solve ODE from last timepoint to new timepoint with old values 223 | 224 | y = odeint(self.rxn, C0, ts, rtol = 1e-7, mxstep= 500000, args=(u,)) 225 | #update C0 226 | C0 = y[-1] 227 | #merge y to C 228 | C[i+1]=y[-1] 229 | 230 | return t, C 231 | 232 | 233 | -------------------------------------------------------------------------------- /Rx_Fermentation_Scerevisiae_Glucose_Aerobic_Batch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Thu Sep 6 13:34:32 2018 4 | 5 | @author: bjogut and simoca 6 | """ 7 | 8 | 9 | from scipy.integrate import odeint 10 | #Package for plotting 11 | import math 12 | #Package for the use of vectors and matrix 13 | import numpy as np 14 | import array as arr 15 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 16 | from matplotlib.figure import Figure 17 | import sys 18 | import os 19 | import matplotlib.pyplot as plt 20 | from matplotlib.ticker import FormatStrFormatter 21 | import glob 22 | from random import sample 23 | import random 24 | import time 25 | 26 | 27 | class SCerevisiae_Aero: 28 | def __init__(self, Control = False): 29 | self.Yox_XG = 0.8 30 | self.Yred_XG = 0.05 31 | self.Yox_XE = 0.72 32 | self.Y_OG = 1.067 33 | self.Y_EG = 0.5 34 | self.Y_OE = 1.5 35 | self.q_g = 3.5 36 | self.q_o = 0.37 37 | self.q_e = 0.32 38 | self.t_lag = 4.66 39 | self.Kg = 0.17 40 | self.Ke = 0.56 41 | self.Ko = 0.0001 42 | self.Ki = 0.31 43 | self.O_sat = 0.00755 44 | self.kla = 1004 45 | 46 | self.G0 = 18 47 | self.E0 = 0.0 48 | self.O0 = 0.00755 49 | self.X0 = 0.1 50 | 51 | self.t_end = 30 52 | self.t_start = 0 53 | self.V0 = 2 54 | 55 | #parameters for control, default every 1/24 hours: 56 | self.Control = Control 57 | self.coolingOn = True 58 | self.Contamination=False 59 | self.steps = (self.t_end - self.t_start)*24 60 | self.T0 = 30 61 | self.K_p = 2.31e+01 62 | self.K_i = 3.03e-01 63 | self.K_d = -3.58e-03 64 | self.Tset = 30 65 | self.u_max = 150 66 | self.u_min = 0 67 | 68 | def rxn(self, C,t , u, fc): 69 | #when there is no control, k has no effect 70 | k=1 71 | #when cooling is off than u = 0 72 | if self.coolingOn == False: 73 | u = 0 74 | if self.Contamination == True: 75 | fc=np.random.randint(0,10) 76 | fc=fc/17 77 | 78 | if self.Control == True : 79 | #Cardinal temperature model with inflection: Salvado et al 2011 "Temperature Adaptation Markedly Determines Evolution within the Genus Saccharomyces" 80 | #Strain S. cerevisiae PE35 M 81 | Topt = 30 82 | Tmax = 45.48 83 | Tmin = 5.04 84 | T = C[5] 85 | if T < Tmin or T > Tmax: 86 | k = 0 87 | else: 88 | D = (T-Tmax)*(T-Tmin)**2 89 | E = (Topt-Tmin)*((Topt-Tmin)*(T-Topt)-(Topt-Tmax)*(Topt+Tmin-2*T)) 90 | k = D/E 91 | 92 | #number of components 93 | n = 4 94 | m = 4 95 | #initialize the stoichiometric matrix, s 96 | s = np.zeros((m,n)) 97 | s[0,0] = -1 98 | s[0,1] = 0 99 | s[0,2] = -self.Y_OG 100 | s[0,3] = self.Yox_XG 101 | 102 | s[1,0] = -1 103 | s[1,1] = self.Y_EG 104 | s[1,2] = 0 105 | s[1,3] = self.Yred_XG 106 | 107 | s[2,0] = 0 108 | s[2,1] = -1 109 | s[2,2] = -self.Y_OE 110 | s[2,3] = self.Yox_XE 111 | 112 | s[3,0] = 0 113 | s[3,1] = 0 114 | s[3,2] = 1 115 | s[3,3] = 0 116 | #initialize the rate vector 117 | rho = np.zeros((4,1)) 118 | ##initialize the overall conversion vector 119 | r=np.zeros((4,1)) 120 | rho[0,0] = k*((1/self.Y_OG)*min(self.q_o*(C[2]/(C[2]+self.Ko)),self.Y_OG*(self.q_g*(C[0]/(C[0]+self.Kg)))))*C[3] 121 | rho[1,0] = k*((1-math.exp(-t/self.t_lag))*((self.q_g*(C[0]/(C[0]+self.Kg)))-(1/self.Y_OG)*min(self.q_o*(C[2]/(C[2]+self.Ko)),self.Y_OG*(self.q_g*(C[0]/(C[0]+self.Kg))))))*C[3] 122 | rho[2,0] = k*((1/self.Y_OE)*min(self.q_o*(C[2]/(C[2]+self.Ko))-(1/self.Y_OG)*min(self.q_o*(C[2]/(C[2]+self.Ko)),self.Y_OG*(self.q_g*(C[0]/(C[0]+self.Kg)))),self.Y_OE*(self.q_e*(C[1]/(C[1]+self.Ke))*(self.Ki/(C[0]+self.Ki)))))*C[3] 123 | rho[3,0] = self.kla*(self.O_sat - C[2]) 124 | 125 | #Developing the matrix, the overall conversion rate is stoichiometric *rates 126 | r[0,0] = (s[0,0]*rho[0,0])+(s[1,0]*rho[1,0])+(s[2,0]*rho[2,0])+(s[3,0]*rho[3,0]) 127 | r[1,0] = (s[0,1]*rho[0,0])+(s[1,1]*rho[1,0])+(s[2,1]*rho[2,0])+(s[3,1]*rho[3,0]) 128 | r[2,0] = (s[0,2]*rho[0,0])+(s[1,2]*rho[1,0])+(s[2,2]*rho[2,0])+(s[3,2]*rho[3,0]) 129 | r[3,0] = (s[0,3]*rho[0,0])+(s[1,3]*rho[1,0])+(s[2,3]*rho[2,0])+(s[3,3]*rho[3,0]) 130 | 131 | #Solving the mass balances 132 | dGdt = r[0,0] 133 | dEdt = r[1,0]*fc 134 | dOdt = r[2,0] 135 | dXdt = r[3,0] 136 | dVdt = 0 137 | if self.Control == True : 138 | ''' 139 | dHrxn heat produced by cells estimated by yeast heat combustion coeficcient dhc0 = -21.2 kJ/g 140 | dHrxn = dGdt*V*dhc0(G)-dEdt*V*dhc0(E)-dXdt*V*dhc0(X) 141 | (when cooling is working) Q = - dHrxn -W , 142 | dT = V[L] * 1000 g/L / 4.1868 [J/gK]*dE [kJ]*1000 J/KJ 143 | dhc0(EtOH) = -1366.8 kJ/gmol/46 g/gmol [KJ/g] 144 | dhc0(Glc) = -2805 kJ/gmol/180g/gmol [KJ/g] 145 | 146 | ''' 147 | #Metabolic heat: [W]=[J/s], dhc0 from book "Bioprocess Engineering Principles" (Pauline M. Doran) : Appendix Table C.8 148 | dHrxndt = dXdt*C[4]*(-21200) #[J/s] + dGdt*C[4]*(15580)- dEdt*C[4]*(29710) 149 | #Shaft work 1 W/L1 150 | W = 1*C[4] #[J/S] negative because exothermic 151 | #Cooling just an initial value (constant cooling to see what happens) 152 | #dQdt = -0.03*C[4]*(-21200) #[J/S] 153 | #velocity of cooling water: u [m3/h] -->controlled by PID 154 | 155 | #Mass flow cooling water 156 | M=u/3600*1000 #[kg/s] 157 | #Define Tin = 5 C, Tout=TReactor 158 | #heat capacity water = 4190 J/kgK 159 | Tin = 5 160 | #Estimate water at outlet same as Temp in reactor 161 | Tout = C[5] 162 | cpc = 4190 163 | #Calculate Q from Eq 9.47 164 | Q=-M*cpc*(Tout-Tin) # J/s 165 | #Calculate Temperature change 166 | dTdt = -1*(dHrxndt - Q + W)/(C[4]*1000*4.1868) #[K/s] 167 | else: 168 | dTdt = 0 169 | return [dGdt,dEdt,dOdt,dXdt,dVdt, dTdt] 170 | 171 | def solve(self): 172 | #solve normal: 173 | t = np.linspace(self.t_start, self.t_end, self.steps) 174 | if self.Control == False : 175 | u = 0 176 | fc= 1 177 | C0 = [self.G0, self.E0, self.O0, self.X0,self.V0,self.T0] 178 | C = odeint(self.rxn, C0, t, rtol = 1e-7, mxstep= 500000, args=(u,fc,)) 179 | 180 | #solve for Control 181 | else: 182 | fc=0 183 | """ 184 | PID Temperature Control: 185 | """ 186 | # storage for recording values 187 | C = np.ones([len(t), 6]) 188 | C0 = [self.G0, self.E0, self.O0, self.X0,self.V0,self.T0] 189 | C[0] = C0 190 | self.ctrl_output = np.zeros(len(t)) # controller output 191 | e = np.zeros(len(t)) # error 192 | ie = np.zeros(len(t)) # integral of the error 193 | dpv = np.zeros(len(t)) # derivative of the pv 194 | P = np.zeros(len(t)) # proportional 195 | I = np.zeros(len(t)) # integral 196 | D = np.zeros(len(t)) # derivative 197 | 198 | for i in range(len(t)-1): 199 | #print(t[i]) 200 | #PID control of cooling water 201 | dt = t[i+1]-t[i] 202 | #Error 203 | e[i] = C[i,5] - self.Tset 204 | #print(e[i]) 205 | if i >= 1: 206 | dpv[i] = (C[i,5]-C[i-1,5])/dt 207 | ie[i] = ie[i-1] + e[i]*dt 208 | P[i]=self.K_p*e[i] 209 | I[i]=self.K_i*ie[i] 210 | D[i]=self.K_d*dpv[i] 211 | 212 | self.ctrl_output[i]=P[i]+I[i]+D[i] 213 | u=self.ctrl_output[i] 214 | if u>self.u_max: 215 | u=self.u_max 216 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 217 | if u < self.u_min: 218 | u =self.u_min 219 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 220 | #time for solving ODE 221 | ts = [t[i],t[i+1]] 222 | #disturbance 223 | #if self.t[i] > 5 and self.t[i] < 10: 224 | # u = 0 225 | #solve ODE from last timepoint to new timepoint with old values 226 | 227 | y = odeint(self.rxn, C0, ts, rtol = 1e-7, mxstep= 500000, args=(u,fc,)) 228 | #update C0 229 | C0 = y[-1] 230 | #merge y to C 231 | C[i+1]=y[-1] 232 | return t, C 233 | 234 | -------------------------------------------------------------------------------- /Rx_Fermentation_Ecoli_Glucose_Aerobic_Batch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Friday Jan 24 13:34:32 2020 4 | 5 | @author: simoca 6 | """ 7 | from scipy.integrate import odeint 8 | #Package for plotting 9 | import math 10 | #Package for the use of vectors and matrix 11 | import numpy as np 12 | import pandas as pd 13 | import array as arr 14 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 15 | from matplotlib.figure import Figure 16 | import sys 17 | import os 18 | import matplotlib.pyplot as plt 19 | from matplotlib.ticker import FormatStrFormatter 20 | import glob 21 | from random import sample 22 | import random 23 | import time 24 | import plotly 25 | import plotly.graph_objs as go 26 | import json 27 | from plotly.subplots import make_subplots 28 | 29 | 30 | class Ecoli_Aero: 31 | def __init__(self, Control = False): 32 | self.Kap=0.5088 #g/L 33 | self.Ksa=0.0128 34 | self.Kia=1.2602 #g/L 35 | self.Ks= 0.0381 #g/L 36 | self.Kis = 1.8383 #g/L affinity constant 37 | self.Ko = 0.0001 38 | self.qAcmax= 0.1148 #g/gh 39 | self.qm=0.0133 #g/gh 40 | self.qOmax= 13.4*31.9988/1000 #g/gh 41 | self.qSmax= 0.635 #g/gh 42 | self.Yas= 0.8938 #g/g 43 | self.Yoa= 0.5221 #g/g 44 | self.Yos= 1.5722 #g/g 45 | self.Yxa= 0.5794 #g/g 46 | self.Yem = 0.5321 #g/g 47 | self.Yxsof = 0.229 # g/g 48 | self.pAmax= 0.2286 #gA/gXh 49 | self.kla= 220 50 | self.H= 1400 #Henry constant 51 | 52 | self.G0 = 4.94 53 | self.A0 = 0.0129 54 | self.O0 = 0.0977 55 | self.X0 = 0.17 56 | self.tau= 35 #response time 57 | self.t_start = 0 58 | self.V0 = 2 59 | self.F0 = 0 60 | self.SFR = 1.5 61 | self.t_expfb_start = 0 62 | self.t_constfb_start = 1.5 63 | self.t_end = 30 64 | 65 | #parameters for control, default every 1/24 hours: 66 | self.Control = Control 67 | self.coolingOn = True 68 | self.Contamination=False 69 | self.steps = (self.t_end - self.t_start)*24 70 | self.T0 = 35 71 | self.K_p = 2.31e+01 72 | self.K_i = 1 73 | self.K_d = 0 74 | self.Tset = 30 75 | self.u_max = 150 76 | self.u_min = 0 77 | 78 | def update_param_value(self, param, new_value): 79 | 80 | class_name = str(self.__class__).split('.')[1].split("'")[0] 81 | param_current_value = self.__dict__.get(param, None) 82 | 83 | if param_current_value is None: 84 | print("Class {} does not contain param with name {}".format(class_name, param)) 85 | return 86 | 87 | self.__dict__[param] = new_value 88 | print("Value of param {} in class {} updated to {}".format(param, class_name, new_value)) 89 | 90 | 91 | def rxn(self, C,t , u, fc): 92 | #when there is no control, k has no effect 93 | k=1 94 | #when cooling is off than u = 0 95 | if self.coolingOn == False: 96 | u = 0 97 | if self.Contamination == True: 98 | fc=np.random.randint(0,10) 99 | fc=fc/17 100 | 101 | if self.Control == True : 102 | #Cardinal temperature model with inflection: Salvado et al 2011 "Temperature Adaptation Markedly Determines Evolution within the Genus Saccharomyces" 103 | #Strain E.coli W310 104 | Topt = 35 105 | Tmax = 45.48 106 | Tmin = 10 107 | T = C[5] 108 | if T < Tmin or T > Tmax: 109 | k = 0 110 | else: 111 | D = (T-Tmax)*(T-Tmin)**2 112 | E = (Topt-Tmin)*((Topt-Tmin)*(T-Topt)-(Topt-Tmax)*(Topt+Tmin-2*T)) 113 | k = D/E 114 | # Volume balance 115 | if (t >= self.t_expfb_start): 116 | if (t < self.t_constfb_start): 117 | Fin = self.F0 * math.exp(self.SFR * (t - self.t_expfb_start)) 118 | Fout = 0 119 | else: 120 | Fin = self.F0 * math.exp(self.SFR * (self.t_constfb_start - self.t_expfb_start)) 121 | Fout = 0 122 | else: 123 | Fin = 0 124 | Fout = 0 125 | 126 | F = Fin - Fout 127 | 128 | qS = (self.qSmax/(1+C[1]/self.Kia))*(C[0]/(C[0]+self.Ks)) 129 | qSof = self.pAmax*(qS/(qS+self.Kap)) 130 | pA = qSof*self.Yas 131 | qSox = (qS-qSof)*(C[2]/(C[2]+self.Ko)) 132 | qSan = (qSox-self.qm)*self.Yem*(0.488/0.391) 133 | qsA = (self.qAcmax/(1+(qS/self.Kis)))*(C[1]/(C[1]+self.Ksa)) 134 | qA = pA - qsA 135 | mu = (qSox - self.qm)*self.Yem+ qsA*self.Yxa + qSof*self.Yxsof 136 | qO = self.Yos*(qSox-qSan)+qsA*self.Yoa 137 | 138 | #Solving the mass balances 139 | dGdt = -(qS*C[3]) 140 | dAdt = qA*C[3] 141 | dOdt = self.kla*(self.O0-C[2]) 142 | dXdt = (mu)*C[3] 143 | dVdt = 0 144 | if self.Control == True : 145 | ''' 146 | dHrxn heat produced by cells estimated by yeast heat combustion coeficcient dhc0 = -21.2 kJ/g 147 | dHrxn = dGdt*V*dhc0(G)-dEdt*V*dhc0(E)-dXdt*V*dhc0(X) 148 | (when cooling is working) Q = - dHrxn -W , 149 | dT = V[L] * 1000 g/L / 4.1868 [J/gK]*dE [kJ]*1000 J/KJ 150 | dhc0(EtOH) = -1366.8 kJ/gmol/46 g/gmol [KJ/g] 151 | dhc0(Glc) = -2805 kJ/gmol/180g/gmol [KJ/g] 152 | 153 | ''' 154 | #Metabolic heat: [W]=[J/s], dhc0 from book "Bioprocess Engineering Principles" (Pauline M. Doran) : Appendix Table C.8 155 | dHrxndt = dXdt*C[4]*(-21200) #[J/s] + dGdt*C[4]*(15580)- dEdt*C[4]*(29710) 156 | #Shaft work 1 W/L1 157 | W = 1*C[4] #[J/S] negative because exothermic 158 | #Cooling just an initial value (constant cooling to see what happens) 159 | #dQdt = -0.03*C[4]*(-21200) #[J/S] 160 | #velocity of cooling water: u [m3/h] -->controlled by PID 161 | 162 | #Mass flow cooling water 163 | M=u/3600*1000 #[kg/s] 164 | #Define Tin = 5 C, Tout=TReactor 165 | #heat capacity water = 4190 J/kgK 166 | Tin = 5 167 | #Estimate water at outlet same as Temp in reactor 168 | Tout = C[5] 169 | cpc = 4190 170 | #Calculate Q from Eq 9.47 171 | Q=-M*cpc*(Tout-Tin) # J/s 172 | #Calculate Temperature change 173 | dTdt = -1*(dHrxndt - Q + W)/(C[4]*1000*4.1868) #[K/s] 174 | else: 175 | dTdt = 0 176 | return [dGdt,dAdt,dOdt,dXdt,dVdt, dTdt] 177 | 178 | def solve(self): 179 | #solve normal: 180 | t = np.linspace(self.t_start, self.t_end, self.steps) 181 | if self.Control == False : 182 | u = 0 183 | fc= 1 184 | C0 = [self.G0, self.A0, self.O0, self.X0,self.V0,self.T0] 185 | C = odeint(self.rxn, C0, t, rtol = 1e-7, mxstep= 500000, args=(u,fc,)) 186 | 187 | #solve for Control 188 | else: 189 | fc=0 190 | """ 191 | PID Temperature Control: 192 | """ 193 | # storage for recording values 194 | C = np.ones([len(t), 6]) 195 | C0 = [self.G0, self.A0, self.O0, self.X0,self.V0,self.T0] 196 | self.ctrl_output = np.zeros(len(t)) # controller output 197 | e = np.zeros(len(t)) # error 198 | ie = np.zeros(len(t)) # integral of the error 199 | dpv = np.zeros(len(t)) # derivative of the pv 200 | P = np.zeros(len(t)) # proportional 201 | I = np.zeros(len(t)) # integral 202 | D = np.zeros(len(t)) # derivative 203 | 204 | for i in range(len(t)-1): 205 | #print(t[i]) 206 | #PID control of cooling water 207 | dt = t[i+1]-t[i] 208 | #Error 209 | e[i] = C[i,5] - self.Tset 210 | #print(e[i]) 211 | if i >= 1: 212 | dpv[i] = (C[i,5]-C[i-1,5])/dt 213 | ie[i] = ie[i-1] + e[i]*dt 214 | P[i]=self.K_p*e[i] 215 | I[i]=self.K_i*ie[i] 216 | D[i]=self.K_d*dpv[i] 217 | 218 | self.ctrl_output[i]=P[i]+I[i]+D[i] 219 | u=self.ctrl_output[i] 220 | if u>self.u_max: 221 | u=self.u_max 222 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 223 | if u < self.u_min: 224 | u =self.u_min 225 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 226 | #time for solving ODE 227 | ts = [t[i],t[i+1]] 228 | #disturbance 229 | #if self.t[i] > 5 and self.t[i] < 10: 230 | # u = 0 231 | #solve ODE from last timepoint to new timepoint with old values 232 | 233 | y = odeint(self.rxn, C0, ts, rtol = 1e-7, mxstep= 500000, args=(u,fc,)) 234 | #update C0 235 | C0 = y[-1] 236 | #merge y to C 237 | C[i+1]=y[-1] 238 | return t, C 239 | def create_plot(self, t, C): 240 | figure = make_subplots(rows=2, cols=1) 241 | [self.G0, self.A0, self.O0, self.X0, self.V0, self.T0] 242 | G = C[:, 0] 243 | A = C[:, 1] 244 | B = C[:, 3] 245 | O = C[:, 2] 246 | V = C[:, 4] 247 | df = pd.DataFrame({'t': t, 'G': G, 'B': B, 'A': A, 'O': O, 'V': V}) 248 | figure.append_trace(go.Scatter(x=df['t'], y=df['G'], name='Glucose'), row=1, col=1) 249 | figure.append_trace(go.Scatter(x=df['t'], y=df['O'], name='Oxygen'), row=1, col=1) 250 | figure.append_trace(go.Scatter(x=df['t'], y=df['B'], name='Biomass'), row=1, col=1) 251 | figure.append_trace(go.Scatter(x=df['t'], y=df['A'], name='Acetate'), row=1, col=1) 252 | # fig.update_layout(title=('Simulation of the model for the Scerevisiae'), 253 | # xaxis_title='time (h)', 254 | # yaxis_title='Concentration (g/L)') 255 | 256 | 257 | Tset = self.T0 258 | df2 = pd.DataFrame({'t': t, 'T': T, 'Tset': Tset}) 259 | figure.append_trace(go.Scatter(x=df2['t'], y=df2['T'], name='Temperature'), row=2, col=1) 260 | figure.append_trace(go.Scatter(x=df2['t'], y=df2['Tset'], name='Set Value Temperature'), row=2, col=1) 261 | figure.update_layout(title=('Simulation of the model for the Saccharomyces cerevisiae'), 262 | xaxis_title='time (h)', 263 | yaxis_title='Concentration (g/L)') 264 | 265 | S = C[:, 0] 266 | A = C[:, 1] 267 | B = C[:, 3] 268 | df = pd.DataFrame({'t': t, 'Substrate': S, 'Biomass': B, 'Acetate': A}) 269 | fig = go.Figure() 270 | fig.add_trace(go.Scatter(x=df['t'], y=df['Substrate'], name='Glucose')) 271 | fig.add_trace(go.Scatter(x=df['t'], y=df['Biomass'], name='Biomass')) 272 | fig.add_trace(go.Scatter(x=df['t'], y=df['Acetate'], name='Acetate')) 273 | fig.update_layout(title=('Simulation of aerobic batch growth of Escherichia coli by acetate cycling'), 274 | xaxis_title='time (h)', 275 | yaxis_title='Concentration (g/L)') 276 | print('print') 277 | graphJson = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) 278 | return graphJson 279 | 280 | 281 | 282 | f= Ecoli_Aero() 283 | f.solve() 284 | t =f.solve()[0] 285 | C= f.solve()[1] 286 | print(C) 287 | 288 | plt.plot(t, C[:,0]) 289 | plt.plot(t, C[:,1]) 290 | plt.plot(t, C[:,2]) 291 | plt.plot(t, C[:,3]) 292 | plt.title("Trial") 293 | plt.xlabel("Time (h)") 294 | plt.ylabel("Concentration") 295 | plt.show() 296 | -------------------------------------------------------------------------------- /Rx_Fermentation_Ecoli_Glucose_Aerobic_Fedbatch.py: -------------------------------------------------------------------------------- 1 | # -*- coding: utf-8 -*- 2 | """ 3 | Created on Friday Jan 24 13:34:32 2020 4 | 5 | @author: simoca 6 | """ 7 | from scipy.integrate import odeint 8 | #Package for plotting 9 | import math 10 | #Package for the use of vectors and matrix 11 | import numpy as np 12 | import pandas as pd 13 | import array as arr 14 | from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg as FigureCanvas 15 | from matplotlib.figure import Figure 16 | import sys 17 | import os 18 | import matplotlib.pyplot as plt 19 | from matplotlib.ticker import FormatStrFormatter 20 | import glob 21 | from random import sample 22 | import random 23 | import time 24 | import plotly 25 | import plotly.graph_objs as go 26 | import json 27 | from plotly.subplots import make_subplots 28 | 29 | 30 | class Ecoli_Aero: 31 | def __init__(self, Control = False): 32 | self.Kap=0.5088 #g/L 33 | self.Ksa=0.0128 34 | self.Kia=1.2602 #g/L 35 | self.Ks= 0.0381 #g/L 36 | self.Kis = 1.8383 #g/L affinity constant 37 | self.Ko = 0.0001 38 | self.qAcmax= 0.1148 #g/gh 39 | self.qm=0.0133 #g/gh 40 | self.qOmax= 13.4*31.9988/1000 #g/gh 41 | self.qSmax= 0.635 #g/gh 42 | self.Yas= 0.8938 #g/g 43 | self.Yoa= 0.5221 #g/g 44 | self.Yos= 1.5722 #g/g 45 | self.Yxa= 0.5794 #g/g 46 | self.Yem = 0.5321 #g/g 47 | self.Yxsof = 0.229 # g/g 48 | self.pAmax= 0.2286 #gA/gXh 49 | self.kla= 220 50 | self.H= 1400 #Henry constant 51 | 52 | self.G0 = 4.94 53 | self.A0 = 0.0129 54 | self.O0 = 98 55 | self.X0 = 0.17 56 | self.tau= 35 #response time 57 | self.t_start = 0 58 | self.V0 = 2 59 | self.F0 = 0 60 | self.SFR = 1.5 61 | self.t_expfb_start = 0 62 | self.t_constfb_start = 1.5 63 | self.t_end = 15 64 | 65 | #parameters for control, default every 1/24 hours: 66 | self.Control = Control 67 | self.coolingOn = True 68 | self.Contamination=False 69 | self.steps = (self.t_end - self.t_start)*24 70 | self.T0 = 35 71 | self.K_p = 2.31e+01 72 | self.K_i = 1 73 | self.K_d = 0 74 | self.Tset = 30 75 | self.u_max = 150 76 | self.u_min = 0 77 | 78 | def update_param_value(self, param, new_value): 79 | 80 | class_name = str(self.__class__).split('.')[1].split("'")[0] 81 | param_current_value = self.__dict__.get(param, None) 82 | 83 | if param_current_value is None: 84 | print("Class {} does not contain param with name {}".format(class_name, param)) 85 | return 86 | 87 | self.__dict__[param] = new_value 88 | print("Value of param {} in class {} updated to {}".format(param, class_name, new_value)) 89 | 90 | 91 | def rxn(self, C,t , u, fc): 92 | #when there is no control, k has no effect 93 | k=1 94 | #when cooling is off than u = 0 95 | if self.coolingOn == False: 96 | u = 0 97 | if self.Contamination == True: 98 | fc=np.random.randint(0,10) 99 | fc=fc/17 100 | 101 | if self.Control == True : 102 | #Cardinal temperature model with inflection: Salvado et al 2011 "Temperature Adaptation Markedly Determines Evolution within the Genus Saccharomyces" 103 | #Strain E.coli W310 104 | Topt = 35 105 | Tmax = 45.48 106 | Tmin = 10 107 | T = C[5] 108 | if T < Tmin or T > Tmax: 109 | k = 0 110 | else: 111 | D = (T-Tmax)*(T-Tmin)**2 112 | E = (Topt-Tmin)*((Topt-Tmin)*(T-Topt)-(Topt-Tmax)*(Topt+Tmin-2*T)) 113 | k = D/E 114 | # Volume balance 115 | if (t >= self.t_expfb_start): 116 | if (t < self.t_constfb_start): 117 | Fin = self.F0 * math.exp(self.SFR * (t - self.t_expfb_start)) 118 | Fout = 0 119 | else: 120 | Fin = self.F0 * math.exp(self.SFR * (self.t_constfb_start - self.t_expfb_start)) 121 | Fout = 0 122 | else: 123 | Fin = 0 124 | Fout = 0 125 | 126 | F = Fin - Fout 127 | 128 | qS = (self.qSmax/(1+C[1]/self.Kia))*(C[0]/(C[0]+self.Ks)) 129 | qSof = self.pAmax*(qS/(qS+self.Kap)) 130 | pA = qSof*self.Yas 131 | qSox = (qS-qSof)*(C[2]/(C[2]+self.Ko)) 132 | qSan = (qSox-self.qm)*self.Yem*(0.488/0.391) 133 | qsA = (self.qAcmax/(1+(qS/self.Kis)))*(C[1]/(C[1]+self.Ksa)) 134 | qA = pA - qsA 135 | mu = (qSox - self.qm)*self.Yem+ qsA*self.Yxa + qSof*self.Yxsof 136 | qO = self.Yos*(qSox-qSan)+qsA*self.Yoa 137 | 138 | #Solving the mass balances 139 | dGdt = (F/C[4])*(self.G0-C[0])-(qS*C[3]) 140 | dAdt = qA*C[3]-((F/C[4])*C[1]) 141 | dOdt = self.kla*(self.O0-C[2])-qO*C[3]*self.H 142 | dXdt = (mu - (F/C[4]))*C[3] 143 | dVdt = F 144 | if self.Control == True : 145 | ''' 146 | dHrxn heat produced by cells estimated by yeast heat combustion coeficcient dhc0 = -21.2 kJ/g 147 | dHrxn = dGdt*V*dhc0(G)-dEdt*V*dhc0(E)-dXdt*V*dhc0(X) 148 | (when cooling is working) Q = - dHrxn -W , 149 | dT = V[L] * 1000 g/L / 4.1868 [J/gK]*dE [kJ]*1000 J/KJ 150 | dhc0(EtOH) = -1366.8 kJ/gmol/46 g/gmol [KJ/g] 151 | dhc0(Glc) = -2805 kJ/gmol/180g/gmol [KJ/g] 152 | 153 | ''' 154 | #Metabolic heat: [W]=[J/s], dhc0 from book "Bioprocess Engineering Principles" (Pauline M. Doran) : Appendix Table C.8 155 | dHrxndt = dXdt*C[4]*(-21200) #[J/s] + dGdt*C[4]*(15580)- dEdt*C[4]*(29710) 156 | #Shaft work 1 W/L1 157 | W = 1*C[4] #[J/S] negative because exothermic 158 | #Cooling just an initial value (constant cooling to see what happens) 159 | #dQdt = -0.03*C[4]*(-21200) #[J/S] 160 | #velocity of cooling water: u [m3/h] -->controlled by PID 161 | 162 | #Mass flow cooling water 163 | M=u/3600*1000 #[kg/s] 164 | #Define Tin = 5 C, Tout=TReactor 165 | #heat capacity water = 4190 J/kgK 166 | Tin = 5 167 | #Estimate water at outlet same as Temp in reactor 168 | Tout = C[5] 169 | cpc = 4190 170 | #Calculate Q from Eq 9.47 171 | Q=-M*cpc*(Tout-Tin) # J/s 172 | #Calculate Temperature change 173 | dTdt = -1*(dHrxndt - Q + W)/(C[4]*1000*4.1868) #[K/s] 174 | else: 175 | dTdt = 0 176 | return [dGdt,dAdt,dOdt,dXdt,dVdt, dTdt] 177 | 178 | def solve(self): 179 | #solve normal: 180 | t = np.linspace(self.t_start, self.t_end, self.steps) 181 | if self.Control == False : 182 | u = 0 183 | fc= 1 184 | C0 = [self.G0, self.A0, self.O0, self.X0,self.V0,self.T0] 185 | C = odeint(self.rxn, C0, t, rtol = 1e-7, mxstep= 500000, args=(u,fc,)) 186 | 187 | #solve for Control 188 | else: 189 | fc=0 190 | """ 191 | PID Temperature Control: 192 | """ 193 | # storage for recording values 194 | C = np.ones([len(t), 6]) 195 | C0 = [self.G0, self.A0, self.O0, self.X0,self.V0,self.T0] 196 | self.ctrl_output = np.zeros(len(t)) # controller output 197 | e = np.zeros(len(t)) # error 198 | ie = np.zeros(len(t)) # integral of the error 199 | dpv = np.zeros(len(t)) # derivative of the pv 200 | P = np.zeros(len(t)) # proportional 201 | I = np.zeros(len(t)) # integral 202 | D = np.zeros(len(t)) # derivative 203 | 204 | for i in range(len(t)-1): 205 | #print(t[i]) 206 | #PID control of cooling water 207 | dt = t[i+1]-t[i] 208 | #Error 209 | e[i] = C[i,5] - self.Tset 210 | #print(e[i]) 211 | if i >= 1: 212 | dpv[i] = (C[i,5]-C[i-1,5])/dt 213 | ie[i] = ie[i-1] + e[i]*dt 214 | P[i]=self.K_p*e[i] 215 | I[i]=self.K_i*ie[i] 216 | D[i]=self.K_d*dpv[i] 217 | 218 | self.ctrl_output[i]=P[i]+I[i]+D[i] 219 | u=self.ctrl_output[i] 220 | if u>self.u_max: 221 | u=self.u_max 222 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 223 | if u < self.u_min: 224 | u =self.u_min 225 | ie[i] = ie[i] - e[i]*dt # anti-reset windup 226 | #time for solving ODE 227 | ts = [t[i],t[i+1]] 228 | #disturbance 229 | #if self.t[i] > 5 and self.t[i] < 10: 230 | # u = 0 231 | #solve ODE from last timepoint to new timepoint with old values 232 | 233 | y = odeint(self.rxn, C0, ts, rtol = 1e-7, mxstep= 500000, args=(u,fc,)) 234 | #update C0 235 | C0 = y[-1] 236 | #merge y to C 237 | C[i+1]=y[-1] 238 | return t, C 239 | def create_plot(self, t, C): 240 | figure = make_subplots(rows=2, cols=1) 241 | [self.G0, self.A0, self.O0, self.X0, self.V0, self.T0] 242 | G = C[:, 0] 243 | A = C[:, 1] 244 | B = C[:, 3] 245 | O = C[:, 2] 246 | V = C[:, 4] 247 | df = pd.DataFrame({'t': t, 'G': G, 'B': B, 'A': A, 'O': O, 'V': V}) 248 | figure.append_trace(go.Scatter(x=df['t'], y=df['G'], name='Glucose'), row=1, col=1) 249 | figure.append_trace(go.Scatter(x=df['t'], y=df['O'], name='Oxygen'), row=1, col=1) 250 | figure.append_trace(go.Scatter(x=df['t'], y=df['B'], name='Biomass'), row=1, col=1) 251 | figure.append_trace(go.Scatter(x=df['t'], y=df['A'], name='Acetate'), row=1, col=1) 252 | # fig.update_layout(title=('Simulation of the model for the Scerevisiae'), 253 | # xaxis_title='time (h)', 254 | # yaxis_title='Concentration (g/L)') 255 | 256 | 257 | Tset = self.T0 258 | df2 = pd.DataFrame({'t': t, 'T': T, 'Tset': Tset}) 259 | figure.append_trace(go.Scatter(x=df2['t'], y=df2['T'], name='Temperature'), row=2, col=1) 260 | figure.append_trace(go.Scatter(x=df2['t'], y=df2['Tset'], name='Set Value Temperature'), row=2, col=1) 261 | figure.update_layout(title=('Simulation of the model for the Saccharomyces cerevisiae'), 262 | xaxis_title='time (h)', 263 | yaxis_title='Concentration (g/L)') 264 | 265 | S = C[:, 0] 266 | A = C[:, 1] 267 | B = C[:, 3] 268 | df = pd.DataFrame({'t': t, 'Substrate': S, 'Biomass': B, 'Acetate': A}) 269 | fig = go.Figure() 270 | fig.add_trace(go.Scatter(x=df['t'], y=df['Substrate'], name='Glucose')) 271 | fig.add_trace(go.Scatter(x=df['t'], y=df['Biomass'], name='Biomass')) 272 | fig.add_trace(go.Scatter(x=df['t'], y=df['Acetate'], name='Acetate')) 273 | fig.update_layout(title=('Simulation of aerobic batch growth of Escherichia coli by acetate cycling'), 274 | xaxis_title='time (h)', 275 | yaxis_title='Concentration (g/L)') 276 | print('print') 277 | graphJson = json.dumps(fig, cls=plotly.utils.PlotlyJSONEncoder) 278 | return graphJson 279 | 280 | 281 | 282 | f= Ecoli_Aero() 283 | f.solve() 284 | C= f.solve()[1] 285 | print(C) 286 | 287 | plt.plot(f.solve()[0], f.solve()[1][:,0]) 288 | plt.plot(f.solve()[0], f.solve()[1][:,1]) 289 | plt.plot(f.solve()[0], f.solve()[1][:,2]) 290 | plt.plot(f.solve()[0], f.solve()[1][:,3]) 291 | plt.show() 292 | --------------------------------------------------------------------------------