use num_bigint::BigInt;
use num_traits::{One, Signed, ToPrimitive, Zero};
use rustc_hash::{FxHashMap, FxHashSet};
use crate::api::expr::{Ex, SimplifyOpts};
use crate::base::arena::Arena;
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode};
use crate::domains::exact_matrix::QMatrix;
use crate::domains::linprog::Q;
use crate::domains::matrix::{RF_POINTS, RF_TERM_BUDGET, rf_sample_point};
use crate::poly::multipoly::{GrevLex, MultiPoly};
#[derive(Debug, Clone)]
pub(crate) struct SymbolicLinearResult {
pub values: Vec<Ex>,
pub free: Vec<usize>,
pub inconsistent: bool,
}
pub(crate) fn extract_linear_row(
arena: &mut Arena,
eq: ExprId,
vars: &[ExprId],
) -> Option<(Vec<ExprId>, ExprId)> {
let expanded = crate::transforms::expand::expand(arena, eq);
let expanded = crate::transforms::eval::eval(arena, expanded);
let terms: Vec<ExprId> = match arena.node(expanded).clone() {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expanded],
};
let mut coeff_parts: Vec<Vec<ExprId>> = vec![Vec::new(); vars.len()];
let mut const_parts: Vec<ExprId> = Vec::new();
for term in terms {
let mut which: Option<usize> = None;
for (i, &v) in vars.iter().enumerate() {
if crate::base::walk::contains(arena, term, v) {
if which.is_some() {
return None; }
which = Some(i);
}
}
match which {
None => const_parts.push(term),
Some(i) => {
let coeffs = crate::transforms::solve::symbolic_poly_coeffs(arena, term, vars[i])?;
if coeffs.len() != 2 || !arena.is_zero_structural(coeffs[0]) {
return None;
}
coeff_parts[i].push(coeffs[1]);
}
}
}
let mut coeffs = Vec::with_capacity(vars.len());
for parts in coeff_parts {
let c = match parts.len() {
0 => arena.zero,
1 => parts[0],
_ => arena.add(&parts),
};
coeffs.push(crate::transforms::eval::eval(arena, c));
}
let constant = match const_parts.len() {
0 => arena.zero,
1 => const_parts[0],
_ => arena.add(&const_parts),
};
let rhs = arena.neg(constant);
let rhs = crate::transforms::eval::eval(arena, rhs);
Some((coeffs, rhs))
}
fn tidy(e: &Ex) -> Ex {
let s = e.eval().simplify();
let (num, den) = s.together().as_numer_denom();
let num = num.expand().eval();
let t = if den.is_one_structural() {
num
} else {
(&num / &den).eval()
};
if t.count_ops() <= s.count_ops() + 1 {
t
} else {
s
}
}
fn normalize(e: &Ex) -> Ex {
let e1 = e.eval();
if e1.is_zero_structural() || is_number(&e1) {
return e1;
}
let e2 = e1.simplify_with(&SimplifyOpts::single_pass());
if e2.count_ops() <= e1.count_ops() {
e2
} else {
e1
}
}
fn is_number(e: &Ex) -> bool {
e.inner.read().arena.as_num(e.raw_id()).is_some()
}
fn is_nonzero_number(e: &Ex) -> bool {
e.inner
.read()
.arena
.as_num(e.raw_id())
.is_some_and(|r| !r.is_zero())
}
fn entry_is_zero(e: &Ex) -> bool {
if e.is_zero_structural() {
return true;
}
if is_number(e) {
return false;
}
if let Some(b) = crate::domains::matrix::rational_function_is_zero(e) {
return b;
}
let s = e.simplify();
if s.is_zero_structural() {
return true;
}
if s.free_symbols().is_empty() && s.eval_complex64().is_ok() {
let mut inner = s.inner.write();
return crate::poly::algebraic::is_zero_checked(&mut inner.arena, s.raw_id()) == Some(true);
}
false
}
pub(crate) fn rref_solve(
rows: Vec<Vec<Ex>>,
rhs: Vec<Ex>,
unknowns: &[Ex],
) -> Result<SymbolicLinearResult, SymplexError> {
let n_vars = unknowns.len();
let m = rows.len();
if m == 0 || n_vars == 0 {
return Err(SymplexError::InvalidArgument {
operation: "linsolve",
reason: "need at least one equation and one unknown".into(),
});
}
let mut mat: Vec<Vec<Ex>> = rows
.into_iter()
.zip(rhs)
.map(|(mut r, b)| {
r.push(b);
r
})
.collect();
let ncols = n_vars + 1;
for r in &mat {
if r.len() != ncols {
return Err(SymplexError::InvalidArgument {
operation: "linsolve",
reason: "row length does not match number of unknowns".into(),
});
}
}
if let Some(result) = rref_solve_rational(&mat, n_vars, unknowns) {
return Ok(result);
}
if let Some(result) = rref_solve_algebraic(&mat, n_vars, unknowns) {
return Ok(result);
}
let mut pivots: Vec<usize> = Vec::new();
let mut pivot_row = 0usize;
for col in 0..n_vars {
if pivot_row >= m {
break;
}
let mut numeric: Option<usize> = None;
let mut symbolic: Option<(usize, usize)> = None; for (r, row) in mat.iter_mut().enumerate().skip(pivot_row) {
let e = normalize(&row[col]);
row[col] = e.clone();
if is_nonzero_number(&e) {
numeric = Some(r);
break;
}
if !entry_is_zero(&e) {
let ops = e.count_ops();
if symbolic.is_none_or(|(_, o)| ops < o) {
symbolic = Some((r, ops));
}
} else {
row[col] = e.context().zero();
}
}
let found = match (numeric, symbolic) {
(Some(r), _) => r,
(None, Some((r, _))) => r,
(None, None) => continue,
};
if found != pivot_row {
mat.swap(pivot_row, found);
}
let pv = mat[pivot_row][col].clone();
for (j, entry) in mat[pivot_row].iter_mut().enumerate() {
if j == col {
*entry = pv.context().one();
} else {
let v = &*entry / &pv;
*entry = normalize(&v);
}
}
let pivot_vals = mat[pivot_row].clone();
for (i, row) in mat.iter_mut().enumerate() {
if i == pivot_row {
continue;
}
let factor = normalize(&row[col]);
if entry_is_zero(&factor) {
row[col] = factor.context().zero();
continue;
}
for (j, entry) in row.iter_mut().enumerate() {
if j == col {
*entry = factor.context().zero();
} else {
let t = &factor * &pivot_vals[j];
let v = &*entry - &t;
*entry = normalize(&v);
}
}
}
pivots.push(col);
pivot_row += 1;
}
for r in &mat {
let all_zero = r[..n_vars].iter().all(entry_is_zero);
if all_zero && !entry_is_zero(&r[n_vars]) {
return Ok(SymbolicLinearResult {
values: Vec::new(),
free: Vec::new(),
inconsistent: true,
});
}
}
let pivot_set: std::collections::HashSet<usize> = pivots.iter().copied().collect();
let free: Vec<usize> = (0..n_vars).filter(|c| !pivot_set.contains(c)).collect();
let mut values: Vec<Ex> = unknowns.to_vec();
for (r, &pc) in pivots.iter().enumerate() {
let mut v = mat[r][n_vars].clone();
for &f in &free {
if !entry_is_zero(&mat[r][f]) {
let t = &mat[r][f] * &unknowns[f];
v = &v - &t;
}
}
values[pc] = tidy(&v);
}
Ok(SymbolicLinearResult {
values,
free,
inconsistent: false,
})
}
fn rref_solve_rational(
mat: &[Vec<Ex>],
n_vars: usize,
unknowns: &[Ex],
) -> Option<SymbolicLinearResult> {
let rows: Vec<Vec<Q>> = mat
.iter()
.map(|r| r.iter().map(Ex::as_rational).collect())
.collect::<Option<_>>()?;
let aug = QMatrix::new(rows).ok()?;
let (r, pivots) = aug.rref_limited(n_vars);
let rank = pivots.len();
if (rank..aug.nrows()).any(|i| !r[(i, n_vars)].is_zero()) {
return Some(SymbolicLinearResult {
values: Vec::new(),
free: Vec::new(),
inconsistent: true,
});
}
let mut is_pivot = vec![false; n_vars];
for &c in &pivots {
is_pivot[c] = true;
}
let free: Vec<usize> = (0..n_vars).filter(|&c| !is_pivot[c]).collect();
let ctx = unknowns.first()?.context();
let mut values: Vec<Ex> = unknowns.to_vec();
for (i, &pc) in pivots.iter().enumerate() {
let mut v = ctx.from_ratio(r[(i, n_vars)].clone());
for &f in &free {
let c = &r[(i, f)];
if !c.is_zero() {
v -= ctx.from_ratio(c.clone()) * &unknowns[f];
}
}
values[pc] = if free.is_empty() { v } else { tidy(&v) };
}
Some(SymbolicLinearResult {
values,
free,
inconsistent: false,
})
}
pub(crate) fn linsolve_symbolic(
eqs: &[Ex],
vars: &[Ex],
) -> Result<SymbolicLinearResult, SymplexError> {
if eqs.is_empty() || vars.is_empty() {
return Err(SymplexError::InvalidArgument {
operation: "linsolve",
reason: "need at least one equation and one unknown".into(),
});
}
let first = &eqs[0];
let var_ids: Vec<ExprId> = vars.iter().map(|v| first.checked_id(v)).collect();
let eq_ids: Vec<ExprId> = eqs.iter().map(|e| first.checked_id(e)).collect();
let mut rows: Vec<Vec<ExprId>> = Vec::with_capacity(eqs.len());
let mut rhs: Vec<ExprId> = Vec::with_capacity(eqs.len());
{
let mut guard = first.inner.write();
let arena = &mut guard.arena;
for (k, &eq) in eq_ids.iter().enumerate() {
match extract_linear_row(arena, eq, &var_ids) {
Some((coeffs, b)) => {
rows.push(coeffs);
rhs.push(b);
}
None => {
let shown = arena.display(eq).to_string();
drop(guard);
return Err(SymplexError::InvalidArgument {
operation: "linsolve",
reason: format!("equation {k} is not linear in the unknowns: {shown}"),
});
}
}
}
}
let rows: Vec<Vec<Ex>> = rows
.into_iter()
.map(|r| r.into_iter().map(|id| first.wrap(id)).collect())
.collect();
let rhs: Vec<Ex> = rhs.into_iter().map(|id| first.wrap(id)).collect();
rref_solve(rows, rhs, vars)
}
type Mp = MultiPoly<GrevLex>;
const MAX_ROOT_INDEX: u32 = 12;
const MAX_POWER: u64 = 64;
const TRIAL_DIVISION_BOUND: u64 = 1 << 16;
const KFRAC_TERM_BUDGET: usize = 20_000;
pub(crate) struct Tower {
syms: Vec<ExprId>,
primes: Vec<BigInt>,
l: u32,
has_i: bool,
atoms: FxHashMap<ExprId, Mp>,
ext: Option<Mp>,
}
type FracResult = Option<Option<(Mp, Mp)>>;
fn small_factorization(n: &BigInt) -> Option<Vec<(BigInt, u32)>> {
let mut m = n.to_u64()?;
if m == 0 {
return None;
}
let mut out = Vec::new();
let mut p = 2u64;
while p * p <= m && p < TRIAL_DIVISION_BOUND {
if m % p == 0 {
let mut e = 0u32;
while m % p == 0 {
m /= p;
e += 1;
}
out.push((BigInt::from(p), e));
}
p += if p == 2 { 1 } else { 2 };
}
if m > 1 {
if p * p <= m {
return None;
}
out.push((BigInt::from(m), 1));
}
Some(out)
}
fn gcd_u32(mut a: u32, mut b: u32) -> u32 {
while b != 0 {
(a, b) = (b, a % b);
}
a
}
impl Tower {
fn scan(arena: &Arena, roots: &[ExprId]) -> Option<Tower> {
Self::scan_opts(arena, roots, false)
}
fn scan_opts(arena: &Arena, roots: &[ExprId], conjugates: bool) -> Option<Tower> {
let mut syms: Vec<ExprId> = Vec::new();
let mut radicals: Vec<(ExprId, Q, i64, u32)> = Vec::new();
let mut golden: Vec<ExprId> = Vec::new();
let mut nested: Vec<(ExprId, i64)> = Vec::new();
let mut nested_base: Option<ExprId> = None;
let mut has_i = false;
let mut seen: FxHashSet<ExprId> = FxHashSet::default();
let mut stack: Vec<ExprId> = roots.to_vec();
while let Some(id) = stack.pop() {
if !seen.insert(id) {
continue;
}
match arena.node(id) {
ExprNode::Num(_) => {}
ExprNode::Symbol(_) => syms.push(id),
ExprNode::Conjugate(b)
if conjugates && matches!(arena.node(*b), ExprNode::Symbol(_)) =>
{
syms.push(id);
}
ExprNode::ImaginaryUnit => has_i = true,
ExprNode::GoldenRatio => golden.push(id),
ExprNode::Add(ch) | ExprNode::Mul(ch) => stack.extend(ch.iter().copied()),
ExprNode::Neg(b) => stack.push(*b),
ExprNode::Pow(b, x) => {
let x = arena.as_num(*x)?;
if x.is_integer() {
if x.to_integer().to_i64()?.unsigned_abs() > MAX_POWER {
return None;
}
stack.push(*b);
} else if let Some(q) = arena.as_num(*b) {
let q = q.clone();
let s = x.numer().to_i64()?;
let t = x.denom().to_u32()?;
if q.is_zero() || s.unsigned_abs() > MAX_POWER {
return None;
}
radicals.push((id, q, s, t));
} else {
let s = x.numer().to_i64()?;
if x.denom().to_u32()? != 2
|| s.unsigned_abs() > MAX_POWER
|| nested_base.is_some_and(|nb| nb != *b)
{
return None;
}
nested_base = Some(*b);
nested.push((id, s));
stack.push(*b);
}
}
_ => return None,
}
}
let mut l: u32 = if golden.is_empty() { 1 } else { 2 };
for (_, q, _, t) in &radicals {
if q.is_negative() {
if *t != 2 {
return None;
}
has_i = true;
}
l = l / gcd_u32(l, *t) * *t;
if l > MAX_ROOT_INDEX {
return None;
}
}
let mut primes: Vec<BigInt> = Vec::new();
let mut factored: Vec<Vec<(BigInt, i64)>> = Vec::with_capacity(radicals.len());
for (_, q, _, _) in &radicals {
let mut f: Vec<(BigInt, i64)> = Vec::new();
for (p, e) in small_factorization(&q.numer().abs())? {
f.push((p, i64::from(e)));
}
for (p, e) in small_factorization(q.denom())? {
f.push((p, -i64::from(e)));
}
for (p, _) in &f {
if !primes.contains(p) {
primes.push(p.clone());
}
}
factored.push(f);
}
let five = BigInt::from(5);
if !golden.is_empty() && !primes.contains(&five) {
primes.push(five.clone());
}
primes.sort();
syms.sort();
let ns = syms.len();
let np = primes.len();
if nested_base.is_some() && l > 2 {
return None;
}
let nv = ns + np + usize::from(has_i) + usize::from(nested_base.is_some());
let li = i64::from(l);
let mut atoms: FxHashMap<ExprId, Mp> = FxHashMap::default();
for ((id, q, s, t), f) in radicals.iter().zip(&factored) {
let mut c = Q::one();
let mut exps = vec![0u32; nv];
for (p, k) in f {
let j = primes.binary_search(p).ok()?;
let e = k.checked_mul(*s)?.checked_mul(li / i64::from(*t))?;
let whole = u32::try_from(e.div_euclid(li).unsigned_abs()).ok()?;
let pw = Q::from_integer(p.pow(whole));
if e.div_euclid(li) >= 0 {
c *= pw;
} else {
c /= pw;
}
exps[ns + j] += u32::try_from(e.rem_euclid(li)).ok()?;
}
if q.is_negative() {
if s.rem_euclid(4) == 3 {
c = -c;
}
exps[ns + np] = 1;
}
let m = Mp::from_terms(nv, vec![(exps, c)])?;
atoms.insert(*id, m);
}
let tower = Tower {
syms,
primes,
l,
has_i,
atoms,
ext: None,
};
let atoms: FxHashMap<ExprId, Mp> = tower
.atoms
.iter()
.map(|(id, m)| (*id, tower.reduce(m.clone())))
.collect();
let mut tower = Tower { atoms, ..tower };
if !golden.is_empty() {
let j = tower.primes.binary_search(&five).ok()?;
let mut e = vec![0u32; nv];
e[ns + j] = l / 2;
let half = Q::new(BigInt::from(1), BigInt::from(2));
let phi = Mp::from_terms(nv, vec![(vec![0u32; nv], half.clone()), (e, half)])?;
for g in golden {
tower.atoms.insert(g, phi.clone());
}
}
if let Some(b) = nested_base {
tower.ext = Some(Mp::zero(nv));
let (n, d) = tower.eval_frac(arena, b, None, RF_TERM_BUDGET)??;
if !tower.in_k(&n) || !tower.in_k(&d) {
return None;
}
let dd = tower.mul_r(&n, &tower.inv_k(&d)?);
let w = Mp::var(nv, nv - 1);
let dinv = if dd.is_zero() { None } else { tower.inv_k(&dd) };
let mut vals: Vec<(ExprId, Mp)> = Vec::with_capacity(nested.len());
for (id, s) in nested {
let v = if dd.is_zero() {
if s < 0 {
return None;
}
Mp::zero(nv)
} else {
let k = (s - 1).div_euclid(2);
let base = if k >= 0 { dd.clone() } else { dinv.clone()? };
tower.mul_r(&w, &tower.pow_r(&base, k.unsigned_abs()))
};
vals.push((id, v));
}
tower.ext = Some(dd);
for (id, v) in vals {
tower.atoms.insert(id, v);
}
}
Some(tower)
}
fn nvars(&self) -> usize {
self.syms.len()
+ self.primes.len()
+ usize::from(self.has_i)
+ usize::from(self.ext.is_some())
}
fn decide(&self, n: &Mp) -> Option<bool> {
let Some(dd) = &self.ext else {
return Some(n.is_zero());
};
let nv = self.nvars();
let wv = nv - 1;
let split = |k: u32| -> Option<Mp> {
let terms: Vec<(Vec<u32>, Q)> = n
.terms()
.filter(|(e, _)| e[wv] == k)
.map(|(e, c)| {
let mut e = e.to_vec();
e[wv] = 0;
(e, c.clone())
})
.collect();
Mp::from_terms(nv, terms)
};
let (p, q) = (split(0)?, split(1)?);
if q.is_zero() {
return Some(p.is_zero());
}
let r = self.mul_r(&p, &p).sub(&self.mul_r(&self.mul_r(&q, &q), dd));
if !r.is_zero() {
return Some(false);
}
let ns = self.syms.len();
let at = |f: &Mp, k: u64| -> Mp {
let mut g = f.clone();
for i in 0..ns {
g = g.substitute(i, &rf_sample_point(i, k));
}
g
};
let (pk, qk) = (0..RF_POINTS)
.map(|k| (at(&p, k), at(&q, k)))
.find(|(_, qk)| !qk.is_zero())?;
let root = self.mul_r(&pk.neg(), &self.inv_k(&qk)?);
let iv = self.has_i.then(|| ns + self.primes.len());
let part = |k: u32| -> Option<Mp> {
let terms: Vec<(Vec<u32>, Q)> = root
.terms()
.filter(|(e, _)| iv.map_or(k == 0, |v| e[v] == k))
.map(|(e, c)| {
let mut e = e.to_vec();
if let Some(v) = iv {
e[v] = 0;
}
(e, c.clone())
})
.collect();
Mp::from_terms(nv, terms)
};
let re = self.real_sign(&part(0)?)?;
let im = self.real_sign(&part(1)?)?;
Some(re > 0 || (re == 0 && im > 0))
}
fn real_sign(&self, p: &Mp) -> Option<i8> {
if p.is_zero() {
return Some(0);
}
let ns = self.syms.len();
let np = self.primes.len();
let mut bits: u32 = 64;
while bits <= 4096 {
let scale = BigInt::from(1) << bits;
let enclosures: Vec<(Q, Q)> = self
.primes
.iter()
.map(|pr| {
let s = (pr * &scale * &scale).sqrt();
(
Q::new(s.clone(), scale.clone()),
Q::new(s + 1, scale.clone()),
)
})
.collect();
let (mut lo, mut hi) = (Q::zero(), Q::zero());
for (e, c) in p.terms() {
let (mut mlo, mut mhi) = (Q::one(), Q::one());
for j in 0..np {
for _ in 0..e[ns + j] {
mlo *= &enclosures[j].0;
mhi *= &enclosures[j].1;
}
}
if c.is_negative() {
lo += c * &mhi;
hi += c * &mlo;
} else {
lo += c * &mlo;
hi += c * &mhi;
}
}
if lo.is_positive() {
return Some(1);
}
if hi.is_negative() {
return Some(-1);
}
bits *= 2;
}
None
}
fn has_generators(&self) -> bool {
!self.primes.is_empty() || self.has_i
}
fn reduce(&self, p: Mp) -> Mp {
let p = self.reduce_generators(p);
let Some(dd) = &self.ext else {
return p;
};
let wv = self.nvars() - 1;
if !p.terms().any(|(e, _)| e[wv] >= 2) {
return p;
}
let mut out = Mp::zero(p.num_vars());
for (e, c) in p.terms() {
let mut e = e.to_vec();
let k = e[wv] / 2;
e[wv] %= 2;
let Some(t) = Mp::from_terms(p.num_vars(), vec![(e, c.clone())]) else {
continue;
};
let t = if k > 0 {
self.reduce_generators(t.mul(&self.pow_r(dd, u64::from(k))))
} else {
t
};
out = out.add(&t);
}
out
}
fn reduce_generators(&self, p: Mp) -> Mp {
let ns = self.syms.len();
let np = self.primes.len();
let l = self.l;
let over = |e: &[u32]| (0..np).any(|j| e[ns + j] >= l) || (self.has_i && e[ns + np] >= 2);
if !p.terms().any(|(e, _)| over(e)) {
return p;
}
let terms: Vec<(Vec<u32>, Q)> = p
.terms()
.map(|(e, c)| {
let mut e = e.to_vec();
let mut c = c.clone();
for j in 0..np {
let k = e[ns + j] / l;
if k > 0 {
c *= Q::from_integer(self.primes[j].pow(k));
e[ns + j] %= l;
}
}
if self.has_i {
if (e[ns + np] / 2) % 2 == 1 {
c = -c;
}
e[ns + np] %= 2;
}
(e, c)
})
.collect();
Mp::from_terms(p.num_vars(), terms).unwrap_or(p)
}
fn mul_r(&self, a: &Mp, b: &Mp) -> Mp {
self.reduce(a.mul(b))
}
fn pow_r(&self, a: &Mp, n: u64) -> Mp {
let mut acc = Mp::from_int(a.num_vars(), 1);
let mut base = a.clone();
let mut k = n;
while k > 0 {
if k & 1 == 1 {
acc = self.mul_r(&acc, &base);
}
k >>= 1;
if k > 0 {
base = self.mul_r(&base, &base);
}
}
acc
}
fn eval_frac(
&self,
arena: &Arena,
root: ExprId,
point: Option<u64>,
budget: usize,
) -> FracResult {
let nv = self.nvars();
let one = Mp::from_int(nv, 1);
let sym_index: FxHashMap<ExprId, usize> =
self.syms.iter().enumerate().map(|(i, &s)| (s, i)).collect();
let mut val: FxHashMap<ExprId, (Mp, Mp)> = FxHashMap::default();
let mut stack = vec![(root, false)];
while let Some((id, ready)) = stack.pop() {
if val.contains_key(&id) {
continue;
}
let node = arena.node(id);
let atom = self.atoms.get(&id);
let children: Vec<ExprId> = match node {
_ if atom.is_some() => Vec::new(),
ExprNode::Add(ch) | ExprNode::Mul(ch) => ch.to_vec(),
ExprNode::Pow(b, _) | ExprNode::Neg(b) => vec![*b],
_ => Vec::new(),
};
if !ready && !children.is_empty() {
stack.push((id, true));
stack.extend(children.into_iter().map(|c| (c, false)));
continue;
}
let r = if let Some(a) = atom {
(a.clone(), one.clone())
} else {
match node {
ExprNode::Num(_) => (Mp::constant(nv, arena.as_num(id)?.clone()), one.clone()),
ExprNode::Symbol(_) | ExprNode::Conjugate(_) => {
let i = *sym_index.get(&id)?;
let v = match point {
Some(k) => Mp::constant(nv, rf_sample_point(i, k)),
None => Mp::var(nv, i),
};
(v, one.clone())
}
ExprNode::ImaginaryUnit if self.has_i => (
Mp::var(nv, self.syms.len() + self.primes.len()),
one.clone(),
),
ExprNode::Neg(b) => {
let (n, d) = val.get(b)?;
(n.neg(), d.clone())
}
ExprNode::Add(ch) => {
let (mut n, mut d) = (Mp::zero(nv), one.clone());
for c in ch {
let (cn, cd) = val.get(c)?;
if *cd == d {
n = n.add(cn);
} else {
n = self.mul_r(&n, cd).add(&self.mul_r(cn, &d));
d = self.mul_r(&d, cd);
}
if n.num_terms().max(d.num_terms()) > budget {
return None;
}
}
(n, d)
}
ExprNode::Mul(ch) => {
let (mut n, mut d) = (one.clone(), one.clone());
for c in ch {
let (cn, cd) = val.get(c)?;
n = self.mul_r(&n, cn);
d = self.mul_r(&d, cd);
if n.num_terms().max(d.num_terms()) > budget {
return None;
}
}
(n, d)
}
ExprNode::Pow(b, x) => {
let k = arena.as_num(*x)?.to_integer().to_i64()?;
let (bn, bd) = val.get(b)?;
if k >= 0 {
(
self.pow_r(bn, k.unsigned_abs()),
self.pow_r(bd, k.unsigned_abs()),
)
} else {
if bn.is_zero() {
return Some(None);
}
(
self.pow_r(bd, k.unsigned_abs()),
self.pow_r(bn, k.unsigned_abs()),
)
}
}
_ => return None,
}
};
if r.0.num_terms().max(r.1.num_terms()) > budget {
return None;
}
val.insert(id, r);
}
Some(val.remove(&root))
}
fn is_zero(&self, arena: &Arena, root: ExprId) -> Option<bool> {
if self.syms.is_empty() || self.ext.is_some() {
let (n, _) = self.eval_frac(arena, root, None, RF_TERM_BUDGET)??;
return self.decide(&n);
}
let mut zero_at_all = true;
for k in 0..RF_POINTS {
match self.eval_frac(arena, root, Some(k), RF_TERM_BUDGET)? {
Some((n, _)) if !n.is_zero() => return Some(false),
Some(_) => {}
None => zero_at_all = false,
}
if let Some(Some((n, _))) = self.eval_frac(arena, root, None, RF_TERM_BUDGET) {
return Some(n.is_zero());
}
}
zero_at_all.then_some(true)
}
fn conjugate(p: &Mp, v: usize) -> Mp {
let terms: Vec<(Vec<u32>, Q)> = p
.terms()
.map(|(e, c)| {
(
e.to_vec(),
if e[v] % 2 == 1 { -c.clone() } else { c.clone() },
)
})
.collect();
Mp::from_terms(p.num_vars(), terms).unwrap_or_else(|| p.clone())
}
fn normalize(&self, n: Mp, d: Mp) -> Option<KFrac> {
if d.is_zero() {
return None;
}
let nv = self.nvars();
let (mut n, mut d) = (n, d);
for v in self.syms.len()..nv {
if d.degree_in(v) > 0 {
let c = Self::conjugate(&d, v);
n = self.mul_r(&n, &c);
d = self.mul_r(&d, &c);
}
}
if n.is_zero() {
return Some(KFrac::zero(nv));
}
if d.as_constant().is_none() {
let ns = self.syms.len();
let mut g = self.project(&d, ns)?;
for comp in self.generator_components(&n) {
g = Mp::gcd(&g, &self.project(&comp, ns)?);
if g.as_constant().is_some() {
break;
}
}
if g.as_constant().is_none() {
let g = self.embed(&g, nv)?;
n = n.div_exact(&g)?;
d = d.div_exact(&g)?;
}
}
let dp = d.primitive_part_q();
let (dlc, plc) = (d.leading_coeff()?.clone(), dp.leading_coeff()?.clone());
let mut scale = plc.clone() / dlc;
let mut dp = dp;
if plc.is_negative() {
dp = dp.neg();
scale = -scale;
}
let n = n.scale(&scale);
if n.num_terms().max(dp.num_terms()) > KFRAC_TERM_BUDGET {
return None;
}
Some(KFrac { num: n, den: dp })
}
fn generator_components(&self, p: &Mp) -> Vec<Mp> {
let ns = self.syms.len();
let mut groups: std::collections::BTreeMap<Vec<u32>, Vec<(Vec<u32>, Q)>> =
std::collections::BTreeMap::new();
for (e, c) in p.terms() {
let mut pe = e.to_vec();
for k in pe.iter_mut().skip(ns) {
*k = 0;
}
groups
.entry(e[ns..].to_vec())
.or_default()
.push((pe, c.clone()));
}
groups
.into_values()
.filter_map(|t| Mp::from_terms(p.num_vars(), t))
.collect()
}
fn project(&self, p: &Mp, ns: usize) -> Option<Mp> {
Mp::from_terms(
ns,
p.terms()
.map(|(e, c)| (e[..ns].to_vec(), c.clone()))
.collect(),
)
}
fn embed(&self, p: &Mp, nv: usize) -> Option<Mp> {
Mp::from_terms(
nv,
p.terms()
.map(|(e, c)| {
let mut e = e.to_vec();
e.resize(nv, 0);
(e, c.clone())
})
.collect(),
)
}
fn in_k(&self, p: &Mp) -> bool {
let ns = self.syms.len();
p.terms().all(|(e, _)| e[..ns].iter().all(|&k| k == 0))
}
fn inv_k(&self, c: &Mp) -> Option<Mp> {
let nv = self.nvars();
let mut prod = c.clone();
let mut cof = Mp::from_int(nv, 1);
for v in self.syms.len()..nv {
if prod.degree_in(v) > 0 {
let conj = Self::conjugate(&prod, v);
cof = self.mul_r(&cof, &conj);
prod = self.mul_r(&prod, &conj);
}
}
let norm = prod.as_constant()?;
if norm.is_zero() {
return None;
}
Some(cof.scale(&(Q::one() / norm)))
}
fn quotient(&self, num: &Mp, den: &Mp) -> Option<KFrac> {
let nv = self.nvars();
if num.is_zero() {
return Some(KFrac::zero(nv));
}
if let Some(q) = self.div_exact_k(num, den) {
return Some(KFrac {
num: q,
den: Mp::from_int(nv, 1),
});
}
let (mut n, mut d) = (num.clone(), den.clone());
let g = Mp::gcd(&n, &d);
if g.as_constant().is_none() {
n = n.div_exact(&g)?;
d = d.div_exact(&g)?;
}
let dp = d.primitive_part_q();
let (dlc, plc) = (d.leading_coeff()?.clone(), dp.leading_coeff()?.clone());
let mut scale = plc.clone() / dlc;
let mut dp = dp;
if plc.is_negative() {
dp = dp.neg();
scale = -scale;
}
Some(KFrac {
num: n.scale(&scale),
den: dp,
})
}
fn div_exact_k(&self, a: &Mp, b: &Mp) -> Option<Mp> {
let ns = self.syms.len();
let nv = self.nvars();
if b.is_zero() {
return None;
}
if a.is_zero() {
return Some(Mp::zero(nv));
}
let lead = |p: &Mp| -> Option<(Vec<u32>, Mp)> {
let key = |e: &[u32]| {
let s: u32 = e[..ns].iter().sum();
(s, e[..ns].to_vec())
};
let (_, best) = p.terms().map(|(e, _)| key(e)).max()?;
let terms: Vec<(Vec<u32>, Q)> = p
.terms()
.filter(|(e, _)| e[..ns] == best[..])
.map(|(e, c)| {
let mut e = e.to_vec();
for k in e.iter_mut().take(ns) {
*k = 0;
}
(e, c.clone())
})
.collect();
Some((best, Mp::from_terms(nv, terms)?))
};
if (0..ns).any(|v| a.degree_in(v) < b.degree_in(v)) {
return None;
}
let (lb_exp, lb_coef) = lead(b)?;
let lb_inv = self.inv_k(&lb_coef)?;
let mut r = a.clone();
let mut q = Mp::zero(nv);
while !r.is_zero() {
let (lr_exp, lr_coef) = lead(&r)?;
if lr_exp.iter().zip(&lb_exp).any(|(x, y)| x < y) {
return None;
}
let mut shift = vec![0u32; nv];
for k in 0..ns {
shift[k] = lr_exp[k] - lb_exp[k];
}
let t = self
.mul_r(&lr_coef, &lb_inv)
.mul_monomial(&Q::one(), &shift);
r = r.sub(&self.mul_r(&t, b));
q = q.add(&t);
if r.num_terms().max(q.num_terms()) > KFRAC_TERM_BUDGET {
return None;
}
}
Some(q)
}
fn frac_to_ex(&self, f: &KFrac, handle: &Ex) -> Ex {
let ctx = handle.context();
let ns = self.syms.len();
let syms: Vec<Ex> = self.syms.iter().map(|&s| handle.wrap(s)).collect();
let poly_ex = |terms: &[(Vec<u32>, Q)]| -> Ex {
let items = terms.iter().map(|(e, c)| {
let mut factors = vec![ctx.from_ratio(c.clone())];
for (k, &p) in e.iter().enumerate() {
if p > 0 {
factors.push(syms[k].powi(i64::from(p)));
}
}
Ex::product_of(&ctx, factors)
});
Ex::sum_of(&ctx, items)
};
let to_ex = |p: &Mp| -> Ex {
let mut groups: std::collections::BTreeMap<Vec<u32>, Vec<(Vec<u32>, Q)>> =
std::collections::BTreeMap::new();
for (e, c) in p.terms() {
groups
.entry(e[ns..].to_vec())
.or_default()
.push((e[..ns].to_vec(), c.clone()));
}
let mut parts: Vec<Ex> = Vec::with_capacity(groups.len());
for (g, terms) in &groups {
let mut factors = vec![poly_ex(terms)];
for (j, p) in self.primes.iter().enumerate() {
if g[j] > 0 {
let root = ctx
.from_ratio(Q::from_integer(p.clone()))
.sqrt()
.powi(i64::from(g[j]));
factors.push(root);
}
}
if self.has_i && g[self.primes.len()] > 0 {
factors.push(ctx.i_unit());
}
parts.push(Ex::product_of(&ctx, factors));
}
Ex::sum_of(&ctx, parts)
};
let num = to_ex(&f.num);
let den = to_ex(&f.den);
if den.is_one_structural() {
num
} else {
&num / &den
}
}
fn conj_poly(&self, p: &Mp, perm: &[usize]) -> Option<Mp> {
let ns = self.syms.len();
let iv = self.has_i.then(|| ns + self.primes.len());
let terms: Vec<(Vec<u32>, Q)> = p
.terms()
.map(|(e, c)| {
let mut f = e.to_vec();
for (k, &to) in perm.iter().enumerate() {
f[to] = e[k];
}
let flip = iv.is_some_and(|v| e[v] % 2 == 1);
(f, if flip { -c.clone() } else { c.clone() })
})
.collect();
Mp::from_terms(p.num_vars(), terms)
}
fn hdot(&self, xbar: &[Mp], y: &[Mp]) -> Option<Mp> {
let mut acc = Mp::zero(self.nvars());
for (a, b) in xbar.iter().zip(y) {
acc = acc.add(&self.mul_r(a, b));
if acc.num_terms() > KFRAC_TERM_BUDGET {
return None;
}
}
Some(acc)
}
}
#[derive(Clone, Debug, PartialEq)]
struct KFrac {
num: Mp,
den: Mp,
}
impl KFrac {
fn zero(nv: usize) -> KFrac {
KFrac {
num: Mp::zero(nv),
den: Mp::from_int(nv, 1),
}
}
}
pub(crate) struct GaussianRational {
pub re: Q,
pub im: Q,
}
pub(crate) fn gaussian_rational_parts(e: &Ex) -> Option<GaussianRational> {
let inner = e.inner.read();
let arena = &inner.arena;
let root = e.raw_id();
if let Some(q) = arena.as_num(root) {
return Some(GaussianRational {
re: q.clone(),
im: Q::zero(),
});
}
let tower = Tower::scan(arena, &[root])?;
if !tower.syms.is_empty() || !tower.primes.is_empty() || !tower.has_i || tower.ext.is_some() {
return None;
}
let (n, d) = tower.eval_frac(arena, root, None, RF_TERM_BUDGET)??;
let f = tower.normalize(n, d)?;
let den = f.den.as_constant()?;
let part = |k: u32| f.num.coeff(&[k]).cloned().unwrap_or_else(Q::zero) / &den;
Some(GaussianRational {
re: part(0),
im: part(1),
})
}
pub(crate) fn algebraic_constant_normal_form(e: &Ex) -> Option<Ex> {
let (tower, frac) = {
let inner = e.inner.read();
let arena = &inner.arena;
let root = e.raw_id();
let tower = Tower::scan(arena, &[root])?;
if !tower.syms.is_empty() || !tower.has_generators() || tower.l > 2 || tower.ext.is_some() {
return None;
}
let (n, d) = tower.eval_frac(arena, root, None, RF_TERM_BUDGET)??;
let f = tower.normalize(n, d)?;
(tower, f)
};
Some(tower.frac_to_ex(&frac, e))
}
pub(crate) fn algebraic_function_is_zero(e: &Ex) -> Option<bool> {
let inner = e.inner.read();
let arena = &inner.arena;
let root = e.raw_id();
let tower = Tower::scan(arena, &[root])?;
if !tower.has_generators() {
return None;
}
tower.is_zero(arena, root)
}
type KPoly = Vec<Mp>;
fn kp_trim(mut p: KPoly) -> KPoly {
while p.last().is_some_and(Mp::is_zero) {
p.pop();
}
p
}
fn kp_sub(a: &[Mp], b: &[Mp], nv: usize) -> KPoly {
let zero = Mp::zero(nv);
let terms = (0..a.len().max(b.len()))
.map(|k| a.get(k).unwrap_or(&zero).sub(b.get(k).unwrap_or(&zero)))
.collect();
kp_trim(terms)
}
fn kp_deriv(p: &[Mp]) -> KPoly {
let terms = p
.iter()
.enumerate()
.skip(1)
.map(|(k, c)| c.scale(&Q::from_integer(BigInt::from(k))))
.collect();
kp_trim(terms)
}
impl Tower {
fn kp_monic(&self, p: &[Mp]) -> Option<KPoly> {
let inv = self.inv_k(p.last()?)?;
Some(p.iter().map(|c| self.mul_r(c, &inv)).collect())
}
fn kp_mul(&self, a: &[Mp], b: &[Mp]) -> KPoly {
if a.is_empty() || b.is_empty() {
return Vec::new();
}
let nv = self.nvars();
let mut out = vec![Mp::zero(nv); a.len() + b.len() - 1];
for (i, x) in a.iter().enumerate() {
for (j, y) in b.iter().enumerate() {
out[i + j] = out[i + j].add(&self.mul_r(x, y));
}
}
kp_trim(out)
}
fn kp_divrem(&self, a: &[Mp], b: &[Mp]) -> Option<[KPoly; 2]> {
let db = b.len().checked_sub(1)?;
let inv = self.inv_k(b.last()?)?;
let mut r: KPoly = a.to_vec();
let mut q = vec![Mp::zero(self.nvars()); a.len().saturating_sub(db).max(1)];
while r.len() > db && !r.is_empty() {
let k = r.len() - 1 - db;
let f = self.mul_r(r.last()?, &inv);
for (j, c) in b.iter().enumerate() {
r[k + j] = r[k + j].sub(&self.mul_r(&f, c));
}
q[k] = f;
r.pop();
r = kp_trim(r);
}
Some([kp_trim(q), r])
}
fn kp_gcd(&self, a: &[Mp], b: &[Mp]) -> Option<KPoly> {
let (mut x, mut y) = (kp_trim(a.to_vec()), kp_trim(b.to_vec()));
while !y.is_empty() {
let [_, r] = self.kp_divrem(&x, &y)?;
x = y;
y = r;
}
if x.is_empty() {
return Some(x);
}
self.kp_monic(&x)
}
fn kp_quo(&self, b: &[Mp], a: &[Mp]) -> Option<KPoly> {
let [q, r] = self.kp_divrem(b, a)?;
r.is_empty().then_some(q)
}
fn kp_squarefree(&self, f: &[Mp]) -> Option<Vec<(KPoly, usize)>> {
let nv = self.nvars();
let df = kp_deriv(f);
let a0 = self.kp_gcd(f, &df)?;
let mut b = self.kp_quo(f, &a0)?;
let c = self.kp_quo(&df, &a0)?;
let mut d = kp_sub(&c, &kp_deriv(&b), nv);
let mut out = Vec::new();
let mut k = 1usize;
while b.len() > 1 {
let a = self.kp_gcd(&b, &d)?;
let nb = self.kp_quo(&b, &a)?;
let nc = self.kp_quo(&d, &a)?;
if a.len() > 1 {
out.push((a, k));
}
d = kp_sub(&nc, &kp_deriv(&nb), nv);
b = nb;
k += 1;
if k > f.len() {
return None;
}
}
Some(out)
}
fn kp_split_by_norm(&self, f: &[Mp]) -> Option<Vec<KPoly>> {
let ns = self.syms.len();
let gens: Vec<usize> = (ns..self.nvars()).collect();
let deg = f.len() - 1;
if deg <= 1 || gens.len() > 6 || deg << gens.len() > 64 {
return Some(vec![f.to_vec()]);
}
let flip = |p: &Mp, mask: usize| -> Option<Mp> {
let terms: Vec<(Vec<u32>, Q)> = p
.terms()
.map(|(e, c)| {
let odd = gens
.iter()
.enumerate()
.filter(|(b, v)| mask >> b & 1 == 1 && e[**v] % 2 == 1)
.count();
(
e.to_vec(),
if odd % 2 == 1 { -c.clone() } else { c.clone() },
)
})
.collect();
Mp::from_terms(p.num_vars(), terms)
};
let mut norm: KPoly = vec![Mp::from_int(self.nvars(), 1)];
for mask in 0..(1usize << gens.len()) {
let g: KPoly = f.iter().map(|c| flip(c, mask)).collect::<Option<_>>()?;
norm = self.kp_mul(&norm, &g);
}
let mut qc: Vec<Q> = Vec::with_capacity(norm.len());
for c in &norm {
match c.as_constant() {
Some(v) => qc.push(v),
None if c.is_zero() => qc.push(Q::zero()),
None => return Some(vec![f.to_vec()]),
}
}
let (_, factors) = crate::poly::dense::Poly::from_coeffs(qc).factor_over_z();
let mut out = Vec::new();
let nv = self.nvars();
for (h, _) in &factors {
if h.degree().unwrap_or(0) == 0 {
continue;
}
let hk: KPoly = h
.coeffs()
.iter()
.map(|c| Mp::constant(nv, c.clone()))
.collect();
let g = self.kp_gcd(f, &hk)?;
if g.len() > 1 {
out.push(g);
}
}
let total: usize = out.iter().map(|g| g.len() - 1).sum();
if total != deg {
return Some(vec![f.to_vec()]);
}
Some(out)
}
fn kp_rem(&self, a: &[Mp], g: &[Mp]) -> Option<KPoly> {
let [_, r] = self.kp_divrem(a, g)?;
Some(r)
}
fn kp_inv_mod(&self, a: &[Mp], g: &[Mp]) -> Option<KPoly> {
let nv = self.nvars();
let (mut r0, mut r1) = (g.to_vec(), self.kp_rem(a, g)?);
let (mut s0, mut s1): (KPoly, KPoly) = (Vec::new(), vec![Mp::from_int(nv, 1)]);
while r1.len() > 1 {
let [q, r2] = self.kp_divrem(&r0, &r1)?;
let s2 = kp_sub(&s0, &self.kp_mul(&q, &s1), nv);
(r0, r1) = (r1, r2);
(s0, s1) = (s1, s2);
}
let c = self.inv_k(r1.first()?)?;
let s: KPoly = s1.iter().map(|x| self.mul_r(x, &c)).collect();
self.kp_rem(&s, g)
}
fn real_constant_sign(&self, c: &Mp) -> Option<i8> {
let ns = self.syms.len();
if self.has_i {
let iv = ns + self.primes.len();
if c.terms().any(|(e, _)| e[iv] % 2 == 1) {
return None;
}
}
self.real_sign(c)
}
}
pub(crate) fn algebraic_poly_roots(coeffs: &[Ex]) -> Option<Vec<(Ex, usize)>> {
let handle = coeffs.first()?.clone();
let (tower, f) = algebraic_k_poly(coeffs)?;
let mut out: Vec<(Ex, usize)> = Vec::new();
for (sf, mult) in tower.kp_squarefree(&f)? {
for g in tower.kp_split_by_norm(&sf)? {
for root in kp_radical_roots(&tower, &g, &handle)? {
out.push((root, mult));
}
}
}
let total: usize = out.iter().map(|(_, m)| *m).sum();
(total == f.len() - 1).then_some(out)
}
fn algebraic_k_poly(coeffs: &[Ex]) -> Option<(Tower, KPoly)> {
let handle = coeffs.first()?;
let (tower, f) = {
let inner = handle.inner.read();
let arena = &inner.arena;
let ids: Vec<ExprId> = coeffs.iter().map(Ex::raw_id).collect();
let tower = Tower::scan(arena, &ids)?;
if !tower.syms.is_empty() || !tower.has_generators() || tower.l > 2 || tower.ext.is_some() {
return None;
}
let mut f: KPoly = Vec::with_capacity(ids.len());
for &id in &ids {
let (n, d) = tower.eval_frac(arena, id, None, RF_TERM_BUDGET)??;
f.push(tower.mul_r(&n, &tower.inv_k(&d)?));
}
(tower, kp_trim(f))
};
if f.len() < 2 {
return None;
}
let f = tower.kp_monic(&f)?;
Some((tower, f))
}
pub(crate) fn algebraic_poly_distinct_root_count(coeffs: &[Ex]) -> Option<usize> {
let (tower, f) = algebraic_k_poly(coeffs)?;
let factors = tower.kp_squarefree(&f)?;
Some(factors.iter().map(|(g, _)| g.len() - 1).sum())
}
pub(crate) fn in_square_root_tower(es: &[Ex]) -> bool {
let Some(first) = es.first() else {
return true;
};
let inner = first.inner.read();
let ids: Vec<ExprId> = es.iter().map(Ex::raw_id).collect();
Tower::scan(&inner.arena, &ids).is_some_and(|t| t.l <= 2)
}
pub(crate) fn algebraic_eigenvectors(
rows: &[Vec<Ex>],
charpoly: &[Ex],
lam: &Ex,
) -> Option<Vec<Vec<Ex>>> {
let n = rows.len();
let (tower, a, f) = {
let inner = lam.inner.read();
let arena = &inner.arena;
let mut ids: Vec<ExprId> = rows.iter().flatten().map(Ex::raw_id).collect();
ids.extend(charpoly.iter().map(Ex::raw_id));
let tower = Tower::scan(arena, &ids)?;
if !tower.syms.is_empty() || !tower.has_generators() || tower.l > 2 || tower.ext.is_some() {
return None;
}
let kval = |id: ExprId| -> Option<Mp> {
let (nm, d) = tower.eval_frac(arena, id, None, RF_TERM_BUDGET)??;
Some(tower.mul_r(&nm, &tower.inv_k(&d)?))
};
let a: Vec<Vec<Mp>> = rows
.iter()
.map(|r| r.iter().map(|e| kval(e.raw_id())).collect::<Option<_>>())
.collect::<Option<_>>()?;
let f: KPoly = charpoly
.iter()
.map(|e| kval(e.raw_id()))
.collect::<Option<_>>()?;
(tower, a, kp_trim(f))
};
let nv = tower.nvars();
let kx = |c: &Mp| -> Ex {
let fr = KFrac {
num: c.clone(),
den: Mp::from_int(nv, 1),
};
tower.frac_to_ex(&fr, lam)
};
let lam_pows: Vec<Ex> = (0..f.len()).map(|k| lam.powi(k as i64)).collect();
let at_lam = |p: &[Mp]| -> Ex {
let terms = p
.iter()
.enumerate()
.filter(|(_, c)| !c.is_zero())
.map(|(k, c)| if k == 0 { kx(c) } else { &kx(c) * &lam_pows[k] });
Ex::sum_of(&lam.context(), terms)
};
let f = tower.kp_monic(&f)?;
let mut gs: Vec<KPoly> = Vec::new();
for (sf, _) in tower.kp_squarefree(&f)? {
gs.extend(tower.kp_split_by_norm(&sf)?);
}
let mut which: Option<usize> = None;
for (idx, g) in gs.iter().enumerate() {
match at_lam(g).eval_decimal(30) {
Ok(s) if s == "0" => {
if which.replace(idx).is_some() {
return None;
}
}
Ok(_) => {}
Err(_) => return None,
}
}
let g = gs.swap_remove(which?);
let t: KPoly = vec![Mp::zero(nv), Mp::from_int(nv, 1)];
let mut m: Vec<Vec<KPoly>> = Vec::with_capacity(n);
for (i, row) in a.iter().enumerate() {
let mut r = Vec::with_capacity(n);
for (j, e) in row.iter().enumerate() {
let c = kp_trim(vec![e.clone()]);
let x = if i == j { kp_sub(&c, &t, nv) } else { c };
r.push(tower.kp_rem(&x, &g)?);
}
m.push(r);
}
let mut pivots: Vec<usize> = Vec::new();
let mut pr = 0usize;
for col in 0..n {
if pr >= n {
break;
}
let Some(f) = (pr..n).find(|&i| !m[i][col].is_empty()) else {
continue;
};
m.swap(pr, f);
let inv = tower.kp_inv_mod(&m[pr][col], &g)?;
let pivot_row: Vec<KPoly> = m[pr]
.iter()
.map(|x| tower.kp_rem(&tower.kp_mul(x, &inv), &g))
.collect::<Option<_>>()?;
for (i, row) in m.iter_mut().enumerate() {
if i == pr || row[col].is_empty() {
continue;
}
let fac = row[col].clone();
for (x, p) in row.iter_mut().zip(&pivot_row) {
let y = kp_sub(x, &tower.kp_mul(&fac, p), nv);
*x = tower.kp_rem(&y, &g)?;
}
}
m[pr] = pivot_row;
pivots.push(col);
pr += 1;
}
let ctx = lam.context();
let mut out = Vec::new();
for free in (0..n).filter(|c| !pivots.contains(c)) {
let mut v = vec![ctx.zero(); n];
v[free] = ctx.one();
for (r, &pc) in pivots.iter().enumerate() {
if !m[r][free].is_empty() {
v[pc] = -at_lam(&m[r][free]);
}
}
out.push(v);
}
(!out.is_empty()).then_some(out)
}
fn kp_radical_roots(tower: &Tower, g: &[Mp], handle: &Ex) -> Option<Vec<Ex>> {
let ctx = handle.context();
let nv = tower.nvars();
let kx = |c: &Mp| -> Ex {
let f = KFrac {
num: c.clone(),
den: Mp::from_int(nv, 1),
};
tower.frac_to_ex(&f, handle)
};
let qn = |n: i64, d: i64| Q::new(BigInt::from(n), BigInt::from(d));
let sc = |c: &Mp, n: i64, d: i64| c.scale(&qn(n, d));
let mul = |a: &Mp, b: &Mp| tower.mul_r(a, b);
let tidy = |e: Ex| e.eval();
match g.len() - 1 {
1 => Some(vec![tidy(-kx(&g[0]))]),
2 => {
let (c, b) = (&g[0], &g[1]);
let disc = mul(b, b).sub(&sc(c, 4, 1));
let s = kx(&disc).sqrt();
let mb = -kx(b);
let two = ctx.int(2);
Some(vec![tidy(&(&mb + &s) / &two), tidy(&(&mb - &s) / &two)])
}
3 => {
let (c, b, a) = (&g[0], &g[1], &g[2]);
let aa = mul(a, a);
let p = b.sub(&sc(&aa, 1, 3));
let q = c.sub(&sc(&mul(a, b), 1, 3)).add(&sc(&mul(&aa, a), 2, 27));
let aon3 = sc(a, 1, 3);
let shift = kx(&aon3);
let coeff = &(&ctx.int(3).sqrt() * &ctx.i_unit()) / &ctx.int(2);
let half = ctx.rational(-1, 2);
let omega = [ctx.int(1), &half + &coeff, &half - &coeff];
if c.is_zero() {
let quad = [b.clone(), a.clone(), Mp::from_int(nv, 1)];
let mut r = kp_radical_roots(tower, &quad, handle)?;
r.insert(1, ctx.zero());
return Some(r);
}
if q.is_zero() {
let s = kx(&p.neg()).sqrt();
return Some(vec![tidy(&s - &shift), tidy(-&shift), tidy(-&s - &shift)]);
}
let u1 = if p.is_zero() {
if tower.real_constant_sign(&q) == Some(1) {
Some(-kx(&q).cbrt())
} else {
Some(kx(&q.neg()).cbrt())
}
} else if tower.real_constant_sign(&q) == Some(-1) {
let r = sc(&mul(&q, &q), 1, 4).add(&sc(&mul(&mul(&p, &p), &p), 1, 27));
let inner = &kx(&sc(&q, -1, 2)) + &kx(&r).sqrt();
Some(-inner.cbrt())
} else {
None
};
let Some(u1) = u1 else {
let d0 = aa.sub(&sc(b, 3, 1));
let d1 = sc(&mul(&aa, a), 2, 1)
.sub(&sc(&mul(a, b), 9, 1))
.add(&sc(c, 27, 1));
let rad = mul(&d1, &d1).sub(&sc(&mul(&mul(&d0, &d0), &d0), 4, 1));
let cc = (&(&kx(&d1) + &kx(&rad).sqrt()) / &ctx.int(2)).cbrt();
let (ae, d0e) = (kx(a), kx(&d0));
let three = ctx.int(3);
return Some(
omega
.iter()
.map(|uk| {
let ukc = uk * &cc;
tidy(-(&(&(&ae + &ukc) + &(&d0e / &ukc)) / &three))
})
.collect(),
);
};
let pon3 = kx(&sc(&p, 1, 3));
Some(
omega
.iter()
.map(|uk| {
let u = uk * &u1;
if p.is_zero() {
tidy(&u - &shift)
} else {
tidy(&(&-&u + &(&pon3 / &u)) - &shift)
}
})
.collect(),
)
}
4 => kp_quartic_roots(tower, g, handle),
_ => None,
}
}
fn kp_quartic_roots(tower: &Tower, g: &[Mp], handle: &Ex) -> Option<Vec<Ex>> {
let ctx = handle.context();
let nv = tower.nvars();
let kx = |c: &Mp| -> Ex {
let f = KFrac {
num: c.clone(),
den: Mp::from_int(nv, 1),
};
tower.frac_to_ex(&f, handle)
};
let qn = |n: i64, d: i64| Q::new(BigInt::from(n), BigInt::from(d));
let sc = |c: &Mp, n: i64, d: i64| c.scale(&qn(n, d));
let mul = |a: &Mp, b: &Mp| tower.mul_r(a, b);
let tidy = |e: Ex| e.eval();
let (d, c, b, a) = (&g[0], &g[1], &g[2], &g[3]);
let one = Mp::from_int(nv, 1);
let two = ctx.int(2);
if d.is_zero() {
let cubic = [c.clone(), b.clone(), a.clone(), one];
let mut r = kp_radical_roots(tower, &cubic, handle)?;
r.insert(0, ctx.zero());
return Some(r);
}
if !a.is_zero() {
let m = mul(c, &tower.inv_k(a)?);
if mul(&m, &m).sub(d).is_zero() {
let disc = mul(a, a).sub(&sc(&b.sub(&sc(&m, 2, 1)), 4, 1));
let s = kx(&disc).sqrt();
let ma = -kx(a);
let four_m = kx(&sc(&m, 4, 1));
let mut out = Vec::with_capacity(4);
for z in [&(&ma + &s) / &two, &(&ma - &s) / &two] {
let r = any_sqrt(&(&(&z * &z) - &four_m));
out.push(tidy(&(&z + &r) / &two));
out.push(tidy(&(&z - &r) / &two));
}
return Some(out);
}
}
let a2 = mul(a, a);
let e = b.sub(&sc(&a2, 3, 8));
let f = c.add(&mul(a, &sc(&a2, 1, 8).sub(&sc(b, 1, 2))));
let aon4 = sc(a, 1, 4);
let g0 = d.sub(&mul(
&aon4,
&mul(a, &sc(&a2, 3, 64).sub(&sc(b, 1, 4))).add(c),
));
let shift = kx(&aon4);
if f.is_zero() {
let disc = mul(&e, &e).sub(&sc(&g0, 4, 1));
let s = kx(&disc).sqrt();
let me = -kx(&e);
let y1 = any_sqrt(&(&(&me + &s) / &two));
let y2 = any_sqrt(&(&(&me - &s) / &two));
let ys = [-&y1, -&y2, y1, y2];
return Some(ys.into_iter().map(|y| tidy(&y - &shift)).collect());
}
if g0.is_zero() {
let cubic = [f, e, Mp::zero(nv), one];
let mut r = kp_radical_roots(tower, &cubic, handle)?;
r.insert(0, ctx.zero());
return Some(r.into_iter().map(|y| tidy(&y - &shift)).collect());
}
let resolvent = [
sc(&mul(&f, &f), -1, 64),
sc(&mul(&e, &e), 1, 16).sub(&sc(&g0, 1, 4)),
sc(&e, 1, 2),
Mp::from_int(nv, 1),
];
let gens = tower.nvars();
let rational_root = if gens <= 6 && 3usize << gens <= 64 {
kp_rational_roots(&resolvent)
.into_iter()
.filter(|r| !r.is_zero())
.max()
} else {
tower
.kp_squarefree(&resolvent)?
.into_iter()
.filter_map(|(h, _)| tower.kp_split_by_norm(&h))
.flatten()
.filter(|h| h.len() == 2)
.filter_map(|h| h[0].neg().as_constant())
.filter(|r| !r.is_zero())
.max()
};
if let Some(r) = rational_root {
let rr = Mp::constant(nv, r);
let c1 = kx(&rr).sqrt();
let bb = &(&-kx(&f) * &c1) / &kx(&sc(&rr, 4, 1));
let aa = kx(&rr.neg().sub(&sc(&e, 1, 2)));
let c2 = any_sqrt(&(&aa + &bb));
let c3 = any_sqrt(&(&aa - &bb));
return Some(vec![
tidy(&(&c1 - &c2) - &shift),
tidy(&(&-&c1 - &c3) - &shift),
tidy(&(&-&c1 + &c3) - &shift),
tidy(&(&c1 + &c2) - &shift),
]);
}
let ee = mul(&e, &e);
let p = sc(&ee, -1, 12).sub(&g0);
let q = sc(&mul(&ee, &e), -1, 108)
.add(&sc(&mul(&e, &g0), 1, 3))
.sub(&sc(&mul(&f, &f), 1, 8));
let five_e_6 = kx(&sc(&e, -5, 6));
let y = if p.is_zero() {
&five_e_6 - &kx(&q).cbrt()
} else {
let rad = sc(&mul(&q, &q), 1, 4).add(&sc(&mul(&mul(&p, &p), &p), 1, 27));
let u = (&kx(&sc(&q, -1, 2)) + &kx(&rad).sqrt()).cbrt();
&(&five_e_6 + &u) - &(&kx(&p) / &(&ctx.int(3) * &u))
};
let w = any_sqrt(&(&kx(&e) + &(&two * &y)));
let arg1 = &kx(&sc(&e, 3, 1)) + &(&two * &y);
let arg2 = &(&two * &kx(&f)) / &w;
let mut out = Vec::with_capacity(4);
for s in [-1i64, 1] {
let sw = &ctx.int(s) * &w;
let root = any_sqrt(&-(&arg1 + &(&ctx.int(s) * &arg2)));
for t in [-1i64, 1] {
let x = &(&(&sw - &(&ctx.int(t) * &root)) / &two) - &shift;
out.push(tidy(x));
}
}
Some(out)
}
fn kp_rational_roots(p: &[Mp]) -> Vec<Q> {
let mut comps: std::collections::BTreeMap<Vec<u32>, Vec<Q>> = std::collections::BTreeMap::new();
for (k, c) in p.iter().enumerate() {
for (e, q) in c.terms() {
let v = comps
.entry(e.to_vec())
.or_insert_with(|| vec![Q::zero(); p.len()]);
v[k] = q.clone();
}
}
let mut g: Option<crate::poly::dense::Poly> = None;
for cs in comps.into_values() {
let pm = crate::poly::dense::Poly::from_coeffs(cs);
g = Some(match g {
None => pm,
Some(g0) => crate::poly::dense::Poly::gcd(&g0, &pm),
});
}
let Some(g) = g.filter(|g| g.degree().unwrap_or(0) > 0) else {
return Vec::new();
};
let (_, factors) = g.factor_over_z();
factors
.iter()
.filter_map(|(h, _)| match h.coeffs() {
[c0, c1] if !c1.is_zero() => Some(-(c0 / c1)),
_ => None,
})
.collect()
}
fn any_sqrt(x: &Ex) -> Ex {
let on_cut = x
.eval_complex64()
.is_ok_and(|z| z.re < 0.0 && z.im.abs() <= 1e-6 * z.re.abs());
if on_cut {
&x.context().i_unit() * &(-x).sqrt()
} else {
x.sqrt()
}
}
pub(crate) struct GramSchmidtFactors {
pub q: Vec<Vec<Ex>>,
pub r: Vec<Vec<Ex>>,
}
pub(crate) enum HermitianGs {
Factors(GramSchmidtFactors),
Dependent(usize),
}
pub(crate) fn hermitian_gram_schmidt(cols: &[Vec<Ex>], normalize: bool) -> Option<HermitianGs> {
let handle = cols.first()?.first()?.clone();
let ctx = handle.context();
let mut syms: Vec<Ex> = Vec::new();
for e in cols.iter().flatten() {
for s in e.free_symbols() {
if !syms.contains(&s) {
syms.push(s);
}
}
}
let mut complex: Vec<[Ex; 2]> = Vec::new();
let mut real: Vec<ExprId> = Vec::new();
for s in syms {
if s.is_real() == Some(true) {
real.push(s.raw_id());
} else {
let c = s.conjugate();
complex.push([s, c]);
}
}
let (tower, a, perm) = {
let inner = handle.inner.read();
let arena = &inner.arena;
for [s, c] in &complex {
if !matches!(arena.node(c.raw_id()), ExprNode::Conjugate(b) if *b == s.raw_id()) {
return None;
}
}
let mut ids: Vec<ExprId> = cols.iter().flatten().map(Ex::raw_id).collect();
ids.extend(complex.iter().map(|[_, c]| c.raw_id()));
let tower = Tower::scan_opts(arena, &ids, true)?;
if tower.l > 2 || tower.ext.is_some() {
return None;
}
let ns = tower.syms.len();
let pos = |id: ExprId| tower.syms.iter().position(|&x| x == id);
let mut perm: Vec<usize> = (0..ns).collect();
for [s, c] in &complex {
let (i, j) = (pos(s.raw_id())?, pos(c.raw_id())?);
perm[i] = j;
perm[j] = i;
}
if (0..ns).any(|k| perm[k] == k && !real.contains(&tower.syms[k])) {
return None;
}
let mut a: Vec<Vec<KFrac>> = Vec::with_capacity(cols.len());
for col in cols {
let mut fc = Vec::with_capacity(col.len());
for e in col {
let (n, d) = tower.eval_frac(arena, e.raw_id(), None, KFRAC_TERM_BUDGET)??;
fc.push(tower.normalize(n, d)?);
}
a.push(fc);
}
(tower, a, perm)
};
let k = a.len();
let m = a[0].len();
let nv = tower.nvars();
let one_p = Mp::from_int(nv, 1);
let mut ap: Vec<Vec<Mp>> = Vec::with_capacity(k);
let mut c: Vec<Mp> = Vec::with_capacity(k);
for col in &a {
let mut l = one_p.clone();
for f in col {
if f.den != l && f.den.as_constant().is_none() {
l = Mp::lcm(&l, &f.den);
}
}
let lbar = tower.conj_poly(&l, &perm)?;
let mut out = Vec::with_capacity(m);
for f in col {
let cof = if f.den == l {
lbar.clone()
} else {
tower.mul_r(&l.div_exact(&f.den)?, &lbar)
};
out.push(tower.mul_r(&f.num, &cof));
}
ap.push(out);
c.push(tower.mul_r(&l, &lbar));
}
let div = |x: &Mp, b: &Mp| -> Option<Mp> {
if tower.in_k(b) {
Some(tower.mul_r(x, &tower.inv_k(b)?))
} else {
tower.div_exact_k(x, b)
}
};
let mut w: Vec<Vec<Mp>> = Vec::with_capacity(k);
let mut wbar: Vec<Vec<Mp>> = Vec::with_capacity(k);
let mut delta: Vec<Mp> = vec![one_p.clone()];
let mut lam: Vec<Vec<Mp>> = vec![vec![Mp::zero(nv); k]; k];
for j in 0..k {
let mut v = ap[j].clone();
for l in 0..j {
let lj = tower.hdot(&wbar[l], &ap[j])?;
for t in 0..m {
let x = tower
.mul_r(&delta[l + 1], &v[t])
.sub(&tower.mul_r(&lj, &w[l][t]));
v[t] = div(&x, &delta[l])?;
}
lam[l][j] = lj;
}
let vbar: Vec<Mp> = v
.iter()
.map(|x| tower.conj_poly(x, &perm))
.collect::<Option<_>>()?;
let next = div(&tower.hdot(&vbar, &v)?, &delta[j])?;
if next.is_zero() {
return Some(HermitianGs::Dependent(j));
}
w.push(v);
wbar.push(vbar);
delta.push(next);
}
let tidy = |e: Ex| if e.is_constant() { e.eval() } else { e };
let pe = |p: &Mp| {
let whole = KFrac {
num: p.clone(),
den: one_p.clone(),
};
tower.frac_to_ex(&whole, &handle)
};
let one = ctx.one();
let mut r: Vec<Vec<Ex>> = vec![vec![ctx.zero(); k]; k];
let mut q: Vec<Vec<Ex>> = Vec::with_capacity(k);
if normalize {
let sq: Vec<Ex> = delta
.iter()
.map(|x| {
if x.as_constant().is_some_and(|v| v.is_one()) {
one.clone()
} else {
pe(x).sqrt()
}
})
.collect();
for j in 0..k {
let s = &sq[j] * &sq[j + 1];
q.push(w[j].iter().map(|x| tidy(&pe(x) / &s)).collect());
let cj = pe(&c[j]);
for i in 0..j {
let den = &(&cj * &sq[i]) * &sq[i + 1];
r[i][j] = tidy(&pe(&lam[i][j]) / &den);
}
r[j][j] = tidy(&sq[j + 1] / &(&sq[j] * &cj));
}
} else {
for j in 0..k {
let den = tower.mul_r(&delta[j], &c[j]);
q.push(
w[j].iter()
.map(|x| Some(tidy(tower.frac_to_ex(&tower.quotient(x, &den)?, &handle))))
.collect::<Option<_>>()?,
);
for i in 0..j {
let num = tower.mul_r(&lam[i][j], &c[i]);
let f = tower.quotient(&num, &tower.mul_r(&c[j], &delta[i + 1]))?;
r[i][j] = tidy(tower.frac_to_ex(&f, &handle));
}
r[j][j] = one.clone();
}
}
Some(HermitianGs::Factors(GramSchmidtFactors { q, r }))
}
fn rref_solve_algebraic(
mat: &[Vec<Ex>],
n_vars: usize,
unknowns: &[Ex],
) -> Option<SymbolicLinearResult> {
let handle = mat.first()?.first()?.clone();
let (tower, mut a) = {
let inner = handle.inner.read();
let arena = &inner.arena;
let ids: Vec<Vec<ExprId>> = mat
.iter()
.map(|r| r.iter().map(Ex::raw_id).collect())
.collect();
let all: Vec<ExprId> = ids.iter().flatten().copied().collect();
let tower = Tower::scan(arena, &all)?;
if !tower.has_generators() || tower.l > 2 || tower.ext.is_some() {
return None;
}
let mut a: Vec<Vec<Mp>> = Vec::with_capacity(ids.len());
for row in &ids {
let mut fr = Vec::with_capacity(row.len());
for &id in row {
let (n, d) = tower.eval_frac(arena, id, None, KFRAC_TERM_BUDGET)??;
fr.push(tower.normalize(n, d)?);
}
let mut lcm = Mp::from_int(tower.nvars(), 1);
for f in &fr {
if f.den != lcm && f.den.as_constant().is_none() {
lcm = Mp::lcm(&lcm, &f.den);
}
}
let mut out = Vec::with_capacity(fr.len());
for f in fr {
let cof = if f.den == lcm {
Mp::from_int(tower.nvars(), 1)
} else {
lcm.div_exact(&f.den)?
};
out.push(f.num.mul(&cof));
}
a.push(out);
}
(tower, a)
};
let m = a.len();
let ncols = n_vars + 1;
let nv = tower.nvars();
let mut pivots: Vec<usize> = Vec::new();
let mut pr = 0usize;
let mut prev = Mp::from_int(nv, 1);
for col in 0..n_vars {
if pr >= m {
break;
}
let Some(r) = (pr..m)
.filter(|&r| !a[r][col].is_zero())
.min_by_key(|&r| (!tower.in_k(&a[r][col]), a[r][col].num_terms()))
else {
continue;
};
a.swap(pr, r);
let p = a[pr][col].clone();
let pivot_row = a[pr].clone();
let prev_inv = if tower.in_k(&prev) {
Some(tower.inv_k(&prev)?)
} else {
None
};
for (i, row) in a.iter_mut().enumerate() {
if i == pr {
continue;
}
let f = row[col].clone();
for j in 0..ncols {
if j == col {
continue;
}
let t = tower.mul_r(&p, &row[j]);
let t = if f.is_zero() {
t
} else {
t.sub(&tower.mul_r(&f, &pivot_row[j]))
};
row[j] = match &prev_inv {
Some(inv) => tower.mul_r(&t, inv),
None => tower.div_exact_k(&t, &prev)?,
};
if row[j].num_terms() > KFRAC_TERM_BUDGET {
return None;
}
}
row[col] = Mp::zero(nv);
}
prev = p;
pivots.push(col);
pr += 1;
}
if a[pr..].iter().any(|row| !row[n_vars].is_zero()) {
return Some(SymbolicLinearResult {
values: Vec::new(),
free: Vec::new(),
inconsistent: true,
});
}
let mut is_pivot = vec![false; n_vars];
for &c in &pivots {
is_pivot[c] = true;
}
let free: Vec<usize> = (0..n_vars).filter(|&c| !is_pivot[c]).collect();
let mut values: Vec<Ex> = unknowns.to_vec();
for (r, &pc) in pivots.iter().enumerate() {
let den = a[r][pc].clone();
let quotient = |num: &Mp| -> Option<KFrac> { tower.quotient(num, &den) };
let mut v = tower.frac_to_ex("ient(&a[r][n_vars])?, &handle);
for &f in &free {
if !a[r][f].is_zero() {
let c = quotient(&a[r][f])?;
v = &v - &(&tower.frac_to_ex(&c, &handle) * &unknowns[f]);
}
}
values[pc] = v;
}
Some(SymbolicLinearResult {
values,
free,
inconsistent: false,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::context::Context;
fn show(e: &Ex) -> String {
format!("{e}")
}
#[test]
fn extract_linear_row_reads_coefficients_and_constant() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let eq = &x * 2 - &y * 3 + 5;
let (coeffs, rhs) = ctx.with_arena_mut(|a| {
extract_linear_row(a, eq.raw_id(), &[x.raw_id(), y.raw_id()])
.map(|(c, r)| {
(
c.iter()
.map(|&i| a.display(i).to_string())
.collect::<Vec<_>>(),
a.display(r).to_string(),
)
})
.expect("linear")
});
assert_eq!(coeffs, ["2", "-3"]);
assert_eq!(rhs, "-5");
}
#[test]
fn extract_linear_row_rejects_products_powers_and_functions() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
for bad in [&x * &y, x.powi(2), x.sin(), &x / &y] {
let none = ctx.with_arena_mut(|a| {
extract_linear_row(a, bad.raw_id(), &[x.raw_id(), y.raw_id()]).is_none()
});
assert!(none, "{bad} should not be linear");
}
}
#[test]
fn unique_solution() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let r = linsolve_symbolic(&[&x + &y - 3, &x - &y - 1], &[x, y]).unwrap();
assert!(!r.inconsistent);
assert!(r.free.is_empty());
assert_eq!(r.values.iter().map(show).collect::<Vec<_>>(), ["2", "1"]);
}
#[test]
fn inconsistent_system_is_flagged() {
let ctx = Context::new();
let x = ctx.symbol("x");
let r = linsolve_symbolic(&[&x - 1, &x - 2], &[x]).unwrap();
assert!(r.inconsistent);
}
#[test]
fn underdetermined_system_reports_free_variable() {
let ctx = Context::new();
let (x, y) = (ctx.symbol("x"), ctx.symbol("y"));
let r = linsolve_symbolic(&[&x + &y - 5], &[x.clone(), y.clone()]).unwrap();
assert!(!r.inconsistent);
assert_eq!(r.free.len(), 1);
let f = r.free[0];
assert_eq!(show(&r.values[f]), show([&x, &y][f]));
let pinned = 1 - f;
let residual = (&x + &y - 5)
.subs([&x, &y][pinned], &r.values[pinned])
.simplify();
assert_eq!(show(&residual), "0");
}
#[test]
fn symbolic_coefficients_and_rational_answer() {
let ctx = Context::new();
let (x, a) = (ctx.symbol("x"), ctx.symbol("a"));
let r = linsolve_symbolic(&[&x * 3 - 1], std::slice::from_ref(&x)).unwrap();
assert_eq!(show(&r.values[0]), "1/3");
let r = linsolve_symbolic(&[&a * &x - 1], std::slice::from_ref(&x)).unwrap();
let at2 = r.values[0].subs(&a, &ctx.int(2)).eval();
assert_eq!(show(&at2), "1/2");
}
#[test]
fn empty_input_is_invalid_argument() {
let ctx = Context::new();
let x = ctx.symbol("x");
assert!(matches!(
linsolve_symbolic(&[], std::slice::from_ref(&x)),
Err(SymplexError::InvalidArgument { .. })
));
assert!(matches!(
linsolve_symbolic(&[&x - 1], &[]),
Err(SymplexError::InvalidArgument { .. })
));
}
}