#
# This file is part of do-mpc
#
# do-mpc: An environment for the easy, modular and efficient implementation of
# robust nonlinear model predictive control
#
# Copyright (c) 2014-2019 Sergio Lucia, Alexandru Tatulea-Codrean
# TU Dortmund. All rights reserved
#
# do-mpc is free software: you can redistribute it and/or modify
# it under the terms of the GNU Lesser General Public License as
# published by the Free Software Foundation, either version 3
# of the License, or (at your option) any later version.
#
# do-mpc is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU Lesser General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with do-mpc. If not, see <http://www.gnu.org/licenses/>.
import numpy as np
#import casadi as cas
import casadi.tools as castools
import pdb
from do_mpc.tools._casstructure import _SymVar
from typing import Union,Tuple
[docs]
class Model:
"""The **do-mpc** model class. This class holds the full model description and is at the core of
:py:class:`do_mpc.simulator.Simulator`, :py:class:`do_mpc.controller.MPC` and :py:class:`do_mpc.estimator.Estimator`.
The :py:class:`Model` class is created with setting the ``model_type`` (continuous or discrete).
A ``continous`` model consists of an underlying ordinary differential equation (ODE) or differential algebraic equation (DAE):
.. math::
\\dot{x}(t) &= f(x(t),u(t),z(t),p(t),p_{\\text{tv}}(t)) + w(t),\\\\
0 &= g(x(t),u(t),z(t),p(t),p_{\\text{tv}}(t))\\\\
y &= h(x(t),u(t),z(t),p(t),p_{\\text{tv}}(t)) + v(t)
whereas a ``discrete`` model consists of a difference equation:
.. math::
x_{k+1} &= f(x_k,u_k,z_k,p_k,p_{\\text{tv},k}) + w_k,\\\\
0 &= g(x_k,u_k,z_k,p_k,p_{\\text{tv},k})\\\\
y_k &= h(x_k,u_k,z_k,p_k,p_{\\text{tv},k}) + v_k
The **do-mpc** model can be initiated with either ``SX`` or ``MX`` variable type.
We refer to the CasADi documentation on the difference of these two types.
Note:
``SX`` vs. ``MX`` in a nutshell: In general use ``SX`` variables (default).
If your model consists of scalar operations ``SX`` variables will be beneficial.
Your implementation will most likely only benefit from ``MX`` variables if you use large(r)-scale matrix-vector multiplications.
Note:
The option ``symvar_type`` will be inherited to all derived classes (e.g. :py:class:`do_mpc.simulator.Simulator`,
:py:class:`do_mpc.controller.MPC` and :py:class:`do_mpc.estimator.Estimator`).
All symbolic variables in these classes will be chosen respectively.
**Configuration and setup:**
Configuring and setting up the :py:class:`Model` involves the following steps:
1. Use :py:func:`set_variable` to introduce new variables to the model.
2. Optionally introduce "auxiliary" expressions as functions of the previously defined variables with :py:func:`set_expression`. The expressions can be used for monitoring or be reused as constraints, the cost function etc.
3. Optionally introduce measurement equations with :py:func:`set_meas`. The syntax is identical to :py:func:`set_expression`. By default state-feedback is assumed.
4. Define the right-hand-side of the `discrete` or `continuous` model as a function of the previously defined variables with :py:func:`set_rhs`. This method must be called once for each introduced state.
5. Call :py:func:`setup` to finalize the :py:class:`Model`. No further changes are possible afterwards.
Note:
All introduced model variables are accessible as **Attributes** of the :py:class:`Model`.
Use these attributes to query to variables, e.g. to form the cost function in a seperate file for the MPC configuration.
Args:
model_type : Set if the model is ``discrete`` or ``continuous``.
symvar_type : Set if the model is configured with CasADi ``SX`` or ``MX`` variables.
Raises:
assertion: model_type must be string
assertion: model_type must be either discrete or continuous
"""
def __init__(self, model_type:str=None, symvar_type:str='SX'):
assert isinstance(model_type, str), 'model_type must be string, you have: {}'.format(type(model_type))
assert model_type in ['discrete', 'continuous'], 'model_type must be either discrete or continuous, you have: {}'.format(model_type)
assert symvar_type in ['SX', 'MX'], 'symvar_type must be either SX or MX, you have: {}'.format(symvar_type)
self.symvar_type = symvar_type
self.model_type = model_type
self.sv = _SymVar(symvar_type)
# Define private class attributes
self._x = {'name': [],'var':[]}
self._u = {'name': ['default'], 'var': [self.sv.sym('default', (0,0))]}
self._z = {'name': ['default'], 'var': [self.sv.sym('default', (0,0))]}
self._p = {'name': ['default'], 'var': [self.sv.sym('default', (0,0))]}
self._tvp = {'name': ['default'], 'var': [self.sv.sym('default', (0,0))]}
self._aux = {'name': ['default'], 'var': [self.sv.sym('default', (1,1))]}
# Process noise
self._w = {'name': ['default'], 'var': [self.sv.sym('default', (0,0))]}
# Measurement noise
self._v = {'name': ['default'], 'var': [self.sv.sym('default', (0,0))]}
# Measurements
self._y = {'name': ['default'], 'var': [self.sv.sym('default', (0,0))]}
# Expressions:
self._aux_expression = [castools.entry('default', expr=castools.DM(0))]
self._y_expression = []
self.rhs_list = []
self.alg_list = [castools.entry('default', expr=[])]
self.flags = {
'setup': False,
}
def __getstate__(self):
"""
Returns the state of the :py:class:`Model` for pickling.
Warnings:
The :py:class:`Model` class supports pickling only if:
1. The model is configured with ``SX`` variables.
2. The model is setup with :py:func:`setup`.
"""
# Raise exception if model is using MX symvars
if self.symvar_type == 'MX':
raise Exception('Pickling of models using MX symvars is not supported.')
# Raise exception if model is not setup
if not self.flags['setup']:
raise Exception('Pickling of unsetup models is not supported.')
state = self.__dict__.copy()
return state
def __setstate__(self, state):
"""
Sets the state of the :py:class:`Model` for unpickling. Please see :py:func:`__getstate__` for details and restrictions on pickling.
"""
self.__dict__.update(state)
# Update expressions with new symbolic variables created when unpickling:
self._rhs = self._rhs(self._rhs_fun(self._x, self._u, self._z, self._tvp, self._p, self._w))
self._alg = self._alg(self._alg_fun(self._x, self._u, self._z, self._tvp, self._p, self._w))
self._aux_expression = self._aux_expression(self._aux_expression_fun(self._x, self._u, self._z, self._tvp, self._p))
self._y_expression = self._y_expression(self._meas_fun(self._x, self._u, self._z, self._tvp, self._p, self._v))
[docs]
def __getitem__(self, ind):
"""The :py:class:`Model` class supports the ``__getitem__`` method,
which can be used to retrieve the model variables (see attribute list).
::
# Query the states like this:
x = model.x
# or like this:
x = model['x']
This also allows to retrieve multiple variables simultaneously:
::
x, u, z = model['x','u','z']
"""
var_names = ['x','u','z','p','tvp','y','aux', 'w']
if isinstance(ind, tuple):
val = []
for ind_i in ind:
assert ind_i in var_names, 'The queried variable {} is not valid. Choose from {}.'.format(ind_i, var_names)
val.append(getattr(self, ind_i))
return val
else:
val = getattr(self,ind)
return val
def _getvar(self, var_name):
""" Function is called from within all property (x, u, z, p, tvp, y, aux, w) getters.
Not part of the public API.
"""
if self.flags['setup']:
return getattr(self, var_name)
else:
# Before calling setup the attributes _x,_u,_z etc. are dicts with the keys: name and var.
sym_dict = getattr(self, var_name)
if var_name == '_aux_expression': # or possibly '_aux, depending on what is called in the getter
# create the required dict from what is currently a
sym_dict = {'name':[entry.name for entry in sym_dict],
'var': [entry.expr for entry in sym_dict]}
# We use the same method as in setup to create symbolic structures from these dicts
sym_struct = self._convert2struct(sym_dict)
# We then create a mutable structure of the same structure
struct = self.sv.struct(sym_struct)
# And set the values of this structure to the original symbolic variables
for key, var in zip(sym_dict['name'], sym_dict['var']):
struct[key] = var
# Indexing this structure returns the original symbolic variables
return struct
@property
def x(self):
""" Dynamic states.
CasADi symbolic structure, can be indexed with user-defined variable names.
Note:
Variables are introduced with :py:func:`Model.set_variable` Use this property only to query
variables.
**Example:**
::
model = do_mpc.model.Model('continuous')
model.set_variable('_x','temperature', shape=(4,1))
# Query:
model.x['temperature', 0] # 0th element of variable
model.x['temperature'] # all elements of variable
model.x['temperature', 0:2] # 0th and 1st element
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set model variables directly Use set_variable instead.
"""
return self._getvar('_x')
@x.setter
def x(self, val):
raise Exception('Cannot set model variables directly Use set_variable instead.')
@property
def u(self):
""" Inputs.
CasADi symbolic structure, can be indexed with user-defined variable names.
Note:
Variables are introduced with :py:func:`Model.set_variable` Use this property only to query
variables.
**Example:**
::
model = do_mpc.model.Model('continuous')
model.set_variable('_u','heating', shape=(4,1))
# Query:
model.u['heating', 0] # 0th element of variable
model.u['heating'] # all elements of variable
model.u['heating', 0:2] # 0th and 1st element
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set model variables directly Use set_variable instead.
"""
return self._getvar('_u')
@u.setter
def u(self, val):
raise Exception('Cannot set model variables directly Use set_variable instead.')
@property
def z(self):
""" Algebraic states.
CasADi symbolic structure, can be indexed with user-defined variable names.
Note:
Variables are introduced with :py:func:`Model.set_variable` Use this property only to query
variables.
**Example:**
::
model = do_mpc.model.Model('continuous')
model.set_variable('_z','temperature', shape=(4,1))
# Query:
model.z['temperature', 0] # 0th element of variable
model.z['temperature'] # all elements of variable
model.z['temperature', 0:2] # 0th and 1st element
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set model variables directly Use set_variable instead.
"""
return self._getvar('_z')
@z.setter
def z(self, val):
raise Exception('Cannot set model variables directly Use set_variable instead.')
@property
def p(self):
""" Static parameters.
CasADi symbolic structure, can be indexed with user-defined variable names.
Note:
Variables are introduced with :py:func:`Model.set_variable` Use this property only to query
variables.
**Example:**
::
model = do_mpc.model.Model('continuous')
model.set_variable('_p','temperature', shape=(4,1))
# Query:
model.p['temperature', 0] # 0th element of variable
model.p['temperature'] # all elements of variable
model.p['temperature', 0:2] # 0th and 1st element
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set model variables directly Use set_variable instead.
"""
return self._getvar('_p')
@p.setter
def p(self, val):
raise Exception('Cannot set model variables directly Use set_variable instead.')
@property
def tvp(self):
""" Time-varying parameters.
CasADi symbolic structure, can be indexed with user-defined variable names.
Note:
Variables are introduced with :py:func:`Model.set_variable` Use this property only to query
variables.
**Example:**
::
model = do_mpc.model.Model('continuous')
model.set_variable('_tvp','temperature', shape=(4,1))
# Query:
model.tvp['temperature', 0] # 0th element of variable
model.tvp['temperature'] # all elements of variable
model.tvp['temperature', 0:2] # 0th and 1st element
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set model variables directly Use set_variable instead.
"""
return self._getvar('_tvp')
@tvp.setter
def tvp(self, val):
raise Exception('Cannot set model variables directly Use set_variable instead.')
@property
def y(self):
""" Measurements.
CasADi symbolic structure, can be indexed with user-defined variable names.
Note:
Measured variables are introduced with :py:func:`Model.set_meas` Use this property only to query
variables.
**Example:**
::
model = do_mpc.model.Model('continuous')
model.set_variable('_x','temperature', 4) # 4 states
model.set_meas('temperature', model.x['temperature',:2]) # first 2 measured
# Query:
model.y['temperature', 0] # 0th element of variable
model.y['temperature'] # all elements of variable
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set model variables directly Use set_meas instead.
"""
return self._getvar('_y')
@y.setter
def y(self, val):
raise Exception('Cannot set model variables directly Use set_variable instead.')
@property
def aux(self):
""" Auxiliary expressions.
CasADi symbolic structure, can be indexed with user-defined variable names.
Note:
Expressions are introduced with :py:func:`Model.set_expression` Use this property only to query
variables.
**Example:**
::
model = do_mpc.model.Model('continuous')
model.set_variable('_x','temperature', 4) # 4 states
dt = model.x['temperature',0]- model.x['temperature', 1]
model.set_expression('dtemp', dt)
# Query:
model.aux['dtemp', 0] # 0th element of variable
model.aux['dtemp'] # all elements of variable
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set aux directly Use set_expression instead.
"""
return self._getvar('_aux_expression')
@aux.setter
def aux(self, val):
raise Exception('Cannot set model variables directly Use set_variable instead.')
@property
def w(self):
""" Process noise.
CasADi symbolic structure, can be indexed with user-defined variable names.
The process noise structure is created automatically, whenever the
:py:func:`Model.set_rhs` method is called with the argument ``process_noise = True``.
Note:
The process noise is used for the :py:class:`do_mpc.estimator.MHE` and
can be used to simulate a disturbed system in the :py:class:`do_mpc.simulator.Simulator`.
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set w directly
"""
return self._getvar('_w')
@w.setter
def w(self, val):
raise Exception('Cannot set process noise directly.')
@property
def v(self):
""" Measurement noise.
CasADi symbolic structure, can be indexed with user-defined variable names.
The measurement noise structure is created automatically, whenever the
:py:func:`Model.set_meas` method is called with the argument ``meas_noise = True``.
Note:
The measurement noise is used for the :py:class:`do_mpc.estimator.MHE` and
can be used to simulate a disturbed system in the :py:class:`do_mpc.simulator.Simulator`.
Useful CasADi symbolic structure methods:
* ``.shape``
* ``.keys()``
* ``.labels()``
Raises:
assertion: Cannot set v directly
"""
return self._getvar('_v')
@v.setter
def v(self, val):
raise Exception('Cannot set measurement noise directly.')
def set_variable(self, var_type:str, var_name:str, shape:Union[int,Tuple]=(1,1))->Union[castools.SX,castools.MX]:
"""Introduce new variables to the model class. Define variable type, name and shape (optional).
**Example:**
::
# States struct (optimization variables):
C_a = model.set_variable(var_type='_x', var_name='C_a', shape=(1,1))
T_K = model.set_variable(var_type='_x', var_name='T_K', shape=(1,1))
# Input struct (optimization variables):
Q_dot = model.set_variable(var_type='_u', var_name='Q_dot')
# Fixed parameters:
alpha = model.set_variable(var_type='_p', var_name='alpha')
Note:
``var_type`` allows a shorthand notation e.g. ``_x`` which is equivalent to ``states``.
Args:
var_type : Declare the type of the variable.
var_name : Set a user-defined name for the parameter. The names are reused throughout do_mpc.
shape : Shape of the current variable (optional), defaults to ``1``.
The following types of **var_type** are valid (long or short name is possible):
=========================== =========== ============================
Long name short name Remark
=========================== =========== ============================
``states`` ``_x`` Required
``inputs`` ``_u`` optional
``algebraic`` ``_z`` Optional
``parameter`` ``_p`` Optional
``timevarying_parameter`` ``_tvp`` Optional
=========================== =========== ============================
Raises:
assertion: **var_type** must be string
assertion: **var_name** must be string
assertion: **shape** must be tuple or int
assertion: Cannot call after :py:func:`setup`.
Returns:
Returns the newly created symbolic variable.
"""
assert self.flags['setup'] == False, 'Cannot call .set_variable after setup.'
assert isinstance(var_type, str), 'var_type must be str, you have: {}'.format(type(var_type))
assert isinstance(var_name, str), 'var_name must be str, you have: {}'.format(type(var_name))
assert isinstance(shape, (tuple,int)), 'shape must be tuple or int, you have: {}'.format(type(shape))
# Get short names:
var_type =var_type.replace('states', '_x'
).replace('inputs', '_u'
).replace('algebraic', '_z'
).replace('parameter', '_p'
).replace('timevarying_parameter', '_tvp')
# Check validity of var_type:
assert var_type in ['_x','_u','_z','_p','_tvp'], 'Trying to set non-existing variable var_type: {} with var_name {}'.format(var_type, var_name)
# Check validity of var_name:
assert var_name not in getattr(self,var_type)['name'], 'The variable name {} for type {} already exists.'.format(var_name, var_type)
# Create variable:
var = self.sv.sym(var_name, shape)
# Extend var list with new entry:
getattr(self, var_type)['var'].append(var)
getattr(self, var_type)['name'].append(var_name)
return var
def set_expression(self, expr_name:str, expr:Union[castools.SX,castools.MX])->Union[castools.SX,castools.MX]:
"""Introduce new expression to the model class. Expressions are not required but can be used
to extract further information from the model.
Expressions must be formulated with respect to ``_x``, ``_u``, ``_z``, ``_tvp``, ``_p``.
**Example:**
Maybe you are interested in monitoring the product of two states?
::
Introduce two scalar states:
x_1 = model.set_variable('_x', 'x_1')
x_2 = model.set_variable('_x', 'x_2')
# Introduce expression:
model.set_expression('x1x2', x_1*x_2)
This new expression ``x1x2`` is then available in all **do-mpc** modules utilizing
this model instance. It can be set, e.g. as the cost function in :py:class:`do_mpc.controller.MPC`
or simply used in a graphical representation of the simulated / controlled system.
Args:
expr_name : Arbitrary name for the given expression. Names are used for key word indexing.
expr : CasADi SX or MX function depending on ``_x``, ``_u``, ``_z``, ``_tvp``, ``_p``.
Raises:
assertion: expr_name must be str
assertion: expr must be a casadi SX or MX type
assertion: Cannot call after :py:func:`setup`.
Returns:
Returns the newly created expression. Expression can be used e.g. for the RHS.
"""
assert self.flags['setup'] == False, 'Cannot call .set_expression after setup'
assert isinstance(expr_name, str), 'expr_name must be str, you have: {}'.format(type(expr_name))
assert isinstance(expr, (castools.SX, castools.MX)), 'expr must be a casadi SX or MX type, you have:{}'.format(type(expr))
self._aux_expression.append(castools.entry(expr_name, expr = expr))
# Create variable:
var = self.sv.sym(expr_name, expr.shape)
self._aux['var'].append(var)
self._aux['name'].append(expr_name)
return expr
def set_meas(self, meas_name:str, expr:Union[castools.SX,castools.MX], meas_noise:bool=True)->Union[castools.SX,castools.MX]:
"""Introduce new measurable output to the model class.
.. math::
y = h(x(t),u(t),z(t),p(t),p_{\\text{tv}}(t)) + v(t)
or in case of discrete dynamics:
.. math::
y_k = h(x_k,u_k,z_k,p_k,p_{\\text{tv},k}) + v_k
By default, the model assumes state-feedback (all states are measured outputs).
Expressions must be formulated with respect to ``_x``, ``_u``, ``_z``, ``_tvp``, ``_p``.
Be default, it is assumed that the measurements experience additive noise :math:`v_k`.
This can be deactivated for individual measured variables by changing the boolean variable
``meas_noise`` to ``False``.
Note that measurement noise is only meaningful for state-estimation and will not affect the controller.
Furthermore, it can be set with each :py:class:`do_mpc.simulator.Simulator` call to obtain imperfect outputs.
Note:
For moving horizon estimation it is suggested to declare all inputs (``_u``) and e.g. a subset of states (``_x``) as
measurable output. Some other MHE formulations treat inputs separately.
Note:
It is often suggested to deactivate measurement noise for "measured" inputs (``_u``).
These can typically seen as certain variables.
**Example:**
::
# Introduce states:
x_meas = model.set_variable('_x', 'x', 3) # 3 measured states (vector)
x_est = model.set_variable('_x', 'x', 3) # 3 estimated states (vector)
# and inputs:
u = model.set_variable('_u', 'u', 2) # 2 inputs (vector)
# define measurements:
model.set_meas('x_meas', x_meas)
model.set_meas('u', u)
Args:
meas_name : Arbitrary name for the given expression. Names are used for key word indexing.
expr : CasADi SX or MX function depending on ``_x``, ``_u``, ``_z``, ``_tvp``, ``_p``.
meas_noise : Set if the measurement equation is disturbed by additive noise.
Raises:
assertion: expr_name must be str
assertion: expr must be a casadi SX or MX type
assertion: Cannot call after :py:func:`setup`.
Returns:
Returns the newly created measurement expression.
"""
assert self.flags['setup'] == False, 'Cannot call .set_meas after setup'
assert isinstance(meas_name, str), 'meas_name must be str, you have: {}'.format(type(meas_name))
assert isinstance(expr, (castools.SX, castools.MX)), 'expr must be a casadi SX or MX type, you have:{}'.format(type(expr))
assert isinstance(meas_noise, bool), 'meas_noise must be of type boolean. You have: {}'.format(type(meas_noise))
# Create a new process noise variable and add it to the rhs equation.
if meas_noise:
var = self.sv.sym(meas_name+'_noise', expr.shape[0])
self._v['name'].append(meas_name+'_noise')
self._v['var'].append(var)
expr += var
self._y_expression.append(castools.entry(meas_name, expr = expr))
# Create variable:
var = self.sv.sym(meas_name, expr.shape)
self._y['var'].append(var)
self._y['name'].append(meas_name)
return expr
def set_rhs(self, var_name:str, expr:Union[castools.SX,castools.MX], process_noise:bool=False)->None:
"""Formulate the right hand side (rhs) of the ODE:
.. math::
\\dot{x}(t) = f(x(t),u(t),z(t),p(t),p_{\\text{tv}}(t)) + w(t),
or the update equation in case of discrete dynamics:
.. math::
x_{k+1} = f(x_k,u_k,z_k,p_k,p_{\\text{tv},k}) + w_k,
Each defined state variable must have a respective equation (of matching dimension)
for the rhs. Match the rhs with the state by choosing the corresponding names.
rhs must be formulated with respect to ``_x``, ``_u``, ``_z``, ``_tvp``, ``_p``.
**Example**:
::
tank_level = model.set_variable('states', 'tank_level')
tank_temp = model.set_variable('states', 'tank_temp')
tank_level_next = 0.5*tank_level
tank_temp_next = ...
model.set_rhs('tank_level', tank_level_next)
model.set_rhs('tank_temp', tank_temp_next)
Optionally, set ``process_noise = True`` to introduce an additive process noise variable.
This is meaningful for the :py:class:`do_mpc.estimator.MHE` (See :py:func:`do_mpc.estimator.MHE.set_default_objective` for more details).
Furthermore, it can be set with each :py:class:`do_mpc.simulator.Simulator` call to obtain imperfect (realistic) simulation results.
Args:
var_name : Reference to previously introduced state names (with :py:func:`Model.set_variable`)
expr : CasADi SX or MX function depending on ``_x``, ``_u``, ``_z``, ``_tvp``, ``_p``.
process_noise : Make the respective state variable non-deterministic.
Raises:
assertion: var_name must be str
assertion: expr must be a casadi SX or MX type
assertion: var_name must refer to the previously defined states
assertion: Cannot call after :py:func`setup`.
"""
assert self.flags['setup'] == False, 'Cannot call .set_rhs after .setup.'
assert isinstance(var_name, str), 'var_name must be str, you have: {}'.format(type(var_name))
assert isinstance(expr, (castools.SX, castools.MX, castools.DM)), 'expr must be a casadi SX, MX or DM type, you have:{}'.format(type(expr))
assert var_name in self._x['name'], 'var_name must refer to the previously defined states ({}). You have: {}'.format(self._x['name'], var_name)
# Create a new process noise variable and add it to the rhs equation.
if process_noise:
if self.symvar_type == 'MX':
var = castools.MX.sym(var_name+'_noise', expr.shape[0])
else:
var = castools.SX.sym(var_name+'_noise', expr.shape[0])
self._w['name'].append(var_name + '_noise')
self._w['var'].append(var)
expr += var
self.rhs_list.extend([{'var_name': var_name, 'expr': expr}])
def set_alg(self, expr_name:str, expr:Union[castools.SX,castools.MX])->None:
""" Introduce new algebraic equation to model.
For the continous time model, the expression must be formulated as
.. math::
0 = g(x(t),u(t),z(t),p(t),p_{\\text{tv}}(t))
or for a ``discrete`` model:
.. math::
0 = g(x_k,u_k,z_k,p_k,p_{\\text{tv},k})
Note:
For the introduced algebraic variables :math:`z \in \mathbb{R}^{n_z}`
it is required to introduce exactly :math:`n_z` algebraic equations.
Otherwise :py:meth:`setup` will throw an error message.
Args:
expr_name : Name of the introduced expression
expr : CasADi SX or MX function depending on ``_x``, ``_u``, ``_z``, ``_tvp``, ``_p``.
"""
assert self.flags['setup'] == False, 'Cannot call .set_alg after .setup.'
assert isinstance(expr_name, str), 'expr_name must be str, you have: {}'.format(type(expr_name))
assert isinstance(expr, (castools.SX, castools.MX, castools.DM)), 'expr must be a casadi SX, MX or DM type, you have:{}'.format(type(expr))
self.alg_list.append(castools.entry(expr_name, expr = expr))
def _convert2struct(self, var_dict:dict)->Union[castools.structure3.SXStruct,castools.structure3.MXStruct]:
"""Helper function for :py:func:`setup`. Not part of the public API.
This method is used to convert the attributes:
::
self._x
self._u
...
into structures of type ``struct_symSX`` or ``struct_symMX`` (depending on the attribute ``symvar_type``).
These structures are created with **newly introduced** symbolic variables which are of the same shapes and names
as those introduced with :py:func:`set_variable`.
**Why is this necessary?**
For the symbolic variable type ``MX`` it is impossible to first create symbolic variables and then combine them into a structure (``struct_symMX``).
We thus create symbolic variables, then create a structure holding similar variables and the substitute these newly introduced variables in all expressions.
Args:
var_dict : Attributes that are configured with :py:func:`set_variable` (e.g. ``self._x``). These attributes are of type ``dict`` with the keys ``name`` and ``var``
Returns:
CasADi structure
"""
result_struct = self.sv.sym_struct([
castools.entry(name, shape = var.shape) for var, name in zip(var_dict['var'], var_dict['name'])
])
return result_struct
def _substitute_struct_vars(self, var_dict_list:list, sym_struct_list:list, expr:Union[castools.structure3.SXStruct,castools.structure3.MXStruct]):
"""Helper function for :py:func:`setup`. Not part of the public API.
This method is used to substitute the newly introduced structured variables with :py:func:`_convert2struct`
into the expressions that define the model (e.g. ``_rhs``).
**Why is this necessary?**
For the symbolic variable type ``MX`` it is impossible to first create symbolic variables and then combine them into a structure (``struct_symMX``).
We thus create symbolic variables, then create a structure holding similar variables and the substitute these newly introduced variables in all expressions.
Args:
var_dict_list : List of attributes that are configured with :py:func:`set_variable` (e.g. ``self._x``). These attributes are of type ``dict`` with the keys ``name`` and ``var``
sym_dict_list : List of the same attributes converted into structures with :py:func:`_convert2struct`.
expr: Casadi structured expr in which the variables from ``var_dict_list`` are substituted with those from ``sym_struct_list``.
"""
assert len(var_dict_list)==len(sym_struct_list)
subs = expr
for var_dict, sym_struct in zip(var_dict_list, sym_struct_list):
assert var_dict['name'] == sym_struct.keys()
for var, name in zip(var_dict['var'], var_dict['name']):
subs = castools.substitute(subs, var, sym_struct[name])
if self.symvar_type == 'MX':
expr = expr(subs)
else:
expr.master = subs
return expr
def _substitute_exported_vars(self, var_dict_list, sym_struct_list):
"""Helper function for :py:func:`setup`. Not part of the public API.
This method is used to substitute the newly introduced structured variables with :py:func:`_convert2struct`
in all previously EXPORTED variables, e.g. with :py:func:`set_variable`.
This is necessary because otherwise variables obtained from the model PRIOR to calling :py:func:`setup`
are not the same as those returned after calling :py:func:`setup`:
::
x = model.set_variable('_x', 'x')
...
model.setup()
# We don't want this:
model.x['x'] == x
>> MX(x==x)
# We want this:
model.x['x'] == x
>> MX(1)
This is a bit of a HACKY solution and might require fixing if CasADi is changing its API.
"""
for var_dict, sym_struct in zip(var_dict_list, sym_struct_list):
assert var_dict['name'] == sym_struct.keys()
for var, name in zip(var_dict['var'], var_dict['name']):
var.__dict__['this'] = sym_struct[name].__dict__['this']
def setup(self)->None:
"""Setup method must be called to finalize the modelling process.
All required model variables must be declared.
The right hand side expression for ``_x`` must have been set with :py:func:`set_rhs`.
Sets default measurement function (state feedback) if :py:func:`set_meas` was not called.
Warnings:
After calling :py:func:`setup`, the model is locked and no further variables,
expressions etc. can be set.
Raises:
assertion: Definition of right hand side (rhs) is incomplete
"""
# Set all states as measurements if set_meas was not called by user.
if not self._y_expression:
for name, var in zip(self._x['name'], self._x['var']):
self.set_meas(name, var)
# Write self._y_expression (measurement equations) as struct symbolic expression structures.
self._y_expression = self.sv.struct(self._y_expression)
# Create structure from listed symbolic variables:
_x = self._convert2struct(self._x)
_w = self._convert2struct(self._w)
_v = self._convert2struct(self._v)
_u = self._convert2struct(self._u)
_z = self._convert2struct(self._z)
_p = self._convert2struct(self._p)
_tvp = self._convert2struct(self._tvp)
_aux = self._convert2struct(self._aux)
_y = self._convert2struct(self._y)
# Write self._aux_expression.
self._aux_expression = self.sv.struct(self._aux_expression)
# Create alg equations:
self._alg = self.sv.struct(self.alg_list)
# Create mutable struct with identical structure as _x to hold the right hand side.
self._rhs = self.sv.struct(_x)
# Set the expressions in self._rhs with the previously defined SX.sym variables.
# Check if an expression is set for every state of the system.
_x_names = set(self._x['name'])
for rhs_i in self.rhs_list:
self._rhs[rhs_i['var_name']] = rhs_i['expr']
_x_names -= set([rhs_i['var_name']])
assert len(_x_names) == 0, 'Definition of right hand side (rhs) is incomplete. Missing: {}. Use: set_rhs to define expressions.'.format(_x_names)
var_dict_list = [self._x, self._w, self._v, self._u, self._z, self._p, self._tvp]
sym_struct_list = [_x, _w, _v, _u, _z, _p, _tvp]
self._rhs = self._substitute_struct_vars(var_dict_list, sym_struct_list, self._rhs)
self._alg = self._substitute_struct_vars(var_dict_list, sym_struct_list, self._alg)
self._aux_expression = self._substitute_struct_vars(var_dict_list, sym_struct_list, self._aux_expression)
self._y_expression = self._substitute_struct_vars(var_dict_list, sym_struct_list, self._y_expression)
self._substitute_exported_vars(var_dict_list, sym_struct_list)
self._x = _x
self._w = _w
self._v = _v
self._u = _u
self._z = _z
self._p = _p
self._tvp = _tvp
self._y = _y
self._aux = _aux
A_lin_expr = castools.jacobian(self._rhs,self._x)
B_lin_expr = castools.jacobian(self._rhs,self._u)
C_lin_expr = castools.jacobian(self._y_expression,self._x)
D_lin_expr = castools.jacobian(self._y_expression,self._u)
# Declare functions for the right hand side and the aux_expressions.
self._rhs_fun = castools.Function('rhs_fun',
[_x, _u, _z, _tvp, _p, _w], [self._rhs],
["_x", "_u", "_z", "_tvp", "_p", "_w"], ["_rhs"])
self._alg_fun = castools.Function('alg_fun',
[_x, _u, _z, _tvp, _p, _w], [self._alg],
["_x", "_u", "_z", "_tvp", "_p", "_w"], ["_alg"])
self._aux_expression_fun = castools.Function('aux_expression_fun',
[_x, _u, _z, _tvp, _p], [self._aux_expression],
["_x", "_u", "_z", "_tvp", "_p"], ["_aux_expression"])
self._meas_fun = castools.Function('meas_fun',
[_x, _u, _z, _tvp, _p, _v], [self._y_expression],
["_x", "_u", "_z", "_tvp", "_p", "_v"], ["_y_expression"])
self.A_fun = castools.Function('A_fun',
[_x, _u, _z, _tvp, _p, _w],[A_lin_expr],
["_x", "_u", "_z", "_tvp", "_p", "_w"],["A_lin_expr"])
self.B_fun = castools.Function('B_fun',
[_x, _u, _z, _tvp, _p, _w],[B_lin_expr],
["_x", "_u", "_z", "_tvp", "_p", "_w"],["B_lin_expr"])
self.C_fun = castools.Function('C_fun',
[_x, _u, _z, _tvp, _p, _v],[C_lin_expr],
["_x", "_u", "_z", "_tvp", "_p", "_v"],["C_lin_expr"])
self.D_fun = castools.Function('D_fun',
[_x, _u, _z, _tvp, _p, _v],[D_lin_expr],
["_x", "_u", "_z", "_tvp", "_p", "_v"],["D_lin_expr"])
# Create and store some information about the model regarding number of variables for
# _x, _y, _u, _z, _tvp, _p, _aux
self.n_x = self._x.shape[0]
self.n_y = self._y.shape[0]
self.n_u = self._u.shape[0]
self.n_z = self._z.shape[0]
self.n_tvp = self._tvp.shape[0]
self.n_p = self._p.shape[0]
self.n_aux = self._aux_expression.shape[0]
self.n_w = self._w.shape[0]
self.n_v = self._v.shape[0]
msg = 'Must have the same number of algebraic equations (you have {}) and variables (you have {}).'
assert self.n_z == self._alg.shape[0], msg.format(self._alg.shape[0], self.n_z)
# Remove temporary storage for the symbolic variables. This allows to pickle the class.
delattr(self, 'rhs_list')
delattr(self, 'alg_list')
self.flags['setup'] = True
@staticmethod
def _transfer_variables(old_model, new_model, transfer=['_x', '_u', '_tvp', '_p']):
"""Private and static method to transfer variables from old model to new model.
This is used in :func:`do_mpc.model.linearize` and :meth:`LinearModel.discretize`.
Extracts information about the noise (measurement and process) from the old model and
returns an array of booleans for the measurements and states (one element for each named instance).
"""
# Initialize array for process noise and measurement noise
process_noise = old_model.x(False)
meas_noise = old_model.y(False)
#Setting states and inputs (get rid of defaults for u, tvp, p)
for key in old_model.x.keys():
new_model.set_variable('_x', key, shape=old_model.x[key].shape)
for key in old_model.u.keys()[1:]:
new_model.set_variable('_u', key, shape=old_model.u[key].shape)
for key in old_model.tvp.keys()[1:]:
new_model.set_variable('_tvp', key, shape=old_model.tvp[key].shape)
for key in old_model.p.keys()[1:]:
new_model.set_variable('_p', key, shape=old_model.p[key].shape)
for key in old_model.aux.keys()[1:]:
expr_fun = old_model._aux_expression_fun
expr = expr_fun(new_model.x, new_model.u, new_model.z, new_model.tvp, new_model.p)
expr_struct = old_model._aux_expression(expr)
new_model.set_expression(key, expr_struct[key])
def get_linear_system_matrices(self,
xss:np.ndarray=None,
uss:np.ndarray=None,
z:np.ndarray=None,
tvp:np.ndarray=None,
p:np.ndarray=None
)->Union[Tuple[castools.SX,castools.SX,castools.SX,castools.SX],Tuple[np.ndarray,np.ndarray,np.ndarray,np.ndarray]]:
"""
Returns the matrix quadrupel :math:`(A,B,C,D)` of the linearized system around the operating point (``xss,uss,z,tvp,p,w,v``).
All arguments are optional in which case the matrices might still be symbolic.
If the matrices are not symbolic, they are returned as numpy arrays.
Args:
xss : Steady state state
uss : Steady state input
z : Steady state algebraic states
tvp : time varying parameters set point
p : parameters set point
Returns:
State matrix, Input matrix, Output matrix, Feedforward matrix
"""
if self.symvar_type == 'MX':
raise ValueError("get_linear_system_matrices requires symvar_type SX")
# Default values for all variables are the symbolic variables themselves.
if isinstance(xss, type(None)):
xss = self.x
if isinstance(uss, type(None)):
uss = self.u
if isinstance(z, type(None)):
z = self.z
if isinstance(tvp, type(None)):
tvp = self.tvp
if isinstance(p, type(None)):
p = self.p
# Noise variables are always set to zero.
w = self.w(0)
v = self.v(0)
# Create A,B,C,D matrices and check if (for the given inputs) they are constant are still symbolic.
# Constant matrices are converted to numpy arrays.
A = self.A_fun(xss, uss, z, tvp, p, w)
if A.is_constant():
A = castools.DM(A).full()
B = self.B_fun(xss, uss, z, tvp, p, w)
if B.is_constant():
B = castools.DM(B).full()
C = self.C_fun(xss, uss, z, tvp, p, v)
if C.is_constant():
C = castools.DM(C).full()
D = self.D_fun(xss, uss, z, tvp, p, v)
if D.is_constant():
D = castools.DM(D).full()
return A,B,C,D