use ocas_domain::Domain;
use ocas_domain::{Rational, RationalDomain};
use crate::groebner::{Algorithm, GroebnerBasis, groebner_basis};
use crate::sparse::{Lex, SparseMultivariatePolynomial};
pub fn ideal_contains<D: Domain + 'static>(
generators: &[SparseMultivariatePolynomial<D, Lex>],
f: &SparseMultivariatePolynomial<D, Lex>,
algo: Algorithm,
) -> bool {
if generators.is_empty() {
return f.is_zero();
}
let gb = groebner_basis(generators, algo);
let remainder = f.reduce(&gb.basis);
remainder.is_zero()
}
pub fn ideal_sum<D: Domain + 'static>(
generators_a: &[SparseMultivariatePolynomial<D, Lex>],
generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex> {
let mut combined = generators_a.to_vec();
combined.extend(generators_b.iter().cloned());
groebner_basis(&combined, Algorithm::Auto)
}
pub fn ideal_product<D: Domain + 'static>(
generators_a: &[SparseMultivariatePolynomial<D, Lex>],
generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex> {
let products: Vec<SparseMultivariatePolynomial<D, Lex>> = generators_a
.iter()
.flat_map(|f| generators_b.iter().map(move |g| f.mul(g)))
.collect();
groebner_basis(&products, Algorithm::Auto)
}
pub fn ideal_quotient<D: Domain + 'static>(
generators_i: &[SparseMultivariatePolynomial<D, Lex>],
generators_j: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex> {
if generators_i.is_empty() || generators_j.is_empty() {
return GroebnerBasis { basis: vec![] };
}
let mut result: Option<Vec<SparseMultivariatePolynomial<D, Lex>>> = None;
for g in generators_j {
if g.is_zero() {
continue;
}
let i_colon_g = quotient_single_generator(generators_i, g);
if let Some(current) = result.take() {
result = Some(intersect_generators(¤t, &i_colon_g));
} else {
result = Some(i_colon_g);
}
}
match result {
None => GroebnerBasis { basis: vec![] },
Some(gens) => {
if gens.is_empty() {
GroebnerBasis { basis: vec![] }
} else {
groebner_basis(&gens, Algorithm::Auto)
}
}
}
}
fn quotient_single_generator<D: Domain + 'static>(
generators_i: &[SparseMultivariatePolynomial<D, Lex>],
g: &SparseMultivariatePolynomial<D, Lex>,
) -> Vec<SparseMultivariatePolynomial<D, Lex>> {
let n_vars = g.n_vars();
let domain = g.domain().clone();
let embedded: Vec<SparseMultivariatePolynomial<D, Lex>> =
generators_i.iter().map(|p| p.embed_new_main()).collect();
let g_embedded = g.embed_new_main();
let w = {
let mut exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
exp[0] = 1;
SparseMultivariatePolynomial::from_terms(
domain.clone(),
n_vars + 1,
vec![(exp.to_vec(), domain.one())],
)
};
let wg = w.mul(&g_embedded);
let one_minus_wg = {
let one_exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
let one = SparseMultivariatePolynomial::from_terms(
domain.clone(),
n_vars + 1,
vec![(one_exp.to_vec(), domain.one())],
);
one.sub(&wg)
};
let mut combined = embedded;
combined.push(one_minus_wg);
let elim_gb = crate::groebner::eliminate(&combined, 1, Algorithm::Auto);
elim_gb
.basis
.into_iter()
.map(|p| p.drop_variable(0))
.collect()
}
fn intersect_generators<D: Domain + 'static>(
generators_a: &[SparseMultivariatePolynomial<D, Lex>],
generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> Vec<SparseMultivariatePolynomial<D, Lex>> {
let n_vars = generators_a
.first()
.or(generators_b.first())
.map(|p| p.n_vars())
.unwrap_or(0);
let domain = generators_a
.first()
.or(generators_b.first())
.map(|p| p.domain().clone())
.unwrap_or_else(|| {
unreachable!("intersect_generators called with empty inputs")
});
if generators_a.is_empty() || generators_b.is_empty() {
return vec![];
}
let t = {
let mut exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
exp[0] = 1;
SparseMultivariatePolynomial::from_terms(
domain.clone(),
n_vars + 1,
vec![(exp.to_vec(), domain.one())],
)
};
let one_minus_t = {
let one_exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
let one = SparseMultivariatePolynomial::from_terms(
domain.clone(),
n_vars + 1,
vec![(one_exp.to_vec(), domain.one())],
);
one.sub(&t)
};
let mut combined: Vec<SparseMultivariatePolynomial<D, Lex>> = Vec::new();
for f in generators_a {
let f_emb = f.embed_new_main();
combined.push(t.mul(&f_emb));
}
for g in generators_b {
let g_emb = g.embed_new_main();
combined.push(one_minus_t.mul(&g_emb));
}
let elim_gb = crate::groebner::eliminate(&combined, 1, Algorithm::Auto);
elim_gb
.basis
.into_iter()
.map(|p| p.drop_variable(0))
.collect()
}
pub fn ideal_intersection<D: Domain + 'static>(
generators_a: &[SparseMultivariatePolynomial<D, Lex>],
generators_b: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex> {
if generators_a.is_empty() || generators_b.is_empty() {
return GroebnerBasis { basis: vec![] };
}
let gens = intersect_generators(generators_a, generators_b);
if gens.is_empty() {
GroebnerBasis { basis: vec![] }
} else {
groebner_basis(&gens, Algorithm::Auto)
}
}
pub fn ideal_saturate<D: Domain + 'static>(
generators_i: &[SparseMultivariatePolynomial<D, Lex>],
generators_j: &[SparseMultivariatePolynomial<D, Lex>],
) -> GroebnerBasis<D, Lex> {
if generators_i.is_empty() {
return GroebnerBasis { basis: vec![] };
}
if generators_j.is_empty() {
return groebner_basis(generators_i, Algorithm::Auto);
}
let mut current_gens = generators_i.to_vec();
let max_iter = 20;
for _ in 0..max_iter {
let current_gb = groebner_basis(¤t_gens, Algorithm::Auto);
let next = ideal_quotient(¤t_gb.basis, generators_j);
let next_gb = groebner_basis(&next.basis, Algorithm::Auto);
let all_in_current = next_gb
.basis
.iter()
.all(|p| p.reduce(¤t_gb.basis).is_zero());
let all_in_next = current_gb
.basis
.iter()
.all(|p| p.reduce(&next_gb.basis).is_zero());
if all_in_current && all_in_next {
return current_gb;
}
current_gens = next_gb.basis;
}
groebner_basis(¤t_gens, Algorithm::Auto)
}
#[derive(Debug, Clone)]
pub struct RealSolution {
pub values: Vec<f64>,
pub multiplicity: usize,
}
#[derive(Debug, Clone)]
pub struct ZeroDimSolutions {
pub solutions: Vec<RealSolution>,
pub vector_space_dimension: usize,
}
#[derive(Debug, Clone)]
pub enum PolynomialSystemSolution {
ZeroDimensional(ZeroDimSolutions),
PositiveDimensional(GroebnerBasis<RationalDomain, Lex>),
Empty,
}
pub fn is_zero_dimensional(gb: &GroebnerBasis<RationalDomain, Lex>) -> bool {
let n_vars = match gb.basis.first() {
Some(p) => p.n_vars(),
None => return false, };
for var in 0..n_vars {
let has_pure_power = gb.basis.iter().any(|p| match p.leading_monomial() {
Some(lm) => lm
.iter()
.enumerate()
.all(|(i, &e)| if i == var { e > 0 } else { e == 0 }),
None => false,
});
if !has_pure_power {
return false;
}
}
true
}
fn extract_univariate(
poly: &SparseMultivariatePolynomial<RationalDomain, Lex>,
var_index: usize,
) -> crate::dense::DenseUnivariatePolynomial<RationalDomain> {
let d = RationalDomain;
let deg = poly.degree_in(var_index);
let mut coeffs = vec![ocas_domain::Rational::new(0, 1); deg + 1];
for (exp, coeff) in poly.terms_ref() {
let power = exp.get(var_index).copied().unwrap_or(0);
coeffs[power] = coeffs[power].clone() + coeff.clone();
}
crate::dense::DenseUnivariatePolynomial::from_coeffs(d, coeffs)
}
fn solve_univariate_f64(
poly: &SparseMultivariatePolynomial<RationalDomain, Lex>,
var_index: usize,
substituted_values: &[f64], ) -> Vec<f64> {
let d = RationalDomain;
let deg = poly.degree_in(var_index);
let mut coeffs_f64 = vec![0.0f64; deg + 1];
for (exp, coeff) in poly.terms_ref() {
let mut coeff_f = format!("{}", coeff).parse::<f64>().unwrap_or(0.0);
for (i, &e) in exp.iter().enumerate() {
if i > var_index && e > 0 {
let sub_idx = i - var_index - 1;
if sub_idx < substituted_values.len() {
coeff_f *= substituted_values[sub_idx].powi(e as i32);
}
}
}
let power = exp.get(var_index).copied().unwrap_or(0);
coeffs_f64[power] += coeff_f;
}
let rational_coeffs: Vec<ocas_domain::Rational> =
coeffs_f64.iter().map(|&c| rational_approx(c)).collect();
let unipoly = crate::dense::DenseUnivariatePolynomial::from_coeffs(d, rational_coeffs);
let intervals = unipoly.isolate_real_roots();
intervals
.iter()
.map(|iv| {
let refined = unipoly.refine_root(iv, 1e-14);
(refined.low + refined.high) / 2.0
})
.collect()
}
fn rational_approx(x: f64) -> ocas_domain::Rational {
if x == 0.0 {
return ocas_domain::Rational::new(0, 1);
}
let sign: i64 = if x < 0.0 { -1 } else { 1 };
let x_abs = x.abs();
let mut a = x_abs.floor() as i64;
let mut frac = x_abs - a as f64;
let mut prev_num = 1i64;
let mut prev_den = 0i64;
let mut num = a;
let mut den = 1i64;
for _ in 0..50 {
if frac.abs() < 1e-12 {
break;
}
let r = 1.0 / frac;
a = r.floor() as i64;
frac = r - a as f64;
let new_num = a * num + prev_num;
let new_den = a * den + prev_den;
prev_num = num;
prev_den = den;
num = new_num;
den = new_den;
if den > 1_000_000 {
break;
}
}
ocas_domain::Rational::new(sign * num, den)
}
pub fn solve_polynomial_system(
equations: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
algo: Algorithm,
) -> PolynomialSystemSolution {
if equations.is_empty() {
return PolynomialSystemSolution::PositiveDimensional(GroebnerBasis { basis: vec![] });
}
let gb = groebner_basis(equations, algo);
if gb.basis.len() == 1
&& gb.basis[0].terms_ref().len() == 1
&& gb.basis[0]
.leading_monomial()
.map(|lm| lm.iter().all(|&e| e == 0))
.unwrap_or(false)
{
return PolynomialSystemSolution::Empty;
}
let gb_lex: GroebnerBasis<RationalDomain, Lex> = gb;
if !is_zero_dimensional(&gb_lex) {
return PolynomialSystemSolution::PositiveDimensional(gb_lex);
}
let solutions = solve_triangular(&gb_lex);
let dim = compute_vector_space_dim(&gb_lex).unwrap_or(solutions.len());
PolynomialSystemSolution::ZeroDimensional(ZeroDimSolutions {
solutions,
vector_space_dimension: dim,
})
}
fn compute_vector_space_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> Option<usize> {
let n_vars = gb.basis.first()?.n_vars();
let mut dim = 1usize;
for var in 0..n_vars {
let max_deg = gb
.basis
.iter()
.filter(|p| {
p.terms_ref()
.keys()
.all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
})
.map(|p| p.degree_in(var))
.max()
.unwrap_or(1);
dim = dim.checked_mul(max_deg)?;
}
Some(dim)
}
fn solve_triangular(gb: &GroebnerBasis<RationalDomain, Lex>) -> Vec<RealSolution> {
let n_vars = match gb.basis.first() {
Some(p) => p.n_vars(),
None => return vec![],
};
let raw = solve_recursive(gb, n_vars - 1, &[]);
raw.into_iter()
.map(|mut s| {
s.values.reverse();
s
})
.collect()
}
fn solve_recursive(
gb: &GroebnerBasis<RationalDomain, Lex>,
var_index: usize,
higher_values: &[f64],
) -> Vec<RealSolution> {
let univariate_poly = gb.basis.iter().find(|p| {
p.degree_in(var_index) > 0
&& p.terms_ref()
.keys()
.all(|e| e.iter().enumerate().all(|(i, &v)| i <= var_index || v == 0))
});
let roots = if let Some(poly) = univariate_poly {
let unipoly = extract_univariate(poly, var_index);
let intervals = unipoly.isolate_real_roots();
let r: Vec<f64> = intervals
.iter()
.map(|iv| {
let refined = unipoly.refine_root(iv, 1e-14);
(refined.low + refined.high) / 2.0
})
.collect();
r
} else {
let poly = gb.basis.iter().find(|p| p.degree_in(var_index) > 0);
let Some(poly) = poly else {
return vec![];
};
solve_univariate_f64(poly, var_index, higher_values)
};
if var_index == 0 {
roots
.into_iter()
.map(|v| RealSolution {
values: vec![v],
multiplicity: 1,
})
.collect()
} else {
let mut results = Vec::new();
for root in &roots {
let mut new_higher = Vec::with_capacity(higher_values.len() + 1);
new_higher.push(*root);
new_higher.extend_from_slice(higher_values);
let sub_solutions = solve_recursive(gb, var_index - 1, &new_higher);
for mut sol in sub_solutions {
sol.values.push(*root);
results.push(sol);
}
}
results
}
}
#[derive(Debug, Clone)]
pub struct PrimaryComponent {
pub primary: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
pub prime: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
}
pub fn ideal_radical(
generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> GroebnerBasis<RationalDomain, Lex> {
if generators.is_empty() {
return GroebnerBasis { basis: vec![] };
}
let gb = groebner_basis(generators, Algorithm::Auto);
if is_zero_dimensional(&gb) {
radical_zero_dim(&gb)
} else {
radical_via_jacobian(&gb)
}
}
fn radical_zero_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> GroebnerBasis<RationalDomain, Lex> {
let n_vars = match gb.basis.first() {
Some(p) => p.n_vars(),
None => return GroebnerBasis { basis: vec![] },
};
let domain = RationalDomain;
let mut radical_gens: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = Vec::new();
for var in 0..n_vars {
let univariate = gb.basis.iter().find(|p| {
p.terms_ref()
.keys()
.all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
&& p.degree_in(var) > 0
});
if let Some(poly) = univariate {
let unipoly = extract_univariate(poly, var);
let deriv = unipoly.derivative();
let g = unipoly.gcd(&deriv);
let sqf = unipoly.div_rem(&g).map(|(q, _)| q).unwrap_or(unipoly);
let terms: Vec<(Vec<usize>, ocas_domain::Rational)> = sqf
.coeffs()
.iter()
.enumerate()
.filter(|(_, c)| !domain.is_zero(c))
.map(|(i, c)| {
let mut exp = vec![0usize; n_vars];
exp[var] = i;
(exp, c.clone())
})
.collect();
if !terms.is_empty() {
radical_gens.push(SparseMultivariatePolynomial::from_terms(
domain, n_vars, terms,
));
}
}
}
for p in &gb.basis {
let is_univariate = p
.terms_ref()
.keys()
.any(|e| e.iter().filter(|&&v| v > 0).count() <= 1);
if !is_univariate {
radical_gens.push(p.clone());
}
}
groebner_basis(&radical_gens, Algorithm::Auto)
}
fn radical_via_jacobian(
gb: &GroebnerBasis<RationalDomain, Lex>,
) -> GroebnerBasis<RationalDomain, Lex> {
let n_vars = match gb.basis.first() {
Some(p) => p.n_vars(),
None => return gb.clone(),
};
if n_vars == 0 || gb.basis.is_empty() {
return gb.clone();
}
let mut derivatives: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = Vec::new();
for f in &gb.basis {
for var in 0..n_vars {
let df = f.derivative(var);
if df.total_degree().is_some_and(|d| d > 0) {
derivatives.push(df);
}
}
}
if derivatives.is_empty() {
return gb.clone();
}
let h = derivatives.into_iter().reduce(|a, b| {
if a.total_degree() <= b.total_degree() {
a
} else {
b
}
});
let Some(h) = h else {
return gb.clone();
};
if h.total_degree() == Some(0) || h.total_degree().is_none() {
return gb.clone();
}
ideal_saturate(&gb.basis, std::slice::from_ref(&h))
}
pub fn primary_decomposition(
generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
) -> Vec<PrimaryComponent> {
if generators.is_empty() {
return vec![];
}
let gb = groebner_basis(generators, Algorithm::Auto);
if is_zero_dimensional(&gb) {
primary_decomp_zero_dim(&gb)
} else {
vec![PrimaryComponent {
primary: gb.basis.clone(),
prime: gb.basis.clone(), }]
}
}
fn primary_decomp_zero_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> Vec<PrimaryComponent> {
if gb.basis.is_empty() {
return vec![];
}
let n_vars = match gb.basis.first() {
Some(p) => p.n_vars(),
None => return vec![],
};
let univariate = gb.basis.iter().find(|p| {
p.terms_ref()
.keys()
.all(|e| e.iter().enumerate().all(|(i, &v)| i == 0 || v == 0))
&& p.degree_in(0) > 0
});
let Some(poly) = univariate else {
return vec![PrimaryComponent {
primary: gb.basis.clone(),
prime: ideal_radical(&gb.basis).basis,
}];
};
let unipoly = extract_univariate(poly, 0);
let deriv = unipoly.derivative();
let g = unipoly.gcd(&deriv);
let sqf = match unipoly.div_rem(&g) {
Some((q, _)) => q,
None => unipoly.clone(),
};
let factors = crate::factor::algebraic::factor_square_free_rationals(&sqf);
if factors.len() <= 1 {
return vec![PrimaryComponent {
primary: gb.basis.clone(),
prime: ideal_radical(&gb.basis).basis,
}];
}
let domain = RationalDomain;
let factor_polys: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = factors
.iter()
.map(|f| {
let terms: Vec<(Vec<usize>, Rational)> = f
.coeffs()
.iter()
.enumerate()
.filter(|(_, c)| !domain.is_zero(c))
.map(|(i, c)| {
let mut exp = vec![0usize; n_vars];
exp[0] = i;
(exp, c.clone())
})
.collect();
SparseMultivariatePolynomial::from_terms(domain, n_vars, terms)
})
.collect();
let mut components = Vec::new();
for (i, fi) in factor_polys.iter().enumerate() {
let mut saturated = GroebnerBasis {
basis: gb.basis.clone(),
};
for (j, fj) in factor_polys.iter().enumerate() {
if i == j {
continue;
}
saturated = ideal_saturate(&saturated.basis, std::slice::from_ref(fj));
}
let mut prime_gens = gb.basis.clone();
prime_gens.push(fi.clone());
let prime_gb = groebner_basis(&prime_gens, Algorithm::Auto);
components.push(PrimaryComponent {
primary: saturated.basis,
prime: prime_gb.basis,
});
}
components
}
pub fn is_prime_ideal(generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>]) -> bool {
if generators.is_empty() {
return false;
}
let gb = groebner_basis(generators, Algorithm::Auto);
if gb.basis.is_empty() {
return false;
}
if gb.basis.len() == 1
&& gb.basis[0]
.leading_monomial()
.map(|lm| lm.iter().all(|&e| e == 0))
.unwrap_or(false)
{
return false;
}
if is_zero_dimensional(&gb) {
is_prime_zero_dim(&gb)
} else {
false
}
}
fn is_prime_zero_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> bool {
let n_vars = match gb.basis.first() {
Some(p) => p.n_vars(),
None => return false,
};
for var in 0..n_vars {
let univariate = gb.basis.iter().find(|p| {
p.terms_ref()
.keys()
.all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
&& p.degree_in(var) > 0
});
if let Some(poly) = univariate {
let unipoly = extract_univariate(poly, var);
if let Some(deg) = unipoly.degree()
&& deg <= 3
{
let has_rational_root = check_rational_roots(&unipoly);
if has_rational_root && deg > 1 {
return false;
}
}
}
}
true
}
fn divisors_of(n: i64) -> Vec<i64> {
if n <= 0 {
return vec![];
}
let mut divs = Vec::new();
let mut i = 1i64;
while i * i <= n {
if n % i == 0 {
divs.push(i);
if i != n / i {
divs.push(n / i);
}
}
i += 1;
}
divs.sort_unstable();
divs
}
fn check_rational_roots(poly: &crate::dense::DenseUnivariatePolynomial<RationalDomain>) -> bool {
let Some(deg) = poly.degree() else {
return false;
};
if deg == 0 {
return false;
}
let coeffs = poly.coeffs();
let constant = &coeffs[0];
if RationalDomain.is_zero(constant) {
return true; }
let Some(lc) = poly.leading_coeff() else {
return false;
};
let p_divs = divisors_of(constant.numer().to_i64().unwrap_or(0).unsigned_abs() as i64);
let q_divs = divisors_of(lc.numer().to_i64().unwrap_or(0).unsigned_abs() as i64);
if p_divs.is_empty() || q_divs.is_empty() {
let one = ocas_domain::Rational::new(1, 1);
let neg_one = ocas_domain::Rational::new(-1, 1);
return RationalDomain.is_zero(&poly.eval(&one))
|| RationalDomain.is_zero(&poly.eval(&neg_one));
}
for &p in &p_divs {
for &q in &q_divs {
let candidate = ocas_domain::Rational::new(p, q);
if RationalDomain.is_zero(&poly.eval(&candidate)) {
return true;
}
let neg_candidate = ocas_domain::Rational::new(-p, q);
if RationalDomain.is_zero(&poly.eval(&neg_candidate)) {
return true;
}
}
}
false
}
pub fn is_primary_ideal(generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>]) -> bool {
let decomp = primary_decomposition(generators);
decomp.len() <= 1
}
#[cfg(test)]
mod tests {
use super::*;
use crate::groebner_basis;
use ocas_domain::{Rational, RationalDomain};
fn r(n: i64, d: i64) -> Rational {
Rational::new(n, d)
}
fn x() -> SparseMultivariatePolynomial<RationalDomain, Lex> {
SparseMultivariatePolynomial::from_terms(RationalDomain, 2, vec![(vec![1, 0], r(1, 1))])
}
fn y() -> SparseMultivariatePolynomial<RationalDomain, Lex> {
SparseMultivariatePolynomial::from_terms(RationalDomain, 2, vec![(vec![0, 1], r(1, 1))])
}
#[test]
fn contains_basic() {
assert!(ideal_contains(&[x(), y()], &x(), Algorithm::Auto));
}
#[test]
fn contains_negative() {
assert!(!ideal_contains(&[y()], &x(), Algorithm::Auto));
}
#[test]
fn sum_xy() {
let gb = ideal_sum(&[x()], &[y()]);
assert!(gb.basis.len() >= 2);
}
#[test]
fn product_xy() {
let gb = ideal_product(&[x()], &[y()]);
assert_eq!(gb.basis.len(), 1);
}
#[test]
fn quotient_x2_xy_by_x() {
let d = RationalDomain;
let x2 =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 0], r(1, 1))]);
let xy =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 1], r(1, 1))]);
let g = x();
let gb = ideal_quotient(&[x2, xy], &[g]);
assert!(!gb.basis.is_empty());
assert!(ideal_contains(&gb.basis, &x(), Algorithm::Auto));
}
#[test]
fn intersection_x_y() {
let gb = ideal_intersection(&[x()], &[y()]);
assert_eq!(gb.basis.len(), 1);
let xy_exp = vec![1usize, 1];
let has_xy = gb
.basis
.iter()
.any(|p| p.terms_ref().len() == 1 && p.terms_ref().contains_key(xy_exp.as_slice()));
assert!(has_xy, "expected xy in intersection basis");
}
#[test]
fn saturate_x2y_xy2_by_x() {
let d = RationalDomain;
let f1 =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 1], r(1, 1))]);
let f2 =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 2], r(1, 1))]);
let g = x();
let gb = ideal_saturate(&[f1, f2], &[g]);
assert!(!gb.basis.is_empty());
assert!(ideal_contains(&gb.basis, &y(), Algorithm::Auto));
}
#[test]
fn is_zero_dim_positive() {
let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![(vec![2, 0], r(1, 1)), (vec![0, 0], r(-1, 1))],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![(vec![0, 1], r(1, 1)), (vec![1, 0], r(-1, 1))],
);
let gb = groebner_basis(&[f1, f2], Algorithm::F4);
assert!(is_zero_dimensional(&gb));
}
#[test]
fn is_zero_dim_negative() {
let d = RationalDomain;
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))],
);
let gb = groebner_basis(&[f], Algorithm::F4);
assert!(!is_zero_dimensional(&gb));
}
#[test]
fn solve_circle_line() {
let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![
(vec![2, 0], r(1, 1)),
(vec![0, 2], r(1, 1)),
(vec![0, 0], r(-1, 1)),
],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))],
);
let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
match sol {
PolynomialSystemSolution::ZeroDimensional(z) => {
assert_eq!(z.solutions.len(), 2);
for s in &z.solutions {
let x_val = s.values[0];
let y_val = s.values[1];
assert!((x_val - y_val).abs() < 1e-10, "x should equal y");
assert!(
(x_val * x_val + y_val * y_val - 1.0).abs() < 1e-10,
"x² + y² should be 1"
);
}
}
_ => panic!("expected zero-dimensional"),
}
}
#[test]
fn solve_empty_variety() {
let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![
(vec![2, 0], r(1, 1)),
(vec![0, 2], r(1, 1)),
(vec![0, 0], r(-1, 1)),
],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![
(vec![2, 0], r(1, 1)),
(vec![0, 2], r(1, 1)),
(vec![0, 0], r(-2, 1)),
],
);
let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
assert!(matches!(sol, PolynomialSystemSolution::Empty));
}
#[test]
fn radical_x2_y2() {
let d = RationalDomain;
let f1 =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 0], r(1, 1))]);
let f2 =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![0, 2], r(1, 1))]);
let rad = ideal_radical(&[f1, f2]);
let x_poly =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 0], r(1, 1))]);
let y_poly =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![0, 1], r(1, 1))]);
assert!(ideal_contains(&rad.basis, &x_poly, Algorithm::Auto));
assert!(ideal_contains(&rad.basis, &y_poly, Algorithm::Auto));
}
#[test]
fn radical_of_prime_is_self() {
let d = RationalDomain;
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
1,
vec![(vec![2], r(1, 1)), (vec![0], r(-2, 1))],
);
let rad = ideal_radical(std::slice::from_ref(&f));
assert!(ideal_contains(&rad.basis, &f, Algorithm::Auto));
}
#[test]
fn primary_decomp_x2_xy() {
let d = RationalDomain;
let f1 =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 0], r(1, 1))]);
let f2 =
SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 1], r(1, 1))]);
let decomp = primary_decomposition(&[f1, f2]);
assert!(!decomp.is_empty());
for comp in &decomp {
assert!(!comp.primary.is_empty());
assert!(!comp.prime.is_empty());
}
}
#[test]
fn is_prime_x2_minus_2() {
let d = RationalDomain;
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
1,
vec![(vec![2], r(1, 1)), (vec![0], r(-2, 1))],
);
assert!(is_prime_ideal(&[f]));
}
#[test]
fn is_primary_x2() {
let d = RationalDomain;
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 1, vec![(vec![2], r(1, 1))]);
assert!(is_primary_ideal(&[f]));
}
}