Skip to content

kups.core.utils.math

Mathematical utilities for numerical computations.

This module provides specialized numerical algorithms including logarithmic factorial ratios, polynomial root finding, and optimized matrix operations for 3×3 matrices.

MatmulSide

Bases: StrEnum

Enumeration for matrix multiplication side.

Source code in src/kups/core/utils/math.py
class MatmulSide(StrEnum):
    """Enumeration for matrix multiplication side."""

    LEFT = "left"
    RIGHT = "right"

SquareMatrix

Bases: Protocol

Source code in src/kups/core/utils/math.py
@runtime_checkable
class SquareMatrix(Protocol):
    @property
    def array(self) -> Array:
        """Underlying array of shape `(..., 3, 3)` representing the matrix."""
        ...

    @overload
    def matmul(self, x: Array, *, side: MatmulSide | str = ...) -> Array: ...
    @overload
    def matmul(
        self, x: SquareMatrix, *, side: MatmulSide | str = ...
    ) -> SquareMatrix: ...
    def matmul(
        self, x: Array | SquareMatrix, *, side: MatmulSide | str = MatmulSide.RIGHT
    ) -> Array | SquareMatrix:
        """Matrix-vector or matrix-matrix multiplication.

        Args:
            x: Vector or matrix to multiply.
            side: Multiplication side:
                - `"right"`: Computes `x @ self`
                - `"left"`: Computes `self @ x`

        Returns:
            Result of the multiplication.
        """
        ...

    def det(self) -> Array:
        """Compute the determinant of the matrix.

        Returns:
            Determinant as a scalar array.
        """
        ...

    def inverse(self) -> SquareMatrix:
        """Compute the inverse of the matrix.

        Returns:
            Inverse of the matrix.
        """
        ...

    def scale(self, factor: Array) -> SquareMatrix:
        """Scale the matrix by a given factor.

        Args:
            factor: Scalar factor to scale the matrix.

        Returns:
            Scaled matrix.
        """
        ...

array property

Underlying array of shape (..., 3, 3) representing the matrix.

det()

Compute the determinant of the matrix.

Returns:

Type Description
Array

Determinant as a scalar array.

Source code in src/kups/core/utils/math.py
def det(self) -> Array:
    """Compute the determinant of the matrix.

    Returns:
        Determinant as a scalar array.
    """
    ...

inverse()

Compute the inverse of the matrix.

Returns:

Type Description
SquareMatrix

Inverse of the matrix.

Source code in src/kups/core/utils/math.py
def inverse(self) -> SquareMatrix:
    """Compute the inverse of the matrix.

    Returns:
        Inverse of the matrix.
    """
    ...

matmul(x, *, side=MatmulSide.RIGHT)

matmul(x: Array, *, side: MatmulSide | str = ...) -> Array
matmul(
    x: SquareMatrix, *, side: MatmulSide | str = ...
) -> SquareMatrix

Matrix-vector or matrix-matrix multiplication.

Parameters:

Name Type Description Default
x Array | SquareMatrix

Vector or matrix to multiply.

required
side MatmulSide | str

Multiplication side: - "right": Computes x @ self - "left": Computes self @ x

RIGHT

Returns:

Type Description
Array | SquareMatrix

Result of the multiplication.

Source code in src/kups/core/utils/math.py
def matmul(
    self, x: Array | SquareMatrix, *, side: MatmulSide | str = MatmulSide.RIGHT
) -> Array | SquareMatrix:
    """Matrix-vector or matrix-matrix multiplication.

    Args:
        x: Vector or matrix to multiply.
        side: Multiplication side:
            - `"right"`: Computes `x @ self`
            - `"left"`: Computes `self @ x`

    Returns:
        Result of the multiplication.
    """
    ...

scale(factor)

Scale the matrix by a given factor.

Parameters:

Name Type Description Default
factor Array

Scalar factor to scale the matrix.

required

Returns:

Type Description
SquareMatrix

Scaled matrix.

Source code in src/kups/core/utils/math.py
def scale(self, factor: Array) -> SquareMatrix:
    """Scale the matrix by a given factor.

    Args:
        factor: Scalar factor to scale the matrix.

    Returns:
        Scaled matrix.
    """
    ...

cubic_roots(coefficients)

Find all roots of a cubic polynomial using the companion matrix method.

Solves ax³ + bx² + cx + d = 0 by computing eigenvalues of the companion matrix. Returns all three roots (real or complex).

Parameters:

Name Type Description Default
coefficients Array

Array of shape (..., 4) containing [a, b, c, d] for each cubic polynomial.

required

Returns:

Type Description
Array

Array of shape (..., 3) containing the three roots. May be complex-valued.

Example
# Solve x^3 - 6x^2 + 11x - 6 = 0 (roots: 1, 2, 3)
coeffs = jnp.array([1.0, -6.0, 11.0, -6.0])
roots = cubic_roots(coeffs)
Note

This method is numerically stable and handles multiple polynomials in parallel via vectorization.

Source code in src/kups/core/utils/math.py
@jit
@vectorize(signature="(4)->(3)")
def cubic_roots(coefficients: Array) -> Array:
    """Find all roots of a cubic polynomial using the companion matrix method.

    Solves `ax³ + bx² + cx + d = 0` by computing eigenvalues of the companion
    matrix. Returns all three roots (real or complex).

    Args:
        coefficients: Array of shape `(..., 4)` containing `[a, b, c, d]` for each
            cubic polynomial.

    Returns:
        Array of shape `(..., 3)` containing the three roots. May be complex-valued.

    Example:
        ```python
        # Solve x^3 - 6x^2 + 11x - 6 = 0 (roots: 1, 2, 3)
        coeffs = jnp.array([1.0, -6.0, 11.0, -6.0])
        roots = cubic_roots(coeffs)
        ```

    Note:
        This method is numerically stable and handles multiple polynomials in
        parallel via vectorization.
    """
    a, b, c, d = coefficients
    C = jnp.array([[0, 0, -d / a], [1, 0, -c / a], [0, 1, -b / a]], dtype=jnp.float_)
    solutions = jnp.linalg.eigvals(C)
    return solutions

det_and_inverse_3x3(A)

Compute determinant and inverse of 3×3 matrices via the adjugate method.

Efficiently computes both the determinant and inverse of 3×3 matrices using explicit formulas for the cofactor matrix. More efficient than general matrix inversion for small matrices.

Parameters:

Name Type Description Default
A Array

Array of shape (..., 3, 3) containing 3×3 matrices.

required

Returns:

Type Description
tuple[Array, Array]

Tuple of (determinant, inverse): - determinant: Array of shape (...) containing scalar determinants. - inverse: Array of shape (..., 3, 3) containing inverted matrices.

Example
A = jnp.eye(3)
det, A_inv = det_and_inverse_3x3(A)
# det = 1.0, A_inv = identity matrix
Source code in src/kups/core/utils/math.py
@jit
@vectorize(signature="(3,3)->(),(3,3)")
def det_and_inverse_3x3(A: jax.Array) -> tuple[jax.Array, jax.Array]:
    """Compute determinant and inverse of 3×3 matrices via the adjugate method.

    Efficiently computes both the determinant and inverse of 3×3 matrices using
    explicit formulas for the cofactor matrix. More efficient than general
    matrix inversion for small matrices.

    Args:
        A: Array of shape `(..., 3, 3)` containing 3×3 matrices.

    Returns:
        Tuple of (determinant, inverse):
            - determinant: Array of shape `(...)` containing scalar determinants.
            - inverse: Array of shape `(..., 3, 3)` containing inverted matrices.

    Example:
        ```python
        A = jnp.eye(3)
        det, A_inv = det_and_inverse_3x3(A)
        # det = 1.0, A_inv = identity matrix
        ```
    """
    assert A.shape == (3, 3), "Input must be a 3x3 matrix"

    # Determinant
    det = (
        A[0, 0] * (A[1, 1] * A[2, 2] - A[1, 2] * A[2, 1])
        - A[0, 1] * (A[1, 0] * A[2, 2] - A[1, 2] * A[2, 0])
        + A[0, 2] * (A[1, 0] * A[2, 1] - A[1, 1] * A[2, 0])
    )
    # Cofactor matrix
    cofactor = jnp.array(
        [
            [
                (A[1, 1] * A[2, 2] - A[1, 2] * A[2, 1]),
                -(A[1, 0] * A[2, 2] - A[1, 2] * A[2, 0]),
                (A[1, 0] * A[2, 1] - A[1, 1] * A[2, 0]),
            ],
            [
                -(A[0, 1] * A[2, 2] - A[0, 2] * A[2, 1]),
                (A[0, 0] * A[2, 2] - A[0, 2] * A[2, 0]),
                -(A[0, 0] * A[2, 1] - A[0, 1] * A[2, 0]),
            ],
            [
                (A[0, 1] * A[1, 2] - A[0, 2] * A[1, 1]),
                -(A[0, 0] * A[1, 2] - A[0, 2] * A[1, 0]),
                (A[0, 0] * A[1, 1] - A[0, 1] * A[1, 0]),
            ],
        ]
    )
    # Adjugate is transpose of cofactor
    adjugate = cofactor.T
    # Inverse
    return det, adjugate / det

log_factorial_ratio(N, M)

Compute log(N!/M!) efficiently using the log-gamma function.

Uses the identity log(n!) = lgamma(n+1) to compute the ratio directly.

Parameters:

Name Type Description Default
N Array

Integer array (numerator factorials).

required
M Array

Integer array (denominator factorials).

required

Returns:

Type Description
Array

Result containing log(N!/M!) for each pair of elements.

Example
N = jnp.array([10, 5])
M = jnp.array([5, 8])
result = log_factorial_ratio(N, M)
# Computes [log(10!/5!), log(5!/8!)]
Source code in src/kups/core/utils/math.py
@jit
def log_factorial_ratio(N: Array, M: Array) -> Array:
    """Compute log(N!/M!) efficiently using the log-gamma function.

    Uses the identity `log(n!) = lgamma(n+1)` to compute the ratio directly.

    Args:
        N: Integer array (numerator factorials).
        M: Integer array (denominator factorials).

    Returns:
        Result containing `log(N!/M!)` for each pair of elements.

    Example:
        ```python
        N = jnp.array([10, 5])
        M = jnp.array([5, 8])
        result = log_factorial_ratio(N, M)
        # Computes [log(10!/5!), log(5!/8!)]
        ```
    """
    return jax.lax.lgamma(N + 1.0) - jax.lax.lgamma(M + 1.0)

logm(A, *, hermitian=False)

Compute the principal matrix logarithm via eigendecomposition.

Inverse of jax.scipy.linalg.expm: returns L with expm(L) == A for diagonalizable A. Diagonalizing \(A = V\,\mathrm{diag}(\lambda)\,V^{-1}\) gives \(\log A = V\,\mathrm{diag}(\log\lambda)\,V^{-1}\).

Parameters:

Name Type Description Default
A Array

Array of shape (..., n, n). Must be diagonalizable; the result is inaccurate for defective matrices (e.g. non-trivial Jordan blocks).

required
hermitian bool

If True, treat A as Hermitian/symmetric and use eigh. This gives a real result for positive-definite A and is differentiable. If False (default), eig handles general matrices but the result is complex and is not differentiable with respect to A, since JAX does not implement derivatives of non-symmetric eigenvectors.

False

Returns:

Type Description
Array

Array of shape (..., n, n) holding the principal logarithm. Real when

Array

hermitian and A is positive-definite, complex otherwise.

Example
A = jax.scipy.linalg.expm(B)
B_recovered = logm(A)  # ≈ B
Source code in src/kups/core/utils/math.py
@jit(static_argnames=("hermitian",))
def logm(A: Array, *, hermitian: bool = False) -> Array:
    r"""Compute the principal matrix logarithm via eigendecomposition.

    Inverse of `jax.scipy.linalg.expm`: returns `L` with `expm(L) == A` for
    diagonalizable `A`. Diagonalizing $A = V\,\mathrm{diag}(\lambda)\,V^{-1}$
    gives $\log A = V\,\mathrm{diag}(\log\lambda)\,V^{-1}$.

    Args:
        A: Array of shape `(..., n, n)`. Must be diagonalizable; the result is
            inaccurate for defective matrices (e.g. non-trivial Jordan blocks).
        hermitian: If True, treat `A` as Hermitian/symmetric and use `eigh`.
            This gives a real result for positive-definite `A` and is
            differentiable. If False (default), `eig` handles general matrices
            but the result is complex and is not differentiable with respect to
            `A`, since JAX does not implement derivatives of non-symmetric
            eigenvectors.

    Returns:
        Array of shape `(..., n, n)` holding the principal logarithm. Real when
        `hermitian` and `A` is positive-definite, complex otherwise.

    Example:
        ```python
        A = jax.scipy.linalg.expm(B)
        B_recovered = logm(A)  # ≈ B
        ```
    """
    if hermitian:
        w, V = jnp.linalg.eigh(A)
        return (V * jnp.log(w)[..., None, :]) @ jnp.conjugate(jnp.swapaxes(V, -1, -2))
    w, V = jnp.linalg.eig(A)
    return (V * jnp.log(w)[..., None, :]) @ jnp.linalg.inv(V)

next_higher_power(value, base=2.0)

Compute the next higher power of a given base for each element.

For each element in value, finds the smallest power of base that is greater than or equal to that element.

Parameters:

Name Type Description Default
value Array

Array of shape (...) containing input values.

required
base Array | float

Base for the power calculation (default is 2.0).

2.0

Returns:

Type Description
Array

Array of shape (...) containing the next higher powers.

Example
values = jnp.array([3, 5, 10])
result = next_higher_power(values, base=2)
# result = [4, 8, 16]
Source code in src/kups/core/utils/math.py
def next_higher_power(value: Array, base: Array | float = 2.0) -> Array:
    """Compute the next higher power of a given base for each element.

    For each element in `value`, finds the smallest power of `base` that is
    greater than or equal to that element.

    Args:
        value: Array of shape `(...)` containing input values.
        base: Base for the power calculation (default is 2.0).

    Returns:
        Array of shape `(...)` containing the next higher powers.

    Example:
        ```python
        values = jnp.array([3, 5, 10])
        result = next_higher_power(values, base=2)
        # result = [4, 8, 16]
        ```
    """
    value = jnp.ceil(value).astype(int)
    log_base = jnp.log(base)
    exponents = jnp.ceil(jnp.log(value) / log_base)
    result = jnp.power(base, exponents)
    # Linear growth for base <= 1
    return jnp.where(base <= 1, value, result).astype(int)

solve_affine_ode(A, b, x0, dt)

Solve the affine ODE \(\dot{x} = A\,x + b\) exactly over \([0, \Delta t]\).

Computes

\[x(\Delta t) = e^{A\,\Delta t}\,x_0 + \varphi_1(A\,\Delta t)\,\Delta t\,b, \qquad \varphi_1(M) = M^{-1}(e^{M} - I).\]

Uses the augmented-matrix trick (Al-Mohy & Higham 2011) so that a single matrix exponential delivers both terms and the formula is well-defined even when A has zero eigenvalues:

\[\exp\!\left(\Delta t \begin{bmatrix} A & b \\ 0 & 0 \end{bmatrix}\right) = \begin{bmatrix} e^{A\,\Delta t} & \varphi_1(A\,\Delta t)\,\Delta t\,b \\ 0 & 1 \end{bmatrix}.\]

Parameters:

Name Type Description Default
A Array

Coefficient matrix, shape (..., n, n). Any shape (triangular or general) is accepted; expm handles all cases.

required
b Array

Constant inhomogeneous term, shape (..., n).

required
x0 Array

Initial state, shape (..., n).

required
dt Array | float

Scalar timestep (broadcastable to the batch shape).

required

Returns:

Type Description
Array

State at dt, shape (..., n).

Source code in src/kups/core/utils/math.py
@jit
def solve_affine_ode(A: Array, b: Array, x0: Array, dt: Array | float) -> Array:
    r"""Solve the affine ODE $\dot{x} = A\,x + b$ exactly over $[0, \Delta t]$.

    Computes

    $$x(\Delta t) = e^{A\,\Delta t}\,x_0 + \varphi_1(A\,\Delta t)\,\Delta t\,b,
    \qquad \varphi_1(M) = M^{-1}(e^{M} - I).$$

    Uses the augmented-matrix trick (Al-Mohy & Higham 2011) so that a single
    matrix exponential delivers both terms and the formula is well-defined
    even when ``A`` has zero eigenvalues:

    $$\exp\!\left(\Delta t \begin{bmatrix} A & b \\ 0 & 0 \end{bmatrix}\right)
      = \begin{bmatrix} e^{A\,\Delta t} & \varphi_1(A\,\Delta t)\,\Delta t\,b \\ 0 & 1 \end{bmatrix}.$$

    Args:
        A: Coefficient matrix, shape ``(..., n, n)``. Any shape (triangular
            or general) is accepted; ``expm`` handles all cases.
        b: Constant inhomogeneous term, shape ``(..., n)``.
        x0: Initial state, shape ``(..., n)``.
        dt: Scalar timestep (broadcastable to the batch shape).

    Returns:
        State at ``dt``, shape ``(..., n)``.
    """
    n = x0.shape[-1]
    # Augmented matrix M = [[A, b], [0, 0]], shape (..., n+1, n+1)
    A_padded = jnp.concatenate([A, b[..., None]], axis=-1)  # (..., n, n+1)
    zero_row = jnp.zeros((*A.shape[:-2], 1, n + 1), dtype=A.dtype)
    M = jnp.concatenate([A_padded, zero_row], axis=-2)  # (..., n+1, n+1)
    E = jax.scipy.linalg.expm(M * dt)
    return jnp.einsum("...ij,...j->...i", E[..., :n, :n], x0) + E[..., :n, n]

triangular_3x3_det_and_inverse(A, *, lower=True)

Compute determinant and inverse of triangular 3×3 matrices.

Exploits triangular structure with closed-form expressions: the determinant is the product of diagonal elements and the inverse follows from forward substitution. Upper-triangular inputs are handled by transposition.

Parameters:

Name Type Description Default
A Array

Array of shape (..., 3, 3) containing triangular matrices.

required
lower bool

Whether matrices are lower (True) or upper (False) triangular.

True

Returns:

Type Description
tuple[Array, Array]

Tuple of (determinant, inverse): - determinant: Array of shape (...) containing diagonal products. - inverse: Array of shape (..., 3, 3) containing inverted matrices.

Example
L = jnp.array([[1, 0, 0], [2, 3, 0], [4, 5, 6]])
det, L_inv = triangular_3x3_det_and_inverse(L, lower=True)
# det = 18 (product of diagonals: 1*3*6)
Source code in src/kups/core/utils/math.py
@jit(static_argnames=("lower",))
def triangular_3x3_det_and_inverse(
    A: jax.Array, *, lower: bool = True
) -> tuple[jax.Array, jax.Array]:
    """Compute determinant and inverse of triangular 3×3 matrices.

    Exploits triangular structure with closed-form expressions: the determinant
    is the product of diagonal elements and the inverse follows from forward
    substitution. Upper-triangular inputs are handled by transposition.

    Args:
        A: Array of shape `(..., 3, 3)` containing triangular matrices.
        lower: Whether matrices are lower (True) or upper (False) triangular.

    Returns:
        Tuple of (determinant, inverse):
            - determinant: Array of shape `(...)` containing diagonal products.
            - inverse: Array of shape `(..., 3, 3)` containing inverted matrices.

    Example:
        ```python
        L = jnp.array([[1, 0, 0], [2, 3, 0], [4, 5, 6]])
        det, L_inv = triangular_3x3_det_and_inverse(L, lower=True)
        # det = 18 (product of diagonals: 1*3*6)
        ```
    """
    if not lower:
        det, inv = triangular_3x3_det_and_inverse(jnp.swapaxes(A, -1, -2), lower=True)
        return det, jnp.swapaxes(inv, -1, -2)

    a = A[..., 0, 0]
    b = A[..., 1, 0]
    c = A[..., 1, 1]
    d = A[..., 2, 0]
    e = A[..., 2, 1]
    f = A[..., 2, 2]

    det = a * c * f
    m00, m11, m22 = 1.0 / a, 1.0 / c, 1.0 / f
    m10 = -b / (a * c)
    m21 = -e / (c * f)
    m20 = (e * b - c * d) / (a * c * f)

    zero = jnp.zeros_like(a)
    inv = jnp.stack(
        [
            jnp.stack([m00, zero, zero], axis=-1),
            jnp.stack([m10, m11, zero], axis=-1),
            jnp.stack([m20, m21, m22], axis=-1),
        ],
        axis=-2,
    )
    return det, inv

triangular_3x3_expm(A, *, lower=True)

Matrix exponential of triangular 3×3 matrices.

Closed form from the fact that B and A = expm(B) commute: the diagonal of A is exp of B's diagonal, and the off-diagonal couplings are divided differences of exp over the diagonal entries. Using divided differences keeps repeated diagonal entries (e.g. cubic / tetragonal cells) well-conditioned.

Real and gradient-safe inverse of triangular_3x3_logm; agrees with jax.scipy.linalg.expm on triangular matrices but avoids its Padé scaling-and-squaring iteration.

Parameters:

Name Type Description Default
A Array

Array of shape (..., 3, 3) containing triangular matrices.

required
lower bool

Whether matrices are lower (True) or upper (False) triangular.

True

Returns:

Type Description
Array

Array of shape (..., 3, 3) containing the triangular exponentials.

Source code in src/kups/core/utils/math.py
@jit(static_argnames=("lower",))
def triangular_3x3_expm(A: Array, *, lower: bool = True) -> Array:
    r"""Matrix exponential of triangular 3×3 matrices.

    Closed form from the fact that ``B`` and ``A = expm(B)`` commute: the diagonal
    of ``A`` is ``exp`` of ``B``'s diagonal, and the off-diagonal couplings are
    divided differences of ``exp`` over the diagonal entries. Using divided
    differences keeps repeated diagonal entries (e.g. cubic / tetragonal cells)
    well-conditioned.

    Real and gradient-safe inverse of
    [triangular_3x3_logm][kups.core.utils.math.triangular_3x3_logm]; agrees with
    ``jax.scipy.linalg.expm`` on triangular matrices but avoids its Padé
    scaling-and-squaring iteration.

    Args:
        A: Array of shape `(..., 3, 3)` containing triangular matrices.
        lower: Whether matrices are lower (True) or upper (False) triangular.

    Returns:
        Array of shape `(..., 3, 3)` containing the triangular exponentials.
    """
    M = A if lower else jnp.swapaxes(A, -1, -2)
    d0, d1, d2 = M[..., 0, 0], M[..., 1, 1], M[..., 2, 2]
    a, b, c = M[..., 1, 0], M[..., 2, 0], M[..., 2, 1]
    zero = jnp.zeros_like(d0)
    out = jnp.stack(
        [
            jnp.stack([jnp.exp(d0), zero, zero], axis=-1),
            jnp.stack([a * _ddexp(d0, d1), jnp.exp(d1), zero], axis=-1),
            jnp.stack(
                [
                    b * _ddexp(d0, d2) + a * c * _ddexp2(d0, d1, d2),
                    c * _ddexp(d1, d2),
                    jnp.exp(d2),
                ],
                axis=-1,
            ),
        ],
        axis=-2,
    )
    return out if lower else jnp.swapaxes(out, -1, -2)

triangular_3x3_from_tril(tril)

Assemble a lower-triangular 3×3 matrix from its 6 elements.

Parameters:

Name Type Description Default
tril Array

Array of shape (..., 6) holding [m00, m10, m11, m20, m21, m22].

required

Returns:

Type Description
Array

Array of shape (..., 3, 3)::

[[m00, 0, 0], [m10, m11, 0], [m20, m21, m22]]

Source code in src/kups/core/utils/math.py
def triangular_3x3_from_tril(tril: Array) -> Array:
    """Assemble a lower-triangular 3×3 matrix from its 6 elements.

    Args:
        tril: Array of shape `(..., 6)` holding ``[m00, m10, m11, m20, m21, m22]``.

    Returns:
        Array of shape `(..., 3, 3)`::

            [[m00,   0,   0],
             [m10, m11,   0],
             [m20, m21, m22]]
    """
    zero = jnp.zeros_like(tril[..., :1])
    return jnp.stack(
        [
            jnp.concatenate([tril[..., 0:1], zero, zero], axis=-1),
            jnp.concatenate([tril[..., 1:3], zero], axis=-1),
            tril[..., 3:6],
        ],
        axis=-2,
    )

triangular_3x3_logm(A, *, lower=True)

Principal matrix logarithm of triangular 3×3 matrices with positive diagonal.

Closed form from the fact that L and B = logm(L) commute (BL = LB): the diagonal of B is log of L's diagonal, and the off-diagonal couplings are divided differences of log over the diagonal entries. Using divided differences keeps repeated diagonal entries (e.g. cubic / tetragonal cells) well-conditioned.

Real and gradient-safe, unlike the eigendecomposition-based logm which is complex and inaccurate for these (defective) matrices. Inverse of jax.scipy.linalg.expm on triangular matrices.

Parameters:

Name Type Description Default
A Array

Array of shape (..., 3, 3) containing triangular matrices with strictly positive diagonal.

required
lower bool

Whether matrices are lower (True) or upper (False) triangular.

True

Returns:

Type Description
Array

Array of shape (..., 3, 3) containing the triangular logarithms.

Source code in src/kups/core/utils/math.py
@jit(static_argnames=("lower",))
def triangular_3x3_logm(A: Array, *, lower: bool = True) -> Array:
    r"""Principal matrix logarithm of triangular 3×3 matrices with positive diagonal.

    Closed form from the fact that ``L`` and ``B = logm(L)`` commute (``BL = LB``):
    the diagonal of ``B`` is ``log`` of ``L``'s diagonal, and the off-diagonal
    couplings are divided differences of ``log`` over the diagonal entries. Using
    divided differences keeps repeated diagonal entries (e.g. cubic / tetragonal
    cells) well-conditioned.

    Real and gradient-safe, unlike the eigendecomposition-based
    [logm][kups.core.utils.math.logm] which is complex and inaccurate for these
    (defective) matrices. Inverse of ``jax.scipy.linalg.expm`` on triangular
    matrices.

    Args:
        A: Array of shape `(..., 3, 3)` containing triangular matrices with
            strictly positive diagonal.
        lower: Whether matrices are lower (True) or upper (False) triangular.

    Returns:
        Array of shape `(..., 3, 3)` containing the triangular logarithms.
    """
    M = A if lower else jnp.swapaxes(A, -1, -2)
    l0, l1, l2 = M[..., 0, 0], M[..., 1, 1], M[..., 2, 2]
    a, b, c = M[..., 1, 0], M[..., 2, 0], M[..., 2, 1]
    zero = jnp.zeros_like(l0)
    out = jnp.stack(
        [
            jnp.stack([jnp.log(l0), zero, zero], axis=-1),
            jnp.stack([a * _ddlog(l0, l1), jnp.log(l1), zero], axis=-1),
            jnp.stack(
                [
                    b * _ddlog(l0, l2) + a * c * _ddlog2(l0, l1, l2),
                    c * _ddlog(l1, l2),
                    jnp.log(l2),
                ],
                axis=-1,
            ),
        ],
        axis=-2,
    )
    return out if lower else jnp.swapaxes(out, -1, -2)

triangular_3x3_matmul(L, x, *, lower=True, side=MatmulSide.RIGHT)

Optimized matrix-vector multiplication for triangular 3×3 matrices.

Specialized implementation that exploits triangular structure to avoid computing with known-zero elements. On CPU, uses unrolled loops for better performance than einsum.

Parameters:

Name Type Description Default
L Array

Array of shape (..., 3, 3) containing triangular matrices.

required
x Array

Array of shape (..., 3) containing vectors to multiply.

required
lower bool

Whether L is lower (True) or upper (False) triangular.

True
side MatmulSide | str

Multiplication side: - "right": Computes xL - "left": Computes Lx

RIGHT

Returns:

Type Description
Array

Shape (..., 3) containing the result.

Example
L = jnp.array([[1, 0, 0], [2, 3, 0], [4, 5, 6]])
x = jnp.array([1.0, 2.0, 3.0])
result = triangular_3x3_matmul(L, x, lower=True, side="right")
# Computes L @ x efficiently
Note

Automatically selects between einsum (GPU) and unrolled loops (CPU) for optimal performance.

Source code in src/kups/core/utils/math.py
@jit(static_argnames=("lower", "side"))
def triangular_3x3_matmul(
    L: Array, x: Array, *, lower: bool = True, side: MatmulSide | str = MatmulSide.RIGHT
) -> Array:
    """Optimized matrix-vector multiplication for triangular 3×3 matrices.

    Specialized implementation that exploits triangular structure to avoid
    computing with known-zero elements. On CPU, uses unrolled loops for better
    performance than einsum.

    Args:
        L: Array of shape `(..., 3, 3)` containing triangular matrices.
        x: Array of shape `(..., 3)` containing vectors to multiply.
        lower: Whether `L` is lower (True) or upper (False) triangular.
        side: Multiplication side:
            - `"right"`: Computes `xL`
            - `"left"`: Computes `Lx`

    Returns:
        Shape `(..., 3)` containing the result.

    Example:
        ```python
        L = jnp.array([[1, 0, 0], [2, 3, 0], [4, 5, 6]])
        x = jnp.array([1.0, 2.0, 3.0])
        result = triangular_3x3_matmul(L, x, lower=True, side="right")
        # Computes L @ x efficiently
        ```

    Note:
        Automatically selects between einsum (GPU) and unrolled loops (CPU) for
        optimal performance.
    """
    side = MatmulSide(side)
    cuda = jax.devices()[0].device_kind == "cuda"

    @vectorize(signature="(3,3),(3)->(3)")
    def inner(L: Array, x: Array) -> Array:
        if cuda:
            if side is MatmulSide.RIGHT:
                return jnp.einsum("ji,j->i", L, x)
            else:
                return jnp.einsum("ij,j->i", L, x)
        # The following unrolled implementation is faster on CPU for small matrices
        if side is MatmulSide.RIGHT:
            if lower:
                L_0, L_1, L_2 = L[:, 0], L[1:, 1], L[2:, 2]
                x_0, x_1, x_2 = x, x[1:], x[2:]
            else:
                L_0, L_1, L_2 = L[:1, 0], L[:2, 1], L[:3, 2]
                x_0, x_1, x_2 = x[:1], x[:2], x
        elif side is MatmulSide.LEFT:
            if lower:
                L_0, L_1, L_2 = L[0, :1], L[1, :2], L[2, :3]
                x_0, x_1, x_2 = x[:1], x[:2], x
            else:
                L_0, L_1, L_2 = L[0, :], L[1, 1:], L[2, 2:]
                x_0, x_1, x_2 = x, x[1:], x[2:]
        else:
            raise ValueError(f"Invalid side argument: {side}")
        return jnp.stack([L_0 @ x_0, L_1 @ x_1, L_2 @ x_2])

    return inner(L, x)