from dataclasses import dataclass
import casadi as cs
from typing import Union, Optional, Tuple
import numpy as np
from copy import copy
from ..casadi_loader import generate_and_compile_casadi_problem
from ..alpaqa import CasADiProblem
import inspect
[docs]
@dataclass
class MinimizationProblemDescription:
"""
High-level description of a minimization problem.
"""
objective_expr: Union[cs.SX, cs.MX]
variable: Union[cs.SX, cs.MX]
constraints_expr: Optional[Union[cs.SX, cs.MX]] = None
penalty_constraints_expr: Optional[Union[cs.SX, cs.MX]] = None
parameter: Optional[Union[cs.SX, cs.MX]] = None
parameter_value: Optional[np.ndarray] = None
regularizer: Optional[Union[float, np.ndarray]] = None
bounds: Optional[Tuple[np.ndarray, np.ndarray]] = None
constraints_bounds: Optional[Tuple[np.ndarray, np.ndarray]] = None
penalty_constraints_bounds: Optional[Tuple[np.ndarray, np.ndarray]] = None
@staticmethod
def _assert_not_set_before(value):
if value is not None:
caller_frame = inspect.getouterframes(inspect.currentframe())[1]
raise ValueError(f"{caller_frame.function} cannot be called twice")
[docs]
def subject_to_box(self, C: Tuple[np.ndarray, np.ndarray]):
"""
Add box constraints :math:`x \\in C` on the problem variables.
"""
self._assert_not_set_before(self.bounds)
ret = copy(self)
ret.bounds = C
return ret
[docs]
def subject_to(
self,
g: Union[cs.SX, cs.MX],
D: Optional[Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]] = None,
):
"""
Add general constraints :math:`g(x) \\in D`, handled using an augmented
Lagrangian method.
"""
self._assert_not_set_before(self.constraints_expr)
self._assert_not_set_before(self.constraints_bounds)
if D is not None and not isinstance(D, tuple):
D = D, D
ret = copy(self)
ret.constraints_expr = g
ret.constraints_bounds = D
return ret
[docs]
def subject_to_penalty(
self,
g: Union[cs.SX, cs.MX],
D: Optional[Union[np.ndarray, Tuple[np.ndarray, np.ndarray]]] = None,
):
"""
Add general constraints :math:`g(x) \\in D`, handled using a quadratic
penalty method.
"""
self._assert_not_set_before(self.penalty_constraints_expr)
self._assert_not_set_before(self.penalty_constraints_bounds)
if D is not None and not isinstance(D, tuple):
D = D, D
ret = copy(self)
ret.penalty_constraints_expr = g
ret.penalty_constraints_bounds = D
return ret
[docs]
def with_l1_regularizer(self, λ: Union[float, np.ndarray]):
"""
Add an :math:`\\ell_1`-regularization term :math:`\\|\\lambda x\\|_1`
to the objective.
"""
self._assert_not_set_before(self.regularizer)
ret = copy(self)
ret.regularizer = λ
return ret
[docs]
def with_param(self, p: Union[cs.SX, cs.MX], value: np.ndarray = None):
"""
Make the problem depend on a symbolic parameter, with an optional
default value. The value can be changed after the problem has been
loaded, as wel as in between solves.
"""
self._assert_not_set_before(self.parameter)
ret = copy(self)
ret.parameter = p
if value is not None:
ret.parameter_value = value
return ret
[docs]
def with_param_value(self, value: np.ndarray):
"""
Explicitly change the parameter value for the parameter added by
:py:func:`with_param`.
"""
if self.parameter is None:
raise RuntimeError("problem has no parameters")
ret = copy(self)
ret.parameter_value = value
return ret
[docs]
def compile(self, **kwargs) -> CasADiProblem:
"""
Generate, compile and load the problem.
:param \\**kwargs:
Arguments passed to :py:func:`alpaqa.casadi_loader.generate_and_compile_casadi_problem`.
:Keyword Arguments:
* **second_order**: ``str`` --
Whether to generate functions for evaluating second-order
derivatives:
* ``'no'``: only first-order derivatives (default).
* ``'full'``: Hessians and Hessian-vector products of the
Lagrangian and the augmented Lagrangian.
* ``'prod'``: Hessian-vector products of the
Lagrangian and the augmented Lagrangian.
* ``'L'``: Hessian of the Lagrangian.
* ``'L_prod'``: Hessian-vector product of the Lagrangian.
* ``'psi'``: Hessian of the augmented Lagrangian.
* ``'psi_prod'``: Hessian-vector product of the augmented
Lagrangian.
* **name**: ``str`` --
Optional string description of the problem (used for filenames).
* **sym**: ``Callable`` --
Symbolic variable constructor, usually either ``cs.SX.sym``
(default) or ``cs.MX.sym``.
"""
# Function arguments (variables and parameters)
x, param = self.variable, self.parameter
args = [cs.vec(x), cs.vec(param)] if param is not None else [cs.vec(x)]
# Objective and constraints functions
f = [self.objective_expr]
g_qpm = self.penalty_constraints_expr
g_alm = self.constraints_expr
g = cs.vertcat(*(g for g in (g_qpm, g_alm) if g is not None))
g = [] if g.shape == (0, 0) else [g]
# Problem dimensions
n = cs.vec(x).shape[0]
p = cs.vec(param).shape[0] if param is not None else 0
m_qpm = g_qpm.shape[0] if g_qpm is not None else 0
m_alm = g_alm.shape[0] if g_alm is not None else 0
# Bound constraints
C = self.bounds
if C is None:
C = (-np.inf * np.ones(n), +np.inf * np.ones(n))
# General quadratic penalty method constraint set
D_qpm = self.penalty_constraints_bounds
if D_qpm is None:
D_qpm = (-np.inf * np.ones(m_qpm), +np.inf * np.zeros(m_qpm))
# General augmented Lagrangian method constraint set
D_alm = self.constraints_bounds
if D_alm is None:
D_alm = (-np.inf * np.ones(m_alm), +np.inf * np.zeros(m_alm))
num_param = self.parameter_value
if num_param is None:
num_param = np.NaN * np.ones(p)
λ = self.regularizer
problem = generate_and_compile_casadi_problem(
f=cs.Function("f", args, f),
g=cs.Function("g", args, g),
C=C,
D=np.hstack((D_qpm, D_alm)),
param=num_param,
l1_reg=λ,
penalty_alm_split=m_qpm,
**kwargs,
)
return problem
[docs]
def minimize(
f: Union[cs.SX, cs.MX], x: Union[cs.SX, cs.MX]
) -> MinimizationProblemDescription:
"""
Formulate a minimization problem with objective function :math:`f(x)` and
unknown variables :math:`x`.
"""
return MinimizationProblemDescription(objective_expr=f, variable=x)
__all__ = [
"MinimizationProblemDescription",
"minimize",
]