use std::collections::{BTreeMap, BTreeSet, BinaryHeap};
use rug::Rational;
use crate::errors::AlkahestError;
use crate::poly::groebner::ideal::GbPoly;
use crate::poly::groebner::monomial_order::MonomialOrder;
use crate::poly::groebner::pairs::{lcm_exp, update_pairs, CriticalPair};
use crate::poly::groebner::paramfield::{ParamPoly, QParam};
use crate::poly::groebner::GroebnerBasis;
#[derive(Debug, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum ParamGroebnerError {
NoGenerators,
ShapeMismatch {
expected: (usize, usize),
got: (usize, usize),
},
WrongArity {
expected: usize,
got: usize,
},
Degenerate {
vanishing: Vec<ParamPoly>,
},
}
impl std::fmt::Display for ParamGroebnerError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ParamGroebnerError::NoGenerators => {
write!(f, "a parametric Gröbner basis needs at least one generator")
}
ParamGroebnerError::ShapeMismatch { expected, got } => write!(
f,
"generator shape mismatch: expected {} variables and {} parameters, got {} and {}",
expected.0, expected.1, got.0, got.1
),
ParamGroebnerError::WrongArity { expected, got } => write!(
f,
"specialisation needs one value per parameter: expected {expected}, got {got}"
),
ParamGroebnerError::Degenerate { vanishing } => write!(
f,
"parameter point is on the degeneracy locus: {} of the basis's conditions vanish \
there, so this basis does not describe that point",
vanishing.len()
),
}
}
}
impl std::error::Error for ParamGroebnerError {}
impl AlkahestError for ParamGroebnerError {
fn code(&self) -> &'static str {
match self {
ParamGroebnerError::NoGenerators => "E-PARAMGB-001",
ParamGroebnerError::ShapeMismatch { .. } => "E-PARAMGB-002",
ParamGroebnerError::WrongArity { .. } => "E-PARAMGB-003",
ParamGroebnerError::Degenerate { .. } => "E-PARAMGB-004",
}
}
fn remediation(&self) -> Option<&'static str> {
match self {
ParamGroebnerError::NoGenerators => Some("pass at least one polynomial"),
ParamGroebnerError::ShapeMismatch { .. } => {
Some("build every generator against the same variable and parameter lists")
}
ParamGroebnerError::WrongArity { .. } => {
Some("supply exactly one value per parameter, in the parameter list's order")
}
ParamGroebnerError::Degenerate { .. } => Some(
"compute the basis directly over ℚ at that parameter point, or move the vanishing \
factors into the generators and recompute",
),
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ParamGbPoly {
pub terms: BTreeMap<Vec<u32>, QParam>,
pub n_vars: usize,
pub n_params: usize,
}
impl ParamGbPoly {
pub fn zero(n_vars: usize, n_params: usize) -> Self {
ParamGbPoly {
terms: BTreeMap::new(),
n_vars,
n_params,
}
}
pub fn from_gbpoly(p: &GbPoly, n_vars: usize, n_params: usize) -> Option<Self> {
if p.n_vars != n_vars + n_params {
return None;
}
let mut out = ParamGbPoly::zero(n_vars, n_params);
for (exp, coeff) in &p.terms {
if *coeff == 0 {
continue;
}
let (var_exp, par_exp) = exp.split_at(n_vars);
let mut mono = ParamPoly::zero(n_params);
mono.terms.insert(par_exp.to_vec(), rug::Integer::from(1));
let c = QParam::from_rational(coeff, n_params).mul(&QParam::from_poly(mono));
let slot = out
.terms
.entry(var_exp.to_vec())
.or_insert_with(|| QParam::zero(n_params));
*slot = slot.add(&c);
}
out.terms.retain(|_, c| !c.is_zero());
Some(out)
}
pub fn is_zero(&self) -> bool {
self.terms.is_empty()
}
pub fn n_terms(&self) -> usize {
self.terms.len()
}
pub fn leading_term(&self, order: MonomialOrder) -> Option<(&Vec<u32>, &QParam)> {
self.terms
.iter()
.max_by(|(ea, _), (eb, _)| order.cmp(ea, eb))
}
pub fn leading_exp(&self, order: MonomialOrder) -> Option<Vec<u32>> {
self.leading_term(order).map(|(e, _)| e.clone())
}
pub fn leading_coeff(&self, order: MonomialOrder) -> Option<QParam> {
self.leading_term(order).map(|(_, c)| c.clone())
}
pub fn sugar(&self) -> u32 {
self.terms
.keys()
.map(|e| crate::poly::exponent::total_degree_or_panic(e))
.max()
.unwrap_or(0)
}
pub fn add(&self, other: &Self) -> Self {
let mut terms = self.terms.clone();
for (e, c) in &other.terms {
let slot = terms
.entry(e.clone())
.or_insert_with(|| QParam::zero(self.n_params));
*slot = slot.add(c);
if slot.is_zero() {
terms.remove(e);
}
}
ParamGbPoly {
terms,
n_vars: self.n_vars,
n_params: self.n_params,
}
}
pub fn neg(&self) -> Self {
ParamGbPoly {
terms: self
.terms
.iter()
.map(|(e, c)| (e.clone(), c.neg()))
.collect(),
n_vars: self.n_vars,
n_params: self.n_params,
}
}
pub fn sub(&self, other: &Self) -> Self {
self.add(&other.neg())
}
pub fn scale(&self, c: &QParam) -> Self {
if c.is_zero() {
return ParamGbPoly::zero(self.n_vars, self.n_params);
}
ParamGbPoly {
terms: self
.terms
.iter()
.map(|(e, v)| (e.clone(), v.mul(c)))
.collect(),
n_vars: self.n_vars,
n_params: self.n_params,
}
}
pub fn mul(&self, other: &Self) -> Self {
let mut out = ParamGbPoly::zero(self.n_vars, self.n_params);
for (ea, ca) in &self.terms {
for (eb, cb) in &other.terms {
let e: Vec<u32> = crate::poly::exponent::add_vecs_or_panic(ea, eb);
let slot = out
.terms
.entry(e)
.or_insert_with(|| QParam::zero(self.n_params));
*slot = slot.add(&ca.mul(cb));
}
}
out.terms.retain(|_, c| !c.is_zero());
out
}
pub fn as_coeff(&self) -> Option<QParam> {
match self.terms.len() {
0 => Some(QParam::zero(self.n_params)),
1 => {
let (e, c) = self.terms.iter().next()?;
e.iter().all(|&k| k == 0).then(|| c.clone())
}
_ => None,
}
}
pub fn mul_monomial(&self, shift: &[u32], c: &QParam) -> Self {
if c.is_zero() {
return ParamGbPoly::zero(self.n_vars, self.n_params);
}
ParamGbPoly {
terms: self
.terms
.iter()
.map(|(e, v)| {
let ne: Vec<u32> = crate::poly::exponent::add_vecs_or_panic(e, shift);
(ne, v.mul(c))
})
.collect(),
n_vars: self.n_vars,
n_params: self.n_params,
}
}
fn make_monic(&self, order: MonomialOrder, conds: &mut ConditionLog) -> Self {
let Some(lc) = self.leading_coeff(order) else {
return self.clone();
};
if lc.is_one() {
return self.clone();
}
conds.record_inversion(&lc);
match lc.inv() {
Some(inv) => self.scale(&inv),
None => self.clone(),
}
}
pub fn specialize(&self, values: &[Rational]) -> Option<GbPoly> {
let mut terms = BTreeMap::new();
for (e, c) in &self.terms {
let v = c.eval(values)?;
if v != 0 {
terms.insert(e.clone(), v);
}
}
Some(GbPoly {
terms,
n_vars: self.n_vars,
})
}
}
#[derive(Debug, Default)]
struct ConditionLog {
raw: Vec<ParamPoly>,
}
impl ConditionLog {
fn record_inversion(&mut self, c: &QParam) {
self.push(c.numerator());
self.push(c.denominator());
}
fn record_regular(&mut self, c: &QParam) {
self.push(c.denominator());
}
fn push(&mut self, p: &ParamPoly) {
if p.is_zero() || p.is_nonzero_constant() {
return; }
self.raw.push(p.clone());
}
fn finish(self) -> Vec<ParamPoly> {
let mut set: BTreeSet<ParamPoly> = BTreeSet::new();
for p in &self.raw {
for f in p.irreducible_factors() {
set.insert(f);
}
}
set.into_iter().collect()
}
}
fn reduce_param(
f: &ParamGbPoly,
gs: &[ParamGbPoly],
order: MonomialOrder,
conds: &mut ConditionLog,
) -> ParamGbPoly {
let mut p = f.clone();
let mut r = ParamGbPoly::zero(f.n_vars, f.n_params);
let mut last_divisor: usize = 0;
let is_graded = order.is_graded();
'outer: while !p.is_zero() {
let (lt_exp, lt_coeff) = match p.leading_term(order) {
Some((e, c)) => (e.clone(), c.clone()),
None => break,
};
let lt_deg: u32 = if is_graded {
crate::poly::exponent::total_degree_or_panic(<_exp)
} else {
0
};
for offset in 0..gs.len() {
let idx = (last_divisor + offset) % gs.len();
let g = &gs[idx];
if let Some((lg_exp, lg_coeff)) = g.leading_term(order) {
if is_graded && crate::poly::exponent::total_degree_or_panic(lg_exp) > lt_deg {
continue;
}
if lt_exp.len() == lg_exp.len()
&& lt_exp.iter().zip(lg_exp.iter()).all(|(a, b)| a >= b)
{
let shift: Vec<u32> = lt_exp
.iter()
.zip(lg_exp.iter())
.map(|(a, b)| a - b)
.collect();
if !lg_coeff.is_one() {
conds.record_inversion(lg_coeff);
}
let Some(coeff) = lt_coeff.div(lg_coeff) else {
continue;
};
p = p.sub(&g.mul_monomial(&shift, &coeff));
last_divisor = idx;
continue 'outer;
}
}
}
let mut lt = ParamGbPoly::zero(f.n_vars, f.n_params);
lt.terms.insert(lt_exp.clone(), lt_coeff);
r = r.add(<);
p.terms.remove(<_exp);
}
r
}
fn s_polynomial_param(
f: &ParamGbPoly,
g: &ParamGbPoly,
order: MonomialOrder,
conds: &mut ConditionLog,
) -> ParamGbPoly {
let (Some((lf_exp, lf_coeff)), Some((lg_exp, lg_coeff))) =
(f.leading_term(order), g.leading_term(order))
else {
return ParamGbPoly::zero(f.n_vars, f.n_params);
};
let lcm = lcm_exp(lf_exp, lg_exp);
let shift_f: Vec<u32> = lcm.iter().zip(lf_exp.iter()).map(|(l, a)| l - a).collect();
let shift_g: Vec<u32> = lcm.iter().zip(lg_exp.iter()).map(|(l, b)| l - b).collect();
let one = QParam::one(f.n_params);
if !lf_coeff.is_one() {
conds.record_inversion(lf_coeff);
}
if !lg_coeff.is_one() {
conds.record_inversion(lg_coeff);
}
let (Some(cf), Some(cg)) = (one.div(lf_coeff), one.div(lg_coeff)) else {
return ParamGbPoly::zero(f.n_vars, f.n_params);
};
f.mul_monomial(&shift_f, &cf)
.sub(&g.mul_monomial(&shift_g, &cg))
}
fn interreduce(
mut basis: Vec<ParamGbPoly>,
order: MonomialOrder,
conds: &mut ConditionLog,
) -> Vec<ParamGbPoly> {
let mut i = 0;
while i < basis.len() {
let others: Vec<ParamGbPoly> = basis
.iter()
.enumerate()
.filter(|&(j, _)| j != i)
.map(|(_, g)| g.clone())
.collect();
let reduced = reduce_param(&basis[i], &others, order, conds);
if reduced.is_zero() {
basis.remove(i);
} else {
basis[i] = reduced.make_monic(order, conds);
i += 1;
}
}
basis
}
#[derive(Clone, Debug)]
pub struct ParamGroebnerBasis {
generators: Vec<ParamGbPoly>,
order: MonomialOrder,
n_vars: usize,
n_params: usize,
conditions: Vec<ParamPoly>,
input_generators: Vec<ParamGbPoly>,
eliminated: Vec<usize>,
}
impl ParamGroebnerBasis {
pub fn compute(
gens: Vec<ParamGbPoly>,
order: MonomialOrder,
) -> Result<Self, ParamGroebnerError> {
let first = gens.first().ok_or(ParamGroebnerError::NoGenerators)?;
let (n_vars, n_params) = (first.n_vars, first.n_params);
for g in &gens {
if (g.n_vars, g.n_params) != (n_vars, n_params) {
return Err(ParamGroebnerError::ShapeMismatch {
expected: (n_vars, n_params),
got: (g.n_vars, g.n_params),
});
}
}
let mut conds = ConditionLog::default();
for g in &gens {
for c in g.terms.values() {
conds.record_regular(c);
}
}
let input_generators = gens.clone();
let initial: Vec<ParamGbPoly> = gens
.into_iter()
.filter(|g| !g.is_zero())
.map(|g| g.make_monic(order, &mut conds))
.collect();
if initial.is_empty() {
return Ok(ParamGroebnerBasis {
generators: vec![],
order,
n_vars,
n_params,
conditions: conds.finish(),
input_generators,
eliminated: vec![],
});
}
let mut basis: Vec<ParamGbPoly> = Vec::with_capacity(initial.len() * 2);
let mut basis_sugar: Vec<u32> = Vec::with_capacity(initial.len() * 2);
let mut basis_lead: Vec<Vec<u32>> = Vec::with_capacity(initial.len() * 2);
let mut pair_vec: Vec<CriticalPair> = Vec::new();
for gen in initial {
let sugar = gen.sugar();
let Some(lead) = gen.leading_exp(order) else {
continue;
};
let new_idx = basis.len();
basis.push(gen);
basis_sugar.push(sugar);
basis_lead.push(lead);
update_pairs(&basis_lead, &basis_sugar, &mut pair_vec, new_idx);
}
let mut heap: BinaryHeap<CriticalPair> = BinaryHeap::from(pair_vec);
while let Some(pair) = heap.pop() {
let sp = s_polynomial_param(&basis[pair.i], &basis[pair.j], order, &mut conds);
let r = reduce_param(&sp, &basis, order, &mut conds);
if !r.is_zero() {
let r = r.make_monic(order, &mut conds);
let sugar = r.sugar();
let Some(lead) = r.leading_exp(order) else {
continue;
};
let new_idx = basis.len();
basis.push(r);
basis_sugar.push(sugar);
basis_lead.push(lead);
let mut pv: Vec<CriticalPair> = heap.into_vec();
update_pairs(&basis_lead, &basis_sugar, &mut pv, new_idx);
heap = BinaryHeap::from(pv);
}
}
let generators = interreduce(basis, order, &mut conds);
Ok(ParamGroebnerBasis {
generators,
order,
n_vars,
n_params,
conditions: conds.finish(),
input_generators,
eliminated: vec![],
})
}
pub fn generators(&self) -> &[ParamGbPoly] {
&self.generators
}
pub fn order(&self) -> MonomialOrder {
self.order
}
pub fn n_vars(&self) -> usize {
self.n_vars
}
pub fn n_params(&self) -> usize {
self.n_params
}
pub fn len(&self) -> usize {
self.generators.len()
}
pub fn is_empty(&self) -> bool {
self.generators.is_empty()
}
pub fn conditions(&self) -> &[ParamPoly] {
&self.conditions
}
pub fn vanishing_conditions(&self, values: &[Rational]) -> Vec<ParamPoly> {
self.conditions
.iter()
.filter(|c| c.eval(values) == 0)
.cloned()
.collect()
}
pub fn is_regular_at(&self, values: &[Rational]) -> bool {
self.conditions.iter().all(|c| c.eval(values) != 0)
}
pub fn specialize(&self, values: &[Rational]) -> Result<Vec<GbPoly>, ParamGroebnerError> {
if values.len() != self.n_params {
return Err(ParamGroebnerError::WrongArity {
expected: self.n_params,
got: values.len(),
});
}
let vanishing = self.vanishing_conditions(values);
if !vanishing.is_empty() {
return Err(ParamGroebnerError::Degenerate { vanishing });
}
let mut out = Vec::with_capacity(self.generators.len());
for g in &self.generators {
match g.specialize(values) {
Some(p) => out.push(p),
None => {
return Err(ParamGroebnerError::Degenerate {
vanishing: g
.terms
.values()
.filter(|c| c.denominator().eval(values) == 0)
.map(|c| c.denominator().clone())
.collect(),
})
}
}
}
Ok(out)
}
pub fn specialize_verified(
&self,
values: &[Rational],
) -> Result<Vec<GbPoly>, ParamGroebnerError> {
if values.len() != self.n_params {
return Err(ParamGroebnerError::WrongArity {
expected: self.n_params,
got: values.len(),
});
}
if self.is_regular_at(values) {
return self.specialize(values);
}
let vanishing = self.vanishing_conditions(values);
let degenerate = || ParamGroebnerError::Degenerate {
vanishing: vanishing.clone(),
};
let mut spec = Vec::with_capacity(self.generators.len());
for g in &self.generators {
spec.push(g.specialize(values).ok_or_else(degenerate)?);
}
let mut direct = Vec::with_capacity(self.input_generators.len());
for g in &self.input_generators {
let p = g.specialize(values).ok_or_else(degenerate)?;
if !p.terms.is_empty() {
direct.push(p);
}
}
if direct.is_empty() {
return Err(degenerate());
}
let mut oracle = GroebnerBasis::compute(direct, self.order);
if !self.eliminated.is_empty() {
oracle = GroebnerBasis::from_generators(
oracle
.generators()
.iter()
.filter(|g| {
!g.terms.keys().any(|e| {
self.eliminated
.iter()
.any(|&i| e.get(i).copied().unwrap_or(0) > 0)
})
})
.cloned()
.collect(),
self.order,
);
}
if !spec.iter().all(|g| oracle.contains(g)) {
return Err(degenerate());
}
let spec_gb = GroebnerBasis::compute(
spec.iter()
.filter(|g| !g.terms.is_empty())
.cloned()
.collect(),
self.order,
);
if !oracle.generators().iter().all(|g| spec_gb.contains(g)) {
return Err(degenerate());
}
Ok(spec)
}
pub fn reduce(&self, p: &ParamGbPoly) -> ParamGbPoly {
let mut sink = ConditionLog::default();
reduce_param(p, &self.generators, self.order, &mut sink)
}
pub fn contains(&self, p: &ParamGbPoly) -> bool {
self.reduce(p).is_zero()
}
pub fn contains_ideal(&self, other: &ParamGroebnerBasis) -> bool {
if (self.n_vars, self.n_params) != (other.n_vars, other.n_params) {
return false;
}
other.generators.iter().all(|g| self.contains(g))
}
pub fn equals_ideal(&self, other: &ParamGroebnerBasis) -> bool {
self.contains_ideal(other) && other.contains_ideal(self)
}
pub fn extend_vars(&self, extra: usize) -> ParamGroebnerBasis {
if extra == 0 {
return self.clone();
}
let pad = |p: &ParamGbPoly| ParamGbPoly {
terms: p
.terms
.iter()
.map(|(e, c)| {
let mut e = e.clone();
e.extend(std::iter::repeat(0u32).take(extra));
(e, c.clone())
})
.collect(),
n_vars: p.n_vars + extra,
n_params: p.n_params,
};
ParamGroebnerBasis {
generators: self.generators.iter().map(pad).collect(),
order: self.order,
n_vars: self.n_vars + extra,
n_params: self.n_params,
conditions: self.conditions.clone(),
input_generators: self.input_generators.iter().map(pad).collect(),
eliminated: self.eliminated.clone(),
}
}
pub fn eliminate(&self, vars: &[usize]) -> ParamGroebnerBasis {
let generators: Vec<ParamGbPoly> = self
.generators
.iter()
.filter(|g| {
!g.terms
.keys()
.any(|e| vars.iter().any(|&i| e.get(i).copied().unwrap_or(0) > 0))
})
.cloned()
.collect();
let mut eliminated = self.eliminated.clone();
eliminated.extend_from_slice(vars);
ParamGroebnerBasis {
generators,
order: self.order,
n_vars: self.n_vars,
n_params: self.n_params,
conditions: self.conditions.clone(),
input_generators: self.input_generators.clone(),
eliminated,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::poly::groebner::GroebnerBasis;
pub(super) fn term(
n_vars: usize,
n_params: usize,
var_exp: &[u32],
par_exp: &[u32],
c: i64,
) -> ParamGbPoly {
let mut mono = ParamPoly::zero(n_params);
mono.terms.insert(par_exp.to_vec(), rug::Integer::from(1));
let coeff =
QParam::from_rational(&Rational::from(c), n_params).mul(&QParam::from_poly(mono));
let mut p = ParamGbPoly::zero(n_vars, n_params);
if !coeff.is_zero() {
p.terms.insert(var_exp.to_vec(), coeff);
}
p
}
pub(super) fn sum(parts: Vec<ParamGbPoly>) -> ParamGbPoly {
let mut it = parts.into_iter();
let first = it.next().expect("non-empty");
it.fold(first, |a, b| a.add(&b))
}
pub(super) fn rat(v: i64) -> Rational {
Rational::from(v)
}
pub(super) fn gb_over_q(polys: &[Vec<(Vec<u32>, Rational)>], n_vars: usize) -> GroebnerBasis {
let gens: Vec<GbPoly> = polys
.iter()
.map(|terms| GbPoly {
terms: terms.iter().cloned().collect(),
n_vars,
})
.collect();
GroebnerBasis::compute(gens, MonomialOrder::Lex)
}
#[test]
fn linear_system_with_a_parametric_coefficient() {
let f = term(2, 1, &[1, 0], &[1], 1).sub(&term(2, 1, &[0, 1], &[0], 1));
let g = sum(vec![
term(2, 1, &[1, 0], &[0], 1),
term(2, 1, &[0, 1], &[0], 1),
term(2, 1, &[0, 0], &[0], -1),
]);
let gb = ParamGroebnerBasis::compute(vec![f, g], MonomialOrder::Lex).unwrap();
assert_eq!(gb.len(), 2, "expected a triangular basis");
assert!(
gb.conditions()
.iter()
.any(|c| *c == ParamPoly::var(0, 1).add(&ParamPoly::one(1))),
"a + 1 must be reported: the system is degenerate at a = -1, got {:?}",
gb.conditions()
);
assert!(!gb.is_regular_at(&[rat(-1)]));
assert!(gb.is_regular_at(&[rat(3)]));
}
#[test]
fn specialisation_agrees_with_computing_over_q() {
let f = term(2, 1, &[1, 0], &[1], 1).sub(&term(2, 1, &[0, 1], &[0], 1));
let g = sum(vec![
term(2, 1, &[1, 0], &[0], 1),
term(2, 1, &[0, 1], &[0], 1),
term(2, 1, &[0, 0], &[0], -1),
]);
let gb = ParamGroebnerBasis::compute(vec![f, g], MonomialOrder::Lex).unwrap();
for a in [2i64, 3, -5, 7] {
let spec = gb.specialize(&[rat(a)]).expect("regular point");
let direct = gb_over_q(
&[
vec![
(vec![1, 0], Rational::from(a)),
(vec![0, 1], Rational::from(-1)),
],
vec![
(vec![1, 0], Rational::from(1)),
(vec![0, 1], Rational::from(1)),
(vec![0, 0], Rational::from(-1)),
],
],
2,
);
let mut got: Vec<_> = spec.iter().map(|p| p.terms.clone()).collect();
let mut want: Vec<_> = direct
.generators()
.iter()
.map(|p| p.terms.clone())
.collect();
got.sort();
want.sort();
assert_eq!(got, want, "specialisation at a = {a} disagrees with ℚ");
}
}
#[test]
fn degenerate_point_is_refused_and_is_genuinely_different() {
let f = term(2, 1, &[1, 0], &[1], 1).sub(&term(2, 1, &[0, 1], &[0], 1));
let g = sum(vec![
term(2, 1, &[1, 0], &[0], 1),
term(2, 1, &[0, 1], &[0], 1),
term(2, 1, &[0, 0], &[0], -1),
]);
let gb = ParamGroebnerBasis::compute(vec![f, g], MonomialOrder::Lex).unwrap();
let err = gb.specialize(&[rat(-1)]).unwrap_err();
assert_eq!(err.code(), "E-PARAMGB-004");
let ParamGroebnerError::Degenerate { vanishing } = &err else {
panic!("expected a degeneracy report, got {err}");
};
assert!(!vanishing.is_empty());
let direct = gb_over_q(
&[
vec![
(vec![1, 0], Rational::from(-1)),
(vec![0, 1], Rational::from(-1)),
],
vec![
(vec![1, 0], Rational::from(1)),
(vec![0, 1], Rational::from(1)),
(vec![0, 0], Rational::from(-1)),
],
],
2,
);
assert_eq!(direct.len(), 1);
assert!(direct.generators()[0].terms.contains_key(&vec![0, 0]));
}
#[test]
fn no_parameters_reproduces_the_rational_engine() {
let f = term(1, 0, &[2], &[], 1).sub(&term(1, 0, &[0], &[], 1));
let g = term(1, 0, &[1], &[], 1).sub(&term(1, 0, &[0], &[], 1));
let gb = ParamGroebnerBasis::compute(vec![f, g], MonomialOrder::Lex).unwrap();
assert_eq!(gb.len(), 1);
assert!(gb.conditions().is_empty());
let spec = gb.specialize(&[]).unwrap();
assert_eq!(spec.len(), 1);
assert!(spec[0].terms.contains_key(&vec![1]));
}
#[test]
fn from_gbpoly_moves_the_trailing_slots_into_the_field() {
let src = GbPoly {
terms: [
(vec![1u32, 0, 1], Rational::from(1)),
(vec![0, 1, 2], Rational::from(1)),
]
.into_iter()
.collect(),
n_vars: 3,
};
let p = ParamGbPoly::from_gbpoly(&src, 2, 1).unwrap();
assert_eq!(p.n_terms(), 2);
let cx = p.terms.get(&vec![1u32, 0]).unwrap();
assert_eq!(*cx, QParam::from_poly(ParamPoly::var(0, 1)));
let cy = p.terms.get(&vec![0u32, 1]).unwrap();
assert_eq!(
*cy,
QParam::from_poly(ParamPoly::var(0, 1).mul(&ParamPoly::var(0, 1)))
);
assert!(ParamGbPoly::from_gbpoly(&src, 1, 1).is_none());
}
#[test]
fn membership_and_elimination() {
let n = 3;
let f = term(n, 1, &[1, 0, 0], &[0], -1).add(&term(n, 1, &[0, 1, 0], &[0], 1));
let g = term(n, 1, &[2, 0, 0], &[0], -1).add(&term(n, 1, &[0, 0, 1], &[0], 1));
let gb = ParamGroebnerBasis::compute(vec![f.clone(), g], MonomialOrder::Lex).unwrap();
assert!(gb.contains(&f));
let el = gb.eliminate(&[0]);
assert_eq!(el.len(), 1);
let rel = &el.generators()[0];
assert!(rel.terms.contains_key(&vec![0, 2, 0]));
assert!(rel.terms.contains_key(&vec![0, 0, 1]));
}
#[test]
fn wrong_arity_is_reported() {
let f = term(1, 2, &[1], &[0, 0], 1);
let gb = ParamGroebnerBasis::compute(vec![f], MonomialOrder::Lex).unwrap();
let err = gb.specialize(&[rat(1)]).unwrap_err();
assert_eq!(err.code(), "E-PARAMGB-003");
}
#[test]
fn no_generators_is_reported() {
let err = ParamGroebnerBasis::compute(vec![], MonomialOrder::Lex).unwrap_err();
assert_eq!(err.code(), "E-PARAMGB-001");
}
#[test]
fn shape_mismatch_is_reported() {
let f = term(2, 1, &[1, 0], &[0], 1);
let g = term(3, 1, &[1, 0, 0], &[0], 1);
let err = ParamGroebnerBasis::compute(vec![f, g], MonomialOrder::Lex).unwrap_err();
assert_eq!(err.code(), "E-PARAMGB-002");
}
}
#[cfg(test)]
mod ideal_and_verify_tests {
use super::tests::*;
use super::*;
fn cramer() -> ParamGroebnerBasis {
let f = sum(vec![
term(2, 4, &[1, 0], &[1, 0, 0, 0], 1),
term(2, 4, &[0, 1], &[0, 1, 0, 0], 1),
term(2, 4, &[0, 0], &[0, 0, 0, 0], -1),
]);
let g = sum(vec![
term(2, 4, &[1, 0], &[0, 0, 1, 0], 1),
term(2, 4, &[0, 1], &[0, 0, 0, 1], 1),
term(2, 4, &[0, 0], &[0, 0, 0, 0], -1),
]);
ParamGroebnerBasis::compute(vec![f, g], MonomialOrder::Lex).unwrap()
}
#[test]
fn a_basis_contains_its_own_generators() {
let gb = cramer();
assert!(!gb.is_empty());
for g in gb.generators() {
assert!(gb.contains(g), "a basis must contain its own generators");
}
}
#[test]
fn equals_ideal_is_mutual_containment() {
let f = term(1, 1, &[1], &[1], 1).sub(&term(1, 1, &[0], &[0], 1));
let g = term(1, 1, &[1], &[2], 1).sub(&term(1, 1, &[0], &[1], 1));
let gf = ParamGroebnerBasis::compute(vec![f], MonomialOrder::Lex).unwrap();
let gg = ParamGroebnerBasis::compute(vec![g], MonomialOrder::Lex).unwrap();
assert!(gf.equals_ideal(&gg));
assert!(gg.equals_ideal(&gf));
let h = term(1, 1, &[1], &[0], 1);
let gh = ParamGroebnerBasis::compute(vec![h], MonomialOrder::Lex).unwrap();
assert!(!gf.equals_ideal(&gh));
assert!(!gh.contains_ideal(&gf));
}
#[test]
fn equals_ideal_refuses_a_shape_mismatch() {
let f = term(1, 1, &[1], &[1], 1);
let g = term(1, 2, &[1], &[1, 0], 1);
let gf = ParamGroebnerBasis::compute(vec![f], MonomialOrder::Lex).unwrap();
let gg = ParamGroebnerBasis::compute(vec![g], MonomialOrder::Lex).unwrap();
assert!(!gf.equals_ideal(&gg));
}
#[test]
fn specialize_verified_accepts_an_unnecessary_refusal() {
let gb = cramer();
let pt = [rat(0), rat(1), rat(1), rat(1)];
assert!(!gb.is_regular_at(&pt));
assert!(matches!(
gb.specialize(&pt),
Err(ParamGroebnerError::Degenerate { .. })
));
let spec = gb
.specialize_verified(&pt)
.expect("refusal was unnecessary");
let direct = gb_over_q(
&[
vec![(vec![0, 1], rat(1)), (vec![0, 0], rat(-1))],
vec![
(vec![1, 0], rat(1)),
(vec![0, 1], rat(1)),
(vec![0, 0], rat(-1)),
],
],
2,
);
let spec_gb = GroebnerBasis::compute(spec, MonomialOrder::Lex);
for g in direct.generators() {
assert!(spec_gb.contains(g));
}
for g in spec_gb.generators() {
assert!(direct.contains(g));
}
}
#[test]
fn specialize_verified_still_refuses_a_genuine_pole() {
let gb = cramer();
for pt in [
[rat(1), rat(1), rat(1), rat(1)],
[rat(1), rat(1), rat(2), rat(2)],
] {
assert!(matches!(
gb.specialize_verified(&pt),
Err(ParamGroebnerError::Degenerate { .. })
));
}
}
#[test]
fn specialize_verified_matches_specialize_at_regular_points() {
let gb = cramer();
let pt = [rat(1), rat(2), rat(3), rat(4)];
assert!(gb.is_regular_at(&pt));
let a = gb.specialize(&pt).unwrap();
let b = gb.specialize_verified(&pt).unwrap();
assert_eq!(a.len(), b.len());
for (x, y) in a.iter().zip(b.iter()) {
assert_eq!(x.terms, y.terms);
}
}
#[test]
fn extend_vars_keeps_membership() {
let gb = cramer();
let wide = gb.extend_vars(3);
assert_eq!(wide.n_vars(), gb.n_vars() + 3);
for (narrow, padded) in gb.generators().iter().zip(wide.generators()) {
assert_eq!(narrow.n_terms(), padded.n_terms());
assert!(wide.contains(padded));
}
}
}