alkahest
Python API

Matrix API

Symbolic matrices and linear algebra operations.

Matrix

class
class Matrix(rows)

A symbolic matrix with Expr entries.

Constructed from a list of rows:

Python
pool = ExprPool()
x = pool.symbol("x")
y = pool.symbol("y")

M = Matrix([[x, pool.integer(1)],
            [y, x**2]])

+, -, * (matrix multiply), ** (integer powers) are supported:

Python
A = Matrix([[x, pool.integer(0)], [pool.integer(0), y]])
B = Matrix([[pool.integer(1), x], [y, pool.integer(1)]])
C = A * B   # matrix multiply
Properties
Methods
from_rows(rows)
staticmethod

Build a matrix from a 2D list of entries.

Each entry may be an Expr, DerivedResult, or a Python number (int, fractions.Fraction, float). Numbers are coerced exactly — Fraction(1, 3) is the rational 1/3 — into the same ExprPool as the first Expr/DerivedResult found anywhere in rows (consistent with how arithmetic operators coerce scalars). An all-number matrix such as [[1, 2], [3, 4]] uses the pool of the active alkahest.context(pool=...), or default_pool() outside one.

shape() → tuple[int, int]

Shape of the matrix as a (rows, cols) tuple.

trace()

Sum of diagonal entries (trace).

get(r, c)

Access entry (i, j). Matrix is not subscriptable — M[i, j] raises TypeError.

transpose() → Matrix

Return the transpose.

multiply(other)

Matrix product self @ other (SymPy-style named alias).

Same result as self @ other and self * other. Raises a MatrixError (E-MAT-001) if the inner dimensions do not match.

scalar_mul(other)

Scale every entry by scalar (an Expr, DerivedResult, Python int, or float). Same as self * scalar.

hadamard(other)

Elementwise (Hadamard) product — multiply corresponding entries.

Requires both matrices to have the same shape. This is not the matrix product; use @ / * / multiply() for that. Exposed only as a named method (never on an operator) so * unambiguously means the matrix product, per the CAS convention.

det() → Expr

Compute the determinant symbolically.

characteristic_polynomial_lambda_minus_m()

det(λI − M) and the fresh λ symbol (Expr) used in that polynomial.

eigenvals(real_form=True)

Dictionary mapping each eigenvalue expression to its algebraic multiplicity.

With real_form=True (the default) each eigenvalue is rewritten by to_real_form() — sqrt(4*a**2) becomes 2*abs(a), a root written through sqrt of a provably non-negative square loses its radical — but only where the imaginary part is proved zero; a genuinely complex eigenvalue (±I of a rotation) is returned as is. Eigenvalues that coincide after the rewrite have their multiplicities added. real_form=False returns the raw spectrum.

eigenvects() → list

SymPy-style triples (eigenvalue, multiplicity, [column eigenvectors …]).

diagonalize()

(P, D) with M @ P == P @ D when the matrix is diagonalizable.

nullspace() → list[Matrix]
rank() → int

Rank of the matrix.

For symbolic entries this is the generic rank (over the field of rational functions in the parameters): Matrix([[a]]).rank() is 1 for a ≠ 0. The pivot hypotheses it rests on are listed by matrix_rank_side_conditions(); the rank is exact where they all hold and an upper bound everywhere.

rref() → list[list[Expr]]
column_space()
row_space()
cholesky()
jordan_form() → Matrix

All five run the same elimination, which uses a three-valued zero test. An entry whose vanishing can be proven neither zero nor non-zero raises E-LINALG-010 (as LinearAlgebraError, or EigenError from eigenvects) rather than picking a branch. Substituting concrete values for the parameters is the remedy.

On symbolic entries rank() is the generic rank: each pivot the elimination divided by is proven not to be the zero function, but may vanish at particular parameter values. The non-constant pivots are listed by matrix_rank_side_conditions() (Matrix([[a]]).rank() is 1 with ['a ≠ 0']); the rank is exact where they hold and an upper bound everywhere.

rational_canonical_form()
minimal_polynomial()
matrix_exp()

e^M.

Correct for a defective (non-diagonalizable) M as well as a diagonalizable one: Matrix([[0,1],[0,0]]).matrix_exp() is [[1,1],[0,1]], not the identity. The answer is checked against an independently computed e^M before it is returned, and withheld with LinearAlgebraError (E-LINALG-011) if the two disagree or if nothing could confirm the eigenvalue list is the spectrum.

For a symbolic M whose eigenvalues can coincide — [[a,1],[0,b]] at a = b — the generic answer is returned and the hypothesis it rests on is listed by matrix_exp_side_conditions().

inverse() → Matrix

M⁻¹.

For a symbolic M the adjugate form adj(M)/det(M) is returned whenever det(M) is proven not to be the zero function; the genericity hypothesis that licenses it is listed by matrix_inverse_side_conditions().

simplify()
to_list()
function
jacobian(f_vec, x_vec)

alkahest.jacobian(f_vec, x_vec) — compute the Jacobian matrix.

Side conditions

function
matrix_exp_side_conditions()

alkahest.matrix_exp_side_conditions() -> list[str]

The hypotheses the most recent Matrix.matrix_exp() on this thread assumed in order to return the matrix it did — one string per condition, e.g. "(a + (b · -1)) ≠ 0".

Exactly one thing lands here: a difference of two eigenvalues that the expansion divided by and could not prove non-zero. Matrix([[a,1],[0,b]]).matrix_exp() has the off-diagonal (e^a − e^b)/(a − b), which is e^A for a ≠ b; at a = b the matrix is defective and the entry is e^a, the limit, not a quotient. That generic-parameter reading is the one every CAS gives and is the useful one — but a Matrix cannot carry the caveat, so it is reported beside the result, as solve reports a ≠ 0 through solve_side_conditions().

An empty list means every gap divided by was proven non-zero, or that the confluent branch was taken and nothing was divided by at all — not that nothing was looked at. Reset by each matrix_exp call, so read it before the next one; repeated reads of the same call agree.

function
matrix_inverse_side_conditions()

The hypotheses the most recent Matrix.inverse() on this thread assumed in order to return the matrix it did — one string per condition, e.g. "((a · d) + (b · c · -1)) ≠ 0".

Exactly one thing lands here: det(M) ≠ 0 for a symbolic M. The determinant of [[a,b],[c,d]] is not the zero function, so adj/det is the inverse — for the parameter values where ad − bc does not vanish. Whether a given set of values is on that locus is a question about the parameters, not about the matrix, and only the caller can settle it.

An empty list means the determinant is a non-zero rational constant and the hypothesis was discharged, not skipped. Reset by each inverse call, so read it before the next one; repeated reads of the same call agree.

The condition is also present in the value — every entry carries a det⁻¹ factor, so evaluating on the singular locus raises rather than returning a number — which is why this is an auditability channel rather than the thing standing between the caller and a wrong answer.