Source code for csnlp.util.math

"""A collection of stand-alone functions that implement some of the basic mathematical
operations that are not available in CasADi. The implementations are simple and thus
not optimized for performance. They are meant to be used as a fallback when the CasADi
really does not provide the required functionality.
"""

import decimal as D
import math
from itertools import product as _product
from typing import TYPE_CHECKING, Literal, Optional, TypeVar, Union

import casadi as cs
import numpy as np
import numpy.typing as npt

if TYPE_CHECKING:
    from ..nlps.nlp import Nlp

SymType = TypeVar("SymType", cs.SX, cs.MX)
SQRT2 = math.sqrt(2)
E = D.Decimal.exp(D.Decimal(1))
HALF = D.Decimal("0.5")
PI = D.Decimal(  # 258 decimal digits
    "3.14159265358979323846264338327950288419716939937510582097494459230781640628620899"
    "8628034825342117067982148086513282306647093844609550582231725359408128481117450284"
    "1027019385211055596446229489549303819644288109756659334461284756482337867831652712"
    "01909145648566"
)
SQRT2PI = (2 * PI).sqrt()
LNSQRT2PI = SQRT2PI.ln()


[docs] def log( x: Union[cs.SX, cs.MX, cs.DM], base: Union[None, cs.SX, cs.MX, cs.DM] = None ) -> Union[cs.SX, cs.MX, cs.DM]: r"""Computes the logarithm. With one argument, return the natural logarithm of :math:`x` (to base :math:`e`). With two arguments, return the logarithm of :math:`x` to the given base :math:`b`, calculated as :math:`\frac{\log x}{\log b}`. Parameters ---------- x : casadi.SX, MX or DM Value to compute the logarithm of. base : casadi.SX, MX or DM, optional Base of the logarithm, by default ``None``, so that the natural logarithm is computed. Returns ------- casadi.SX, MX or DM The logarithm of ``x`` with base ``base``, i.e., :math:`\log_{base} x`. """ if base is None: return cs.log(x) if base == 10 or base == 10.0: return cs.log10(x) return cs.log(x) / cs.log(base)
[docs] def prod( x: Union[cs.SX, cs.MX, cs.DM], axis: Optional[Literal[0, 1, -1, -2]] = None ) -> Union[cs.SX, cs.MX, cs.DM]: r"""Computes the product of all the elements in ``x`` (CasADi version of :func:`numpy.prod`). Parameters ---------- x : casadi.SX, MX or DM The variable whose entries must be multiplied together. axis : {0, 1, None, -1, -2} Axis or along which a product is performed. The default, ``axis=None``, will calculate the product of all the elements in the matrix. If axis is negative it counts from the last to the first axis. Returns ------- casadi.SX, MX or DM Product of the elements, i.e., :math:`\prod_{i=1}^{|x|}{x_i}`. """ if axis is None: x = cs.vec(x) axis = 0 # prod of vector elements if x.is_vector(): if x.shape[axis] == 1: return x if not isinstance(x, cs.MX): return cs.det(cs.diag(x)) # does not work with MX p = x[0] for i in range(1, x.shape[axis]): p *= x[i] return p # prod of matrix elements sum_ = cs.sum1 if (axis == 0 or axis == -2) else cs.sum2 n_negatives = sum_(x < 0) p = cs.exp(sum_(cs.log(cs.fabs(x)))) return cs.if_else(cs.remainder(n_negatives, 2) == 0.0, 1, -1) * p
[docs] def normal_cdf( x: Union[cs.SX, cs.MX, cs.DM], loc: Union[cs.SX, cs.MX, cs.DM] = 0, scale: Union[cs.SX, cs.MX, cs.DM] = 1, ) -> Union[cs.SX, cs.MX, cs.DM]: """Computes the cdf of a normal distribution (CasADi version of :data:`scipy.stats.norm`'s ``cdf`` method). Parameters ---------- x : casadi.SX, MX or DM The value at which the cdf is computed. loc : casadi.SX, MX or DM, optional Mean of the normal distribution. By default, ``loc=0``. scale : casadi.SX, MX or DM, optional Standard deviation of the normal distribution. By default, ``scale=0``. Returns ------- casadi.SX, MX or DM The cdf of the normal distribution. """ return 0.5 * (1 + cs.erf((x - loc) / SQRT2 / scale))
[docs] def normal_ppf( p: Union[cs.SX, cs.MX, cs.DM], loc: Union[cs.SX, cs.MX, cs.DM] = 0, scale: Union[cs.SX, cs.MX, cs.DM] = 1, ) -> Union[cs.SX, cs.MX, cs.DM]: """Computes the quantile (invese of :func:`norm_cdf`) of a normal distribution (CasADi version of :data:`scipy.stats.norm`'s ``ppf`` method). Parameters ---------- x : casadi.SX, MX or DM The value at which the quantile is computed. loc : casadi.SX, MX or DM, optional Mean of the normal distribution. By default, ``loc=0``. scale : casadi.SX, MX or DM, optional Standard deviation of the normal distribution. By default, ``scale=0``. Returns ------- casadi.SX, MX or DM The quantile of the normal distribution. """ return SQRT2 * scale * cs.erfinv(2 * p - 1) + loc
[docs] def repeat( a: Union[cs.SX, cs.MX, cs.DM], repeats: Union[int, tuple[int, int]] ) -> Union[cs.SX, cs.MX, cs.DM]: """Repeats elements in array. Parameters ---------- a : casadi.SX or MX or DM The array/matrix whose elements are to be repeated. repeats : int or tuple of 2 ints The number of repeats, in the first and/or second axis. Returns ------- casadi.SX, MX or DM Output array with repeated elements. """ return cs.kron(a, cs.GenDM_ones(repeats))
[docs] def norm_1( nlp: "Nlp[SymType]", name: str, x: SymType, ) -> SymType: r"""Computes the 1-norm of the vector in the context of an optimization problem, i.e., it converts the 1-norm into a linear programme formulation by introducing auxiliary variables and two constraints (see, e.g., [1]_), and returns the corresponding scalar objective. Parameters ---------- nlp : Nlp The optimization problem for which to compute the norm. name : str Name of the norm. Used to yield unique names of the auxiliary variable and constraints. x : casadi SX or MX The expression whose norm is to be computed. If not a vector, it is reshaped into one first. Returns ------- casadi SX or MX The corresponding value of the 1-norm. Raises ------ ValueError Raises if the given name is already in use. References ---------- .. [1] Boyd, S. and Vandenberghe, L., 2004. Convex optimization. Cambridge University Press. Examples -------- Consider the following optimization problem: .. math:: \min_{x} \lVert A x - b \rVert_1 It is well known [1]_ that this is equivalent to the LP .. math:: \begin{aligned} \min_{x, t} \quad & 1^\top t \\ \text{s.t.} \quad & A x - b \le t \\ & A x - b \ge -t \end{aligned} where :math:`t` are the auxiliary variables of the same size as `b`, and two auxiliary sets of new inequality constraints have been added (vector inequalities are understood component-wise). Instead of performing this conversion manually, it can be quickly achieved as: >>> import numpy as np >>> from csnlp import Nlp, util >>> m, n = np.random.randint(10, 20, size=2) >>> A = np.random.randn(n, m) >>> b = np.random.randn(n) >>> nlp = Nlp(sym_type) >>> x, _, _ = nlp.variable("x", (m, 1)) >>> nlp.minimize(util.math.norm_1(nlp, "some_name", A @ x - b)) >>> nlp.init_solver(solver="clp") >>> sol = nlp.solve() """ t, _, _ = nlp.variable(f"{name}_norm_1_aux_var", x.shape) nlp.constraint(f"{name}_norm_1_aux_con_lb", x, ">=", -t) nlp.constraint(f"{name}_norm_1_aux_con_ub", x, "<=", t) return cs.sum1(cs.vec(t))
[docs] def norm_inf( nlp: "Nlp[SymType]", name: str, x: SymType, ) -> SymType: r"""Computes the infinity-norm of the vector in the context of an optimization problem, i.e., it converts the inf-norm into a linear programme formulation by introducing an auxiliary variable and two constraints (see, e.g., [1]_), and returns the corresponding scalar objective. Parameters ---------- nlp : Nlp The optimization problem for which to compute the norm. name : str Name of the norm. Used to yield unique names of the auxiliary variable and constraints. x : casadi SX or MX The expression whose norm is to be computed. If not a vector, it is reshaped into one first. Returns ------- casadi SX or MX The corresponding value of the 1-norm. Raises ------ ValueError Raises if the given name is already in use. References ---------- .. [1] Boyd, S. and Vandenberghe, L., 2004. Convex optimization. Cambridge University Press. Examples -------- Consider the following optimization problem: .. math:: \min_{x} \lVert A x - b \rVert_\infty It is well known [1]_ that this is equivalent to the LP .. math:: \begin{aligned} \min_{x, t} \quad & t \\ \text{s.t.} \quad & A x - b \le t 1 \\ & A x - b \ge -t 1 \end{aligned} where :math:`t` is the scalar auxiliary variable, and two auxiliary sets of new inequality constraints have been added (vector inequalities are understood component-wise). Instead of performing this conversion manually, it can be quickly achieved as: >>> import numpy as np >>> from csnlp import Nlp, util >>> m, n = np.random.randint(10, 20, size=2) >>> A = np.random.randn(n, m) >>> b = np.random.randn(n) >>> nlp = Nlp(sym_type) >>> x, _, _ = nlp.variable("x", (m, 1)) >>> nlp.minimize(util.math.norm_inf(nlp, "some_name", A @ x - b)) >>> nlp.init_solver(solver="clp") >>> sol = nlp.solve() """ t, _, _ = nlp.variable(f"{name}_norm_inf_aux_var") nlp.constraint(f"{name}_norm_inf_aux_con_lb", x, ">=", -t) nlp.constraint(f"{name}_norm_inf_aux_con_ub", x, "<=", t) return t
[docs] def godfrey_coefficients( g: float, n: int, scaled: bool = True ) -> npt.NDArray[np.object_]: """Computes the coefficients of the Godfrey series for the Lanczos' approximation of the gamma function. Parameters ---------- g : int or float The constant :math:`g` in the Lanczos' approximation. n : int The number of coefficients to compute. prec : int, optional The precision of the computations for :class:`decimal.Decimal`, by default 128. Returns ------- array of decimal.Decimal Returns a 1D array of the coefficients of the Godfrey series. These are instances of :class:`decimal.Decimal`. Notes ----- Requires :mod:`scipy` to be installed. For important details, see - https://www.boost.org/doc/libs/1_87_0/libs/math/doc/html/math_toolkit/sf_gamma/lgamma.html - https://www.boost.org/doc/libs/1_87_0/libs/math/doc/html/math_toolkit/lanczos.html - https://www.mrob.com/pub/ries/lanczos-gamma.html """ # to increase precision # - we use python ints instead of numpy ints (that's the reason for dtype=object) # - we use decimals for the final computations instead of floats from scipy.special import factorial, factorial2 N = np.arange(2 * n) FAC = factorial(N, exact=True).astype(object) # compute Dc (ints) Dc = 2 * factorial2(2 * N[:n] - 1, exact=True) Dc[0] = Dc[1] # correct how factorial2(0) works # compute Dr (ints) elems = [-FAC[2 * k + 2] // (2 * FAC[k] * FAC[k + 1]) for k in range(n - 1)] Dr = np.asarray([1, *elems], dtype=object) # compute C (ints) C = np.empty((n, n), dtype=object) for r, c in _product(range(n), repeat=2): if c > r: C[r, c] = 0 elif r == 0 and c == 0: C[r, c] = 1 # C(1, 1) = 1 / 2 in reality else: sign = 1 if (r + c) % 2 == 0 else -1 C[r, c] = ( sign * 2 # times 2 * 4**c * r * FAC[r + c - 1] // FAC[r - c] // FAC[2 * c] ) # compute B (ints) B = np.empty((n, n), dtype=object) for r, c in _product(range(n), repeat=2): if r == 0: B[r, c] = 1 elif r > c: B[r, c] = 0 else: sign = 1 if (c - r) % 2 == 0 else -1 B[r, c] = sign * math.comb(c + r - 1, 2 * r - 1) # compute F and final coefficients (decimals) v = N[:n] G = D.Decimal(str(g)) F = 2 ** (-HALF) * (E / (2 * (v + G) + 1)) ** (v + HALF) # div 2 # compute coefficients coeffs = ((Dr.reshape(-1, 1) * B) @ (C * Dc)) @ F return coeffs * G.exp() / SQRT2PI if scaled else coeffs
[docs] def gammaln( z: Union[cs.SX, cs.MX, cs.DM], g: float, n: int, prec: int = 128 ) -> Union[cs.SX, cs.MX, cs.DM]: """Computes the logarithm of the gamma function using the Lanczos approximation. Only valid for non-negative real scalars. Parameters ---------- z : casadi.SX, MX or DM The value at which to compute the logarithm of the gamma function. g : int or float The constant :math:`g` in the Lanczos' approximation. n : int The number of coefficients to compute for the approximation. prec : int, optional The precision of the computations for :class:`decimal.Decimal`, by default 128. Returns ------- casadi.SX, MX or DM The value of the logarithm of the gamma function at ``z``. Notes ----- Requires :mod:`scipy` to be installed. For important details, see - https://www.boost.org/doc/libs/1_87_0/libs/math/doc/html/math_toolkit/sf_gamma/lgamma.html - https://www.boost.org/doc/libs/1_87_0/libs/math/doc/html/math_toolkit/lanczos.html - https://www.mrob.com/pub/ries/lanczos-gamma.html """ assert z.is_scalar(), "z must be a scalar" with D.localcontext() as ctx: ctx.prec = prec p = godfrey_coefficients(g, n, False) logLg_const = p[0].ln() A = 0.0 for i in range(1, len(p)): A += float(p[i] / p[0]) / (z - 1 + i) logLg = float(logLg_const) + cs.log1p(A) return (z - 0.5) * cs.log((z + g - 0.5) / math.e) + logLg
# cs.if_else(z < 1, cs.substitute(out, z, z + 1) - cs.log(z), out)
[docs] def digamma1p(z: Union[cs.SX, cs.MX, cs.DM], n: int) -> Union[cs.SX, cs.MX, cs.DM]: """Computes the digamma function evaluated at :math:`z+1` via asymptotic expansion. Only valid for non-negative real scalars. Parameters ---------- z : casadi.SX, MX or DM The value at which to compute the logarithm of the gamma function. n : int, optional The number of coefficients to compute for the approximation. Returns ------- casadi.SX, MX or DM The value of the digamma function at ``z``. Notes ----- Requires :mod:`scipy` to be installed. For important details, see - https://www.boost.org/doc/libs/1_87_0/libs/math/doc/html/math_toolkit/sf_gamma/digamma.html """ from scipy.special import bernoulli N_2 = 2 * np.arange(1, n + 1) B = bernoulli(2 * n)[2::2] z1p = z + 1 powers = cs.power(z1p, N_2) return cs.log1p(z) - 0.5 / z1p - cs.sum1(1 / ((N_2 / B) * powers))
[docs] def digamma(z: Union[cs.SX, cs.MX, cs.DM], n: int) -> Union[cs.SX, cs.MX, cs.DM]: """Computes the digamma function via asymptotic expansion. Only valid for non-negative real scalars. Parameters ---------- z : casadi.SX, MX or DM The value at which to compute the logarithm of the gamma function. n : int, optional The number of coefficients to compute for the approximation. Returns ------- casadi.SX, MX or DM The value of the digamma function at ``z``. Notes ----- Requires :mod:`scipy` to be installed. For important details, see - https://www.boost.org/doc/libs/1_87_0/libs/math/doc/html/math_toolkit/sf_gamma/digamma.html """ # we shift by one since digamma(z) = digamma(z + 1) - 1 / z, and the approximation # is not good for z < 1 return digamma1p(z, n) - 1 / z