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::base::numeric::Q;
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::invalid_argument(operation, reason)
}
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()
}
}
}
enum RealRoot {
Rational(Q),
RootOf { factor: usize, index: usize },
}
struct RootCell {
factor: usize,
interval: crate::base::interval::Interval<Q>,
}
struct RealRootCells {
factors: Vec<Poly>,
cells: Vec<RootCell>,
roots_cache: Vec<Option<Vec<crate::poly::roots::Complex>>>,
}
impl RealRootCells {
fn locate(f: &Poly) -> Option<Self> {
let (_content, parts, complete) =
crate::poly::factor_zassenhaus::factor_zassenhaus_checked(f);
if !complete {
tracing::debug!(
degree = f.degree().unwrap_or(0),
"real_roots: factorisation over ℤ not certified complete; naming roots of square-free factors"
);
}
let mut factors: Vec<Poly> = Vec::with_capacity(parts.len());
let mut chains: Vec<SturmChain> = Vec::with_capacity(parts.len());
let mut square_free = Poly::from_int(1);
for (g, _mult) in &parts {
if g.degree()? == 0 {
continue;
}
square_free = &square_free * g;
chains.push(SturmChain::new(g));
factors.push(g.clone());
}
if square_free.degree().unwrap_or(0) == 0 {
return None;
}
let intervals = SturmChain::new(&square_free).isolate_all_real_roots();
let mut cells: Vec<RootCell> = Vec::with_capacity(intervals.len());
for iv in intervals {
let owner = factors.iter().zip(&chains).position(|(g, chain)| {
if iv.is_point() {
g.eval(&iv.lower).is_zero()
} else {
chain.count_roots_in(&iv.lower, &iv.upper) == 1
}
})?;
cells.push(RootCell {
factor: owner,
interval: iv,
});
}
let roots_cache = vec![None; factors.len()];
Some(Self {
factors,
cells,
roots_cache,
})
}
fn name(&mut self, i: usize) -> Option<RealRoot> {
let cell = self.cells.get(i)?;
let g = &self.factors[cell.factor];
if g.degree() == Some(1) {
return Some(RealRoot::Rational(-(g.coeff(0) / g.coeff(1))));
}
let roots = self.roots_cache[cell.factor].get_or_insert_with(|| {
crate::poly::roots::rootof_roots(g, crate::poly::roots::ROOTOF_DEFAULT_PREC)
});
let index =
crate::poly::roots::real_root_index(roots, &cell.interval.lower, &cell.interval.upper)?;
Some(RealRoot::RootOf {
factor: cell.factor,
index,
})
}
}
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));
}
if let Some(r) = parametric_resultant(f, g) {
return Some(r);
}
sylvester_determinant(ctx, f, g)
}
struct ParametricPair {
f: Vec<MultiPoly<GrevLex>>,
g: Vec<MultiPoly<GrevLex>>,
gens: Vec<ExprId>,
}
fn coefficient_multipolys(f: &[Ex], g: &[Ex]) -> Option<ParametricPair> {
let probe = f.first()?;
let inner = probe.inner.read();
let arena = &inner.arena;
let ids: Vec<ExprId> = f.iter().chain(g).map(|c| probe.checked_id(c)).collect();
let gens = shared_generators(arena, &ids);
let convert = |cs: &[ExprId]| -> Option<Vec<MultiPoly<GrevLex>>> {
cs.iter()
.rev()
.map(|&c| expr_to_multipoly(arena, c, &gens))
.collect()
};
let fp = convert(&ids[..f.len()])?;
let gp = convert(&ids[f.len()..])?;
Some(ParametricPair { f: fp, g: gp, gens })
}
fn parametric_resultant(f: &[Ex], g: &[Ex]) -> Option<Ex> {
let pair = coefficient_multipolys(f, g)?;
let r = multipoly_resultant(&pair.f, &pair.g)?;
let probe = f.first()?;
let id = {
let mut inner = probe.inner.write();
multipoly_to_expr(&mut inner.arena, &r, &pair.gens)
};
Some(probe.wrap(id))
}
fn parametric_discriminant(coeffs: &[Ex], deriv: &[Ex], n: usize) -> Option<Ex> {
let pair = coefficient_multipolys(coeffs, deriv)?;
let res = multipoly_resultant(&pair.f, &pair.g)?;
let quotient = res.div_exact(pair.f.last()?)?;
let disc = if (n * (n - 1) / 2) % 2 == 1 {
quotient.neg()
} else {
quotient
};
let probe = coeffs.first()?;
let id = {
let mut inner = probe.inner.write();
multipoly_to_expr(&mut inner.arena, &disc, &pair.gens)
};
Some(probe.wrap(id))
}
fn sylvester_discriminant(
ctx: &crate::api::context::Context,
coeffs: &[Ex],
deriv: &[Ex],
n: usize,
) -> Option<Ex> {
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 matrix = crate::domains::matrix::Matrix::new(minor);
debug_assert!(
matrix.is_ok(),
"discriminant minor is square by construction"
);
let det = matrix.ok()?.det();
debug_assert!(det.is_ok(), "determinant of a square matrix over Ex");
let det = det.ok()?.expand();
Some(if (n * (n - 1) / 2) % 2 == 1 {
-det
} else {
det
})
}
fn mp_normalize(p: &mut Vec<MultiPoly<GrevLex>>) {
while p.last().is_some_and(MultiPoly::is_zero) {
p.pop();
}
}
fn mp_neg(p: &[MultiPoly<GrevLex>]) -> Vec<MultiPoly<GrevLex>> {
p.iter().map(MultiPoly::neg).collect()
}
fn mp_prem(f: &[MultiPoly<GrevLex>], g: &[MultiPoly<GrevLex>]) -> Option<Vec<MultiPoly<GrevLex>>> {
let lc_g = g.last()?;
let dg = g.len() - 1;
let mut r = f.to_vec();
mp_normalize(&mut r);
if r.len() <= dg {
return Some(r);
}
let mut passes_left = r.len() - dg;
while r.len() > dg {
let top = r.len() - 1;
let lc_r = r.pop()?;
let shift = top - dg;
for c in r.iter_mut() {
*c = c.try_mul(lc_g)?;
}
for (rk, gk) in r[shift..].iter_mut().zip(&g[..dg]) {
*rk = rk.try_sub(&lc_r.try_mul(gk)?)?;
}
mp_normalize(&mut r);
passes_left = passes_left.saturating_sub(1);
}
if passes_left > 0 {
let s = lc_g.try_pow(u32::try_from(passes_left).ok()?)?;
for c in r.iter_mut() {
*c = c.try_mul(&s)?;
}
}
Some(r)
}
fn multipoly_resultant(
f: &[MultiPoly<GrevLex>],
g: &[MultiPoly<GrevLex>],
) -> Option<MultiPoly<GrevLex>> {
let (mut f, mut g) = (f.to_vec(), g.to_vec());
mp_normalize(&mut f);
mp_normalize(&mut g);
if f.len() < 2 || g.len() < 2 {
return None;
}
let nv = f[0].num_vars();
let mut swap_sign = false;
if f.len() < g.len() {
swap_sign = (f.len() - 1) * (g.len() - 1) % 2 == 1;
std::mem::swap(&mut f, &mut g);
}
let pow = |p: &MultiPoly<GrevLex>, k: usize| p.try_pow(u32::try_from(k).ok()?);
let mut m = g.len() - 1;
let d = f.len() - g.len();
let mut h = mp_prem(&f, &g)?;
if d % 2 == 0 {
h = mp_neg(&h);
}
let mut lc = g.last()?.clone();
let mut c = pow(&lc, d)?.neg();
while !h.is_empty() {
let k = h.len() - 1;
let d = m - k;
m = k;
f = std::mem::replace(&mut g, h);
let b = lc.try_mul(&pow(&c, d)?)?.neg();
h = mp_prem(&f, &g)?
.iter()
.map(|p| p.div_exact(&b))
.collect::<Option<Vec<_>>>()?;
mp_normalize(&mut h);
lc = g.last()?.clone();
c = if d > 1 {
pow(&lc.neg(), d)?.div_exact(&pow(&c, d - 1)?)?
} else {
lc.neg()
};
}
let r = if g.len() == 1 {
c.neg()
} else {
MultiPoly::zero(nv)
};
Some(if swap_sign { r.neg() } else { r })
}
fn sylvester_determinant(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)?;
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 matrix = crate::domains::matrix::Matrix::new(rows);
debug_assert!(matrix.is_ok(), "Sylvester matrix is square by construction");
let det = matrix.ok()?.det();
debug_assert!(det.is_ok(), "determinant of a square matrix over Ex");
Some(det.ok()?.expand())
}
impl Expr<Numeric> {
#[must_use]
pub fn minimal_polynomial(&self, var: &Ex) -> Option<Ex> {
use crate::poly::algebraic::{AlgExpr, minimal_polynomial_of};
let var_id = self.checked_id(var);
let alg = {
let inner = self.inner.read();
AlgExpr::from_arena(&inner.arena, self.raw_id())
};
let mp = alg.and_then(|a| minimal_polynomial_of(&a)).or_else(|| {
let expanded = self.expand();
if expanded == *self {
return None;
}
let alg = {
let inner = self.inner.read();
AlgExpr::from_arena(&inner.arena, expanded.raw_id())
}?;
minimal_polynomial_of(&alg)
})?;
let id = {
let mut inner = self.inner.write();
poly_to_expr(&mut inner.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 (gens, a, b) = {
let inner = self.inner.read();
let arena = &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)?;
(gens, a, b)
};
let result = op(&a, &b);
let id = {
let mut inner = self.inner.write();
multipoly_to_expr(&mut inner.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 mps = {
let inner = probe.inner.read();
to_multipolys(&inner.arena, &poly_ids, &var_ids, OP)?
};
let basis = groebner_in_order(&mps, order);
let ids: Vec<ExprId> = {
let mut inner = probe.inner.write();
let arena = &mut inner.arena;
basis
.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 (f, divisors) = {
let inner = self.inner.read();
let arena = &inner.arena;
let Some(f) = to_multipolys(arena, &[self.raw_id()], &var_ids, OP)?
.into_iter()
.next()
else {
return Err(SymplexError::ComputationFailed {
operation: OP,
reason: "to_multipolys returned no polynomial for one input".into(),
});
};
(f, to_multipolys(arena, &basis_ids, &var_ids, OP)?)
};
let r = reduce_in_order(&f, &divisors, order);
let id = {
let mut inner = self.inner.write();
multipoly_to_expr(&mut inner.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 f = {
let inner = self.inner.read();
expr_to_poly(&inner.arena, self.raw_id(), var_id)?
};
if f.degree().unwrap_or(0) == 0 {
return None;
}
let mut cells = RealRootCells::locate(&f)?;
let roots: Vec<RealRoot> = (0..cells.cells.len())
.map(|i| cells.name(i))
.collect::<Option<_>>()?;
let ids = self.intern_real_roots(&cells.factors, roots, var_id);
Some(ids.into_iter().map(|id| self.wrap(id)).collect())
}
fn intern_real_roots(
&self,
factors: &[Poly],
roots: Vec<RealRoot>,
var_id: ExprId,
) -> Vec<ExprId> {
let mut inner = self.inner.write();
let arena = &mut inner.arena;
let mut factor_ids: Vec<Option<ExprId>> = vec![None; factors.len()];
roots
.into_iter()
.map(|root| match root {
RealRoot::Rational(r) => arena.num_ratio(r),
RealRoot::RootOf { factor, index } => {
let g_expr = *factor_ids[factor]
.get_or_insert_with(|| poly_to_expr(arena, &factors[factor], var_id));
let idx = arena.int(index as i64);
arena.intern(ExprNode::RootOf(g_expr, var_id, idx))
}
})
.collect()
}
#[must_use]
pub fn root_of(&self, var: &Ex, index: usize) -> Option<Ex> {
let var_id = self.checked_id(var);
let f = {
let inner = self.inner.read();
expr_to_poly(&inner.arena, self.raw_id(), var_id)?
};
if f.degree().unwrap_or(0) == 0 {
return None;
}
let mut cells = RealRootCells::locate(&f)?;
let root = cells.name(index)?;
let ids = self.intern_real_roots(&cells.factors, vec![root], var_id);
ids.into_iter().next().map(|id| self.wrap(id))
}
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 f = {
let inner = self.inner.read();
expr_to_poly(&inner.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, factor_ids) = {
let mut inner = self.inner.write();
let arena = &mut inner.arena;
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();
parametric_discriminant(&coeffs, &deriv, n)
.or_else(|| sylvester_discriminant(&ctx, &coeffs, &deriv, n))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::api::context::Context;
#[test]
fn parametric_resultant_is_the_sylvester_determinant() {
let ctx = Context::new();
let (a, b, c) = (ctx.symbol("a"), ctx.symbol("b"), ctx.symbol("c"));
let x = ctx.symbol("x");
let one = ctx.int(1);
let pairs: Vec<(Ex, Ex)> = vec![
(&x.powi(3) + &(&a * &x) + &b, &(&x.powi(2) * &c) - &one),
(&(&x * &a) - &b, &(&x.powi(4) * 3) + &(&a * &x.powi(2)) + &c),
(&(&x - &a) * &(&x + &b), &(&x - &a) * &(&x.powi(2) + &c)),
(&x.powi(3) - 2, &(&x.powi(2) * 5) + &(&x * 7) - 1),
(
&(&(&a * &b) * &x.powi(2)) + &(&c * &x) - &a.powi(2),
&(&x.powi(3) * &b) - &(&c * &x) + &(&a * &c),
),
];
for (f, g) in pairs {
for (p, q) in [(&f, &g), (&g, &f)] {
let pc = crate::api::poly_ex::Poly::new(p, &[&x])
.unwrap()
.all_coeffs()
.unwrap();
let qc = crate::api::poly_ex::Poly::new(q, &[&x])
.unwrap()
.all_coeffs()
.unwrap();
let fast = parametric_resultant(&pc, &qc).expect("polynomial coefficients");
let slow = sylvester_determinant(&ctx, &pc, &qc).unwrap();
assert_eq!(fast, slow, "res({p}, {q})");
}
}
}
#[test]
fn parametric_discriminant_is_the_sylvester_discriminant() {
let ctx = Context::new();
let (a, b, c) = (ctx.symbol("a"), ctx.symbol("b"), ctx.symbol("c"));
let x = ctx.symbol("x");
let polys: Vec<Ex> = vec![
&(&a * &x.powi(2)) + &(&b * &x) + &c,
&x.powi(3) + &(&a * &x) + &b,
&(&x.powi(4) * &a) - &(&x.powi(2) * &b) + &(&x * &c) - 7,
&(&x.powi(5) * 3) + &(&x.powi(3) * &a.powi(2)) - &(&b * &c),
&(&x - &a).powi(2) * &(&x + &b),
&(&x.powi(6) * &ctx.rational(2, 3)) - &(&x * &a) + &(&b * &c),
&(&x.powi(3) * &a) + &(&x.powi(2) * &b) + &(&x * &c) + &(&a * &b),
];
for f in polys {
let coeffs = crate::api::poly_ex::Poly::new(&f, &[&x])
.unwrap()
.all_coeffs()
.unwrap();
let n = coeffs.len() - 1;
let deriv: Vec<Ex> = coeffs[..n]
.iter()
.enumerate()
.map(|(j, c)| c * ((n - j) as i64))
.collect();
let fast = parametric_discriminant(&coeffs, &deriv, n).expect("polynomial");
let slow = sylvester_discriminant(&ctx, &coeffs, &deriv, n).unwrap();
assert_eq!(fast, slow, "disc({f})");
}
}
}