Source code for mpcrl.util.math

"""A collection of functions for mathematical operations and utilities. In particular,
these functions support the creation of monomial basis functions for approximating the
value function, and the modifications of Hessian matrices to positive-definite ones."""

from itertools import combinations as _combinations
from typing import Optional, Union
from typing import TypeVar as _TypeVar

import casadi as cs
import numpy as np
import numpy.typing as npt
from csnlp.util.math import prod as _prod
from scipy.linalg import get_lapack_funcs as _get_lapack_funcs
from scipy.special import comb as _comb

SymType = _TypeVar("SymType", cs.SX, cs.MX)
SymOrNumType = _TypeVar("SymOrNumType", cs.SX, cs.MX, cs.DM, np.ndarray)


[docs] def summarize_array( a: npt.NDArray[np.floating], precision: int = 3, ddof: int = 0, axis: Optional[int] = None, ) -> str: """Summarizes the stats of a given array, i.e., the shape, the minimum and maximum, and the mean and standard deviation. Parameters ---------- a : array The numerical array to summarize. precision : int, optional The decimal precision, by default ``3``. ddof : int, optional Degrees of freedom used to compute the standard deviation, by default ``0``. axis : int, optional The axis along which to summarize the array, by default ``None``. Returns ------- str The summarizing string. """ s = a.shape mean = a.mean(axis=axis) std = a.std(ddof=ddof, axis=axis) min = a.min(axis=axis) max = a.max(axis=axis) return ( f"s={s}, x∈[{min:.{precision}f}, {max:.{precision}f}], " f"μ={mean:.{precision}f}, σ={std:.{precision}f}" )
[docs] def cholesky_added_multiple_identity( A: npt.NDArray[np.floating], beta: float = 1e-3, maxiter: int = 1000 ) -> npt.NDArray[np.floating]: r"""Lower Cholesky factorization with added multiple of the identity matrix to ensure positive-definitiveness from Algorithm 3.3 in :cite:`nocedal_numerical_2006`. The basic idea is to add a multiple of the identity matrix to the original matrix unitl the factorization is successful, i.e., find :math:`\tau \ge 0` such that :math:`L L^\top = A + \tau I` is successful. Parameters ---------- A : array of double The 2D matrix to compute the cholesky factorization of. beta : float, optional Initial tolerance of the algorithm, by default ``1e-3``. maxiter : int, optional Maximum iterations of the algorithm, by default ``1000``. Returns ------- array of double The lower cholesky factorization of the modified ``A`` (with the addition of identity matrices to ensure that it is positive-definite). Raises ------ ValueError If the factorization is unsuccessful for the maximum number of iterations. """ a_min = np.diag(A).min() tau = 0 if a_min > 0 else -a_min + beta identity = np.eye(A.shape[0]) (potrf,) = _get_lapack_funcs(("potrf",), (A,)) # taken from scipy.linalg.cholesky for _ in range(maxiter): L, info = potrf(A + tau * identity, lower=True, overwrite_a=False, clean=True) if info == 0: return L if info < 0: raise ValueError( f"LAPACK reported an illegal value in {-info}-th argument on entry to " '"POTRF".' ) tau = max(1.05 * tau, beta) raise ValueError("Maximum iterations reached.")
[docs] def nchoosek(n: Union[int, npt.ArrayLike], k: int) -> Union[int, np.ndarray]: """Emulates `MATLAB's nchoosek <https://www.mathworks.com/help/matlab/ref/nchoosek.html>`_, and returns the binomial coefficient, i.e., the number of combinations of ``n`` items taken ``k`` at a time. If ``n`` is an array, then it is flatten and all possible combinations of its elements are returned. Parameters ---------- n : int or array_like Number of elements or array of elements to choose from. k : int Number of elements to choose. Returns ------- int or array Depending on the type of input ``n``, the output is either the total number of combinations or the combinations in a matrix. """ return ( _comb(n, k, exact=True) if isinstance(n, int) else np.vstack(list(_combinations(np.asarray(n).reshape(-1), k))) )
[docs] def monomial_powers(n: int, k: int) -> npt.NDArray[np.int_]: r"""Computes the powers of all ``n``-dimensional monomials of degree ``k``. In mathematical terms, consider the monomial :math:`\prod_{i=1}^n x_i^{p_i}`. Then, this functions returns all possible combinations of powers :math:`(p_1, p_2, \ldots, p_n)` such that their sum is equal to ``k``, i.e., :math:`\sum_{i=1}^n p_i = k`. Parameters ---------- n : int The number of monomial elements. k : int The degree of each monomial. Returns ------- 2d array of ints A 2D array containing in each row the power of each index in order to obtain the desired monomial of power ``k``. The shape of the array is :math:`(c, n)`, where :math:`c = \frac{(k + n - 1)!}{k!(n - 1)!}`. Examples -------- >>> from mpcrl.util.math import monomial_powers >>> monomial_powers(3, 2) array([[2, 0, 0], [1, 1, 0], [1, 0, 1], [0, 2, 0], [0, 1, 1], [0, 0, 2]]) """ # see https://en.wikipedia.org/wiki/Homogeneous_polynomial#Properties and # https://math.stackexchange.com/a/36251 m = nchoosek(k + n - 1, n - 1) dividers = np.column_stack( ( np.zeros((m, 1), int), np.vstack(nchoosek(np.arange(1, k + n), n - 1)), np.full((m, 1), k + n, int), ) ) return np.flipud(np.diff(dividers, axis=1) - 1).astype(int)
[docs] def monomials_basis_function(n: int, mindegree: int, maxdegree: int) -> cs.Function: r"""Creates a :class:`casadi.Function` made of monomials as bases in ``n`` variables and degrees from ``mindegree`` to ``maxdegree``. In mathematical terms, given an input vector :math:`s \in \mathbb{R^n}` and the minimum and maximum degrees :math:`m` and :math:`M`, consider the degree :math:`d_i=m+i` with :math:`i=0,\ldots,M-m`. Associated to this degree, there exist :math:`c_i = \frac{(d_i + n - 1)!}{d_i!(n - 1)!}` monomials of the form :math:`m_{i,k}(s) = \prod_{j=1}^n s_j^{p_{j,k}}, \ k = 1,\ldots,c_i` satisfying the condition :math:`\sum_{j=1}^n p_{j,k} = d_i` (see :func:`monomial_powers`). This function returns a basis vector :math:`\Phi : \mathbb{R}^n \rightarrow \mathbb{R}^{\sum_{i=0}^{M-m}{c_i}}` that stacks all of the aforementioned monomials: .. math:: \Phi(s) = \Bigr[ m_{0,1}(s), \ldots, m_{0,c_0}(s), m_{1,1}(s), \ldots, m_{1,c_1}(s), \ldots, m_{M-m,1}(s), \ldots, m_{M-m,c_{M-m}}(s) \Bigl]^\top Parameters ---------- n : int Dimension of the input vector. mindegree : int Minimum degree of monomials (included). maxdegree : int Maximum degree of monomials (included). Returns ------- :class:`casadi.Function` A casadi function of the form :math:`\Phi(s)`, where :math:`s \in \mathbb{R^n}` is the input and :math:`\Phi` the basis vector. Examples -------- >>> import casadi as cs >>> from mpcrl.util.math import monomial_powers >>> Phi = monomials_basis_function(3, 1, 2) >>> s = cs.SX.sym("s", Phi.size1_in(0), 1) >>> Phi(s) SX([s_0, s_1, s_2, sq(s_0), (s_0*s_1), (s_0*s_2), sq(s_1), (s_1*s_2), sq(s_2)]) """ s = cs.SX.sym("s", n, 1) # prod is faster with SX, and during runtime y = cs.vertcat( *( _prod(s**p) for k in range(mindegree, maxdegree + 1) for p in monomial_powers(n, k) ) ) return cs.Function("Phi", (s,), (y,), ("s",), ("Phi(s)",), {"cse": True})
[docs] def clip(x: SymOrNumType, lower: SymOrNumType, upper: SymOrNumType) -> SymOrNumType: """Clips variable ``x`` to the range defined by ``lower`` and ``upper``. Parameters ---------- x : casadi SX or MX or array-like The variable to clip. lower : casadi SX or MX or array-like The lower bound of the clipping. upper : casadi SX or MX or array-like The upper bound of the clipping. Returns ------- casadi SX or MX or array-like The clipped variable. """ return cs.fmax(lower, cs.fmin(upper, x))
[docs] def lie_derivative( ex: SymType, arg: SymType, field: SymType, order: int = 1 ) -> SymType: """Computes the Lie derivative of the expression ``ex`` with respect to the argument ``arg`` along the field ``field``. Parameters ---------- ex : casadi SX or MX Expression to compute the Lie derivative of. arg : casadi SX or MX Argument with respect to which to compute the Lie derivative. field : casadi SX or MX Field along which to compute the Lie derivative. order : int, optional Order (>= 1) of the Lie derivative, by default ``1``. Returns ------- casadi SX or MX The Lie derivative of the expression ``ex`` with respect to the argument ``arg`` along the field ``field``. """ deriv = cs.mtimes(cs.jacobian(ex, arg), field) if order <= 1: return deriv return lie_derivative(deriv, arg, field, order - 1)
[docs] def dual_norm(x: SymOrNumType, ord: float) -> SymOrNumType: r"""Computes the dual norm of a given vector ``x`` with respect to the given order ``ord``. The dual norm of :math:`x` is defined as .. math:: || x ||_q = \sup_{|| y ||_p \leq 1} y^\top x, where :math:`p` is specified by ``ord`` and :math:`q` is its conjugate power, i.e., :math:`1/p + 1/q = 1`. Parameters ---------- x : casadi SX, MX, DM, or array-like The input scalar or vector. If a scalar, the absolute value is returned. ord : float The order of the norm. Must be larger than or equal to 1. Returns ------- casadi SX, MX, or DM The dual norm of the input vector. Depending on the input type, the output is either symbolic or numeric. """ if getattr(x, "shape", ()) in ((), (1,), (1, 1)): return cs.fabs(x) if ord == 1: return cs.norm_inf(x) if ord == 2: return cs.norm_2(x) if np.isposinf(ord).item(): return cs.norm_1(x) dual_norm = ord / (ord - 1) return cs.power(cs.sum1(cs.power(cs.fabs(x), dual_norm)), 1 / dual_norm)