I'm optimizing a tubular column design using gekko python. I experimented with the code using the different variable types m.SV and m.CV in place of m.Var and there was no apparent effect on the solver or the results. What purpose do these different variable types serve?
I've included my model below.
m = GEKKO()
#%% Constants
pi = m.Const(3.14159,'pi')
P = 2300 # compressive load (kg_f)
o_y = 450 # yield stress (kg_f/cm^2)
E = 0.65e6 # elasticity (kg_f/cm^2)
p = 0.0020 # weight density (kg_f/cm^3)
l = 300 # length of the column (cm)
#%% Variables
d = m.CV(value=8.0,lb=2.0,ub=14.0) # mean diameter (cm)
t = m.SV(value=0.3,lb=0.2,ub=0.8) # thickness (cm)
cost = m.Var()
#%% Intermediates
d_i = m.Intermediate(d - t)
d_o = m.Intermediate(d + t)
W = m.Intermediate(p*l*pi*(d_o**2 - d_i**2)/4) # weight (kgf)
o_i = m.Intermediate(P/(pi*d*t)) # induced stress
# second moment of area of the cross section of the column
I = m.Intermediate((pi/64)*(d_o**4 - d_i**4))
# buckling stress (Euler buckling load/cross-sectional area)
o_b = m.Intermediate((pi**2*E*I/l**2)*(1/(pi*d*t)))
#%% Equations
m.Equations([
o_i - o_y <= 0,
o_i - o_b <= 0,
cost == 5*W + 2*d
])
#%% Objective
m.Obj(cost)
#%% Solve and print solution
m.options.SOLVER = 1
m.solve()
print('Optimal cost: ' + str(cost[0]))
print('Optimal mean diameter: ' + str(d[0]))
print('Optimal thickness: ' + str(t[0]))
Variables
Variables are values that are adjusted by the solver to satisfy an equation or determine the best outcome among many options. There is typically at least one variable for every equation. To avoid over-specification, a simulation often has equal numbers of equations and variables. For optimization problems, there are typically more variables than equations. The extra variables are changed to minimize or maximize an objective function. There is more information on these objects in the Gekko documentation and APMonitor documentation.
x = m.Var(5) # declare a variable with initial condition
There are also "special" types of variables that perform certain functions. For example, additional equations are added to the model for variables that have a measurement for data reconciliation. To avoid adding these extra equations for all variables, the measurement equations are only added for those designated as Controlled Variables (CVs). State Variables (SVs) may also be measured are typically designated as such just for monitoring purposes.
State Variables (SVs)
States are model variables that may be measured or are of special interest for observation. For time-varying simulations, the SVs change over the time horizon to satisfy equation feasibility.
x = m.SV() # state variable
Controlled Variables (CVs)
Controlled variables are model variables that are included in the objective of a controller or optimizer. These variables are controlled to a range, maximized, or minimized. Controlled variables may also be measured values that are included for data reconciliation. For time-varying simulations, the CVs change over the time horizon to satisfy the equations and minimize the objective function.
x = m.CV() # controlled variable
Example Application
There is documentation for options for the different variable and parameter types (FV, MV, SV, CV). Below is a Model Predictive Control Application that shows the use of a Manipulated Variable and Controlled Variable.
from gekko import GEKKO
import numpy as np
import matplotlib.pyplot as plt
m = GEKKO()
m.time = np.linspace(0,20,41)
# Parameters
mass = 500
b = m.Param(value=50)
K = m.Param(value=0.8)
# Manipulated variable
p = m.MV(value=0, lb=0, ub=100)
p.STATUS = 1 # allow optimizer to change
p.DCOST = 0.1 # smooth out gas pedal movement
p.DMAX = 20 # slow down change of gas pedal
# Controlled Variable
v = m.CV(value=0)
v.STATUS = 1 # add the SP to the objective
m.options.CV_TYPE = 2 # squared error
v.SP = 40 # set point
v.TR_INIT = 1 # set point trajectory
v.TAU = 5 # time constant of trajectory
# Process model
m.Equation(mass*v.dt() == -v*b + K*b*p)
m.options.IMODE = 6 # control
m.solve(disp=False)
# get additional solution information
import json
with open(m.path+'//results.json') as f:
results = json.load(f)
plt.figure()
plt.subplot(2,1,1)
plt.plot(m.time,p.value,'b-',label='MV Optimized')
plt.legend()
plt.ylabel('Input')
plt.subplot(2,1,2)
plt.plot(m.time,results['v1.tr'],'k-',label='Reference Trajectory')
plt.plot(m.time,v.value,'r--',label='CV Response')
plt.ylabel('Output')
plt.xlabel('Time')
plt.legend(loc='best')
plt.show()
Related
Objective: To add boundary/initial conditions (BCs/ICs) to a system of ODEs
I have used the method of lines to convert a system of PDEs into a system of ODEs. The ODEs themselves involve a lot of variables so I will present simple versions here
For the purpose of this simulation all I am interested in is the structure of the code to set up the BCs/ICs, so I have removed any correlations etc and just put constants
For reference, the actual system is the flow of a gas through a packed bed. For terminology, I am calling the initial conditions the constant conditions along the bed initially (the bed temperature, the fluid temperature, the density of the fluid in the bed) and the boundary conditions would be the mass flow into the bed, the temperature of the flow into the bed.
I have two functions. df, which is used to set up the non IC/BC ODEs (generally happy with the structure of this one):
def df(t,x,*constants):
##the time derivative of x
a,b,c,d = constants ## unpack constants
##setting up solution array
x = x.reshape(2,number_of_nodes)
dxdt = np.zeros_like(x)
## internal nodes
for j in range(2,2*n,2): ##start at index 2 (to avoid ICs/BCs), go to 2*n and increment by 2
##equation 1
dxdt[0][j] = a*(x[1][j-1] - b*x[1][j])
## equation 2
dxdt[1][j] = c*(x[1][j-1] - d*x[1][j])
return dxdt.reshape(-1)
And initialize_system, which is supposed to set up the BCs/ICs (which is currently incorrect):
def initialize_system(*constants):
#sets the appropriate initial/boundary conditions
a,b,c,d = constants #unpack constants
x0 = np.zeros((2, number_of_nodes))
y01 = 1
y02 = 1
#set up initial boundary values
# eq 1
x0[0][0] = a*(y01 - b*x0[1][0])
# eq 2
x0[1][0] = c * (y02 - d*x0[1][0])
for j in range(2, 2*n, 2):
# eq 1
x0[0][j] = a*(y0CO2 - b*x0[1][j])
# eq 2
x0[1][j] = b*(y0H2O - d*x0[1][j])
return x0.reshape(-1)
My question is:
How can I correctly set up the ICs/BCs in initialize_system here?
Edit: There are very limited examples of implementing the method of lines in python on the internet. I would also highly appreciate any resources on this
I've been learning python and trying to get a handle on GEKKO by modelling a simple dynamic system where there is a storage vessel with an inlet and outlet mass flow. I am trying to optimize for minimum required storage given time-series inlet flow data, and a controlled outlet flowrate.
The inlet flowrate is defined on an hourly basis as parameter feed_rate, while the outlet flowrate is set via the control variable outlet_frac_flow which gives a fractional output, multiplied by fixed flowrate outlet_rate_ref, to maintain the mass within the storage vessel stored_mass between defined upper and lower bounds.
I have got the model to work fine if I fix the minimum and maximum bounds for stored_mass as constant values, with GEKKO solving over the time series to adjust the outlet rate. What I am trying to do now is add a layer of optimization to minimize the maximum storage volume required via new FV max_storage.
Ideally, I would like to use this FV as the upper bound of MV stored_mass, but it does not seem to be working.
When I run the below code, I get the output that Max storage: 73.94 tonnes vs limit of 60.0 tonnes. So the solver is minimizing the 'max_storage' variable, but it does not seem to be carried through into the upper bound set for stored_mass.
I thought maybe I had to use an Intermediate variable to pass max_storage into stored_mass as upper bound but when I tried that then I couldn't even get the model to solve.
Can anyone advise if what I am trying to do is even feasible for the solver that GEKKO uses? And if so where I might be going wrong?
Much appreciated!
from gekko import GEKKO
import matplotlib.pyplot as plt
import numpy as np
# Initialise GEKKO model
m = GEKKO(remote=False)
# set initial, minimum and maximum tank storage (tonnes)
min_storage_init = 30
max_storage_init = 120
# initial storage set at 10 tonnes above minimum
init_storage = min_storage_init + 10
# set reference outlet flowrate (to be adjusted by solver)
outlet_rate_ref = 20
# set hourly feed rate into tank in tonnes/hr
feed_rate_input = [4.2, 11.2, 10.2, 5.64, 0.0, 16.92, 18.31, 12.67, 18.31, 18.49, 18.49, 18.49, 18.49, 18.49, 17.95, 15.13, 12.25, 6.61, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0]
# set time
m.time = np.linspace(0,len(feed_rate_input)-1,len(feed_rate_input))
# input feed_rate into model as parameter
feed_rate = m.Param(value=feed_rate_input, name = 'feed rate')
# add maximum storage as fixed variable
max_storage = m.FV(value = max_storage_init, lb = min_storage_init*2, ub = max_storage_init)
max_storage.STATUS = 1
# add outlet_frac_flow as manipulated variable, to adjust the fractional reference outlet rate
outlet_frac_flow = m.MV(value=0.6, lb = 0.3, ub = 1)
outlet_frac_flow.STATUS = 1
outlet_frac_flow.DCOST = 1 # give some cost to changes in rate
outlet_frac_flow.DMAX = 0.3 # max of 30% change per step
outlet_frac_flow.MV_STEP_HOR = 5 # max of 3 changes in any 24hr period
# add stored mass as controlled variable
# initial value = init_storage, upper bound = fixed variable max_storage
stored_mass = m.CV(value=init_storage, lb = min_storage_init, ub = max_storage, name = 'mass stored')
stored_mass.STATUS = 1
# set high setpoint to be 95% of fixed variable max_storage
stored_mass.SPHI = max_storage *0.95
# set low setpoint to be 5% higher than min_storage_init
stored_mass.SPLO = min_storage_init / 0.95
# define mass balance differential
m.Equation(stored_mass.dt() == feed_rate - outlet_frac_flow * outlet_rate_ref)
# try to minimise max_storage
m.Minimize(max_storage)
#solver options
m.options.CV_TYPE=1
m.options.IMODE = 6
m.options.NODES = 3
m.options.SOLVER = 3
#solve model
m.solve(disp=False)
flowrate=[i for i in outlet_frac_flow]
for i in range (0,len(feed_rate_input)):
flowrate[i]=outlet_frac_flow[i] * outlet_rate_ref
print('Max storage: ' + str(round(max(stored_mass),2)) + ' tonnes vs limit of ' + str(round(max_storage[0],2)) + ' tonnes')
#Plot graphs
#plt.figure(1) # plot results
#plt.set_figheight(13)
#plt.set_figwidth(18)
plt.figure(figsize=(16, 8))
plt.subplot(2,1,1)
plt.plot(m.time ,feed_rate.value,'r-',label='Feed rate (te/hr)')
plt.plot(m.time ,stored_mass.value,'b-',label='Mass stored (te)')
plt.legend()
plt.xticks(np.arange(min(m.time), max(m.time)+1, 1))
plt.subplot(2,1,2)
plt.plot(m.time,flowrate,'k--',label='outlet flowrate (te/hr)')
plt.xlabel('Time (hours)')
plt.xticks(np.arange(min(m.time), max(m.time)+1, 1))
plt.legend()
plt.show()
Add an inequality equation to change the max_storage to be greater than the stored_mass value:
m.Equation(stored_mass<=max_storage)
The initialization of the CV upper bound or the stored_mass.SPHI only use the values of max_storage at initialization. They do not update when max_storage is updated by the solver. To include the constraint, add it as an inequality constraint.
If you need the SPHI to also use the updated value of 0.95*max_storage then define a new CV such as ratio = m.CV() with ratio.STATUS=1, ratio.SPHI=0.95, and m.Equation(ratio*max_storage==stored_mass). By not writing the equation as ratio==stored_mass/max_storage, it avoids a potential divide-by-zero problem with the solver.
TL;DR I've been implementing a python program to solve numerically equations for natural convection based on a particular similarity variable using runge-kutta 4 and the shooting method. However I don't get the right solutions when I plot it. Did I make a mistake somewhere ?
Hi !
Starting from a special case of natural convection, we get these similitude equations.
The first describe the fluid flow, the second describe the heat flow.
"Pr" is for Prandtl it's basically a dimensionless number used in fluid dynamics (Prandtl) :
These equations are subjects to the following boundary values such that the temperature near the plate is greater than the temperature outside the boundary layer and such that the fluid velocity is 0 far away from the boundary layer.
I've been trying to resolve these numerically with Runge-Kutta 4 and the shooting method to transform the boundary value problem into an initial value problem. The way the shooting method is implemented is with the newton method.
However, I don't get the right solutions.
As you can see in the following, the temperature (in red) is increasing as we are moving away from the plate whereas it should decrease exponentially.
It's more consistent for the fluid velocity (in blue), however the speed i think it should go up faster then go down faster. Here the curve is smoother.
Now, the fact is that we have a system of 2 coupled ODE. However, right now, I'm only trying to find the one of the two initials values (e.g. f''(0) = a, trying to find a) such that we have a solution to the boundary value problem (shooting method). Once found, I suppose we have the solution for the whole problem.
I guess I should maybe manage the two (f''(0) = a ; theta'(0) = b) but I don't know how to manage these two in parallel.
Last think to mention, if I try to get the initial value of theta' (so theta'(0)) I don't get the right heat profile.
Here is the code :
"""
The goal is to resolve a 3rd order non-linear ODE for the blasius problem.
It's made of 2 equations (flow / heat)
f''' = 3ff'' - 2(f')^2 + theta
3 Pr f theta' + theta'' = 0
RK4 + Shooting Method
"""
import numpy as np
import math
from scipy.integrate import odeint
from scipy.optimize import newton
from edo_solver.plot import plot
from constants import PRECISION
def blasius_edo(y, t, prandtl):
f = y[0:3]
theta = y[3:5]
return np.array([
# flow edo
f[1], # f' = df/dn
f[2], # f'' = d^2f/dn^2
- 3 * f[0] * f[2] + (2 * math.pow(f[1], 2)) - theta[0], # f''' = - 3ff'' + 2(f')^2 - theta,
# heat edo
theta[1], # theta' = dtheta/dn
- 3 * prandtl * f[0] * theta[1], # theta'' = - 3 Pr f theta'
])
def rk4(eta_range, shoot):
prandtl = 0.01
# initial values
f_init = [0, 0, shoot] # f(0), f'(0), f''(0)
theta_init = [1, shoot] # theta(0), theta'(0)
ci = f_init + theta_init # concatenate two ci
# note: tuple with single argument must have "," at the end of the tuple
return odeint(func=blasius_edo, y0=ci, t=eta_range, args=(prandtl,))
"""
if we have :
f'(t_0) = fprime_t0 ; f'(eta -> infty) = fprime_inf
we can transform it into :
f'(t_0) = fprime_t0 ; f''(t_0) = a
we define the function F(a) = f'(infty ; a) - fprime_inf
if F(a) has a root in "a",
then the solutions to the initial value problem with f''(t_0) = a
is also the solution the boundary problem with f'(eta -> infty) = fprime_inf
our goal is to find the root, we have the root...we have the solution.
it can be done with bissection method or newton method.
"""
def shooting(eta_range):
# boundary value
fprimeinf = 0 # f'(eta -> infty) = 0
# initial guess
# as far as I understand
# it has to be the good guess
# otherwise the result can be completely wrong
initial_guess = 10 # guess for f''(0)
# define our function to optimize
# our goal is to take big eta because eta should approach infty
# [-1, 1] : last row, second column => f'(eta_final) ~ f'(eta -> infty)
fun = lambda initial_guess: rk4(eta_range, initial_guess)[-1, 1] - fprimeinf
# newton method resolve the ODE system until eta_final
# then adjust the shoot and resolve again until we have a correct shoot
shoot = newton(func=fun, x0=initial_guess)
# resolve our system of ODE with the good "a"
y = rk4(eta_range, shoot)
return y
def compute_blasius_edo(title, eta_final):
ETA_0 = 0
ETA_INTERVAL = 0.1
ETA_FINAL = eta_final
# default values
title = title
x_label = "$\eta$"
y_label = "profil de vitesse $(f'(\eta))$ / profil de température $(\\theta)$"
legends = ["$f'(\eta)$", "$\\theta$"]
eta_range = np.arange(ETA_0, ETA_FINAL + ETA_INTERVAL, ETA_INTERVAL)
# shoot
y_set = shooting(eta_range)
plot(eta_range, y_set, title, legends, x_label, y_label)
compute_blasius_edo(
title="Convection naturelle - Solution de similitude",
eta_final=10
)
I could be completely off base here, but I wrote something similar to solve 1D fluid-reaction-heat equations. Try using solve_ivp and using the RADAU solver method, it helps with more difficult systems.
Also maybe try converting your system of ODES to a system of first order ODEs as that may help.
You are implementing the additional but wrong boundary condition f''(0) = theta'(0), as both slots get the same initial value in the shooting method. You need to hold them separate, giving 2 free variables and thus the need for a 2-dimensional Newton method or any other solver for non-scalar functions.
You could just as well use the solve_bvp routine with a sensible initial guess.
This question is focussed somewhat on economic optimisation, and somewhat on python implementation, but maybe some in the community are able to help. I'm trying to implement a standard continuous-time macroeconomic savings model in Python's GEKKO platform, but haven't been able to get it to solve. I've taken the economic example provided in GEKKO's documentation, and adapted to the basic savings decision model, but things are not quite working out. The model maximises the sum of utility from consumption, where consumption + investment = output. E.g. max integral(U(y-i)). Output y = k^ALPHA. investment = dk/dt+delta*k.
Can anyone tell why my code can't be solved? Is the platform even capable of solving such a model? I haven't seen many examples of economists using this platform to solve models, but not sure if this is because the platform is not suited or otherwise. It's a great platform and really keen to make it work if possible. Thank you in advance.
from gekko import GEKKO
import numpy as np
import matplotlib.pyplot as plt
m = GEKKO()
n=501
m.time = np.linspace(0,10,n)
ALPHA,DELTA = 0.333,0.99
i = m.MV(value=0)
i.STATUS = 1
i.DCOST = 0
x = m.Var(value=20,lb=0) # fish population
m.Equation(x.dt() == i-DELTA*x)
J = m.Var(value=0) # objective (profit)
Jf = m.FV() # final objective
Jf.STATUS = 1
m.Connection(Jf,J,pos2='end')
m.Equation(J.dt() == m.log(x**ALPHA-i))
m.Obj(-Jf) # maximize profit
m.options.IMODE = 6 # optimal control
m.options.NODES = 3 # collocation nodes
m.options.SOLVER = 3 # solver (IPOPT)
m.solve(disp=True) # Solve
You are getting NaN in the equation dJ/dt = ln(x**ALPHA-i). When you include bounds i>0 and i<1, the solver finds a solution.
from gekko import GEKKO
import numpy as np
import matplotlib.pyplot as plt
m = GEKKO()
n=501
m.time = np.linspace(0,10,n)
ALPHA,DELTA = 0.333,0.99
i = m.MV(value=0,lb=0,ub=1)
i.STATUS = 1
i.DCOST = 0
x = m.Var(value=20,lb=0) # fish population
m.Equation(x.dt() == i-DELTA*x)
J = m.Var(value=0) # objective (profit)
Jf = m.FV() # final objective
Jf.STATUS = 1
m.Connection(Jf,J,pos2='end')
m.Equation(J.dt() == m.log(x**ALPHA-i))
m.Obj(-Jf) # maximize profit
m.options.IMODE = 6 # optimal control
m.options.NODES = 3 # collocation nodes
m.options.SOLVER = 3 # solver (IPOPT)
m.solve(disp=True) # Solve
plt.subplot(2,1,1)
plt.plot(m.time,x.value)
plt.ylabel('x')
plt.subplot(2,1,2)
plt.plot(m.time,i.value)
plt.ylabel('i')
plt.show()
Instead of m.Obj() (minimize) you can use the newer functions m.Minimize() or m.Maximize() to clarify the objective function intent. For example, you could switch to m.Maximize(Jf) to make it more readable.
There are also a couple other examples that may help you with integral objectives (see solution 2) and economic dynamic optimization.
I'm using a system of ode's to model coffee bean roasting for a class assignment. The equations are below.
The parameters (other than X_b and T_b) are all constants.
When I try to use odeint to solve this system, it gives a constant T_b and X_b profile (which conceptually doesn't make sense).
Below is the code I'm using
from scipy.integrate import odeint
import numpy as np
import matplotlib.pyplot as plt
# Write function for bean temperature T_b differential equation
def deriv(X,t):
T_b, X_b = X
dX_b = (-4.32*10**9*X_b**2)/(l_b**2)*np.exp(-9889/T_b)
dT_b = ((h_gb*A_gb*(T_gi - T_b))+(m_b*A_arh*np.exp(-H_a/R_g/T_b))+
(m_b*lam*dX_b))/(m_b*(1.099+0.0070*T_b+5*X_b)*1000)
return [dT_b, dX_b]
# Establish initial conditions
t = 0 #seconds
T_b = 298 # degrees K
X_b = 0.1 # mass fraction of moisture
# Set time step
dt = 1 # second
# Establish location to store data
history = [[t,T_b, X_b]]
# Use odeint to solve DE
while t < 600:
T_b, X_b = odeint(deriv, [T_b, X_b], [t+dt])[-1]
t += dt
history.append([t,T_b, X_b])
# Plot Results
def plot_history(history, labels):
"""Plots a simulation history."""
history = np.array(history)
t = history[:,0]
n = len(labels) - 1
plt.figure(figsize=(8,1.95*n))
for k in range(0,n):
plt.subplot(n, 1, k+1)
plt.plot(t, history[:,k+1])
plt.title(labels[k+1])
plt.xlabel(labels[0])
plt.grid()
plt.tight_layout()
plot_history(history, ['t (s)','Bean Temperature $T_b$ (K)', 'Bean Moisture Content $X_b$'])
plt.show()
Do you have any ideas why the integration step isn't working?
Thank You!!
You're repeatedly solving the system of equations for only a single timepoint.
From the odeint documentation, the odeint command takes an argument t which is:
A sequence of time points for which to solve for y. The initial value point should be the first element of this sequence.
Since you pass [t+dt] to odeint, there is only a single timepoint so you get back only a single value which is simply your initial condition.
The correct way to use odeint is similar to the following:
output = odeint(deriv, [T_b, X_b], np.linspace(0,600,600))
Here output, again according to the documentation is:
Array containing the value of y for each desired time in t, with the initial value y0 in the first row.