I am trying to use GEKKO on PYTHON to control a cstr. The CVS are the temperature and the level of the tank - gekko

Attached is the code I wrote: When it runs, the level controlled variable is not tracking its setpoint.
On the other hand, the Temperature controlled variable is tracking its setpoint very well. I am using manipulating the cooling temperature and inlet flow rate. I am trying to control the level of the tank, temperature and concentration.
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from gekko import GEKKO
# Steady State Initial Condition
u1_ss = 300.0
u2_ss=100.0
Ca_ss = 0.87725294
T_ss = 324.47544313
h_ss=75.82018806
# Feed Temperature (K)
Tf = 350
# Feed Concentration (mol/m^3)
Caf = 1
# Steady State Initial Conditions for the States
x0 = np.empty(2)
x0[0] = Ca_ss
x0[1] = T_ss
p0=np.empty(1)
p0[0]=h_ss
#%% GEKKO nonlinear MPC
m = GEKKO(remote=False)
m.time = [0,0.02,0.04,0.06,0.08,0.1,0.12,0.15,0.2]
c1=10
Ac=400.0
# Volume of CSTR (m^3)
V = 100
# Density of A-B Mixture (kg/m^3)
rho = 1000
# Heat capacity of A-B Mixture (J/kg-K)
Cp = 0.239
# Heat of reaction for A->B (J/mol)
mdelH = 5e4
# E - Activation energy in the Arrhenius Equation (J/mol)
# R - Universal Gas Constant = 8.31451 J/mol-K
EoverR = 8750
# Pre-exponential factor (1/sec)
k0 = 7.2e10
# U - Overall Heat Transfer Coefficient (W/m^2-K)
# A - Area - this value is specific for the U calculation (m^2)
UA = 5e4
# initial conditions
Tc0 = 300
T0 = 324.47544313
Ca0 = 0.87725294
h0=75.82018806
q0=100.0
tau = m.Const(value=0.5)
Kp = m.Const(value=1)
m.Tc = m.MV(value=Tc0,lb=250,ub=350)
m.T = m.CV(value=T_ss)
m.rA = m.Var(value=0)
m.Ca = m.CV(value=Ca_ss,lb=0,ub=1)
m.h=m.CV(value=h_ss)
m.q=m.MV(value=q0,lb=0,ub=1000)
m.Equation(m.rA == k0*m.exp(-EoverR/m.T)*m.Ca)
m.Equation(m.T.dt() == m.q/V*(Tf - m.T) \
+ mdelH/(rho*Cp)*m.rA \
+ UA/V/rho/Cp*(m.Tc-m.T))
m.Equation(m.Ca.dt() == (m.q)/V*(Caf - m.Ca) - m.rA)
m.Equation(m.h.dt()==(m.q-c1*pow(m.h,0.5))/Ac)
#MV tuning
m.Tc.STATUS = 1
m.Tc.FSTATUS = 0
m.Tc.DMAX = 100
m.Tc.DMAXHI = 20
m.Tc.DMAXLO = -100
m.q.STATUS = 1
m.q.FSTATUS = 0
m.q.DMAX = 10
#CV tuning
m.T.STATUS = 1
m.T.FSTATUS = 1
m.T.TR_INIT = 1
m.T.TAU = 1.0
DT = 0.5 # deadband
m.h.STATUS = 1
m.h.FSTATUS = 1
m.h.TR_INIT = 1
m.h.TAU = 1.0
m.Ca.STATUS = 1
m.Ca.FSTATUS = 0 # no measurement
m.Ca.TR_INIT = 0
m.options.CV_TYPE = 1
m.options.IMODE = 6
m.options.SOLVER = 3
# define CSTR model
def cstr(x,t,u1,u2,Tf,Caf):
# Inputs (3):
# Temperature of cooling jacket (K)
Tc = u1
q=u2
# Tf = Feed Temperature (K)
# Caf = Feed Concentration (mol/m^3)
# States (2):
# Concentration of A in CSTR (mol/m^3)
Ca = x[0]
# Temperature in CSTR (K)
T = x[1]
# Parameters:
# Volume of CSTR (m^3)
V = 100
# Density of A-B Mixture (kg/m^3)
rho = 1000
# Heat capacity of A-B Mixture (J/kg-K)
Cp = 0.239
# Heat of reaction for A->B (J/mol)
mdelH = 5e4
# E - Activation energy in the Arrhenius Equation (J/mol)
# R - Universal Gas Constant = 8.31451 J/mol-K
EoverR = 8750
# Pre-exponential factor (1/sec)
k0 = 7.2e10
# U - Overall Heat Transfer Coefficient (W/m^2-K)
# A - Area - this value is specific for the U calculation (m^2)
UA = 5e4
# reaction rate
rA = k0*np.exp(-EoverR/T)*Ca
# Calculate concentration derivative
dCadt = q/V*(Caf - Ca) - rA
# Calculate temperature derivative
dTdt = q/V*(Tf - T) \
+ mdelH/(rho*Cp)*rA \
+ UA/V/rho/Cp*(Tc-T)
# Return xdot:
xdot = np.zeros(2)
xdot[0] = dCadt
xdot[1] = dTdt
return xdot
def tank(p,t,u2,Ac):
q=u2
h=p[0]
dhdt=(q-c1*pow(h,0.5))/Ac
if p[0]>=300 and dhdt>0:
dhdt = 0
return dhdt
# Time Interval (min)
t = np.linspace(0,10,410)
# Store results for plotting
Ca = np.ones(len(t)) * Ca_ss
T = np.ones(len(t)) * T_ss
Tsp=np.ones(len(t))*T_ss
hsp=np.ones(len(t))*h_ss
h=np.ones(len(t))*h_ss
u1 = np.ones(len(t)) * u1_ss
u2 = np.ones(len(t)) * u2_ss
# Set point steps
Tsp[0:100] = 330.0
Tsp[100:200] = 350.0
hsp[200:300] = 150.0
hsp[300:] = 190.0
# Create plot
plt.figure(figsize=(10,7))
plt.ion()
plt.show()
# Simulate CSTR
for i in range(len(t)-1):
ts = [t[i],t[i+1]]
y = odeint(cstr,x0,ts,args=(u1[i+1],u2[i+1],Tf,Caf))
y1=odeint(tank,p0,ts,args=(u2[i+1],Ac))
Ca[i+1] = y[-1][0]
T[i+1] = y[-1][1]
h[i+1]=y1[-1][0]
# insert measurement
m.T.MEAS = T[i+1]
m.h.MEAS= h[i+1]
# solve MPC
m.solve(disp=True)
m.T.SPHI = Tsp[i+1] + DT
m.T.SPLO = Tsp[i+1] - DT
m.h.SPHI = hsp[i+1] + DT
m.h.SPLO = hsp[i+1] - DT
# retrieve new Tc value
u1[i+1] = m.Tc.NEWVAL
u2[i+1]= m.q.NEWVAL
# update initial conditions
x0[0] = Ca[i+1]
x0[1] = T[i+1]
p0[0]=h[i+1]
plt.clf()
# Plot the results
plt.subplot(5,1,1)
plt.plot(t[0:i],u1[0:i],'b--',linewidth=3)
plt.ylabel('Cooling T (K)')
plt.legend(['Jacket Temperature'],loc='best')
plt.subplot(5,1,2)
plt.plot(t[0:i],u2[0:i],'g--')
plt.xlabel('time')
plt.ylabel('flow in')
plt.subplot(5,1,3)
plt.plot(t[0:i],Ca[0:i],'r-',linewidth=3)
plt.ylabel('Ca (mol/L)')
plt.legend(['Reactor Concentration'],loc='best')
plt.subplot(5,1,4)
plt.plot(t[0:i],Tsp[0:i],'r-',linewidth=3,label=r'$T_{sp}$')
plt.plot(t[0:i],T[0:i],'k.-',linewidth=3,label=r'$T_{meas}$')
plt.ylabel('T (K)')
plt.xlabel('Time (min)')
plt.legend(loc='best')
plt.subplot(5,1,5)
plt.plot(t[0:i],hsp[0:i],'g--',linewidth=3,label=r'$h_{sp}$')
plt.plot(t[0:i],h[0:i],'k.-',linewidth=3,label=r'$h_{meas}$')
plt.xlabel('time')
plt.ylabel('tank level')
plt.legend(loc='best')
plt.draw()
plt.pause(0.01)

One problem is that the function pow is not supported by Gekko and is evaluating that part to a constant. Here is a modified version of your equation that should work better:
m.Equation(m.h.dt()==(m.q-c1*m.h**0.5)/Ac)
One other issue is that your similar is broken into two parts and should be one model:
def tank(p,t,u2,Ac):
q=u2
h=p[0]
dhdt=(q-c1*pow(h,0.5))/Ac
if p[0]>=300 and dhdt>0:
dhdt = 0
return dhdt
You should add a third state to your simulator
# Return xdot:
xdot = np.zeros(3)
xdot[0] = dCadt
xdot[1] = dTdt
xdot[2] = dhdt
return xdot
When you have a variable height, the volume is changing so you can't assume that it is constant in the other equations. You'll need to modify your energy balance and species balance as shown in the material on balance equations.

Related

Comparing Biweekly HFMD Cases with and without using the Squared Error Objective & L1-Norm Objective

I wish to model the biweekly HFMD cases in Malaysia.
Then, I want to show that the model using the Squared Error Objective and L1-Norm Objective can better model the biweekly HFMD cases than the model without objectives.
My question is, is it possible to model the biweekly HFMD cases without using the Squared Error Objective and L1-Norm Objective?
With this, I have attached the coding below:
from gekko import GEKKO
import numpy as np
import matplotlib.pyplot as plt
m1 = GEKKO(remote=False)
m2 = GEKKO(remote=False)
m = m1
# Known parameters
nb = 26 # Number of biweeks in a year
ny = 3 # Number of years
biweeks = np.zeros((nb,ny*nb+1))
biweeks[0][0] = 1
for i in range(nb):
for j in range(ny):
biweeks[i][j*nb+i+1] = 1
# Write csv data file
tm = np.linspace(0,78,79)
# case data
# Malaysia weekly HFMD data from the year 2013 - 2015
cases = np.array([506,506,700,890,1158,1605,1694,1311,1490,1310,1368,\
1009,1097,934,866,670,408,481,637,749,700,648,710,\
740,627,507,516,548,636,750,1066,1339,1565,\
1464,1575,1759,1631,1601,1227,794,774,623,411,\
750,1017,976,1258,1290,1546,1662,1720,1553,1787,1291,1712,2227,2132,\
2550,2140,1645,1743,1296,1153,871,621,570,388,\
347,391,446,442,390,399,421,398,452,470,437,411])
data = np.vstack((tm,cases))
data = data.T
# np.savetxt('measles_biweek_2.csv',data,delimiter=',',header='time,cases')
np.savetxt('hfmd_biweek_2.csv',data,delimiter=',',header='time,cases')
# Load data from csv
# m.time, cases_meas = np.loadtxt('measles_biweek_2.csv', \
m.time, cases_hfmd = np.loadtxt('hfmd_biweek_2.csv', \
delimiter=',',skiprows=1,unpack=True)
# m.Vr = m.Param(value = 0)
# Variables
# m.N = m.FV(value = 3.2e6)
# m.mu = m.FV(value = 7.8e-4)
# m.N = m.FV(value = 3.11861e7)
# m.mu = m.FV(value = 6.42712e-4)
m.N = m.FV(value = 3.16141e7) # Malaysia average total population (2015 - 2017)
m.mu = m.FV(value = 6.237171519e-4) # Malaysia scaled birth rate (births/biweek/total population)
m.rep_frac = m.FV(value = 0.45) # Percentage of underreporting
# Beta values (unknown parameters in the model)
m.beta = [m.FV(value=1, lb=0.1, ub=5) for i in range(nb)]
# Predicted values
m.S = m.SV(value = 0.162492875*m.N.value, lb=0,ub=m.N) # Susceptibles (Kids from 0 - 9 YO: 5137066 people) - Average of 94.88% from total reported cases
m.I = m.SV(value = 7.907863896e-5*m.N.value, lb=0,ub=m.N) #
# m.V = m.Var(value = 2e5)
# measured values
m.cases = m.CV(value = cases_hfmd, lb=0)
# turn on feedback status for CASES
m.cases.FSTATUS = 1
# weight on prior model predictions
m.cases.WMODEL = 0
# meas_gap = deadband that represents level of
# accuracy / measurement noise
db = 100
m.cases.MEAS_GAP = db
for i in range(nb):
m.beta[i].STATUS=1
#m.gamma = m.FV(value=0.07)
m.gamma = m.FV(value=0.07)
m.gamma.STATUS = 1
m.gamma.LOWER = 0.05
m.gamma.UPPER = 0.5
m.biweek=[None]*nb
for i in range(nb):
m.biweek[i] = m.Param(value=biweeks[i])
# Intermediate
m.Rs = m.Intermediate(m.S*m.I/m.N)
# Equations
sum_biweek = sum([m.biweek[i]*m.beta[i]*m.Rs for i in range(nb)])
# m.Equation(m.S.dt()== -sum_biweek + m.mu*m.N - m.Vr)
m.Equation(m.S.dt()== -sum_biweek + m.mu*m.N)
m.Equation(m.I.dt()== sum_biweek - m.gamma*m.I)
m.Equation(m.cases == m.rep_frac*sum_biweek)
# m.Equation(m.V.dt()==-m.Vr)
# options
m.options.SOLVER = 1
m.options.NODES=3
# imode = 5, dynamic estimation
m.options.IMODE = 5
# ev_type = 1 (L1-norm) or 2 (squared error)
m.options.EV_TYPE = 2
# solve model and print solver output
m.solve()
[print('beta['+str(i+1)+'] = '+str(m.beta[i][0])) \
for i in range(nb)]
print('gamma = '+str(m.gamma.value[0]))
# export data
# stack time and avg as column vectors
my_data = np.vstack((m.time,np.asarray(m.beta),m.gamma))
# transpose data
my_data = my_data.T
# save text file with comma delimiter
beta_str = ''
for i in range(nb):
beta_str = beta_str + ',beta[' + str(i+1) + ']'
header_name = 'time,gamma' + beta_str
##np.savetxt('solution_data.csv',my_data,delimiter=',',\
## header = header_name, comments='')
np.savetxt('solution_data_EVTYPE_'+str(m.options.EV_TYPE)+\
'_gamma'+str(m.gamma.STATUS)+'.csv',\
my_data,delimiter=',',header = header_name)
plt.figure(num=1, figsize=(16,8))
plt.suptitle('Estimation')
plt.subplot(2,2,1)
plt.plot(m.time,m.cases, label='Cases (model)')
plt.plot(m.time,cases_hfmd, label='Cases (measured)')
if m.options.EV_TYPE==2:
plt.plot(m.time,cases_hfmd+db/2, 'k-.',\
lw=0.5, label=r'$Cases_{db-hi}$')
plt.plot(m.time,cases_hfmd-db/2, 'k-.',\
lw=0.5, label=r'$Cases_{db-lo}$')
plt.fill_between(m.time,cases_hfmd-db/2,\
cases_hfmd+db/2,color='gold',alpha=.5)
plt.legend(loc='best')
plt.ylabel('Cases')
plt.subplot(2,2,2)
plt.plot(m.time,m.S,'r--')
plt.ylabel('S')
plt.subplot(2,2,3)
[plt.plot(m.time,m.beta[i], label='_nolegend_')\
for i in range(nb)]
plt.plot(m.time,m.gamma,'c--', label=r'$\gamma$')
plt.legend(loc='best')
plt.ylabel(r'$\beta, \gamma$')
plt.xlabel('Time')
plt.subplot(2,2,4)
plt.plot(m.time,m.I,'g--')
plt.xlabel('Time')
plt.ylabel('I')
plt.subplots_adjust(hspace=0.2,wspace=0.4)
name = 'cases_EVTYPE_'+ str(m.options.EV_TYPE) +\
'_gamma' + str(m.gamma.STATUS) + '.png'
plt.savefig(name)
plt.show()
To define a custom objective, use the m.Minimize() or m.Maximize() functions instead of the squared error or l1-norm objectives that are built into the m.CV() objects. To create a custom objective, use m.Var() instead of m.CV() such as:
from gekko import GEKKO
import numpy as np
m = GEKKO()
x = m.Array(m.Var,4,value=1,lb=1,ub=5)
x1,x2,x3,x4 = x
# change initial values
x2.value = 5; x3.value = 5
m.Equation(x1*x2*x3*x4>=25)
m.Equation(x1**2+x2**2+x3**2+x4**2==40)
m.Minimize(x1*x4*(x1+x2+x3)+x3)
m.solve()
print('x: ', x)
print('Objective: ',m.options.OBJFCNVAL)
Here is a similar problem with disease prediction (Measles) that uses m.CV().
import numpy as np
from gekko import GEKKO
import matplotlib.pyplot as plt
# Import Data
# write csv data file
t_s = np.linspace(0,78,79)
# case data
cases_s = np.array([180,180,271,423,465,523,649,624,556,420,\
423,488,441,268,260,163,83,60,41,48,65,82,\
145,122,194,237,318,450,671,1387,1617,2058,\
3099,3340,2965,1873,1641,1122,884,591,427,282,\
174,127,84,97,68,88,79,58,85,75,121,174,209,458,\
742,929,1027,1411,1885,2110,1764,2001,2154,1843,\
1427,970,726,416,218,160,160,188,224,298,436,482,468])
# Initialize gekko model
m = GEKKO()
# Number of collocation nodes
nodes = 4
# Number of phases (years in this case)
n = 3
#Biweek periods per year
bi = 26
# Time horizon (for all 3 phases)
m.time = np.linspace(0,1,bi+1)
# Parameters that will repeat each year
N = m.Param(3.2e6)
mu = m.Param(7.8e-4)
rep_frac = m.Param(0.45)
Vr = m.Param(0)
beta = m.MV(2,lb = 0.1)
beta.STATUS = 1
gamma = m.FV(value=0.07)
gamma.STATUS = 1
gamma.LOWER = 0.05
gamma.UPPER = 0.5
# Data used to control objective function
casesobj1 = m.Param(cases_s[0:(bi+1)])
casesobj2 = m.Param(cases_s[bi:(2*bi+1)])
casesobj3 = m.Param(cases_s[2*bi:(3*bi+1)])
# Variables that vary between years, one version for each year
cases = [m.CV(value = cases_s[(i*bi):(i+1)*(bi+1)-i],lb=0) for i in range(n)]
for i in cases:
i.FSTATUS = 1
i.WMODEL = 0
i.MEAS_GAP = 100
S = [m.Var(0.06*N,lb = 0,ub = N) for i in range(n)]
I = [m.Var(0.001*N, lb = 0,ub = N) for i in range(n)]
V = [m.Var(2e5) for i in range(n)]
# Equations (created for each year)
for i in range(n):
R = m.Intermediate(beta*S[i]*I[i]/N)
m.Equation(S[i].dt() == -R + mu*N - Vr)
m.Equation(I[i].dt() == R - gamma*I[i])
m.Equation(cases[i] == rep_frac*R)
m.Equation(V[i].dt() == -Vr)
# Connect years together at endpoints
for i in range(n-1):
m.Connection(cases[i+1],cases[i],1,bi,1,nodes)#,1,nodes)
m.Connection(cases[i+1],'CALCULATED',pos1=1,node1=1)
m.Connection(S[i+1],S[i],1,bi,1,nodes)
m.Connection(S[i+1],'CALCULATED',pos1=1,node1=1)
m.Connection(I[i+1],I[i],1,bi,1,nodes)
m.Connection(I[i+1],'CALCULATED',pos1=1, node1=1)
# Solver options
m.options.IMODE = 5
m.options.NODES = nodes
m.EV_TYPE = 1
m.options.SOLVER = 1
# Solve
m.Obj(2*(casesobj1-cases[0])**2+(casesobj3-cases[2])**2)
m.solve()
# Calculate the start time of each phase
ts = np.linspace(1,n,n)
# Plot
plt.figure()
plt.subplot(4,1,1)
tm = np.empty(len(m.time))
for i in range(n):
tm = m.time + ts[i]
plt.plot(tm,cases[i].value,label='Cases Year %s'%(i+1))
plt.plot(tm,cases_s[(i*bi):(i+1)*(bi+1)-i],'.')
plt.legend()
plt.ylabel('Cases')
plt.subplot(4,1,2)
for i in range(n):
tm = m.time + ts[i]
plt.plot(tm,beta.value,label='Beta Year %s'%(i+1))
plt.legend()
plt.ylabel('Contact Rate')
plt.subplot(4,1,3)
for i in range(n):
tm = m.time + ts[i]
plt.plot(tm,I[i].value,label='I Year %s'%(i+1))
plt.legend()
plt.ylabel('Infectives')
plt.subplot(4,1,4)
for i in range(n):
tm = m.time + ts[i]
plt.plot(tm,S[i].value,label='S Year %s'%(i+1))
plt.legend()
plt.ylabel('Susceptibles')
plt.xlabel('Time (yr)')
plt.show()

MEAS of MV when fstatus=1

everyone and Professor John
We are using gekko to do MPC on tclab simulation model. We try to emulate the situation that on site the actuator deviates from MV calculated by gekko because of the problems of actuator.
If the deviation is in the fixed pattern, for example a quite big constant deviation happens for a long time and may come back then work well for a long time. We can deal with it by extra logic to detect deviation and add the deviation value to the mv calculated by gekko.
one day, I noticed that there could be meas for MV when fstatus = 1. So I gave it a try. I hope gekko could deal with the deviation by itself. for example, if mv from gekko is 10 and the measurement is 5 and the pattern continues, gekko may spit out a higher MV value than 10, for example 15 and measurement is 10.
In the simulation, when I set MV's fstatus=1, the MV's curve becomes to :
q1a is the q1 with manual deviation. In the above pic, q1a == q1. It looks like gekko takes one more step thinking about the MV's effect.
In the below pic, there are two times range, one with "q1a == q1+20" and the other with "q1a == q1 -20". q1a's value is fed to tclab and mv(q1)'s meas.
I do not understand why the q1 calculated by gekko is going up or going down when meas deviates despite the t1 is going far away from sp.
Edit: Example Code
See the screen shot below from "normal" HMI. The sluggish MV disappeared, so it maybe caused by bug in my code. But the up-going or down-going could still be seen.
See my code below:
from random import random
from random import randrange
import tclab
from tclab import labtime
from tclab import TCLabModel
import numpy as np
import time
import matplotlib.pyplot as plt
from gekko import GEKKO
import json
from tclab import TCLabModel
make_mp4 = True
if make_mp4:
import imageio # required to make animation
import os
try:
os.mkdir('./figures')
except:
pass
class tclab_heaterpipe():
def __init__(self,d1,d2,model):
if(d1 >= 1 and d2 >=1):
self.delay_q1_step = int(d1)
self.delay_q2_step = int(d2)
self.q1_buffer = [0] * self.delay_q1_step
self.q2_buffer = [0] * self.delay_q2_step
self.m = model
else:
self.delay_q1_step =0
self.delay_q2_step =0
return
def Q1_delay(self,q1):
if(self.delay_q1_step == 0):
self.m.Q1(q1)
self.q1_buffer.insert(0,q1)
self.m.Q1(self.q1_buffer.pop())
def Q2_delay(self,q2):
if(self.delay_q2_step == 0):
self.m.Q1(q2)
self.q2_buffer.insert(0,q2)
self.m.Q2(self.q2_buffer.pop())
# Connect to Arduino
connected = False
theta1 = 1
theta2 = 1
T = tclab.setup(connected)
a = T()
tclab_delay = tclab_heaterpipe(theta1,theta2,a)
# Turn LED on
print('LED On')
a.LED(100)
# Simulate a time delay
# Run time in minutes
run_time = 80.0
# Number of cycles
loops = int(60.0*run_time)
# Temperature (K)
t1sp = 45.0
t2sp = 35.0
#########################################################
# Initialize Model
#########################################################
# use remote=True for MacOS
m = GEKKO(name='tclab-mpc',remote=False)
m.time = np.linspace(0,400,41)
step = 10
T1 = np.ones(int(loops/step)+1) * a.T1 # temperature (degC)
T2 = np.ones(int(loops/step)+1) * a.T2 # temperature (degC)
Tsp1 = np.ones(int(loops/step)+1) * t1sp # set point (degC)
Tsp2 = np.ones(int(loops/step)+1) * t2sp # set point (degC)
# heater values
Q1s = np.ones(int(loops/step)+1) * 0.0
Q2s = np.ones(int(loops/step)+1) * 0.0
# Parameters
Q1_ss = m.Param(value=0)
TC1_ss = m.Param(value=a.T1)
Q2_ss = m.Param(value=0)
TC2_ss = m.Param(value=a.T2)
Kp1 = m.Param(value= 0.7)
tau1 = m.Param(value=160.0)
Kp2 = m.Param(value=0.05)
tau2 = m.Param(value=160.0)
Kp3= m.Param(value=0.05)
tau3 = m.Param(value=160.0)
Kp4 = m.Param(value=0.4)
tau4 = m.Param(value=200.0)
sp1 = m.Param(value=a.T1)
sp2 = m.Param(value=a.T2)
# Manipulated variable
Q1 = m.MV(value=0, name='q1')
Q1.STATUS = 1 # use to control temperature
Q1.FSTATUS = 1 # no feedback measurement
Q1.LOWER = 0.0
Q1.UPPER = 100.0
Q1.DMAX = 10.0
Q1.DCOST = 5.0
Q2 = m.MV(value=0, name='q2')
Q2.STATUS = 1 # use to control temperature
Q2.FSTATUS = 1 # no feedback measurement
Q2.LOWER = 0.0
Q2.UPPER = 100.0
Q2.DMAX = 10.0
Q2.DCOST = 5.0
# Controlled variable
TC1 = m.CV(value=a.T1, name='tc1')
TC1.STATUS = 1 # minimize error with setpoint range
TC1.FSTATUS = 1 # receive measurement
TC1.TR_INIT = 2 # reference trajectory
# TC1.COST = 0.1
TC1.WSPHI = 20
TC1.WSPLO = 20
TC1.TAU = 50 # time constant for response
#TC1.TR_OPEN = 3
TC2 = m.CV(value=a.T2, name='tc2')
TC2.STATUS = 1 # minimize error with setpoint range
TC2.FSTATUS = 1 # receive measurement
TC2.TR_INIT = 2 # reference trajectory
# TC2.COST = 0.1
TC2.WSPHI = 20
TC2.WSPLO = 20
TC2.TAU = 30 # time constant for response
#kTC2.TR_OPEN = 3
# 添加延时
Q1d=m.Var()
m.delay(Q1, Q1d, theta1)
Q2d=m.Var()
m.delay(Q2, Q2d, theta2)
# Equation
#m.Equation(tau1 * TC1.dt() + (TC1 - TC1_ss) == Kp1 * (Q1d - Q1_ss))
# m.Equation(tau2 * TC2.dt() + (TC2 - TC2_ss) == Kp2 * (Q1d - Q1_ss))
# m.Equation(tau3 * TC1.dt() + (TC1 - TC1_ss) == Kp3 * (Q2d - Q2_ss))
# m.Equation(tau2 * TC2.dt() + (TC2 - TC2_ss) == Kp4 * (Q2d - Q2_ss))
m.Equation(0.5 * (tau1 * TC1.dt() + (TC1 - TC1_ss) + tau3 * TC1.dt() + (TC1 - TC1_ss)) == Kp1 * (Q1d - Q1_ss) + Kp3 * (Q2d -Q2_ss))
m.Equation(0.5 * (tau2 * TC2.dt() + (TC2 - TC2_ss) + tau4 * TC2.dt() + (TC2 - TC2_ss)) == Kp4 * (Q2d - Q2_ss) + Kp2 * (Q1d - Q1_ss))
# Steady-state initializations
m.options.IMODE = 1
m.options.SOLVER = 1 # 1=APOPT, 3=IPOPT
m.solve()
sp1.VALUE = 45
sp2.VALUE = 35
# Global Options
m.options.IMODE = 6 # MPC
m.options.CV_TYPE = 3 # Objective type
m.options.NODES = 2 # Collocation nodes
m.options.MAX_TIME = 10
m.options.SOLVER = 1 # 1=APOPT, 3=IPOPT
#m.options.CV_WGT_START = 2*theta
#m.options.CV_WGT_SLOPE = theta
# m.options.MV_STEP_HOR = 5
##################################################################
# Create plot
plt.figure()
plt.ion()
plt.show()
# Main Loop
a.Q1(0)
a.Q2(0)
Q2s[0:] = 0
start_time = time.time()
tm = np.linspace(1,loops,int(loops/step)+1)
j=0
try:
time_start = time.time()
labtime_start = labtime.time()
if(not connected):
labtime.set_rate(10)
for i in tclab.clock(loops,adaptive=False):
i = int(i)
if(i == 0):
continue
print("-----------------------")
t_real = time.time() - time_start
t_lab = labtime.time() - labtime_start
print("real time = {0:4.1f} lab time = {1:4.1f} m.time = {1:4.1f}".format(t_real, t_lab,m.time))
#print("real time = {0:4.1f} m.time = {1:4.1f}".format(t_real, m.time))
if(i%step != 0):
continue
j = i/step
j = int(j)
print(j)
T1[j:] = a.T1
T2[j:] = a.T2
tm[j] = i
###############################
### MPC CONTROLLER ###
###############################
TC1.MEAS = T1[j]
TC2.MEAS = T2[j]
print("T1 meas:{0:4.1f} ".format(a.T1))
print("T2 meas:{0:4.1f} ".format(a.T2))
# input setpoint with deadband +/- DT
DT =0.5
TC1.SPHI = Tsp1[j] +DT
TC1.SPLO = Tsp1[j] -DT
TC2.SPHI = Tsp2[j] +DT
TC2.SPLO = Tsp2[j] -DT
try:
# stop model time to solve MPC in cast the solver takes too much time
if(not connected):
labtime.stop()
m.solve(disp=False)
#start model time
if(not connected):
labtime.start()
except Exception as e:
if(not connected):
if(not labtime.running):
labtime.start()
print("sovle's exception:")
print(e)
if(j != 0):
Q1s[j] = Q1s[j-1]
Q2s[j] = Q2s[j-1]
continue
# test for successful solution
if (m.options.APPSTATUS==1):
# retrieve the first Q value
Q1s[j:] = np.ones(len(Q1s)-j) * Q1.NEWVAL
Q2s[j:] = np.ones(len(Q2s)-j) * Q2.NEWVAL
#a.Q1(Q1.NEWVAL)
#a.Q2(Q2.NEWVAL)
print("Q1 applied with delay: {0:4.1f} ".format(Q1.NEWVAL))
print("Q2 applied with delay: {0:4.1f} ".format(Q2.NEWVAL))
with open(m.path+'//results.json') as f:
results = json.load(f)
else:
# not successful, set heater to zero
print("APPSTATUS is not 1,set Q to 0")
#Q1s[j] = 0
#Q2s[j] = 0
if i> 300 and i < 600:
Q1s[j] = Q1s[j] - 20
Q2s[j] = Q2s[j] - 20
if i>= 600:
Q1s[j] = Q1s[j] + 20
Q2s[j] = Q2s[j] + 20
Q1.meas= Q1s[j]
Q2.meas= Q2s[j]
tclab_delay.Q1_delay(Q1s[j])
tclab_delay.Q2_delay(Q2s[j])
print("calc:"+str(Q1s[j]))
print("calc:"+str(Q2s[j]))
#apply disturbance on 50s, 200s,
#if(i == 600):
# Q2s[j] = 100
#if(i == 1400):
# Q2s[j] = 0
#Q2s[j] = 20 - randrange(20)
#Q2s[j:] = np.ones(len(Q2s)-j) * Q2s[j]
#restore Q2 to 0
#if(i == 300):
#Q2s[j:] = 0
#a.Q2(Q2s[j])
#tclab_delay.Q2_delay(Q2s[j])
#take Q2 to FV
#Q2.MEAS = Q2s[j]
if(not connected):
labtime.stop()
# Plot
try:
plt.clf()
ax=plt.subplot(2,1,1)
ax.grid()
plt.plot(tm[0:j],T1[0:j],'ro',markersize=3,label=r'$T_1$')
plt.plot(tm[0:j],Tsp1[0:j],'r-',markersize=3,label=r'$T_1 Setpoint$')
plt.plot(tm[0:j],T2[0:j],'bo',markersize=3,label=r'$T_2$')
plt.plot(tm[0:j],Tsp2[0:j],'b-',markersize=3,label=r'$T_2 Setpoint$')
plt.plot(tm[j]+m.time,results['tc1.bcv'],'r-.',markersize=1,\
label=r'$T_1$ predicted',linewidth=1)
plt.plot(tm[j]+m.time,results['tc2.bcv'],'b-.',markersize=1,\
label=r'$T_2$ predicted',linewidth=1)
plt.plot(tm[j]+m.time,results['tc1.tr_hi'],'k--',\
label=r'$T_1$ trajectory')
plt.plot(tm[j]+m.time,results['tc1.tr_lo'],'k--')
plt.plot(tm[j]+m.time,results['tc2.tr_hi'],'k--',\
label=r'$T_2$ trajectory')
plt.plot(tm[j]+m.time,results['tc2.tr_lo'],'k--')
plt.ylabel('Temperature (degC)')
plt.legend(loc='best')
ax=plt.subplot(2,1,2)
ax.grid()
plt.plot(tm[0:j],Q1s[0:j],'r-',linewidth=3,label=r'$Q_1$')
plt.plot(tm[0:j],Q2s[0:j],'b-',linewidth=3,label=r'$Q_2$')
plt.plot(tm[j]+m.time,Q1.value,'r-.',\
label=r'$Q_1$ plan',linewidth=1)
plt.plot(tm[j]+m.time,Q2.value,'b-.',\
label=r'$Q_2$ plan',linewidth=1)
#plt.plot(tm[0:i],Q2s[0:i],'b:',LineWidth=3,label=r'$Q_2$')
plt.ylabel('Heaters')
plt.xlabel('Time (sec)')
plt.legend(loc='best')
plt.draw()
plt.pause(0.05)
if make_mp4:
filename='./figures/plot_'+str(j+10000)+'.png'
plt.savefig(filename)
except Exception as e:
print(e)
pass
if(not connected):
labtime.start()
# Turn off heaters
a.Q1(0)
a.Q2(0)
print('Shutting down')
input("Press Enter to continue...")
a.close()
# Allow user to end loop with Ctrl-C
except KeyboardInterrupt:
# Disconnect from Arduino
a.Q1(0)
a.Q2(0)
print('Shutting down')
a.close()
if make_mp4:
images = []
iset = 0
for i in range(1,int(loops/step)+1):
filename='./figures/plot_'+str(i+10000)+'.png'
if os.path.exists(filename):
images.append(imageio.imread(filename))
if ((i+1)%350)==0:
imageio.mimsave('results_'+str(iset)+'.mp4', images)
iset += 1
images = []
if images!=[]:
imageio.mimsave('results_'+str(iset)+'.mp4', images)
# Make sure serial connection still closes when there's an error
except:
# Disconnect from Arduino
a.Q1(0)
a.Q2(0)
print('Error: Shutting down')
a.close()
raise
Regards
Tibalt
Is the FSTATUS also ON for the CVs such as t1.FSTATUS=1? If you update the measurement such as:
t1.MEAS = lab.T1
t2.MEAS = lab.T2
then this updates the BIAS for t1 and t2 (BIAS documentation). This should take care of any process / model mismatch that you are introducing by arbitrarily increasing or decreasing the heater by 20%. If t1.FSTATUS is OFF (0) then it is not able to compensate for the mismatch.
Another thing to try is to adjust the reference trajectory. The controller can appear sluggish if TAU is too high. Here is an example application with MPC and a linear model.
One additional way to compensate for the mismatch is to use Moving Horizon Estimation as shown here.
It looks like you have created a nice interface!
Response to Edit
Thanks for adding the code. The problem is that Q1.DMAX=10 and Q2.DMAX=10. When the Q1 and Q2 values are shifted up by 20 each cycle, the most that the controller can shift down is 20-10=10 so the controller appears that it is ramping in the wrong direction. Changing to DMAX=100 fixes the problem. There is still offset from the setpoint because the recommended Q1 and Q2 are shifted each cycle. The true recommended values are never implemented. Another thing to try is to impose an offset on the measured values such as TC1.MEAS = T1[j] + 20. The model bias will remove the offset in this case.
from random import random
from random import randrange
import tclab
from tclab import labtime
from tclab import TCLabModel
import numpy as np
import time
import matplotlib.pyplot as plt
from gekko import GEKKO
import json
from tclab import TCLabModel
make_gif = True
make_mp4 = True
if make_gif or make_mp4:
# pip install imageio-ffmpeg with imageio to make MP4
import imageio # required to make animation
import os
try:
os.mkdir('./figures')
except:
pass
class tclab_heaterpipe():
def __init__(self,d1,d2,model):
if(d1 >= 1 and d2 >=1):
self.delay_q1_step = int(d1)
self.delay_q2_step = int(d2)
self.q1_buffer = [0] * self.delay_q1_step
self.q2_buffer = [0] * self.delay_q2_step
self.m = model
else:
self.delay_q1_step =0
self.delay_q2_step =0
return
def Q1_delay(self,q1):
if(self.delay_q1_step == 0):
self.m.Q1(q1)
self.q1_buffer.insert(0,q1)
self.m.Q1(self.q1_buffer.pop())
def Q2_delay(self,q2):
if(self.delay_q2_step == 0):
self.m.Q1(q2)
self.q2_buffer.insert(0,q2)
self.m.Q2(self.q2_buffer.pop())
# Connect to Arduino
connected = False # switch to connected=True with physical hardware
theta1 = 1
theta2 = 1
T = tclab.setup(connected)
a = T()
tclab_delay = tclab_heaterpipe(theta1,theta2,a)
# Turn LED on
print('LED On')
a.LED(100)
# Simulate a time delay
# Run time in minutes
run_time = 20.0
# Number of cycles
loops = int(60.0*run_time)
# Temperature (K)
t1sp = 45.0
t2sp = 35.0
#########################################################
# Initialize Model
#########################################################
# use remote=True for MacOS
m = GEKKO(name='tclab-mpc',remote=False)
m.time = np.linspace(0,400,41)
step = 10
T1 = np.ones(int(loops/step)+1) * a.T1 # temperature (degC)
T2 = np.ones(int(loops/step)+1) * a.T2 # temperature (degC)
Tsp1 = np.ones(int(loops/step)+1) * t1sp # set point (degC)
Tsp2 = np.ones(int(loops/step)+1) * t2sp # set point (degC)
# heater values
Q1s = np.ones(int(loops/step)+1) * 0.0
Q2s = np.ones(int(loops/step)+1) * 0.0
# Parameters
Q1_ss = m.Param(value=0)
TC1_ss = m.Param(value=a.T1)
Q2_ss = m.Param(value=0)
TC2_ss = m.Param(value=a.T2)
Kp1 = m.Param(value= 0.7)
tau1 = m.Param(value=160.0)
Kp2 = m.Param(value=0.05)
tau2 = m.Param(value=160.0)
Kp3= m.Param(value=0.05)
tau3 = m.Param(value=160.0)
Kp4 = m.Param(value=0.4)
tau4 = m.Param(value=200.0)
sp1 = m.Param(value=a.T1)
sp2 = m.Param(value=a.T2)
# Manipulated variable
Q1 = m.MV(value=0, name='q1')
Q1.STATUS = 1 # use to control temperature
Q1.FSTATUS = 1 # no feedback measurement
Q1.LOWER = 0.0
Q1.UPPER = 100.0
Q1.DMAX = 100.0
Q1.DCOST = 1e-3
Q2 = m.MV(value=0, name='q2')
Q2.STATUS = 1 # use to control temperature
Q2.FSTATUS = 1 # no feedback measurement
Q2.LOWER = 0.0
Q2.UPPER = 100.0
Q2.DMAX = 100.0
Q2.DCOST = 1e-3
# Controlled variable
TC1 = m.CV(value=a.T1, name='tc1')
TC1.STATUS = 1 # minimize error with setpoint range
TC1.FSTATUS = 1 # receive measurement
TC1.TR_INIT = 2 # reference trajectory
# TC1.COST = 0.1
TC1.WSPHI = 20
TC1.WSPLO = 20
TC1.TAU = 50 # time constant for response
#TC1.TR_OPEN = 3
TC2 = m.CV(value=a.T2, name='tc2')
TC2.STATUS = 1 # minimize error with setpoint range
TC2.FSTATUS = 1 # receive measurement
TC2.TR_INIT = 2 # reference trajectory
# TC2.COST = 0.1
TC2.WSPHI = 20
TC2.WSPLO = 20
TC2.TAU = 30 # time constant for response
#kTC2.TR_OPEN = 3
# 添加延时
Q1d=m.Var()
m.delay(Q1, Q1d, theta1)
Q2d=m.Var()
m.delay(Q2, Q2d, theta2)
# Equation
#m.Equation(tau1 * TC1.dt() + (TC1 - TC1_ss) == Kp1 * (Q1d - Q1_ss))
# m.Equation(tau2 * TC2.dt() + (TC2 - TC2_ss) == Kp2 * (Q1d - Q1_ss))
# m.Equation(tau3 * TC1.dt() + (TC1 - TC1_ss) == Kp3 * (Q2d - Q2_ss))
# m.Equation(tau2 * TC2.dt() + (TC2 - TC2_ss) == Kp4 * (Q2d - Q2_ss))
m.Equation(0.5 * (tau1 * TC1.dt() + (TC1 - TC1_ss) + tau3 * TC1.dt() + (TC1 - TC1_ss)) == Kp1 * (Q1d - Q1_ss) + Kp3 * (Q2d -Q2_ss))
m.Equation(0.5 * (tau2 * TC2.dt() + (TC2 - TC2_ss) + tau4 * TC2.dt() + (TC2 - TC2_ss)) == Kp4 * (Q2d - Q2_ss) + Kp2 * (Q1d - Q1_ss))
# Steady-state initializations
m.options.IMODE = 1
m.options.SOLVER = 1 # 1=APOPT, 3=IPOPT
m.solve()
sp1.VALUE = 45
sp2.VALUE = 35
# Global Options
m.options.IMODE = 6 # MPC
m.options.CV_TYPE = 3 # Objective type
m.options.NODES = 2 # Collocation nodes
m.options.MAX_TIME = 10
m.options.SOLVER = 1 # 1=APOPT, 3=IPOPT
#m.options.CV_WGT_START = 2*theta
#m.options.CV_WGT_SLOPE = theta
# m.options.MV_STEP_HOR = 5
##################################################################
# Create plot
plt.figure(figsize=(12,8))
plt.ion()
plt.show()
# Main Loop
a.Q1(0)
a.Q2(0)
Q2s[0:] = 0
start_time = time.time()
tm = np.linspace(1,loops,int(loops/step)+1)
j=0
try:
time_start = time.time()
labtime_start = labtime.time()
if(not connected):
labtime.set_rate(10)
for i in tclab.clock(loops,adaptive=False):
i = int(i)
if(i == 0):
continue
print("-----------------------")
t_real = time.time() - time_start
t_lab = labtime.time() - labtime_start
print("real time = {0:4.1f} lab time = {1:4.1f} m.time = {1:4.1f}".format(t_real, t_lab,m.time))
#print("real time = {0:4.1f} m.time = {1:4.1f}".format(t_real, m.time))
if(i%step != 0):
continue
j = i/step
j = int(j)
print(j)
T1[j:] = a.T1
T2[j:] = a.T2
tm[j] = i
###############################
### MPC CONTROLLER ###
###############################
TC1.MEAS = T1[j]
TC2.MEAS = T2[j]
print("T1 meas:{0:4.1f} ".format(a.T1))
print("T2 meas:{0:4.1f} ".format(a.T2))
# input setpoint with deadband +/- DT
DT =0.5
TC1.SPHI = Tsp1[j] +DT
TC1.SPLO = Tsp1[j] -DT
TC2.SPHI = Tsp2[j] +DT
TC2.SPLO = Tsp2[j] -DT
try:
# stop model time to solve MPC in cast the solver takes too much time
if(not connected):
labtime.stop()
m.solve(disp=False)
#start model time
if(not connected):
labtime.start()
except Exception as e:
if(not connected):
if(not labtime.running):
labtime.start()
print("sovle's exception:")
print(e)
if(j != 0):
Q1s[j] = Q1s[j-1]
Q2s[j] = Q2s[j-1]
continue
# test for successful solution
if (m.options.APPSTATUS==1):
# retrieve the first Q value
Q1s[j:] = np.ones(len(Q1s)-j) * Q1.NEWVAL
Q2s[j:] = np.ones(len(Q2s)-j) * Q2.NEWVAL
#a.Q1(Q1.NEWVAL)
#a.Q2(Q2.NEWVAL)
print("Q1 applied with delay: {0:4.1f} ".format(Q1.NEWVAL))
print("Q2 applied with delay: {0:4.1f} ".format(Q2.NEWVAL))
with open(m.path+'//results.json') as f:
results = json.load(f)
else:
# not successful, set heater to zero
print("APPSTATUS is not 1,set Q to 0")
#Q1s[j] = 0
#Q2s[j] = 0
if i> 300 and i < 600:
Q1s[j] = max(0,Q1s[j] - 20)
Q2s[j] = max(0,Q2s[j] - 20)
if i>= 600:
Q1s[j] = min(100,Q1s[j] + 20)
Q2s[j] = min(100,Q2s[j] + 20)
Q1.meas= Q1s[j]
Q2.meas= Q2s[j]
tclab_delay.Q1_delay(Q1s[j])
tclab_delay.Q2_delay(Q2s[j])
print("calc:"+str(Q1s[j]))
print("calc:"+str(Q2s[j]))
if(not connected):
labtime.stop()
# Plot
try:
plt.clf()
ax=plt.subplot(2,1,1)
ax.grid()
plt.plot(tm[0:j],T1[0:j],'ro',markersize=3,label=r'$T_1$')
plt.plot(tm[0:j],Tsp1[0:j],'r-',markersize=3,label=r'$T_1 Setpoint$')
plt.plot(tm[0:j],T2[0:j],'bo',markersize=3,label=r'$T_2$')
plt.plot(tm[0:j],Tsp2[0:j],'b-',markersize=3,label=r'$T_2 Setpoint$')
plt.plot(tm[j]+m.time,results['tc1.bcv'],'r-.',markersize=1,\
label=r'$T_1$ predicted',linewidth=1)
plt.plot(tm[j]+m.time,results['tc2.bcv'],'b-.',markersize=1,\
label=r'$T_2$ predicted',linewidth=1)
plt.plot(tm[j]+m.time,results['tc1.tr_hi'],'k--',\
label=r'$T_1$ trajectory')
plt.plot(tm[j]+m.time,results['tc1.tr_lo'],'k--')
plt.plot(tm[j]+m.time,results['tc2.tr_hi'],'k--',\
label=r'$T_2$ trajectory')
plt.plot(tm[j]+m.time,results['tc2.tr_lo'],'k--')
plt.ylabel('Temperature (degC)')
plt.legend(loc=1)
ax=plt.subplot(2,1,2)
ax.grid()
plt.plot(tm[0:j],Q1s[0:j],'r-',linewidth=3,label=r'$Q_1$')
plt.plot(tm[0:j],Q2s[0:j],'b-',linewidth=3,label=r'$Q_2$')
plt.plot(tm[j]+m.time,Q1.value,'r-.',\
label=r'$Q_1$ plan',linewidth=1)
plt.plot(tm[j]+m.time,Q2.value,'b-.',\
label=r'$Q_2$ plan',linewidth=1)
#plt.plot(tm[0:i],Q2s[0:i],'b:',LineWidth=3,label=r'$Q_2$')
plt.ylabel('Heaters')
plt.xlabel('Time (sec)')
plt.legend(loc=1)
plt.draw()
plt.pause(0.05)
if make_mp4:
filename='./figures/plot_'+str(j+10000)+'.png'
plt.savefig(filename)
except Exception as e:
print(e)
pass
if(not connected):
labtime.start()
# Turn off heaters
a.Q1(0)
a.Q2(0)
print('Shutting down')
input("Press Enter to continue...")
a.close()
# make gif
if make_gif:
images = []
iset = 0
for i in range(1,int(loops/step)+1):
filename='./figures/plot_'+str(i+10000)+'.png'
if os.path.exists(filename):
images.append(imageio.imread(filename))
if ((i+1)%350)==0:
imageio.mimsave('results_'+str(iset)+'.gif', images)
iset += 1
images = []
if images!=[]:
imageio.mimsave('results_'+str(iset)+'.gif', images)
if make_mp4:
images = []
iset = 0
for i in range(1,int(loops/step)+1):
filename='./figures/plot_'+str(i+10000)+'.png'
if os.path.exists(filename):
images.append(imageio.imread(filename))
if ((i+1)%350)==0:
imageio.mimsave('results_'+str(iset)+'.gif', images)
iset += 1
images = []
if images!=[]:
imageio.mimsave('results_'+str(iset)+'.gif', images)
# Allow user to end loop with Ctrl-C
except KeyboardInterrupt:
# Disconnect from Arduino
a.Q1(0)
a.Q2(0)
print('Shutting down')
a.close()
if make_gif:
images = []
iset = 0
for i in range(1,int(loops/step)+1):
filename='./figures/plot_'+str(i+10000)+'.png'
if os.path.exists(filename):
images.append(imageio.imread(filename))
if ((i+1)%350)==0:
imageio.mimsave('results_'+str(iset)+'.gif', images)
iset += 1
images = []
if images!=[]:
imageio.mimsave('results_'+str(iset)+'.gif', images)
if make_mp4:
images = []
iset = 0
for i in range(1,int(loops/step)+1):
filename='./figures/plot_'+str(i+10000)+'.png'
if os.path.exists(filename):
images.append(imageio.imread(filename))
if ((i+1)%350)==0:
imageio.mimsave('results_'+str(iset)+'.mp4', images)
iset += 1
images = []
if images!=[]:
imageio.mimsave('results_'+str(iset)+'.mp4', images)
# Make sure serial connection still closes when there's an error
except:
# Disconnect from Arduino
a.Q1(0)
a.Q2(0)
print('Error: Shutting down')
a.close()
raise

Fit Amplitude (Frequency response) of a capacitor with lmfit

I am trying to fit measured data with lmfit.
My goal is to get the parameters of the capacitor with an equivalent circuit diagram.
So, I want to create a model with parameters (C, R1, L1,...) and fit it to the measured data.
I know that the resonance frequency is at the global minimum and there must also be R1. Also known is C.
So I could fix the parameter C and R1. With the resonance frequency I could calculate L1 too.
I created the model, but the fit doesn't work right.
Maybe someone could help me with this.
Thanks in advance.
from lmfit import minimize, Parameters
from lmfit import report_fit
params = Parameters()
params.add('C', value = 220e-9, vary = False)
params.add('L1', value = 0.00001, min = 0, max = 0.1)
params.add('R1', value = globalmin, vary = False)
params.add('Rp', value = 10000, min = 0, max = 10e20)
params.add('Cp', value = 0.1, min = 0, max = 0.1)
def get_elements(params, freq, data):
C = params['C'].value
L1 = params['L1'].value
R1 = params['R1'].value
Rp = params['Rp'].value
Cp = params['Cp'].value
XC = 1/(1j*2*np.pi*freq*C)
XL = 1j*2*np.pi*freq*L1
XP = 1/(1j*2*np.pi*freq*Cp)
Z1 = R1 + XC*Rp/(XC+Rp) + XL
real = np.real(Z1*XP/(Z1+XP))
imag = np.imag(Z1*XP/(Z1+XP))
model = np.sqrt(real**2 + imag**2)
#model = np.sqrt(R1**2 + ((2*np.pi*freq*L1 - 1/(2*np.pi*freq*C))**2))
#model = (np.arctan((2*np.pi*freq*L1 - 1/(2*np.pi*freq*C))/R1)) * 360/((2*np.pi))
return data - model
out = minimize(get_elements, params , args=(freq, data))
report_fit(out)
#make reconstruction for plotting
C = out.params['C'].value
L1 = out.params['L1'].value
R1 = out.params['R1'].value
Rp = out.params['Rp'].value
Cp = out.params['Cp'].value
XC = 1/(1j*2*np.pi*freq*C)
XL = 1j*2*np.pi*freq*L1
XP = 1/(1j*2*np.pi*freq*Cp)
Z1 = R1 + XC*Rp/(XC+Rp) + XL
real = np.real(Z1*XP/(Z1+XP))
imag = np.imag(Z1*XP/(Z1+XP))
reconst = np.sqrt(real**2 + imag**2)
reconst_phase = np.arctan(imag/real)* 360/(2*np.pi)
'''
PLOTTING
'''
#plot of filtred signal vs measered data (AMPLITUDE)
fig = plt.figure(figsize=(40,15))
file_title = 'Measured Data'
plt.subplot(311)
plt.xscale('log')
plt.yscale('log')
plt.xlim([min(freq), max(freq)])
plt.ylabel('Amplitude')
plt.xlabel('Frequency in Hz')
plt.grid(True, which="both")
plt.plot(freq, z12_fac, 'g', alpha = 0.7, label = 'data')
#Plot Impedance of model in magenta
plt.plot(freq, reconst, 'm', label='Reconstruction (Model)')
plt.legend()
#(PHASE)
plt.subplot(312)
plt.xscale('log')
plt.xlim([min(freq), max(freq)])
plt.ylabel('Phase in °')
plt.xlabel('Frequency in Hz')
plt.grid(True, which="both")
plt.plot(freq, z12_deg, 'g', alpha = 0.7, label = 'data')
#Plot Phase of model in magenta
plt.plot(freq, reconst_phase, 'm', label='Reconstruction (Model)')
plt.legend()
plt.savefig(file_title)
plt.close(fig)
measured data
equivalent circuit diagram (model)
Edit 1:
Fit-Report:
[[Fit Statistics]]
# fitting method = leastsq
# function evals = 28
# data points = 4001
# variables = 3
chi-square = 1197180.70
reduced chi-square = 299.444897
Akaike info crit = 22816.4225
Bayesian info crit = 22835.3054
## Warning: uncertainties could not be estimated:
L1: at initial value
Rp: at boundary
Cp: at initial value
Cp: at boundary
[[Variables]]
C: 2.2e-07 (fixed)
L1: 1.0000e-05 (init = 1e-05)
R1: 0.06375191 (fixed)
Rp: 0.00000000 (init = 10000)
Cp: 0.10000000 (init = 0.1)
Edit 2:
Data can be found here:
https://1drv.ms/u/s!AsLKp-1R8HlZhcdlJER5T7qjmvfmnw?e=r8G2nN
Edit 3:
I now have simplified my model to a simple RLC-series. With a another set of data this works pretty good. see here the plot with another set of data
def get_elements(params, freq, data):
C = params['C'].value
L1 = params['L1'].value
R1 = params['R1'].value
#Rp = params['Rp'].value
#Cp = params['Cp'].value
#k = params['k'].value
#freq = np.log10(freq)
XC = 1/(1j*2*np.pi*freq*C)
XL = 1j*2*np.pi*freq*L1
# XP = 1/(1j*2*np.pi*freq*Cp)
# Z1 = R1*k + XC*Rp/(XC+Rp) + XL
# real = np.real(Z1*XP/(Z1+XP))
# imag = np.imag(Z1*XP/(Z1+XP))
Z1 = R1 + XC + XL
real = np.real(Z1)
imag= np.imag(Z1)
model = np.sqrt(real**2 + imag**2)
return np.sqrt(np.real(data)**2+np.imag(data)**2) - model
out = minimize(get_elements, params , args=(freq, data))
Report:
Chi-Square is really high...
[[Fit Statistics]]
# fitting method = leastsq
# function evals = 25
# data points = 4001
# variables = 2
chi-square = 5.0375e+08
reduced chi-square = 125968.118
Akaike info crit = 46988.8798
Bayesian info crit = 47001.4684
[[Variables]]
C: 3.3e-09 (fixed)
L1: 5.2066e-09 +/- 1.3906e-08 (267.09%) (init = 1e-05)
R1: 0.40753691 +/- 24.5685882 (6028.56%) (init = 0.05)
[[Correlations]] (unreported correlations are < 0.100)
C(L1, R1) = -0.174
With my originally set of data I get this:
plot original data (complex)
Which is not bad, but also not good. That's why I want to make my model more detailed, so I can fit also in higher frequency regions...
Report of this one:
[[Fit Statistics]]
# fitting method = leastsq
# function evals = 25
# data points = 4001
# variables = 2
chi-square = 109156.170
reduced chi-square = 27.2958664
Akaike info crit = 13232.2473
Bayesian info crit = 13244.8359
[[Variables]]
C: 2.2e-07 (fixed)
L1: 2.3344e-08 +/- 1.9987e-10 (0.86%) (init = 1e-05)
R1: 0.17444702 +/- 0.29660571 (170.03%) (init = 0.05)
Please note: I also have changed the input data of the model. Now I give the model complex values and then I calculate the Amplitude. Find this also here: https://1drv.ms/u/s!AsLKp-1R8HlZhcdlJER5T7qjmvfmnw?e=qnrZk1

How to set up GEKKO for parameter estimation from multiple independent sets of data?

I am learning how to use GEKKO for kinetic parameter estimation based on laboratory batch reactor data, which essentially consists of the concentration profiles of three species A, C, and P. For the purposes of my question, I am using a model that I previously featured in a question related to parameter estimation from a single data set.
My ultimate goal is to be able to use multiple experimental runs for parameter estimation, leveraging data that may be collected at different temperatures, species concentrations, etc. Due to the independent nature of individual batch reactor experiments, each data set features samples collected at different time points. These different time points (and in the future, different temperatures for instance) are difficult for me to implement into a GEKKO model, as I previosly used the experimental data collection time points as the m.time parameter for the GEKKO model. (See end of post for code) I have solved problems like this in the past with gPROMS and Athena Visual Studio.
To illustrate my problem, I generated an artificial data set of 'experimental' data from my original model by introducing noise to the species concentration profiles, and shifting the experimental time points slightly. I then combined all data sets of the same experimental species into new arrays featuring multiple columns. My thought process here was that GEKKO would carry out the parameter estimation by using the experimental data of each corresponding column of the arrays, so that times_comb[:,0] would be related to A_comb[:,0] while times_comb[:,1] would be related to A_comb[:,1].
When I attempt to run the GEKKO model, the system does obtain a solution for the parameter estimation, but it is unclear to me if the problem solution is reasonable, as I notice that the GEKKO Variables A, B, C, and P are 34 element vectors, which is double the elements in each of the experimental data sets. I presume GEKKO is somehow combining both columns of the time and Parameter vectors during model setup that leads to those 34 element variables? I am also concerned that during this combination of the columns of each input parameter, that the relationship between a certain time point and the collected species information is lost.
How could I improve the use of multiple data sets that GEKKO can simultaneously use for parameter estimation, with the consideration that the time points of each data set may be different? I looked on the GEKKO documentation examples as well as the APMonitor website, but I could not find examples featuring multiple data sets that I could use for guidance, as I am fairly new to the GEKKO package.
Thank you for your time reading my question and for any help/ideas you may have.
Code below:
import numpy as np
import matplotlib.pyplot as plt
from gekko import GEKKO
#Experimental data
times = np.array([0.0, 0.071875, 0.143750, 0.215625, 0.287500, 0.359375, 0.431250,
0.503125, 0.575000, 0.646875, 0.718750, 0.790625, 0.862500,
0.934375, 1.006250, 1.078125, 1.150000])
A_obs = np.array([1.0, 0.552208, 0.300598, 0.196879, 0.101175, 0.065684, 0.045096,
0.028880, 0.018433, 0.011509, 0.006215, 0.004278, 0.002698,
0.001944, 0.001116, 0.000732, 0.000426])
C_obs = np.array([0.0, 0.187768, 0.262406, 0.350412, 0.325110, 0.367181, 0.348264,
0.325085, 0.355673, 0.361805, 0.363117, 0.327266, 0.330211,
0.385798, 0.358132, 0.380497, 0.383051])
P_obs = np.array([0.0, 0.117684, 0.175074, 0.236679, 0.234442, 0.270303, 0.272637,
0.274075, 0.278981, 0.297151, 0.297797, 0.298722, 0.326645,
0.303198, 0.277822, 0.284194, 0.301471])
#Generate second set of 'experimental data'
times_new = times + np.random.uniform(0.0,0.01)
P_obs_noisy = P_obs+np.random.normal(0,0.05,P_obs.shape)
A_obs_noisy = A_obs+np.random.normal(0,0.05,A_obs.shape)
C_obs_noisy = A_obs+np.random.normal(0,0.05,C_obs.shape)
#Combine two data sets into multi-column arrays
times_comb = np.array([times, times_new]).T
P_comb = np.array([P_obs, P_obs_noisy]).T
A_comb = np.array([A_obs, A_obs_noisy]).T
C_comb = np.array([C_obs, C_obs_noisy]).T
m = GEKKO(remote=False)
t = m.time = times_comb #using two column time array
Am = m.Param(value=A_comb) #Using the two column data as observed parameter
Cm = m.Param(value=C_comb)
Pm = m.Param(value=P_comb)
A = m.Var(1, lb = 0)
B = m.Var(0, lb = 0)
C = m.Var(0, lb = 0)
P = m.Var(0, lb = 0)
k = m.Array(m.FV,6,value=1,lb=0)
for ki in k:
ki.STATUS = 1
k1,k2,k3,k4,k5,k6 = k
r1 = m.Var(0, lb = 0)
r2 = m.Var(0, lb = 0)
r3 = m.Var(0, lb = 0)
r4 = m.Var(0, lb = 0)
r5 = m.Var(0, lb = 0)
r6 = m.Var(0, lb = 0)
m.Equation(r1 == k1 * A)
m.Equation(r2 == k2 * A * B)
m.Equation(r3 == k3 * C * B)
m.Equation(r4 == k4 * A)
m.Equation(r5 == k5 * A)
m.Equation(r6 == k6 * A * B)
#mass balance diff eqs, function calls rxn function
m.Equation(A.dt() == - r1 - r2 - r4 - r5 - r6)
m.Equation(B.dt() == r1 - r2 - r3 - r6)
m.Equation(C.dt() == r2 - r3 + r4)
m.Equation(P.dt() == r3 + r5 + r6)
m.Minimize((A-Am)**2)
m.Minimize((P-Pm)**2)
m.Minimize((C-Cm)**2)
m.options.IMODE = 5
m.options.SOLVER = 3 #IPOPT optimizer
m.options.NODES = 6
m.solve()
k_opt = []
for ki in k:
k_opt.append(ki.value[0])
print(k_opt)
plt.plot(t,A)
plt.plot(t,C)
plt.plot(t,P)
plt.plot(t,B)
plt.plot(times,A_obs,'bo')
plt.plot(times,C_obs,'gx')
plt.plot(times,P_obs,'rs')
plt.plot(times_new, A_obs_noisy,'b*')
plt.plot(times_new, C_obs_noisy,'g*')
plt.plot(times_new, P_obs_noisy,'r*')
plt.show()
To have multiple data sets with different times and data points, you can join the data sets as a pandas dataframe. Here is a simple example:
# data set 1
t_data1 = [0.0, 0.1, 0.2, 0.4, 0.8, 1.00]
x_data1 = [2.0, 1.6, 1.2, 0.7, 0.3, 0.15]
# data set 2
t_data2 = [0.0, 0.15, 0.25, 0.45, 0.85, 0.95]
x_data2 = [3.6, 2.25, 1.75, 1.00, 0.35, 0.20]
The merged data has NaN where the data is missing:
x1 x2
Time
0.00 2.0 3.60
0.10 1.6 NaN
0.15 NaN 2.25
0.20 1.2 NaN
0.25 NaN 1.75
Take note of where the data is missing with a 1=measured and 0=not measured.
# indicate which points are measured
z1 = (data['x1']==data['x1']).astype(int) # 0 if NaN
z2 = (data['x2']==data['x2']).astype(int) # 1 if number
The final step is to set up Gekko variables, equations, and objective to accommodate the data sets.
xm = m.Array(m.Param,2)
zm = m.Array(m.Param,2)
for i in range(2):
m.Equation(x[i].dt()== -k * x[i]) # differential equations
m.Minimize(zm[i]*(x[i]-xm[i])**2) # objectives
You can also calculate the initial condition with m.free_initial(x[i]). This gives an optimal solution for one parameter value (k) over the 2 data sets. This approach can be expanded to multiple variables or multiple data sets with different times.
from gekko import GEKKO
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# data set 1
t_data1 = [0.0, 0.1, 0.2, 0.4, 0.8, 1.00]
x_data1 = [2.0, 1.6, 1.2, 0.7, 0.3, 0.15]
# data set 2
t_data2 = [0.0, 0.15, 0.25, 0.45, 0.85, 0.95]
x_data2 = [3.6, 2.25, 1.75, 1.00, 0.35, 0.20]
# combine with dataframe join
data1 = pd.DataFrame({'Time':t_data1,'x1':x_data1})
data2 = pd.DataFrame({'Time':t_data2,'x2':x_data2})
data1.set_index('Time', inplace=True)
data2.set_index('Time', inplace=True)
data = data1.join(data2,how='outer')
print(data.head())
# indicate which points are measured
z1 = (data['x1']==data['x1']).astype(int) # 0 if NaN
z2 = (data['x2']==data['x2']).astype(int) # 1 if number
# replace NaN with any number (0)
data.fillna(0,inplace=True)
m = GEKKO(remote=False)
# measurements
xm = m.Array(m.Param,2)
xm[0].value = data['x1'].values
xm[1].value = data['x2'].values
# index for objective (0=not measured, 1=measured)
zm = m.Array(m.Param,2)
zm[0].value=z1
zm[1].value=z2
m.time = data.index
x = m.Array(m.Var,2) # fit to measurement
x[0].value=x_data1[0]; x[1].value=x_data2[0]
k = m.FV(); k.STATUS = 1 # adjustable parameter
for i in range(2):
m.free_initial(x[i]) # calculate initial condition
m.Equation(x[i].dt()== -k * x[i]) # differential equations
m.Minimize(zm[i]*(x[i]-xm[i])**2) # objectives
m.options.IMODE = 5 # dynamic estimation
m.options.NODES = 2 # collocation nodes
m.solve(disp=True) # solve
k = k.value[0]
print('k = '+str(k))
# plot solution
plt.plot(m.time,x[0].value,'b.--',label='Predicted 1')
plt.plot(m.time,x[1].value,'r.--',label='Predicted 2')
plt.plot(t_data1,x_data1,'bx',label='Measured 1')
plt.plot(t_data2,x_data2,'rx',label='Measured 2')
plt.legend(); plt.xlabel('Time'); plt.ylabel('Value')
plt.xlabel('Time');
plt.show()
Including my updated code (not fully cleaned up to minimize number of variables) incorporating the selected answer to my question for reference. The model does a regression of 3 measured species in two separate 'datasets.'
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from gekko import GEKKO
#Experimental data
times = np.array([0.0, 0.071875, 0.143750, 0.215625, 0.287500, 0.359375, 0.431250,
0.503125, 0.575000, 0.646875, 0.718750, 0.790625, 0.862500,
0.934375, 1.006250, 1.078125, 1.150000])
A_obs = np.array([1.0, 0.552208, 0.300598, 0.196879, 0.101175, 0.065684, 0.045096,
0.028880, 0.018433, 0.011509, 0.006215, 0.004278, 0.002698,
0.001944, 0.001116, 0.000732, 0.000426])
C_obs = np.array([0.0, 0.187768, 0.262406, 0.350412, 0.325110, 0.367181, 0.348264,
0.325085, 0.355673, 0.361805, 0.363117, 0.327266, 0.330211,
0.385798, 0.358132, 0.380497, 0.383051])
P_obs = np.array([0.0, 0.117684, 0.175074, 0.236679, 0.234442, 0.270303, 0.272637,
0.274075, 0.278981, 0.297151, 0.297797, 0.298722, 0.326645,
0.303198, 0.277822, 0.284194, 0.301471])
#Generate second set of 'experimental data'
times_new = times + np.random.uniform(0.0,0.01)
P_obs_noisy = (P_obs+ np.random.normal(0,0.05,P_obs.shape))
A_obs_noisy = (A_obs+np.random.normal(0,0.05,A_obs.shape))
C_obs_noisy = (C_obs+np.random.normal(0,0.05,C_obs.shape))
#Combine two data sets into multi-column arrays using pandas DataFrames
#Set dataframe index to be combined time discretization of both data sets
exp1 = pd.DataFrame({'Time':times,'A':A_obs,'C':C_obs,'P':P_obs})
exp2 = pd.DataFrame({'Time':times_new,'A':A_obs_noisy,'C':C_obs_noisy,'P':P_obs_noisy})
exp1.set_index('Time',inplace=True)
exp2.set_index('Time',inplace=True)
exps = exp1.join(exp2, how ='outer',lsuffix = '_1',rsuffix = '_2')
#print(exps.head())
#Combine both data sets into a single data frame
meas_data = pd.DataFrame().reindex_like(exps)
#define measurement locations for each data set, with NaN written for time points
#not common in both data sets
for cols in exps:
meas_data[cols] = (exps[cols]==exps[cols]).astype(int)
exps.fillna(0,inplace = True) #replace NaN with 0
m = GEKKO(remote=False)
t = m.time = exps.index #set GEKKO time domain to use experimental time points
#Generate two-column GEKKO arrays to store observed values of each species, A, C and P
Am = m.Array(m.Param,2)
Cm = m.Array(m.Param,2)
Pm = m.Array(m.Param,2)
Am[0].value = exps['A_1'].values
Am[1].value = exps['A_2'].values
Cm[0].value = exps['C_1'].values
Cm[1].value = exps['C_2'].values
Pm[0].value = exps['P_1'].values
Pm[1].value = exps['P_2'].values
#Define GEKKO variables that determine if time point contatins data to be used in regression
#If time point contains species data, meas_ variable = 1, else = 0
meas_A = m.Array(m.Param,2)
meas_C = m.Array(m.Param,2)
meas_P = m.Array(m.Param,2)
meas_A[0].value = meas_data['A_1'].values
meas_A[1].value = meas_data['A_2'].values
meas_C[0].value = meas_data['C_1'].values
meas_C[1].value = meas_data['C_2'].values
meas_P[0].value = meas_data['P_1'].values
meas_P[1].value = meas_data['P_2'].values
#Define Variables for differential equations A, B, C, P, with initial conditions set by experimental observation at first time point
A = m.Array(m.Var,2, lb = 0)
B = m.Array(m.Var,2, lb = 0)
C = m.Array(m.Var,2, lb = 0)
P = m.Array(m.Var,2, lb = 0)
A[0].value = exps['A_1'][0] ; A[1].value = exps['A_2'][0]
B[0].value = 0 ; B[1].value = 0
C[0].value = exps['C_1'][0] ; C[1].value = exps['C_2'][0]
P[0].value = exps['P_1'][0] ; P[1].value = exps['P_2'][0]
#Define kinetic coefficients, k1-k6 as regression FV's
k = m.Array(m.FV,6,value=1,lb=0,ub = 20)
for ki in k:
ki.STATUS = 1
k1,k2,k3,k4,k5,k6 = k
#If doing paramrter estimation, enable free_initial condition, else not include them in model to reduce DOFs (for simulation, for example)
if k1.STATUS == 1:
for i in range(2):
m.free_initial(A[i])
m.free_initial(B[i])
m.free_initial(C[i])
m.free_initial(P[i])
#Define reaction rate variables
r1 = m.Array(m.Var,2, value = 1, lb = 0)
r2 = m.Array(m.Var,2, value = 1, lb = 0)
r3 = m.Array(m.Var,2, value = 1, lb = 0)
r4 = m.Array(m.Var,2, value = 1, lb = 0)
r5 = m.Array(m.Var,2, value = 1, lb = 0)
r6 = m.Array(m.Var,2, value = 1, lb = 0)
#Model Equations
for i in range(2):
#Rate equations
m.Equation(r1[i] == k1 * A[i])
m.Equation(r2[i] == k2 * A[i] * B[i])
m.Equation(r3[i] == k3 * C[i] * B[i])
m.Equation(r4[i] == k4 * A[i])
m.Equation(r5[i] == k5 * A[i])
m.Equation(r6[i] == k6 * A[i] * B[i])
#Differential species balances
m.Equation(A[i].dt() == - r1[i] - r2[i] - r4[i] - r5[i] - r6[i])
m.Equation(B[i].dt() == r1[i] - r2[i] - r3[i] - r6[i])
m.Equation(C[i].dt() == r2[i] - r3[i] + r4[i])
m.Equation(P[i].dt() == r3[i] + r5[i] + r6[i])
#Minimization objective functions
m.Obj(meas_A[i]*(A[i]-Am[i])**2)
m.Obj(meas_P[i]*(P[i]-Pm[i])**2)
m.Obj(meas_C[i]*(C[i]-Cm[i])**2)
#Solver options
m.options.IMODE = 5
m.options.SOLVER = 3 #APOPT optimizer
m.options.NODES = 6
m.solve()
k_opt = []
for ki in k:
k_opt.append(ki.value[0])
print(k_opt)
plt.plot(t,A[0],'b-')
plt.plot(t,A[1],'b--')
plt.plot(t,C[0],'g-')
plt.plot(t,C[1],'g--')
plt.plot(t,P[0],'r-')
plt.plot(t,P[1],'r--')
plt.plot(times,A_obs,'bo')
plt.plot(times,C_obs,'gx')
plt.plot(times,P_obs,'rs')
plt.plot(times_new, A_obs_noisy,'b*')
plt.plot(times_new, C_obs_noisy,'g*')
plt.plot(times_new, P_obs_noisy,'r*')
plt.show()

How to use gekko to control two variables while manipulating two variables for a cstr?

Attached below is my PYTHON code:
I have a CSTR and im trying to control the height of the tank and the temperature while manipulating the inlet flow and the cooling temperature. The problem is that the CV's are not tracking their respective setpoints. I tried doing the problem for only 1 CV and 1 MV, it worked really well.
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from gekko import GEKKO
# Steady State Initial Condition
u1_ss = 280.0
u2_ss=100.0
# Feed Temperature (K)
Tf = 350
# Feed Concentration (mol/m^3)
Caf = 1
# Steady State Initial Conditions for the States
Ca_ss = 1
T_ss = 304
h_ss=94.77413303
V_ss=8577.41330293
x0 = np.empty(4)
x0[0] = Ca_ss
x0[1] = T_ss
x0[2]= h_ss
x0[3]= V_ss
#%% GEKKO nonlinear MPC
m = GEKKO(remote=False)
m.time = [0,0.02,0.04,0.06,0.08,0.1,0.12,0.15,0.2]
c1=10.0
Ac=100.0
# Density of A-B Mixture (kg/m^3)
rho = 1000
# Heat capacity of A-B Mixture (J/kg-K)
Cp = 0.239
# Heat of reaction for A->B (J/mol)
mdelH = 5e4
# E - Activation energy in the Arrhenius Equation (J/mol)
# R - Universal Gas Constant = 8.31451 J/mol-K
EoverR = 8750
# Pre-exponential factor (1/sec)
k0 = 7.2e10
# U - Overall Heat Transfer Coefficient (W/m^2-K)
# A - Area - this value is specific for the U calculation (m^2)
UA = 5e4
# initial conditions
Tc0 = 280
T0 = 304
Ca0 = 1.0
h0=94.77413303
q0=100.0
V0=8577.41330293
tau = m.Const(value=0.5)
Kp = m.Const(value=1)
m.Tc = m.MV(value=Tc0,lb=250,ub=350)
m.T = m.CV(value=T_ss)
m.h= m.CV(value=h0)
m.rA = m.Var(value=0)
m.Ca = m.CV(value=Ca_ss,lb=0,ub=1)
m.V= m.CV(value=V_ss,lb=0,ub=100000)
m.q=m.MV(value=q0,lb=0,ub=100000)
m.Equation(m.rA == k0*m.exp(-EoverR/m.T)*m.Ca)
m.Equation(m.T.dt() == m.q/m.V*(Tf - m.T) \
+ mdelH/(rho*Cp)*m.rA \
+ UA/m.V/rho/Cp*(m.Tc-m.T))
m.Equation(m.Ca.dt() == m.q/m.V*(Caf - m.Ca) - m.rA)
m.Equation(m.h.dt()==(m.q-c1*m.h**0.5)/Ac)
m.Equation(m.V.dt() == m.q- c1*m.h**0.5)
#MV tuning
m.Tc.STATUS = 1
m.Tc.FSTATUS = 0
m.Tc.DMAX = 100
m.Tc.DMAXHI = 20
m.Tc.DMAXLO = -100
m.q.STATUS = 1
m.q.FSTATUS = 0
m.q.DMAX = 10
#CV tuning
m.T.STATUS = 1
m.T.FSTATUS = 1
m.T.TR_INIT = 1
m.T.TAU = 1.0
DT = 0.5 # deadband
m.h.STATUS = 1
m.h.FSTATUS = 1
m.h.TR_INIT = 1
m.h.TAU = 1.0
m.Ca.STATUS = 1
m.Ca.FSTATUS = 0 # no measurement
m.Ca.TR_INIT = 0
m.V.STATUS = 1
m.V.FSTATUS = 0 # no measurement
m.V.TR_INIT = 0
m.options.CV_TYPE = 1
m.options.IMODE = 6
m.options.SOLVER = 3
#%% define CSTR model
def cstr(x,t,u1,u2,Tf,Caf,Ac):
# Inputs (3):
# Temperature of cooling jacket (K)
Tc = u1
q=u2
# Tf = Feed Temperature (K)
# Caf = Feed Concentration (mol/m^3)
# States (2):
# Concentration of A in CSTR (mol/m^3)
Ca = x[0]
# Temperature in CSTR (K)
T = x[1]
# the height of the tank (m)
h=x[2]
V=x[3]
# Parameters:
# Density of A-B Mixture (kg/m^3)
rho = 1000
# Heat capacity of A-B Mixture (J/kg-K)
Cp = 0.239
# Heat of reaction for A->B (J/mol)
mdelH = 5e4
# E - Activation energy in the Arrhenius Equation (J/mol)
# R - Universal Gas Constant = 8.31451 J/mol-K
EoverR = 8750
# Pre-exponential factor (1/sec)
k0 = 7.2e10
# U - Overall Heat Transfer Coefficient (W/m^2-K)
# A - Area - this value is specific for the U calculation (m^2)
UA = 5e4
# reaction rate
rA = k0*np.exp(-EoverR/T)*Ca
# Calculate concentration derivative
dCadt = q/V*(Caf - Ca) - rA
# Calculate temperature derivative
dTdt = q/V*(Tf - T) \
+ mdelH/(rho*Cp)*rA \
+ UA/V/rho/Cp*(Tc-T)
# Calculate height derivative
dhdt=(q-c1*h**0.5)/Ac
if x[2]>=300 and dhdt>0:
dhdt = 0
dVdt= q-c1*h**0.5
# Return xdot:
xdot = np.zeros(4)
xdot[0] = dCadt
xdot[1] = dTdt
xdot[2]= dhdt
xdot[3]= dVdt
return xdot
# Time Interval (min)
t = np.linspace(0,8,401)
# Store results for plotting
Ca = np.ones(len(t)) * Ca_ss
V=np.ones(len(t))*V_ss
T = np.ones(len(t)) * T_ss
Tsp = np.ones(len(t)) * T_ss
hsp=np.ones(len(t))*h_ss
h=np.ones(len(t))*h_ss
u1 = np.ones(len(t)) * u1_ss
u2 = np.ones(len(t)) * u2_ss
# Set point steps
Tsp[0:100] = 330.0
Tsp[100:200] = 350.0
Tsp[230:260] = 370.0
Tsp[260:290] = 390.0
hsp[0:100] = 30.0
hsp[100:200] =60.0
hsp[200:250]=90.0
# Create plot
plt.figure(figsize=(10,7))
plt.ion()
plt.show()
# Simulate CSTR
for i in range(len(t)-1):
# simulate one time period (0.05 sec each loop)
ts = [t[i],t[i+1]]
y = odeint(cstr,x0,ts,args=(u1[i+1],u2[i+1],Tf,Caf,Ac))
# retrieve measurements
Ca[i+1] = y[-1][0]
T[i+1] = y[-1][1]
h[i+1]= y[-1][2]
V[i+1]= y[-1][3]
# insert measurement
m.T.MEAS = T[i+1]
m.h.MEAS=h[i+1]
# solve MPC
m.solve(disp=True)
m.T.SPHI = Tsp[i+1] + DT
m.T.SPLO = Tsp[i+1] - DT
m.h.SPHI = hsp[i+1] + DT
m.h.SPLO = hsp[i+1] - DT
# retrieve new Tc value
u1[i+1] = m.Tc.NEWVAL
u2[i+1] = m.q.NEWVAL
# update initial conditions
x0[0] = Ca[i+1]
x0[1] = T[i+1]
x0[2]= h[i+1]
x0[3]= V[i+1]
#%% Plot the results
plt.clf()
plt.subplot(6,1,1)
plt.plot(t[0:i],u1[0:i],'b--',linewidth=3)
plt.ylabel('Cooling T (K)')
plt.legend(['Jacket Temperature'],loc='best')
plt.subplot(6,1,2)
plt.plot(t[0:i],u2[0:i],'b--',linewidth=3)
plt.ylabel('inlet flow')
plt.subplot(6,1,3)
plt.plot(t[0:i],Ca[0:i],'b.-',linewidth=3,label=r'$C_A$')
plt.plot([0,t[i-1]],[0.2,0.2],'r--',linewidth=2,label='limit')
plt.ylabel(r'$C_A$ (mol/L)')
plt.legend(loc='best')
plt.subplot(6,1,4)
plt.plot(t[0:i],V[0:i],'g--',linewidth=3)
plt.xlabel('time')
plt.ylabel('Volume of Tank')
plt.subplot(6,1,5)
plt.plot(t[0:i],Tsp[0:i],'k-',linewidth=3,label=r'$T_{sp}$')
plt.plot(t[0:i],T[0:i],'b.-',linewidth=3,label=r'$T_{meas}$')
plt.plot([0,t[i-1]],[400,400],'r--',linewidth=2,label='limit')
plt.ylabel('T (K)')
plt.xlabel('Time (min)')
plt.legend(loc='best')
plt.subplot(6,1,6)
plt.plot(t[0:i],hsp[0:i],'g--',linewidth=3,label=r'$h_{sp}$')
plt.plot(t[0:i],h[0:i],'k.-',linewidth=3,label=r'$h_{meas}$')
plt.xlabel('time')
plt.ylabel('tank level')
plt.legend(loc='best')
plt.draw()
plt.pause(0.01)

Resources