#![allow(clippy::needless_range_loop)]
use crate::kernel::{Domain, ExprData, ExprId, ExprPool};
use crate::matrix::eigen::{
self, characteristic_polynomial_lambda_minus_m, concatenate_columns, kernel_column_basis,
m_minus_lambda_scaled, KernelFailure, KnownSingular,
};
use crate::matrix::normal_form::{smith_form_poly, PolyMatrixQ, RatUniPoly};
use crate::matrix::{exp_gate, putzer, spectrum, zero_test, Matrix, MatrixError};
use crate::poly::unipoly::UniPoly;
use crate::poly::{factor_univariate_z, FactorError};
use crate::simplify::engine::{simplify, simplify_expanded};
use rug::Rational;
use std::fmt;
use std::ops::Mul;
use std::sync::atomic::{AtomicUsize, Ordering};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum LinearAlgebraError {
NonSquare,
KernelFailed,
NotPositiveDefinite,
CharPolyConversion(crate::poly::error::ConversionError),
Factorization(FactorError),
UnsupportedIrreducibleDegree {
degree: usize,
},
UnsupportedField,
SingularTransform,
NonRationalEntry,
}
impl fmt::Display for LinearAlgebraError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
LinearAlgebraError::NonSquare => write!(f, "operation requires a square matrix"),
LinearAlgebraError::KernelFailed => write!(f, "could not compute nullspace basis"),
LinearAlgebraError::NotPositiveDefinite => {
write!(f, "matrix is not symmetric positive definite")
}
LinearAlgebraError::CharPolyConversion(e) => {
write!(f, "characteristic polynomial: {e}")
}
LinearAlgebraError::Factorization(e) => write!(f, "factorization failed: {e}"),
LinearAlgebraError::UnsupportedIrreducibleDegree { degree } => write!(
f,
"irreducible factor of degree {degree} in minimal polynomial"
),
LinearAlgebraError::UnsupportedField => {
write!(
f,
"this routine could not answer over the field the entries live in: \
a Smith-based decomposition needs rational constants, elimination \
needs entries whose vanishing it can decide, and the matrix \
exponential needs a confirmed spectrum whose eigenvalue gaps are \
settled"
)
}
LinearAlgebraError::SingularTransform => {
write!(
f,
"the similarity transform could not be produced: the computed \
transform is singular, or it does not conjugate the matrix to \
the canonical form that was computed for it"
)
}
LinearAlgebraError::NonRationalEntry => {
write!(f, "matrix entry is not a rational constant")
}
}
}
}
impl std::error::Error for LinearAlgebraError {}
impl crate::errors::AlkahestError for LinearAlgebraError {
fn code(&self) -> &'static str {
match self {
LinearAlgebraError::NonSquare => "E-LINALG-001",
LinearAlgebraError::KernelFailed => "E-LINALG-002",
LinearAlgebraError::NotPositiveDefinite => "E-LINALG-003",
LinearAlgebraError::CharPolyConversion(_) => "E-LINALG-004",
LinearAlgebraError::Factorization(_) => "E-LINALG-005",
LinearAlgebraError::UnsupportedIrreducibleDegree { .. } => "E-LINALG-006",
LinearAlgebraError::UnsupportedField => "E-LINALG-007",
LinearAlgebraError::SingularTransform => "E-LINALG-008",
LinearAlgebraError::NonRationalEntry => "E-LINALG-009",
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
LinearAlgebraError::NonSquare => Some("pass a square matrix"),
LinearAlgebraError::KernelFailed => {
Some("try a matrix with rational entries or a ℚ-splitting spectrum")
}
LinearAlgebraError::NotPositiveDefinite => {
Some("Cholesky requires a symmetric positive definite matrix")
}
LinearAlgebraError::CharPolyConversion(_) => {
Some("entries must simplify to rationals so det(λI−M) is a polynomial in λ")
}
LinearAlgebraError::Factorization(_) => None,
LinearAlgebraError::UnsupportedIrreducibleDegree { .. } => {
Some("minimal polynomial has an irreducible factor of degree > 2")
}
LinearAlgebraError::UnsupportedField => Some(
"use entries this routine can work over: rational or integer entries \
for Smith-based decompositions, and entries whose vanishing is \
decidable for elimination",
),
LinearAlgebraError::SingularTransform => Some(
"no verified similarity transform is available for this matrix; \
substitute concrete values for any parameters",
),
LinearAlgebraError::NonRationalEntry => {
Some("convert symbolic entries to rationals before calling this routine")
}
}
}
}
pub fn nullspace_basis(m: &Matrix, pool: &ExprPool) -> Result<Vec<Matrix>, LinearAlgebraError> {
kernel_column_basis(m, pool, KnownSingular::No).map_err(|f| kernel_failure_to_error(f, pool))
}
fn kernel_failure_to_error(f: KernelFailure, pool: &ExprPool) -> LinearAlgebraError {
match f {
KernelFailure::Undecidable(e) => inconclusive(pool, e),
}
}
pub fn rank(m: &Matrix, pool: &ExprPool) -> Result<usize, LinearAlgebraError> {
Ok(row_echelon_pivots(m, pool)?.pivot_cols.len())
}
pub fn rref(m: &Matrix, pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
Ok(row_echelon_pivots(m, pool)?.echelon)
}
pub fn column_space_basis(m: &Matrix, pool: &ExprPool) -> Result<Vec<Matrix>, LinearAlgebraError> {
let rref = row_echelon_pivots(m, pool)?;
Ok(rref
.pivot_cols
.iter()
.map(|&c| {
Matrix::new(m.col(c).into_iter().map(|e| vec![e]).collect()).expect("column vector")
})
.collect())
}
pub fn row_space_basis(m: &Matrix, pool: &ExprPool) -> Result<Vec<Matrix>, LinearAlgebraError> {
let rref = row_echelon_pivots(m, pool)?;
Ok(rref
.pivot_row_flags
.iter()
.enumerate()
.filter_map(|(ri, &is_pivot)| {
if is_pivot {
Some(Matrix::new(vec![rref.echelon.row(ri)]).expect("row vector"))
} else {
None
}
})
.collect())
}
struct RowEchelonInfo {
pivot_cols: Vec<usize>,
pivot_row_flags: Vec<bool>,
echelon: Matrix,
}
fn row_echelon_pivots(m: &Matrix, pool: &ExprPool) -> Result<RowEchelonInfo, LinearAlgebraError> {
let rows = m.rows;
let cols = m.cols;
if let Some(grid) = matrix_to_rational_grid(m, pool) {
let (pivot_cols, pivot_row_flags, echelon_grid) =
rational_row_echelon_pivots(&grid, rows, cols);
return Ok(RowEchelonInfo {
pivot_cols,
pivot_row_flags,
echelon: rational_grid_to_matrix(&echelon_grid, pool),
});
}
let mut a: Vec<Vec<ExprId>> = (0..rows)
.map(|r| {
(0..cols)
.map(|c| simplify(m.get(r, c), pool).value)
.collect()
})
.collect();
let neg_one = pool.integer(-1_i32);
let mut pivot_cols = Vec::new();
let mut pivot_row_flags = vec![false; rows];
let mut r_at = 0usize;
for c in 0..cols {
if r_at >= rows {
break;
}
let Some((pr, piv)) = find_pivot(&mut a, r_at, rows, c, pool)? else {
continue;
};
if pr != r_at {
a.swap(pr, r_at);
}
let inv_p = simplify(pool.pow(piv, pool.integer(-1_i32)), pool).value;
for cc in 0..cols {
a[r_at][cc] = simplify(pool.mul(vec![inv_p, a[r_at][cc]]), pool).value;
}
for rr in 0..rows {
if rr == r_at {
continue;
}
let f = simplify(a[rr][c], pool).value;
if zero_test::zero_status(pool, f).is_proven_zero() {
continue;
}
for cc in 0..cols {
let term = simplify(pool.mul(vec![f, a[r_at][cc]]), pool).value;
let neg_term = simplify(pool.mul(vec![neg_one, term]), pool).value;
a[rr][cc] = simplify(pool.add(vec![a[rr][cc], neg_term]), pool).value;
}
}
pivot_cols.push(c);
pivot_row_flags[r_at] = true;
r_at += 1;
}
Ok(RowEchelonInfo {
pivot_cols,
pivot_row_flags,
echelon: Matrix::new(a).expect("row echelon grid"),
})
}
fn find_pivot(
a: &mut [Vec<ExprId>],
r_at: usize,
rows: usize,
c: usize,
pool: &ExprPool,
) -> Result<Option<(usize, ExprId)>, LinearAlgebraError> {
let zero = pool.integer(0_i32);
let mut undecided: Option<ExprId> = None;
for rr in r_at..rows {
let e = simplify(a[rr][c], pool).value;
match zero_test::zero_status(pool, e) {
zero_test::ZeroStatus::NonZero => return Ok(Some((rr, e))),
zero_test::ZeroStatus::Zero => a[rr][c] = zero,
zero_test::ZeroStatus::Unknown => undecided = undecided.or(Some(e)),
}
}
match undecided {
None => Ok(None),
Some(e) => Err(inconclusive(pool, e)),
}
}
fn inconclusive(pool: &ExprPool, e: ExprId) -> LinearAlgebraError {
zero_test::record_refusal(pool, e, zero_test::RefusalSite::Pivot);
LinearAlgebraError::UnsupportedField
}
fn unsupported_field() -> LinearAlgebraError {
zero_test::forget_refusal();
LinearAlgebraError::UnsupportedField
}
fn rational_row_echelon_pivots(
mat: &[Vec<Rational>],
rows: usize,
cols: usize,
) -> (Vec<usize>, Vec<bool>, Vec<Vec<Rational>>) {
use crate::flint::{mat::FlintMat, FlintInteger};
if rows == 0 || cols == 0 {
return (Vec::new(), vec![false; rows], mat.to_vec());
}
let mut fm = FlintMat::new(rows, cols);
for (i, row) in mat.iter().enumerate() {
let mut l = rug::Integer::from(1);
for c in row {
if *c.denom() != 1 {
l.lcm_mut(c.denom());
}
}
for (j, c) in row.iter().enumerate() {
let v = if l == 1 {
c.numer().clone()
} else {
c.numer() * rug::Integer::from(&l / c.denom())
};
fm.set_entry(i, j, &FlintInteger::from_rug(&v));
}
}
let (rank, den, b) = fm.rref();
let mut out = vec![vec![Rational::new(); cols]; rows];
let mut pivot_cols = Vec::with_capacity(rank);
let mut pivot_row_flags = vec![false; rows];
if rank > 0 {
let den = den.to_rug();
for (i, out_row) in out.iter_mut().enumerate().take(rank) {
for (j, o) in out_row.iter_mut().enumerate() {
let e = b.get_flint(i, j).to_rug();
if e != 0 {
if !pivot_row_flags[i] {
pivot_row_flags[i] = true;
pivot_cols.push(j);
}
*o = Rational::from((e, den.clone()));
}
}
}
}
(pivot_cols, pivot_row_flags, out)
}
#[cfg(test)]
fn rational_row_echelon_reference(
mat: &[Vec<Rational>],
rows: usize,
cols: usize,
) -> (Vec<usize>, Vec<bool>, Vec<Vec<Rational>>) {
let mut a = mat.to_vec();
let mut pivot_cols = Vec::new();
let mut pivot_row_flags = vec![false; rows];
let mut r = 0usize;
for c in 0..cols {
if r >= rows {
break;
}
let mut piv = None;
for rr in r..rows {
if a[rr][c] != 0 {
piv = Some(rr);
break;
}
}
let Some(pr) = piv else { continue };
if pr != r {
a.swap(pr, r);
}
let inv = Rational::from(1) / a[r][c].clone();
for cc in 0..cols {
a[r][cc] *= inv.clone();
}
for rr in 0..rows {
if rr == r {
continue;
}
let f = a[rr][c].clone();
if f == 0 {
continue;
}
for cc in 0..cols {
let pivot_val = a[r][cc].clone();
a[rr][cc] -= f.clone() * pivot_val;
}
}
pivot_cols.push(c);
pivot_row_flags[r] = true;
r += 1;
}
(pivot_cols, pivot_row_flags, a)
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct LuDecomposition {
pub l: Matrix,
pub u: Matrix,
pub perm: Vec<usize>,
}
pub fn lu_decomposition(
m: &Matrix,
pool: &ExprPool,
) -> Result<LuDecomposition, LinearAlgebraError> {
let n = m.rows;
let cols = m.cols;
if n == 0 {
return Ok(LuDecomposition {
l: Matrix::identity(0, pool),
u: Matrix::zeros(0, cols, pool),
perm: vec![],
});
}
if let Some(mut a) = matrix_to_rational_grid(m, pool) {
let mut perm: Vec<usize> = (0..n).collect();
let mut l = vec![vec![Rational::from(0); n]; n];
let mut u = vec![vec![Rational::from(0); cols]; n];
for i in 0..n {
l[i][i] = Rational::from(1);
}
for k in 0..n.min(cols) {
let mut piv_row = k;
for r in (k + 1)..n {
if a[r][k].clone().abs() > a[piv_row][k].clone().abs() {
piv_row = r;
}
}
if a[piv_row][k] == 0 {
for j in k..cols {
u[k][j] = a[k][j].clone();
}
continue;
}
if piv_row != k {
a.swap(piv_row, k);
perm.swap(piv_row, k);
let (head, tail) = l.split_at_mut(piv_row);
head[k][..k].swap_with_slice(&mut tail[0][..k]);
}
let pivot = a[k][k].clone();
for j in k..cols {
u[k][j] = a[k][j].clone();
}
for i in (k + 1)..n {
let factor = a[i][k].clone() / pivot.clone();
l[i][k] = factor.clone();
let pivot_row: Vec<Rational> = a[k][k..cols].to_vec();
for j in k..cols {
a[i][j] -= factor.clone() * pivot_row[j - k].clone();
}
}
}
return Ok(LuDecomposition {
l: rational_grid_to_matrix(&l, pool),
u: rational_grid_to_matrix(&u, pool),
perm,
});
}
expr_lu_decomposition(m, pool)
}
fn expr_lu_decomposition(
m: &Matrix,
pool: &ExprPool,
) -> Result<LuDecomposition, LinearAlgebraError> {
let n = m.rows;
let cols = m.cols;
let mut a: Vec<Vec<ExprId>> = (0..n)
.map(|r| {
(0..cols)
.map(|c| simplify(m.get(r, c), pool).value)
.collect()
})
.collect();
let mut perm: Vec<usize> = (0..n).collect();
let mut l = Matrix::identity(n, pool);
let mut u = Matrix::zeros(n, cols, pool);
for k in 0..n.min(cols) {
let Some((piv_row, _)) = find_pivot(&mut a, k, n, k, pool)? else {
for j in k..cols {
u.set(k, j, a[k][j]);
}
continue;
};
if piv_row != k {
a.swap(piv_row, k);
perm.swap(piv_row, k);
for j in 0..k {
let t = l.get(k, j);
l.set(k, j, l.get(piv_row, j));
l.set(piv_row, j, t);
}
}
let pivot = a[k][k];
let inv_p = simplify(pool.pow(pivot, pool.integer(-1_i32)), pool).value;
for j in k..cols {
u.set(k, j, a[k][j]);
}
for i in (k + 1)..n {
let factor = simplify(pool.mul(vec![a[i][k], inv_p]), pool).value;
l.set(i, k, factor);
for j in k..cols {
let sub = simplify(pool.mul(vec![factor, a[k][j]]), pool).value;
let neg_sub = simplify(pool.mul(vec![pool.integer(-1_i32), sub]), pool).value;
a[i][j] = simplify(pool.add(vec![a[i][j], neg_sub]), pool).value;
}
}
}
Ok(LuDecomposition { l, u, perm })
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct QrDecomposition {
pub q: Matrix,
pub r: Matrix,
}
pub fn qr_decomposition(
m: &Matrix,
pool: &ExprPool,
) -> Result<QrDecomposition, LinearAlgebraError> {
let n = m.rows;
let k = m.cols;
if k == 0 {
return Ok(QrDecomposition {
q: Matrix::zeros(n, 0, pool),
r: Matrix::zeros(0, 0, pool),
});
}
let mut q_cols: Vec<Matrix> = Vec::with_capacity(k);
let mut r = Matrix::zeros(k, k, pool);
for j in 0..k {
let mut v = Matrix::new(m.col(j).into_iter().map(|e| vec![e]).collect())
.map_err(|_| LinearAlgebraError::KernelFailed)?;
for i in 0..j {
let qi = &q_cols[i];
let rij = dot_columns(qi, &v, pool)?;
r.set(i, j, rij);
let proj = qi.scale(rij, pool);
v = v
.sub(&proj, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
}
let rjj = norm_column(&v, pool)?;
match zero_test::zero_status(pool, rjj) {
zero_test::ZeroStatus::Zero => {
r.set(j, j, pool.integer(0_i32));
q_cols.push(orthonormal_completion(&q_cols, n, pool));
continue;
}
zero_test::ZeroStatus::NonZero => {}
zero_test::ZeroStatus::Unknown => return Err(inconclusive(pool, rjj)),
}
r.set(j, j, rjj);
let inv = simplify(pool.pow(rjj, pool.integer(-1_i32)), pool).value;
v = v.scale(inv, pool);
q_cols.push(v);
}
let q = concatenate_columns(&q_cols, pool).map_err(|_| LinearAlgebraError::KernelFailed)?;
Ok(QrDecomposition { q, r })
}
fn orthonormal_completion(built: &[Matrix], n: usize, pool: &ExprPool) -> Matrix {
if built.len() >= n {
return Matrix::zeros(n, 1, pool);
}
for i in 0..n {
let Ok(mut v) = unit_column_vector(i, n, pool) else {
continue;
};
let mut ok = true;
for q in built {
let Ok(c) = dot_columns(q, &v, pool) else {
ok = false;
break;
};
let proj = q.scale(c, pool);
match v.sub(&proj, pool) {
Ok(next) => v = next.simplify_entries(pool),
Err(_) => {
ok = false;
break;
}
}
}
if !ok {
continue;
}
let Ok(norm) = norm_column(&v, pool) else {
continue;
};
if zero_test::zero_status(pool, norm) != zero_test::ZeroStatus::NonZero {
continue;
}
let inv = simplify(pool.pow(norm, pool.integer(-1_i32)), pool).value;
return v.scale(inv, pool).simplify_entries(pool);
}
Matrix::zeros(n, 1, pool)
}
pub fn cholesky(m: &Matrix, pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
if m.rows != m.cols {
return Err(LinearAlgebraError::NonSquare);
}
let n = m.rows;
if !is_symmetric(m, pool) {
return Err(LinearAlgebraError::NotPositiveDefinite);
}
if let Some(a) = matrix_to_rational_grid(m, pool) {
return rational_cholesky(&a, n, pool);
}
let mut l = Matrix::zeros(n, n, pool);
for i in 0..n {
for j in 0..=i {
let mut s = pool.integer(0_i32);
for t in 0..j {
s = simplify(
pool.add(vec![s, pool.mul(vec![l.get(i, t), l.get(j, t)])]),
pool,
)
.value;
}
if i == j {
let inner = simplify(
pool.add(vec![m.get(i, i), pool.mul(vec![pool.integer(-1_i32), s])]),
pool,
)
.value;
l.set(i, j, simplify(pool.func("sqrt", vec![inner]), pool).value);
} else {
let num = simplify(
pool.add(vec![m.get(i, j), pool.mul(vec![pool.integer(-1_i32), s])]),
pool,
)
.value;
l.set(
i,
j,
simplify(
pool.mul(vec![num, pool.pow(l.get(j, j), pool.integer(-1_i32))]),
pool,
)
.value,
);
}
}
}
Ok(l)
}
fn is_symmetric(m: &Matrix, pool: &ExprPool) -> bool {
for i in 0..m.rows {
for j in (i + 1)..m.cols {
let diff = simplify_expanded(
pool.add(vec![
m.get(i, j),
pool.mul(vec![pool.integer(-1_i32), m.get(j, i)]),
]),
pool,
)
.value;
if !zero_test::zero_status(pool, diff).is_proven_zero() {
return false;
}
}
}
true
}
fn rational_cholesky(
a: &[Vec<Rational>],
n: usize,
pool: &ExprPool,
) -> Result<Matrix, LinearAlgebraError> {
let mut l = vec![vec![Rational::from(0); n]; n];
let mut d = vec![Rational::from(0); n];
for j in 0..n {
let mut s = Rational::from(0);
for t in 0..j {
s += l[j][t].clone() * l[j][t].clone() * d[t].clone();
}
d[j] = a[j][j].clone() - s;
if d[j] <= 0 {
return Err(LinearAlgebraError::NotPositiveDefinite);
}
l[j][j] = Rational::from(1);
for i in (j + 1)..n {
let mut s = Rational::from(0);
for t in 0..j {
s += l[i][t].clone() * l[j][t].clone() * d[t].clone();
}
l[i][j] = (a[i][j].clone() - s) / d[j].clone();
}
}
let mut out = Matrix::zeros(n, n, pool);
for j in 0..n {
let root = match rational_sqrt(&d[j]) {
Some(r) => rational_expr(&r, pool),
None => {
let dj = rational_expr(&d[j], pool);
simplify(pool.func("sqrt", vec![dj]), pool).value
}
};
for i in j..n {
let lij = rational_expr(&l[i][j], pool);
out.set(i, j, simplify(pool.mul(vec![lij, root]), pool).value);
}
}
Ok(out)
}
pub fn jordan_form(m: &Matrix, pool: &ExprPool) -> Result<(Matrix, Matrix), LinearAlgebraError> {
if m.rows != m.cols {
return Err(LinearAlgebraError::NonSquare);
}
let n = m.rows;
let vals = eigen::eigenvalues(m, pool).map_err(map_eigen_err)?;
let mut j_blocks: Vec<Matrix> = Vec::new();
let mut p_cols: Vec<Matrix> = Vec::new();
for (lambda, alg_m) in vals {
let shifted = m_minus_lambda_scaled(m, lambda, pool);
let mut ker_dims = vec![0usize];
let mut pow = Matrix::identity(n, pool);
for _k in 1..=alg_m {
pow = pow
.mul(&shifted, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
ker_dims.push(
kernel_column_basis(&pow, pool, KnownSingular::Yes)
.map_err(|f| kernel_failure_to_error(f, pool))?
.len(),
);
}
let mut nu = vec![0usize; alg_m + 2];
for s in 1..=alg_m {
nu[s] = ker_dims[s] - ker_dims[s - 1];
}
let mut block_sizes = Vec::new();
for s in 1..=alg_m {
let nb = nu[s] - nu[s + 1];
for _ in 0..nb {
block_sizes.push(s);
}
}
block_sizes.sort_by(|a, b| b.cmp(a));
for &sz in &block_sizes {
let mut nk = Matrix::identity(n, pool);
for _ in 0..sz {
nk = nk
.mul(&shifted, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
}
let bas = kernel_column_basis(&nk, pool, KnownSingular::Yes)
.map_err(|f| kernel_failure_to_error(f, pool))?;
let chain = chain_generator(&bas, &shifted, sz, &p_cols, pool)?;
for col in chain {
p_cols.push(col);
}
j_blocks.push(jordan_block_matrix(lambda, sz, pool));
}
}
if p_cols.len() != n {
return Err(LinearAlgebraError::KernelFailed);
}
let p = concatenate_columns(&p_cols, pool).map_err(|_| LinearAlgebraError::KernelFailed)?;
if matches!(rank(&p, pool), Ok(r) if r < n) {
return Err(LinearAlgebraError::SingularTransform);
}
let j = block_diagonal(&j_blocks, pool)?;
Ok((p, j))
}
fn chain_generator(
bas: &[Matrix],
shifted: &Matrix,
sz: usize,
built: &[Matrix],
pool: &ExprPool,
) -> Result<Vec<Matrix>, LinearAlgebraError> {
let mut first: Option<Vec<Matrix>> = None;
for v_top in bas.iter().rev() {
let mut chain = vec![v_top.clone()];
let mut cur = v_top.clone();
for _ in 1..sz {
cur = shifted
.mul(&cur, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
chain.push(cur.clone());
}
chain.reverse();
if chain[0]
.entries()
.iter()
.all(|&e| zero_test::zero_status(pool, simplify(e, pool).value).is_proven_zero())
{
continue;
}
if first.is_none() {
first = Some(chain.clone());
}
if extends_independently(built, &chain, pool) {
return Ok(chain);
}
}
first
.or_else(|| bas.last().map(|v| vec![v.clone()]))
.ok_or(LinearAlgebraError::KernelFailed)
}
fn extends_independently(built: &[Matrix], chain: &[Matrix], pool: &ExprPool) -> bool {
let cols: Vec<Matrix> = built.iter().chain(chain.iter()).cloned().collect();
let Ok(m) = concatenate_columns(&cols, pool) else {
return false;
};
matches!(rank(&m, pool), Ok(r) if r == cols.len())
}
fn jordan_block_matrix(lambda: ExprId, size: usize, pool: &ExprPool) -> Matrix {
let mut m = Matrix::zeros(size, size, pool);
for i in 0..size {
m.set(i, i, lambda);
if i + 1 < size {
m.set(i, i + 1, pool.integer(1_i32));
}
}
m
}
pub fn rational_canonical_form(
m: &Matrix,
pool: &ExprPool,
) -> Result<(Matrix, Matrix), LinearAlgebraError> {
if m.rows != m.cols {
return Err(LinearAlgebraError::NonSquare);
}
let poly_m = lambda_identity_minus_m_poly(m, pool)?;
let (s, _u, _v) = smith_form_poly(&poly_m);
let factors = invariant_factors_from_smith(&s)?;
let c = companion_block_diagonal(&factors, pool)?;
let p = frobenius_p_from_cyclic_vectors(m, &factors, pool)?;
confirm_similarity(m, &p, &c, pool)?;
Ok((p, c))
}
fn confirm_similarity(
m: &Matrix,
p: &Matrix,
c: &Matrix,
pool: &ExprPool,
) -> Result<(), LinearAlgebraError> {
let n = m.rows;
if p.rows != n || p.cols != n || c.rows != n || c.cols != n {
return Err(LinearAlgebraError::SingularTransform);
}
let lhs = m
.mul(p, pool)
.map_err(|_| LinearAlgebraError::SingularTransform)?;
let rhs = p
.mul(c, pool)
.map_err(|_| LinearAlgebraError::SingularTransform)?;
for i in 0..n {
for j in 0..n {
let diff = simplify_expanded(
pool.add(vec![
lhs.get(i, j),
pool.mul(vec![pool.integer(-1_i32), rhs.get(i, j)]),
]),
pool,
)
.value;
if !zero_test::zero_status(pool, diff).is_proven_zero() {
return Err(LinearAlgebraError::SingularTransform);
}
}
}
if rank(p, pool)? != n {
return Err(LinearAlgebraError::SingularTransform);
}
Ok(())
}
fn fresh_frobenius_lambda(pool: &ExprPool) -> ExprId {
static SEQ: AtomicUsize = AtomicUsize::new(0);
let k = SEQ.fetch_add(1, Ordering::Relaxed);
pool.symbol(format!("__frobenius_lambda_{k}"), Domain::Complex)
}
fn lambda_identity_minus_m_poly(
m: &Matrix,
pool: &ExprPool,
) -> Result<PolyMatrixQ, LinearAlgebraError> {
let _lam = fresh_frobenius_lambda(pool);
let n = m.rows;
let x = RatUniPoly::x();
let mut rows = Vec::with_capacity(n);
for i in 0..n {
let mut row = Vec::with_capacity(n);
for j in 0..n {
let entry = if i == j {
let c = expr_to_rat_uni_poly(m.get(i, j), pool)?;
(&x - &c).trim()
} else {
let c = expr_to_rat_uni_poly(m.get(i, j), pool)?;
(-&c).trim()
};
row.push(entry);
}
rows.push(row);
}
PolyMatrixQ::from_nested(rows).map_err(|_| unsupported_field())
}
fn expr_to_rat_uni_poly(e: ExprId, pool: &ExprPool) -> Result<RatUniPoly, LinearAlgebraError> {
match pool.get(e) {
ExprData::Integer(n) => Ok(RatUniPoly::constant(Rational::from((n.0.clone(), 1)))),
ExprData::Rational(r) => Ok(RatUniPoly::constant(r.0.clone())),
ExprData::Add(args) => {
let mut acc = RatUniPoly::zero();
for a in args {
acc = (&acc + &expr_to_rat_uni_poly(a, pool)?).trim();
}
Ok(acc)
}
ExprData::Mul(args) => {
let mut acc = RatUniPoly::one();
for a in args {
acc = (&acc * &expr_to_rat_uni_poly(a, pool)?).trim();
}
Ok(acc)
}
_ => Err(LinearAlgebraError::NonRationalEntry),
}
}
fn invariant_factors_from_smith(s: &PolyMatrixQ) -> Result<Vec<RatUniPoly>, LinearAlgebraError> {
let n = s.rows.min(s.cols);
let mut facs = Vec::new();
for i in 0..n {
let p = s.get(i, i).clone().trim();
if !p.is_zero() && p.degree() > 0 {
facs.push(p);
}
}
if facs.is_empty() {
return Err(unsupported_field());
}
Ok(facs)
}
fn companion_matrix(f: &RatUniPoly, pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
let f = f.clone().trim();
if f.degree() <= 0 {
return Err(unsupported_field());
}
let d = f.degree() as usize;
let coeffs = f.coeffs.clone();
let lead = coeffs[d].clone();
if lead == 0 {
return Err(unsupported_field());
}
let mut c = Matrix::zeros(d, d, pool);
for i in 0..d - 1 {
c.set(i + 1, i, pool.integer(1_i32));
}
for j in 0..d {
let a = coeffs[j].clone() / lead.clone();
let coeff = rational_expr(&a, pool);
c.set(
j,
d - 1,
simplify(pool.mul(vec![pool.integer(-1_i32), coeff]), pool).value,
);
}
Ok(c)
}
fn companion_block_diagonal(
factors: &[RatUniPoly],
pool: &ExprPool,
) -> Result<Matrix, LinearAlgebraError> {
let mut blocks = Vec::new();
for f in factors {
blocks.push(companion_matrix(f, pool)?);
}
block_diagonal(&blocks, pool)
}
fn block_diagonal(blocks: &[Matrix], pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
let total: usize = blocks.iter().map(|b| b.rows).sum();
let mut out = Matrix::zeros(total, total, pool);
let mut off = 0usize;
for b in blocks {
for i in 0..b.rows {
for j in 0..b.cols {
out.set(off + i, off + j, b.get(i, j));
}
}
off += b.rows;
}
Ok(out)
}
fn frobenius_p_from_cyclic_vectors(
m: &Matrix,
factors: &[RatUniPoly],
pool: &ExprPool,
) -> Result<Matrix, LinearAlgebraError> {
let n = m.rows;
let Some(g) = factors.last() else {
return Err(LinearAlgebraError::KernelFailed);
};
let mut cols: Vec<Matrix> = Vec::with_capacity(n);
for f in factors {
let f = f.clone().trim();
if f.degree() <= 0 {
return Err(unsupported_field());
}
let d = f.degree() as usize;
let (q, r) = RatUniPoly::div_rem(g, &f);
if !r.trim().is_zero() {
return Err(unsupported_field());
}
let qm = rat_poly_at_matrix(&q, m, pool)?;
let chain = cyclic_chain_for_factor(m, &qm, d, n, &cols, pool)?;
cols.extend(chain);
}
if cols.len() != n {
return Err(LinearAlgebraError::KernelFailed);
}
concatenate_columns(&cols, pool).map_err(|_| LinearAlgebraError::KernelFailed)
}
fn rat_poly_at_matrix(
p: &RatUniPoly,
m: &Matrix,
pool: &ExprPool,
) -> Result<Matrix, LinearAlgebraError> {
let n = m.rows;
let p = p.clone().trim();
let mut acc = Matrix::zeros(n, n, pool);
for coeff in p.coeffs.iter().rev() {
let scaled = acc
.mul(m, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
let c = rational_expr(coeff, pool);
let term = Matrix::identity(n, pool).scale(c, pool);
acc = scaled
.add(&term, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?
.simplify_entries(pool);
}
Ok(acc)
}
fn cyclic_chain_for_factor(
m: &Matrix,
qm: &Matrix,
d: usize,
n: usize,
built: &[Matrix],
pool: &ExprPool,
) -> Result<Vec<Matrix>, LinearAlgebraError> {
let mut seeds: Vec<Matrix> = Vec::new();
for c in 0..n {
seeds.push(unit_column_vector(c, n, pool)?);
}
for i in 0..n {
for j in (i + 1)..n {
seeds.push(sum_unit_columns(i, j, n, pool)?);
}
}
for w in &seeds {
let v = qm
.mul(w, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?
.simplify_entries(pool);
let mut chain = vec![v.clone()];
let mut cur = v;
for _ in 1..d {
cur = m
.mul(&cur, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?
.simplify_entries(pool);
chain.push(cur.clone());
}
if extends_independently(built, &chain, pool) {
return Ok(chain);
}
}
Err(LinearAlgebraError::KernelFailed)
}
fn sum_unit_columns(
i: usize,
j: usize,
n: usize,
pool: &ExprPool,
) -> Result<Matrix, LinearAlgebraError> {
let zero = pool.integer(0_i32);
let one = pool.integer(1_i32);
let rows: Vec<Vec<ExprId>> = (0..n)
.map(|r| vec![if r == i || r == j { one } else { zero }])
.collect();
Matrix::new(rows).map_err(|_| LinearAlgebraError::KernelFailed)
}
fn unit_column_vector(col: usize, n: usize, pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
let zero = pool.integer(0_i32);
let one = pool.integer(1_i32);
let rows: Vec<Vec<ExprId>> = (0..n)
.map(|r| vec![if r == col { one } else { zero }])
.collect();
Matrix::new(rows).map_err(|_| LinearAlgebraError::KernelFailed)
}
pub fn minimal_polynomial(
m: &Matrix,
pool: &ExprPool,
) -> Result<(ExprId, ExprId), LinearAlgebraError> {
if m.rows != m.cols {
return Err(LinearAlgebraError::NonSquare);
}
let (char_e, lam) = characteristic_polynomial_lambda_minus_m(m, pool).map_err(map_eigen_err)?;
let uni = UniPoly::from_symbolic_clear_denoms(char_e, lam, pool)
.map_err(LinearAlgebraError::CharPolyConversion)?;
let fac = factor_univariate_z(&uni).map_err(LinearAlgebraError::Factorization)?;
let mut divisors = all_divisors_from_factors(
&fac.factors
.iter()
.map(|(p, e)| (p.clone(), *e as usize))
.collect::<Vec<_>>(),
);
divisors.sort_by_key(|p| p.degree());
for cand in divisors {
if matrix_annihilated_by_uni(m, &cand, pool)? {
return Ok((uni_poly_to_expr(&cand, lam, pool), lam));
}
}
Err(LinearAlgebraError::KernelFailed)
}
fn all_divisors_from_factors(factors: &[(UniPoly, usize)]) -> Vec<UniPoly> {
let Some((first, _)) = factors.first() else {
return Vec::new();
};
let mut out = vec![UniPoly::constant(first.var, 1)];
for (base, exp) in factors {
let mut next = Vec::new();
for d in &out {
let mut cur = d.clone();
for _e in 0..=*exp {
next.push(cur.clone());
if _e < *exp {
cur = cur.mul(base.clone());
}
}
}
out = next;
}
out
}
fn matrix_annihilated_by_uni(
m: &Matrix,
p: &UniPoly,
pool: &ExprPool,
) -> Result<bool, LinearAlgebraError> {
let n = m.rows;
let coeffs = p.coefficients();
let mut acc = Matrix::zeros(n, n, pool);
let mut pow = Matrix::identity(n, pool);
for (deg, coeff) in coeffs.iter().enumerate() {
if !coeff.is_zero() {
let c = pool.integer(coeff.clone());
let term = pow.scale(c, pool);
acc = acc
.add(&term, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
}
if deg + 1 < coeffs.len() {
pow = pow
.mul(m, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
}
}
for e in acc.entries() {
let entry = simplify(*e, pool).value;
match zero_test::zero_status(pool, entry) {
zero_test::ZeroStatus::Zero => {}
zero_test::ZeroStatus::NonZero => return Ok(false),
zero_test::ZeroStatus::Unknown => return Err(inconclusive(pool, entry)),
}
}
Ok(true)
}
fn uni_poly_to_expr(p: &UniPoly, lam: ExprId, pool: &ExprPool) -> ExprId {
let mut terms = Vec::new();
for (deg, coeff) in p.coefficients().iter().enumerate() {
if coeff.is_zero() {
continue;
}
let c = pool.integer(coeff.clone());
let term = if deg == 0 {
c
} else if deg == 1 {
simplify(pool.mul(vec![c, lam]), pool).value
} else {
simplify(
pool.mul(vec![c, pool.pow(lam, pool.integer(deg as i32))]),
pool,
)
.value
};
terms.push(term);
}
match terms.len() {
0 => pool.integer(0_i32),
1 => simplify(terms[0], pool).value,
_ => simplify(pool.add(terms), pool).value,
}
}
pub fn matrix_exponential(m: &Matrix, pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
exp_gate::forget_refusal();
if m.rows != m.cols {
return Err(LinearAlgebraError::NonSquare);
}
if is_diagonal(m, pool) {
return diagonal_matrix_exp(m, pool);
}
if let Ok((p, d)) = eigen::diagonalize(m, pool) {
let exp_d = diagonal_matrix_exp(&d, pool)?;
let inv_p = matrix_inverse(&p, pool).map_err(|_| LinearAlgebraError::SingularTransform)?;
let candidate = p
.mul(&exp_d, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?
.mul(&inv_p, pool)
.map_err(|_| LinearAlgebraError::KernelFailed)?;
return gated(m, candidate, pool);
}
let lambdas = spectrum_with_multiplicity(m, pool)?;
match spectrum::confirm_spectrum(m, &lambdas, pool) {
Ok(check) if check.is_confirmed() => {}
Ok(_) => return Err(unconfirmed_spectrum(m.rows)),
Err(refusal) => {
spectrum::record_refusal(refusal);
return Err(LinearAlgebraError::UnsupportedIrreducibleDegree { degree: m.rows });
}
}
let one = pool.integer(1_i32);
let mut gaps = putzer::RecordedGaps::default();
let candidate = putzer::matrix_exponential(m, &lambdas, one, &mut gaps, pool)
.ok_or(LinearAlgebraError::NonSquare)?;
let out = gated(m, candidate.simplify_entries(pool), pool)?;
exp_gate::record_side_conditions(gaps.assumed_nonzero);
Ok(out)
}
fn gated(m: &Matrix, candidate: Matrix, pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
match exp_gate::confirm_matrix_exponential(m, &candidate, pool) {
Ok(_) => Ok(candidate),
Err(refusal) => {
exp_gate::record_refusal(refusal.reason(), refusal.to_string());
Err(LinearAlgebraError::UnsupportedField)
}
}
}
fn spectrum_with_multiplicity(
m: &Matrix,
pool: &ExprPool,
) -> Result<Vec<ExprId>, LinearAlgebraError> {
let n = m.rows;
if is_triangular(m, pool) {
return Ok((0..n).map(|i| simplify(m.get(i, i), pool).value).collect());
}
let eigs = eigen::eigenvalues(m, pool).map_err(map_eigen_err)?;
let mut out = Vec::with_capacity(n);
for (lam, mult) in eigs {
for _ in 0..mult {
out.push(simplify(lam, pool).value);
}
}
if out.len() != n {
return Err(LinearAlgebraError::UnsupportedIrreducibleDegree { degree: n });
}
Ok(out)
}
fn is_triangular(m: &Matrix, pool: &ExprPool) -> bool {
let n = m.rows;
let proven_zero = |i: usize, j: usize| {
zero_test::zero_status(pool, simplify(m.get(i, j), pool).value).is_proven_zero()
};
let lower = (0..n).all(|i| (0..i).all(|j| proven_zero(i, j)));
let upper = (0..n).all(|i| ((i + 1)..n).all(|j| proven_zero(i, j)));
lower || upper
}
fn unconfirmed_spectrum(n: usize) -> LinearAlgebraError {
exp_gate::record_refusal(
exp_gate::ExpRefusalReason::SpectrumUnconfirmed,
exp_gate::spectrum_unconfirmed(n),
);
LinearAlgebraError::UnsupportedField
}
fn is_diagonal(m: &Matrix, pool: &ExprPool) -> bool {
if m.rows != m.cols {
return false;
}
for r in 0..m.rows {
for c in 0..m.cols {
let entry = simplify(m.get(r, c), pool).value;
if r != c && !zero_test::zero_status(pool, entry).is_proven_zero() {
return false;
}
}
}
true
}
fn diagonal_matrix_exp(d: &Matrix, pool: &ExprPool) -> Result<Matrix, LinearAlgebraError> {
let n = d.rows;
let mut out = Matrix::zeros(n, n, pool);
for i in 0..n {
out.set(
i,
i,
simplify(pool.func("exp", vec![d.get(i, i)]), pool).value,
);
}
Ok(out)
}
#[cfg(test)]
fn apply_row_permutation(m: &Matrix, perm: &[usize]) -> Matrix {
let rows: Vec<Vec<ExprId>> = perm.iter().map(|&r| m.row(r)).collect();
Matrix::new(rows).expect("row permutation")
}
thread_local! {
static INVERSE_SIDE_CONDITIONS: std::cell::RefCell<Vec<ExprId>> =
const { std::cell::RefCell::new(Vec::new()) };
}
pub fn take_matrix_inverse_side_conditions() -> Vec<crate::deriv::SideCondition> {
INVERSE_SIDE_CONDITIONS.with(|c| {
std::mem::take(&mut *c.borrow_mut())
.into_iter()
.map(crate::deriv::SideCondition::NonZero)
.collect()
})
}
pub fn matrix_inverse(m: &Matrix, pool: &ExprPool) -> Result<Matrix, MatrixError> {
INVERSE_SIDE_CONDITIONS.with(|c| c.borrow_mut().clear());
if m.rows != m.cols {
return Err(MatrixError::NotSquare);
}
let n = m.rows;
let Some(a) = matrix_to_rational_grid(m, pool) else {
return symbolic_inverse(m, pool);
};
let mut aug: Vec<Vec<Rational>> = a
.into_iter()
.map(|mut row| {
row.resize(2 * n, Rational::from(0));
row
})
.collect();
for i in 0..n {
aug[i][n + i] = Rational::from(1);
}
for col in 0..n {
let mut piv = None;
for r in col..n {
if aug[r][col] != 0 {
piv = Some(r);
break;
}
}
let Some(pr) = piv else {
return Err(singular());
};
if pr != col {
aug.swap(pr, col);
}
let inv = Rational::from(1) / aug[col][col].clone();
for j in 0..2 * n {
aug[col][j] *= inv.clone();
}
for r in 0..n {
if r == col {
continue;
}
let f = aug[r][col].clone();
if f == 0 {
continue;
}
for j in 0..2 * n {
let pivot_val = aug[col][j].clone();
aug[r][j] -= f.clone() * pivot_val;
}
}
}
let inv_grid: Vec<Vec<Rational>> = aug.into_iter().map(|row| row[n..].to_vec()).collect();
Ok(rational_grid_to_matrix(&inv_grid, pool))
}
fn symbolic_inverse(m: &Matrix, pool: &ExprPool) -> Result<Matrix, MatrixError> {
symbolic_inverse_with(m, pool, true)
}
const POLY_INVERSE_MIN_DIM: usize = 4;
fn symbolic_inverse_with(
m: &Matrix,
pool: &ExprPool,
use_bareiss: bool,
) -> Result<Matrix, MatrixError> {
let n = m.rows;
if n == 0 {
return Ok(Matrix::zeros(0, 0, pool));
}
let poly = if use_bareiss && n >= POLY_INVERSE_MIN_DIM {
super::poly_det::PolyMatrix::from_matrix(m, pool)
} else {
None
};
let (det, poly_minors) = match poly.as_ref().and_then(|p| p.det_and_minors_expanded(pool)) {
Some((d, minors)) => (d, minors),
None => (simplify_expanded(m.det(pool)?, pool).value, None),
};
match zero_test::zero_status(pool, det) {
zero_test::ZeroStatus::Zero => return Err(singular()),
zero_test::ZeroStatus::NonZero => {
if expr_to_rational_strict(det, pool).is_none() {
INVERSE_SIDE_CONDITIONS.with(|c| c.borrow_mut().push(det));
}
}
zero_test::ZeroStatus::Unknown => {
zero_test::record_refusal(pool, det, zero_test::RefusalSite::Determinant);
return Err(MatrixError::SingularMatrix);
}
}
let inv_det = simplify(pool.pow(det, pool.integer(-1_i32)), pool).value;
let mut memo = super::DetMemo::new(m, pool);
let mut rows: Vec<Vec<ExprId>> = Vec::with_capacity(n);
for i in 0..n {
let mut row: Vec<ExprId> = Vec::with_capacity(n);
for j in 0..n {
let minor_det = if n == 1 {
pool.integer(1_i32)
} else if let Some(minors) = &poly_minors {
minors[j * n + i]
} else {
simplify_expanded(m.minor_det_memo(j, i, &mut memo, pool), pool).value
};
let sign = if (i + j) % 2 == 0 {
pool.integer(1_i32)
} else {
pool.integer(-1_i32)
};
let cofactor = pool.mul(vec![sign, minor_det, inv_det]);
row.push(simplify(cofactor, pool).value);
}
rows.push(row);
}
Matrix::new(rows).map_err(|_| singular())
}
fn singular() -> MatrixError {
zero_test::forget_refusal();
MatrixError::SingularMatrix
}
fn map_eigen_err(e: eigen::EigenError) -> LinearAlgebraError {
match e {
eigen::EigenError::NonSquare => LinearAlgebraError::NonSquare,
eigen::EigenError::CharPolyConversion(c) => LinearAlgebraError::CharPolyConversion(c),
eigen::EigenError::Factorization(f) => LinearAlgebraError::Factorization(f),
eigen::EigenError::UnsupportedIrreducibleDegree { degree } => {
LinearAlgebraError::UnsupportedIrreducibleDegree { degree }
}
eigen::EigenError::KernelComputationFailed
| eigen::EigenError::NonDiagonalizable
| eigen::EigenError::SingularModalMatrix => LinearAlgebraError::KernelFailed,
}
}
fn matrix_to_rational_grid(m: &Matrix, pool: &ExprPool) -> Option<Vec<Vec<Rational>>> {
let mut g = Vec::with_capacity(m.rows);
for r in 0..m.rows {
let mut row = Vec::with_capacity(m.cols);
for c in 0..m.cols {
row.push(expr_to_rational_strict(m.get(r, c), pool)?);
}
g.push(row);
}
Some(g)
}
fn expr_to_rational_strict(e: ExprId, pool: &ExprPool) -> Option<Rational> {
match pool.get(e) {
ExprData::Integer(n) => Some(Rational::from((n.0.clone(), 1))),
ExprData::Rational(r) => Some(r.0.clone()),
ExprData::Add(args) => {
let mut acc = Rational::from(0);
for a in args {
acc += expr_to_rational_strict(a, pool)?;
}
Some(acc)
}
ExprData::Mul(args) => {
let mut acc = Rational::from(1);
for a in args {
acc *= expr_to_rational_strict(a, pool)?;
}
Some(acc)
}
_ => None,
}
}
fn rational_grid_to_matrix(grid: &[Vec<Rational>], pool: &ExprPool) -> Matrix {
let rows: Vec<Vec<ExprId>> = grid
.iter()
.map(|row| row.iter().map(|r| rational_expr(r, pool)).collect())
.collect();
Matrix::new(rows).expect("rational grid")
}
fn rational_expr(r: &Rational, pool: &ExprPool) -> ExprId {
if *r.denom() == 1 {
pool.integer(r.numer().clone())
} else {
pool.rational(r.numer().clone(), r.denom().clone())
}
}
fn dot_columns(a: &Matrix, b: &Matrix, pool: &ExprPool) -> Result<ExprId, LinearAlgebraError> {
let mut terms = Vec::new();
for r in 0..a.rows {
terms.push(simplify(pool.mul(vec![a.get(r, 0), b.get(r, 0)]), pool).value);
}
Ok(simplify(pool.add(terms), pool).value)
}
fn rational_sqrt(r: &Rational) -> Option<Rational> {
let num = r.numer();
let den = r.denom();
let sn = integer_sqrt(num)?;
let sd = integer_sqrt(den)?;
Some(Rational::from((sn, sd)))
}
fn integer_sqrt(n: &rug::Integer) -> Option<rug::Integer> {
if n < &0 {
return None;
}
if n.is_zero() {
return Some(rug::Integer::from(0));
}
let root = n.clone().sqrt();
let sq = root.clone() * root.clone();
if sq == *n {
Some(root)
} else {
None
}
}
fn norm_column(v: &Matrix, pool: &ExprPool) -> Result<ExprId, LinearAlgebraError> {
let mut terms = Vec::new();
for r in 0..v.rows {
let e = v.get(r, 0);
terms.push(simplify(pool.mul(vec![e, e]), pool).value);
}
let sum = simplify_expanded(pool.add(terms), pool).value;
let sum = simplify_expanded(zero_test::fold_rational_radicals(pool, sum), pool).value;
Ok(simplify(pool.func("sqrt", vec![sum]), pool).value)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::matrix::eigen;
fn pool() -> ExprPool {
ExprPool::new()
}
#[test]
fn nullspace_line_in_plane() {
let p = pool();
let one = p.integer(1_i32);
let two = p.integer(2_i32);
let m = Matrix::new(vec![vec![one, two]]).unwrap();
let bas = nullspace_basis(&m, &p).unwrap();
assert_eq!(bas.len(), 1);
}
#[test]
fn rank_identity() {
let p = pool();
let id = Matrix::identity(3, &p);
assert_eq!(rank(&id, &p).unwrap(), 3);
}
#[test]
fn rank_sees_through_the_exponential_functional_equation() {
let p = pool();
let a = p.symbol("a", Domain::Real);
let exp_a = p.func("exp", vec![a]);
let m = Matrix::new(vec![
vec![p.integer(1_i32), exp_a, exp_a],
vec![
exp_a,
p.mul(vec![exp_a, exp_a]),
p.func("exp", vec![p.add(vec![a, a])]),
],
])
.unwrap();
assert_eq!(rank(&m, &p).unwrap(), 1);
let echelon = rref(&m, &p).unwrap();
for c in 0..echelon.cols {
assert_eq!(
echelon.get(1, c),
p.integer(0_i32),
"rank-1 matrix must have an all-zero second rref row"
);
}
}
#[test]
fn rank_still_separates_independent_exponential_rows() {
let p = pool();
let a = p.symbol("a", Domain::Real);
let exp_a = p.func("exp", vec![a]);
let m = Matrix::new(vec![
vec![p.integer(1_i32), exp_a, exp_a],
vec![exp_a, p.mul(vec![exp_a, exp_a]), exp_a],
])
.unwrap();
assert_eq!(rank(&m, &p).unwrap(), 2);
}
#[test]
fn rank_refuses_an_undecidable_pivot() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let opaque = p.func("mystery", vec![x]);
let zero = p.integer(0_i32);
let m = Matrix::new(vec![vec![opaque, zero], vec![zero, zero]]).unwrap();
let err = rank(&m, &p).expect_err("an undecidable pivot must refuse");
assert!(
matches!(err, LinearAlgebraError::UnsupportedField),
"expected a zero-test refusal, got {err:?}"
);
use crate::errors::AlkahestError;
assert!(err.code().starts_with("E-LINALG-"));
let refusal = crate::matrix::take_zero_test_refusal()
.expect("the refusal must be recoverable, or the code is lost");
assert_eq!(refusal.code(), "E-LINALG-010");
assert!(
refusal.entry().contains("mystery"),
"refusal should name the undecided entry, got {}",
refusal.entry()
);
assert_eq!(
crate::matrix::take_zero_test_refusal(),
None,
"taking must consume, so one refusal cannot be reported twice"
);
}
#[test]
fn a_proven_singularity_is_not_a_zero_test_refusal() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let opaque = p.func("mystery", vec![x]);
let zero = p.integer(0_i32);
let undecidable = Matrix::new(vec![vec![opaque, zero], vec![zero, zero]]).unwrap();
let _ = rank(&undecidable, &p);
let m = Matrix::new(vec![
vec![p.integer(1_i32), p.integer(2_i32)],
vec![p.integer(2_i32), p.integer(4_i32)],
])
.unwrap();
assert_eq!(matrix_inverse(&m, &p), Err(MatrixError::SingularMatrix));
assert_eq!(
crate::matrix::take_zero_test_refusal(),
None,
"a proven singularity must not be reported as an undecided determinant"
);
}
#[test]
fn inverse_refuses_an_undecidable_determinant() {
let p = pool();
let x = p.symbol("x", Domain::Real);
let opaque = p.func("mystery", vec![x]);
let m = Matrix::new(vec![
vec![opaque, p.integer(0_i32)],
vec![p.integer(0_i32), p.integer(1_i32)],
])
.unwrap();
assert_eq!(matrix_inverse(&m, &p), Err(MatrixError::SingularMatrix));
use crate::errors::AlkahestError;
let refusal = crate::matrix::take_zero_test_refusal()
.expect("an undecided determinant must record its refusal");
assert_eq!(refusal.code(), "E-MAT-004");
}
fn undecidable_matrix(p: &ExprPool) -> Matrix {
let x = p.symbol("x", Domain::Real);
let opaque = p.func("mystery", vec![x]);
let zero = p.integer(0_i32);
Matrix::new(vec![vec![opaque, zero], vec![zero, zero]]).unwrap()
}
#[test]
fn nullspace_reports_the_specific_undecidable_entry() {
use crate::errors::AlkahestError;
let p = pool();
let err = nullspace_basis(&undecidable_matrix(&p), &p)
.expect_err("an undecidable pivot must refuse");
assert!(
matches!(err, LinearAlgebraError::UnsupportedField),
"expected the zero-test carrier variant, got {err:?}"
);
let refusal = crate::matrix::take_zero_test_refusal()
.expect("the refusal must be recoverable, or the specific code is lost");
assert_eq!(refusal.code(), "E-LINALG-010");
assert!(
refusal.entry().contains("mystery"),
"refusal should name the undecided entry, got {}",
refusal.entry()
);
}
#[test]
fn jordan_form_reports_the_specific_undecidable_entry() {
use crate::errors::AlkahestError;
let p = pool();
let err =
jordan_form(&undecidable_matrix(&p), &p).expect_err("an undecidable pivot must refuse");
assert!(
matches!(err, LinearAlgebraError::UnsupportedField),
"expected the zero-test carrier variant, got {err:?}"
);
let refusal = crate::matrix::take_zero_test_refusal()
.expect("the refusal must be recoverable, or the specific code is lost");
assert_eq!(refusal.code(), "E-LINALG-010");
}
#[test]
fn eigenvectors_report_the_specific_undecidable_entry() {
use crate::errors::AlkahestError;
let p = pool();
let err = eigen::eigenvectors(&undecidable_matrix(&p), &p)
.expect_err("an undecidable pivot must refuse");
assert_eq!(err, eigen::EigenError::KernelComputationFailed);
let refusal = crate::matrix::take_zero_test_refusal()
.expect("the refusal must be recoverable, or the specific code is lost");
assert_eq!(refusal.code(), "E-LINALG-010");
}
#[test]
fn a_nullspace_refusal_is_not_re_attributed_to_a_later_error() {
let p = pool();
assert!(nullspace_basis(&undecidable_matrix(&p), &p).is_err());
let singular = Matrix::new(vec![
vec![p.integer(1_i32), p.integer(2_i32)],
vec![p.integer(2_i32), p.integer(4_i32)],
])
.unwrap();
assert_eq!(
matrix_inverse(&singular, &p),
Err(MatrixError::SingularMatrix)
);
assert_eq!(
crate::matrix::take_zero_test_refusal(),
None,
"a proven singularity must not inherit the nullspace refusal's code"
);
}
fn kernel_vectors_are_annihilated(m: &Matrix, basis: &[Matrix], p: &ExprPool) -> bool {
basis.iter().all(|v| {
let prod = m.mul(v, p).expect("M·v");
(0..prod.rows).all(|r| {
zero_test::zero_status(p, simplify(prod.get(r, 0), p).value)
== zero_test::ZeroStatus::Zero
})
})
}
#[test]
fn nullspace_refuses_an_undecidable_determinant() {
use crate::errors::AlkahestError;
let p = pool();
let x = p.symbol("x", Domain::Real);
let opaque = p.func("mystery", vec![x]);
let m = Matrix::new(vec![
vec![opaque, p.integer(1_i32)],
vec![p.integer(0_i32), p.integer(1_i32)],
])
.unwrap();
let err = nullspace_basis(&m, &p)
.expect_err("an undecidable determinant must refuse, not return the det=0 answer");
assert!(matches!(err, LinearAlgebraError::UnsupportedField));
let refusal = crate::matrix::take_zero_test_refusal().expect("recoverable refusal");
assert_eq!(refusal.code(), "E-LINALG-010");
assert!(rank(&m, &p).is_err());
let _ = crate::matrix::take_zero_test_refusal();
}
#[test]
fn a_generically_invertible_symbolic_matrix_has_a_trivial_kernel() {
let p = pool();
let x = p.symbol("x", Domain::Real);
for m in [
Matrix::new(vec![
vec![x, p.integer(0_i32)],
vec![p.integer(0_i32), p.integer(1_i32)],
])
.unwrap(),
Matrix::new(vec![
vec![x, p.integer(1_i32)],
vec![p.integer(0_i32), p.integer(1_i32)],
])
.unwrap(),
Matrix::new(vec![vec![x, p.integer(0_i32)], vec![p.integer(0_i32), x]]).unwrap(),
] {
let basis = nullspace_basis(&m, &p).expect("a generic determinant is decidable");
assert!(
basis.is_empty(),
"det is generically non-zero, so the kernel is trivial; got {} vector(s)",
basis.len()
);
assert_eq!(rank(&m, &p).unwrap() + basis.len(), m.cols);
}
}
#[test]
fn a_genuinely_singular_symbolic_matrix_still_returns_its_kernel() {
let p = pool();
let x = p.symbol("x", Domain::Real);
for m in [
Matrix::new(vec![vec![x, x], vec![x, x]]).unwrap(),
Matrix::new(vec![
vec![p.integer(1_i32), p.integer(1_i32)],
vec![p.integer(1_i32), p.integer(1_i32)],
])
.unwrap(),
{
let a = p.symbol("a", Domain::Real);
let ea = p.func("exp", vec![a]);
Matrix::new(vec![
vec![p.integer(1_i32), ea],
vec![ea, p.mul(vec![ea, ea])],
])
.unwrap()
},
] {
let basis = nullspace_basis(&m, &p).expect("a provably singular matrix has a kernel");
assert_eq!(basis.len(), 1, "rank-1 2×2 has a 1-dimensional kernel");
assert!(
kernel_vectors_are_annihilated(&m, &basis, &p),
"returned basis vector is not in the kernel"
);
assert_eq!(rank(&m, &p).unwrap() + basis.len(), m.cols);
}
}
#[test]
fn a_computable_nullspace_records_no_refusal() {
let p = pool();
let a = p.symbol("a", Domain::Real);
let exp_a = p.func("exp", vec![a]);
let m = Matrix::new(vec![
vec![p.integer(1_i32), exp_a],
vec![exp_a, p.mul(vec![exp_a, exp_a])],
])
.unwrap();
assert_eq!(nullspace_basis(&m, &p).unwrap().len(), 1);
assert_eq!(crate::matrix::take_zero_test_refusal(), None);
}
#[test]
fn rref_2x3_rational() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(1), p.integer(2), p.integer(3)],
vec![p.integer(2), p.integer(4), p.integer(6)],
])
.unwrap();
let r = rref(&m, &p).unwrap();
assert_eq!(r.rows, 2);
assert_eq!(r.cols, 3);
let one = p.integer(1_i32);
let two = p.integer(2_i32);
let three = p.integer(3_i32);
let z = p.integer(0_i32);
assert!(eigen::matrix_eq_simplified(
&r,
&Matrix::new(vec![vec![one, two, three], vec![z, z, z]]).unwrap(),
&p
));
}
#[test]
fn lu_2x2_rational() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(2), p.integer(1)],
vec![p.integer(4), p.integer(3)],
])
.unwrap();
let lu = lu_decomposition(&m, &p).unwrap();
let reconstructed = lu.l.mul(&lu.u, &p).unwrap();
let permuted = apply_row_permutation(&m, &lu.perm);
assert!(eigen::matrix_eq_simplified(&reconstructed, &permuted, &p));
}
#[test]
fn jordan_block_2x2() {
let p = pool();
let two = p.integer(2_i32);
let one = p.integer(1_i32);
let z = p.integer(0_i32);
let m = Matrix::new(vec![vec![two, one], vec![z, two]]).unwrap();
let (p_mat, j) = jordan_form(&m, &p).unwrap();
let inv = matrix_inverse(&p_mat, &p).unwrap();
let check = p_mat
.mul(&j, &p)
.unwrap()
.mul(&inv, &p)
.unwrap()
.simplify_entries(&p);
assert!(eigen::matrix_eq_simplified(&check, &m, &p));
}
#[test]
fn rational_canonical_identity_2() {
let p = pool();
let id = Matrix::identity(2, &p);
let (p_mat, c) = rational_canonical_form(&id, &p).unwrap();
assert_eq!(p_mat.rows, 2);
assert_eq!(c.rows, 2);
}
#[test]
fn rational_canonical_diagonal_1_2() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(1_i32), p.integer(0_i32)],
vec![p.integer(0_i32), p.integer(2_i32)],
])
.unwrap();
let poly_m = lambda_identity_minus_m_poly(&m, &p).unwrap();
let (s, _, _) = smith_form_poly(&poly_m);
let factors = invariant_factors_from_smith(&s).unwrap();
frobenius_p_from_cyclic_vectors(&m, &factors, &p).expect("cyclic P");
let (p_mat, c) = rational_canonical_form(&m, &p).unwrap();
assert_eq!(p_mat.rows, 2);
assert_eq!(c.rows, 2);
}
#[test]
fn matrix_exp_diagonal_shape() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(1), p.integer(0)],
vec![p.integer(0), p.integer(2)],
])
.unwrap();
let expm = matrix_exponential(&m, &p).unwrap();
assert_eq!(expm.rows, 2);
assert_eq!(expm.cols, 2);
assert!(!zero_test::zero_status(&p, expm.get(0, 0)).is_proven_zero());
assert!(!zero_test::zero_status(&p, expm.get(1, 1)).is_proven_zero());
}
#[test]
fn matrix_exp_symbolic_diagonal() {
let p = pool();
let a = p.symbol("a", Domain::Real);
let b = p.symbol("b", Domain::Real);
let z = p.integer(0_i32);
let m = Matrix::new(vec![vec![a, z], vec![z, b]]).unwrap();
let expm = matrix_exponential(&m, &p).unwrap().simplify_entries(&p);
let ea = simplify(p.func("exp", vec![a]), &p).value;
let eb = simplify(p.func("exp", vec![b]), &p).value;
let expected = Matrix::new(vec![vec![ea, z], vec![z, eb]]).unwrap();
assert!(
eigen::matrix_eq_simplified(&expm, &expected, &p),
"got {}",
expm.display(&p)
);
}
#[test]
fn matrix_exp_symbolic_oscillator_has_closed_form() {
let p = pool();
let w = p.symbol("w", Domain::Real);
let z = p.integer(0_i32);
let one = p.integer(1_i32);
let w2 = p.pow(w, p.integer(2_i32));
let neg_w2 = p.mul(vec![p.integer(-1_i32), w2]);
let a = Matrix::new(vec![vec![z, one], vec![neg_w2, z]]).unwrap();
let expm = matrix_exponential(&a, &p).expect("symbolic e^A closed form");
assert_eq!(expm.rows, 2);
assert_eq!(expm.cols, 2);
let s = expm.display(&p);
assert!(s.contains("exp"), "expected exponential entries: {s}");
assert!(s.contains('w'), "expected dependence on free symbol w: {s}");
}
fn int_matrix(rows: &[&[i32]], pool: &ExprPool) -> Matrix {
Matrix::new(
rows.iter()
.map(|r| r.iter().map(|&v| pool.integer(v)).collect())
.collect(),
)
.expect("square integer matrix")
}
fn assert_exp_entries(rows: &[&[i32]], expected: &[&[f64]]) {
let p = pool();
let a = int_matrix(rows, &p);
let e = matrix_exponential(&a, &p).expect("e^A for an integer matrix");
let env = std::collections::HashMap::new();
for (i, row) in expected.iter().enumerate() {
for (j, &want) in row.iter().enumerate() {
let got = crate::eval::eval_complex_f64(e.get(i, j), &p, &env)
.unwrap_or_else(|err| panic!("entry ({i},{j}) is not a number: {err}"));
assert!(
(got.re - want).abs() < 1e-9 && got.im.abs() < 1e-9,
"entry ({i},{j}): got {} + {}i, sympy says {want}\n{}",
got.re,
got.im,
e.display(&p)
);
}
}
}
const E1: f64 = std::f64::consts::E;
#[test]
fn matrix_exp_nilpotent_2x2_is_not_the_identity() {
assert_exp_entries(&[&[0, 1], &[0, 0]], &[&[1.0, 1.0], &[0.0, 1.0]]);
}
#[test]
fn matrix_exp_defective_off_diagonal_is_e_lambda_not_lambda_e_lambda() {
let e2 = E1 * E1;
assert_exp_entries(&[&[2, 1], &[0, 2]], &[&[e2, e2], &[0.0, e2]]);
}
#[test]
fn matrix_exp_nilpotent_3x3_keeps_the_half() {
assert_exp_entries(
&[&[0, 1, 0], &[0, 0, 1], &[0, 0, 0]],
&[&[1.0, 1.0, 0.5], &[0.0, 1.0, 1.0], &[0.0, 0.0, 1.0]],
);
}
#[test]
fn matrix_exp_full_3x3_jordan_block() {
let e2 = E1 * E1;
assert_exp_entries(
&[&[2, 1, 0], &[0, 2, 1], &[0, 0, 2]],
&[&[e2, e2, e2 / 2.0], &[0.0, e2, e2], &[0.0, 0.0, e2]],
);
}
#[test]
fn matrix_exp_two_jordan_blocks_for_one_eigenvalue() {
let e3 = E1.powi(3);
assert_exp_entries(
&[&[3, 1, 0, 0], &[0, 3, 0, 0], &[0, 0, 3, 1], &[0, 0, 0, 3]],
&[
&[e3, e3, 0.0, 0.0],
&[0.0, e3, 0.0, 0.0],
&[0.0, 0.0, e3, e3],
&[0.0, 0.0, 0.0, e3],
],
);
}
#[test]
fn matrix_exp_mixed_defective_and_simple_eigenvalue() {
assert_exp_entries(
&[&[1, 1, 0], &[0, 1, 0], &[0, 0, 5]],
&[&[E1, E1, 0.0], &[0.0, E1, 0.0], &[0.0, 0.0, E1.powi(5)]],
);
}
#[test]
fn matrix_exp_defective_but_not_triangular() {
let e2 = E1 * E1;
assert_exp_entries(&[&[1, 1], &[-1, 3]], &[&[0.0, e2], &[-e2, 2.0 * e2]]);
}
#[test]
fn matrix_exp_defective_3x3_but_not_triangular() {
let e2 = E1 * E1;
assert_exp_entries(
&[&[4, 1, 1], &[-2, 1, -1], &[0, 1, 1]],
&[
&[4.0 * e2, 2.0 * e2, e2],
&[-3.0 * e2, -e2, -e2],
&[-e2, 0.0, 0.0],
],
);
}
#[test]
fn matrix_exp_rotation_is_the_rotation_by_one_radian() {
let (c, s) = (1.0_f64.cos(), 1.0_f64.sin());
assert_exp_entries(&[&[0, 1], &[-1, 0]], &[&[c, s], &[-s, c]]);
}
#[test]
fn matrix_exp_diagonalizable_controls_do_not_regress() {
assert_exp_entries(&[&[1, 0], &[0, 2]], &[&[E1, 0.0], &[0.0, E1 * E1]]);
assert_exp_entries(
&[&[1, 2], &[3, 4]],
&[
&[51.968956198705, 74.73656456700321],
&[112.10484685050481, 164.07380304920983],
],
);
}
#[test]
fn jordan_form_never_returns_a_singular_p() {
let p = pool();
let m = int_matrix(
&[&[3, 1, 0, 0], &[0, 3, 0, 0], &[0, 0, 3, 1], &[0, 0, 0, 3]],
&p,
);
match jordan_form(&m, &p) {
Ok((pm, j)) => {
assert_eq!(
rank(&pm, &p).expect("rational rank"),
4,
"P must be a basis"
);
let inv = matrix_inverse(&pm, &p).expect("an invertible P");
let recon = pm
.mul(&j, &p)
.unwrap()
.mul(&inv, &p)
.unwrap()
.simplify_entries(&p);
assert!(
eigen::matrix_eq_simplified(&recon, &m, &p),
"P·J·P⁻¹ must be M, got {}",
recon.display(&p)
);
}
Err(e) => assert_eq!(e, LinearAlgebraError::SingularTransform, "{e}"),
}
}
#[test]
fn jordan_form_handles_two_blocks_of_different_sizes() {
let p = pool();
let m = int_matrix(&[&[0, 0, 1], &[0, 0, 0], &[0, 0, 0]], &p);
let (pm, j) = jordan_form(&m, &p).expect("J₂(0) ⊕ J₁(0)");
assert_eq!(rank(&pm, &p).expect("rational rank"), 3);
let inv = matrix_inverse(&pm, &p).expect("an invertible P");
let recon = pm
.mul(&j, &p)
.unwrap()
.mul(&inv, &p)
.unwrap()
.simplify_entries(&p);
assert!(
eigen::matrix_eq_simplified(&recon, &m, &p),
"P·J·P⁻¹ must be M, got {}",
recon.display(&p)
);
assert_exp_entries(
&[&[0, 0, 1], &[0, 0, 0], &[0, 0, 0]],
&[&[1.0, 0.0, 1.0], &[0.0, 1.0, 0.0], &[0.0, 0.0, 1.0]],
);
}
#[test]
fn matrix_exp_reports_the_eigenvalue_gap_it_divided_by() {
let p = pool();
let a = p.symbol("a", Domain::Real);
let b = p.symbol("b", Domain::Real);
let z = p.integer(0_i32);
let one = p.integer(1_i32);
let m = Matrix::new(vec![vec![a, one], vec![z, b]]).unwrap();
matrix_exponential(&m, &p).expect("the generic branch");
let conds = crate::matrix::take_matrix_exp_side_conditions();
assert_eq!(conds.len(), 1, "a − b must be reported: {conds:?}");
assert!(matches!(conds[0], crate::deriv::SideCondition::NonZero(_)));
assert!(crate::matrix::take_matrix_exp_side_conditions().is_empty());
}
#[test]
fn matrix_exp_reports_nothing_when_every_gap_is_settled() {
let p = pool();
let m = int_matrix(&[&[1, 2], &[3, 4]], &p);
matrix_exponential(&m, &p).expect("distinct rational spectrum");
assert!(crate::matrix::take_matrix_exp_side_conditions().is_empty());
let d = int_matrix(&[&[2, 1], &[0, 2]], &p);
matrix_exponential(&d, &p).expect("repeated rational spectrum");
assert!(
crate::matrix::take_matrix_exp_side_conditions().is_empty(),
"the confluent branch divides by nothing"
);
}
#[test]
fn matrix_exp_symbolic_state_matrix_t_zero_is_identity() {
let p = pool();
let k = p.symbol("k", Domain::Real);
let z = p.integer(0_i32);
let one = p.integer(1_i32);
let neg_k = p.mul(vec![p.integer(-1_i32), k]);
let a = Matrix::new(vec![vec![z, one], vec![neg_k, z]]).unwrap();
let zero_a = a.scale(z, &p);
let expm = matrix_exponential(&zero_a, &p)
.unwrap()
.simplify_entries(&p);
assert!(
eigen::matrix_eq_simplified(&expm, &Matrix::identity(2, &p), &p),
"exp(0) should be I, got {}",
expm.display(&p)
);
}
#[test]
fn symbolic_inverse_diag_s_s() {
let p = pool();
let s = p.symbol("s", Domain::Real);
let z = p.integer(0_i32);
let m = Matrix::new(vec![vec![s, z], vec![z, s]]).unwrap();
let inv = matrix_inverse(&m, &p).unwrap();
let inv_s = simplify(p.pow(s, p.integer(-1_i32)), &p).value;
let expected = Matrix::new(vec![vec![inv_s, z], vec![z, inv_s]]).unwrap();
assert!(eigen::matrix_eq_simplified(&inv, &expected, &p));
let prod = m.mul(&inv, &p).unwrap().simplify_entries(&p);
assert!(eigen::matrix_eq_simplified(
&prod,
&Matrix::identity(2, &p),
&p
));
}
#[test]
fn symbolic_inverse_bareiss_matches_cofactor_route() {
let mut taken = 0;
for n in 1..=6usize {
for seed in 0..12u64 {
let p = pool();
let m = super::super::poly_det::tests::poly_matrix(n, seed * 53 + n as u64, &p);
if n >= POLY_INVERSE_MIN_DIM
&& super::super::poly_det::PolyMatrix::from_matrix(&m, &p).is_some()
{
taken += 1;
}
INVERSE_SIDE_CONDITIONS.with(|c| c.borrow_mut().clear());
let fast = symbolic_inverse_with(&m, &p, true);
let fast_side =
INVERSE_SIDE_CONDITIONS.with(|c| std::mem::take(&mut *c.borrow_mut()));
INVERSE_SIDE_CONDITIONS.with(|c| c.borrow_mut().clear());
let slow = symbolic_inverse_with(&m, &p, false);
let slow_side =
INVERSE_SIDE_CONDITIONS.with(|c| std::mem::take(&mut *c.borrow_mut()));
assert_eq!(fast, slow, "n={n} seed={seed}");
assert_eq!(fast_side, slow_side, "n={n} seed={seed}");
}
}
assert!(taken > 25, "only {taken} matrices took the Bareiss route");
}
fn bench_symmat(n: usize, p: &ExprPool) -> Matrix {
let x = p.symbol("x", Domain::Real);
let y = p.symbol("y", Domain::Real);
let rows = (0..n)
.map(|i| {
(0..n)
.map(|j: usize| {
if i == j {
p.add(vec![x, p.integer(i as i64)])
} else {
p.add(vec![
p.pow(x, p.integer(i.abs_diff(j) as i64)),
p.mul(vec![p.integer(i as i64 + 1), y]),
])
}
})
.collect()
})
.collect();
Matrix::new(rows).unwrap()
}
#[test]
#[ignore = "timing report; also a larger differential run"]
fn symbolic_inverse_timing() {
let max_n: usize = std::env::var("INV_MAX_N")
.ok()
.and_then(|v| v.parse().ok())
.unwrap_or(7);
for n in 3..=max_n {
for (label, seed) in [("bench", None), ("dense", Some(7u64))] {
let build = |p: &ExprPool| match seed {
None => bench_symmat(n, p),
Some(s) => super::super::poly_det::tests::poly_matrix(n, s + n as u64, p),
};
let pf = pool();
let mf = build(&pf);
let t = std::time::Instant::now();
let fast = symbolic_inverse_with(&mf, &pf, true);
let t_fast = t.elapsed();
let ps = pool();
let ms = build(&ps);
let t = std::time::Instant::now();
let _ = symbolic_inverse_with(&ms, &ps, false);
let t_slow = t.elapsed();
assert_eq!(
fast,
symbolic_inverse_with(&mf, &pf, false),
"{label} n={n}"
);
println!("inverse {label} n={n}: cofactor {t_slow:?} bareiss {t_fast:?}");
}
}
}
#[test]
fn symbolic_inverse_2x2_product_is_identity() {
let p = pool();
let s = p.symbol("s", Domain::Real);
let one = p.integer(1_i32);
let two = p.integer(2_i32);
let s_plus_3 = simplify(p.add(vec![s, p.integer(3_i32)]), &p).value;
let m = Matrix::new(vec![vec![s, one], vec![two, s_plus_3]]).unwrap();
let inv = matrix_inverse(&m, &p).unwrap();
let det = simplify_expanded(m.det(&p).unwrap(), &p).value;
let det_inv = simplify(p.pow(det, p.integer(-1_i32)), &p).value;
let adj_entry = |i: usize, j: usize, p: &ExprPool| -> ExprId {
let minor_det = simplify_expanded(m.minor(j, i).det(p).unwrap(), p).value;
let sign = if (i + j) % 2 == 0 { 1_i32 } else { -1_i32 };
simplify(p.mul(vec![p.integer(sign), minor_det]), p).value
};
for i in 0..2 {
for j in 0..2 {
let expected = simplify(p.mul(vec![adj_entry(i, j, &p), det_inv]), &p).value;
assert!(
eigen::matrix_eq_simplified(
&Matrix::new(vec![vec![inv.get(i, j)]]).unwrap(),
&Matrix::new(vec![vec![expected]]).unwrap(),
&p,
),
"inverse entry [{i}][{j}] mismatch"
);
}
}
let adj = Matrix::new(
(0..2)
.map(|i| (0..2).map(|j| adj_entry(i, j, &p)).collect())
.collect(),
)
.unwrap();
let prod = m.mul(&adj, &p).unwrap();
for i in 0..2 {
for j in 0..2 {
let expected = if i == j { det } else { p.integer(0_i32) };
let diff = simplify_expanded(
p.add(vec![
prod.get(i, j),
p.mul(vec![p.integer(-1_i32), expected]),
]),
&p,
)
.value;
assert!(
zero_test::zero_status(&p, diff).is_proven_zero(),
"(A·adj)[{i}][{j}] != det·I[{i}][{j}]: {:?}",
p.get(diff)
);
}
}
}
#[test]
fn symbolic_inverse_singular_returns_error() {
let p = pool();
let s = p.symbol("s", Domain::Real);
let one = p.integer(1_i32);
let m = Matrix::new(vec![vec![s, s], vec![one, one]]).unwrap();
assert_eq!(matrix_inverse(&m, &p), Err(MatrixError::SingularMatrix));
}
#[test]
fn numeric_inverse_still_works() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(4), p.integer(7)],
vec![p.integer(2), p.integer(6)],
])
.unwrap();
let inv = matrix_inverse(&m, &p).unwrap();
let prod = m.mul(&inv, &p).unwrap().simplify_entries(&p);
assert!(eigen::matrix_eq_simplified(
&prod,
&Matrix::identity(2, &p),
&p
));
}
fn seeded_matrix(seed: u64, rows: usize, cols: usize, pool: &ExprPool) -> Matrix {
let mut s = seed.wrapping_mul(6_364_136_223_846_793_005).wrapping_add(1);
let mut next = || {
s = s
.wrapping_mul(6_364_136_223_846_793_005)
.wrapping_add(1_442_695_040_888_963_407);
((s >> 33) % 9) as i32 - 4
};
let grid: Vec<Vec<ExprId>> = (0..rows)
.map(|_| (0..cols).map(|_| pool.integer(next())).collect())
.collect();
Matrix::new(grid).expect("seeded matrix")
}
fn entries_equal(a: &Matrix, b: &Matrix, pool: &ExprPool) -> bool {
if a.rows != b.rows || a.cols != b.cols {
return false;
}
for i in 0..a.rows {
for j in 0..a.cols {
let diff = simplify_expanded(
pool.add(vec![
a.get(i, j),
pool.mul(vec![pool.integer(-1_i32), b.get(i, j)]),
]),
pool,
)
.value;
if !zero_test::zero_status(pool, diff).is_proven_zero() {
return false;
}
}
}
true
}
fn exact_rational(e: ExprId, pool: &ExprPool) -> Option<Rational> {
match pool.get(e) {
ExprData::Integer(n) => Some(Rational::from((n.0.clone(), rug::Integer::from(1)))),
ExprData::Rational(r) => Some(r.0.clone()),
ExprData::Add(args) => {
let mut acc = Rational::from(0);
for a in args {
acc += exact_rational(a, pool)?;
}
Some(acc)
}
ExprData::Mul(args) => {
let mut acc = Rational::from(1);
for a in args {
acc *= exact_rational(a, pool)?;
}
Some(acc)
}
ExprData::Pow { base, exp } => {
let b = exact_rational(base, pool)?;
let k = match pool.get(exp) {
ExprData::Integer(n) => n.0.to_i32()?,
_ => return None,
};
if k < 0 && b == 0 {
return None;
}
let mut acc = Rational::from(1);
for _ in 0..k.unsigned_abs() {
acc *= b.clone();
}
Some(if k >= 0 { acc } else { Rational::from(1) / acc })
}
_ => None,
}
}
fn permute_rows(m: &Matrix, perm: &[usize]) -> Matrix {
Matrix::new(perm.iter().map(|&r| m.row(r)).collect()).expect("permuted")
}
#[test]
fn lu_partial_pivot_permutes_l() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(2_i32), p.integer(-1_i32), p.integer(4_i32)],
vec![p.integer(-1_i32), p.integer(1_i32), p.integer(-1_i32)],
vec![p.integer(3_i32), p.integer(-4_i32), p.integer(0_i32)],
])
.unwrap();
let lu = lu_decomposition(&m, &p).unwrap();
let reconstructed = lu.l.mul(&lu.u, &p).unwrap();
let permuted = permute_rows(&m, &lu.perm);
assert!(
eigen::matrix_eq_simplified(&reconstructed, &permuted, &p),
"P·A = L·U must hold; perm = {:?}",
lu.perm
);
}
#[test]
fn lu_reconstructs_every_random_shape() {
let p = pool();
for seed in 0..120u64 {
let rows = 1 + (seed % 5) as usize;
let cols = 1 + ((seed / 5) % 5) as usize;
let m = seeded_matrix(seed, rows, cols, &p);
let lu = lu_decomposition(&m, &p).unwrap();
assert_eq!(lu.l.rows, rows);
assert_eq!(lu.l.cols, rows);
assert_eq!(lu.u.rows, rows);
assert_eq!(lu.u.cols, cols);
let mut sorted = lu.perm.clone();
sorted.sort_unstable();
assert_eq!(
sorted,
(0..rows).collect::<Vec<_>>(),
"perm must be a permutation, got {:?}",
lu.perm
);
for i in 0..rows {
assert_eq!(lu.l.get(i, i), p.integer(1_i32), "seed {seed}: L diagonal");
for j in (i + 1)..rows {
assert_eq!(
simplify(lu.l.get(i, j), &p).value,
p.integer(0_i32),
"seed {seed}: L must be lower triangular"
);
}
}
for i in 0..rows {
for j in 0..i.min(cols) {
assert_eq!(
simplify(lu.u.get(i, j), &p).value,
p.integer(0_i32),
"seed {seed}: U must be upper triangular"
);
}
}
let reconstructed = lu.l.mul(&lu.u, &p).unwrap();
let permuted = permute_rows(&m, &lu.perm);
assert!(
eigen::matrix_eq_simplified(&reconstructed, &permuted, &p),
"seed {seed}: P·A = L·U failed, perm = {:?}",
lu.perm
);
}
}
#[test]
fn row_space_of_a_swapped_matrix_is_not_the_zero_vector() {
let p = pool();
let z = p.integer(0_i32);
let one = p.integer(1_i32);
let m = Matrix::new(vec![vec![z, z], vec![one, z]]).unwrap();
let basis = row_space_basis(&m, &p).unwrap();
assert_eq!(basis.len(), 1);
let row = &basis[0];
assert_eq!(row.rows, 1);
assert_eq!(row.cols, 2);
assert_eq!(simplify(row.get(0, 0), &p).value, one);
assert_eq!(simplify(row.get(0, 1), &p).value, z);
}
#[test]
fn row_space_basis_of_a_duplicated_row_is_independent() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(1_i32), p.integer(2_i32), p.integer(3_i32)],
vec![p.integer(1_i32), p.integer(2_i32), p.integer(3_i32)],
vec![p.integer(0_i32), p.integer(1_i32), p.integer(0_i32)],
])
.unwrap();
let basis = row_space_basis(&m, &p).unwrap();
assert_eq!(basis.len(), 2, "rank is 2");
let stacked = Matrix::new(basis.iter().map(|b| b.row(0)).collect()).unwrap();
assert_eq!(
rank(&stacked, &p).unwrap(),
2,
"a basis must be independent"
);
}
#[test]
fn row_space_basis_is_a_basis_for_every_random_shape() {
let p = pool();
for seed in 200..280u64 {
let rows = 1 + (seed % 4) as usize;
let cols = 1 + ((seed / 4) % 4) as usize;
let mut grid: Vec<Vec<ExprId>> = (0..rows)
.map(|r| seeded_matrix(seed + r as u64, 1, cols, &p).row(0))
.collect();
if rows > 1 && seed % 2 == 0 {
grid[0] = grid[rows - 1].clone();
}
let m = Matrix::new(grid).unwrap();
let r = rank(&m, &p).unwrap();
let basis = row_space_basis(&m, &p).unwrap();
assert_eq!(basis.len(), r, "seed {seed}: basis size must equal rank");
if r == 0 {
continue;
}
let stacked = Matrix::new(basis.iter().map(|b| b.row(0)).collect()).unwrap();
assert_eq!(
rank(&stacked, &p).unwrap(),
r,
"seed {seed}: basis must be independent"
);
let mut all: Vec<Vec<ExprId>> = (0..rows).map(|i| m.row(i)).collect();
all.extend(basis.iter().map(|b| b.row(0)));
let combined = Matrix::new(all).unwrap();
assert_eq!(
rank(&combined, &p).unwrap(),
r,
"seed {seed}: basis must stay inside row(m)"
);
}
}
#[test]
fn minimal_polynomial_of_the_zero_matrix_is_lambda() {
let p = pool();
for n in 1..=4 {
let m = Matrix::zeros(n, n, &p);
let (poly, lam) = minimal_polynomial(&m, &p).unwrap();
assert_eq!(
simplify(poly, &p).value,
simplify(lam, &p).value,
"minimal polynomial of the {n}×{n} zero matrix is λ, not λ^k"
);
}
}
#[test]
fn minimal_polynomial_of_two_equal_nilpotent_blocks_is_lambda_squared() {
let p = pool();
let z = p.integer(0_i32);
let one = p.integer(1_i32);
let m = Matrix::new(vec![
vec![z, one, z, z],
vec![z, z, z, z],
vec![z, z, z, one],
vec![z, z, z, z],
])
.unwrap();
let (poly, lam) = minimal_polynomial(&m, &p).unwrap();
let expected = simplify(p.pow(lam, p.integer(2_i32)), &p).value;
assert_eq!(simplify(poly, &p).value, expected);
}
#[test]
fn minimal_polynomial_of_a_single_nilpotent_block_is_lambda_to_its_size() {
let p = pool();
for size in 1..=4usize {
let mut m = Matrix::zeros(size, size, &p);
for i in 0..size.saturating_sub(1) {
m.set(i, i + 1, p.integer(1_i32));
}
let (poly, lam) = minimal_polynomial(&m, &p).unwrap();
let expected = if size == 1 {
simplify(lam, &p).value
} else {
simplify(p.pow(lam, p.integer(size as i32)), &p).value
};
assert_eq!(
simplify(poly, &p).value,
expected,
"J_{size}(0) is annihilated by λ^{size} and nothing smaller"
);
}
}
#[test]
fn rational_canonical_form_reconstructs_the_matrix() {
let p = pool();
let cases: Vec<Vec<Vec<i32>>> = vec![
vec![vec![1, 0], vec![0, 2]],
vec![vec![1, 1], vec![0, 1]],
vec![vec![4, -1], vec![-2, -2]],
vec![vec![0, 1], vec![0, 0]],
vec![vec![2, 0, 0], vec![0, 2, 0], vec![0, 0, 3]],
vec![vec![1, 2, 3], vec![0, 1, 4], vec![0, 0, 1]],
vec![vec![0, 0, 0], vec![0, 0, 0], vec![0, 0, 0]],
vec![vec![2, 1, 0], vec![0, 2, 1], vec![0, 0, 2]],
];
for case in cases {
let n = case.len();
let m = Matrix::new(
case.iter()
.map(|r| r.iter().map(|&v| p.integer(v)).collect())
.collect(),
)
.unwrap();
let (pm, c) = match rational_canonical_form(&m, &p) {
Ok(v) => v,
Err(_) => continue,
};
let lhs = m.mul(&pm, &p).unwrap();
let rhs = pm.mul(&c, &p).unwrap();
assert!(
eigen::matrix_eq_simplified(&lhs, &rhs, &p),
"M·P = P·C failed for {case:?}"
);
assert_eq!(
rank(&pm, &p).unwrap(),
n,
"P must be invertible for {case:?}"
);
}
}
#[test]
fn rational_canonical_form_preserves_determinant_and_trace() {
let p = pool();
for seed in 400..460u64 {
let n = 2 + (seed % 3) as usize;
let m = seeded_matrix(seed, n, n, &p);
let Ok((_, c)) = rational_canonical_form(&m, &p) else {
continue;
};
let det_m = simplify_expanded(m.det(&p).unwrap(), &p).value;
let det_c = simplify_expanded(c.det(&p).unwrap(), &p).value;
assert_eq!(det_m, det_c, "seed {seed}: det must be preserved");
let tr = |x: &Matrix| {
let terms: Vec<ExprId> = (0..n).map(|i| x.get(i, i)).collect();
simplify_expanded(p.add(terms), &p).value
};
assert_eq!(tr(&m), tr(&c), "seed {seed}: trace must be preserved");
}
}
#[test]
fn rational_canonical_form_is_right_or_refuses() {
let p = pool();
let mut answered = 0usize;
for seed in 500..560u64 {
let n = 1 + (seed % 4) as usize;
let m = seeded_matrix(seed, n, n, &p);
let Ok((pm, c)) = rational_canonical_form(&m, &p) else {
continue;
};
answered += 1;
let lhs = m.mul(&pm, &p).unwrap();
let rhs = pm.mul(&c, &p).unwrap();
assert!(
eigen::matrix_eq_simplified(&lhs, &rhs, &p),
"seed {seed}: M·P = P·C failed"
);
assert_eq!(rank(&pm, &p).unwrap(), n, "seed {seed}: P singular");
}
assert!(
answered > 30,
"the gate must not have turned this into a refusal machine; \
only {answered} of 60 answered"
);
}
#[test]
fn cholesky_refuses_a_non_symmetric_matrix() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(1_i32), p.integer(5_i32)],
vec![p.integer(0_i32), p.integer(1_i32)],
])
.unwrap();
assert_eq!(
cholesky(&m, &p),
Err(LinearAlgebraError::NotPositiveDefinite),
"a non-symmetric matrix has no Cholesky factor"
);
}
#[test]
fn cholesky_of_2i_is_sqrt2_i() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(2_i32), p.integer(0_i32)],
vec![p.integer(0_i32), p.integer(2_i32)],
])
.unwrap();
let l = cholesky(&m, &p).unwrap();
let recon = l.mul(&l.transpose(), &p).unwrap();
assert!(
entries_equal(&recon, &m, &p),
"L·Lᵀ must be 2I, got {:?}",
recon.entries()
);
let sqrt2 = simplify(p.func("sqrt", vec![p.integer(2_i32)]), &p).value;
assert_eq!(simplify(l.get(0, 0), &p).value, sqrt2);
assert_eq!(simplify(l.get(1, 0), &p).value, p.integer(0_i32));
}
#[test]
fn cholesky_reconstructs_every_spd_matrix() {
let p = pool();
let mut answered = 0usize;
for seed in 600..660u64 {
let n = 1 + (seed % 4) as usize;
let b = seeded_matrix(seed, n, n, &p);
let m = b
.transpose()
.mul(&b, &p)
.unwrap()
.add(&Matrix::identity(n, &p).scale(p.integer(n as i32), &p), &p)
.unwrap()
.simplify_entries(&p);
let l = cholesky(&m, &p).expect("BᵀB + nI is positive definite");
answered += 1;
for i in 0..n {
for j in (i + 1)..n {
assert_eq!(
simplify(l.get(i, j), &p).value,
p.integer(0_i32),
"seed {seed}: L must be lower triangular"
);
}
}
let recon = l.mul(&l.transpose(), &p).unwrap();
assert!(
entries_equal(&recon, &m, &p),
"seed {seed}: L·Lᵀ = M failed"
);
}
assert_eq!(answered, 60);
}
#[test]
fn cholesky_still_refuses_an_indefinite_matrix() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(1_i32), p.integer(2_i32)],
vec![p.integer(2_i32), p.integer(1_i32)],
])
.unwrap();
assert_eq!(
cholesky(&m, &p),
Err(LinearAlgebraError::NotPositiveDefinite),
"[[1,2],[2,1]] has eigenvalues 3 and −1"
);
let neg = Matrix::new(vec![
vec![p.integer(-1_i32), p.integer(0_i32)],
vec![p.integer(0_i32), p.integer(-1_i32)],
])
.unwrap();
assert_eq!(
cholesky(&neg, &p),
Err(LinearAlgebraError::NotPositiveDefinite)
);
}
#[test]
fn symbolic_inverse_handles_a_fully_symbolic_six_by_six() {
let p = pool();
for n in 1..=6usize {
let grid: Vec<Vec<ExprId>> = (0..n)
.map(|i| {
(0..n)
.map(|j| p.symbol(format!("a{n}_{i}_{j}"), Domain::Complex))
.collect()
})
.collect();
let m = Matrix::new(grid).unwrap();
let inv = matrix_inverse(&m, &p)
.unwrap_or_else(|e| panic!("{n}×{n} fully symbolic inverse refused: {e}"));
let prod = inv.mul(&m, &p).unwrap();
let id = Matrix::identity(n, &p);
for round in 0..3usize {
let mut map = std::collections::HashMap::new();
let mut seed = 0x2545_F491_4F6C_DD1Du64 ^ (round as u64).wrapping_mul(0x9E37_79B9);
let mut next = || {
seed ^= seed << 13;
seed ^= seed >> 7;
seed ^= seed << 17;
(seed % 199) as i64 + 1
};
for i in 0..n {
for j in 0..n {
let sym = p.symbol(format!("a{n}_{i}_{j}"), Domain::Complex);
map.insert(sym, p.rational(next(), 7 + 2 * round as i64));
}
}
for i in 0..n {
for j in 0..n {
let diff = p.add(vec![
prod.get(i, j),
p.mul(vec![p.integer(-1_i32), id.get(i, j)]),
]);
let at = crate::kernel::subs::subs(diff, &map, &p);
let v = exact_rational(at, &p).unwrap_or_else(|| {
panic!(
"{n}×{n} round {round}: entry ({i},{j}) is not a constant: {}",
crate::kernel::display::render_unicode(at, &p)
)
});
assert_eq!(
v,
Rational::from(0),
"{n}×{n} round {round}: (A⁻¹·A − I)[{i}][{j}] must vanish"
);
}
}
}
}
}
#[test]
fn symbolic_inverse_still_refuses_a_dependent_row() {
let p = pool();
for n in 2..=5usize {
let grid: Vec<Vec<ExprId>> = (0..n)
.map(|i| {
(0..n)
.map(|j| {
let s = p.symbol(format!("b_{i}_{j}"), Domain::Complex);
if i == 1 {
let s0 = p.symbol(format!("b_0_{j}"), Domain::Complex);
p.mul(vec![p.integer(2_i32), s0])
} else {
s
}
})
.collect()
})
.collect();
let m = Matrix::new(grid).unwrap();
assert_eq!(
matrix_inverse(&m, &p),
Err(MatrixError::SingularMatrix),
"{n}×{n} with a dependent row must be refused"
);
}
}
#[test]
fn a_symbolic_inverse_reports_the_determinant_it_divided_by() {
let p = pool();
let a = p.symbol("a", Domain::Complex);
let b = p.symbol("b", Domain::Complex);
let c = p.symbol("c", Domain::Complex);
let d = p.symbol("d", Domain::Complex);
let m = Matrix::new(vec![vec![a, b], vec![c, d]]).unwrap();
let _ = crate::matrix::take_matrix_inverse_side_conditions();
matrix_inverse(&m, &p).expect("[[a,b],[c,d]] is generically invertible");
let conds = crate::matrix::take_matrix_inverse_side_conditions();
assert_eq!(conds.len(), 1, "exactly one hypothesis: det ≠ 0");
let crate::deriv::SideCondition::NonZero(recorded) = conds[0] else {
panic!("the inverse's hypothesis is a non-vanishing one");
};
let want = simplify_expanded(
p.add(vec![
p.mul(vec![a, d]),
p.mul(vec![p.integer(-1_i32), b, c]),
]),
&p,
)
.value;
let diff = simplify_expanded(
p.add(vec![recorded, p.mul(vec![p.integer(-1_i32), want])]),
&p,
)
.value;
assert!(
zero_test::zero_status(&p, diff).is_proven_zero(),
"the recorded condition must be the determinant itself"
);
assert!(crate::matrix::take_matrix_inverse_side_conditions().is_empty());
}
#[test]
fn a_constant_determinant_records_no_hypothesis() {
let p = pool();
let _ = crate::matrix::take_matrix_inverse_side_conditions();
let m = Matrix::new(vec![
vec![p.integer(1_i32), p.integer(2_i32)],
vec![p.integer(3_i32), p.integer(4_i32)],
])
.unwrap();
matrix_inverse(&m, &p).unwrap();
assert!(crate::matrix::take_matrix_inverse_side_conditions().is_empty());
let s = p.symbol("s", Domain::Complex);
let shear = Matrix::new(vec![
vec![p.integer(1_i32), s],
vec![p.integer(0_i32), p.integer(1_i32)],
])
.unwrap();
matrix_inverse(&shear, &p).unwrap();
assert!(
crate::matrix::take_matrix_inverse_side_conditions().is_empty(),
"det = 1 is not a hypothesis"
);
}
#[test]
fn a_refused_inverse_records_no_hypothesis() {
let p = pool();
let a = p.symbol("q", Domain::Complex);
let b = p.symbol("r", Domain::Complex);
let m = Matrix::new(vec![
vec![a, b],
vec![
p.mul(vec![p.integer(2_i32), a]),
p.mul(vec![p.integer(2_i32), b]),
],
])
.unwrap();
let _ = crate::matrix::take_matrix_inverse_side_conditions();
assert_eq!(matrix_inverse(&m, &p), Err(MatrixError::SingularMatrix));
assert!(crate::matrix::take_matrix_inverse_side_conditions().is_empty());
}
fn assert_gram_is_identity(q: &Matrix, pool: &ExprPool, what: &str) {
for a in 0..q.cols {
for b in 0..q.cols {
let mut acc = 0.0_f64;
for i in 0..q.rows {
let env = std::collections::HashMap::new();
let x = crate::eval::eval_f64(simplify(q.get(i, a), pool).value, pool, &env)
.unwrap_or_else(|e| panic!("{what}: Q[{i}][{a}] did not evaluate: {e}"));
let y = crate::eval::eval_f64(simplify(q.get(i, b), pool).value, pool, &env)
.unwrap_or_else(|e| panic!("{what}: Q[{i}][{b}] did not evaluate: {e}"));
acc += x * y;
}
let want = if a == b { 1.0 } else { 0.0 };
assert!(
(acc - want).abs() < 1e-9,
"{what}: (QᵀQ)[{a}][{b}] = {acc}, want {want}"
);
}
}
}
#[test]
fn qr_returns_an_orthonormal_q_for_every_square_or_tall_shape() {
let p = pool();
let mut checked = 0usize;
let mut deficient = 0usize;
for seed in 700..780u64 {
let cols = 1 + (seed % 4) as usize;
let rows = cols + ((seed / 4) % 3) as usize;
let mut m = seeded_matrix(seed, rows, cols, &p);
if cols > 1 && seed % 2 == 0 {
for i in 0..rows {
m.set(
i,
cols - 1,
simplify(p.mul(vec![p.integer(2_i32), m.get(i, 0)]), &p).value,
);
}
deficient += 1;
}
let qr = qr_decomposition(&m, &p).expect("integer entries are decidable");
assert_eq!(qr.q.rows, rows);
assert_eq!(qr.q.cols, cols);
let recon = qr.q.mul(&qr.r, &p).unwrap();
assert!(entries_equal(&recon, &m, &p), "seed {seed}: Q·R = A failed");
assert_gram_is_identity(&qr.q, &p, &format!("seed {seed}"));
for i in 0..qr.r.rows {
for j in 0..i.min(qr.r.cols) {
assert_eq!(
simplify(qr.r.get(i, j), &p).value,
p.integer(0_i32),
"seed {seed}: R must be upper triangular"
);
}
}
checked += 1;
}
assert_eq!(checked, 80);
assert!(
deficient >= 20,
"only {deficient} rank-deficient shapes were built"
);
}
#[test]
fn qr_of_a_rank_one_two_by_two_has_an_invertible_q() {
let p = pool();
let m = Matrix::new(vec![
vec![p.integer(-4_i32), p.integer(-4_i32)],
vec![p.integer(-2_i32), p.integer(-2_i32)],
])
.unwrap();
let qr = qr_decomposition(&m, &p).unwrap();
assert!(entries_equal(&qr.q.mul(&qr.r, &p).unwrap(), &m, &p));
assert_gram_is_identity(&qr.q, &p, "[[-4,-4],[-2,-2]]");
assert_eq!(simplify(qr.r.get(1, 1), &p).value, p.integer(0_i32));
assert_eq!(simplify(qr.r.get(1, 0), &p).value, p.integer(0_i32));
}
fn arb_grid() -> impl proptest::strategy::Strategy<Value = (usize, usize, Vec<Vec<Rational>>)> {
use proptest::prelude::*;
let entry = prop_oneof![
4 => (-6i64..7).prop_map(Rational::from),
2 => (-30i64..30, 1i64..8).prop_map(|(a, b)| Rational::from((a, b))),
1 => (any::<i64>(), any::<i64>()).prop_map(|(a, b)| {
Rational::from(rug::Integer::from(a) * rug::Integer::from(b) * 3u32)
}),
3 => Just(Rational::new()),
];
(0usize..7, 0usize..7).prop_flat_map(move |(r, c)| {
(
Just(r),
Just(c),
prop::collection::vec(prop::collection::vec(entry.clone(), c), r),
)
})
}
proptest::proptest! {
#[test]
fn rref_flint_matches_gauss_jordan((r, c, g) in arb_grid(), dup in 0usize..7, k in -2i64..3) {
proptest::prop_assert_eq!(
rational_row_echelon_pivots(&g, r, c),
rational_row_echelon_reference(&g, r, c)
);
if r >= 2 {
let mut g = g;
let d = dup % (r - 1);
let new_row: Vec<Rational> = (0..c)
.map(|j| Rational::from(k) * g[d][j].clone() + g[0][j].clone())
.collect();
g[r - 1] = new_row;
proptest::prop_assert_eq!(
rational_row_echelon_pivots(&g, r, c),
rational_row_echelon_reference(&g, r, c)
);
}
}
}
#[test]
fn rref_flint_small_cases() {
let q = |a: i64, b: i64| Rational::from((a, b));
for (g, r, c) in [
(vec![], 0, 0),
(vec![], 0, 3),
(vec![vec![], vec![]], 2, 0),
(vec![vec![q(0, 1)]], 1, 1),
(vec![vec![q(-3, 4)]], 1, 1),
(vec![vec![q(0, 1), q(0, 1)], vec![q(1, 1), q(0, 1)]], 2, 2),
(vec![vec![q(0, 1); 3]; 3], 3, 3),
] {
assert_eq!(
rational_row_echelon_pivots(&g, r, c),
rational_row_echelon_reference(&g, r, c),
"{g:?}"
);
}
}
#[test]
#[ignore]
fn rref_timing() {
let _ = rational_row_echelon_pivots(&[vec![Rational::from(1)]], 1, 1);
for &(r, c) in &[
(3usize, 3usize),
(4, 6),
(10, 10),
(20, 25),
(40, 40),
(60, 50),
] {
let g: Vec<Vec<Rational>> = (0..r)
.map(|i| {
(0..c)
.map(|j| {
let k = (i * c + j) as i64;
let v = ((k * 7919 + 13) % 201) - 100;
if k % 5 == 0 {
Rational::from((v, 7))
} else {
Rational::from(v)
}
})
.collect()
})
.collect();
let reps = if r <= 5 {
2000
} else if r <= 20 {
20
} else {
3
};
let t = std::time::Instant::now();
let mut a = None;
for _ in 0..reps {
a = Some(rational_row_echelon_reference(&g, r, c));
}
let old = t.elapsed().as_secs_f64() * 1e3 / reps as f64;
let t = std::time::Instant::now();
let mut b = None;
for _ in 0..reps {
b = Some(rational_row_echelon_pivots(&g, r, c));
}
let new = t.elapsed().as_secs_f64() * 1e3 / reps as f64;
assert_eq!(a, b);
println!("rref {r}x{c}: gauss-jordan {old:.3} ms, flint {new:.3} ms");
}
}
}