Pyomo: most efficient limited foresight operational optimization of PV battery system? - python

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!

Related

Can I use implicit objective function in a design optimization process in Gekko. this implicit function is a subroutine that creates numerical model

I am doing an optimization for designing a seismic resistant structural system using Abaqus.
I am intending to use Gekko for this purpose. But it seems that I am making a mistakes in writing down the suitable syntax concerning this task.
"Objective" is the name of the subroutine that is responsible for creating the numerical model of Abaqus, analyzing the model, processing results and calculating the penalty function.
"Objective" returns the values of the cost of the building summed up with the penalty function to be minimized.
this is the message of error:
#error: Insufficient Data
CSV Read Error : number of data rows must be >= 2 for dynamic problems
Data Points Identified: 1
STOPPING. . .
Traceback (most recent call last):
File "C:\temp\AK\24-Gekko\opti1.10.3.0.py", line 721, in
m.solve()
File "C:\Users\amjad\AppData\Roaming\Python\Python38\site-packages\gekko\gekko.py", line 2140, in solve
raise Exception(apm_error)
Exception: #error: Insufficient Data
CSV Read Error : number of data rows must be >= 2 for dynamic problems
Data Points Identified: 1
STOPPING. . .
this is the main code of the optimization process:
'''
m = GEKKO(remote=False)
Alfa1 = m.Const(1.25)
Alfa2 = m.Const(0.3)
Alfa3 = m.Const(1.25)
UCS = m.Const(1000.) # $/Ton of steel
UCC = m.Const(67.) # $/m3 of concrete
UCCF = m.Const(20.) # $/m2 of column wood framework
UCBF = m.Const(28.) # $/m2 of beam wood framework
UCWF = m.Const(20.) # $/m2 of wall wood framework
GammaS = m.Const(7850)
GammaC = m.Const(2500)
cover = m.Const(10.)
f_c = m.Const(30.)
fy = m.Const(400.)
Est = m.Const(200000.)
eu_c = m.Const(0.003)
bw = m.Const(700.)
fai = 6.
#defining material parameters
Mats = {
"CDP30.0" :("mm",GammaC.value*1.e-12,0.18,0.,[f_c.value,0.0015,0.4,100],[f_c.value/10., 0.001 , 1.0],35,0.1,1.12,0.667,0.1,1.0,0.0),
"C30.0" :("mm",GammaC.value*1.e-12,0.18,0.,f_c.value),
"S400.0":("mm",GammaS.value*1.e-12,0.3,0.,fy.value),
}
#defining section's locations within building members and lengths of members
Sections = {
"C11":[[("A0-A1","A1-A2","D0-D1","D1-D2"),750.],[],[],[]],
"C22":[[("A2-A3","A3-A4","A4-A5","D2-D3","D3-D4","D4-D5"),750.],[],[],[]],
"C33":[[("B0-B1","C0-C1","B1-B2","C1-C2","B2-B3","C2-C3","B3-B4","C3-C4","B4-B5","C4-C5",),750.],[],[],[]],
"BB1":[[("A1-B1","C1-D1","A2-B2","C2-D2","A3-B3","C3-D3","A4-B4","C4-D4","A5-B5","C5-D5",),700.],[],[]],
"WW1":[[("B0-C1","B1-C2","B2-C3","B3-C4","B4-C5",),750.],[],[]],
}
#creating materials of the model
MatText = []
for i in Mats:
if i[1:3]=="DP":CDPs(i,Mats[i][0],Mats[i][1],Mats[i][2],Mats[i][3],Mats[i][4],Mats[i][5],Mats[i][6],Mats[i][7],Mats[i][8],Mats[i][9],Mats[i][10],Mats[i][11],Mats[i][12])
elif i[0] =="C" :EPPs(i,Mats[i][0],Mats[i][1],Mats[i][2],Mats[i][3],Mats[i][4])
elif i[0] =="S" :EPPs(i,Mats[i][0],Mats[i][1],Mats[i][2],Mats[i][3],Mats[i][4])
#objective function initializing
FX = 0.
Vars = []
for i in [x for x in Sections if x[0]=="C"]:
a = m.Var(value=125,lb=100,ub=250,integer = False)
b = m.Var(value=125,lb=100,ub=250,integer = False)
Us = m.Var(value=0.02,lb=0.01,ub=0.045,integer = False)
rebarS = rectRC(a.value,b.value,Us.value,cover.value,fai,"C")
Sections[i][1] = [a.value,b.value,Us.value,rebarS]
As = np.pi*fai**2./4*(4+2*rebarS[1][0]+2*rebarS[2][0])
#calculating the cost of a Column section
Sections[i][2] = m.Intermediate(Alfa1*As/(1000**3)*GammaS*UCS+a*b/(1000*1000)*UCC+2*(a+b)/(1000)*UCCF)
Sections[i][3] = PMinteraction(eu_c.value,cover.value,a.value,b.value,f_c.value,fy.value,Est.value,(2+rebarS[1][0])*np.pi*fai**2/4,(2+rebarS[1][0])*np.pi*fai**2/4)
FX += len(Sections[i][0][0])*Sections[i][0][1]/1000*Sections[i][2]
Vars.append((i,Sections[i][1][:3]))
for i in [x for x in Sections if x[0]=="B"]:
a = m.Var(value=125,lb=100,ub=250,integer = False)
b = m.Var(value=125,lb=100,ub=250,integer = False)
Us = m.Var(value=0.02,lb=0.01,ub=0.045)
rebarS = rectRC(a.value,b.value,Us.value,cover.value,fai,"B")
Sections[i][1] = [a.value,b.value,Us.value,rebarS]
As = np.pi*fai**2./4*(4+2*rebarS[1][0]+2*rebarS[2][0])
#calculating the cost of a Beam section
Sections[i][2] = m.Intermediate((2*Alfa2*As/(1000**3)+(1-2*Alfa2)*As/(1000**3))*GammaS*UCS+a*b/(1000*1000)*UCC+(a+2*b)/(1000)*UCBF)
print (Sections[i][2])
FX += len(Sections[i][0][0])*Sections[i][0][1]/1000*Sections[i][2]
Vars.append((i,Sections[i][1][:3]))
for i in [x for x in Sections if x[0]=="W"]:
a = m.Var(value=125,lb=100,ub=250,integer = False)
b = bw.value
Us = m.Var(value=0.009,lb=0.007,ub=0.01)
rebarS = rectRC(a.value,b.value,Us.value,cover.value,fai,"W")
Sections[i][1] = [a.value,b.value,Us.value,rebarS]
As = np.pi*fai**2./4*(4+2*rebarS[1][0]+2*rebarS[2][0])
#calculating the cost of a Wall section
Sections[i][2] = m.Intermediate(Alfa3*As/(1000**3)*GammaS*UCS+a*b/(1000*1000)*UCC+2*b/(1000)*UCWF)
print (Sections[i][2])
FX += len(Sections[i][0][0])*Sections[i][0][1]/1000*Sections[i][2]
Vars.append((i,Sections[i][1][:3]))
#modifying object function by a reference value
FX = FX/ReferenceFX * 1.
m.Minimize(Objective(Vars))
m.options.SOLVER = 1
m.options.IMODE = 6
m.solve()
'''
Gekko solution modes are described in more detail in the documentation. The current mode is IMODE=6 that should have differential and algebraic equations. In this mode, it is required to define the time points for the solution such as:
m.time=[0,0.1,0.2,0.5,1.0,1.5,2.0]
If it is a steady state solution (no differential equations) then it should be IMODE=3 for steady state optimization.
m.options.IMODE=3
There is currently no definition for PMinteraction. If this is an Abaqus model call then it will need to be replaced by a suitable model approximation that Gekko can compile into byte-code. Some options are cspline (1D), bspline (2D), or machine learning models (higher dimension functions).

How can I get sco.minimize to give me a solution that isn't the initial guesses?

I have a piece of code that worked well when I optimized advertising budget with 2 variables (channels) but when I added aditional channels, it stopped optimizing with no error messages.
import numpy as np
import scipy.optimize as sco
# setup variables
media_budget = 100000 # total media budget
media_labels = ['launchvideoviews', 'conversion', 'traffic', 'videoviews', 'reach'] # channel names
media_coefs = [0.3524764781, 5.606903166, -0.1761937775, 5.678596017, 10.50445914] #
# model coefficients
media_drs = [-1.15, 2.09, 6.7, -0.201, 1.21] # diminishing returns
const = -243.1018144
# the function for our model
def model_function(x, media_coefs, media_drs, const):
# transform variables and multiply them by coefficients to get contributions
channel_1_contrib = media_coefs[0] * x[0]**media_drs[0]
channel_2_contrib = media_coefs[1] * x[1]**media_drs[1]
channel_3_contrib = media_coefs[2] * x[2]**media_drs[2]
channel_4_contrib = media_coefs[3] * x[3]**media_drs[3]
channel_5_contrib = media_coefs[4] * x[4]**media_drs[4]
# sum contributions and add constant
y = channel_1_contrib + channel_2_contrib + channel_3_contrib + channel_4_contrib + channel_5_contrib + const
# return negative conversions for the minimize function to work
return -y
# set up guesses, constraints and bounds
num_media_vars = len(media_labels)
guesses = num_media_vars*[media_budget/num_media_vars,] # starting guesses: divide budget evenly
args = (media_coefs, media_drs, const) # pass non-optimized values into model_function
con_1 = {'type': 'eq', 'fun': lambda x: np.sum(x) - media_budget} # so we can't go over budget
constraints = (con_1)
bound = (0, media_budget) # spend for a channel can't be negative or higher than budget
bounds = tuple(bound for x in range(5))
# run the SciPy Optimizer
solution = sco.minimize(model_function, x0=guesses, args=args, method='SLSQP', constraints=constraints, bounds=bounds)
# print out the solution
print(f"Spend: ${round(float(media_budget),2)}\n")
print(f"Optimized CPA: ${round(media_budget/(-1 * solution.fun),2)}")
print("Allocation:")
for i in range(len(media_labels)):
print(f"-{media_labels[i]}: ${round(solution.x[i],2)} ({round(solution.x[i]/media_budget*100,2)}%)")
And the result is
Spend: $100000.0
Optimized CPA: $-0.0
Allocation:
-launchvideoviews: $20000.0 (20.0%)
-conversion: $20000.0 (20.0%)
-traffic: $20000.0 (20.0%)
-videoviews: $20000.0 (20.0%)
-reach: $20000.0 (20.0%)
Which is the same as the initial guesses argument.
Thank you very much!
Update: Following #joni comment, I passed the gradient function explicitly, but still no result.
I don't know how to change the constrains to test #chthonicdaemon
comment yet.
import numpy as np
import scipy.optimize as sco
# setup variables
media_budget = 100000 # total media budget
media_labels = ['launchvideoviews', 'conversion', 'traffic', 'videoviews', 'reach'] # channel names
media_coefs = [0.3524764781, 5.606903166, -0.1761937775, 5.678596017, 10.50445914] #
# model coefficients
media_drs = [-1.15, 2.09, 6.7, -0.201, 1.21] # diminishing returns
const = -243.1018144
# the function for our model
def model_function(x, media_coefs, media_drs, const):
# transform variables and multiply them by coefficients to get contributions
channel_1_contrib = media_coefs[0] * x[0]**media_drs[0]
channel_2_contrib = media_coefs[1] * x[1]**media_drs[1]
channel_3_contrib = media_coefs[2] * x[2]**media_drs[2]
channel_4_contrib = media_coefs[3] * x[3]**media_drs[3]
channel_5_contrib = media_coefs[4] * x[4]**media_drs[4]
# sum contributions and add constant (objetive function)
y = channel_1_contrib + channel_2_contrib + channel_3_contrib + channel_4_contrib + channel_5_contrib + const
# return negative conversions for the minimize function to work
return -y
# partial derivative of the objective function
def fun_der(x, media_coefs, media_drs, const):
d_chan1 = 1
d_chan2 = 1
d_chan3 = 1
d_chan4 = 1
d_chan5 = 1
return np.array([d_chan1, d_chan2, d_chan3, d_chan4, d_chan5])
# set up guesses, constraints and bounds
num_media_vars = len(media_labels)
guesses = num_media_vars*[media_budget/num_media_vars,] # starting guesses: divide budget evenly
args = (media_coefs, media_drs, const) # pass non-optimized values into model_function
con_1 = {'type': 'eq', 'fun': lambda x: np.sum(x) - media_budget} # so we can't go over budget
constraints = (con_1)
bound = (0, media_budget) # spend for a channel can't be negative or higher than budget
bounds = tuple(bound for x in range(5))
# run the SciPy Optimizer
solution = sco.minimize(model_function, x0=guesses, args=args, method='SLSQP', constraints=constraints, bounds=bounds, jac=fun_der)
# print out the solution
print(f"Spend: ${round(float(media_budget),2)}\n")
print(f"Optimized CPA: ${round(media_budget/(-1 * solution.fun),2)}")
print("Allocation:")
for i in range(len(media_labels)):
print(f"-{media_labels[i]}: ${round(solution.x[i],2)} ({round(solution.x[i]/media_budget*100,2)}%)")
The reason you are not able to solve this exact problem turns out to be all about the specific coefficients you have. For the problem as it is specified, the optimum appears to be near allocations where some spends are zero. However, at spends near zero, due to the negative coefficients in media_drs, the objective function rapidly becomes infinite. I believe this is what is causing the issues you are experiencing. I can get a solution with success = True by manipulating the 6.7 to be 0.7 in the coefficients and setting lower bound that is larger than 0 to stop the objective function from exploding. So this isn't so much of a programming issue as a problem formulation issue.
I cannot imagine it would be true that you would see more payoff when you reduce the budget on a particular item, so all the negative powers in media_dirs seem off to me.
I will also post here some improvements I made while debugging this issue. Notice that I'm using numpy arrays more to make some of the functions easier to read. Also notice how I have calculated a correct jacobian:
import numpy as np
import scipy.optimize as sco
# setup variables
media_budget = 100000 # total media budget
media_labels = ['launchvideoviews', 'conversion', 'traffic', 'videoviews', 'reach'] # channel names
media_coefs = np.array([0.3524764781, 5.606903166, -0.1761937775, 5.678596017, 10.50445914]) #
# model coefficients
media_drs = np.array([-1.15, 2.09, 1.7, -0.201, 1.21]) # diminishing returns
const = -243.1018144
# the function for our model
def model_function(x, media_coefs, media_drs, const):
# transform variables and multiply them by coefficients to get contributions
channel_contrib = media_coefs * x**media_drs
# sum contributions and add constant
y = channel_contrib.sum() + const
# return negative conversions for the minimize function to work
return -y
def model_function_jac(x, media_coefs, media_drs, const):
dy_dx = media_coefs * media_drs * x**(media_drs-1)
return -dy_dx
# set up guesses, constraints and bounds
num_media_vars = len(media_labels)
guesses = num_media_vars*[media_budget/num_media_vars,] # starting guesses: divide budget evenly
args = (media_coefs, media_drs, const) # pass non-optimized values into model_function
con_1 = {'type': 'ineq', 'fun': lambda x: media_budget - sum(x)} # so we can't go over budget
constraints = (con_1,)
bound = (10, media_budget) # spend for a channel can't be negative or higher than budget
bounds = tuple(bound for x in range(5))
# run the SciPy Optimizer
solution = sco.minimize(
model_function, x0=guesses, args=args,
method='SLSQP',
jac=model_function_jac,
constraints=constraints,
bounds=bounds
)
# print out the solution
print(solution)
print(f"Spend: ${round(float(media_budget),2)}\n")
print(f"Optimized CPA: ${round(media_budget/(-1 * solution.fun),2)}")
print("Allocation:")
for i in range(len(media_labels)):
print(f"-{media_labels[i]}: ${round(solution.x[i],2)} ({round(solution.x[i]/media_budget*100,2)}%)")
This solution at least "works" in the sense that it reports a successful solve and returns an answer different from the initial guess.

How to solve overshoot by tuning parameters with gekko?

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.

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