use std::borrow::Cow;
use std::collections::{BTreeMap, BTreeSet};
use std::fmt;
use std::sync::{Arc, OnceLock};
use num_bigint::BigInt;
use num_complex::Complex64;
use num_integer::Integer;
use num_rational::Ratio;
use num_traits::{One, Signed, Zero};
use rustc_hash::FxHashMap;
use crate::api::context::Context;
use crate::api::expr::{Ex, ExprType};
use crate::base::arena::Arena;
use crate::base::config::EvalConfig;
use crate::base::errors::SymplexError;
use crate::base::interval::Interval;
use crate::base::node::{ExprId, ExprNode};
use crate::domains::matrix::Matrix;
use crate::poly::multipoly::{GrevLex, Lex, MultiPoly};
use crate::poly::polybridge;
use crate::poly::zpoly::pow_ratio;
fn invalid(operation: &'static str, reason: impl Into<String>) -> SymplexError {
SymplexError::invalid_argument(operation, reason)
}
fn wrap(ctx: &Context, id: ExprId) -> Ex {
Ex::from_raw_parts(ctx.id, Arc::clone(&ctx.inner), id)
}
fn norm_coeff(arena: &mut Arena, id: ExprId) -> ExprId {
let e = crate::transforms::expand::expand(arena, id);
crate::transforms::eval::eval(arena, e)
}
fn mentions_any(arena: &Arena, expr: ExprId, gens: &[ExprId]) -> bool {
gens.iter()
.any(|&g| crate::base::walk::contains(arena, expr, g))
}
fn normalize_terms(arena: &mut Arena, raw: Vec<(Vec<u32>, ExprId)>) -> Vec<(Vec<u32>, ExprId)> {
let mut buckets: BTreeMap<Vec<u32>, Vec<ExprId>> = BTreeMap::new();
for (e, c) in raw {
buckets.entry(e).or_default().push(c);
}
let mut out = Vec::with_capacity(buckets.len());
for (e, cs) in buckets {
let c = if cs.len() == 1 { cs[0] } else { arena.add(&cs) };
let c = norm_coeff(arena, c);
if !arena.is_zero_structural(c) {
out.push((e, c));
}
}
out
}
fn monomial_expr(arena: &mut Arena, c: ExprId, exps: &[u32], gens: &[ExprId]) -> ExprId {
let mut factors: Vec<ExprId> = Vec::with_capacity(exps.len() + 1);
if c != arena.one {
factors.push(c);
}
for (&e, &g) in exps.iter().zip(gens) {
match e {
0 => {}
1 => factors.push(g),
_ => {
let e_id = arena.int(i64::from(e));
factors.push(arena.pow(g, e_id));
}
}
}
match factors.len() {
0 => arena.one,
1 => factors[0],
_ => arena.mul(&factors),
}
}
fn validate_gens(
ctx: &Context,
gens: &[&Ex],
operation: &'static str,
) -> Result<Vec<Ex>, SymplexError> {
let probe = ctx.zero();
let mut ids: Vec<ExprId> = Vec::with_capacity(gens.len());
for g in gens {
let id = probe.checked_id(g);
if g.expr_type() != ExprType::Symbol {
return Err(invalid(
operation,
format!("generator `{g}` is not a symbol"),
));
}
if ids.contains(&id) {
return Err(invalid(operation, format!("duplicate generator `{g}`")));
}
ids.push(id);
}
Ok(gens.iter().map(|g| (*g).clone()).collect())
}
fn intern_ratio(arena: &mut Arena, r: &Ratio<BigInt>) -> ExprId {
let nid = arena.intern_num(r.clone());
arena.intern(ExprNode::Num(nid))
}
fn digits(r: &Ratio<BigInt>) -> usize {
r.numer().to_string().len() + r.denom().to_string().len()
}
fn pow_within_limits(config: &EvalConfig, base: &Ratio<BigInt>, exp: u32) -> bool {
if exp <= 1 || base.is_one() {
return true;
}
let exp = exp as usize;
exp <= config.max_pow_exponent
&& exp
.checked_mul(digits(base))
.is_some_and(|d| d <= config.max_result_digits)
}
fn degree_list_of(mp: &MultiPoly<Lex>) -> Vec<u32> {
let mut out = vec![0u32; mp.num_vars()];
for (e, _) in mp.terms() {
for (o, &x) in out.iter_mut().zip(e) {
*o = (*o).max(x);
}
}
out
}
fn mul_checked(a: &MultiPoly<Lex>, b: &MultiPoly<Lex>) -> Option<MultiPoly<Lex>> {
a.try_mul(b)
}
fn pow_checked(base: &MultiPoly<Lex>, n: u32) -> Option<MultiPoly<Lex>> {
base.try_pow(n)
}
fn eval_exact(mp: &MultiPoly<Lex>, vals: &[Ratio<BigInt>]) -> Ratio<BigInt> {
mp.eval(vals)
}
fn exact_pow(
config: &EvalConfig,
base: &MultiPoly<Lex>,
exp: &Ratio<BigInt>,
) -> Option<MultiPoly<Lex>> {
if !exp.is_integer() || exp.is_negative() {
return None;
}
let n: u32 = exp.to_integer().try_into().ok()?;
let nv = base.num_vars();
match n {
0 => return (!base.is_zero()).then(|| MultiPoly::from_int(nv, 1)),
1 => return Some(base.clone()),
_ => {}
}
if base.num_terms() > 1 && n as usize > config.max_pow_exponent.min(200) {
return None;
}
if base.terms().any(|(_, c)| !pow_within_limits(config, c, n)) {
return None;
}
match base.num_terms() {
0 => Some(MultiPoly::zero(nv)),
1 => {
let (e, c) = base.terms().next()?;
let exps: Option<Vec<u32>> = e.iter().map(|&x| x.checked_mul(n)).collect();
Some(MultiPoly::monomial(pow_ratio(c, n), exps?))
}
_ => pow_checked(base, n),
}
}
fn flat_factor(
arena: &Arena,
factor: ExprId,
gens: &[ExprId],
exps: &mut [u32],
coeff: &mut Option<Ratio<BigInt>>,
) -> Option<()> {
if let Some(i) = gens.iter().position(|&g| g == factor) {
exps[i] = exps[i].checked_add(1)?;
return Some(());
}
match arena.node(factor) {
ExprNode::Num(nid) => {
let c = arena.num(*nid);
*coeff = Some(match coeff.take() {
None => c.clone(),
Some(acc) => acc * c,
});
Some(())
}
ExprNode::Pow(base, exp) => {
let i = gens.iter().position(|&g| g == *base)?;
let k = arena.as_num(*exp)?;
if !k.is_integer() || k.is_negative() {
return None;
}
let k: u32 = k.to_integer().try_into().ok()?;
exps[i] = exps[i].checked_add(k)?;
Some(())
}
ExprNode::Mul(children) => {
for &c in children {
flat_factor(arena, c, gens, exps, coeff)?;
}
Some(())
}
ExprNode::Neg(inner) => {
flat_factor(arena, *inner, gens, exps, coeff)?;
*coeff = Some(match coeff.take() {
None => -Ratio::<BigInt>::one(),
Some(acc) => -acc,
});
Some(())
}
_ => None,
}
}
fn flat_terms(
arena: &Arena,
expr: ExprId,
gens: &[ExprId],
) -> Option<Vec<(Vec<u32>, Ratio<BigInt>)>> {
let terms: Vec<ExprId> = match arena.node(expr) {
ExprNode::Add(children) => children.to_vec(),
_ => vec![expr],
};
let mut out = Vec::with_capacity(terms.len());
for term in terms {
let mut exps = vec![0u32; gens.len()];
let mut coeff = None;
flat_factor(arena, term, gens, &mut exps, &mut coeff)?;
out.push((exps, coeff.unwrap_or_else(Ratio::one)));
}
Some(out)
}
fn exact_multipoly(arena: &Arena, expr: ExprId, gens: &[ExprId]) -> Option<MultiPoly<Lex>> {
let nv = gens.len();
let mut cache: FxHashMap<ExprId, MultiPoly<Lex>> = FxHashMap::default();
for id in crate::base::walk::post_order_ids(arena, expr) {
if let Some(i) = gens.iter().position(|&g| g == id) {
cache.insert(id, MultiPoly::var(nv, i));
continue;
}
let poly = match arena.node(id) {
ExprNode::Num(nid) => MultiPoly::constant(nv, arena.num(*nid).clone()),
ExprNode::Add(children) => {
let mut terms: Vec<(Vec<u32>, Ratio<BigInt>)> = Vec::new();
for c in children {
terms.extend(cache.get(c)?.terms().map(|(e, r)| (e.to_vec(), r.clone())));
}
MultiPoly::from_distinct_terms(nv, terms)?
}
ExprNode::Mul(children) => {
let mut acc = MultiPoly::from_int(nv, 1);
for c in children {
acc = mul_checked(&acc, cache.get(c)?)?;
}
acc
}
ExprNode::Pow(base, exp) => {
exact_pow(&arena.config, cache.get(base)?, arena.as_num(*exp)?)?
}
ExprNode::Neg(inner) => cache.get(inner)?.neg(),
_ => return None,
};
cache.insert(id, poly);
}
cache.remove(&expr)
}
fn rational_multipoly(nv: usize, terms: &BTreeMap<Vec<u32>, Ex>) -> Option<MultiPoly<Lex>> {
let mut out: Vec<(Vec<u32>, Ratio<BigInt>)> = Vec::with_capacity(terms.len());
for (e, c) in terms {
out.push((e.clone(), c.as_rational()?));
}
MultiPoly::from_distinct_terms(nv, out)
}
#[derive(Clone, Debug)]
struct ExactTerms {
mp: MultiPoly<Lex>,
ex: OnceLock<BTreeMap<Vec<u32>, Ex>>,
}
#[derive(Clone, Debug)]
enum Terms {
Exact(ExactTerms),
Symbolic(BTreeMap<Vec<u32>, Ex>),
}
enum CoeffView<'a> {
Rational(&'a Ratio<BigInt>),
Symbolic(&'a Ex),
}
#[derive(Clone, Debug)]
pub struct Poly {
ctx: Context,
gens: Vec<Ex>,
terms: Terms,
}
impl Poly {
fn from_exact(ctx: &Context, gens: Vec<Ex>, mp: MultiPoly<Lex>) -> Poly {
Poly {
ctx: ctx.clone(),
gens,
terms: Terms::Exact(ExactTerms {
mp,
ex: OnceLock::new(),
}),
}
}
fn from_normalized(ctx: &Context, gens: Vec<Ex>, ids: Vec<(Vec<u32>, ExprId)>) -> Poly {
let rational: Option<Vec<(Vec<u32>, Ratio<BigInt>)>> = {
let inner = ctx.inner.read();
ids.iter()
.map(|(e, c)| inner.arena.as_num(*c).map(|r| (e.clone(), r.clone())))
.collect()
};
match rational.and_then(|terms| MultiPoly::from_distinct_terms(gens.len(), terms)) {
Some(mp) => Self::from_exact(ctx, gens, mp),
None => {
let terms = ids.into_iter().map(|(e, c)| (e, wrap(ctx, c))).collect();
Poly {
ctx: ctx.clone(),
gens,
terms: Terms::Symbolic(terms),
}
}
}
}
fn from_raw(ctx: &Context, gens: Vec<Ex>, raw: Vec<(Vec<u32>, ExprId)>) -> Poly {
let ids = ctx.with_arena_mut(|arena| normalize_terms(arena, raw));
Self::from_normalized(ctx, gens, ids)
}
fn gen_ids(&self) -> Vec<ExprId> {
self.gens.iter().map(Ex::raw_id).collect()
}
fn exact(&self) -> Option<&MultiPoly<Lex>> {
match &self.terms {
Terms::Exact(t) => Some(&t.mp),
Terms::Symbolic(_) => None,
}
}
fn lex_multipoly(&self) -> Option<Cow<'_, MultiPoly<Lex>>> {
match &self.terms {
Terms::Exact(t) => Some(Cow::Borrowed(&t.mp)),
Terms::Symbolic(m) => rational_multipoly(self.gens.len(), m).map(Cow::Owned),
}
}
fn ex_terms(&self) -> &BTreeMap<Vec<u32>, Ex> {
match &self.terms {
Terms::Symbolic(m) => m,
Terms::Exact(t) => t.ex.get_or_init(|| {
let ids: Vec<(Vec<u32>, ExprId)> = self.ctx.with_arena_mut(|arena| {
t.mp.terms()
.map(|(e, c)| (e.to_vec(), intern_ratio(arena, c)))
.collect()
});
ids.into_iter()
.map(|(e, c)| (e, wrap(&self.ctx, c)))
.collect()
}),
}
}
fn exponents(&self) -> Box<dyn DoubleEndedIterator<Item = &[u32]> + '_> {
match &self.terms {
Terms::Exact(t) => Box::new(t.mp.terms().map(|(e, _)| e)),
Terms::Symbolic(m) => Box::new(m.keys().map(Vec::as_slice)),
}
}
fn raw_terms(&self) -> Vec<(Vec<u32>, ExprId)> {
self.ex_terms()
.iter()
.map(|(e, c)| (e.clone(), c.raw_id()))
.collect()
}
fn eval_config(&self) -> EvalConfig {
self.ctx.inner.read().arena.config.clone()
}
#[must_use]
pub fn new(expr: &Ex, gens: &[&Ex]) -> Option<Poly> {
Self::try_new(expr, gens).ok()
}
pub fn try_new(expr: &Ex, gens: &[&Ex]) -> Result<Poly, SymplexError> {
const OP: &str = "Poly::new";
if gens.is_empty() {
return Err(invalid(OP, "at least one generator is required"));
}
let ctx = expr.context();
let gens = validate_gens(&ctx, gens, OP)?;
let gen_ids: Vec<ExprId> = gens.iter().map(Ex::raw_id).collect();
let exact = {
let inner = ctx.inner.read();
let arena = &inner.arena;
match flat_terms(arena, expr.raw_id(), &gen_ids) {
Some(terms) => MultiPoly::from_distinct_terms(gen_ids.len(), terms),
None => exact_multipoly(arena, expr.raw_id(), &gen_ids),
}
};
if let Some(mp) = exact {
return Ok(Self::from_exact(&ctx, gens, mp));
}
let ids = ctx.with_arena_mut(|arena| {
let raw = polybridge::symbolic_multipoly_terms(arena, expr.raw_id(), &gen_ids)?;
Some(normalize_terms(arena, raw))
});
match ids {
Some(ids) => Ok(Self::from_normalized(&ctx, gens, ids)),
None => {
let reason = ctx.with_arena_mut(|arena| {
let expanded = crate::transforms::expand::expand(arena, expr.raw_id());
polybridge::non_polynomial_reason(arena, expanded, &gen_ids).or_else(|| {
polybridge::non_polynomial_reason(arena, expr.raw_id(), &gen_ids)
})
});
Err(invalid(
OP,
reason.unwrap_or_else(|| {
format!("`{expr}` is not a polynomial in the generators")
}),
))
}
}
}
pub fn from_terms(
ctx: &Context,
gens: &[&Ex],
terms: Vec<(Vec<u32>, Ex)>,
) -> Result<Poly, SymplexError> {
const OP: &str = "Poly::from_terms";
if gens.is_empty() {
return Err(invalid(OP, "at least one generator is required"));
}
let gens = validate_gens(ctx, gens, OP)?;
let gen_ids: Vec<ExprId> = gens.iter().map(Ex::raw_id).collect();
let probe = ctx.zero();
let wrong_length = |e: &[u32]| {
invalid(
OP,
format!(
"exponent vector has length {} but there are {} generators",
e.len(),
gen_ids.len()
),
)
};
let mut rational: Vec<(Vec<u32>, Ratio<BigInt>)> = Vec::with_capacity(terms.len());
for (e, c) in &terms {
if e.len() != gen_ids.len() {
return Err(wrong_length(e));
}
probe.checked_id(c);
match c.as_rational() {
Some(r) => rational.push((e.clone(), r)),
None => {
rational.clear();
break;
}
}
}
if rational.len() == terms.len()
&& let Some(mp) = MultiPoly::from_distinct_terms(gen_ids.len(), rational)
{
return Ok(Self::from_exact(ctx, gens, mp));
}
let mut raw: Vec<(Vec<u32>, ExprId)> = Vec::with_capacity(terms.len());
for (e, c) in terms {
if e.len() != gen_ids.len() {
return Err(wrong_length(&e));
}
let cid = probe.checked_id(&c);
let mentions = ctx.with_arena_mut(|arena| mentions_any(arena, cid, &gen_ids));
if mentions {
return Err(invalid(
OP,
format!("coefficient `{c}` mentions a generator"),
));
}
raw.push((e, cid));
}
Ok(Self::from_raw(ctx, gens, raw))
}
pub fn zero(ctx: &Context, gens: &[&Ex]) -> Result<Poly, SymplexError> {
if gens.is_empty() {
return Err(invalid("Poly::zero", "at least one generator is required"));
}
let gens = validate_gens(ctx, gens, "Poly::zero")?;
let n = gens.len();
Ok(Self::from_exact(ctx, gens, MultiPoly::zero(n)))
}
pub fn one(ctx: &Context, gens: &[&Ex]) -> Result<Poly, SymplexError> {
Self::constant(ctx, gens, &ctx.one())
}
pub fn constant(ctx: &Context, gens: &[&Ex], c: &Ex) -> Result<Poly, SymplexError> {
if gens.is_empty() {
return Err(invalid(
"Poly::constant",
"at least one generator is required",
));
}
let n = gens.len();
Self::from_terms(ctx, gens, vec![(vec![0; n], c.clone())])
}
pub fn from_multipoly(
ctx: &Context,
gens: &[&Ex],
mp: &MultiPoly<GrevLex>,
) -> Result<Poly, SymplexError> {
const OP: &str = "Poly::from_multipoly";
if gens.is_empty() {
return Err(invalid(OP, "at least one generator is required"));
}
if gens.len() != mp.num_vars() {
return Err(invalid(
OP,
format!(
"{} generators but the polynomial has {} variables",
gens.len(),
mp.num_vars()
),
));
}
let gens = validate_gens(ctx, gens, OP)?;
Ok(Self::from_exact(ctx, gens, mp.convert_order()))
}
}
impl<O: crate::poly::multipoly::MonomialOrd> MultiPoly<O> {
pub fn to_ex(&self, ctx: &Context, gens: &[&Ex]) -> Result<Ex, SymplexError> {
Ok(Poly::from_multipoly(ctx, gens, &self.convert_order())?.to_ex())
}
}
impl Poly {
#[must_use]
pub fn gens(&self) -> &[Ex] {
&self.gens
}
#[must_use]
pub fn num_gens(&self) -> usize {
self.gens.len()
}
#[must_use]
pub fn context(&self) -> Context {
self.ctx.clone()
}
#[must_use]
pub fn is_zero(&self) -> bool {
self.num_terms() == 0
}
#[must_use]
pub fn is_ground(&self) -> bool {
self.exponents().all(|e| e.iter().all(|&x| x == 0))
}
#[must_use]
pub fn is_univariate(&self) -> bool {
self.gens.len() == 1
}
#[must_use]
pub fn is_linear(&self) -> bool {
self.exponents().all(|e| e.iter().sum::<u32>() <= 1)
}
#[must_use]
pub fn is_homogeneous(&self) -> bool {
let mut degs = self.exponents().map(|e| e.iter().sum::<u32>());
match degs.next() {
None => true,
Some(d) => degs.all(|x| x == d),
}
}
#[must_use]
pub fn has_rational_coeffs(&self) -> bool {
match &self.terms {
Terms::Exact(_) => true,
Terms::Symbolic(m) => m.values().all(|c| c.as_rational().is_some()),
}
}
#[must_use]
pub fn num_terms(&self) -> usize {
match &self.terms {
Terms::Exact(t) => t.mp.num_terms(),
Terms::Symbolic(m) => m.len(),
}
}
#[must_use]
pub fn terms(&self) -> Vec<(Vec<u32>, Ex)> {
self.ex_terms()
.iter()
.rev()
.map(|(e, c)| (e.clone(), c.clone()))
.collect()
}
pub fn terms_iter(&self) -> impl Iterator<Item = (&[u32], &Ex)> + '_ {
self.ex_terms().iter().rev().map(|(e, c)| (e.as_slice(), c))
}
#[must_use]
pub fn coeffs_rational(&self) -> Option<Vec<Ratio<BigInt>>> {
match &self.terms {
Terms::Exact(t) => Some(t.mp.terms().rev().map(|(_, c)| c.clone()).collect()),
Terms::Symbolic(m) => m.values().rev().map(Ex::as_rational).collect(),
}
}
#[must_use]
pub fn monoms(&self) -> Vec<Vec<u32>> {
self.exponents().rev().map(<[u32]>::to_vec).collect()
}
#[must_use]
pub fn coeffs(&self) -> Vec<Ex> {
self.ex_terms().values().rev().cloned().collect()
}
pub fn coeff_monomial(&self, exps: &[u32]) -> Result<Ex, SymplexError> {
if exps.len() != self.gens.len() {
return Err(invalid(
"Poly::coeff_monomial",
format!(
"exponent vector has length {} but there are {} generators",
exps.len(),
self.gens.len()
),
));
}
Ok(match &self.terms {
Terms::Exact(t) => match t.ex.get() {
Some(m) => m.get(exps).cloned(),
None => t.mp.coeff(exps).map(|r| self.ctx.from_ratio(r.clone())),
},
Terms::Symbolic(m) => m.get(exps).cloned(),
}
.unwrap_or_else(|| self.ctx.zero()))
}
#[must_use]
pub fn total_degree(&self) -> Option<u32> {
self.exponents().map(|e| e.iter().sum::<u32>()).max()
}
#[must_use]
pub fn degree_in(&self, var: &Ex) -> Option<u32> {
let i = self.gens.iter().position(|g| g == var)?;
self.exponents().map(|e| e[i]).max()
}
#[must_use]
pub fn degree_list(&self) -> Vec<u32> {
let mut out = vec![0u32; self.gens.len()];
for e in self.exponents() {
for (o, &x) in out.iter_mut().zip(e) {
*o = (*o).max(x);
}
}
out
}
#[must_use]
pub fn leading_term(&self) -> Option<(Vec<u32>, Ex)> {
match &self.terms {
Terms::Exact(t) => match t.ex.get() {
Some(m) => m.last_key_value().map(|(e, c)| (e.clone(), c.clone())),
None => {
t.mp.leading_term()
.map(|(e, c)| (e.to_vec(), self.ctx.from_ratio(c.clone())))
}
},
Terms::Symbolic(m) => m.last_key_value().map(|(e, c)| (e.clone(), c.clone())),
}
}
#[must_use]
pub fn leading_coeff(&self) -> Ex {
self.leading_term()
.map_or_else(|| self.ctx.zero(), |(_, c)| c)
}
#[must_use]
pub fn leading_monomial(&self) -> Option<Vec<u32>> {
self.exponents().next_back().map(<[u32]>::to_vec)
}
#[must_use]
pub fn all_coeffs(&self) -> Option<Vec<Ex>> {
if self.gens.len() != 1 {
return None;
}
let terms = self.ex_terms();
let deg = terms.keys().map(|e| e[0]).max().unwrap_or(0);
let zero = self.ctx.zero();
let mut out = vec![zero; deg as usize + 1];
for (e, c) in terms {
out[(deg - e[0]) as usize] = c.clone();
}
Some(out)
}
#[must_use]
pub fn equals(&self, other: &Poly) -> bool {
if self.gens != other.gens {
return false;
}
match (&self.terms, &other.terms) {
(Terms::Exact(a), Terms::Exact(b)) => a.mp == b.mp,
(Terms::Symbolic(a), Terms::Symbolic(b)) => a == b,
(Terms::Exact(a), Terms::Symbolic(b)) | (Terms::Symbolic(b), Terms::Exact(a)) => {
rational_multipoly(self.gens.len(), b).is_some_and(|mp| mp == a.mp)
}
}
}
}
impl Poly {
#[must_use]
pub fn to_ex(&self) -> Ex {
let gens = self.gen_ids();
let raw = self.raw_terms();
let id = self.ctx.with_arena_mut(|arena| {
let terms: Vec<ExprId> = raw
.iter()
.map(|(e, c)| monomial_expr(arena, *c, e, &gens))
.collect();
match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
}
});
wrap(&self.ctx, id)
}
pub fn eval(&self, values: &[&Ex]) -> Result<Ex, SymplexError> {
if values.len() != self.gens.len() {
return Err(invalid(
"Poly::eval",
format!("{} values for {} generators", values.len(), self.gens.len()),
));
}
let probe = self.ctx.zero();
let value_ids: Vec<ExprId> = values.iter().map(|v| probe.checked_id(v)).collect();
if let Some(mp) = self.exact() {
let rationals: Option<Vec<Ratio<BigInt>>> = {
let inner = self.ctx.inner.read();
value_ids
.iter()
.map(|&v| inner.arena.as_num(v).cloned())
.collect()
};
if let Some(vals) = rationals {
let config = self.eval_config();
let within = degree_list_of(mp)
.iter()
.zip(&vals)
.all(|(&d, v)| pow_within_limits(&config, v, d));
if within {
return Ok(self.ctx.from_ratio(eval_exact(mp, &vals)));
}
}
}
let raw = self.raw_terms();
let id = self.ctx.with_arena_mut(|arena| {
let terms: Vec<ExprId> = raw
.iter()
.map(|(e, c)| monomial_expr(arena, *c, e, &value_ids))
.collect();
match terms.len() {
0 => arena.zero,
1 => terms[0],
_ => arena.add(&terms),
}
});
Ok(wrap(&self.ctx, id).eval())
}
pub fn eval_gen(&self, var: &Ex, value: &Ex) -> Result<Poly, SymplexError> {
const OP: &str = "Poly::eval_gen";
let probe = self.ctx.zero();
let gen_id = probe.checked_id(var);
let value_id = probe.checked_id(value);
let Some(i) = self.gens.iter().position(|g| g == var) else {
return Err(invalid(OP, format!("`{var}` is not a generator")));
};
let remaining: Vec<Ex> = self
.gens
.iter()
.enumerate()
.filter(|(k, _)| *k != i)
.map(|(_, g)| g.clone())
.collect();
if let Some(mp) = self.exact()
&& let Some(v) = value.as_rational()
&& pow_within_limits(&self.eval_config(), &v, mp.degree_in(i))
{
return Ok(Self::from_exact(&self.ctx, remaining, mp.substitute(i, &v)));
}
let remaining_ids: Vec<ExprId> = remaining.iter().map(Ex::raw_id).collect();
let raw = self.raw_terms();
let result: Result<Vec<(Vec<u32>, ExprId)>, SymplexError> =
self.ctx.with_arena_mut(|arena| {
if crate::base::walk::contains(arena, value_id, gen_id) {
return Err(invalid(
OP,
format!(
"value `{}` mentions the generator itself",
arena.display(value_id)
),
));
}
let mut subst: Vec<ExprId> = Vec::with_capacity(raw.len());
for (e, c) in &raw {
let mut rest: Vec<u32> = e.clone();
let k = rest.remove(i);
let mut factors = vec![*c];
match k {
0 => {}
1 => factors.push(value_id),
_ => {
let k_id = arena.int(i64::from(k));
factors.push(arena.pow(value_id, k_id));
}
}
let coeff = if factors.len() == 1 {
factors[0]
} else {
arena.mul(&factors)
};
subst.push(monomial_expr(arena, coeff, &rest, &remaining_ids));
}
let total = match subst.len() {
0 => arena.zero,
1 => subst[0],
_ => arena.add(&subst),
};
if remaining_ids.is_empty() {
let c = norm_coeff(arena, total);
return Ok(if arena.is_zero_structural(c) {
vec![]
} else {
vec![(vec![], c)]
});
}
match polybridge::symbolic_multipoly_terms(arena, total, &remaining_ids) {
Some(terms) => Ok(normalize_terms(arena, terms)),
None => Err(invalid(
OP,
"value is not polynomial in the remaining generators",
)),
}
});
Ok(Self::from_normalized(&self.ctx, remaining, result?))
}
#[must_use]
pub fn to_multipoly(&self) -> Option<MultiPoly<GrevLex>> {
self.lex_multipoly().map(|mp| mp.convert_order())
}
pub fn nroots(&self, digits: u32) -> Result<Vec<Complex64>, SymplexError> {
if self.gens.len() != 1 {
return Err(invalid("Poly::nroots", "polynomial must be univariate"));
}
if !self.has_rational_coeffs() {
return Err(invalid(
"Poly::nroots",
"polynomial must have rational coefficients",
));
}
self.to_ex().nroots(&self.gens[0], digits)
}
fn univariate_rational_gen(&self) -> Option<&Ex> {
if self.gens.len() == 1 && self.has_rational_coeffs() {
self.gens.first()
} else {
None
}
}
#[must_use]
pub fn count_real_roots(&self) -> Option<usize> {
let x = self.univariate_rational_gen()?;
self.to_ex().count_real_roots(x)
}
#[must_use]
pub fn count_real_roots_in(&self, lo: &Ex, hi: &Ex) -> Option<usize> {
let x = self.univariate_rational_gen()?;
self.to_ex().count_real_roots_in(x, lo, hi)
}
#[must_use]
pub fn real_roots_isolate(&self) -> Vec<Interval<Ex>> {
match self.univariate_rational_gen() {
Some(x) => self.to_ex().real_roots_isolate(x),
None => Vec::new(),
}
}
#[must_use]
pub fn is_nonnegative_on(&self, lo: &Ex, hi: &Ex) -> Option<bool> {
let x = self.univariate_rational_gen()?;
self.to_ex().poly_is_nonnegative_on(x, lo, hi)
}
#[must_use]
pub fn is_positive_on(&self, lo: &Ex, hi: &Ex) -> Option<bool> {
let x = self.univariate_rational_gen()?;
self.to_ex().poly_is_positive_on(x, lo, hi)
}
pub fn shift(&self, var: &Ex, a: &Ex) -> Result<Poly, SymplexError> {
let probe = self.ctx.zero();
let gen_id = probe.checked_id(var);
if !self.gens.iter().any(|g| g.raw_id() == gen_id) {
return Err(invalid("Poly::shift", "not a generator of this polynomial"));
}
for g in &self.gens {
if a.contains(g) {
return Err(invalid(
"Poly::shift",
"the shift must not mention a generator",
));
}
}
let replacement = var + a;
let shifted = self.to_ex().subs(var, &replacement);
let gens: Vec<&Ex> = self.gens.iter().collect();
Poly::new(&shifted, &gens).ok_or_else(|| SymplexError::ComputationFailed {
operation: "Poly::shift",
reason: "shifted expression is not polynomial in the generators".into(),
})
}
}
impl Poly {
fn check_same_gens(&self, other: &Poly, operation: &'static str) -> Result<(), SymplexError> {
if self.gens != other.gens {
return Err(invalid(operation, "polynomials have different generators"));
}
Ok(())
}
pub fn add(&self, other: &Poly) -> Result<Poly, SymplexError> {
self.check_same_gens(other, "Poly::add")?;
if let (Some(a), Some(b)) = (self.exact(), other.exact()) {
return Ok(Self::from_exact(&self.ctx, self.gens.clone(), a.add(b)));
}
let mut raw = self.raw_terms();
raw.extend(other.raw_terms());
Ok(Self::from_raw(&self.ctx, self.gens.clone(), raw))
}
pub fn sub(&self, other: &Poly) -> Result<Poly, SymplexError> {
self.check_same_gens(other, "Poly::sub")?;
if let (Some(a), Some(b)) = (self.exact(), other.exact()) {
return Ok(Self::from_exact(&self.ctx, self.gens.clone(), a.sub(b)));
}
let mine = self.raw_terms();
let theirs = other.raw_terms();
let ids = self.ctx.with_arena_mut(|arena| {
let mut raw = mine;
for (e, c) in theirs {
raw.push((e, arena.neg(c)));
}
normalize_terms(arena, raw)
});
Ok(Self::from_normalized(&self.ctx, self.gens.clone(), ids))
}
pub fn mul(&self, other: &Poly) -> Result<Poly, SymplexError> {
self.check_same_gens(other, "Poly::mul")?;
if let (Some(a), Some(b)) = (self.exact(), other.exact()) {
let prod =
mul_checked(a, b).ok_or_else(|| invalid("Poly::mul", "exponent overflow"))?;
return Ok(Self::from_exact(&self.ctx, self.gens.clone(), prod));
}
let mine = self.raw_terms();
let theirs = other.raw_terms();
let ids: Result<Vec<(Vec<u32>, ExprId)>, SymplexError> = self.ctx.with_arena_mut(|arena| {
let mut raw: Vec<(Vec<u32>, ExprId)> = Vec::with_capacity(mine.len() * theirs.len());
for (e1, c1) in &mine {
for (e2, c2) in &theirs {
let mut e = Vec::with_capacity(e1.len());
for (a, b) in e1.iter().zip(e2) {
e.push(
a.checked_add(*b)
.ok_or_else(|| invalid("Poly::mul", "exponent overflow"))?,
);
}
raw.push((e, arena.mul(&[*c1, *c2])));
}
}
Ok(normalize_terms(arena, raw))
});
Ok(Self::from_normalized(&self.ctx, self.gens.clone(), ids?))
}
#[must_use]
pub fn neg(&self) -> Poly {
if let Some(mp) = self.exact() {
return Self::from_exact(&self.ctx, self.gens.clone(), mp.neg());
}
let raw = self.raw_terms();
let ids = self.ctx.with_arena_mut(|arena| {
let negated: Vec<(Vec<u32>, ExprId)> =
raw.into_iter().map(|(e, c)| (e, arena.neg(c))).collect();
normalize_terms(arena, negated)
});
Self::from_normalized(&self.ctx, self.gens.clone(), ids)
}
pub fn scale(&self, c: &Ex) -> Result<Poly, SymplexError> {
let cid = self.ctx.zero().checked_id(c);
if let Some(mp) = self.exact()
&& let Some(r) = c.as_rational()
{
return Ok(Self::from_exact(&self.ctx, self.gens.clone(), mp.scale(&r)));
}
let gens = self.gen_ids();
let raw = self.raw_terms();
let ids: Result<Vec<(Vec<u32>, ExprId)>, SymplexError> = self.ctx.with_arena_mut(|arena| {
if mentions_any(arena, cid, &gens) {
return Err(invalid("Poly::scale", "scale factor mentions a generator"));
}
let scaled: Vec<(Vec<u32>, ExprId)> = raw
.into_iter()
.map(|(e, coeff)| (e, arena.mul(&[cid, coeff])))
.collect();
Ok(normalize_terms(arena, scaled))
});
Ok(Self::from_normalized(&self.ctx, self.gens.clone(), ids?))
}
pub fn pow(&self, n: u32) -> Result<Poly, SymplexError> {
if let Some(mp) = self.exact() {
let power =
pow_checked(mp, n).ok_or_else(|| invalid("Poly::mul", "exponent overflow"))?;
return Ok(Self::from_exact(&self.ctx, self.gens.clone(), power));
}
let gens: Vec<&Ex> = self.gens.iter().collect();
let mut result = Self::one(&self.ctx, &gens)?;
let mut base = self.clone();
let mut k = n;
while k > 0 {
if k & 1 == 1 {
result = result.mul(&base)?;
}
k >>= 1;
if k > 0 {
base = base.mul(&base)?;
}
}
Ok(result)
}
pub fn derivative(&self, var: &Ex) -> Result<Poly, SymplexError> {
let Some(i) = self.gens.iter().position(|g| g == var) else {
return Err(invalid(
"Poly::derivative",
format!("`{var}` is not a generator"),
));
};
if let Some(mp) = self.exact() {
return Ok(Self::from_exact(
&self.ctx,
self.gens.clone(),
mp.partial_derivative(i),
));
}
let raw = self.raw_terms();
let ids = self.ctx.with_arena_mut(|arena| {
let mut out: Vec<(Vec<u32>, ExprId)> = Vec::with_capacity(raw.len());
for (mut e, c) in raw {
let k = e[i];
if k == 0 {
continue;
}
e[i] -= 1;
let k_id = arena.int(i64::from(k));
out.push((e, arena.mul(&[k_id, c])));
}
normalize_terms(arena, out)
});
Ok(Self::from_normalized(&self.ctx, self.gens.clone(), ids))
}
#[must_use]
pub fn content_and_primitive(&self) -> Option<(Ex, Poly)> {
let mp = self.lex_multipoly()?;
if mp.is_zero() {
return Some((self.ctx.zero(), self.clone()));
}
let mut num = BigInt::zero();
let mut den = BigInt::one();
for (_, c) in mp.terms() {
num = num.gcd(c.numer());
den = den.lcm(c.denom());
}
let mut content = Ratio::new(num, den);
if mp.leading_coeff().is_some_and(Signed::is_negative) {
content = -content;
}
let inv = Ratio::one() / &content;
let prim = Self::from_exact(&self.ctx, self.gens.clone(), mp.scale(&inv));
Some((self.ctx.from_ratio(content), prim))
}
#[must_use]
pub fn monic(&self) -> Option<Poly> {
let mp = self.lex_multipoly()?;
if mp.is_zero() {
return None;
}
Some(Self::from_exact(&self.ctx, self.gens.clone(), mp.monic()))
}
}
impl Poly {
pub fn monomial_basis(polys: &[&Poly]) -> Result<Vec<Vec<u32>>, SymplexError> {
let Some(first) = polys.first() else {
return Err(invalid(
"Poly::monomial_basis",
"at least one polynomial is required",
));
};
let mut set: BTreeSet<Vec<u32>> = BTreeSet::new();
for p in polys {
first.check_same_gens(p, "Poly::monomial_basis")?;
set.extend(p.exponents().map(<[u32]>::to_vec));
}
Ok(set.into_iter().rev().collect())
}
pub fn coefficient_matrix(polys: &[&Poly], monos: &[Vec<u32>]) -> Result<Matrix, SymplexError> {
const OP: &str = "Poly::coefficient_matrix";
let Some(first) = polys.first() else {
return Err(invalid(OP, "at least one polynomial is required"));
};
if monos.is_empty() {
return Err(invalid(OP, "at least one monomial is required"));
}
for p in polys {
first.check_same_gens(p, OP)?;
}
let mut rows: Vec<Vec<Ex>> = Vec::with_capacity(monos.len());
for m in monos {
let mut row = Vec::with_capacity(polys.len());
for p in polys {
row.push(p.coeff_monomial(m)?);
}
rows.push(row);
}
Matrix::new(rows)
}
}
fn rational_sign_magnitude(r: &Ratio<BigInt>) -> (bool, String) {
let abs = r.abs();
let s = if abs.is_integer() {
abs.numer().to_string()
} else {
format!("{}/{}", abs.numer(), abs.denom())
};
(r.is_negative(), s)
}
fn coeff_sign_magnitude(coeff: &CoeffView<'_>, has_monomial: bool) -> (bool, String) {
match coeff {
CoeffView::Rational(r) => rational_sign_magnitude(r),
CoeffView::Symbolic(coeff) => match coeff.as_rational() {
Some(r) => rational_sign_magnitude(&r),
None => {
let s = coeff.to_string();
if coeff.expr_type() == ExprType::Add && has_monomial {
(false, format!("({s})"))
} else if let Some(rest) = s.strip_prefix('-') {
(true, rest.to_string())
} else {
(false, s)
}
}
},
}
}
impl fmt::Display for Poly {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "Poly(")?;
if self.is_zero() {
write!(f, "0")?;
}
let terms: Box<dyn Iterator<Item = (&[u32], CoeffView<'_>)> + '_> = match &self.terms {
Terms::Exact(t) => {
Box::new(t.mp.terms().rev().map(|(e, c)| (e, CoeffView::Rational(c))))
}
Terms::Symbolic(m) => Box::new(
m.iter()
.rev()
.map(|(e, c)| (e.as_slice(), CoeffView::Symbolic(c))),
),
};
for (i, (exps, coeff)) in terms.enumerate() {
let monomial: Vec<String> = exps
.iter()
.zip(&self.gens)
.filter(|(e, _)| **e > 0)
.map(|(e, g)| {
if *e == 1 {
g.to_string()
} else {
format!("{g}^{e}")
}
})
.collect();
let monomial = monomial.join("*");
let (negative, magnitude) = coeff_sign_magnitude(&coeff, !monomial.is_empty());
match (i, negative) {
(0, false) => {}
(0, true) => write!(f, "-")?,
(_, false) => write!(f, " + ")?,
(_, true) => write!(f, " - ")?,
}
if monomial.is_empty() {
write!(f, "{magnitude}")?;
} else if magnitude == "1" {
write!(f, "{monomial}")?;
} else {
write!(f, "{magnitude}*{monomial}")?;
}
}
for g in &self.gens {
write!(f, ", {g}")?;
}
write!(f, ")")
}
}
impl Ex {
#[must_use]
pub fn as_poly(&self, gens: &[&Ex]) -> Option<Poly> {
Poly::new(self, gens)
}
}