368 lines
13 KiB
Python
368 lines
13 KiB
Python
# -*- coding: future_fstrings -*-
|
|
#
|
|
# Copyright (c) The acados authors.
|
|
#
|
|
# This file is part of acados.
|
|
#
|
|
# The 2-Clause BSD License
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted provided that the following conditions are met:
|
|
#
|
|
# 1. Redistributions of source code must retain the above copyright notice,
|
|
# this list of conditions and the following disclaimer.
|
|
#
|
|
# 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
# this list of conditions and the following disclaimer in the documentation
|
|
# and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE
|
|
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
|
|
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
|
|
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
|
|
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
|
|
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
|
|
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.;
|
|
#
|
|
|
|
import numpy as np
|
|
import os
|
|
from .acados_model import AcadosModel
|
|
from .utils import get_acados_path, get_lib_ext
|
|
|
|
class AcadosSimDims:
|
|
"""
|
|
Class containing the dimensions of the model to be simulated.
|
|
"""
|
|
def __init__(self):
|
|
self.__nx = None
|
|
self.__nu = None
|
|
self.__nz = 0
|
|
self.__np = 0
|
|
|
|
@property
|
|
def nx(self):
|
|
""":math:`n_x` - number of states. Type: int > 0"""
|
|
return self.__nx
|
|
|
|
@property
|
|
def nz(self):
|
|
""":math:`n_z` - number of algebraic variables. Type: int >= 0"""
|
|
return self.__nz
|
|
|
|
@property
|
|
def nu(self):
|
|
""":math:`n_u` - number of inputs. Type: int >= 0"""
|
|
return self.__nu
|
|
|
|
@property
|
|
def np(self):
|
|
""":math:`n_p` - number of parameters. Type: int >= 0"""
|
|
return self.__np
|
|
|
|
@nx.setter
|
|
def nx(self, nx):
|
|
if isinstance(nx, int) and nx > 0:
|
|
self.__nx = nx
|
|
else:
|
|
raise Exception('Invalid nx value, expected positive integer.')
|
|
|
|
@nz.setter
|
|
def nz(self, nz):
|
|
if isinstance(nz, int) and nz > -1:
|
|
self.__nz = nz
|
|
else:
|
|
raise Exception('Invalid nz value, expected nonnegative integer.')
|
|
|
|
@nu.setter
|
|
def nu(self, nu):
|
|
if isinstance(nu, int) and nu > -1:
|
|
self.__nu = nu
|
|
else:
|
|
raise Exception('Invalid nu value, expected nonnegative integer.')
|
|
|
|
@np.setter
|
|
def np(self, np):
|
|
if isinstance(np, int) and np > -1:
|
|
self.__np = np
|
|
else:
|
|
raise Exception('Invalid np value, expected nonnegative integer.')
|
|
|
|
def set(self, attr, value):
|
|
setattr(self, attr, value)
|
|
|
|
|
|
class AcadosSimOpts:
|
|
"""
|
|
class containing the solver options
|
|
"""
|
|
def __init__(self):
|
|
self.__integrator_type = 'ERK'
|
|
self.__collocation_type = 'GAUSS_LEGENDRE'
|
|
self.__Tsim = None
|
|
# ints
|
|
self.__sim_method_num_stages = 1
|
|
self.__sim_method_num_steps = 1
|
|
self.__sim_method_newton_iter = 3
|
|
# doubles
|
|
self.__sim_method_newton_tol = 0.0
|
|
# bools
|
|
self.__sens_forw = True
|
|
self.__sens_adj = False
|
|
self.__sens_algebraic = False
|
|
self.__sens_hess = False
|
|
self.__output_z = True
|
|
self.__sim_method_jac_reuse = 0
|
|
self.__ext_fun_compile_flags = '-O2'
|
|
|
|
@property
|
|
def integrator_type(self):
|
|
"""Integrator type. Default: 'ERK'."""
|
|
return self.__integrator_type
|
|
|
|
@property
|
|
def num_stages(self):
|
|
"""Number of stages in the integrator. Default: 1"""
|
|
return self.__sim_method_num_stages
|
|
|
|
@property
|
|
def num_steps(self):
|
|
"""Number of steps in the integrator. Default: 1"""
|
|
return self.__sim_method_num_steps
|
|
|
|
@property
|
|
def newton_iter(self):
|
|
"""Number of Newton iterations in simulation method. Default: 3"""
|
|
return self.__sim_method_newton_iter
|
|
|
|
@property
|
|
def newton_tol(self):
|
|
"""
|
|
Tolerance for Newton system solved in implicit integrator (IRK, GNSF).
|
|
0.0 means this is not used and exactly newton_iter iterations are carried out.
|
|
Default: 0.0
|
|
"""
|
|
return self.__sim_method_newton_tol
|
|
|
|
@property
|
|
def sens_forw(self):
|
|
"""Boolean determining if forward sensitivities are computed. Default: True"""
|
|
return self.__sens_forw
|
|
|
|
@property
|
|
def sens_adj(self):
|
|
"""Boolean determining if adjoint sensitivities are computed. Default: False"""
|
|
return self.__sens_adj
|
|
|
|
@property
|
|
def sens_algebraic(self):
|
|
"""Boolean determining if sensitivities wrt algebraic variables are computed. Default: False"""
|
|
return self.__sens_algebraic
|
|
|
|
@property
|
|
def sens_hess(self):
|
|
"""Boolean determining if hessians are computed. Default: False"""
|
|
return self.__sens_hess
|
|
|
|
@property
|
|
def output_z(self):
|
|
"""Boolean determining if values for algebraic variables (corresponding to start of simulation interval) are computed. Default: True"""
|
|
return self.__output_z
|
|
|
|
@property
|
|
def sim_method_jac_reuse(self):
|
|
"""Integer determining if jacobians are reused (0 or 1). Default: 0"""
|
|
return self.__sim_method_jac_reuse
|
|
|
|
@property
|
|
def T(self):
|
|
"""Time horizon"""
|
|
return self.__Tsim
|
|
|
|
@property
|
|
def collocation_type(self):
|
|
"""Collocation type: relevant for implicit integrators
|
|
-- string in {GAUSS_RADAU_IIA, GAUSS_LEGENDRE}
|
|
|
|
Default: GAUSS_LEGENDRE
|
|
"""
|
|
return self.__collocation_type
|
|
|
|
@property
|
|
def ext_fun_compile_flags(self):
|
|
"""
|
|
String with compiler flags for external function compilation.
|
|
Default: '-O2'.
|
|
"""
|
|
return self.__ext_fun_compile_flags
|
|
|
|
@ext_fun_compile_flags.setter
|
|
def ext_fun_compile_flags(self, ext_fun_compile_flags):
|
|
if isinstance(ext_fun_compile_flags, str):
|
|
self.__ext_fun_compile_flags = ext_fun_compile_flags
|
|
else:
|
|
raise Exception('Invalid ext_fun_compile_flags, expected a string.\n')
|
|
|
|
@integrator_type.setter
|
|
def integrator_type(self, integrator_type):
|
|
integrator_types = ('ERK', 'IRK', 'GNSF')
|
|
if integrator_type in integrator_types:
|
|
self.__integrator_type = integrator_type
|
|
else:
|
|
raise Exception('Invalid integrator_type value. Possible values are:\n\n' \
|
|
+ ',\n'.join(integrator_types) + '.\n\nYou have: ' + integrator_type + '.\n\n')
|
|
|
|
@collocation_type.setter
|
|
def collocation_type(self, collocation_type):
|
|
collocation_types = ('GAUSS_RADAU_IIA', 'GAUSS_LEGENDRE')
|
|
if collocation_type in collocation_types:
|
|
self.__collocation_type = collocation_type
|
|
else:
|
|
raise Exception('Invalid collocation_type value. Possible values are:\n\n' \
|
|
+ ',\n'.join(collocation_types) + '.\n\nYou have: ' + collocation_type + '.\n\n')
|
|
|
|
@T.setter
|
|
def T(self, T):
|
|
self.__Tsim = T
|
|
|
|
@num_stages.setter
|
|
def num_stages(self, num_stages):
|
|
if isinstance(num_stages, int):
|
|
self.__sim_method_num_stages = num_stages
|
|
else:
|
|
raise Exception('Invalid num_stages value. num_stages must be an integer.')
|
|
|
|
@num_steps.setter
|
|
def num_steps(self, num_steps):
|
|
if isinstance(num_steps, int):
|
|
self.__sim_method_num_steps = num_steps
|
|
else:
|
|
raise Exception('Invalid num_steps value. num_steps must be an integer.')
|
|
|
|
@newton_iter.setter
|
|
def newton_iter(self, newton_iter):
|
|
if isinstance(newton_iter, int):
|
|
self.__sim_method_newton_iter = newton_iter
|
|
else:
|
|
raise Exception('Invalid newton_iter value. newton_iter must be an integer.')
|
|
|
|
@newton_tol.setter
|
|
def newton_tol(self, newton_tol):
|
|
if isinstance(newton_tol, float):
|
|
self.__sim_method_newton_tol = newton_tol
|
|
else:
|
|
raise Exception('Invalid newton_tol value. newton_tol must be an float.')
|
|
|
|
@sens_forw.setter
|
|
def sens_forw(self, sens_forw):
|
|
if sens_forw in (True, False):
|
|
self.__sens_forw = sens_forw
|
|
else:
|
|
raise Exception('Invalid sens_forw value. sens_forw must be a Boolean.')
|
|
|
|
@sens_adj.setter
|
|
def sens_adj(self, sens_adj):
|
|
if sens_adj in (True, False):
|
|
self.__sens_adj = sens_adj
|
|
else:
|
|
raise Exception('Invalid sens_adj value. sens_adj must be a Boolean.')
|
|
|
|
@sens_hess.setter
|
|
def sens_hess(self, sens_hess):
|
|
if sens_hess in (True, False):
|
|
self.__sens_hess = sens_hess
|
|
else:
|
|
raise Exception('Invalid sens_hess value. sens_hess must be a Boolean.')
|
|
|
|
@sens_algebraic.setter
|
|
def sens_algebraic(self, sens_algebraic):
|
|
if sens_algebraic in (True, False):
|
|
self.__sens_algebraic = sens_algebraic
|
|
else:
|
|
raise Exception('Invalid sens_algebraic value. sens_algebraic must be a Boolean.')
|
|
|
|
@output_z.setter
|
|
def output_z(self, output_z):
|
|
if output_z in (True, False):
|
|
self.__output_z = output_z
|
|
else:
|
|
raise Exception('Invalid output_z value. output_z must be a Boolean.')
|
|
|
|
@sim_method_jac_reuse.setter
|
|
def sim_method_jac_reuse(self, sim_method_jac_reuse):
|
|
if sim_method_jac_reuse in (0, 1):
|
|
self.__sim_method_jac_reuse = sim_method_jac_reuse
|
|
else:
|
|
raise Exception('Invalid sim_method_jac_reuse value. sim_method_jac_reuse must be 0 or 1.')
|
|
|
|
class AcadosSim:
|
|
"""
|
|
The class has the following properties that can be modified to formulate a specific simulation problem, see below:
|
|
|
|
:param acados_path: string with the path to acados. It is used to generate the include and lib paths.
|
|
|
|
- :py:attr:`dims` of type :py:class:`acados_template.acados_ocp.AcadosSimDims` - are automatically detected from model
|
|
- :py:attr:`model` of type :py:class:`acados_template.acados_model.AcadosModel`
|
|
- :py:attr:`solver_options` of type :py:class:`acados_template.acados_sim.AcadosSimOpts`
|
|
|
|
- :py:attr:`acados_include_path` (set automatically)
|
|
- :py:attr:`shared_lib_ext` (set automatically)
|
|
- :py:attr:`acados_lib_path` (set automatically)
|
|
- :py:attr:`parameter_values` - used to initialize the parameters (can be changed)
|
|
|
|
"""
|
|
def __init__(self, acados_path=''):
|
|
if acados_path == '':
|
|
acados_path = get_acados_path()
|
|
self.dims = AcadosSimDims()
|
|
"""Dimension definitions, automatically detected from :py:attr:`model`. Type :py:class:`acados_template.acados_sim.AcadosSimDims`"""
|
|
self.model = AcadosModel()
|
|
"""Model definitions, type :py:class:`acados_template.acados_model.AcadosModel`"""
|
|
self.solver_options = AcadosSimOpts()
|
|
"""Solver Options, type :py:class:`acados_template.acados_sim.AcadosSimOpts`"""
|
|
|
|
self.acados_include_path = os.path.join(acados_path, 'include').replace(os.sep, '/') # the replace part is important on Windows for CMake
|
|
"""Path to acados include directory (set automatically), type: `string`"""
|
|
self.acados_lib_path = os.path.join(acados_path, 'lib').replace(os.sep, '/') # the replace part is important on Windows for CMake
|
|
"""Path to where acados library is located (set automatically), type: `string`"""
|
|
|
|
self.code_export_directory = 'c_generated_code'
|
|
"""Path to where code will be exported. Default: `c_generated_code`."""
|
|
self.shared_lib_ext = get_lib_ext()
|
|
|
|
# get cython paths
|
|
from sysconfig import get_paths
|
|
self.cython_include_dirs = [np.get_include(), get_paths()['include']]
|
|
|
|
self.__parameter_values = np.array([])
|
|
self.__problem_class = 'SIM'
|
|
|
|
@property
|
|
def parameter_values(self):
|
|
""":math:`p` - initial values for parameter - can be updated"""
|
|
return self.__parameter_values
|
|
|
|
@parameter_values.setter
|
|
def parameter_values(self, parameter_values):
|
|
if isinstance(parameter_values, np.ndarray):
|
|
self.__parameter_values = parameter_values
|
|
else:
|
|
raise Exception('Invalid parameter_values value. ' +
|
|
f'Expected numpy array, got {type(parameter_values)}.')
|
|
|
|
def set(self, attr, value):
|
|
# tokenize string
|
|
tokens = attr.split('_', 1)
|
|
if len(tokens) > 1:
|
|
setter_to_call = getattr(getattr(self, tokens[0]), 'set')
|
|
else:
|
|
setter_to_call = getattr(self, 'set')
|
|
|
|
setter_to_call(tokens[1], value)
|
|
|
|
return
|