I am trying to make a simple cleaning scheduling tool for when to conduct chemical cleaning in a heat exchanger network. But when I, correctly, find the optimal time for cleaning (x-variable) I cannot set the scaling thickness to zero (sigma) at time t, I have tried using m.if3 but to no avail. I have added a simple version of my problem below. Any feedback is appreciated.
from gekko import GEKKO
import numpy as np
import matplotlib.pyplot as plt
def LN(x):
return m.log(x)/np.log(2.718)
m = GEKKO(remote=False)
lambdag=0.1 #[W/mK]
days_to_consider = 1
m.time=np.linspace(0, 24*days_to_consider, 24*days_to_consider+1)
N = 6 #Number of heat exchanger
sigm = m.Array(m.Var,N,value=0.0,lb=0)
Rf = m.Array(m.Var,N,value=0.0,lb=0) #[m2K/W]
U = m.Array(m.Param,N,lb=0)
LMTD = m.Array(m.Param,N,lb=0)
Tco = m.Array(m.Param,N,lb=0)
Tci = m.Array(m.Param,N,lb=0)
Q = m.Array(m.Param,N,value=0.0)
dQ = m.Array(m.Var,N,value=0.0)
x = m.Array(m.MV,N,value=0,lb=0,ub=1,integer=True)
x[0].STATUS=1
x[1].STATUS=1
x[2].STATUS=1
x[3].STATUS=1
x[4].STATUS=1
x[5].STATUS=1
EL = m.Array(m.Param,N,value=0)
ELchc = m.Array(m.Param,N,value=0)
Thilist = [105,116,125,129,136,142] #Hot vapor entering [degC] ->Condensing
mdotlist = [582.5,582.5,582.5,582.5,582.5,582.5] # Solution flow [t/h]
Arealist = [600,400,200,300,200,300] #Heating surface [m2]
kglist = [0.0094,0.0003,0.0007,4.5019e-05,0.0003,4.6977e-05] # Deposit rate
Ucllist = [1700,2040,3300,3300,3200,2300] # Cleaned Heat transfer Coefficient [W/m2K]
Qcllist = [10036.4,9336.6,7185.8,5255.4,5112.5,5678.8]
CE = 0.5 #fuel cost[EUR/kWh]
Cchc = 500 #Cleaning cost [EUR/CIP]
#Temperature into heat exchanger network (HEN)
Tci[0] = 90 # degC
#Loop through HEN
for u in range(0,N):
Thi = Thilist[u]
Tci = Thi-8
mdot = mdotlist[u]
Area=Arealist[u]
# Scaling kinematics
kg = kglist[u]
Ucl = Ucllist[u]
Qcl = Qcllist[u]
m.Equation(sigm[u].dt()==kg*lambdag)
#TODO PROBLEM: cannot set sigma to zero at time t when x(t) is 1
#b = m.if3(x[u]-1,1,0) # binary switch
m.Equation(sigm[u]==(1)*Rf[u]*lambdag)
U[u] = m.Intermediate(Ucl/(1+Ucl*Rf[u]))
# Thermodynamics
LMTD[u]=m.Intermediate(((Thi-Tci)-(Thi-Tco[u]))/LN((Thi-Tci)/(Thi-Tco[u])))
Tco[u]=m.Intermediate(LMTD[u]*U[u]*Area/(mdot/3.6*3300*1000)+Tci)
Q[u]=m.Intermediate(U[u]*Area*LMTD[u]/1000)
m.Equation(dQ[u].dt()==1/6*(Qcl - Q[u]))
EL[u]=m.Intermediate(CE*dQ[u])
ELchc[u]=m.Intermediate(CE*(Q[u] -1/6*Q[u] )*2.44+Cchc)
u +=1
m.Minimize(m.sum([EL[u]*(1-x[u])+(ELchc[u]*x[u]) for u in range(0,len(x))]))
#Constrains
m.Equation(m.sum(x)<=1.0) # Only one clean at time
m.options.IMODE=6
m.solver_options = ['minlp_maximum_iterations 500', \
'minlp_gap_tol 0.01',\
'nlp_maximum_iterations 500']
m.options.SOLVER = 1
m.solve(debug=True,disp=True)
plt.figure(figsize=(12, 6))
plt.subplot(141)
for i in range(0,5):
plt.bar(m.time,x[i].value,label='CIP'+str(i), width=1.0)
plt.legend()
plt.subplot(142)
plt.plot(m.time,EL[0].value,label='Energy cost')
plt.plot(m.time,ELchc[0].value,label='CIP cost')
plt.legend()
plt.subplot(143)
for i in range(0,5):
plt.plot(m.time,U[i].value,label='U'+str(i))
plt.legend()
plt.subplot(144)
for i in range(0,5):
plt.plot(m.time,sigm[i].value,label='scaling'+str(i))
plt.legend()
plt.show()
If the simulation does not need to proceed past that condition then there is a variable time method that divides derivatives by a final time variable (tf). This adjusts the final time, similar to the method show for the Jennings benchmark problem. Here is a simplified problem:
import numpy as np
from gekko import GEKKO
import matplotlib.pyplot as plt
m = GEKKO()
m.time = np.linspace(0,1,101)
x = m.Var(0,lb=0,ub=1)
tf = m.FV(1,lb=0.1,ub=10); tf.STATUS=1
m.Equation(x.dt()/tf==0.2)
m.Maximize(tf)
m.options.IMODE=6
m.options.SOLVER=1
m.solve()
t = [ti*tf.value[0] for ti in m.time]
plt.plot(t,x.value,label='x')
plt.legend()
plt.show()
The final time is adjusted to maximize tf while observing the constraint that x<1. The differential equation is dx/dt=0.2 so the terminal constraint is reached at t=5. Could a similar strategy be used for the heat exchanger problem? There are methods to simulate a heat exchanger cleanout but a change to variable time may be the simplest solution.
Related
I would like to build a GEKKO model for dynamic schedule optimisation of a system. I am exploring GEKKO functionality with a toy problem (code attached below). I foresee that it would be required to specify different targets for some CV's for different parts of the horizon as depicted below.
I tried assigning an array to the SPLO parameter of the Level CV but it simply collapsed the SPHI and SPLO to the starting value of the CV.
I like the flexibility of using the objective function to drive the solution as opposed to 'hard' constraints. Can this be accomplished in a non-iterative implementation and if so how?
from gekko import GEKKO
import numpy as np
import json
import pandas as pd
from matplotlib import pyplot as plt
def G1_offline(timespace=100):
tk_lowlimit=[37]*100 #init low limit
tk_lowlimit[40:70]=[38]*30 #increase low limit for portion of horizon
m=GEKKO(remote=False)
#tk_lowlimit_hard=m.Param(tk_lowlimit)
rundown_schedule=[100]*timespace #init rundown schedule
rundown_schedule[40:45]=[95]*5 #adjust schedile for few points
m.time=np.linspace(0,timespace-1,timespace)
m.Unit1_Feed=m.MV(value=25,lb=0,ub=60,name='Unit1 Feed')
m.Unit2_Feed=m.MV(value=27,lb=0,ub=60,name='Unit2 Feed')
m.Fuel=m.MV(value=10,lb=0,ub=100,name='Fuel')
m.Rundown=m.MV(name='Rundown') #This is a DV
m.Efficiency=m.FV(value=0.99,lb=0.95,ub=1,name='Efficiency')
m.Rundown.value=rundown_schedule
m.Flare=m.SV(value=30,lb=0,ub=100,name='Flare')
m.TankLevel=m.CV(value=25, lb=0,ub=300,name='tklevel')
m.Consumers=m.MV(value=30,lb=0,ub=130,name='Consumers')
m.Product=m.Intermediate((m.Unit1_Feed+m.Unit2_Feed)*m.Efficiency,name='Product')
m.Balance=m.Intermediate(m.Product-m.Consumers,name='Balance')
m.Equation(m.TankLevel.dt()==m.Balance)
m.Equation(m.Flare==m.Rundown-(m.Unit1_Feed+m.Unit2_Feed+m.Fuel))
#m.Equation(m.Flare>=1)
#GLOBAL OPTIONS
m.options.IMODE=6 #control mode,dynamic control, simultaneous
m.options.NODES=2 #collocation nodes
m.options.SOLVER=1 # 1=APOPT, 2=BPOPT, 3=IPOPT
m.options.CV_TYPE=1 #2 = squared error from reference trajectory
m.options.CTRL_UNITS=3 #control time steps units (3= HOURS)
m.options.CTRL_TIME=1 #1=1 hour per time step
m.options.REQCTRLMODE=3 #3= CONTROL
#m.options.SCALING=2
m.options.RTOL=1e-6
m.options.OTOL=1e-6
#m.options.CV_WGT_START=5
m.options.CSV_WRITE=2
#MV/DV modes
m.Unit1_Feed.STATUS=1 #1 = can change
m.Unit2_Feed.STATUS=1 #1 = can change
m.Fuel.STATUS=1 #1 = can change
m.Consumers.STATUS=1 #1 = can change
m.Rundown.STATUS=0 #0 = cannot change, this is a DV
m.Efficiency.STATUS=0
m.Efficiency.FSTATUS=1
#CV Modes
m.TankLevel.STATUS=1 #1 = Control this CV
#m.Flare.STATUS=0 #0 = Do Not Control this CV
m.TankLevel.FSTATUS=1 #Allow Feedback
m.TankLevel.STATUS=1 #Control this CV
m.TankLevel.TAU=12 #Time constant for trajectory
m.TankLevel.SPHI=40 #Upper limit for trajectory
m.TankLevel.SPLO=37 #Lower limit for trajectory
m.TankLevel.WSPLO=20 #Penalty for crossing LO limit
m.TankLevel.WSPHI=20 #Penalty for crossing HI limit
m.TankLevel.TR_INIT=0 #0 -Do not re-center.
m.TankLevel.TR_OPEN=1 #Openi#ng shape of trajectory
m.Consumers.COST=-40
m.Unit1_Feed.COST=5
m.Unit2_Feed.COST=4
m.Fuel.COST=-2
#m.Flare.COST=0
m.Consumers.DCOST=15
m.Unit1_Feed.DCOST=5
m.Unit2_Feed.DCOST=5
m.Fuel.DCOST=1
m.Consumers.DMAX=10
m.Unit1_Feed.DMAX=10
m.Unit2_Feed.DMAX=8
m.Fuel.DMAX=10
m.Consumers.MV_STEP_HOR=1
m.Unit1_Feed.MV_STEP_HOR=1
m.Unit2_Feed.MV_STEP_HOR=1
m.Fuel.MV_STEP_HOR=1
m.solve(GUI=False)
with open(m.path+'//results.json') as f:
results = json.load(f)
#print(results)
results_df=pd.DataFrame(results)
print(results_df)
#results_df.to_excel(r'c:\data\toyproblem.xlsx')
fig = plt.figure(figsize=(14,6))
plt.plot(results_df['time'],results_df['tklevel'],color='red',label='Level')
plt.fill_between(x=results_df['time'],y1=results_df['tklevel.tr_lo'], y2=results_df['tklevel.tr_hi'],color='green',alpha=0.2, label='Tklevel CV bounds')
plt.xlabel('TIME')
plt.title('Controlled solution')
plt.ylabel('TankLevel')
plt.legend(bbox_to_anchor=(0.0, 1), loc='upper left', borderaxespad=0.5)
plt.minorticks_on()
plt.grid(color = 'b', linestyle = '--', linewidth = 0.5, axis='y')
plt.show()
fig = plt.figure(figsize=(14,6))
plt.plot(results_df['time'],results_df['unit1_feed'],color='red',label='Unit1')
plt.plot(results_df['time'],results_df['unit2_feed'],color='green',label='Unit2')
plt.plot(results_df['time'],results_df['consumers'],color='black',label='Consumers')
plt.plot(results_df['time'],results_df['flare'],color='orange',label='Flare')
plt.plot(results_df['time'],results_df['fuel'],color='blue',label='Fuel')
plt.plot(results_df['time'],results_df['rundown'],color='purple',label='Rundown')
plt.xlabel('TIME'), plt.ylabel('knm3/h'), plt.title('Independent variables'),
plt.legend(bbox_to_anchor=(0.0, 1), loc='upper left', borderaxespad=0.5)
plt.minorticks_on()
plt.grid(color = 'b', linestyle = '--', linewidth = 0.5, axis='y')
trj_hi=results_df['tklevel.tr_hi']
trj_lo=results_df['tklevel.tr_lo']
return m,results_df
#----main----
c1,results_df=G1_offline(100)
It is possible to customize SPHI and SPLO instead of a fixed target value. This is accomplished by redefining the CV as a difference between the current and target value. The target value can be a feedforward traj=m.Param() with the values updated each cycle of the controller with something like traj.value = [custom_setpoint]. There is an example of this approach in the Dynamic Optimization course (see bottom of the page).
# Error
e = m.CV(value=0,name='e')
m.Equation(e==v-traj)
# CV tuning
e.STATUS = 1 #add the CV to the objective
m.options.CV_TYPE = 1 #Dead-band
db = 2
e.SPHI = db #set point
e.SPLO = -db #set point
e.TR_INIT = 0 #dead-band
Some applications require a custom reference trajectory that does not fit a standard form. A custom reference trajectory is specified by creating a new error (e) variable that is the difference between the specified trajectory (sinusoidal, sawtooth, random, etc) and the model output. This error is specified as a controlled variable (CV) with an upper and lower dead-band denoted as SPHI and SPLO. The CV can also be a value of zero with a squared error objective (e.SP=0, m.options.CV_TYPE=2) to drive to a target instead of a dead-band range.
import numpy as np
from random import random
from gekko import GEKKO
import matplotlib.pyplot as plt
# initialize GEKKO model
m = GEKKO()
# time
m.time = np.linspace(0,20,41)
# constants
mass = 500
# Parameters
b = m.Param(value=50)
K = m.Param(value=0.8)
# Manipulated variable
p = m.MV(value=0, lb=-100, ub=100)
# Reference trajectory
sine = 10*np.sin(m.time/20*4*np.pi)
traj = m.Param(value=sine)
# Controlled Variable
v = m.SV(value=0,name='v')
# Error
e = m.CV(value=0,name='e')
# Equations
m.Equation(mass*v.dt() == -v*b + K*b*p)
m.Equation(e==v-traj)
m.options.IMODE = 6 # control
# MV tuning
p.STATUS = 1 #allow optimizer to change
p.DCOST = 0.1 #smooth out MV
p.DMAX = 50 #slow down change of MV
# CV tuning
e.STATUS = 1 #add the CV to the objective
m.options.CV_TYPE = 1 #Dead-band
db = 2
e.SPHI = db #set point
e.SPLO = -db #set point
e.TR_INIT = 0 #dead-band
# Solve
m.solve()
# get additional solution information
import json
with open(m.path+'//results.json') as f:
results = json.load(f)
# Plot solution
plt.figure()
plt.subplot(3,1,1)
plt.plot(m.time,p.value,'b-',lw=2,label='MV')
plt.legend(loc='best')
plt.ylabel('MV')
plt.subplot(3,1,2)
plt.plot(m.time,sine+db,'k-',label='SPHI')
plt.plot(m.time,sine-db,'k-',label='SPLO')
plt.plot(m.time,v.value,'r--',lw=2,label='CV')
plt.legend(loc='best')
plt.ylabel('CV')
plt.subplot(3,1,3)
plt.plot(m.time,results['e.tr_hi'],'k-',label='SPHI')
plt.plot(m.time,results['e.tr_lo'],'k-',label='SPLO')
plt.plot(m.time,e.value,'r--',lw=2,label='Error')
plt.legend(loc='best')
plt.ylabel('Error')
plt.xlabel('time')
plt.show()
The acquisition channel of scipy and the same version are used.
The result of least_squares is different depending on the environment.
Differences in the environment, the PC is different.
version:1.9.1 py39h316f440_0
channel:conda-forge
environment:windows
I've attached the source code I ran.
If the conditions are the same except for the environment, I would like to get the same results.
Why different causes? How can I do that?
thank you.
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from scipy.optimize import least_squares
import random
random.seed(134)
import numpy as np
np.random.seed(134)
%matplotlib inline
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from scipy.optimize import least_squares
def report_params(fit_params_values, fit_param_names):
for each in range(len(fit_param_names)):
print(fit_param_names[each], 'is', fit_params_values[each])
# define your modules
def pCon1():
# This is the module for a specific insubstatiation of a constituitive promoter
# the input is nothing
# the output is a protein production amount per time unit
pCon1_production_rate = 100
return pCon1_production_rate
def pLux1(LuxR, AHL):
# This is the module for a specific insubstatiation of a lux promoter
# the input is a LuxR amount and an AHL amount
# the output is a protein production amount per time unit
# For every promoter there is some function that determines what the promoter's
# maximal and basal expression are based on the amount of transcriptional factor
# is floating around in the cell. These numbers are empircally determined, and
# for demonstration purposes are fictionally and arbitrarily filled in here.
# These functions take the form of hill functions.
basal_n = 2
basal_basal = 2
basal_max = 2
basal_kd = 2
basal_expression_rate = basal_basal + (basal_max * (LuxR**basal_n / (LuxR**basal_n + basal_kd)))
max_n = 2
max_max = 2
max_kd = 2
maximal_expression_rate = (LuxR**max_n / (LuxR**max_n + max_kd))
pLux1_n = 2
pLux1_kd = 10
pLux1_production_rate = basal_expression_rate + maximal_expression_rate*(AHL**pLux1_n / (pLux1_kd + AHL**pLux1_n))
return pLux1_production_rate
def simulation_set_of_equations(y, t, *args):
# Args are strictly for parameters we want to eventually estimate.
# Everything else must be hardcoded below. Sorry for the convience.
# Unpack your parameters
k_pCon_express = args[0] # A summation of transcription and translation from a pCon promoter
k_pLux_express = args[1] # A summation of transcription and translation from a pLux promoter
k_loss = args[2] # A summation of dilution and degredation
# Unpack your current amount of each species
LuxR, GFP, AHL = y
# Determine the change in each species
dLuxR = pCon1() - k_loss*LuxR
dGFP = pLux1(LuxR, AHL)*k_pLux_express - k_loss*GFP
dAHL = 0 # for now we're assuming AHL was added exogenously and never degrades
# Return the change in each species; make sure same order as your init values
# scipy.odeint will take these values and apply them to the current value of each species in the next time step for you
return [dLuxR, dGFP, dAHL]
# Parameters
k_pCon_express = 101
k_pLux_express = 50
k_loss = 0.1
params = (k_pCon_express, k_pLux_express, k_loss)
param_names = ['k_pCon_express', 'k_pLux_express', 'k_loss'] # somehow this is honestly necessary in Python?!
# Initial Conditions
# LuxR, GFP, AHL
init_P = [1000, 0, 11]
# Timesteps
n_steps = 500
t = np.linspace(0, 30, n_steps)
num_P = odeint(simulation_set_of_equations, init_P, t, args = (params))
plt.plot(t, num_P[:,0], c='b', label = 'LuxR')
plt.plot(t, num_P[:,1], c='g', label = 'GFP')
plt.plot(t, num_P[:,2], c='r', label = 'AHL')
plt.xlabel('Time')
plt.ylabel('Concentration')
plt.legend(loc = 'best')
plt.grid()
plt.yscale('log')
plt.show()
noise = np.random.normal(0, 10, num_P.shape)
exp_P = num_P + noise
exp_t = t[::10]
exp_P = exp_P[::10]
# Create experimental data. Just take the regular simulation data and add some gaussian noise to it.
def residuals(params):
params = tuple(params)
sim_P = odeint(simulation_set_of_equations, init_P, exp_t, args = params)
res = sim_P - exp_P
return res.flatten()
initial_guess = (100, 100, 100)
low_bounds = [0, 0, 0]
up_bounds = [1000, 1000, 1000]
fitted_params = least_squares(residuals, initial_guess, bounds=(low_bounds, up_bounds)).x
# small reminder: .x is the fitted parameters attribute of the least_squares output
# With least_squares function, unlike, say, curve_fit, it does not compute the covariance matrix for you
# TODO calculate standard deviation of parameter estimation
# (will this ever be used other than sanity checking?)
print(params)
report_params(fitted_params, param_names)
(101, 50, 0.1)
k_pCon_express is 100.0
k_pLux_express is 49.9942246627
k_loss is 0.100037839987
plt.plot(t, odeint(simulation_set_of_equations, init_P, t, args = tuple(params))[:,1], c='r', label='GFP - Given Param Simulation')
plt.scatter(exp_t, exp_P[:,1], c='b', label='GFP - Fake Experimental Data')
plt.plot(t, odeint(simulation_set_of_equations, init_P, t, args = tuple(fitted_params))[:,1], c='g', label='GFP - Fitted Param Simlulation')
plt.legend(loc = 'best')
plt.xlabel('Time')
plt.ylabel('Concentration')
plt.grid()
plt.yscale('log')
plt.show()
GEKKO is optimization software for mixed-integer and differential algebraic equations. It is coupled with large-scale solvers for linear, quadratic, nonlinear, and mixed integer programming (LP, QP, NLP, MILP, MINLP).
I use gekko to control my TCLab Arduino, but when I give a disturbance, no matter how I adjust the parameters, there will be a overshoot temperature. How can I solve this problem?
Here is my code:
import tclab
import numpy as np
import time
import matplotlib.pyplot as plt
from gekko import GEKKO
# Connect to Arduino
a = tclab.TCLab()
# Get Version
print(a.version)
# Turn LED on
print('LED On')
a.LED(100)
# Run time in minutes
run_time = 60.0
# Number of cycles
loops = int(60.0*run_time)
tm = np.zeros(loops)
# Temperature (K)
T1 = np.ones(loops) * a.T1 # temperature (degC)
Tsp1 = np.ones(loops) * 35.0 # set point (degC)
# heater values
Q1s = np.ones(loops) * 0.0
#########################################################
# Initialize Model
#########################################################
# use remote=True for MacOS
m = GEKKO(name='tclab-mpc',remote=False)
# 100 second time horizon
m.time = np.linspace(0,100,101)
# Parameters
Q1_ss = m.Param(value=0)
TC1_ss = m.Param(value=a.T1)
Kp = m.Param(value=0.8)
tau = m.Param(value=160.0)
# Manipulated variable
Q1 = m.MV(value=0)
Q1.STATUS = 1 # use to control temperature
Q1.FSTATUS = 0 # no feedback measurement
Q1.LOWER = 0.0
Q1.UPPER = 100.0
Q1.DMAX = 50.0
# Q1.COST = 0.0
Q1.DCOST = 0.2
# Controlled variable
TC1 = m.CV(value=TC1_ss.value)
TC1.STATUS = 1 # minimize error with setpoint range
TC1.FSTATUS = 1 # receive measurement
TC1.TR_INIT = 2 # reference trajectory
TC1.TR_OPEN = 2 # reference trajectory
TC1.TAU = 35 # time constant for response
m.Equation(tau * TC1.dt() + (TC1-TC1_ss) == Kp * (Q1-Q1_ss))
# Global Options
m.options.IMODE = 6 # MPC
m.options.CV_TYPE = 1 # Objective type
m.options.NODES = 2 # Collocation nodes
m.options.SOLVER = 1 # 1=APOPT, 3=IPOPT
##################################################################
# Create plot
plt.figure()
plt.ion()
plt.show()
filter_tc1 = []
def movefilter(predata, new, n):
if len(predata) < n:
predata.append(new)
else:
predata.pop(0)
predata.append(new)
return np.average(predata)
# Main Loop
start_time = time.time()
prev_time = start_time
try:
for i in range(1,loops):
# Sleep time
sleep_max = 1.0
sleep = sleep_max - (time.time() - prev_time)
if sleep>=0.01:
time.sleep(sleep)
else:
time.sleep(0.01)
# Record time and change in time
t = time.time()
dt = t - prev_time
prev_time = t
tm[i] = t - start_time
# Read temperatures in Kelvin
curr_T1 = a.T1
last_T1 = curr_T1
avg_T1 = movefilter(filter_tc1, last_T1, 3)
T1[i] = curr_T1
###############################
### MPC CONTROLLER ###
###############################
TC1.MEAS = avg_T1
# input setpoint with deadband +/- DT
DT = 0.1
TC1.SPHI = Tsp1[i] + DT
TC1.SPLO = Tsp1[i] - DT
# solve MPC
m.solve(disp=False)
# test for successful solution
if (m.options.APPSTATUS==1):
# retrieve the first Q value
Q1s[i] = Q1.NEWVAL
else:
# not successful, set heater to zero
Q1s[i] = 0
# Write output (0-100)
a.Q1(Q1s[i])
# Plot
plt.clf()
ax=plt.subplot(2,1,1)
ax.grid()
plt.plot(tm[0:i],T1[0:i],'ro',MarkerSize=3,label=r'$T_1$')
plt.plot(tm[0:i],Tsp1[0:i],'b-',MarkerSize=3,label=r'$T_1 Setpoint$')
plt.ylabel('Temperature (degC)')
plt.legend(loc='best')
ax=plt.subplot(2,1,2)
ax.grid()
plt.plot(tm[0:i],Q1s[0:i],'r-',LineWidth=3,label=r'$Q_1$')
plt.ylabel('Heaters')
plt.xlabel('Time (sec)')
plt.legend(loc='best')
plt.draw()
plt.pause(0.05)
# Turn off heaters
a.Q1(0)
a.Q2(0)
print('Shutting down')
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()
# 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
There is the test result picture.
When you add the disturbance (such as turn on the other heater), the apparent system gain increases because the temperature rises higher than anticipated by the controller. That means you start to go left on the mismatch plot (leads to worst control performance).
This is Figure 14 in Hedengren, J. D., Eaton, A. N., Overview of Estimation Methods for Industrial Dynamic Systems, Optimization and Engineering, Springer, Vol 18 (1), 2017, pp. 155-178, DOI: 10.1007/s11081-015-9295-9.
One of the reasons for the overshoot is because of model mismatch. Here are a few ways to deal with this:
Increase your model gain K (maybe to 1) or decrease your model tau (maybe to 120) so that the controller becomes less aggressive. You may also want to re-identify your model so that it better reflects your TCLab system dynamics. Here is a tutorial on getting a first order or second order model. A higher order ARX model also works well for the TCLab.
Change the reference trajectory to be less aggressive with TC.TAU=50 and include the reference trajectory on the plot so that you can observe what the controller is planning. I also like to include the unbiased model on the plot to show how the model is performing.
Check out this Control Tuning page for help with other MV and CV tuning options. The Jupyter notebook widget can help give you an intuitive understanding of those options.
I am trying to estimate the parameters of my set of ODEs in my program, always minimizing the error between my experimental data and predicted data.
The problem is, I can obtain a good prediction and a very good fit, but I can only estimate the same number of points as my experimental data, which returns a very strange output.
Can you please provide me more information in how can I obtain a more accurate set of predicted points?
Code can be found below.
from gekko import GEKKO
import numpy as np
import matplotlib.pyplot as plt
xm = np.array([0,1,2,3,4,5])
ym = np.array([2.0,1.5,np.nan,2.2,3.0,5.0])
m = GEKKO(remote=False)
m.time = xm
a = m.FV(lb=0.1,ub=2.0)
a.STATUS=1
y = m.CV(value=ym,name='y',fixed_initial=False)
y.FSTATUS=1
m.Equation(y.dt()==a*y)
m.options.IMODE = 5
m.options.SOLVER = 1
m.solve(disp=True)
print('Optimized, a = ' + str(a.value[0]))
plt.figure(figsize=(6,2))
plt.plot(xm,ym,'bo',label='Meas')
plt.plot(xm,y.value,'r-',label='Pred')
plt.ylabel('y')
plt.ylim([0,6])
plt.legend()
plt.show()
If I replace variable m.time to obtain more data predicted with :
m.time = np.linspace(0,5,30)
I get the error: raise Exception('Data arrays must have the same length, and match time discretization in dynamic problems')
Exception: Data arrays must have the same length, and match time discretization in dynamic problems
There are two options (Methods 1 and 2) that I've shown below. You can either plot the interpolating nodes to give you more resolution or create a new model for simulation.
from gekko import GEKKO
import numpy as np
import matplotlib.pyplot as plt
xm = np.array([0,1,2,3,4,5])
ym = np.array([2.0,1.5,np.nan,2.2,3.0,5.0])
m = GEKKO(remote=False)
m.time = xm
a = m.FV(lb=0.1,ub=2.0)
a.STATUS=1
y = m.CV(value=ym,name='y',fixed_initial=False)
y.FSTATUS=1
m.Equation(y.dt()==a*y)
m.options.IMODE = 5
m.options.SOLVER = 1
m.options.CSV_WRITE = 2 # For Method 1
m.options.NODES = 3 # For Method 1 (options 3-6)
m.solve(disp=True)
print('Optimized, a = ' + str(a.value[0]))
# Method 1: Plot interpolating nodes
import json
with open(m.path+'//results_all.json') as f:
results = json.load(f)
# Method 2: Re-simulate with more points
sim = GEKKO(remote=False)
ap = a.value[0]
xp = np.linspace(0,7); sim.time=xp
yp = sim.Var(y.value[0])
sim.Equation(yp.dt()==ap*yp)
sim.options.NODES = 3
sim.options.IMODE=4; sim.solve()
plt.figure(figsize=(6,2))
plt.plot(xm,ym,'bo',label='Meas')
plt.plot(xm,y.value,'gs-.',label='Pred Original')
plt.plot(results['time'],results['y'],'kx-',\
MarkerSize=10,label='Pred Method 1')
plt.plot(xp,yp,'r.--',label='Pred Method 2')
plt.ylabel('y')
plt.ylim([0,10])
plt.legend()
plt.show()
A third option is to reset the .value of the original model but that can be tedious. Instead, you can also create both the estimation and simulation models in a loop as is done with an example of Moving Horizon Estimation and Model Predictive Control that use the same model but transfer parameters between them:
# use remote=True for MacOS
mhe = GEKKO(name='tclab-mhe',remote=False)
mpc = GEKKO(name='tclab-mpc',remote=False)
# create 2 models (MHE and MPC) in one loop
for m in [mhe,mpc]:
# Parameters with bounds
m.K1 = m.FV(value=0.607,lb=0.1,ub=1.0)
m.K2 = m.FV(value=0.293,lb=0.1,ub=1.0)
m.K3 = m.FV(value=0.24,lb=0.1,ub=1.0)
m.tau12 = m.FV(value=192,lb=100,ub=200)
m.tau3 = m.FV(value=15,lb=10,ub=20)
m.Ta = m.Param(value=23.0) # degC
m.Q1 = m.MV(value=0,lb=0,ub=100,name='q1')
m.Q2 = m.MV(value=0,lb=0,ub=100,name='q2')
# Heater temperatures
m.TH1 = m.SV(value=T1m[0])
m.TH2 = m.SV(value=T2m[0])
# Sensor temperatures
m.TC1 = m.CV(value=T1m[0],name='tc1')
m.TC2 = m.CV(value=T2m[0],name='tc2')
# Temperature difference between two heaters
m.DT = m.Intermediate(m.TH2-m.TH1)
# Equations
m.Equation(m.tau12*m.TH1.dt()+(m.TH1-m.Ta)==m.K1*m.Q1+m.K3*m.DT)
m.Equation(m.tau12*m.TH2.dt()+(m.TH2-m.Ta)==m.K2*m.Q2-m.K3*m.DT)
m.Equation(m.tau3*m.TC1.dt()+m.TC1==m.TH1)
m.Equation(m.tau3*m.TC2.dt()+m.TC2==m.TH2)
I have two thermodynamic relationships for low (300-1000K) and high (1000-3000K) temperatures. If I want to use both of these in Gekko, how can I combine them into a single correlation that I can use in an optimization problem?
Here is a section of Python code that calculates either the low or high temperature relationship from 300K to 3000K.
import numpy as np
import matplotlib.pyplot as plt
T = np.linspace(300.0,3000.0,50)
a_lo = np.array([ 5.15,-1.37E-02,4.92E-05,-4.85E-08,1.67E-11])
a_hi = np.array([7.49E-02,1.34E-02,-5.73E-06,1.22E-09,-1.02E-13])
i_lo = np.where(np.logical_and(T>=300.0, T<1000.0))
i_hi = np.where(np.logical_and(T>=1000.0, T<=3000.0))
cp = np.zeros(50)
Rg = 8.314 # J/mol-K
cp[i_lo] = a_lo[0] + a_lo[1]*T[i_lo] + a_lo[2]*T[i_lo]**2.0 + \
a_lo[3]*T[i_lo]**3.0 + a_lo[4]*T[i_lo]**4.0
cp[i_hi] = a_hi[0] + a_hi[1]*T[i_hi] + a_hi[2]*T[i_hi]**2.0 + \
a_hi[3]*T[i_hi]**3.0 + a_hi[4]*T[i_hi]**4.0
cp *= Rg
plt.plot(T,cp,'k-',lw=5)
plt.plot(T[i_lo],cp[i_lo],'.',color='orange')
plt.plot(T[i_hi],cp[i_hi],'.',color='red')
plt.xlabel('Temperature (K)'); plt.grid()
plt.ylabel(r'$CH_4$ Heat Capacity $\left(\frac{J}{mol-K}\right)$')
plt.show()
I tried using a conditional (if) statement in building my model but it only uses the correlation that is selected from the initialized values. If temperature T is a variable in my model, I want it to switch to one or the other based on the temperature variable.
There are a few approaches to use a conditional function in your optimization or simulation problem. The first approach not exact but may be a suitable approximation by using a cubic spline that creates an interpolation between sampled points (see approach #1). The second approach is exact but requires either an Mathematical Program with Complementarity Constraints (MPCC) with if2() or an Integer Switch variable with if3() (see approach #2). These two approaches are discussed in the Design Optimization Course page on Logical Conditions in Optimization.
import numpy as np
import matplotlib.pyplot as plt
from gekko import GEKKO
# CH4 Heat capacity parameters (LO: 300-1000K, HI: 1000K-3000K)
a_lo = np.array([ 5.15,-1.37E-02,4.92E-05,-4.85E-08,1.67E-11])
a_hi = np.array([7.49E-02,1.34E-02,-5.73E-06,1.22E-09,-1.02E-13])
Rg = 8.314 # J/mol-K
m = GEKKO()
# Approach #1: Cubic Spline
def cp1(T):
if T>=300 and T<=1000:
a = a_lo
elif T>1000 and T<=3000:
a = a_hi
else:
raise Exception('Temperature ' + str(T) + ' out of range')
cp = (a[0]+a[1]*T+a[2]*T**2.0+a[3]*T**3.0+a[4]*T**4.0)*Rg
return cp
# Calculate cp at 50 pts
T = np.linspace(300.0,3000.0,50)
cp = [cp1(Ti) for Ti in T]
x1 = m.Var(lb=300,ub=3000); y1 = m.Var()
m.cspline(x1,y1,T,cp)
# Approach #2: Gekko conditional statements
def cp2(a,T):
return (a[0]+a[1]*T+a[2]*T**2.0+a[3]*T**3.0+a[4]*T**4.0)*Rg
x2 = m.Var(lb=300,ub=3000)
y2a = m.Intermediate(cp2(a_lo,x2));
y2b = m.Intermediate(cp2(a_hi,x2));
y2 = m.if3(x2-1000,y2a,y2b)
m.Equation(y1==80)
m.Equation(y2==80)
m.solve()
print('Find Temperature where cp=80 J/mol-K')
print(x1.value[0],x2.value[0])