Gekko model with variable delay - python

I have a system with a non-constant delay. Does gekko support this type of problem and can it be handled in the MHE and MPC formulation?
Reading the docs I can see how to implement the delay, but I am not sure how the state estimation part of the MPC/MHE will handle this or if it is even capable to deal with such problems.

There is no problem to include variable time delay in estimation or control problems. There is a reformulation of the problem to allow for continuous 1st and 2nd derivatives that are needed for a gradient-based optimizer. I recommend that you use a cubic spline to create a continuous approximation of the discontinuous delay function. This way, the delay can be fractional such as theta=2.3. If the delay must be integer steps then set integer=True for the theta decision variable.
theta_ub = 30 # upper bound to dead-time
theta = m.FV(0,lb=0,ub=theta_ub); theta.STATUS=1
# add extrapolation points
td = np.concatenate((np.linspace(-theta_ub,min(t)-1e-5,5),t))
ud = np.concatenate((u[0]*np.ones(5),u))
# create cubic spline with t versus u
uc = m.Var(u); tc = m.Var(t); m.Equation(tc==time-theta)
m.cspline(tc,uc,td,ud,bound_x=False)
Here is an example of one cycle of Moving Horizon Estimation with a first-order plus dead-time (FOPDT) model with variable time delay. This example is from the Process Dynamics and Control online course.
import numpy as np
import pandas as pd
from gekko import GEKKO
import matplotlib.pyplot as plt
# Import CSV data file
# Column 1 = time (t)
# Column 2 = input (u)
# Column 3 = output (yp)
url = 'http://apmonitor.com/pdc/uploads/Main/data_fopdt.txt'
data = pd.read_csv(url)
t = data['time'].values - data['time'].values[0]
u = data['u'].values
y = data['y'].values
m = GEKKO(remote=False)
m.time = t; time = m.Var(0); m.Equation(time.dt()==1)
K = m.FV(2,lb=0,ub=10); K.STATUS=1
tau = m.FV(3,lb=1,ub=200); tau.STATUS=1
theta_ub = 30 # upper bound to dead-time
theta = m.FV(0,lb=0,ub=theta_ub); theta.STATUS=1
# add extrapolation points
td = np.concatenate((np.linspace(-theta_ub,min(t)-1e-5,5),t))
ud = np.concatenate((u[0]*np.ones(5),u))
# create cubic spline with t versus u
uc = m.Var(u); tc = m.Var(t); m.Equation(tc==time-theta)
m.cspline(tc,uc,td,ud,bound_x=False)
ym = m.Param(y); yp = m.Var(y)
m.Equation(tau*yp.dt()+(yp-y[0])==K*(uc-u[0]))
m.Minimize((yp-ym)**2)
m.options.IMODE=5
m.solve()
print('Kp: ', K.value[0])
print('taup: ', tau.value[0])
print('thetap: ', theta.value[0])
# plot results
plt.figure()
plt.subplot(2,1,1)
plt.plot(t,y,'k.-',lw=2,label='Process Data')
plt.plot(t,yp.value,'r--',lw=2,label='Optimized FOPDT')
plt.ylabel('Output')
plt.legend()
plt.subplot(2,1,2)
plt.plot(t,u,'b.-',lw=2,label='u')
plt.legend()
plt.ylabel('Input')
plt.xlabel('Time')
plt.show()

Related

Improving the quality of non-linear regression in Python gekko

I am trying to use the python GEKKO non-linear regression tools to perform system identification of a second order over-damped system using the step response.
My code is as follows:
m = GEKKO()
m_input = m.Param(value=input)
m_time=m.Param(value=time)
m_T1 = m.FV(value=initT1, lb=T1bounds[0], ub=T1bounds[1])
m_T1.STATUS = 1
m_k = m.FV(value=initk,lb=100)
m_k.STATUS = 1
m_T2 = m.FV(value=initT2, lb=T2bounds[0], ub=T2bounds[1])
m_T2.STATUS = 1
m_output = m.CV(value=output)
m_output.FSTATUS=1
m.Equation(m_output==(m_k/(m_T1+m_T2))*(1+((m_T1/(m_T2-m_T1))*m.exp(-m_time/m_T2))-((m_T2/(m_T2-m_T1))*m.exp(-m_time/m_T1)))*m_input)
m.options.IMODE = 2
m.options.MAX_ITER = 10000
m.options.OTOL = 1e-8
m.options.RTOL = 1e-8
m.solve(disp=True)
The results have not been promising. It seems that the optimizer seems to get stuck in local minimas of the objective function leaving the objective function too high
The output of the solver is:
The final value of the objective function is 160453.282142838
---------------------------------------------------
Solver : IPOPT (v3.12)
Solution time : 7.60390000000189 sec
Objective : 160453.282605857
Successful solution
---------------------------------------------------
What can I do to improve the quality of the fit? Can I place limits on the objective function value?
Try using the equation for an underdamped 2nd order system instead of an overdamped 2nd order system. There is more information on the explicit solution to 2nd order systems on the Process Dynamics and Control website. An even better approach is to not use the explicit solution where it must be pre-determined if it is underdamped, critically damped, or overdamped. If the original 2nd order differential equation is used then the solver can decide if it is underdamped (overshoot with oscillations) or overdamped. Here is example code for fitting a 2nd order system.
import numpy as np
import pandas as pd
from gekko import GEKKO
import matplotlib.pyplot as plt
# Import CSV data file
# Column 1 = time (t)
# Column 2 = input (u)
# Column 3 = output (y)
url = 'http://apmonitor.com/pdc/uploads/Main/data_sopdt.txt'
data = pd.read_csv(url)
t = data['time'].values - data['time'].values[0]
u = data['u'].values
y = data['y'].values
m = GEKKO(remote=False)
m.time = t; time = m.Var(0); m.Equation(time.dt()==1)
K = m.FV(2,lb=0,ub=10); K.STATUS=1
tau = m.FV(3,lb=1,ub=200); tau.STATUS=1
theta_ub = 30 # upper bound to dead-time
theta = m.FV(0,lb=0,ub=theta_ub); theta.STATUS=1
zeta = m.FV(1,lb=0.1,ub=3); zeta.STATUS=1
# add extrapolation points
td = np.concatenate((np.linspace(-theta_ub,min(t)-1e-5,5),t))
ud = np.concatenate((u[0]*np.ones(5),u))
# create cubic spline with t versus u
uc = m.Var(u); tc = m.Var(t); m.Equation(tc==time-theta)
m.cspline(tc,uc,td,ud,bound_x=False)
ym = m.Param(y); yp = m.Var(y); xp = m.Var(y)
m.Equation(xp==yp.dt())
m.Equation((tau**2)*xp.dt()+2*zeta*tau*yp.dt()+yp==K*(uc-u[0]))
m.Minimize((yp-ym)**2)
m.options.IMODE=5
m.solve(disp=False)
print('Kp: ', K.value[0])
print('taup: ', tau.value[0])
print('thetap: ', theta.value[0])
print('zetap: ', zeta.value[0])
# plot results
plt.figure()
plt.subplot(2,1,1)
plt.plot(t,y,'k.-',lw=2,label='Process Data')
plt.plot(t,yp.value,'r--',lw=2,label='Optimized SOPDT')
plt.ylabel('Output')
plt.legend()
plt.subplot(2,1,2)
plt.plot(t,u,'b.-',lw=2,label='u')
plt.legend()
plt.ylabel('Input')
plt.show()
Please try this with your data to see if it gives a better solution.

Implementing uncertainty in dynamic optimization problem

I have a question about implementing uncertainty terms into the Gekko optimization problem. I am a beginner in coding and starting by adding parts from the "fish management" example. I have two main questions.
I would like to add an uncertainty term in the model (e.g. fluctuating future prices) but it seems like I am not understanding how the module works. I am trying to draw a random value from a certain distribution and put it into m.Var, 'ss', hoping the module will take each value at each time as t moves on. But it seems like the module does not work that way. I am wondering if there is any way I can implement uncertainty terms into the process.
Assuming the optimization problem to allocate initial land, A(0), between use A and E, is solved for a single agent by controlling land to convert, e, I am planning to expand this to a multiple agents problem. For example, if land quality, h, and land quantity A differ among agents, n, I am planning to solve multiple optimization problems using for algorithm by calling the initial m.Var value and some parameters from a loaded dataframe. If possible, may I have a brief comment on this plan?
# -*- coding: utf-8 -*-
from gekko import GEKKO
from scipy.stats import norm
from scipy.stats import truncnorm
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import operator
import math
import random
# create GEKKO model
m = GEKKO()
# Below, an agent is given initial land A(0) and makes a decision to convert this land to E
# Objective of an agent is to get maximum present utility(=log(income)) from both land use, income each period=(A+E(1-y))*Pa-C*u+E*Pe
# Uncertainty in future price lies for Pe, which I want to include with ss below
# After solving for a single agent, I want to solve this for all agents with different land quality h, risk aversion, mu, and land size A
# Then lastly collect data for total land use over time
# time points
n=51
year=10
k=50
m.time = np.linspace(0,year,n)
t=m.time
tt=t*(n-1)/year
tt = tt.astype(int)
ttt = np.exp(-t/(n-1))
# constants
Pa = 1
Pe = 1
C = 0.1
r = 0.05
y = 0.1
# distribution
# I am trying to generate a distribution, and use it as uncertainty term later in objective function
mu, sigma = 1, 0.1 # mean and standard deviation
#mu, sigman = df.loc[tt][2]
sn = np.random.normal(mu, sigma, n)
s= pd.DataFrame(sn)
ss=s.loc[tt][0]
# Control
# What is the difference between MV and CV? They give completely different solution
# MV seems to give correct answer
u = m.MV(value=0,lb=0,ub=10)
u.STATUS = 1
u.DCOST = 0
#u = m.CV(value=0,lb=0,ub=10)
# Variables
# m.Var and m.SV does not seem to lead to different results
# Can I call initial value from a dataset? for example, df.loc[tt][0] instead of 10 below?
# A = m.Var(value=df.loc[tt][0])
# h = m.Var(value=df.loc[tt][1])
A = m.SV(value=10)
E = m.SV(value=0)
#A = m.Var(value=10)
#E = m.Var(value=0)
t = m.Param(value=m.time)
Pe = m.Var(value=Pe)
d = m.Var(value=1)
# Equation
# It seems necessary to include restriction on u
m.Equation(A.dt()==-u)
m.Equation(E.dt()==u)
m.Equation(Pe.dt()==-1/k*Pe)
m.Equation(d==m.exp(-t*r))
m.Equation(A>=0)
# Objective (Utility)
J = m.Var(value=0)
# Final objective
# I want to include ss, uncertainty term in objective function
Jf = m.FV()
Jf.STATUS = 1
m.Connection(Jf,J,pos2='end')
#m.Equation(J.dt() == m.log(A*Pa-C*u+E*Pe))
m.Equation(J.dt() == m.log((A+E*(1-y))*Pa-C*u+E*Pe)*d)
#m.Equation(J.dt() == m.log(A*Pa-C*u+E*Pe*ss)*d)
# maximize profit
m.Maximize(Jf)
#m.Obj(-Jf)
# options
m.options.IMODE = 6 # optimal control
m.options.NODES = 3 # collocation nodes
m.options.SOLVER = 3 # solver (IPOPT)
# solve optimization problem
m.solve()
# print profit
print('Optimal Profit: ' + str(Jf.value[0]))
# collect data
# et=u.value
# print(et)
# At=A.value
# print(At)
# index = range(1, 2)
# columns = range(1, n+1)
# Ato=pd.DataFrame(index=index,columns=columns)
# Ato=At
# plot results
plt.figure(1)
plt.subplot(4,1,1)
plt.plot(m.time,J.value,'r--',label='profit')
plt.plot(m.time[-1],Jf.value[0],'ro',markersize=10,\
label='final profit = '+str(Jf.value[0]))
plt.plot(m.time,A.value,'b-',label='Agricultural Land')
plt.ylabel('Value')
plt.legend()
plt.subplot(4,1,2)
plt.plot(m.time,u.value,'k.-',label='adoption')
plt.ylabel('conversion')
plt.xlabel('Time (yr)')
plt.legend()
plt.subplot(4,1,3)
plt.plot(m.time,Pe.value,'k.-',label='Pe')
plt.ylabel('price')
plt.xlabel('Time (yr)')
plt.legend()
plt.subplot(4,1,4)
plt.plot(m.time,d.value,'k.-',label='d')
plt.ylabel('Discount factor')
plt.xlabel('Time (yr)')
plt.legend()
plt.show()
Try using the m.Param() (or m.FV, m.MV) instead of m.Var() to implement an uncertain value that is specified for each optimization. With m.Var(), an initial value can be specified but then it is calculated by the optimizer and the initial guess is no longer preserved.
Another way to implement uncertainty is to create an array of models with different parameters among the instances. Here is a simple example with K as a randomly selected value. This leads to 10 instances of the model that is optimized to a value of 40.
import numpy as np
from gekko import GEKKO
import matplotlib.pyplot as plt
# uncertain parameter
n = 10
K = np.random.rand(n)+1.0
m = GEKKO()
m.time = np.linspace(0,20,41)
# manipulated variable
p = m.MV(value=0, lb=0, ub=100)
p.STATUS = 1
p.DCOST = 0.1
p.DMAX = 20
# controlled variable
v = m.Array(m.CV,n)
for i in range(n):
v[i].STATUS = 1
v[i].SP = 40
v[i].TAU = 5
m.Equation(10*v[i].dt() == -v[i] + K[i]*p)
# solve optimal control problem
m.options.IMODE = 6
m.options.CV_TYPE = 2
m.solve()
# plot results
plt.figure()
plt.subplot(2,1,1)
plt.plot(m.time,p.value,'b-',LineWidth=2)
plt.ylabel('MV')
plt.subplot(2,1,2)
plt.plot([0,m.time[-1]],[40,40],'k-',LineWidth=3)
for i in range(n):
plt.plot(m.time,v[i].value,':',LineWidth=2)
plt.ylabel('CV')
plt.xlabel('Time')
plt.show()

How to estimate theta value in FOPDT equation using gekko?

I'm trying to use GEKKO to fit to a certain dataset, using the FOPDT Optimization Method to estimate k, tau and theta.
I saw the example using odeint on https://apmonitor.com/pdc/index.php/Main/FirstOrderOptimization and tried to do the same thing with GEKKO, but I can't use the value of theta in the equation.
I saw this question where should the delay call be placed inside a gekko code? and the docs https://apmonitor.com/wiki/index.php/Apps/TimeDelay, but in this case I wanted to estimate the value of theta and not use the initial guess value. I tried to use gekko's delay, but I get an error that it only works if the delay is an int value (and not a gekko FV).
I also tried to use time directly in the equation, but I can't figure out how to place x(t-theta) in there, since I can't do that syntax with gekko variables.
import pandas as pd
import numpy as np
from gekko import GEKKO
import plotly.express as px
data = pd.read_csv('data.csv',sep=',',header=0,index_col=0)
xm1 = data['x']
ym1 = data['y']
xm = xm1.to_numpy()
ym = ym1.to_numpy()
xm_r = len(xm)
tm = np.linspace(0,xm_r-1,xm_r)
m = GEKKO()
m.options.IMODE=5
m.time = tm
k = m.FV()
k.STATUS=1
tau = m.FV()
tau.STATUS=1
theta = m.FV()
theta.STATUS=1
x = m.Param(value=xm)
y = m.CV()
y.FSTATUS = 1
yObj = m.Param(value=ym)
xtheta = m.Var()
m.delay(x,xtheta,theta)
m.Equation(y.dt()==(-y + k * xtheta)/tau)
m.Minimize((y-yObj)**2)
m.options.EV_TYPE=2
m.solve(disp=True)
Here are some strategies for implementing variable time-delay in a model such as when an optimizer adjusts the time delay in a First Order Plus Dead Time (FOPDT) model.
Create a cubic spline (continuous approximation) of the relationship between time t and the input u. This allows a fractional time delay that is not restricted to an integer multiple of the sample interval.
Create time as a variable with derivative equal to 1.
Define tc with an equation tc==time-theta to get the time shifted value. This will lookup the spline uc value that corresponds to this tc value.
You can also fit the FOPDT model to data with Excel or other tools.
from gekko import GEKKO
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# load data
url = 'http://apmonitor.com/do/uploads/Main/tclab_siso_data.txt'
data = pd.read_csv(url)
t = data['time'].values
u = data['voltage'].values
y = data['temperature'].values
m = GEKKO(remote=False)
m.time = t; time = m.Var(0); m.Equation(time.dt()==1)
K = m.FV(lb=0,ub=1); K.STATUS=1
tau = m.FV(lb=1,ub=300); tau.STATUS=1
theta = m.FV(lb=2,ub=30); theta.STATUS=1
# create cubic spline with t versus u
uc = m.Var(u); tc = m.Var(t); m.Equation(tc==time-theta)
m.cspline(tc,uc,t,u,bound_x=False)
ym = m.Param(y)
yp = m.Var(y); m.Equation(tau*yp.dt()+(yp-y[0])==K*(uc-u[0]))
m.Minimize((yp-ym)**2)
m.options.IMODE=5
m.solve()
print('K: ', K.value[0])
print('tau: ', tau.value[0])
print('theta: ', theta.value[0])
plt.figure()
plt.subplot(2,1,1)
plt.plot(t,u)
plt.legend([r'$V_1$ (mV)'])
plt.ylabel('MV Voltage (mV)')
plt.subplot(2,1,2)
plt.plot(t,y)
plt.plot(t,yp)
plt.legend([r'$T_{1meas}$',r'$T_{1pred}$'])
plt.ylabel('CV Temp (degF)')
plt.xlabel('Time')
plt.savefig('sysid.png')
plt.show()
K: 0.25489655932
tau: 229.06377617
theta: 2.0
Another way to approach this is to estimate a higher-order ARX model and then determine the statistical significance of the beta terms. Here is an example of using the Gekko sysid function.
from gekko import GEKKO
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
# load data and parse into columns
url = 'http://apmonitor.com/do/uploads/Main/tclab_siso_data.txt'
data = pd.read_csv(url)
t = data['time']
u = data['voltage']
y = data['temperature']
# generate time-series model
m = GEKKO()
# system identification
na = 5 # output coefficients
nb = 5 # input coefficients
yp,p,K = m.sysid(t,u,y,na,nb,pred='meas')
print('alpha: ', p['a'])
print('beta: ', p['b'])
print('gamma: ', p['c'])
plt.figure()
plt.subplot(2,1,1)
plt.plot(t,u)
plt.legend([r'$V_1$ (mV)'])
plt.ylabel('MV Voltage (mV)')
plt.subplot(2,1,2)
plt.plot(t,y)
plt.plot(t,yp)
plt.legend([r'$T_{1meas}$',r'$T_{1pred}$'])
plt.ylabel('CV Temp (degF)')
plt.xlabel('Time')
plt.savefig('sysid.png')
plt.show()
With results:
alpha: [[0.525143 ]
[0.19284469]
[0.08177381]
[0.06152181]
[0.12918898]]
beta: [[[-8.51804876e-05]
[ 5.88425202e-04]
[ 1.99205676e-03]
[-2.81456773e-03]
[ 2.38110003e-03]]]
gamma: [0.75189199]
The first two beta terms are nearly zero but they can also be left in the model for a higher-order representation of the system.

How to build process simulator in gekko knowing time constant and steady-state values

I have a very complicated non-linear dynamical system for which I know time constant and steady-state responses at each time instance from CFD (Computational Fluid Dynamics). How do I (1) build a process simulator using this information? and (2) how do I tune time-constant values if I know measured inputs and outputs and steady-state values?
Question 1: Build a Process Simulator
You may want to first try a linear time-series model and then go to nonlinear if this doesn't work. Below is a sample script to identify a linear time-series model.
from gekko import GEKKO
import pandas as pd
import matplotlib.pyplot as plt
# load data and parse into columns
url = 'http://apmonitor.com/do/uploads/Main/tclab_dyn_data2.txt'
data = pd.read_csv(url)
t = data['Time']
u = data[['H1','H2']]
y = data[['T1','T2']]
# generate time-series model
m = GEKKO(remote=False) # remote=True for MacOS
# system identification
na = 2 # output coefficients
nb = 2 # input coefficients
yp,p,K = m.sysid(t,u,y,na,nb,diaglevel=1)
plt.figure()
plt.subplot(2,1,1)
plt.plot(t,u)
plt.legend([r'$u_0$',r'$u_1$'])
plt.ylabel('MVs')
plt.subplot(2,1,2)
plt.plot(t,y)
plt.plot(t,yp)
plt.legend([r'$y_0$',r'$y_1$',r'$z_0$',r'$z_1$'])
plt.ylabel('CVs')
plt.xlabel('Time')
plt.savefig('sysid.png')
plt.show()
Note that the data can be dynamic data, not necessarily separated into steady-state and dynamic parts. You need to call m.sysid with the correct inputs as detailed in the documentation. Once you have a good model, you can transform it into a simulator with m.arx(p) where p is the parameter output from the m.sysid function.
If linear identification doesn't work then you can try a nonlinear approach such as shown in TCLab B Exercise (See Python Gekko Neural Network). You can use Gekko's Deep Learning capabilities to simplify the coding. Once you have a steady-state relationship, add dynamics with a first-order or second-order relationship with a differential equation that relates the steady-state output to the dynamic output such as m.Equation(tau * x.dt() + x = x_ss) where tau is the time constant, x.dt() is the time derivative, x is the dynamic output, and x_ss is the steady state output. This is called a Hammerstein model because the steady state precedes the dynamic calculations. You can also put the dynamics on the inputs as a Wiener model. You'll be able to find more information on-line about Hammerstein-Wiener Models.
Question 2: Tuning the time constants
If you already have a steady-state relationship and you want to tune the time constants then regression is a powerful method because it can try many different combinations of your time constant to minimize the difference between model and measurements. There are a few examples of doing this with scipy.optimize.minimize and gekko.
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
from gekko import GEKKO
# Import or generate data
filename = 'tclab_dyn_data2.csv'
try:
data = pd.read_csv(filename)
except:
url = 'http://apmonitor.com/do/uploads/Main/tclab_dyn_data2.txt'
data = pd.read_csv(url)
# Create GEKKO Model
m = GEKKO()
m.time = data['Time'].values
# Parameters to Estimate
U = m.FV(value=10,lb=1,ub=20)
alpha1 = m.FV(value=0.01,lb=0.003,ub=0.03) # W / % heater
alpha2 = m.FV(value=0.005,lb=0.002,ub=0.02) # W / % heater
# STATUS=1 allows solver to adjust parameter
U.STATUS = 1
alpha1.STATUS = 1
alpha2.STATUS = 1
# Measured inputs
Q1 = m.MV(value=data['H1'].values)
Q2 = m.MV(value=data['H2'].values)
# State variables
TC1 = m.CV(value=data['T1'].values)
TC1.FSTATUS = 1 # minimize fstatus * (meas-pred)^2
TC2 = m.CV(value=data['T2'].values)
TC2.FSTATUS = 1 # minimize fstatus * (meas-pred)^2
Ta = m.Param(value=19.0+273.15) # K
mass = m.Param(value=4.0/1000.0) # kg
Cp = m.Param(value=0.5*1000.0) # J/kg-K
A = m.Param(value=10.0/100.0**2) # Area not between heaters in m^2
As = m.Param(value=2.0/100.0**2) # Area between heaters in m^2
eps = m.Param(value=0.9) # Emissivity
sigma = m.Const(5.67e-8) # Stefan-Boltzmann
# Heater temperatures in Kelvin
T1 = m.Intermediate(TC1+273.15)
T2 = m.Intermediate(TC2+273.15)
# Heat transfer between two heaters
Q_C12 = m.Intermediate(U*As*(T2-T1)) # Convective
Q_R12 = m.Intermediate(eps*sigma*As*(T2**4-T1**4)) # Radiative
# Semi-fundamental correlations (energy balances)
m.Equation(TC1.dt() == (1.0/(mass*Cp))*(U*A*(Ta-T1) \
+ eps * sigma * A * (Ta**4 - T1**4) \
+ Q_C12 + Q_R12 \
+ alpha1*Q1))
m.Equation(TC2.dt() == (1.0/(mass*Cp))*(U*A*(Ta-T2) \
+ eps * sigma * A * (Ta**4 - T2**4) \
- Q_C12 - Q_R12 \
+ alpha2*Q2))
# Options
m.options.IMODE = 5 # MHE
m.options.EV_TYPE = 2 # Objective type
m.options.NODES = 2 # Collocation nodes
m.options.SOLVER = 3 # IPOPT
# Solve
m.solve(disp=True)
# Parameter values
print('U : ' + str(U.value[0]))
print('alpha1: ' + str(alpha1.value[0]))
print('alpha2: ' + str(alpha2.value[0]))
# Create plot
plt.figure()
ax=plt.subplot(2,1,1)
ax.grid()
plt.plot(data['Time'],data['T1'],'ro',label=r'$T_1$ measured')
plt.plot(m.time,TC1.value,color='purple',linestyle='--',\
linewidth=3,label=r'$T_1$ predicted')
plt.plot(data['Time'],data['T2'],'bx',label=r'$T_2$ measured')
plt.plot(m.time,TC2.value,color='orange',linestyle='--',\
linewidth=3,label=r'$T_2$ predicted')
plt.ylabel('Temperature (degC)')
plt.legend(loc=2)
ax=plt.subplot(2,1,2)
ax.grid()
plt.plot(data['Time'],data['H1'],'r-',\
linewidth=3,label=r'$Q_1$')
plt.plot(data['Time'],data['H2'],'b:',\
linewidth=3,label=r'$Q_2$')
plt.ylabel('Heaters')
plt.xlabel('Time (sec)')
plt.legend(loc='best')
plt.show()

where should the delay call be placed inside a gekko code?

I am trying to use GEKKO MPC to control the level of a tank while manipulating the inlet flow. I want to model the GEKKO controller as a FOPDT. I got all the parameters I need, but I want to account for the time delay using the delay function. I am not sure of the exact position of this function since it is giving me an error when I put it in the code. When I remove it (i.e. no time delay) the code works fine but I want to be more realistic and put a time delay. Here is code attached:
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from gekko import GEKKO
# Steady State Initial Condition
u2_ss=10.0
h_ss=50.0
x0 = np.empty(1)
x0[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]
Ac=30.0
# initial conditions
h0=50.0
q0=10.0
Kp=93.48425357240352
taup=1010.8757590561246
thetap= 3
m.q=m.MV(value=q0,lb=0,ub=100)
m.h= m.CV(value=h0)
m.delay(m.q,m.h,thetap)
m.Equation(taup * m.h.dt()==m.q*Kp -m.h)
#MV tuning
m.q.STATUS = 1
m.q.FSTATUS = 0
m.q.DMAX = 100
#CV tuning
m.h.STATUS = 1
m.h.FSTATUS = 1
m.h.TR_INIT = 2
m.h.TAU = 1.0
m.h.SP = 55.0
m.options.CV_TYPE = 2
m.options.IMODE = 6
m.options.SOLVER = 3
#%% define CSTR model
def cstr(x,t,u2,Ac):
q=u2
Ac=30.0
# States (2):
# the height of the tank (m)
h=x[0]
# Parameters:
# Calculate height derivative
dhdt=(q-5)/Ac
# Return xdot:
xdot = np.zeros(1)
xdot[0]= dhdt
return xdot
# Time Interval (min)
t = np.linspace(0,20,400)
# Store results for plotting
hsp=np.ones(len(t))*h_ss
h=np.ones(len(t))*h_ss
u2 = np.ones(len(t)) * u2_ss
# Set point steps
hsp[0:100] = 55.0
hsp[100:]=70.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=(u2[i],Ac))
# retrieve measurements
h[i+1]= y[-1][0]
# insert measurement
m.h.MEAS=h[i+1]
m.h.SP=hsp[i+1]
# solve MPC
m.solve(disp=True)
# retrieve new q value
u2[i+1] = m.q.NEWVAL
# update initial conditions
x0[0]= h[i+1]
#%% Plot the results
plt.clf()
plt.subplot(2,1,1)
plt.plot(t[0:i],u2[0:i],'b--',linewidth=3)
plt.ylabel('inlet flow')
plt.subplot(2,1,2)
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)
The problem with your model is that the differential equation and the delay model are trying to solve the value of m.h based on the value of m.q. Both equations cannot be satisfied at the same time. The delay object requires that m.h is delayed version of m.q from 3 cycles ago. The differential equation requires the solution of the linear differential equation. They do not produce the same answer for m.h so this leads to an infeasible solution as the solver correctly reports.
m.delay(m.q,m.h,thetap)
m.Equation(taup * m.h.dt()==m.q*Kp -m.h)
Instead, you should create a new variable such as m.qd as a delayed version of m.q. The m.dq is then the input to the differential equation.
m.qd=m.Var()
m.delay(m.q,m.qd,thetap)
m.Equation(taup * m.h.dt()==m.qd*Kp -m.h)
Other Issues, Not Related to the Question
There were a couple other issues with your application.
The time synchronization between the simulator and controller are not correct. You should use the same cycle time for the simulator and controller. I changed the simulation time to t = np.linspace(0,20,201) for a 0.1 min cycle time.
The delay model requires that the controller have uniform time spacing because it is a discrete model. I changed the controller time spacing to m.time = np.linspace(0,2,21) or 0.1 min for a cycle time.
The simulator (solved with ODEINT) does not have input delay so there is model mismatch between the controller and simulator. This is still okay because it is a realistic scenario to have model mismatch but you'll need to realize that there will be some corrective control action based on feedback from the simulator. The controller is able to drive the level to the set point but there is chatter in the MV due to model mismatch and the squared error objective.
To improve the chatter, I switched to m.options.CV_TYPE=1, set a SPHI and SPLO dead-band, opened the initial trajectory with m.options.TR_OPEN=50, and added a move suppression with m.q.DCOST. These have the effect of achieving similar performance but without the valve chatter.
Here is the source code:
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import odeint
from gekko import GEKKO
# Steady State Initial Condition
u2_ss=10.0
h_ss=50.0
x0 = np.empty(1)
x0[0]= h_ss
#%% GEKKO nonlinear MPC
m = GEKKO(remote=False)
m.time = np.linspace(0,2,21)
Ac=30.0
# initial conditions
h0=50.0
q0=10.0
Kp=93.48425357240352
taup=1010.8757590561246
thetap= 3
m.q=m.MV(value=q0,lb=0,ub=100)
m.qd=m.Var(value=q0)
m.h= m.CV(value=h0)
m.delay(m.q,m.qd,thetap)
m.Equation(taup * m.h.dt()==m.qd*Kp -m.h)
#MV tuning
m.q.STATUS = 1
m.q.FSTATUS = 0
m.q.DMAX = 100
m.q.DCOST = 1
#CV tuning
m.h.STATUS = 1
m.h.FSTATUS = 1
m.h.TR_INIT = 1
m.h.TR_OPEN = 50
m.h.TAU = 0.5
m.options.CV_TYPE = 1
m.options.IMODE = 6
m.options.SOLVER = 3
#%% define CSTR model
def cstr(x,t,u2,Ac):
q=u2
Ac=30.0
# States (2):
# the height of the tank (m)
h=x[0]
# Parameters:
# Calculate height derivative
dhdt=(q-5)/Ac
# Return xdot:
xdot = np.zeros(1)
xdot[0]= dhdt
return xdot
# Time Interval (min)
t = np.linspace(0,20,201)
# Store results for plotting
hsp=np.ones(len(t))*h_ss
h=np.ones(len(t))*h_ss
u2 = np.ones(len(t)) * u2_ss
# Set point steps
hsp[0:100] = 55.0
hsp[100:] = 70.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=(u2[i],Ac))
# retrieve measurements
h[i+1]= y[-1][0]
# insert measurement
m.h.MEAS=h[i+1]
# for CV_TYPE = 1
m.h.SPHI=hsp[i+1]+0.05
m.h.SPLO=hsp[i+1]-0.05
# for CV_TYPE = 2
m.h.SP=hsp[i+1]
# solve MPC
m.solve(disp=False)
# retrieve new q value
u2[i+1] = m.q.NEWVAL
# update initial conditions
x0[0]= h[i+1]
#%% Plot the results
plt.clf()
plt.subplot(2,1,1)
plt.plot(t[0:i],u2[0:i],'b--',linewidth=3)
plt.ylabel('inlet flow')
plt.subplot(2,1,2)
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)

Categories