def include_equations(self, *args, **kwargs): if callable(getattr(super(), 'include_equations', None)): super().include_equations(*args, **kwargs) ode = kwargs.pop('ode', None) x = kwargs.pop('x', None) if ode is None and x is not None: raise ValueError("`ode` is None but `x` is not None") # if is in the list form if isinstance(ode, collections.abc.Sequence): ode = vertcat(*ode) if isinstance(x, collections.abc.Sequence): x = vertcat(*x) # if ode was passed but not x, try to guess the x if x is None and ode is not None: # Check if None are all sequential, ortherwise we don't know who it belongs first_none = list(self._ode.values()).index(None) if not all(eq is None for eq in islice(self._ode.values(), 0, first_none)): raise ValueError( "ODE should be inserted on the equation form or in the list form." "You can't mix both without explicit passing the states associated with the equation." ) x = vertcat(*list(self._ode.keys())[first_none:first_none + ode.numel()]) if len(args) > 0 and ode is None: x = SX([]) ode = SX([]) # get ode and x from equality equations for eq in args: if isinstance(eq, EqualityEquation): if isinstance(eq.lhs, Derivative): ode = vertcat(ode, eq.rhs) x = vertcat(x, eq.lhs.inner) # actually include the equations if ode is not None and ode.numel() > 0: for x_i in vertcat(x).nz: if self._ode[x_i] is not None: raise Warning( f'State "{x_i}" already had an ODE associated, overriding it!' ) ode_dict = dict(self._ode) ode_dict.update({x_i: ode[ind] for ind, x_i in enumerate(x.nz)}) self._ode = ode_dict
class ParameterMixin: def __init__(self, **kwargs): super().__init__(**kwargs) self.p = SX([]) self.theta = SX([]) @property def n_p(self): return self.p.numel() @property def n_theta(self): return self.theta.numel() @property def p_names(self): return [self.p[i].name() for i in range(self.n_p)] @property def theta_names(self): return [self.theta[i].name() for i in range(self.n_theta)] def create_parameter(self, name="p", size=1): """ Create a new parameter name "name" and size "size" :param name: str :param size: int :return: """ if callable(getattr(self, 'name_variable', None)): name = self.name_variable(name) new_p = SX.sym(name, size) self.include_parameter(vec(new_p)) return new_p def create_theta(self, name="theta", size=1): """ Create a new parameter name "name" and size "size" :param name: str :param size: int :return: """ if callable(getattr(self, 'name_variable', None)): name = self.name_variable(name) new_theta = SX.sym(name, size) self.include_theta(vec(new_theta)) return new_theta def include_parameter(self, p): self.p = vertcat(self.p, p) def include_theta(self, theta): self.theta = vertcat(self.theta, theta) def remove_parameter(self, var): self.p = remove_variables_from_vector(var, self.p) def remove_theta(self, var): self.theta = remove_variables_from_vector(var, self.theta)
class ControlMixin: def __init__(self, **kwargs): super().__init__(**kwargs) self.u = SX([]) self._parametrized_controls = [] self.u_par = vertcat(self.u) self.u_expr = vertcat(self.u) @property def n_u(self): return self.u.numel() @property def n_u_par(self): return self.u_par.numel() @property def u_names(self): return [self.u[i].name() for i in range(self.n_u)] def create_control(self, name="u", size=1): """ Create a new control variable name "name" and size "size". Size can be an int or a tuple (e.g. (2,2)). However, the new control variable will be vectorized (casadi.vec) to be included in the control vector (model.u). :param name: str :param size: int :return: """ if callable(getattr(self, 'name_variable', None)): name = self.name_variable(name) new_u = SX.sym(name, size) self.include_control(vec(new_u)) return new_u def include_control(self, var): self.u = vertcat(self.u, var) self.u_expr = vertcat(self.u_expr, var) self.u_par = vertcat(self.u_par, var) def remove_control(self, var): self.u_expr = remove_variables_from_vector_by_indices( find_variables_indices_in_vector(var, self.u), self.u_expr) self.u = remove_variables_from_vector(var, self.u) self.u_par = remove_variables_from_vector(var, self.u_par) def replace_variable(self, original, replacement): if isinstance(original, list): original = vertcat(*original) if isinstance(replacement, list): replacement = vertcat(*replacement) if not original.numel() == replacement.numel(): raise ValueError( "Original and replacement must have the same number of elements!" "original.numel()={}, replacement.numel()={}".format( original.numel(), replacement.numel())) if callable(getattr(super(), 'replace_variable', None)): super().replace_variable(original, replacement) # self.u_par = substitute(self.u_par, original, replacement) self.u_expr = substitute(self.u_expr, original, replacement) def parametrize_control(self, u, expr, u_par=None): """ Parametrize a control variables so it is a function of a set of parameters or other model variables. :param list|casadi.SX u: :param list|casadi.SX expr: :param list|casadi.SX u_par: """ # input check if isinstance(u, list): u = vertcat(*u) if isinstance(u_par, list): u_par = vertcat(*u_par) if isinstance(expr, list): expr = vertcat(*expr) if not u.numel() == expr.numel(): raise ValueError( "Passed control and parametrization expression does not have same size. " "u ({}) and expr ({})".format(u.numel(), expr.numel())) # Check and register the control parametrization. for u_i in u.nz: if self.control_is_parametrized(u_i): raise ValueError( f'The control "{u_i}" is already parametrized.') # to get have a new memory address self._parametrized_controls = self._parametrized_controls + [u_i] # Remove u from u_par if they are going to be parametrized self.u_par = remove_variables_from_vector(u, self.u_par) if u_par is not None: self.u_par = vertcat(self.u_par, u_par) # Replace u by expr into the system self.replace_variable(u, expr) def create_input(self, name="u", size=1): """ Same as the "model.create_control" function. Create a new control/input variable name "name" and size "size". Size can be an int or a tuple (e.g. (2,2)). However, the new control variable will be vectorized (casadi.vec) to be included in the control vector (model.u). :param name: str :param size: int :return: """ return self.create_control(name, size) def control_is_parametrized(self, u): """ Check if the control "u" is parametrized :param casadi.SX u: :rtype bool: """ u = vertcat(u) if not u.numel() == 1: raise ValueError( 'The parameter "u" is expected to be of size 1x1, given: {}x{}' .format(*u.shape)) if any([ is_equal(u, parametrized_u) for parametrized_u in self._parametrized_controls ]): return True return False
class StateMixin: def __init__(self, **kwargs): super().__init__(**kwargs) self.x = SX([]) self.x_0 = SX([]) self._ode = dict() @property def n_x(self): return self.x.numel() @property def x_names(self): return [self.x[i].name() for i in range(self.n_x)] @property def ode(self): try: return vertcat( *[val for val in self._ode.values() if val is not None]) except NotImplementedError: return SX.zeros(0, 1) def create_state(self, name="x", size=1): """ Create a new state with the name "name" and size "size". Size can be an int or a tuple (e.g. (2,2)). However, the new state will be vectorized (casadi.vec) to be included in the state vector (model.x). :param name: str :param size: int|tuple :return: """ if callable(getattr(self, 'name_variable', None)): name = self.name_variable(name) new_x = SX.sym(name, size) new_x_0 = SX.sym(name + "_0_sym", size) self.include_state(vec(new_x), ode=None, x_0=vec(new_x_0)) return new_x def include_state(self, var, ode=None, x_0=None): n_x = var.numel() self.x = vertcat(self.x, var) if x_0 is None: x_0 = vertcat(*[SX.sym(var_i.name()) for var_i in var.nz]) self.x_0 = vertcat(self.x_0, x_0) # crate entry for included state for ind, x_i in enumerate(var.nz): if x_i in self._ode: raise ValueError(f'State "{x_i}" already in this model') self._ode[x_i] = None if ode is not None: self.include_equations(ode=ode, x=var) return x_0 def remove_state(self, var, eq=None): self.x = remove_variables_from_vector(var, self.x) for x_i in var.nz: del self._ode[x_i] def replace_variable(self, original, replacement): if isinstance(original, list): original = vertcat(*original) if isinstance(replacement, list): replacement = vertcat(*replacement) if not original.numel() == replacement.numel(): raise ValueError( "Original and replacement must have the same number of elements!" "original.numel()={}, replacement.numel()={}".format( original.numel(), replacement.numel())) if callable(getattr(super(), 'replace_variable', None)): super().replace_variable(original, replacement) if original.numel() > 0: for x_i, x_i_eq in self._ode.items(): self._ode[x_i] = substitute(x_i_eq, original, replacement) def include_equations(self, *args, **kwargs): if callable(getattr(super(), 'include_equations', None)): super().include_equations(*args, **kwargs) ode = kwargs.pop('ode', None) x = kwargs.pop('x', None) if ode is None and x is not None: raise ValueError("`ode` is None but `x` is not None") # if is in the list form if isinstance(ode, collections.abc.Sequence): ode = vertcat(*ode) if isinstance(x, collections.abc.Sequence): x = vertcat(*x) # if ode was passed but not x, try to guess the x if x is None and ode is not None: # Check if None are all sequential, ortherwise we don't know who it belongs first_none = list(self._ode.values()).index(None) if not all(eq is None for eq in islice(self._ode.values(), 0, first_none)): raise ValueError( "ODE should be inserted on the equation form or in the list form." "You can't mix both without explicit passing the states associated with the equation." ) x = vertcat(*list(self._ode.keys())[first_none:first_none + ode.numel()]) if len(args) > 0 and ode is None: x = SX([]) ode = SX([]) # get ode and x from equality equations for eq in args: if isinstance(eq, EqualityEquation): if isinstance(eq.lhs, Derivative): ode = vertcat(ode, eq.rhs) x = vertcat(x, eq.lhs.inner) # actually include the equations if ode is not None and ode.numel() > 0: for x_i in vertcat(x).nz: if self._ode[x_i] is not None: raise Warning( f'State "{x_i}" already had an ODE associated, overriding it!' ) ode_dict = dict(self._ode) ode_dict.update({x_i: ode[ind] for ind, x_i in enumerate(x.nz)}) self._ode = ode_dict
class AlgebraicMixin: def __init__(self, **kwargs): super().__init__(**kwargs) self.alg = SX([]) self.y = SX([]) @property def n_y(self): return self.y.numel() @property def y_names(self): return [self.y[i].name() for i in range(self.n_y)] def create_algebraic_variable(self, name="y", size=1): """ Create a new algebraic variable with the name "name" and size "size". Size can be an int or a tuple (e.g. (2,2)). However, the new algebraic variable will be vectorized (casadi.vec) to be included in the algebraic vector (model.y). :param str name: :param int||tuple size: :return: """ if callable(getattr(self, 'name_variable', None)): name = self.name_variable(name) new_y = SX.sym(name, size) self.include_algebraic(vec(new_y)) return new_y def find_algebraic_variable(self, x, u, guess=None, t=0.0, p=None, theta_value=None, rootfinder_options=None): if guess is None: guess = [1] * self.n_y if rootfinder_options is None: rootfinder_options = dict( nlpsol="ipopt", nlpsol_options=config.SOLVER_OPTIONS["nlpsol_options"]) if p is None: p = [] if theta_value is None: theta_value = [] # replace known variables alg = self.alg known_var = vertcat(self.t, self.x, self.u, self.p, self.theta) known_var_values = vertcat(t, x, u, p, theta_value) alg = substitute(alg, known_var, known_var_values) f_alg = Function("f_alg", [self.y], [alg]) rf = rootfinder("rf_algebraic_variable", "nlpsol", f_alg, rootfinder_options) res = rf(guess) return res def include_algebraic(self, var, alg=None): self.y = vertcat(self.y, var) self.include_equations(alg=alg) def remove_algebraic(self, var, eq=None): self.y = remove_variables_from_vector(var, self.y) if eq is not None: self.alg = remove_variables_from_vector(eq, self.alg) def replace_variable(self, original, replacement): if isinstance(original, list): original = vertcat(*original) if isinstance(replacement, list): replacement = vertcat(*replacement) if not original.numel() == replacement.numel(): raise ValueError( "Original and replacement must have the same number of elements!" "original.numel()={}, replacement.numel()={}".format( original.numel(), replacement.numel())) if callable(getattr(super(), 'replace_variable', None)): super().replace_variable(original, replacement) self.alg = substitute(self.alg, original, replacement) def include_equations(self, *args, **kwargs): if callable(getattr(super(), 'include_equations', None)): super().include_equations(*args, **kwargs) alg = kwargs.pop('alg', None) if len(args) > 0 and alg is None: alg = SX([]) # in case a list of equations `y == x + u` has been passed for eq in args: if is_equality(eq): alg = vertcat(alg, eq.dep(0) - eq.dep(1)) if isinstance(alg, collections.abc.Sequence): alg = vertcat(*alg) if alg is not None: self.alg = vertcat(self.alg, alg)