Matrix API
Symbolic matrices and linear algebra operations.
Matrix
A symbolic matrix with Expr entries.
Constructed from a list of rows:
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:
A = Matrix([[x, pool.integer(0)], [pool.integer(0), y]])
B = Matrix([[pool.integer(1), x], [y, pool.integer(1)]])
C = A * B # matrix multiplyBuild 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.
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.
Scale every entry by scalar (an Expr, DerivedResult,
Python int, or float). Same as self * scalar.
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(λI − M) and the fresh λ symbol (Expr) used in that polynomial.
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.
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.
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.
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().
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().
alkahest.jacobian(f_vec, x_vec) — compute the Jacobian matrix.
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.
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.