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.
Related
I want to optimize the operation of a grid-coupled PV battery system with Pyomo. I assume a given timeseries of PV power and electricty load in kWh and a static grid selling and buying price.
But I do not want to implement a perfect foresight approach - 1 optimization problem, where the optimizer knows all timesteps. Instead the optimizer should only see only some timestps into the future. In the following mwe the solver should see the next 6 timesteps for the optimization of the overall 12 timesteps.
Question1: I wonder if this is the most efficient implementation (in terms of computing time)?
#%%
import pyomo.environ as pyo
import pandas as pd
#%% Define data
data_import = pd.DataFrame(data={'pv': [0,0,0,0,0,0,0,0,10,20,40,100],
'load': [20,20,20,20,20,20,20,30,30,30,30,30]},
index=[1,2,3,4,5,6,7,8,9,10,11,12])
#%% Define chunked data
chunked_data = list()
chunk_size = 6
# Add repeated index starting at 1 until chunk_size
data_import.insert(loc=0, column='index', value=list(range(1,chunk_size+1))+list(range(1,chunk_size+1)))
# Chunk data into chunked_data of length 6
for i in range(0, len(data_import), chunk_size):
chunked_data.append(data_import[i:i+chunk_size])
# results list of optimization results
results_battery_soc = []
results_grid_power_import = []
results_grid_power_export = []
#%% Define model for each chunked_data
for i in range(0, len(chunked_data)):
print(i)
# Get chunked data list
data = chunked_data[i]
if i == 0:
bat_soc_initial = 0.5
else:
bat_soc_initial = results_battery_soc[-1]
# Define model
model = pyo.ConcreteModel()
# Define timeperiods
model.T = pyo.RangeSet(len(data))
#%%
# Defien general parameter
model.grid_cost_buy = 0.30
model.grid_cost_sell = 0.10
# Define model parameters from chunked input data
model.pv_power = pyo.Param(model.T, initialize=data.set_index('index')['pv'].to_dict())
model.load_power = pyo.Param(model.T, initialize=data.set_index('index')['load'].to_dict())
#%% Create a block for a single time period
def block_rule(b, t):
# define the parameters
b.battery_soc_initial_value = pyo.Param(initialize=bat_soc_initial)
b.battery_eff = pyo.Param(initialize=0.9)
b.battery_eoCH = pyo.Param(initialize=1.0)
b.battery_eoDCH = pyo.Param(initialize=0.1)
b.battery_capacity = pyo.Param(initialize=380)
# define the variables
b.battery_power_CH = pyo.Var(domain=pyo.NonNegativeReals)
b.battery_power_DCH = pyo.Var(domain=pyo.NonNegativeReals)
b.battery_soc = pyo.Var(bounds=(b.battery_eoDCH, b.battery_eoCH))
b.battery_soc_initial = pyo.Var()
b.grid_power_import = pyo.Var(domain=pyo.NonNegativeReals)
b.grid_power_export = pyo.Var(domain=pyo.NonNegativeReals)
## Define the constraints
# Balanced electricity bus rule
def balanced_bus_rule(_b):
return (0 == (_b.model().pv_power[t] - _b.model().load_power[t]
+ _b.battery_power_DCH - _b.battery_power_CH
+ _b.grid_power_import - _b.grid_power_export))
b.bus_c = pyo.Constraint(rule=balanced_bus_rule)
# Battery end of CH/ constraints
def battery_end_of_CH_rule(_b):
return (_b.battery_eoCH >= _b.battery_soc)
b.battery_eoCH_c = pyo.Constraint(rule=battery_end_of_CH_rule)
# Battery end of DCH constraints
def battery_end_of_DCH_rule(_b):
return (_b.battery_eoDCH <= _b.battery_soc)
b.battery_eoDCH_c = pyo.Constraint(rule=battery_end_of_DCH_rule)
# Battery SoC constraint
def battery_soc_rule(_b):
return (_b.battery_soc == _b.battery_soc_initial +
((_b.battery_power_CH - _b.battery_power_DCH) / _b.battery_capacity))
b.battery_soc_c = pyo.Constraint(rule=battery_soc_rule)
# Initialize Blocks for each timestep defined in T
model.pvbatb = pyo.Block(model.T, rule=block_rule)
#model.pprint()
#%% Further constraints
# link the battery SoC variables between different timesteps
def battery_soc_linking_rule(m, t):
if t == m.T.first():
return m.pvbatb[t].battery_soc_initial == m.pvbatb[t].battery_soc_initial_value
return m.pvbatb[t].battery_soc_initial == m.pvbatb[t-1].battery_soc
model.battery_soc_linking = pyo.Constraint(model.T, rule=battery_soc_linking_rule)
#%% Objective function
# define the cost function
def obj_rule(m):
return sum(m.pvbatb[t].grid_power_import * m.grid_cost_buy
- m.pvbatb[t].grid_power_export * m.grid_cost_sell for t in m.T)
model.obj = pyo.Objective(rule=obj_rule, sense=pyo.minimize)
#%% Solve the problem
solver = pyo.SolverFactory('glpk')
results = solver.solve(model)
#%% Access of results
for t in model.T:
results_battery_soc.append(pyo.value(model.pvbatb[t].battery_soc))
results_grid_power_import.append(pyo.value(model.pvbatb[t].grid_power_import))
results_grid_power_export.append(pyo.value(model.pvbatb[t].grid_power_export))
Question2: This example divides the 12 timestep long timeseries into 2 timeseries of each 6 timesteps and optimizes them individually - 2 optimization problems. The number of sold problems further increases if one optimizes the timeseries of timestep 1-6, then 2-7, then 3-8, then 4-9, then 5-10, then 6-11 and finally 7-12, which results into 7 optimization problems. In this case the question of a efficient implementation becomes even more important.
Thank you so much for your support and proposals. I am tottaly stucked at this point!
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.
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 currently making a model where I will be integrated Battery and thermal energy storage with a nuclear power plant on a power grid. I have made my model so that it will run both types of power storage seperately correctly. The issue that I am having is that when I comment one of the storages out so that it is not involved in storing the energy somehow in the first 2 timesteps it gets half of its over all capacity in energy even though it is disconnected. When I comment the other system out the same issue happens to the power system that is commented out. Do you know what is causing this?
Here is my code for it. I have simplified it down so that I can include all of it.
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()
m.Equation(EStored.dt() == batteff*(Enuc - Cons)) #Energy balance for Battery
#m.Equation(T.dt() == thermeff*(Enuc - Cons)/(mass*Cp)) #Energy balance for Thermal Storage
m.Equation(Cost == Enuc*1000 + Capacity*1000 + mass*5000)
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)
plt.subplot(3,1,2)
plt.plot(t,EStored.value, label=f'Capacity = {EStored.value[12]:.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()
The issue is that you are eliminating the equation but the variable is still adjustable by the optimizer. The optimizer determines that it can fill up the storage for free without the equation. You could try the following instead to switch on or off the ability to do battery or thermal energy storage.
# select battery or thermal storage
battery_storage = True
thermal_storage = True
if battery_storage:
# Energy balance for Battery
m.Equation(EStored.dt() == batteff*(Enuc - Cons))
else:
# Battery storage off
m.Equation(EStored.dt() == 0)
if thermal storage:
# Energy balance for Thermal Storage
m.Equation(T.dt() == thermeff*(Enuc - Cons)/(mass*Cp))
else:
# Thermal storage off
m.Equation(T.dt() == 0)
Another option is to define the decision variables as Manipulated Variables and turn the STATUS on (1) or off (0) depending on whether the optimizer can use those.
Another option (more condensed) is to use battery_storage parameter directly in the equation as m.Equation(EStored.dt() == battery_storage*batteff*(Enuc - Cons)). When battery_storage is zero (off) then it will set the derivative to zero. You could do the same for thermal_storage. If you make battery_storage and thermal_storage an adjustable parameter as a Gekko variable then you can turn them on or off as the simulation runs cycle-to-cycle.
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.