Skip to content

Linear Algebra

Operations from mechestim.linalg. Cost formulas vary per operation — see each function's docstring.

mechestim.linalg._svd

Truncated SVD with FLOP counting.

svd(a, k=None)

Truncated singular value decomposition.

Computes the top-k singular values and corresponding vectors of a 2-D matrix, wrapping numpy.linalg.svd with FLOP counting.

FLOP Cost

m × n × k FLOPs.

Parameters:

Name Type Description Default
a ndarray

Input matrix of shape (m, n).

required
k int

Number of singular values/vectors to return. Must satisfy 1 <= k <= min(m, n). If None, returns all min(m, n) components.

None

Returns:

Name Type Description
U ndarray

Left singular vectors, shape (m, k).

S ndarray

Singular values in descending order, shape (k,).

Vt ndarray

Right singular vectors (transposed), shape (k, n).

Raises:

Type Description
BudgetExhaustedError

If the operation would exceed the FLOP budget.

ValueError

If a is not 2-D or k is out of range.

Notes

mechestim cost: m × n × k FLOPs.

See Also

numpy.linalg.svd : Full NumPy SVD documentation.

Source code in src/mechestim/linalg/_svd.py
def svd(
    a: _np.ndarray, k: int | None = None
) -> tuple[_np.ndarray, _np.ndarray, _np.ndarray]:
    """Truncated singular value decomposition.

    Computes the top-*k* singular values and corresponding vectors
    of a 2-D matrix, wrapping ``numpy.linalg.svd`` with FLOP counting.

    FLOP Cost
    ---------
    m × n × k FLOPs.

    Parameters
    ----------
    a : numpy.ndarray
        Input matrix of shape ``(m, n)``.
    k : int, optional
        Number of singular values/vectors to return. Must satisfy
        ``1 <= k <= min(m, n)``. If ``None``, returns all ``min(m, n)``
        components.

    Returns
    -------
    U : numpy.ndarray
        Left singular vectors, shape ``(m, k)``.
    S : numpy.ndarray
        Singular values in descending order, shape ``(k,)``.
    Vt : numpy.ndarray
        Right singular vectors (transposed), shape ``(k, n)``.

    Raises
    ------
    BudgetExhaustedError
        If the operation would exceed the FLOP budget.
    ValueError
        If *a* is not 2-D or *k* is out of range.

    Notes
    -----
    **mechestim cost:** m × n × k FLOPs.

    See Also
    --------
    numpy.linalg.svd : Full NumPy SVD documentation.
    """
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2:
        raise ValueError(f"Input must be 2D, got {a.ndim}D")
    m, n = a.shape
    if k is None:
        k = min(m, n)
    if not (1 <= k <= min(m, n)):
        raise ValueError(f"k must satisfy 1 <= k <= min(m, n) = {min(m, n)}, got k={k}")
    cost = svd_cost(m, n, k)
    budget.deduct("linalg.svd", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    U, S, Vt = _np.linalg.svd(a, full_matrices=False)
    U = U[:, :k]
    S = S[:k]
    Vt = Vt[:k, :]
    check_nan_inf(S, "linalg.svd")
    return U, S, Vt

mechestim.linalg._decompositions

Matrix decomposition wrappers with FLOP counting.

Each operation has a co-located cost function documenting the formula, source, and assumptions. Cost functions are pure (shape params) -> int.

cholesky(a)

Cholesky decomposition with FLOP counting.

Source code in src/mechestim/linalg/_decompositions.py
def cholesky(a):
    """Cholesky decomposition with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    cost = cholesky_cost(n)
    budget.deduct("linalg.cholesky", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.cholesky(a)

cholesky_cost(n)

FLOP cost of Cholesky decomposition.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required

Returns:

Type Description
int

Estimated FLOP count: \(n^3 / 3\).

Notes

Source: Golub & Van Loan, Matrix Computations, 4th ed., §4.2. Assumes standard column-outer-product Cholesky algorithm.

Source code in src/mechestim/linalg/_decompositions.py
def cholesky_cost(n: int) -> int:
    """FLOP cost of Cholesky decomposition.

    Parameters
    ----------
    n : int
        Matrix dimension.

    Returns
    -------
    int
        Estimated FLOP count: $n^3 / 3$.

    Notes
    -----
    Source: Golub & Van Loan, *Matrix Computations*, 4th ed., §4.2.
    Assumes standard column-outer-product Cholesky algorithm.
    """
    return max(n**3 // 3, 1)

eig(a)

Eigendecomposition with FLOP counting.

Source code in src/mechestim/linalg/_decompositions.py
def eig(a):
    """Eigendecomposition with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    cost = eig_cost(n)
    budget.deduct("linalg.eig", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.eig(a)

eig_cost(n)

FLOP cost of eigendecomposition.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required

Returns:

Type Description
int

Estimated FLOP count: \(10n^3\).

Notes

Source: Golub & Van Loan, Matrix Computations, 4th ed., §7.5. Assumes Francis double-shift QR algorithm. The constant ~10 accounts for Hessenberg reduction (~\(10n^3/3\)) plus ~2 QR iterations per eigenvalue. This is an accepted asymptotic estimate; actual count is data-dependent.

Source code in src/mechestim/linalg/_decompositions.py
def eig_cost(n: int) -> int:
    """FLOP cost of eigendecomposition.

    Parameters
    ----------
    n : int
        Matrix dimension.

    Returns
    -------
    int
        Estimated FLOP count: $10n^3$.

    Notes
    -----
    Source: Golub & Van Loan, *Matrix Computations*, 4th ed., §7.5.
    Assumes Francis double-shift QR algorithm. The constant ~10 accounts
    for Hessenberg reduction (~$10n^3/3$) plus ~2 QR iterations per
    eigenvalue. This is an accepted asymptotic estimate; actual count
    is data-dependent.
    """
    return max(10 * n**3, 1)

eigh(a, UPLO='L')

Symmetric eigendecomposition with FLOP counting.

Source code in src/mechestim/linalg/_decompositions.py
def eigh(a, UPLO="L"):
    """Symmetric eigendecomposition with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    cost = eigh_cost(n)
    budget.deduct("linalg.eigh", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    vals, vecs = _np.linalg.eigh(a, UPLO=UPLO)
    return _np.asarray(vals), _np.asarray(vecs)

eigh_cost(n)

FLOP cost of symmetric eigendecomposition.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required

Returns:

Type Description
int

Estimated FLOP count: \((4/3)n^3\).

Notes

Source: Golub & Van Loan, Matrix Computations, 4th ed., §8.3. Assumes tridiagonalization via Householder followed by implicit QR sweeps.

Source code in src/mechestim/linalg/_decompositions.py
def eigh_cost(n: int) -> int:
    """FLOP cost of symmetric eigendecomposition.

    Parameters
    ----------
    n : int
        Matrix dimension.

    Returns
    -------
    int
        Estimated FLOP count: $(4/3)n^3$.

    Notes
    -----
    Source: Golub & Van Loan, *Matrix Computations*, 4th ed., §8.3.
    Assumes tridiagonalization via Householder followed by implicit
    QR sweeps.
    """
    return max((4 * n**3) // 3, 1)

eigvals(a)

Eigenvalues (nonsymmetric) with FLOP counting.

Source code in src/mechestim/linalg/_decompositions.py
def eigvals(a):
    """Eigenvalues (nonsymmetric) with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    cost = eigvals_cost(n)
    budget.deduct("linalg.eigvals", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.eigvals(a)

eigvals_cost(n)

FLOP cost of computing eigenvalues.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required

Returns:

Type Description
int

Estimated FLOP count: \(7n^3\).

Notes

Same algorithm as eig (Francis QR), but eigenvectors are not accumulated. Without back-accumulation of eigenvectors the cost is dominated by Hessenberg reduction (~(10/3)n^3) plus QR iterations (~4n^3), totalling ~7n^3.

Source code in src/mechestim/linalg/_decompositions.py
def eigvals_cost(n: int) -> int:
    """FLOP cost of computing eigenvalues.

    Parameters
    ----------
    n : int
        Matrix dimension.

    Returns
    -------
    int
        Estimated FLOP count: $7n^3$.

    Notes
    -----
    Same algorithm as ``eig`` (Francis QR), but eigenvectors are not
    accumulated. Without back-accumulation of eigenvectors the cost
    is dominated by Hessenberg reduction (~(10/3)n^3) plus QR iterations
    (~4n^3), totalling ~7n^3.
    """
    return max(7 * n**3, 1)

eigvalsh(a, UPLO='L')

Eigenvalues (symmetric) with FLOP counting.

Source code in src/mechestim/linalg/_decompositions.py
def eigvalsh(a, UPLO="L"):
    """Eigenvalues (symmetric) with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    cost = eigvalsh_cost(n)
    budget.deduct("linalg.eigvalsh", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.eigvalsh(a, UPLO=UPLO)

eigvalsh_cost(n)

FLOP cost of computing eigenvalues of a symmetric matrix.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required

Returns:

Type Description
int

Estimated FLOP count: \((4/3)n^3\).

Notes

Same algorithm as eigh, but eigenvectors are not accumulated.

Source code in src/mechestim/linalg/_decompositions.py
def eigvalsh_cost(n: int) -> int:
    """FLOP cost of computing eigenvalues of a symmetric matrix.

    Parameters
    ----------
    n : int
        Matrix dimension.

    Returns
    -------
    int
        Estimated FLOP count: $(4/3)n^3$.

    Notes
    -----
    Same algorithm as ``eigh``, but eigenvectors are not accumulated.
    """
    return max((4 * n**3) // 3, 1)

qr(a, mode='reduced')

QR decomposition with FLOP counting.

Source code in src/mechestim/linalg/_decompositions.py
def qr(a, mode="reduced"):
    """QR decomposition with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2:
        raise ValueError(f"Input must be 2D, got {a.ndim}D")
    m, n = a.shape
    cost = qr_cost(m, n)
    budget.deduct("linalg.qr", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.qr(a, mode=mode)

qr_cost(m, n)

FLOP cost of QR decomposition.

Parameters:

Name Type Description Default
m int

Number of rows.

required
n int

Number of columns.

required

Returns:

Type Description
int

Estimated FLOP count: \(2mn^2 - (2/3)n^3\) (for \(m \geq n\)).

Notes

Source: Golub & Van Loan, Matrix Computations, 4th ed., §5.2. Assumes Householder QR. For m < n, roles are swapped.

Source code in src/mechestim/linalg/_decompositions.py
def qr_cost(m: int, n: int) -> int:
    r"""FLOP cost of QR decomposition.

    Parameters
    ----------
    m : int
        Number of rows.
    n : int
        Number of columns.

    Returns
    -------
    int
        Estimated FLOP count: $2mn^2 - (2/3)n^3$ (for $m \geq n$).

    Notes
    -----
    Source: Golub & Van Loan, *Matrix Computations*, 4th ed., §5.2.
    Assumes Householder QR. For m < n, roles are swapped.
    """
    if m < n:
        m, n = n, m
    return max(2 * m * n**2 - (2 * n**3) // 3, 1)

svdvals(a)

Singular values with FLOP counting.

Source code in src/mechestim/linalg/_decompositions.py
def svdvals(a):
    """Singular values with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2:
        raise ValueError(f"Input must be 2D, got {a.ndim}D")
    m, n = a.shape
    cost = svdvals_cost(m, n)
    budget.deduct("linalg.svdvals", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.svdvals(a)

svdvals_cost(m, n)

FLOP cost of computing singular values.

Parameters:

Name Type Description Default
m int

Number of rows.

required
n int

Number of columns.

required

Returns:

Type Description
int

Estimated FLOP count: m * n * min(m, n).

Notes

Source: Golub-Reinsch bidiagonalization. Same cost model as full SVD.

Source code in src/mechestim/linalg/_decompositions.py
def svdvals_cost(m: int, n: int) -> int:
    """FLOP cost of computing singular values.

    Parameters
    ----------
    m : int
        Number of rows.
    n : int
        Number of columns.

    Returns
    -------
    int
        Estimated FLOP count: m * n * min(m, n).

    Notes
    -----
    Source: Golub-Reinsch bidiagonalization. Same cost model as full SVD.
    """
    return max(m * n * min(m, n), 1)

mechestim.linalg._solvers

Linear solver wrappers with FLOP counting.

inv(a)

Matrix inverse with FLOP counting.

Source code in src/mechestim/linalg/_solvers.py
def inv(a):
    """Matrix inverse with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    is_symmetric = isinstance(a, SymmetricTensor)
    cost = inv_cost(n, symmetric=is_symmetric)
    budget.deduct("linalg.inv", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    result = _np.linalg.inv(a)
    if is_symmetric:
        result = as_symmetric(result, symmetric_axes=(0, 1))
    return result

inv_cost(n, symmetric=False)

FLOP cost of matrix inverse.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required
symmetric bool

If True, assume symmetric input. Default is False.

False

Returns:

Type Description
int

Estimated FLOP count.

Notes

Uses \(n^3/3 + n^3\) for symmetric input (Cholesky factorization + n triangular solves against identity), or \(n^3\) for general input (LU-based).

Source code in src/mechestim/linalg/_solvers.py
def inv_cost(n: int, symmetric: bool = False) -> int:
    """FLOP cost of matrix inverse.

    Parameters
    ----------
    n : int
        Matrix dimension.
    symmetric : bool, optional
        If True, assume symmetric input. Default is False.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    Uses $n^3/3 + n^3$ for symmetric input (Cholesky factorization + n
    triangular solves against identity), or $n^3$ for general input (LU-based).
    """
    if symmetric:
        return max(n**3 // 3 + n**3, 1)
    return max(n**3, 1)

lstsq(a, b, rcond=None)

Least-squares solution with FLOP counting.

Source code in src/mechestim/linalg/_solvers.py
def lstsq(a, b, rcond=None):
    """Least-squares solution with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2:
        raise ValueError(f"First argument must be 2D, got {a.ndim}D")
    m, n = a.shape
    cost = lstsq_cost(m, n)
    budget.deduct("linalg.lstsq", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.lstsq(a, b, rcond=rcond)

lstsq_cost(m, n)

FLOP cost of least-squares solution.

Parameters:

Name Type Description Default
m int

Number of rows.

required
n int

Number of columns.

required

Returns:

Type Description
int

Estimated FLOP count: m * n * min(m, n).

Notes

NumPy uses LAPACK gelsd (SVD-based) by default.

Source code in src/mechestim/linalg/_solvers.py
def lstsq_cost(m: int, n: int) -> int:
    """FLOP cost of least-squares solution.

    Parameters
    ----------
    m : int
        Number of rows.
    n : int
        Number of columns.

    Returns
    -------
    int
        Estimated FLOP count: m * n * min(m, n).

    Notes
    -----
    NumPy uses LAPACK ``gelsd`` (SVD-based) by default.
    """
    return max(m * n * min(m, n), 1)

pinv(a, rcond=None, hermitian=False)

Pseudoinverse with FLOP counting.

Source code in src/mechestim/linalg/_solvers.py
def pinv(a, rcond=None, hermitian=False):
    """Pseudoinverse with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2:
        raise ValueError(f"Input must be 2D, got {a.ndim}D")
    m, n = a.shape
    cost = pinv_cost(m, n)
    budget.deduct("linalg.pinv", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    kwargs = {"hermitian": hermitian}
    if rcond is not None:
        kwargs["rcond"] = rcond
    return _np.linalg.pinv(a, **kwargs)

pinv_cost(m, n)

FLOP cost of pseudoinverse.

Parameters:

Name Type Description Default
m int

Number of rows.

required
n int

Number of columns.

required

Returns:

Type Description
int

Estimated FLOP count: m * n * min(m, n).

Notes

Computed via SVD.

Source code in src/mechestim/linalg/_solvers.py
def pinv_cost(m: int, n: int) -> int:
    """FLOP cost of pseudoinverse.

    Parameters
    ----------
    m : int
        Number of rows.
    n : int
        Number of columns.

    Returns
    -------
    int
        Estimated FLOP count: m * n * min(m, n).

    Notes
    -----
    Computed via SVD.
    """
    return max(m * n * min(m, n), 1)

solve(a, b)

Solve linear system with FLOP counting.

Source code in src/mechestim/linalg/_solvers.py
def solve(a, b):
    """Solve linear system with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"First argument must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    if not isinstance(b, _np.ndarray):
        b = _np.asarray(b)
    nrhs = b.shape[1] if b.ndim == 2 else 1
    is_symmetric = isinstance(a, SymmetricTensor)
    cost = solve_cost(n, nrhs=nrhs, symmetric=is_symmetric)
    budget.deduct("linalg.solve", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.solve(a, b)

solve_cost(n, nrhs=1, symmetric=False)

FLOP cost of solving a linear system Ax = b.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required
nrhs int

Number of right-hand sides. Default is 1.

1
symmetric bool

If True, assume Cholesky factorization. Default is False (LU).

False

Returns:

Type Description
int

Estimated FLOP count.

Notes

Uses \(n^3/3 + 2n^2 \cdot n_{\text{rhs}}\) for Cholesky (symmetric; two triangular solves), or \(2n^3/3 + 2n^2 \cdot n_{\text{rhs}}\) for LU (general; two triangular solves). Source: Golub & Van Loan, Matrix Computations, 4th ed.

Source code in src/mechestim/linalg/_solvers.py
def solve_cost(n: int, nrhs: int = 1, symmetric: bool = False) -> int:
    r"""FLOP cost of solving a linear system Ax = b.

    Parameters
    ----------
    n : int
        Matrix dimension.
    nrhs : int, optional
        Number of right-hand sides. Default is 1.
    symmetric : bool, optional
        If True, assume Cholesky factorization. Default is False (LU).

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    Uses $n^3/3 + 2n^2 \cdot n_{\text{rhs}}$ for Cholesky (symmetric; two triangular
    solves), or $2n^3/3 + 2n^2 \cdot n_{\text{rhs}}$ for LU (general; two triangular
    solves). Source: Golub & Van Loan, *Matrix Computations*, 4th ed.
    """
    if symmetric:
        return max(n**3 // 3 + 2 * n**2 * nrhs, 1)
    return max(2 * n**3 // 3 + 2 * n**2 * nrhs, 1)

tensorinv(a, ind=2)

Tensor inverse with FLOP counting.

Source code in src/mechestim/linalg/_solvers.py
def tensorinv(a, ind=2):
    """Tensor inverse with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    cost = tensorinv_cost(a.shape, ind=ind)
    budget.deduct(
        "linalg.tensorinv", flop_cost=cost, subscripts=None, shapes=(a.shape,)
    )
    return _np.linalg.tensorinv(a, ind=ind)

tensorinv_cost(a_shape, ind=2)

FLOP cost of tensor inverse.

Parameters:

Name Type Description Default
a_shape tuple of int

Shape of the input tensor.

required
ind int

Number of leading indices. Default is 2.

2

Returns:

Type Description
int

Estimated FLOP count: \(n^3\) where \(n\) = product of leading dims.

Notes

Reduces to a standard matrix inverse after reshaping.

Source code in src/mechestim/linalg/_solvers.py
def tensorinv_cost(a_shape: tuple, ind: int = 2) -> int:
    """FLOP cost of tensor inverse.

    Parameters
    ----------
    a_shape : tuple of int
        Shape of the input tensor.
    ind : int, optional
        Number of leading indices. Default is 2.

    Returns
    -------
    int
        Estimated FLOP count: $n^3$ where $n$ = product of leading dims.

    Notes
    -----
    Reduces to a standard matrix inverse after reshaping.
    """
    n = 1
    for d in a_shape[:ind]:
        n *= d
    return max(n**3, 1)

tensorsolve(a, b, axes=None)

Tensor solve with FLOP counting.

Source code in src/mechestim/linalg/_solvers.py
def tensorsolve(a, b, axes=None):
    """Tensor solve with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    cost = tensorsolve_cost(a.shape)
    budget.deduct(
        "linalg.tensorsolve", flop_cost=cost, subscripts=None, shapes=(a.shape,)
    )
    return _np.linalg.tensorsolve(a, b, axes=axes)

tensorsolve_cost(a_shape, ind=None)

FLOP cost of tensor solve.

Parameters:

Name Type Description Default
a_shape tuple of int

Shape of the coefficient tensor.

required
ind int or None

Number of leading indices for the solution. Default is 2.

None

Returns:

Type Description
int

Estimated FLOP count: \(n^3\) where \(n\) = product of trailing dims.

Notes

Reduces to a standard linear solve after reshaping.

Source code in src/mechestim/linalg/_solvers.py
def tensorsolve_cost(a_shape: tuple, ind: int | None = None) -> int:
    """FLOP cost of tensor solve.

    Parameters
    ----------
    a_shape : tuple of int
        Shape of the coefficient tensor.
    ind : int or None, optional
        Number of leading indices for the solution. Default is 2.

    Returns
    -------
    int
        Estimated FLOP count: $n^3$ where $n$ = product of trailing dims.

    Notes
    -----
    Reduces to a standard linear solve after reshaping.
    """
    if ind is None:
        ind = 2
    n = 1
    for d in a_shape[ind:]:
        n *= d
    return max(n**3, 1)

mechestim.linalg._properties

Matrix property wrappers with FLOP counting.

cond(x, p=None)

Condition number with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def cond(x, p=None):
    """Condition number with FLOP counting."""
    budget = require_budget()
    if not isinstance(x, _np.ndarray):
        x = _np.asarray(x)
    if x.ndim != 2:
        raise ValueError(f"Input must be 2D, got {x.ndim}D")
    m, n = x.shape
    cost = cond_cost(m, n, p=p)
    budget.deduct("linalg.cond", flop_cost=cost, subscripts=None, shapes=(x.shape,))
    return _np.linalg.cond(x, p=p)

cond_cost(m, n, p=None)

FLOP cost of condition number.

Parameters:

Name Type Description Default
m int

Number of rows.

required
n int

Number of columns.

required
p (None, 2, -2, 1, -1, inf, -inf)

Norm type. None and 2/-2 use SVD; 1/-1/inf/-inf use LU factorization, which is cheaper.

None

Returns:

Type Description
int

Estimated FLOP count.

Notes

For p=None, p=2, or p=-2, computed via SVD (cost mnmin(m,n)). For p=1, p=-1, p=inf, or p=-inf, computed via LU factorization (cost ~min(m,n)^3 + m*n for norm).

Source code in src/mechestim/linalg/_properties.py
def cond_cost(m: int, n: int, p=None) -> int:
    """FLOP cost of condition number.

    Parameters
    ----------
    m : int
        Number of rows.
    n : int
        Number of columns.
    p : {None, 2, -2, 1, -1, inf, -inf}, optional
        Norm type. ``None`` and ``2``/``-2`` use SVD; ``1``/``-1``/``inf``/``-inf``
        use LU factorization, which is cheaper.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    For ``p=None``, ``p=2``, or ``p=-2``, computed via SVD (cost m*n*min(m,n)).
    For ``p=1``, ``p=-1``, ``p=inf``, or ``p=-inf``, computed via LU factorization
    (cost ~min(m,n)^3 + m*n for norm).
    """
    if p is None or p == 2 or p == -2:
        return max(m * n * min(m, n), 1)
    # LU-based: factorization cost + norm computation
    k = min(m, n)
    return max(k**3 + m * n, 1)

det(a)

Determinant with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def det(a):
    """Determinant with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    is_symmetric = isinstance(a, SymmetricTensor)
    cost = det_cost(n, symmetric=is_symmetric)
    budget.deduct("linalg.det", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.det(a)

det_cost(n, symmetric=False)

FLOP cost of determinant.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required
symmetric bool

If True, assume symmetric input. Default is False.

False

Returns:

Type Description
int

Estimated FLOP count.

Notes

Uses \(n^3/3\) for symmetric input (Cholesky), or \(2n^3/3\) for general input (LU factorization).

Source code in src/mechestim/linalg/_properties.py
def det_cost(n: int, symmetric: bool = False) -> int:
    """FLOP cost of determinant.

    Parameters
    ----------
    n : int
        Matrix dimension.
    symmetric : bool, optional
        If True, assume symmetric input. Default is False.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    Uses $n^3/3$ for symmetric input (Cholesky), or $2n^3/3$ for general
    input (LU factorization).
    """
    if symmetric:
        return max(n**3 // 3, 1)
    return max(2 * n**3 // 3, 1)

matrix_norm(x, ord='fro', keepdims=False)

Matrix norm with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def matrix_norm(x, ord="fro", keepdims=False):
    """Matrix norm with FLOP counting."""
    budget = require_budget()
    if not isinstance(x, _np.ndarray):
        x = _np.asarray(x)
    cost = matrix_norm_cost(x.shape, ord=ord)
    budget.deduct(
        "linalg.matrix_norm", flop_cost=cost, subscripts=None, shapes=(x.shape,)
    )
    return _np.linalg.matrix_norm(x, ord=ord, keepdims=keepdims)

matrix_norm_cost(shape, ord=None)

FLOP cost of matrix norm.

Parameters:

Name Type Description Default
shape tuple of int

Shape of the input array (last two dims are the matrix).

required
ord (None, 'fro', 'nuc', inf, -inf, 1, -1, 2, -2)

Order of the norm.

None

Returns:

Type Description
int

Estimated FLOP count.

Notes

SVD-based norms (2-norm, nuclear norm) cost m * n * min(m, n). Frobenius norm costs 2mn. Entry-sum norms cost mn.

Source code in src/mechestim/linalg/_properties.py
def matrix_norm_cost(shape: tuple, ord=None) -> int:
    """FLOP cost of matrix norm.

    Parameters
    ----------
    shape : tuple of int
        Shape of the input array (last two dims are the matrix).
    ord : {None, 'fro', 'nuc', inf, -inf, 1, -1, 2, -2}, optional
        Order of the norm.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    SVD-based norms (2-norm, nuclear norm) cost m * n * min(m, n).
    Frobenius norm costs 2mn. Entry-sum norms cost mn.
    """
    m, n = shape[-2], shape[-1]
    numel = m * n
    if ord is None or ord == "fro":
        return 2 * numel
    elif ord in (1, -1, _np.inf, -_np.inf):
        return numel
    elif ord == 2 or ord == -2:
        return m * n * min(m, n)
    elif ord == "nuc":
        return m * n * min(m, n)
    return numel

matrix_rank(A, tol=None, hermitian=False)

Matrix rank with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def matrix_rank(A, tol=None, hermitian=False):
    """Matrix rank with FLOP counting."""
    budget = require_budget()
    if not isinstance(A, _np.ndarray):
        A = _np.asarray(A)
    if A.ndim != 2:
        raise ValueError(f"Input must be 2D, got {A.ndim}D")
    m, n = A.shape
    cost = matrix_rank_cost(m, n)
    budget.deduct(
        "linalg.matrix_rank", flop_cost=cost, subscripts=None, shapes=(A.shape,)
    )
    return _np.linalg.matrix_rank(A, tol=tol, hermitian=hermitian)

matrix_rank_cost(m, n)

FLOP cost of matrix rank.

Parameters:

Name Type Description Default
m int

Number of rows.

required
n int

Number of columns.

required

Returns:

Type Description
int

Estimated FLOP count: m * n * min(m, n).

Notes

Computed via SVD.

Source code in src/mechestim/linalg/_properties.py
def matrix_rank_cost(m: int, n: int) -> int:
    """FLOP cost of matrix rank.

    Parameters
    ----------
    m : int
        Number of rows.
    n : int
        Number of columns.

    Returns
    -------
    int
        Estimated FLOP count: m * n * min(m, n).

    Notes
    -----
    Computed via SVD.
    """
    return max(m * n * min(m, n), 1)

norm(x, ord=None, axis=None, keepdims=False)

Matrix or vector norm with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def norm(x, ord=None, axis=None, keepdims=False):
    """Matrix or vector norm with FLOP counting."""
    budget = require_budget()
    if not isinstance(x, _np.ndarray):
        x = _np.asarray(x)
    if axis is None:
        effective_shape = x.shape
    elif isinstance(axis, int):
        effective_shape = (x.shape[axis],)
    else:
        effective_shape = tuple(x.shape[ax] for ax in axis)
    cost = norm_cost(effective_shape, ord=ord)
    budget.deduct("linalg.norm", flop_cost=cost, subscripts=None, shapes=(x.shape,))
    return _np.linalg.norm(x, ord=ord, axis=axis, keepdims=keepdims)

norm_cost(shape, ord=None)

FLOP cost of matrix or vector norm.

Parameters:

Name Type Description Default
shape tuple of int

Shape of the input array (or effective shape along norm axes).

required
ord (None, 'fro', 'nuc', inf, -inf, int)

Order of the norm.

None

Returns:

Type Description
int

Estimated FLOP count.

Notes

Cost depends on the ord parameter and input dimensionality. SVD-based norms (2-norm, nuclear norm) cost m * n * min(m, n).

Source code in src/mechestim/linalg/_properties.py
def norm_cost(shape: tuple, ord=None) -> int:
    """FLOP cost of matrix or vector norm.

    Parameters
    ----------
    shape : tuple of int
        Shape of the input array (or effective shape along norm axes).
    ord : {None, 'fro', 'nuc', inf, -inf, int}, optional
        Order of the norm.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    Cost depends on the ``ord`` parameter and input dimensionality.
    SVD-based norms (2-norm, nuclear norm) cost m * n * min(m, n).
    """
    numel = 1
    for d in shape:
        numel *= d
    numel = max(numel, 1)
    if len(shape) == 1:
        if ord is None or ord == 2 or ord == -2:
            return numel
        elif ord in (1, -1, _np.inf, -_np.inf, 0):
            return numel
        else:
            return 2 * numel
    else:
        m, n = shape[-2], shape[-1]
        if ord is None or ord == "fro":
            return 2 * numel
        elif ord in (1, -1, _np.inf, -_np.inf):
            return numel
        elif ord == 2 or ord == -2:
            return m * n * min(m, n)
        elif ord == "nuc":
            return m * n * min(m, n)
        return numel

slogdet(a)

Sign and log-determinant with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def slogdet(a):
    """Sign and log-determinant with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    n = a.shape[0]
    is_symmetric = isinstance(a, SymmetricTensor)
    cost = slogdet_cost(n, symmetric=is_symmetric)
    budget.deduct("linalg.slogdet", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.linalg.slogdet(a)

slogdet_cost(n, symmetric=False)

FLOP cost of sign and log-determinant.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required
symmetric bool

If True, assume symmetric input. Default is False.

False

Returns:

Type Description
int

Estimated FLOP count.

Notes

Uses \(n^3/3\) for symmetric input (Cholesky), or \(2n^3/3\) for general input (LU factorization).

Source code in src/mechestim/linalg/_properties.py
def slogdet_cost(n: int, symmetric: bool = False) -> int:
    """FLOP cost of sign and log-determinant.

    Parameters
    ----------
    n : int
        Matrix dimension.
    symmetric : bool, optional
        If True, assume symmetric input. Default is False.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    Uses $n^3/3$ for symmetric input (Cholesky), or $2n^3/3$ for general
    input (LU factorization).
    """
    if symmetric:
        return max(n**3 // 3, 1)
    return max(2 * n**3 // 3, 1)

trace(a, offset=0, axis1=0, axis2=1, dtype=None, out=None)

Matrix trace with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def trace(a, offset=0, axis1=0, axis2=1, dtype=None, out=None):
    """Matrix trace with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    n = min(a.shape[axis1], a.shape[axis2])
    if offset > 0:
        n = min(n, a.shape[axis2] - offset)
    elif offset < 0:
        n = min(n, a.shape[axis1] + offset)
    n = max(n, 0)
    cost = trace_cost(n)
    budget.deduct("linalg.trace", flop_cost=cost, subscripts=None, shapes=(a.shape,))
    return _np.trace(a, offset=offset, axis1=axis1, axis2=axis2, dtype=dtype, out=out)

trace_cost(n)

FLOP cost of matrix trace.

Parameters:

Name Type Description Default
n int

Number of diagonal elements to sum.

required

Returns:

Type Description
int

Estimated FLOP count: n.

Notes

Simply sums n diagonal elements.

Source code in src/mechestim/linalg/_properties.py
def trace_cost(n: int) -> int:
    """FLOP cost of matrix trace.

    Parameters
    ----------
    n : int
        Number of diagonal elements to sum.

    Returns
    -------
    int
        Estimated FLOP count: n.

    Notes
    -----
    Simply sums n diagonal elements.
    """
    return max(n, 1)

vector_norm(x, ord=2, axis=None, keepdims=False)

Vector norm with FLOP counting.

Source code in src/mechestim/linalg/_properties.py
def vector_norm(x, ord=2, axis=None, keepdims=False):
    """Vector norm with FLOP counting."""
    budget = require_budget()
    if not isinstance(x, _np.ndarray):
        x = _np.asarray(x)
    if axis is not None:
        if isinstance(axis, int):
            effective_shape = (x.shape[axis],)
        else:
            effective_shape = tuple(x.shape[ax] for ax in axis)
    else:
        effective_shape = x.shape
    cost = vector_norm_cost(effective_shape, ord=ord)
    budget.deduct(
        "linalg.vector_norm", flop_cost=cost, subscripts=None, shapes=(x.shape,)
    )
    return _np.linalg.vector_norm(x, ord=ord, axis=axis, keepdims=keepdims)

vector_norm_cost(shape, ord=None)

FLOP cost of vector norm.

Parameters:

Name Type Description Default
shape tuple of int

Shape of the input array (or effective shape along norm axes).

required
ord (None, inf, -inf, int, float)

Order of the norm.

None

Returns:

Type Description
int

Estimated FLOP count.

Notes

Most norms cost n FLOPs (one pass over elements). General p-norms cost 2n due to exponentiation.

Source code in src/mechestim/linalg/_properties.py
def vector_norm_cost(shape: tuple, ord=None) -> int:
    """FLOP cost of vector norm.

    Parameters
    ----------
    shape : tuple of int
        Shape of the input array (or effective shape along norm axes).
    ord : {None, inf, -inf, int, float}, optional
        Order of the norm.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    Most norms cost n FLOPs (one pass over elements). General p-norms
    cost 2n due to exponentiation.
    """
    numel = 1
    for d in shape:
        numel *= d
    numel = max(numel, 1)
    if ord is None or ord == 2 or ord == -2 or ord in (1, -1, _np.inf, -_np.inf, 0):
        return numel
    return 2 * numel

mechestim.linalg._compound

Compound linalg operations with FLOP counting.

matrix_power(a, n)

Matrix power with FLOP counting.

Source code in src/mechestim/linalg/_compound.py
def matrix_power(a, n):
    """Matrix power with FLOP counting."""
    budget = require_budget()
    if not isinstance(a, _np.ndarray):
        a = _np.asarray(a)
    if a.ndim != 2 or a.shape[0] != a.shape[1]:
        raise ValueError(f"Input must be square 2D array, got shape {a.shape}")
    size = a.shape[0]
    cost = matrix_power_cost(size, n)
    budget.deduct(
        "linalg.matrix_power", flop_cost=cost, subscripts=None, shapes=(a.shape,)
    )
    return _np.linalg.matrix_power(a, n)

matrix_power_cost(n, k)

FLOP cost of matrix power A**k.

Parameters:

Name Type Description Default
n int

Matrix dimension.

required
k int

Exponent.

required

Returns:

Type Description
int

Estimated FLOP count.

Notes

Uses exponentiation by repeated squaring. For \(k < 0\), adds \(n^3\) for the initial matrix inversion.

Source code in src/mechestim/linalg/_compound.py
def matrix_power_cost(n: int, k: int) -> int:
    """FLOP cost of matrix power A**k.

    Parameters
    ----------
    n : int
        Matrix dimension.
    k : int
        Exponent.

    Returns
    -------
    int
        Estimated FLOP count.

    Notes
    -----
    Uses exponentiation by repeated squaring. For $k < 0$, adds $n^3$ for
    the initial matrix inversion.
    """
    if k == 0 or k == 1:
        return 0
    if k < 0:
        return n**3 + matrix_power_cost(n, abs(k))
    num_ops = math.floor(math.log2(k)) + _popcount(k) - 1
    return max(num_ops * n**3, 1)

multi_dot(arrays, *, out=None)

Efficient multi-matrix dot product with FLOP counting.

Source code in src/mechestim/linalg/_compound.py
def multi_dot(arrays, *, out=None):
    """Efficient multi-matrix dot product with FLOP counting."""
    budget = require_budget()
    arrays = [a if isinstance(a, _np.ndarray) else _np.asarray(a) for a in arrays]
    shapes = [arr.shape for arr in arrays]
    cost = multi_dot_cost(shapes)
    budget.deduct(
        "linalg.multi_dot", flop_cost=cost, subscripts=None, shapes=tuple(shapes)
    )
    return _np.linalg.multi_dot(arrays, out=out)

multi_dot_cost(shapes)

FLOP cost of optimal matrix chain multiplication.

Parameters:

Name Type Description Default
shapes list of tuple of int

Shapes of the matrices to be multiplied.

required

Returns:

Type Description
int

Estimated FLOP count using optimal parenthesization.

Notes

Uses dynamic programming for optimal parenthesization. Source: Cormen et al., Introduction to Algorithms (CLRS), §15.2.

Source code in src/mechestim/linalg/_compound.py
def multi_dot_cost(shapes: list[tuple[int, ...]]) -> int:
    """FLOP cost of optimal matrix chain multiplication.

    Parameters
    ----------
    shapes : list of tuple of int
        Shapes of the matrices to be multiplied.

    Returns
    -------
    int
        Estimated FLOP count using optimal parenthesization.

    Notes
    -----
    Uses dynamic programming for optimal parenthesization.
    Source: Cormen et al., *Introduction to Algorithms* (CLRS), §15.2.
    """
    n = len(shapes)
    if n < 2:
        return 0
    dims = [s[0] for s in shapes] + [shapes[-1][-1]]
    if n == 2:
        return 2 * dims[0] * dims[1] * dims[2]
    cost_table = [[0] * n for _ in range(n)]
    for chain_len in range(2, n + 1):
        for i in range(n - chain_len + 1):
            j = i + chain_len - 1
            cost_table[i][j] = float("inf")
            for k in range(i, j):
                cost = (
                    cost_table[i][k]
                    + cost_table[k + 1][j]
                    + 2 * dims[i] * dims[k + 1] * dims[j + 1]
                )
                if cost < cost_table[i][j]:
                    cost_table[i][j] = cost
    return max(int(cost_table[0][n - 1]), 1)

mechestim.linalg._aliases

Linalg namespace aliases that delegate to top-level mechestim operations.

These functions exist in numpy.linalg as convenience aliases. FLOP costs are handled by the delegated top-level implementations.

cross(a, b, **kwargs)

Cross product (linalg namespace). Delegates to mechestim.cross.

Source code in src/mechestim/linalg/_aliases.py
def cross(a, b, **kwargs):
    """Cross product (linalg namespace). Delegates to mechestim.cross."""
    return _me.cross(a, b, **kwargs)

diagonal(a, **kwargs)

Diagonal (linalg namespace). Delegates to mechestim.diagonal. Cost: 0 FLOPs.

Source code in src/mechestim/linalg/_aliases.py
def diagonal(a, **kwargs):
    """Diagonal (linalg namespace). Delegates to mechestim.diagonal. Cost: 0 FLOPs."""
    return _me.diagonal(a, **kwargs)

matmul(a, b)

Matrix multiply (linalg namespace). Delegates to mechestim.matmul.

Source code in src/mechestim/linalg/_aliases.py
def matmul(a, b):
    """Matrix multiply (linalg namespace). Delegates to mechestim.matmul."""
    return _me.matmul(a, b)

matrix_transpose(a)

Transpose (linalg namespace). Delegates to mechestim.matrix_transpose. Cost: 0 FLOPs.

Source code in src/mechestim/linalg/_aliases.py
def matrix_transpose(a):
    """Transpose (linalg namespace). Delegates to mechestim.matrix_transpose. Cost: 0 FLOPs."""
    return _me.matrix_transpose(a)

outer(a, b, out=None)

Outer product (linalg namespace). Delegates to mechestim.outer.

Source code in src/mechestim/linalg/_aliases.py
def outer(a, b, out=None):
    """Outer product (linalg namespace). Delegates to mechestim.outer."""
    return _me.outer(a, b, out=out)

tensordot(a, b, axes=2)

Tensor dot product (linalg namespace). Delegates to mechestim.tensordot.

Source code in src/mechestim/linalg/_aliases.py
def tensordot(a, b, axes=2):
    """Tensor dot product (linalg namespace). Delegates to mechestim.tensordot."""
    return _me.tensordot(a, b, axes=axes)

vecdot(a, b, **kwargs)

Vector dot product (linalg namespace). Delegates to mechestim.vecdot.

Source code in src/mechestim/linalg/_aliases.py
def vecdot(a, b, **kwargs):
    """Vector dot product (linalg namespace). Delegates to mechestim.vecdot."""
    return _me.vecdot(a, b, **kwargs)