Source code for nmpyc.constraints

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

# @author: Jonas Schiessl

import nmpyc as mpc 
from nmpyc.utils import mpc_convert

import numpy as np

from inspect import signature
import dill

[docs] class constraints: """ Class used to define the constraints of the optimnal control problem. Support for nonlinear, linear and box constraints are implemented and provided. To define the constraints, first, an empty object have to be created. Then the individual constraints can be added with the help of the methods :py:meth:`add_bound` and :py:meth:`add_constr`. """ def __init__(self): self._lower_bndx = None self._upper_bndx = None self._lower_bndu = None self._upper_bndu = None self._lower_bndend = None self._upper_bndend = None self._linear_constr = {'eq': [], 'ineq': [], 'terminal_eq': [], 'terminal_ineq': []} self._nonlinear_constr = {'eq': [], 'ineq': [], 'terminal_eq': [], 'terminal_ineq': []} self._type = 'LQP' @property def lower_bndx(self): """array : Lower bound :math:`l_x \in \mathbb{R}^{nx}` of the state. For all states :math:`x(t_k)` the inequalities .. math:: x_i(t_k) \geq l_{x_i} \quad \\text{for } i = 1,\ldots,nx hold as a constraint. """ return self._lower_bndx @property def upper_bndx(self): """array : Upper bound :math:`u_x \in \mathbb{R}^{nx}` of the state. For all states :math:`x(t_k)` the inequalities .. math:: x_i(t_k) \leq u_{x_i} \quad \\text{for } i = 1,\ldots,nx hold as a constraint. """ return self._upper_bndx @property def lower_bndu(self): """array : Lower bound :math:`l_u \in \mathbb{R}^{nu}` for the control. For all controls :math:`u(t_k)` the inequalities .. math:: u_i(t_k) \geq l_{u_i} \quad \\text{for } i = 1,\ldots,nu hold as a constraint. """ return self._lower_bndu @property def upper_bndu(self): """array : Upper bound :math:`u_u \in \mathbb{R}^{nu}` of the control. For all controls :math:`u(t_k)` the inequalities .. math:: u_i(t_k) \leq u_{u_i} \quad \\text{for } i = 1,\ldots,nu hold as a constraint. """ return self._upper_bndu @property def lower_bndend(self): """array : Lower bound :math:`l_x \in \mathbb{R}^{nx}` of the terminal state. For the terminal state :math:`x(t_N)` the inequality .. math:: x_i(t_N) \geq l_{x_i} \quad \\text{for } i = 1,\ldots,nx holds as a constraint. """ return self._lower_bndend @property def upper_bndend(self): """array : Upper bound :math:`u_x \in \mathbb{R}^{nx}` of the terminal state. For the terminal state :math:`x(t_N)` the inequalities .. math:: x_i(t_N) \leq u_{x_i} \quad \\text{for } i = 1,\ldots,nx hold as a constraint. """ return self._upper_bndend @property def linear_constr(self): """dict : Collection of all linear constraints. This dictionary has the following form: >>> linear_constr = {'eq': [..], 'ineq': [..], >>> 'terminal_eq': [..], 'terminal_ineq': [..]} The arrays that define the constraints are saved as lists which are contained in the dictionary. For example >>> linear_constr['eq'][0] returns a list with the arrays :math:`H`, :math:`F` and :math:`h` defining the first equality constraint .. math:: Hx + Fu = h. """ return self._linear_constr @property def nonlinear_constr(self): """dict : Collection of all nonlinear constraints. This dictionary has the following form: >>> nonlinear_constr = {'eq': [..], 'ineq': [..], >>> 'terminal_eq': [..], 'terminal_ineq': [..]} In the lists contained in the dictionary the functions defining the constraints are saved. For example >>> nonlinear_constr['eq'][0] returns the function :math:`h(t,x,u)` defining the first equality constraint .. math:: h(t,x,u) = 0. """ return self._nonlinear_constr @property def type(self): """str : Indicating whether all constraints are linear. If `LQP`, all constraints are linear. Then all constraints are of the form .. math:: Ex + Fu \leq h If at least one constraint is initialized as a nonlinear constraint this attribute has the value `NLP`. """ return self._type def __str__(self): string = '' nlbx = 0 nubx = 0 nlbe = 0 nube = 0 if self._lower_bndx is not None: for i in range(len(self._lower_bndx)): if self._lower_bndx[i] != -mpc.inf: nlbx += 1 if self._upper_bndx is not None: for i in range(len(self._upper_bndx)): if self.upper_bndx[i] != -mpc.inf: nubx += 1 if self._lower_bndend is not None: for i in range(len(self._lower_bndend)): if self.lower_bndend[i] != -mpc.inf: nlbe += 1 if self._upper_bndend is not None: for i in range(len(self._upper_bndend)): if self._upper_bndend[i] != -mpc.inf: nube += 1 string += 'number of lower bounds for state: ' + str(nlbx) +'\n' string += 'number of upper bounds for state: ' + str(nubx) +'\n' string += ('number of lower bounds for terminal state: ' + str(nlbe) +'\n') string += ('number of upper bounds for sterminal tate: ' + str(nube) +'\n') nlbu = 0 nubu = 0 if self._lower_bndu is not None: for i in range(len(self._lower_bndu)): if self._lower_bndu[i] != -mpc.inf: nlbu += 1 if self._upper_bndu is not None: for i in range(len(self._upper_bndu)): if self._upper_bndu[i] != -mpc.inf: nubu += 1 string += 'number of lower bounds for control: ' + str(nlbu) +'\n' string += 'number of upper bounds for control: ' + str(nubu) +'\n' for key in self._linear_constr.keys(): string += ('number of linear ' + key + ' constraints: ' + str(len(self._linear_constr[key])) +'\n') for key in self._nonlinear_constr.keys(): string += ('number of nonlinear ' + key + ' constraints: ' + str(len(self._nonlinear_constr[key])) +'\n') return string[:-1] def add_bound(self, bnd_type, variable, bound): """Add bounds as linear constraints to the OCP. Note while adding the bound it is not checked if the bounds have the correct shape. This will be verified later during the optimization progress. Parameters ---------- bnd_type : str String defining whether the bound is a lower or upper bound. variable : str String defining on which variable the bound should be applied. Possible values are *state*, *control* and *terminal*. bound : array Array containing the values of the bound. For example >>> constraints.add_bound('lower', 'state', lbx) will add `lbx` as :py:attr:`~lower_bndx` while >>> constraints.add_bound('upper', 'terminal', ub_end) will add `ub_end` as :py:attr:`~upper_bndend`. """ if not isinstance(bound, mpc.array): raise TypeError( 'bound must be of type array - not ' + str(type(bound))) if bound.symbolic: raise ValueError( 'bound has to be purely numeric,' + ' but also has symbolic entries') if not bound.dim[1] == 1: if not bound.dim[0] == 1: print(bound.dim) raise ValueError( 'bound must be a flat array' + ' but booth dimnensions are greter than 1') else: bound = bound.transpose() if not isinstance(bnd_type, str): raise TypeError( 'bnd_type must be of type string - not ' + str(type(bnd_type))) if not bnd_type in ['upper', 'lower']: raise ValueError( 'bnd_type has to be upper or lower - not ' + bnd_type) if not isinstance(variable, str): raise TypeError( 'variable must be of type string - not ' + str(type(variable))) if not variable in ['state', 'control','terminal']: raise ValueError( 'variable has to be state, control or terminal - not ' + variable) if variable == 'state': if bnd_type == 'lower': self._lower_bndx = bound elif bnd_type == 'upper': self._upper_bndx = bound elif variable == 'control': if bnd_type == 'lower': self._lower_bndu = bound elif bnd_type == 'upper': self._upper_bndu = bound elif variable == 'terminal': if bnd_type == 'lower': self._lower_bndend = bound elif bnd_type == 'upper': self._upper_bndend = bound def add_constr(self, cons_type, *args): """Add linear or nonlinear constraints to the OCP. Nonlinear inequality constraints are of the form .. math:: g(t,x,u) \geq 0 \quad \\text{or} \quad g(x,u) \geq 0. Nonlinear equality constraints are of the form .. math:: h(t,x,u) = 0 \quad \\text{or} \quad h(x,u) = 0. Linear inequality constraints are of the form .. math:: Ex + Fu \geq h. Linear equality constraints are of the form .. math:: Ex + Fu = h. For the form of terminal constrains see :py:meth:`~add_terminalconstr`. Parameters ---------- cons_type : str String that defines the type of the constraints. Possible values are *eq*, *ineq*, *terminal_eq* and *terminal_ineq*. *args : callable or arrays Function defining the (nonlinear) constraints or arrays defining the linear constraints. In the letter case the order of arguments are E, F, h and if h is undefined this array is set to zero. For example >>> constraints.add_constr('ineq', E, F, h) will add a linear inequality constraint to :py:attr:`linear_constr` while >>> constraints.add_constr('terminal_eq',h_end) will add a nonlinear equality terminal constraint to :py:attr:`nonlinear_constr`. """ if cons_type == 'terminal_eq': self.add_terminalconstr('eq', *args) return None elif cons_type == 'terminal_ineq': self.add_terminalconstr('ineq', *args) return None if not isinstance(cons_type, str): raise TypeError() if cons_type not in ['eq', 'ineq']: raise ValueError() if len(args) == 1: linear = False if callable(args[0]): sig = signature(args[0]) params = sig.parameters if len(params) == 2: cons = lambda t,x,u : args[0](x, u) elif len(params) == 3: cons = args[0] else: raise ValueError( 'callable non-terminal constraints must have' + ' two or three input arguments - not ' + str(len(params))) else: raise TypeError( 'constraints must be defined by a callable function ' + 'or as linear constraints by two arrays - not by ' + str(type(args[0]))) elif len(args) == 2: linear = True for i in range(len(args)): if not isinstance(args[i], mpc.array): raise TypeError( 'linear constraints must be defined by arrays - not ' + str(type(args[i]))) h = np.zeros(args[0].dim[0]) args = (args[0], args[1], h) elif len(args) == 3: linear = True for i in range(len(args)): if not isinstance(args[i], mpc.array): raise TypeError( 'linear constraints must be defined by arrays - not ' + str(type(args[i]))) else: raise ValueError( 'given constraint must be a callable or two arrays') if linear: self._linear_constr[cons_type] += [args] else: self._nonlinear_constr[cons_type] += [mpc_convert(cons)] self._type = 'NLP' def add_terminalconstr(self, cons_type, *args): """Add linear or nonlinear terminal constraints to the OCP. Nonlinear terminal inequality constraints are of the form .. math:: g(t,x) \geq 0 \quad \\text{or} \quad g(x) \geq 0. Nonlinear terminal equality constraints are of the form .. math:: h(t,x) = 0 \quad \\text{or} \quad h(x) = 0. Linear terminal inequality constraints are of the form .. math:: Ex \geq h. Linear terminal equality constraints are of the form .. math:: Ex = h. Parameters ---------- cons_type : str String that defines the type of the terminal constraints. Possible values are *eq* or *ineq*. *args : callable or arrays Function defining the (nonlinear) terminal constraints or arrays defining the linear constraints. In the letter case the order of arguments are E, h and if h is undefined this array is set to zero. For example >>> constraints.add_terminalconstr('ineq', E, F, h) will add a linear inequality terminal constraint to :py:attr:`linear_constr` while >>> constraints.add_constr('eq',h) will add a nonlinear equality terminal constraint to :py:attr:`nonlinear_constr`. """ if not isinstance(cons_type, str): raise TypeError() if cons_type not in ['eq', 'ineq']: raise ValueError() if len(args) > 2: print(args) raise TypeError( 'terminal constraints must be defined by a callable function ' + 'or as linear constraints by an arrays') if len(args) == 1: if callable(args[0]): linear = False sig = signature(args[0]) params = sig.parameters if len(params) == 1: cons = lambda t,x : args[0](x) elif len(params) == 2: cons = args[0] else: raise ValueError( 'callable terminal constraints must have' + ' one or two input arguments - not ' + str(len(params))) elif isinstance(args[0], mpc.array): linear = True h = np.zeros(args[0].dim[0]) args = (args[0], h) else: raise ValueError( 'given constraint must be a callable or arrays') elif len(args) == 2: linear = True for i in range(len(args)): if not isinstance(args[i], mpc.array): raise TypeError( 'linear constraints must be defined by arrays - not ' + str(type(args[i]))) else: raise ValueError('given constraint must be a callable or arrays') cons_type = 'terminal_' + cons_type if linear: self._linear_constr[cons_type] += [args] else: self._nonlinear_constr[cons_type] += [mpc_convert(cons)] self._type = 'NLP' def save(self, path): """Saving the constraints to a given file with `dill <https://dill.readthedocs.io/en/latest/dill.html>`_. The path can be absolut or relative and the ending of the file is arbitrary. Parameters ---------- path : str String defining the path to the desired file. For example >>> constraints.save('constraints.pickle') will create a file `constraints.pickle` containing the nMPyC constraints object. """ with open(path, "wb") as output_file: dill.dump(self, output_file, -1) @classmethod def load(cls, path): """Loads a nMPyC constraints object from a file. The specified path must lead to a file that was previously saved with :py:meth:`~save`. Parameters ---------- path : str String defining the path to the file containing the nMPyC constraints object. For example >>> constraints.load('constraints.pickle') will load the constraints previously saved with :py:meth:`~save`. """ try: with open(path, "rb") as input_file: e = dill.load(input_file) except: raise Exception( 'Can not load constraints from file. File not readable!') if not isinstance(e, constraints): raise Exception( 'Can not load constraints from file. File does not cotain constraints!') return e