This is a followup to my previous question here
I have the following dynamical system,
$\frac{d \phi}{dt} = -M^TDM\phi \tag{1}\label{1}$
$\frac{d \hat\phi}{dt} = -M^T\tilde{D}M\hat \phi \tag{2} \label{2}$
$\eqref{1}$ represents the exact dynamics of a system and $\eqref{2}$ is the approximate dynamics that should give the same time course profiles as $\eqref{1}$, after optimization. Ideally, I am solving for the dynamics of the same system in $\eqref{1}$ and $\eqref{2}$. $\eqref{2}$ is more like a perturbed version of $\eqref{1}$. The perturbation is done by setting $\hat{D}$ = D/10. And for the sake of understanding, let us assume $\eqref{1}$ gives experimental values and $\eqref{2}$ are the predicted values.
I've set up this system in GEKKO
# Copyright 2020, Natasha, All rights reserved.
import numpy as np
from gekko import GEKKO
from pprint import pprint
import matplotlib.pyplot as plt
from scipy.integrate import odeint
def get_mmt():
"""
M and M transpose required for differential equations
:params: None
:return: M transpose and M -- 2D arrays ~ matrices
"""
MT = np.array([[-1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, -1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, -1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, -1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, -1, 0, 0],
[0, 0, 0, 0, 0, 0, 1, -1, 0],
[0, 0, 0, 0, 0, 0, 0, 1, -1],
[0, 0, 0, 0, 0, 0, 0, 0, 1]])
M = np.transpose(MT)
return M, MT
def actual(phi, t):
"""
Actual system/ Experimental measures
:param phi: 1D array
:return: time course of variable phi -- 2D arrays ~ matrices
"""
# spatial nodes
ngrid = 10
end = -1
M, MT = get_mmt()
D = 5000*np.ones(ngrid-1)
A = [email protected](D)@M
A = A[1:ngrid-1]
# differential equations
dphi = np.zeros(ngrid)
# first node
dphi[0] = 0
# interior nodes
dphi[1:end] = -A@phi # value at interior nodes
# terminal node
dphi[end] = D[end]*2*(phi[end-1] - phi[end])
return dphi
if __name__ == '__main__':
# ref: https://apmonitor.com/do/index.php/Main/PartialDifferentialEquations
ngrid = 10 # spatial discretization
end = -1
# integrator settings (for ode solver)
tf = 0.5
nt = int(tf / 0.01) + 1
tm = np.linspace(0, tf, nt)
# ------------------------------------------------------------------------------------------------------------------
# measurements
# ref: https://www.youtube.com/watch?v=xOzjeBaNfgo
# using odeint to solve the differential equations of the actual system
# ------------------------------------------------------------------------------------------------------------------
phi_0 = np.array([5, 0, 0, 0, 0, 0, 0, 0, 0, 0])
phi = odeint(actual, phi_0, tm)
# plot results
plt.figure()
plt.plot(tm*60, phi[:, :])
plt.ylabel('phi')
plt.xlabel('Time (s)')
plt.show()
# ------------------------------------------------------------------------------------------------------------------
# GEKKO model
# ------------------------------------------------------------------------------------------------------------------
m = GEKKO(remote=False)
m.time = tm
# ------------------------------------------------------------------------------------------------------------------
# initialize state variables: phi_hat
# ref: https://apmonitor.com/do/uploads/Main/estimate_hiv.zip
# ------------------------------------------------------------------------------------------------------------------
phi_hat = [m.CV(value=phi_0[i]) for i in range(ngrid)] # initialize phi_hat; variable to match with measurement
# ------------------------------------------------------------------------------------------------------------------
# parameters (/control parameters to be optimized while minimizing the cost function in GEKKO)
# ref: http://apmonitor.com/do/index.php/Main/DynamicEstimation
# ref: https://apmonitor.com/do/index.php/Main/EstimatorObjective
# def model
# ------------------------------------------------------------------------------------------------------------------
# Manually enter guesses for parameters
Dhat0 = 5000*np.ones(ngrid-1)
Dhat = [m.MV(value=Dhat0[i]) for i in range(0, ngrid-1)]
for i in range(ngrid-1):
Dhat[i].STATUS = 1 # Allow optimizer to fit these values
# Dhat[i].LOWER = 0
# ------------------------------------------------------------------------------------------------------------------
# differential equations
# ------------------------------------------------------------------------------------------------------------------
M, MT = get_mmt()
A = MT @ np.diag(Dhat) @ M
A = A[1:ngrid - 1]
# first node
m.Equation(phi_hat[0].dt() == 0)
# interior nodes
int_value = -A @ phi_hat # function value at interior nodes
m.Equations(phi_hat[i].dt() == int_value[i] for i in range(0, ngrid-2))
# terminal node
m.Equation(phi_hat[ngrid-1].dt() == Dhat[end] * 2 * (phi_hat[end-1] - phi_hat[end]))
# ------------------------------------------------------------------------------------------------------------------
# simulation
# ------------------------------------------------------------------------------------------------------------------
m.options.IMODE = 5 # simultaneous dynamic estimation
m.options.NODES = 3 # collocation nodes
m.options.EV_TYPE = 2 # squared-error :minimize model prediction to measurement
for i in range(ngrid):
phi_hat[i].FSTATUS = 1 # fit to measurement phi obtained from 'def actual'
phi_hat[i].STATUS = 1 # build objective function to match measurement and prediction
phi_hat[i].value = phi[:, i]
m.solve()
pprint(Dhat)
RESULT:
Dhat
is the parameter vector that is returned by the solver. Dhat
is fit by the optimizer to minimize the error between measured and predicted values of state variables.
To check how the solver performs, I set $\tilde{D}$ (in equation 2, model system) = $D$ (in equation 1, actual system) for preliminary tests. This would imply, equation 1 is equal to equation 2 (no perturbation); error in objective will be zero; the output of $\tilde{D}$ returned by the solver will be expected to be equal to input $D$, in equation 1.
However, the output Dhat
returned by the solver is equal to D only when Dhat
is initialized as a manipulated variable (m.MV) in the code and not as a fixed variable (m.FV).
When,
Dhat = [m.MV(value=Dhat0[i]) for i in range(0, ngrid-1)]
Output at last time point:
4965.7481122
4969.8889601
4977.2097334
4991.4733925
5003.2160715
5008.6109002
5008.2076146
5004.688907
5000.8233427
Objective : 2.377072623938945
When,
Dhat = [m.FV(value=Dhat0[i]) for i in range(0, ngrid-1)]
Output at all time points:
3841.8094003
4198.623965
5319.3033474
6065.5329592
6467.5482342
6703.7146752
6859.9707626
9454.6282272
5098.1687634
Objective : 0.3068466339064452
I am not sure why there is a difference in solution returned for these settings and why the solver doesn't return $\tilde{D} = D$ (initial values set for Dhat in the code Dhat0 = 5000*np.ones(ngrid-1)
) when equation1 = equation 2.
Any explanations will be really helpful.
EDIT : I'd also like to understand the role of collocation time points in solving this optimal control problem
I changed the number of time points, nt
from 51 nt = int(tf / 0.01) + 1
to 501 nt = int(tf / 0.001)
+ 1 and the solution was not found. Here, I am trying to check whether increasing nt
will return all 5000 while using m.FV
Objective function at the last iteration where convergence failed
iter objective inf_pr inf_du lg(mu) ||d|| lg(rg) alpha_du alpha_pr ls
60 6.8984929e+002 8.58e+002 2.45e+013 -11.0 2.77e+002 5.6 1.00e+000 5.00e-001h 2
WARNING: Problem in step computation; switching to emergency mode.
63r7.3217465e+002 1.86e+002 9.99e+002 0.3 0.00e+000 6.0 0.00e+000 0.00e+000R 1
MUMPS returned INFO(1) =-13 - out of memory when trying to allocate 128655080 bytes.
In some cases it helps to decrease the value of the option "mumps_mem_percent".
WARNING: Problem in step computation; switching to emergency mode.
Restoration phase is called at point that is almost feasible,
with constraint violation 0.000000e+000. Abort.
Restoration phase in the restoration phase failed.
From what's reported here, I understand mumps_mem_percent
is associated with IPOPT solver, but I am not sure how to change in the settings. I'd like to know how to increase mumps_mem_percent
in GEKKO.
EDIT 2:
From what has been explained below, I tried to check the in solutions generated by integration solvers used in GEKKO
and scipy's odeint
I could observe that at initial time steps solution generated by using the integration solver in GEKKO yields negative values. Would it help if the relative/ absolute tolerance is decreased? I'm not sure of the default values used here. In the documentation available here rtol and atol is = 1.49012e-8 by default for scipy's odeint.
EDIT3: After changing rtol and otol as suggested below, GEKKO still returns negative values at the initial time steps. The following code is used to solve and compare just the differential equations in odeint and GEKKO. Please note: m.options.NODES = 3 is not used to solve and compare just the odes.
import numpy as np
from gekko import GEKKO
from pprint import pprint
import matplotlib.pyplot as plt
from scipy.integrate import odeint
def get_mmt():
"""
M and M transpose required for differential equations
:params: None
:return: M transpose and M -- 2D arrays ~ matrices
"""
# M^T
MT = np.array([[-1, 0, 0, 0, 0, 0, 0, 0, 0],
[1, -1, 0, 0, 0, 0, 0, 0, 0],
[0, 1, -1, 0, 0, 0, 0, 0, 0],
[0, 0, 1, -1, 0, 0, 0, 0, 0],
[0, 0, 0, 1, -1, 0, 0, 0, 0],
[0, 0, 0, 0, 1, -1, 0, 0, 0],
[0, 0, 0, 0, 0, 1, -1, 0, 0],
[0, 0, 0, 0, 0, 0, 1, -1, 0],
[0, 0, 0, 0, 0, 0, 0, 1, -1],
[0, 0, 0, 0, 0, 0, 0, 0, 1]])
M = np.transpose(MT)
return M, MT
def actual(phi, t):
"""
Actual system/ Experimental measures
:param phi: 1D array
:return: time course of variable phi -- 2D arrays ~ matrices
"""
# spatial nodes
ngrid = 10
end = -1
M, MT = get_mmt()
D = 5000*np.ones(ngrid-1)
A = [email protected](D)@M
A = A[1:ngrid-1]
# differential equations
dphi = np.zeros(ngrid)
# first node
dphi[0] = 0
# interior nodes
dphi[1:end] = -A@phi # value at interior nodes
# terminal node
dphi[end] = D[end]*2*(phi[end-1] - phi[end])
return dphi
if __name__ == '__main__':
# ref: https://apmonitor.com/do/index.php/Main/PartialDifferentialEquations
ngrid = 10 # spatial discretization
end = -1
# integrator settings (for ode solver)
tf = 0.05
nt = int(tf / 0.0001) + 1
tm = np.linspace(0, tf, nt)
# ------------------------------------------------------------------------------------------------------------------
# measurements
# ref: https://www.youtube.com/watch?v=xOzjeBaNfgo
# using odeint to solve the differential equations of the actual system
# ------------------------------------------------------------------------------------------------------------------
phi_0 = np.array([5, 0, 0, 0, 0, 0, 0, 0, 0, 0])
phi = odeint(actual, phi_0, tm)
# ------------------------------------------------------------------------------------------------------------------
# GEKKO model
# ------------------------------------------------------------------------------------------------------------------
m = GEKKO(remote=False)
m.time = tm
# ------------------------------------------------------------------------------------------------------------------
# initialize phi_hat
# ------------------------------------------------------------------------------------------------------------------
phi_hat = [m.Var(value=phi_0[i]) for i in range(ngrid)]
# ------------------------------------------------------------------------------------------------------------------
# state variables
# ------------------------------------------------------------------------------------------------------------------
#phi_hat = m.CV(value=phi)
#phi_hat.FSTATUS = 1 # fit to measurement phi obtained from 'def actual'
# ------------------------------------------------------------------------------------------------------------------
# parameters (/control variables to be optimized by GEKKO)
# ref: http://apmonitor.com/do/index.php/Main/DynamicEstimation
# def model
# ------------------------------------------------------------------------------------------------------------------
Dhat0 = 5000*np.ones(ngrid-1)
Dhat = [m.FV(value=Dhat0[i]) for i in range(0, ngrid-1)]
# Dhat.STATUS = 1 # adjustable parameter
# ------------------------------------------------------------------------------------------------------------------
# differential equations
# ------------------------------------------------------------------------------------------------------------------
M, MT = get_mmt()
A = MT @ np.diag(Dhat) @ M
A = A[1:ngrid - 1]
# first node
m.Equation(phi_hat[0].dt() == 0)
# interior nodes
int_value = -A @ phi_hat # function value at interior nodes
pprint(int_value.shape)
m.Equations(phi_hat[i].dt() == int_value[i] for i in range(0, ngrid-2))
# terminal node
m.Equation(phi_hat[ngrid-1].dt() == Dhat[end] * 2 * (phi_hat[end-1] - phi_hat[end]))
# ------------------------------------------------------------------------------------------------------------------
# objective
# ------------------------------------------------------------------------------------------------------------------
# f = sum((phi(:) - phi_tilde(:)).^2);(MATLAB)
# m.Minimize()
# ------------------------------------------------------------------------------------------------------------------
# simulation
# ------------------------------------------------------------------------------------------------------------------
m.options.IMODE = 4 # simultaneous dynamic estimation
#m.options.NODES = 3 # collocation nodes
#m.options.EV_TYPE = 2 # squared-error :minimize model prediction to measurement
m.options.RTOL = 1.0e-8
m.options.OTOL = 1.0e-8
m.solve()
"""
#------------------------------------------------------------------------------------------------------------------
plt.figure()
for i in range(0, ngrid):
plt.plot(tm*60, phi_hat[i].value, '--', label=f'gekko_{i}')
plt.plot(tm*60, phi[:, i], label=f'odeint_{i}')
plt.legend(loc="upper right")
plt.ylabel('phi/phi_hat')
plt.xlabel('Time (s)')
plt.xlim([0, 3])
plt.show()