How to solve overshoot by tuning parameters with gekko? - python

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.

Related

Is it possible to specify different SPHI or SPLO limits for CVs for different parts of the horizon in GEKKO?

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()

Problem setting variable to zero when statement is met

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.

Having Optimizer switch power storage based on Capacities and net load

I am trying to have an optimizer use two power storages for a cyclic power generation and consumption cycle. The goal is to have it turn off the primary energy storage(in this case electric battery storage) when it has reached full capacity, and then to discharge first until it is empty. The secondary storage is to charge after the primary and discharge after the primary storage. I would like the optimizer to solve as it goes based on the system. I have tried using a series of switches, but it isn't quite working. I know that using if statements are tricky for gradient based solvers so if there is any help that would be great thanks!
Capacity = 76.2
EStored = m.SV(value=0,lb=0,ub=Capacity)
batteryeff = .95
batteff = m.if3((Enuc - Cons),1/batteryeff,batteryeff)
#Energy Balance
Cost = m.Var()
eneed = m.sign2(Enuc-Cons) #gives sign of energy need for energy storage or removal from storage
eswitch = m.if3(eneed,0, 1) #Turns eneed into a binary switch
switch = m.if3(eswitch*(Capacity-EStored)+(1-eswitch)*(EStored),0,1) #supposed to charge battery until at capacity and then discharge until EStored is 0. Then use thermal energy second
m.Equation(EStored.dt() == (switch)*batteff*(Enuc - Cons)) #Energy balance for Battery
m.Equation(T.dt() == (1-switch)*thermeff*(Enuc - Cons)/(mass*Cp)) #Energy balance for Thermal Storage
m.Equation(Cost == Enuc*1000 )
m.Obj(Cost)
m.options.IMODE = 5
m.options.SOLVER = 3
m.solve()
This is the section of the code that I am working with however if more details are necissary here is the simplified version of total code.
from gekko import GEKKO
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from scipy.integrate import quad
#Set up basic power consumption data
n = 24
t = np.linspace(0,n,n)
def load(t):
return -10*np.sin(2*np.pi*t/24)+40
Load = load(t)
Gen = np.ones(n)*40
def need(t):
return 10*np.sin(2*np.pi*t/24)+10
#Set up Model
m = GEKKO()
m.time = t
Cons = m.Param(value=Load)
Enuc = m.FV(value=45, lb=0) #nuclear power
Enuc.STATUS = 1
#Thermal Energy Storage
T = m.SV(value=300,ub=500,lb=300)
mass = m.FV(value=.0746,lb=0)
mass.STATUS=0
Cp = m.Param(value=5)
thermaleff = .8 #80%efficient
thermeff = m.if3((Enuc - Cons)/(mass*Cp),1/thermaleff,thermaleff)
#Battery Electrical storage
Capacity = 76.2
EStored = m.SV(value=0,lb=0,ub=Capacity)
batteryeff = .95
batteff = m.if3((Enuc - Cons),1/batteryeff,batteryeff)
#Energy Balance
Cost = m.Var()
eneed = m.sign2(Enuc-Cons) #gives sign of energy need for energy storage or removal from storage
eswitch = m.if3(eneed,0, 1) #Turns eneed into a binary switch
switch = m.if3(eswitch*(Capacity-EStored)+(1-eswitch)*(EStored),0,1) #supposed to charge battery until at capacity and then discharge until EStored is 0. Then use thermal energy second
m.Equation(EStored.dt() == (switch)*batteff*(Enuc - Cons)) #Energy balance for Battery
m.Equation(T.dt() == (1-switch)*thermeff*(Enuc - Cons)/(mass*Cp)) #Energy balance for Thermal Storage
m.Equation(Cost == Enuc*1000 )
m.Obj(Cost)
m.options.IMODE = 5
m.options.SOLVER = 3
m.solve()
#plot
plt.subplot(3,1,1)
plt.plot(t,Load)
plt.plot(t,Enuc.value, label=f'Enuc = {Enuc.value[-1]}')
plt.ylabel("Energy")
plt.legend()
plt.subplot(3,1,2)
plt.plot(t,EStored.value, label=f'Capacity = {np.max(EStored.value):.03}')
plt.title("Battery Storage")
plt.ylabel("Energy")
plt.legend()
plt.subplot(3,1,3)
plt.plot(t,T.value,label=f'mass = {mass.value[-1]:.03}')
plt.title("Thermal Storage")
plt.ylabel("Temperature(K)")
plt.legend()
plt.show()```
You can use slack variables instead of switching conditions. Here is a simple problem with two tanks and a total inlet flow of 100. The inlet flow can be split between the two tanks but the tanks have a maximum volume of 300 and 1000, respectively. Tank 2 is more expensive to use (like your thermal energy system).
import numpy as np
from gekko import GEKKO
m = GEKKO(remote=False)
m.time = np.linspace(0,10,100)
t = m.Var(0); m.Equation(t.dt()==1) # define time
flow = 100
V1 = m.Var(0,lb=0,ub=300) # tank 1
inlet1 = m.MV(0,lb=0); inlet1.STATUS = 1; inlet1.DCOST = 1e-4
m.Minimize(inlet1)
V2 = m.Var(0,lb=0,ub=1000) # tank 2
inlet2 = m.MV(0,lb=0); inlet2.STATUS = 1; inlet2.DCOST = 1e-4
m.Minimize(10*inlet2) # more expensive
# use tank 2 if tank 1 is projected to be filled
# only one in use, could also use inlet1*inlet2==0 to
# enforce the complementarity condition
m.Minimize(inlet1*inlet2)
# mass balance
m.Equation(flow==inlet1+inlet2)
m.Equations([V1.dt()==inlet1,V2.dt()==inlet2])
m.options.IMODE = 6
m.solve(disp=False)
import matplotlib.pyplot as plt
plt.plot(m.time,inlet1.value,'r--',label='Inlet 1')
plt.plot(m.time,V1.value,'r-',label='Volume 1')
plt.plot(m.time,inlet2.value,'b--',label='Inlet 2')
plt.plot(m.time,V2.value,'b-',label='Volume 2')
plt.grid()
plt.legend()
plt.show()
If the optimizer knows that both electrical and thermal energy storage will fill up then it doesn't necessarily matter when they are stored. If you do want logical conditions to enforce precidence then an m.if3() statement may be better but may also be harder to solve. If you can charge the electrical and thermal energy at the same time then the complementarity enforcer m.Minimize(inlet1*inlet2) also isn't needed and gives a different but still optimal solution.
In both cases, tank 1 is the cheaper option so it will reach capacity and minimize the filling of tank 2. This may be a better solution for your case because then you don't need the switching conditions that can cause problems. The battery storage will fill completely but it may do it at the same time as filling the thermal energy if the prediction is that battery will fill up during the night when electricity needs to be stored.

Incorporating a function into an ODE integration

This question is probably very simple but for the life of me I can't figure it out. Basically, I have a neuron whose voltage I'm modeling, but I have it receiving input spikes from other neurons randomly. So a friend of mine helped to create a function that essentially has some excitatory neurons provide a random Poisson spike which increases the voltage randomly and some inhibitory neurons providing downward spikes lowering the voltage. I've included the code below. Basically the step I'm trying to figure out how to do is how to make the I_syn term in the iterative step work. I would normally think to just write I_syn[i-1], but that gives me an error:
'function' object has no attribute '__getitem__'.
So I'm sure this question is really simple, but it's a problem I don't know how to overcome. How do I get this program to iterate the I_syn term properly so I can do a basic iterative scheme of an ODE while including a function defined previously in the code? It's important because I'll likely have more complicated neuron equations in the near future, so it would be much better to write the functions beforehand and then call them into the iteration step as needed. Thank you!
from numpy import *
from pylab import *
## setup parameters and state variables
T = 50 # total time to simulate (msec)
dt = 0.125 # simulation time step (msec)
time = arange(0, T+dt, dt) # time array
t_rest = 0 # initial refractory time
## LIF properties
Vm = zeros(len(time)) # potential (V) trace over time
Rm = 1 # resistance (kOhm)
Cm = 10 # capacitance (uF)
tau_m = Rm*Cm # time constant (msec)
tau_ref = 4 # refractory period (msec)
Vth = 1 # spike threshold (V)
V_spike = 0.5 # spike delta (V)
## Stimulus
I = 1.5 # input current (A)
N = 1000
N_ex = 0.8*N #(0..79)
N_in = 0.2*N #(80..99)
G_ex = 0.1
K = 4
def I_syn(spks, t):
"""
Synaptic current
spks = [[synid, t],]
"""
if len(spks) == 0:
return 0
exspk = spks[spks[:,0]<N_ex] # Check for all excitatory spikes
delta_k = exspk[:,1] == t # Delta function
if np.any(delta_k) > 0:
h_k = np.random.rand(len(delta_k)) < 0.90 # probability of successful transmission
else:
h_k = 0
inspk = spks[spks[:,0] >= N_ex] #Check remaining neurons for inhibitory spikes
delta_m = inspk[:,1] == t #Delta function for inhibitory neurons
if np.any(delta_m) > 0:
h_m = np.random.rand(len(delta_m)) < 0.90
else:
h_m = 0
isyn = C_m*G_ex*(np.sum(h_k*delta_k) - K*np.sum(h_m*delta_m))
return isyn
## iterate over each time step
for i, t in enumerate(time):
if t > t_rest:
Vm[i] = Vm[i-1] + (-Vm[i-1] + I_syn*Rm) / tau_m * dt
if Vm[i] >= Vth:
Vm[i] += V_spike
t_rest = t + tau_ref
## plot membrane potential trace
plot(time, Vm)
title('Leaky Integrate-and-Fire Example')
ylabel('Membrane Potential (V)')
xlabel('Time (msec)')
ylim([0,2])
show()
I_syn is just a function so using I_syn[i-1] will throw this error:
'function' object has no attribute '__getitem__'
If what you are looking for is a return value from the function, then you should first call it and then access what you want.
# pass related arguments as well since the function expects it
I_syn(arg1, arg2)[i-1]

Incorporating real data when solving a system of ODE's using scipy.integrate.odeint

I am trying to construct a simple model of a heating system represented by a system of ODEs and solved using scipy's odeint function.
I would like to incorporate 'real' data in this model, for instance external temperature (simulated as a sinewave below). The code below shows my current solution/hack which uses a function called FindVal to interpolate the real data to the timestamp being evaluated by odeint.
This is very slow so I am looking for suggestions as to how this can be done in a better way.
Here is the code...
from scipy.integrate import odeint
from numpy import linspace
from numpy import interp
from numpy import sin
from numpy.random import randint
from numpy import array
from numpy import zeros
from numpy import where
def FindVal(timeseries, t):
''' finds the value of a timeseries at the time given by the ode solver
INPUTS: timeseries - [list of times, list of values]
t - timestamp being evaluated
OUTPUTS: interpolated value at t
'''
ts_t = timeseries[0]
ts_v = timeseries[1]
# if t is beyond the end of the time series chose the last value
if t > ts_t[-1]:
val = ts_v[-1]
else:
val = interp(t, ts_t, ts_v)
return val
def SpaceHeat(Tin, t):
''' calculates the change in internal temperature
INPUTS: Tin - temperature at t - 1
t - timestep
OUTPUTS: dTdt - change in T
'''
# unpack params
ma = params['ma'] # mass of air
ca = params['ca'] # heat capacity of air
hlp = params['hlp'] # heat loss parameter
qp = params['qp'] # heater power
Touts = params['Tout'] # list of outside temps
Tout = FindVal(Touts, t) # value of Tout in this timestep
Qonoffs = params['Qonoff'] # list of heater on/offs
Qonoff = FindVal(Qonoffs, t) # heater state at this timestep
qin = qp * Qonoff # heat input
qmass = 0 # ignore mass effects for now
# calculate energy lost
qloss = (Tin - Tout) * hlp #
# calculate the change in temperature
dTdt = (qin - qmass - qloss) / (ma * ca)
return dTdt
def Solve(timeline, Qonoff):
# simulate the outside temp as a sinewave
Tout = [timeline, (sin(0.001 * timeline + 1500) * 10) + 2] # outside temp
# create a dict of model parameters
global params
params = {'ma' : 1000.0 * 250, # air mass, kg
'ca' : 1.0, # air heat capacity j/kg
'hlp' : 200.0, # home heat loss parameter wk
'qp' : 10000.0, # heater output w
'Qonoff' : Qonoff, # list of on off events
'Tout' : Tout,
}
# set the initial temperature
Tinit = 10.0
# solve
temps = odeint(SpaceHeat, Tinit, timeline)
return temps
# create a timeline for the simulation
timeline = linspace(0, 6000, 96)
# calculate the optimum control
Qonoff = zeros(len(timeline))
temps = Solve(timeline, qonoff)
This was a while ago and my knowledge has moved on significantly....
The answer to this is that you have to solve an ODE at each step of the external data you want to use, using the results from the previous integration at the start of this new state.
One line is missing, qonoff was not defined.
Qonoff = zeros(len(timeline))
qonoff = [timeline, zeros(len(timeline))]
temps = Solve(timeline, qonoff)
It's not a solution yet, just a comment.

Categories

Resources