pub use crate::api::expr_solve_ext::{
GeneralSolution, LinearSolution, NewtonOpts, ZeroForm, linsolve, linsolve_matrix,
solve_numeric_system, solve_numeric_system_with,
};
use crate::base::arena::Arena;
use crate::poly::groebner;
use crate::poly::multipoly::*;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{One, ToPrimitive, Zero};
use crate::api::expr::Ex;
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode};
use crate::base::walk;
use rustc_hash::FxHashMap;
pub fn solve_polynomial_system(
polys: &[MultiPoly<GrevLex>],
) -> Result<Vec<Vec<Ratio<BigInt>>>, String> {
if polys.is_empty() {
return Ok(vec![vec![]]);
}
let nonzero: Vec<_> = polys.iter().filter(|p| !p.is_zero()).cloned().collect();
if nonzero.is_empty() {
return Ok(vec![vec![]]);
}
let num_vars = nonzero[0].num_vars();
if num_vars == 0 {
for p in &nonzero {
if !p.is_zero() {
return Ok(vec![]); }
}
return Ok(vec![vec![]]);
}
let grevlex_gb = groebner::groebner_basis(&nonzero);
if grevlex_gb.is_empty() {
return Err("trivial ideal: system is underdetermined".into());
}
for p in &grevlex_gb {
if let Some(0) = p.total_degree() {
return Ok(vec![]);
}
}
if !groebner::is_zero_dimensional(&grevlex_gb) {
return Err("ideal is not zero-dimensional: infinitely many solutions".into());
}
let lex_gb = groebner::groebner_basis_lex(&nonzero.to_vec());
if lex_gb.is_empty() {
return Ok(vec![]);
}
solve_triangular(&lex_gb, num_vars)
}
fn solve_triangular(
basis: &[MultiPoly<Lex>],
num_vars: usize,
) -> Result<Vec<Vec<Ratio<BigInt>>>, String> {
if num_vars == 0 {
return Ok(vec![vec![]]);
}
if basis.is_empty() {
return Ok(vec![vec![]]);
}
let last_var = num_vars - 1;
let univariate = basis.iter().find(|p| {
!p.is_zero()
&& p.terms().all(|(exp, _)| {
exp.iter()
.enumerate()
.all(|(i, &e)| i == last_var || e == 0)
})
});
let univariate = match univariate {
Some(u) => u,
None => {
return Err(format!(
"no univariate polynomial found in variable {last_var} for back-substitution"
));
}
};
let roots = rational_roots_of_univariate(univariate, last_var);
let mut all_solutions = Vec::new();
for root in &roots {
if num_vars == 1 {
all_solutions.push(vec![root.clone()]);
} else {
let reduced: Vec<MultiPoly<Lex>> = basis
.iter()
.map(|p| p.substitute(last_var, root))
.filter(|p| !p.is_zero())
.collect();
match solve_triangular(&reduced, num_vars - 1) {
Ok(sub_sols) => {
for mut sol in sub_sols {
sol.push(root.clone());
all_solutions.push(sol);
}
}
Err(_) => {
continue;
}
}
}
}
Ok(all_solutions)
}
fn rational_roots_of_univariate(poly: &MultiPoly<Lex>, var_idx: usize) -> Vec<Ratio<BigInt>> {
let mut max_deg: u32 = 0;
for (exp, _) in poly.terms() {
max_deg = max_deg.max(exp[var_idx]);
}
let mut coeffs = vec![Ratio::<BigInt>::zero(); max_deg as usize + 1];
for (exp, coeff) in poly.terms() {
let deg = exp[var_idx] as usize;
coeffs[deg] = coeffs[deg].clone() + coeff.clone();
}
let uni = crate::poly::Poly::from_coeffs(coeffs);
rational_roots_of_poly(&uni)
}
fn rational_roots_of_poly(poly: &crate::poly::Poly) -> Vec<Ratio<BigInt>> {
if poly.is_zero() {
return vec![];
}
if poly.is_constant() {
return vec![];
}
let mut roots = Vec::new();
if poly.eval(&Ratio::zero()).is_zero() {
roots.push(Ratio::zero());
}
let prim = poly.primitive_part();
let const_term = prim.eval(&Ratio::zero());
let lc = match prim.leading_coeff() {
Some(c) => c.clone(),
None => return roots,
};
let const_abs = const_term.numer().to_i64().map(|n| n.abs()).unwrap_or(0);
let lc_abs = lc.numer().to_i64().map(|n| n.abs()).unwrap_or(1);
if const_abs == 0 {
let x_poly = crate::poly::Poly::from_coeffs(vec![Ratio::zero(), Ratio::one()]);
let (quotient, _) = poly.div_rem(&x_poly);
let mut other_roots = rational_roots_of_poly("ient);
for r in other_roots.drain(..) {
if !roots.contains(&r) {
roots.push(r);
}
}
return roots;
}
let const_for_divs = if const_abs > super::MAX_DIVISOR_COEFFICIENT as i64 {
tracing::debug!(
"polysys: coefficient {} exceeds divisor cap {}; truncating for rational root search",
const_abs,
super::MAX_DIVISOR_COEFFICIENT
);
super::MAX_DIVISOR_COEFFICIENT as i64
} else {
const_abs
};
let lc_for_divs = if lc_abs > super::MAX_DIVISOR_COEFFICIENT as i64 {
tracing::debug!(
"polysys: leading coefficient {} exceeds divisor cap {}; truncating for rational root search",
lc_abs,
super::MAX_DIVISOR_COEFFICIENT
);
super::MAX_DIVISOR_COEFFICIENT as i64
} else {
lc_abs
};
let p_divs = divisors(const_for_divs);
let q_divs = divisors(lc_for_divs);
for &p in &p_divs {
for &q in &q_divs {
for &sign in &[1i64, -1] {
let candidate = Ratio::new(BigInt::from(sign * p), BigInt::from(q));
if !roots.contains(&candidate) && poly.eval(&candidate).is_zero() {
roots.push(candidate);
}
}
}
}
roots
}
fn divisors(n: i64) -> Vec<i64> {
if n == 0 {
return vec![1];
}
let n = n.abs();
let mut result = Vec::new();
let mut d = 1i64;
while d * d <= n {
if n % d == 0 {
result.push(d);
if d != n / d {
result.push(n / d);
}
}
d += 1;
}
result.sort();
result
}
fn expr_to_multipoly(
arena: &crate::base::arena::Arena,
expr: ExprId,
num_vars: usize,
var_map: &FxHashMap<ExprId, usize>,
) -> Option<MultiPoly<GrevLex>> {
if let Some(&idx) = var_map.get(&expr) {
return Some(MultiPoly::var(num_vars, idx));
}
let post_order = walk::post_order_ids(arena, expr);
let mut cache: FxHashMap<ExprId, MultiPoly<GrevLex>> = FxHashMap::default();
for &id in &post_order {
let poly = convert_node_multi(arena, id, num_vars, var_map, &cache)?;
cache.insert(id, poly);
}
cache.remove(&expr)
}
fn convert_node_multi(
arena: &crate::base::arena::Arena,
id: ExprId,
num_vars: usize,
var_map: &FxHashMap<ExprId, usize>,
cache: &FxHashMap<ExprId, MultiPoly<GrevLex>>,
) -> Option<MultiPoly<GrevLex>> {
if let Some(&idx) = var_map.get(&id) {
return Some(MultiPoly::var(num_vars, idx));
}
let node = arena.node(id);
match node {
ExprNode::Num(nid) => {
let r = arena.num(*nid).clone();
Some(MultiPoly::constant(num_vars, r))
}
ExprNode::Symbol(_) => None,
ExprNode::Add(children) => {
let mut result = MultiPoly::zero(num_vars);
for &child in children.iter() {
let child_poly = cache.get(&child)?;
result = result.add(child_poly);
}
Some(result)
}
ExprNode::Mul(children) => {
let mut result = MultiPoly::from_int(num_vars, 1);
for &child in children.iter() {
let child_poly = cache.get(&child)?;
result = result.mul(child_poly);
}
Some(result)
}
ExprNode::Pow(base, exp) => {
let base_poly = cache.get(base)?;
if var_map.contains_key(exp) {
return None;
}
let exp_val = match arena.node(*exp) {
ExprNode::Num(nid) => arena.num(*nid).clone(),
_ => return None,
};
if !exp_val.is_integer() {
return None;
}
let n: i64 = exp_val.to_integer().try_into().ok()?;
if n < 0 {
return None;
}
let mut result = MultiPoly::from_int(num_vars, 1);
for _ in 0..n {
result = result.mul(base_poly);
}
Some(result)
}
ExprNode::Neg(inner_id) => {
let inner_poly = cache.get(inner_id)?;
let neg_one = MultiPoly::from_int(num_vars, -1);
Some(neg_one.mul(inner_poly))
}
_ => None,
}
}
pub fn solve_system_ex(eqs: &[Ex], vars: &[Ex]) -> Result<Vec<Vec<Ex>>, SymplexError> {
if eqs.is_empty() || vars.is_empty() {
return Ok(vec![vec![]]);
}
let first = &eqs[0];
let num_vars = vars.len();
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 var_map: FxHashMap<ExprId, usize> =
var_ids.iter().enumerate().map(|(i, &id)| (id, i)).collect();
let mut polys = Vec::with_capacity(eqs.len());
{
let inner = first.inner.read();
let arena = &inner.arena;
for &eq_id in &eq_ids {
match expr_to_multipoly(arena, eq_id, num_vars, &var_map) {
Some(p) => polys.push(p),
None => {
let shown = arena.display(eq_id).to_string();
return Err(SymplexError::ComputationFailed {
operation: "solve_system_ex",
reason: format!(
"expression is not polynomial in the given variables: {shown}"
),
});
}
}
}
}
let nonzero: Vec<MultiPoly<GrevLex>> = polys.into_iter().filter(|p| !p.is_zero()).collect();
if nonzero.is_empty() {
return Err(SymplexError::InfiniteSolutions {
operation: "solve_system_ex",
reason: "every equation is identically zero".into(),
});
}
if nonzero.iter().all(|p| p.total_degree().unwrap_or(0) <= 1) {
return match linsolve(eqs, vars)? {
LinearSolution::Unique(pairs) => Ok(vec![pairs.into_iter().map(|(_, v)| v).collect()]),
LinearSolution::Inconsistent => Ok(vec![]),
LinearSolution::Parametric { free, .. } => {
let names: Vec<String> = free.iter().map(|f| format!("{f}")).collect();
Err(SymplexError::InfiniteSolutions {
operation: "solve_system_ex",
reason: format!(
"linear system is under-determined (free variables: {})",
names.join(", ")
),
})
}
};
}
let grevlex_gb = groebner::groebner_basis(&nonzero);
if grevlex_gb.is_empty() {
return Err(SymplexError::InfiniteSolutions {
operation: "solve_system_ex",
reason: "trivial ideal: system is under-determined".into(),
});
}
if grevlex_gb.iter().any(|p| p.total_degree() == Some(0)) {
return Ok(vec![]); }
if !groebner::is_zero_dimensional(&grevlex_gb) {
return Err(SymplexError::InfiniteSolutions {
operation: "solve_system_ex",
reason: "ideal is positive-dimensional: infinitely many solutions".into(),
});
}
let lex_gb = groebner::groebner_basis_lex(&nonzero);
if lex_gb.is_empty() {
return Ok(vec![]);
}
let solutions = {
let mut guard = first.inner.write();
let arena = &mut guard.arena;
let basis_exprs: Vec<ExprId> = lex_gb
.iter()
.map(|p| multipoly_to_expr(arena, p, &var_ids))
.collect();
let candidates = solve_triangular_symbolic(arena, &basis_exprs, &var_ids);
let degrees: Vec<usize> = nonzero
.iter()
.map(|p| p.total_degree().unwrap_or(0) as usize)
.collect();
let nonzero_eq_ids: Vec<ExprId> = eq_ids
.iter()
.copied()
.filter(|&id| !arena.is_zero_structural(id))
.collect();
let checks: Vec<(ExprId, usize)> = if nonzero_eq_ids.len() == degrees.len() {
nonzero_eq_ids.into_iter().zip(degrees).collect()
} else {
eq_ids.iter().map(|&id| (id, 1)).collect()
};
candidates
.into_iter()
.filter(|sol| tuple_satisfies_all(arena, &checks, &var_ids, sol) == Some(true))
.collect::<Vec<_>>()
};
let result: Vec<Vec<Ex>> = solutions
.into_iter()
.map(|sol| {
sol.into_iter()
.map(|id| first.wrap(id).eval().simplify())
.collect()
})
.collect();
Ok(result)
}
const RESIDUAL_REL_TOL: f64 = 1e-8;
fn tuple_satisfies_all(
arena: &mut Arena,
eqs: &[(ExprId, usize)],
vars: &[ExprId],
vals: &[ExprId],
) -> Option<bool> {
if vars.len() != vals.len() {
return None;
}
let mut max_abs = 0.0f64;
for &v in vals {
let ev = crate::transforms::eval::eval(arena, v);
if crate::base::walk::has_unevaluated(arena, ev) {
return None;
}
let s = crate::transforms::evalf::evalf(arena, ev, 30).ok()?;
let mag = crate::transforms::solve::parse_evalf_magnitude(&s)?;
if !mag.is_finite() {
return Some(false);
}
max_abs = max_abs.max(mag);
}
let pairs: Vec<(ExprId, ExprId)> = vars.iter().copied().zip(vals.iter().copied()).collect();
for &(eq, deg) in eqs {
let s = crate::transforms::subs::subs_map(arena, eq, &pairs);
let s = crate::transforms::eval::eval(arena, s);
if arena.is_zero_structural(s) {
continue;
}
if let Some(r) = arena.as_num(s) {
if r.is_zero() {
continue;
}
return Some(false);
}
if !crate::base::walk::free_symbols(arena, s).is_empty() {
return None;
}
let text = crate::transforms::evalf::evalf(arena, s, 30).ok()?;
let residual = crate::transforms::solve::parse_evalf_magnitude(&text)?;
if !residual.is_finite() {
return Some(false);
}
let scale = (1.0 + max_abs).powi(deg.min(64) as i32);
if residual > RESIDUAL_REL_TOL * scale {
return Some(false);
}
}
Some(true)
}
fn constant_is_zero(arena: &mut Arena, e: ExprId) -> Option<bool> {
let e1 = crate::transforms::eval::eval(arena, e);
if arena.is_zero_structural(e1) {
return Some(true);
}
if let Some(r) = arena.as_num(e1) {
return Some(r.is_zero());
}
let e2 = crate::transforms::expand::expand(arena, e1);
let e2 = crate::transforms::eval::eval(arena, e2);
if arena.is_zero_structural(e2) {
return Some(true);
}
if let Some(r) = arena.as_num(e2) {
return Some(r.is_zero());
}
if crate::base::walk::free_symbols(arena, e2).is_empty()
&& let Ok(s) = crate::transforms::evalf::evalf(arena, e2, 20)
&& let Some(mag) = crate::transforms::solve::parse_evalf_magnitude(&s)
{
return Some(mag < 1e-10);
}
None
}
fn poly_vanishes_numerically(arena: &mut Arena, p: ExprId, var: ExprId) -> bool {
let Some(coeffs) = crate::transforms::solve::symbolic_poly_coeffs(arena, p, var) else {
return false;
};
if coeffs.is_empty() {
return true;
}
coeffs
.iter()
.all(|&c| constant_is_zero(arena, c) == Some(true))
}
fn substitute_and_filter(
arena: &mut Arena,
exprs: &[ExprId],
var: ExprId,
value: ExprId,
remaining_vars: &[ExprId],
) -> Option<Vec<ExprId>> {
let mut out = Vec::with_capacity(exprs.len());
for &e in exprs {
let s = crate::transforms::subs::subs(arena, e, var, value);
let s = crate::transforms::eval::eval(arena, s);
let s = crate::transforms::expand::expand(arena, s);
let s = crate::transforms::eval::eval(arena, s);
let involves_var = remaining_vars
.iter()
.any(|&v| crate::base::walk::contains(arena, s, v));
if involves_var {
out.push(s);
} else {
match constant_is_zero(arena, s) {
Some(true) => {}
Some(false) => return None,
None => {} }
}
}
Some(out)
}
fn solve_triangular_symbolic(
arena: &mut Arena,
polys: &[ExprId],
var_ids: &[ExprId],
) -> Vec<Vec<ExprId>> {
if var_ids.is_empty() {
for &p in polys {
if constant_is_zero(arena, p) == Some(false) {
return vec![];
}
}
return vec![vec![]];
}
let last_idx = var_ids.len() - 1;
let last = var_ids[last_idx];
let earlier = &var_ids[..last_idx];
let mut univariate: Vec<(usize, ExprId)> = Vec::new();
let mut with_last: Vec<(usize, ExprId)> = Vec::new();
for &p in polys {
if !crate::base::walk::contains(arena, p, last) {
continue;
}
let deg = arena.degree_of(p, last).unwrap_or(usize::MAX);
if earlier
.iter()
.any(|&v| crate::base::walk::contains(arena, p, v))
{
with_last.push((deg, p));
} else {
univariate.push((deg, p));
}
}
if !univariate.is_empty() {
univariate.sort_by_key(|(d, _)| *d);
let mut chosen: Option<(ExprId, Vec<crate::transforms::solve::Solution>)> = None;
for &(_, cand) in &univariate {
if poly_vanishes_numerically(arena, cand, last) {
continue;
}
match crate::transforms::solve::solve_classified(arena, cand, last) {
crate::transforms::solve::SolveOutcome::Solutions(s) if !s.is_empty() => {
chosen = Some((cand, s));
break;
}
crate::transforms::solve::SolveOutcome::Solutions(_) => continue,
crate::transforms::solve::SolveOutcome::Identity => continue,
crate::transforms::solve::SolveOutcome::NoSolution(_) => return vec![],
}
}
let Some((pivot, roots)) = chosen else {
return vec![];
};
let mut all = Vec::new();
for root in roots {
let others: Vec<ExprId> = univariate
.iter()
.filter(|(_, p)| *p != pivot)
.map(|(_, p)| *p)
.collect();
if substitute_and_filter(arena, &others, last, root.value, &[]).is_none() {
continue;
}
let rest: Vec<ExprId> = polys
.iter()
.copied()
.filter(|p| !univariate.iter().any(|(_, u)| u == p))
.collect();
let Some(reduced) = substitute_and_filter(arena, &rest, last, root.value, earlier)
else {
continue;
};
for mut sub in solve_triangular_symbolic(arena, &reduced, earlier) {
sub.push(root.value);
all.push(sub);
}
}
return all;
}
if !with_last.is_empty() {
with_last.sort_by_key(|(d, _)| *d);
let mut chosen: Option<(ExprId, Vec<crate::transforms::solve::Solution>)> = None;
for &(_, cand) in &with_last {
match crate::transforms::solve::solve_classified(arena, cand, last) {
crate::transforms::solve::SolveOutcome::Solutions(s) if !s.is_empty() => {
chosen = Some((cand, s));
break;
}
crate::transforms::solve::SolveOutcome::NoSolution(_) => return vec![],
_ => continue,
}
}
let Some((pivot, roots)) = chosen else {
return vec![];
};
let mut all = Vec::new();
for root in roots {
let rest: Vec<ExprId> = polys.iter().copied().filter(|&p| p != pivot).collect();
let Some(reduced) = substitute_and_filter(arena, &rest, last, root.value, earlier)
else {
continue;
};
for sub in solve_triangular_symbolic(arena, &reduced, earlier) {
let mut val = root.value;
for (i, &v) in earlier.iter().enumerate() {
val = crate::transforms::subs::subs(arena, val, v, sub[i]);
}
let val = crate::transforms::eval::eval(arena, val);
let mut full = sub;
full.push(val);
all.push(full);
}
}
return all;
}
vec![]
}
fn multipoly_to_expr(arena: &mut Arena, poly: &MultiPoly<Lex>, var_ids: &[ExprId]) -> ExprId {
let mut terms = Vec::new();
for (exp, coeff) in poly.terms() {
let coeff_id = ratio_to_expr(arena, coeff);
let mut factors = vec![coeff_id];
for (i, &e) in exp.iter().enumerate() {
if e > 0 && i < var_ids.len() {
if e == 1 {
factors.push(var_ids[i]);
} else {
let exp_id = arena.int(i64::from(e));
factors.push(arena.pow(var_ids[i], exp_id));
}
}
}
terms.push(arena.mul(&factors));
}
if terms.is_empty() {
arena.zero
} else {
arena.add(&terms)
}
}
fn ratio_to_expr(arena: &mut Arena, r: &Ratio<BigInt>) -> ExprId {
let nid = arena.intern_num(r.clone());
arena.intern(ExprNode::Num(nid))
}
#[cfg(test)]
mod tests {
use super::*;
fn rat(n: i64) -> Ratio<BigInt> {
Ratio::from_integer(BigInt::from(n))
}
fn ratio(p: i64, q: i64) -> Ratio<BigInt> {
Ratio::new(BigInt::from(p), BigInt::from(q))
}
#[test]
fn test_divisors_basic() {
assert_eq!(divisors(1), vec![1]);
assert_eq!(divisors(6), vec![1, 2, 3, 6]);
assert_eq!(divisors(0), vec![1]);
}
#[test]
fn test_rational_roots_simple() {
let poly = crate::poly::Poly::from_coeffs(vec![rat(-4), rat(0), rat(1)]);
let mut roots = rational_roots_of_poly(&poly);
roots.sort();
assert_eq!(roots, vec![rat(-2), rat(2)]);
}
#[test]
fn test_rational_roots_with_rational_root() {
let poly = crate::poly::Poly::from_coeffs(vec![rat(-1), rat(2)]);
let roots = rational_roots_of_poly(&poly);
assert_eq!(roots, vec![ratio(1, 2)]);
}
#[test]
fn test_rational_roots_no_rational() {
let poly = crate::poly::Poly::from_coeffs(vec![rat(-2), rat(0), rat(1)]);
let roots = rational_roots_of_poly(&poly);
assert!(roots.is_empty());
}
#[test]
fn test_rational_roots_zero_root() {
let poly = crate::poly::Poly::from_coeffs(vec![rat(0), rat(-1), rat(1)]);
let mut roots = rational_roots_of_poly(&poly);
roots.sort();
assert_eq!(roots, vec![rat(0), rat(1)]);
}
#[test]
fn test_solve_linear_single_var() {
let p = MultiPoly::<GrevLex>::var(1, 0).add(&MultiPoly::from_int(1, -3));
let sols = solve_polynomial_system(&[p]).unwrap();
assert_eq!(sols.len(), 1);
assert_eq!(sols[0], vec![rat(3)]);
}
}