use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::{Signed, Zero};
use crate::api::expr::{Ex, Expr, ExprType, Numeric};
use crate::base::arena::Arena;
use crate::base::errors::SymplexError;
use crate::base::node::{ExprId, ExprNode};
use crate::poly::Poly;
use crate::poly::groebner::{groebner_basis, groebner_basis_lex};
use crate::poly::multipoly::{GrevLex, Lex, MultiPoly};
use crate::poly::polybridge::{expr_to_multipoly, expr_to_poly, multipoly_to_expr, poly_to_expr};
use crate::poly::sturm::SturmChain;
pub use crate::poly::multipoly::MonomialOrder;
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::InvalidArgument {
operation,
reason: reason.into(),
}
}
fn num_expr(arena: &mut Arena, r: Ratio<BigInt>) -> ExprId {
let nid = arena.intern_num(r);
arena.intern(ExprNode::Num(nid))
}
fn integer_primitive(p: &Poly) -> Poly {
let prim = p.primitive_part();
match prim.leading_coeff() {
Some(lc) if lc.is_negative() => -&prim,
_ => prim,
}
}
fn shared_generators(arena: &Arena, exprs: &[ExprId]) -> Vec<ExprId> {
let mut gens: Vec<ExprId> = Vec::new();
for &e in exprs {
for s in crate::base::walk::free_symbols(arena, e) {
if !gens.contains(&s) {
gens.push(s);
}
}
}
gens.sort_by(|a, b| {
arena
.sort_key(*a)
.cmp(arena.sort_key(*b))
.then_with(|| a.0.cmp(&b.0))
});
gens
}
fn validate_vars(
probe: &Ex,
vars: &[Ex],
operation: &'static str,
) -> Result<Vec<ExprId>, SymplexError> {
if vars.is_empty() {
return Err(invalid(operation, "at least one variable is required"));
}
let mut ids: Vec<ExprId> = Vec::with_capacity(vars.len());
for v in vars {
let id = probe.checked_id(v);
if v.expr_type() != ExprType::Symbol {
return Err(invalid(
operation,
format!("variable `{v}` is not a symbol"),
));
}
if ids.contains(&id) {
return Err(invalid(operation, format!("duplicate variable `{v}`")));
}
ids.push(id);
}
Ok(ids)
}
fn to_multipolys(
arena: &Arena,
exprs: &[ExprId],
vars: &[ExprId],
operation: &'static str,
) -> Result<Vec<MultiPoly<GrevLex>>, SymplexError> {
exprs
.iter()
.map(|&e| {
expr_to_multipoly(arena, e, vars).ok_or_else(|| {
invalid(
operation,
format!(
"`{}` is not a polynomial in the given variables with rational coefficients",
arena.display(e)
),
)
})
})
.collect()
}
fn groebner_in_order(
polys: &[MultiPoly<GrevLex>],
order: MonomialOrder,
) -> Vec<MultiPoly<GrevLex>> {
match order {
MonomialOrder::GrevLex => groebner_basis(polys),
MonomialOrder::Lex => groebner_basis_lex(polys)
.iter()
.map(MultiPoly::convert_order)
.collect(),
}
}
fn reduce_in_order(
f: &MultiPoly<GrevLex>,
basis: &[MultiPoly<GrevLex>],
order: MonomialOrder,
) -> MultiPoly<GrevLex> {
match order {
MonomialOrder::GrevLex => {
let refs: Vec<&MultiPoly<GrevLex>> = basis.iter().collect();
f.reduce(&refs)
}
MonomialOrder::Lex => {
let lex_basis: Vec<MultiPoly<Lex>> =
basis.iter().map(MultiPoly::convert_order).collect();
let refs: Vec<&MultiPoly<Lex>> = lex_basis.iter().collect();
f.convert_order::<Lex>().reduce(&refs).convert_order()
}
}
}
fn real_roots_ids(arena: &mut Arena, f: &Poly, var: ExprId) -> Option<Vec<ExprId>> {
struct Factor {
poly: Poly,
chain: SturmChain,
rational: Option<Ratio<BigInt>>,
indices: Vec<usize>,
emitted: usize,
expr: Option<ExprId>,
}
let (_content, parts) = f.factor_over_z();
let mut factors: Vec<Factor> = Vec::with_capacity(parts.len());
let mut square_free = Poly::from_int(1);
for (g, _mult) in &parts {
let deg = g.degree()?;
if deg == 0 {
continue;
}
square_free = &square_free * g;
let chain = SturmChain::new(g);
let real_count = chain.count_real_roots();
let rational = (deg == 1).then(|| -(g.coeff(0) / g.coeff(1)));
let mut indices = Vec::new();
if rational.is_none() && real_count > 0 {
indices = crate::poly::roots::nroots_f64(g, 128)
.iter()
.enumerate()
.filter(|(_, (_, im))| *im == 0.0)
.map(|(i, _)| i)
.collect();
if indices.len() != real_count {
return None;
}
}
factors.push(Factor {
poly: g.clone(),
chain,
rational,
indices,
emitted: 0,
expr: None,
});
}
if square_free.degree().unwrap_or(0) == 0 {
return None;
}
let intervals = SturmChain::new(&square_free).isolate_all_real_roots();
let mut out: Vec<ExprId> = Vec::with_capacity(intervals.len());
for (lo, hi) in intervals {
let owner = factors.iter_mut().find(|fac| {
if lo == hi {
fac.poly.eval(&lo).is_zero()
} else {
fac.chain.count_roots_in(&lo, &hi) == 1
}
})?;
match &owner.rational {
Some(r) => out.push(num_expr(arena, r.clone())),
None => {
let k = *owner.indices.get(owner.emitted)?;
owner.emitted += 1;
let g_expr = match owner.expr {
Some(e) => e,
None => {
let e = poly_to_expr(arena, &owner.poly, var);
owner.expr = Some(e);
e
}
};
let idx = arena.int(k as i64);
out.push(arena.intern(ExprNode::RootOf(g_expr, idx)));
}
}
}
Some(out)
}
fn sylvester_resultant(ctx: &crate::api::context::Context, f: &[Ex], g: &[Ex]) -> Option<Ex> {
let m = f.len().checked_sub(1)?;
let n = g.len().checked_sub(1)?;
if m == 0 && n == 0 {
return Some(ctx.one());
}
if m == 0 {
return Some(f[0].powi(n as i64));
}
if n == 0 {
return Some(g[0].powi(m as i64));
}
let size = m + n;
let zero = ctx.zero();
let mut rows: Vec<Vec<Ex>> = Vec::with_capacity(size);
for i in 0..n {
let mut row = vec![zero.clone(); size];
for (j, c) in f.iter().enumerate() {
row[i + j] = c.clone();
}
rows.push(row);
}
for i in 0..m {
let mut row = vec![zero.clone(); size];
for (j, c) in g.iter().enumerate() {
row[i + j] = c.clone();
}
rows.push(row);
}
let det = crate::domains::matrix::Matrix::new(rows).ok()?.det().ok()?;
Some(det.expand())
}
impl Expr<Numeric> {
#[must_use]
pub fn minimal_polynomial(&self, var: &Ex) -> Option<Ex> {
let var_id = self.checked_id(var);
let id = {
let mut inner = self.inner.write();
let arena = &mut inner.arena;
let mp =
crate::poly::algebraic::minimal_polynomial(arena, self.raw_id()).or_else(|| {
let expanded = crate::transforms::expand::expand(arena, self.raw_id());
if expanded == self.raw_id() {
None
} else {
crate::poly::algebraic::minimal_polynomial(arena, expanded)
}
})?;
poly_to_expr(arena, &integer_primitive(&mp), var_id)
};
Some(self.wrap(id))
}
#[must_use]
pub fn gcd_all(&self, other: &Ex) -> Option<Ex> {
self.multipoly_binary(other, MultiPoly::gcd)
}
#[must_use]
pub fn lcm_all(&self, other: &Ex) -> Option<Ex> {
self.multipoly_binary(other, MultiPoly::lcm)
}
fn multipoly_binary(
&self,
other: &Ex,
op: fn(&MultiPoly<GrevLex>, &MultiPoly<GrevLex>) -> MultiPoly<GrevLex>,
) -> Option<Ex> {
let other_id = self.checked_id(other);
let id = {
let mut inner = self.inner.write();
let arena = &mut inner.arena;
let gens = shared_generators(arena, &[self.raw_id(), other_id]);
let a = expr_to_multipoly(arena, self.raw_id(), &gens)?;
let b = expr_to_multipoly(arena, other_id, &gens)?;
let result = op(&a, &b);
multipoly_to_expr(arena, &result, &gens)
};
Some(self.wrap(id))
}
pub fn groebner(
polys: &[Ex],
vars: &[Ex],
order: MonomialOrder,
) -> Result<Vec<Ex>, SymplexError> {
const OP: &str = "groebner";
let probe = vars
.first()
.ok_or_else(|| invalid(OP, "at least one variable is required"))?;
let var_ids = validate_vars(probe, vars, OP)?;
let poly_ids: Vec<ExprId> = polys.iter().map(|p| probe.checked_id(p)).collect();
let ids: Vec<ExprId> = {
let mut inner = probe.inner.write();
let arena = &mut inner.arena;
let mps = to_multipolys(arena, &poly_ids, &var_ids, OP)?;
groebner_in_order(&mps, order)
.iter()
.map(|g| multipoly_to_expr(arena, g, &var_ids))
.collect()
};
Ok(ids.into_iter().map(|id| probe.wrap(id)).collect())
}
pub fn reduce_modulo(
&self,
basis: &[Ex],
vars: &[Ex],
order: MonomialOrder,
) -> Result<Ex, SymplexError> {
const OP: &str = "reduce_modulo";
let var_ids = validate_vars(self, vars, OP)?;
let basis_ids: Vec<ExprId> = basis.iter().map(|b| self.checked_id(b)).collect();
let id = {
let mut inner = self.inner.write();
let arena = &mut inner.arena;
let f = to_multipolys(arena, &[self.raw_id()], &var_ids, OP)?.remove(0);
let divisors = to_multipolys(arena, &basis_ids, &var_ids, OP)?;
let r = reduce_in_order(&f, &divisors, order);
multipoly_to_expr(arena, &r, &var_ids)
};
Ok(self.wrap(id))
}
#[must_use]
pub fn real_roots(&self, var: &Ex) -> Option<Vec<Ex>> {
let var_id = self.checked_id(var);
let ids = {
let mut inner = self.inner.write();
let arena = &mut inner.arena;
let f = expr_to_poly(arena, self.raw_id(), var_id)?;
if f.degree().unwrap_or(0) == 0 {
return None;
}
real_roots_ids(arena, &f, var_id)?
};
Some(ids.into_iter().map(|id| self.wrap(id)).collect())
}
#[must_use]
pub fn root_of(&self, var: &Ex, index: usize) -> Option<Ex> {
self.real_roots(var)?.into_iter().nth(index)
}
pub fn factor_mod(&self, var: &Ex, p: u64) -> Result<(Ex, Vec<(Ex, u32)>), SymplexError> {
const OP: &str = "factor_mod";
let var_id = self.checked_id(var);
if !crate::domains::ntheory::isprime(p) {
return Err(invalid(OP, format!("modulus {p} is not prime")));
}
if p == 2 || p >= crate::poly::factor_zassenhaus::MAX_PRIME {
return Err(invalid(
OP,
format!(
"modulus {p} is not supported: p must be an odd prime below {}",
crate::poly::factor_zassenhaus::MAX_PRIME
),
));
}
let (lc_id, factor_ids) = {
let mut inner = self.inner.write();
let arena = &mut inner.arena;
let f = expr_to_poly(arena, self.raw_id(), var_id).ok_or_else(|| {
invalid(
OP,
"expression is not a polynomial in the given variable with rational coefficients",
)
})?;
let pb = BigInt::from(p);
if f.coeffs().iter().any(|c| (c.denom() % &pb).is_zero()) {
return Err(invalid(
OP,
format!(
"a coefficient has a denominator divisible by {p}, so it has no inverse mod {p}"
),
));
}
let (lc, factors) =
crate::poly::factor_zassenhaus::factor_mod_p(&f, p).ok_or_else(|| {
SymplexError::ComputationFailed {
operation: OP,
reason: "factor_mod_p rejected a valid modulus and polynomial".into(),
}
})?;
let lc_id = arena.int(lc as i64);
let factor_ids: Vec<(ExprId, u32)> = factors
.iter()
.map(|(coeffs, m)| {
let g = Poly::from_coeffs(
coeffs
.iter()
.map(|&c| Ratio::from_integer(BigInt::from(c)))
.collect(),
);
(poly_to_expr(arena, &g, var_id), *m)
})
.collect();
(lc_id, factor_ids)
};
Ok((
self.wrap(lc_id),
factor_ids
.into_iter()
.map(|(id, m)| (self.wrap(id), m))
.collect(),
))
}
#[must_use]
pub fn resultant_symbolic(&self, other: &Ex, var: &Ex) -> Option<Ex> {
let _ = self.checked_id(other);
let _ = self.checked_id(var);
let f = crate::api::poly_ex::Poly::new(self, &[var])?;
let g = crate::api::poly_ex::Poly::new(other, &[var])?;
if f.is_zero() || g.is_zero() {
return Some(self.context().zero());
}
let fc = f.all_coeffs()?;
let gc = g.all_coeffs()?;
sylvester_resultant(&self.context(), &fc, &gc)
}
#[must_use]
pub fn discriminant_symbolic(&self, var: &Ex) -> Option<Ex> {
let _ = self.checked_id(var);
let f = crate::api::poly_ex::Poly::new(self, &[var])?;
let coeffs = f.all_coeffs()?; let n = coeffs.len().checked_sub(1)?;
if n == 0 {
return None;
}
let ctx = self.context();
if n == 1 {
return Some(ctx.one());
}
let deriv: Vec<Ex> = coeffs[..n]
.iter()
.enumerate()
.map(|(j, c)| c * ((n - j) as i64))
.collect();
let size = 2 * n - 1;
let zero = ctx.zero();
let mut rows: Vec<Vec<Ex>> = Vec::with_capacity(size);
for i in 0..n - 1 {
let mut row = vec![zero.clone(); size];
for (j, c) in coeffs.iter().enumerate() {
row[i + j] = c.clone();
}
rows.push(row);
}
for i in 0..n {
let mut row = vec![zero.clone(); size];
for (j, c) in deriv.iter().enumerate() {
row[i + j] = c.clone();
}
rows.push(row);
}
for (j, (slot, c)) in rows[n - 1].iter_mut().zip(coeffs.iter()).enumerate() {
*slot = -&(c * (j as i64));
}
let minor: Vec<Vec<Ex>> = rows[1..].iter().map(|row| row[1..].to_vec()).collect();
let det = crate::domains::matrix::Matrix::new(minor)
.ok()?
.det()
.ok()?;
let det = det.expand();
Some(if (n * (n - 1) / 2) % 2 == 1 {
-det
} else {
det
})
}
}