use smallvec::SmallVec;
use ocas_core::FastHashMap as HashMap;
use ocas_core::FastHashSet as HashSet;
use ocas_domain::{Domain, FiniteField};
use super::GroebnerBasis;
use crate::sparse::{MonomialOrder, SparseMultivariatePolynomial, monomial_divides, monomial_lcm};
#[derive(Debug, Clone)]
pub(super) struct CriticalPair {
pub(super) idx1: usize,
pub(super) idx2: usize,
pub(super) lcm: SmallVec<[usize; 4]>,
pub(super) degree: usize,
}
pub(super) trait BasisPoly: Clone {
fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>>;
fn n_vars(&self) -> usize;
fn n_terms(&self) -> usize;
fn mul_monomial(&self, exp: &[usize]) -> Self;
}
impl<D: Domain, O: MonomialOrder> BasisPoly for SparseMultivariatePolynomial<D, O> {
fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>> {
SparseMultivariatePolynomial::leading_monomial(self)
}
fn n_vars(&self) -> usize {
SparseMultivariatePolynomial::n_vars(self)
}
fn n_terms(&self) -> usize {
SparseMultivariatePolynomial::n_terms(self)
}
fn mul_monomial(&self, exp: &[usize]) -> Self {
SparseMultivariatePolynomial::mul_monomial(self, exp)
}
}
impl CriticalPair {
fn new<P: BasisPoly>(basis: &[P], i: usize, j: usize) -> Option<Self> {
let lm_i = basis[i].leading_monomial()?;
let lm_j = basis[j].leading_monomial()?;
let lcm = monomial_lcm(lm_i, lm_j);
let degree: usize = lcm.iter().sum();
Some(Self {
idx1: i,
idx2: j,
lcm,
degree,
})
}
}
pub(super) type SimpCache<P> = Vec<(SmallVec<[usize; 4]>, P)>;
#[derive(Debug, Clone)]
struct MonomialData {
column: usize,
}
fn support_mask(exp: &[usize]) -> u64 {
let mut mask = 0u64;
for (v, &e) in exp.iter().enumerate() {
if e > 0 && v < 64 {
mask |= 1 << v;
}
}
mask
}
struct DivisorIndex {
buckets: HashMap<u64, Vec<usize>>,
}
impl DivisorIndex {
fn new() -> Self {
Self {
buckets: HashMap::default(),
}
}
fn push(&mut self, lm: &[usize], idx: usize) {
self.buckets.entry(support_mask(lm)).or_default().push(idx);
}
}
fn find_reducer<P: BasisPoly>(index: &DivisorIndex, basis: &[P], exp: &[usize]) -> Option<usize> {
let mask = support_mask(exp);
let mut best: Option<usize> = None;
let mut sub = mask;
loop {
if let Some(ids) = index.buckets.get(&sub) {
for &bi in ids {
if let Some(blm) = basis[bi].leading_monomial()
&& monomial_divides(exp, blm)
{
match best {
Some(b) if basis[b].n_terms() <= basis[bi].n_terms() => {}
_ => best = Some(bi),
}
}
}
}
if sub == 0 {
break;
}
sub = (sub - 1) & mask;
}
best
}
pub fn f4<D: Domain + 'static, O: MonomialOrder>(
ideal: &[SparseMultivariatePolynomial<D, O>],
) -> GroebnerBasis<D, O> {
if ideal.is_empty() {
return GroebnerBasis { basis: vec![] };
}
if std::any::TypeId::of::<D>() == std::any::TypeId::of::<FiniteField>() {
let domain_ptr = ideal[0].domain() as *const D;
let ff = unsafe { &*domain_ptr.cast::<FiniteField>() };
return f4_fp(ideal, ff.prime_u64() as i64);
}
let mut initial: Vec<SparseMultivariatePolynomial<D, O>> =
ideal.iter().filter(|p| !p.is_zero()).cloned().collect();
for p in &mut initial {
make_monic(p);
}
if initial.is_empty() {
return GroebnerBasis { basis: vec![] };
}
let order = ideal[0].order.clone();
let mut basis: Vec<SparseMultivariatePolynomial<D, O>> = Vec::new();
let mut pairs: Vec<CriticalPair> = Vec::new();
let mut simplifications: Vec<SimpCache<SparseMultivariatePolynomial<D, O>>> = Vec::new();
let mut div_index = DivisorIndex::new();
let mut basis_lm_set: HashSet<SmallVec<[usize; 4]>> = HashSet::default();
for p in initial {
update_pairs(&mut basis, &mut pairs, &mut simplifications, p);
let idx = basis.len() - 1;
if let Some(lm) = basis[idx].leading_monomial() {
div_index.push(lm, idx);
basis_lm_set.insert(lm.clone());
}
}
let mut all_monomials: HashMap<SmallVec<[usize; 4]>, MonomialData> = HashMap::default();
let mut monomial_list: Vec<SmallVec<[usize; 4]>> = Vec::new();
let mut matrix: Vec<Vec<(D::Element, usize)>> = Vec::new();
let mut pivots: Vec<Option<usize>> = Vec::new();
let mut input_heads: HashSet<SmallVec<[usize; 4]>> = HashSet::default();
let mut seen_rows: HashSet<(usize, SmallVec<[usize; 4]>)> = HashSet::default();
let mut worklist: Vec<SmallVec<[usize; 4]>> = Vec::new();
while !pairs.is_empty() {
let min_deg = pairs.iter().map(|cp| cp.degree).min().unwrap();
let selected: Vec<CriticalPair> = pairs
.iter()
.filter(|cp| cp.degree == min_deg)
.cloned()
.collect();
let sel_set: std::collections::HashSet<(usize, usize)> =
selected.iter().map(|cp| (cp.idx1, cp.idx2)).collect();
pairs.retain(|cp| !sel_set.contains(&(cp.idx1, cp.idx2)));
if selected.is_empty() {
continue;
}
all_monomials.clear();
monomial_list.clear();
matrix.clear();
input_heads.clear();
seen_rows.clear();
worklist.clear();
for cp in &selected {
let i = cp.idx1;
let j = cp.idx2;
let lm_i = basis[i].leading_monomial().unwrap();
let lm_j = basis[j].leading_monomial().unwrap();
let lcm_exp = &cp.lcm;
let diff_i: SmallVec<[usize; 4]> = lcm_exp
.iter()
.zip(lm_i.iter())
.map(|(&a, b)| a - b)
.collect();
let diff_j: SmallVec<[usize; 4]> = lcm_exp
.iter()
.zip(lm_j.iter())
.map(|(&a, b)| a - b)
.collect();
input_heads.insert(lcm_exp.clone());
for (idx, diff) in [(i, &diff_i), (j, &diff_j)] {
if seen_rows.insert((idx, diff.clone())) {
let mult = get_simplified(&simplifications[idx], diff, &basis[idx]);
add_poly_to_matrix(
&mult,
&mut matrix,
&mut all_monomials,
&mut monomial_list,
&mut worklist,
);
}
}
}
if matrix.is_empty() {
continue;
}
while let Some(exp) = worklist.pop() {
if let Some(bi) = find_reducer(&div_index, &basis, &exp) {
let blm = basis[bi].leading_monomial().unwrap();
let diff: SmallVec<[usize; 4]> =
exp.iter().zip(blm.iter()).map(|(a, b)| a - b).collect();
let reducer = get_simplified(&simplifications[bi], &diff, &basis[bi]);
input_heads.insert(exp.clone());
add_poly_to_matrix(
&reducer,
&mut matrix,
&mut all_monomials,
&mut monomial_list,
&mut worklist,
);
}
}
if matrix.is_empty() || monomial_list.is_empty() {
continue;
}
let ncols = monomial_list.len();
let mut col_order: Vec<usize> = (0..ncols).collect();
col_order.sort_unstable_by(|&a, &b| order.cmp(&monomial_list[b], &monomial_list[a]));
let mut col_inv = vec![0usize; ncols];
for (new_col, &old_col) in col_order.iter().enumerate() {
col_inv[old_col] = new_col;
}
for row in &mut matrix {
for (_, col) in row.iter_mut() {
*col = col_inv[*col];
}
}
let mut sorted_monomials: Vec<SmallVec<[usize; 4]>> = vec![SmallVec::new(); ncols];
for (new_col, &old_col) in col_order.iter().enumerate() {
sorted_monomials[new_col] = monomial_list[old_col].clone();
}
echelonize_generic(&mut matrix, ncols, basis[0].domain(), &mut pivots);
for row in &matrix {
if row.is_empty() {
continue;
}
let row_lm = &sorted_monomials[row[0].1];
if input_heads.contains(row_lm) {
continue;
}
let mut poly = basis[0].zero();
for (coeff, col) in row.iter().rev() {
poly.append_monomial(coeff.clone(), &sorted_monomials[*col]);
}
if poly.is_zero() {
continue;
}
let new_lm = poly.leading_monomial().unwrap().clone();
if basis_lm_set.contains(&new_lm) {
continue;
}
update_pairs(&mut basis, &mut pairs, &mut simplifications, poly);
let idx = basis.len() - 1;
if let Some(lm) = basis[idx].leading_monomial() {
div_index.push(lm, idx);
basis_lm_set.insert(lm.clone());
}
}
}
GroebnerBasis { basis }.minimize().auto_reduce()
}
pub(super) fn update_pairs<P: BasisPoly>(
basis: &mut Vec<P>,
pairs: &mut Vec<CriticalPair>,
simplifications: &mut Vec<SimpCache<P>>,
new_poly: P,
) {
let new_lm = match new_poly.leading_monomial() {
Some(m) => m.clone(),
None => {
basis.push(new_poly);
return;
}
};
let new_idx = basis.len();
basis.push(new_poly);
simplifications.push(vec![(
SmallVec::from_elem(0, basis[new_idx].n_vars()),
basis[new_idx].clone(),
)]);
let is_disjoint = |cp: &CriticalPair| {
let a = basis[cp.idx1].leading_monomial().unwrap();
let b = basis[cp.idx2].leading_monomial().unwrap();
a.iter().zip(b.iter()).all(|(x, y)| *x == 0 || *y == 0)
};
let mut new_pairs: Vec<(CriticalPair, bool)> = (0..new_idx)
.filter_map(|i| CriticalPair::new(basis, i, new_idx))
.map(|cp| {
let disjoint = is_disjoint(&cp);
(cp, disjoint)
})
.collect();
for i in 0..new_pairs.len() {
new_pairs[i].1 = false;
let disjoint = is_disjoint(&new_pairs[i].0);
let survive = disjoint
|| new_pairs.iter().all(|p2| {
!p2.1
|| new_pairs[i]
.0
.lcm
.iter()
.zip(p2.0.lcm.iter())
.any(|(a, b)| a < b)
});
new_pairs[i].1 = survive;
}
let kept: Vec<CriticalPair> = new_pairs
.into_iter()
.filter(|(cp, k)| *k && !is_disjoint(cp))
.map(|(cp, _)| cp)
.collect();
pairs.retain(|cp| {
let new_divides = cp.lcm.iter().zip(new_lm.iter()).all(|(a, b)| a >= b);
if !new_divides {
return true;
}
let lm1 = basis[cp.idx1].leading_monomial().unwrap();
let lm2 = basis[cp.idx2].leading_monomial().unwrap();
let same1 = lm1
.iter()
.zip(new_lm.iter())
.zip(cp.lcm.iter())
.all(|((a, b), c)| (*a).max(*b) == *c);
let same2 = lm2
.iter()
.zip(new_lm.iter())
.zip(cp.lcm.iter())
.all(|((a, b), c)| (*a).max(*b) == *c);
same1 || same2
});
pairs.extend(kept);
}
fn add_poly_to_matrix<D: Domain, O: MonomialOrder>(
poly: &SparseMultivariatePolynomial<D, O>,
matrix: &mut Vec<Vec<(D::Element, usize)>>,
monomial_map: &mut HashMap<SmallVec<[usize; 4]>, MonomialData>,
monomial_list: &mut Vec<SmallVec<[usize; 4]>>,
worklist: &mut Vec<SmallVec<[usize; 4]>>,
) {
let mut row: Vec<(D::Element, usize)> = Vec::new();
for (exp, coeff) in poly.sorted_terms().iter().rev() {
if poly.domain().is_zero(coeff) {
continue;
}
let mut new_col = None;
let md = monomial_map.entry((*exp).clone()).or_insert_with(|| {
let idx = monomial_list.len();
monomial_list.push((*exp).clone());
new_col = Some(idx);
MonomialData { column: idx }
});
if new_col.is_some() {
worklist.push((*exp).clone());
}
row.push(((*coeff).clone(), md.column));
}
if !row.is_empty() {
matrix.push(row);
}
}
pub(super) fn get_simplified<P: BasisPoly>(
cache: &SimpCache<P>,
diff: &[usize],
basis_poly: &P,
) -> P {
for (cached_diff, cached_poly) in cache.iter().rev() {
if cached_diff.as_slice() == diff {
return cached_poly.clone();
}
}
for (cached_diff, cached_poly) in cache.iter().rev() {
if diff.iter().zip(cached_diff.iter()).all(|(d, c)| d >= c) {
let remaining: SmallVec<[usize; 4]> = diff
.iter()
.zip(cached_diff.iter())
.map(|(d, c)| d - c)
.collect();
return cached_poly.mul_monomial(&remaining);
}
}
basis_poly.mul_monomial(diff)
}
#[inline]
pub(super) fn norm_mod(a: i64, p: i64) -> i64 {
let r = a % p;
if r < 0 { r + p } else { r }
}
#[derive(Debug, Clone)]
pub(super) struct FpPoly {
pub(super) terms: Vec<(SmallVec<[usize; 4]>, i64)>,
pub(super) n_vars: usize,
}
impl FpPoly {
pub(super) fn zero(n_vars: usize) -> Self {
Self {
terms: Vec::new(),
n_vars,
}
}
pub(super) fn is_zero(&self) -> bool {
self.terms.is_empty()
}
pub(super) fn n_terms(&self) -> usize {
self.terms.len()
}
pub(super) fn from_domain<D: Domain + 'static, O: MonomialOrder>(
p: &SparseMultivariatePolynomial<D, O>,
prime: i64,
) -> Self {
let mut terms: Vec<(SmallVec<[usize; 4]>, i64)> = Vec::with_capacity(p.n_terms());
for (exp, coeff) in p.sorted_terms().iter().rev() {
let c = norm_mod(domain_to_i64_fp::<D>(coeff, prime), prime);
if c != 0 {
terms.push(((*exp).clone(), c));
}
}
Self {
terms,
n_vars: p.n_vars(),
}
}
pub(super) fn to_domain<D: Domain + 'static, O: MonomialOrder>(
&self,
domain: &D,
prime: i64,
) -> SparseMultivariatePolynomial<D, O> {
let mut poly = SparseMultivariatePolynomial::new(domain.clone(), self.n_vars);
for (exp, c) in &self.terms {
poly.append_monomial(i64_to_domain_fp::<D>(domain, *c, prime), exp);
}
poly
}
pub(super) fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>> {
self.terms.first().map(|t| &t.0)
}
pub(super) fn n_vars(&self) -> usize {
self.n_vars
}
pub(super) fn mul_monomial(&self, exp: &[usize]) -> Self {
Self {
terms: self
.terms
.iter()
.map(|(e, c)| {
(
e.iter()
.zip(exp.iter())
.map(|(a, b)| a + b)
.collect::<SmallVec<[usize; 4]>>(),
*c,
)
})
.collect(),
n_vars: self.n_vars,
}
}
}
impl BasisPoly for FpPoly {
fn leading_monomial(&self) -> Option<&SmallVec<[usize; 4]>> {
self.terms.first().map(|t| &t.0)
}
fn n_vars(&self) -> usize {
self.n_vars
}
fn n_terms(&self) -> usize {
FpPoly::n_terms(self)
}
fn mul_monomial(&self, exp: &[usize]) -> Self {
Self {
terms: self
.terms
.iter()
.map(|(e, c)| {
(
e.iter()
.zip(exp.iter())
.map(|(a, b)| a + b)
.collect::<SmallVec<[usize; 4]>>(),
*c,
)
})
.collect(),
n_vars: self.n_vars,
}
}
}
pub(super) fn monic_fp(p: &mut FpPoly, prime: i64) {
if let Some(&(_, lc)) = p.terms.first()
&& lc != 1
{
let inv = mod_inv(lc, prime);
for (_, c) in &mut p.terms {
*c = norm_mod(*c * inv, prime);
}
}
}
fn register_row_fp(
basis_idx: usize,
diff: &SmallVec<[usize; 4]>,
basis: &[FpPoly],
simplifications: &[SimpCache<FpPoly>],
row_store: &mut Vec<FpPoly>,
row_cache: &mut HashMap<(usize, SmallVec<[usize; 4]>), usize>,
) -> Option<usize> {
let key = (basis_idx, diff.clone());
if let Some(&rs) = row_cache.get(&key) {
return Some(rs);
}
let poly = get_simplified(&simplifications[basis_idx], diff, &basis[basis_idx]);
if poly.is_zero() {
return None;
}
let rs = row_store.len();
row_store.push(poly);
row_cache.insert(key, rs);
Some(rs)
}
#[allow(clippy::too_many_lines)]
fn f4_fp<D: Domain + 'static, O: MonomialOrder>(
ideal: &[SparseMultivariatePolynomial<D, O>],
prime: i64,
) -> GroebnerBasis<D, O> {
let n_vars = ideal[0].n_vars();
let order = ideal[0].order.clone();
let mut initial: Vec<FpPoly> = ideal
.iter()
.filter(|p| !p.is_zero())
.map(|p| FpPoly::from_domain(p, prime))
.collect();
for p in &mut initial {
monic_fp(p, prime);
}
if initial.is_empty() {
return GroebnerBasis { basis: vec![] };
}
let mut basis: Vec<FpPoly> = Vec::new();
let mut pairs: Vec<CriticalPair> = Vec::new();
let mut simplifications: Vec<SimpCache<FpPoly>> = Vec::new();
let mut div_index = DivisorIndex::new();
let mut basis_lm_set: HashSet<SmallVec<[usize; 4]>> = HashSet::default();
for p in initial {
update_pairs(&mut basis, &mut pairs, &mut simplifications, p);
let idx = basis.len() - 1;
if let Some(lm) = basis[idx].leading_monomial() {
div_index.push(lm, idx);
basis_lm_set.insert(lm.clone());
}
}
let mut all_monomials: HashMap<SmallVec<[usize; 4]>, MonomialData> = HashMap::default();
let mut monomial_list: Vec<SmallVec<[usize; 4]>> = Vec::new();
let mut matrix: Vec<Vec<(i64, usize)>> = Vec::new();
let mut pivots: Vec<Option<usize>> = Vec::new();
let mut input_heads: HashSet<SmallVec<[usize; 4]>> = HashSet::default();
let mut seen_rows: HashSet<(usize, SmallVec<[usize; 4]>)> = HashSet::default();
let mut worklist: Vec<SmallVec<[usize; 4]>> = Vec::new();
let mut seen_monomials: HashSet<SmallVec<[usize; 4]>> = HashSet::default();
let mut row_store: Vec<FpPoly> = Vec::new();
let mut row_cache: HashMap<(usize, SmallVec<[usize; 4]>), usize> = HashMap::default();
let mut round_rows: Vec<usize> = Vec::new();
let stats = std::env::var("OCAS_F4_STATS").is_ok();
let mut rounds = 0usize;
let mut added = 0usize;
let mut t_build = std::time::Duration::ZERO;
let mut t_pre = std::time::Duration::ZERO;
let mut t_ech = std::time::Duration::ZERO;
let mut t_ext = std::time::Duration::ZERO;
let round_stats = std::env::var("OCAS_F4_ROUND_STATS").is_ok();
while !pairs.is_empty() {
rounds += 1;
let t0 = std::time::Instant::now();
let min_deg = pairs.iter().map(|cp| cp.degree).min().unwrap();
let selected: Vec<CriticalPair> = pairs
.iter()
.filter(|cp| cp.degree == min_deg)
.cloned()
.collect();
let sel_set: std::collections::HashSet<(usize, usize)> =
selected.iter().map(|cp| (cp.idx1, cp.idx2)).collect();
pairs.retain(|cp| !sel_set.contains(&(cp.idx1, cp.idx2)));
if selected.is_empty() {
continue;
}
all_monomials.clear();
monomial_list.clear();
matrix.clear();
input_heads.clear();
seen_rows.clear();
worklist.clear();
seen_monomials.clear();
round_rows.clear();
for cp in &selected {
let i = cp.idx1;
let j = cp.idx2;
let lm_i = basis[i].leading_monomial().unwrap();
let lm_j = basis[j].leading_monomial().unwrap();
let lcm_exp = &cp.lcm;
let diff_i: SmallVec<[usize; 4]> = lcm_exp
.iter()
.zip(lm_i.iter())
.map(|(&a, b)| a - b)
.collect();
let diff_j: SmallVec<[usize; 4]> = lcm_exp
.iter()
.zip(lm_j.iter())
.map(|(&a, b)| a - b)
.collect();
input_heads.insert(lcm_exp.clone());
for (idx, diff) in [(i, &diff_i), (j, &diff_j)] {
if seen_rows.insert((idx, diff.clone()))
&& let Some(rs) = register_row_fp(
idx,
diff,
&basis,
&simplifications,
&mut row_store,
&mut row_cache,
)
{
round_rows.push(rs);
}
}
}
if round_rows.is_empty() {
continue;
}
t_build += t0.elapsed();
let t1 = std::time::Instant::now();
for &rs in &round_rows {
for (exp, _) in &row_store[rs].terms {
if seen_monomials.insert(exp.clone()) {
worklist.push(exp.clone());
}
}
}
while let Some(exp) = worklist.pop() {
if let Some(bi) = find_reducer(&div_index, &basis, &exp) {
let blm = basis[bi].leading_monomial().unwrap();
let diff: SmallVec<[usize; 4]> =
exp.iter().zip(blm.iter()).map(|(a, b)| a - b).collect();
input_heads.insert(exp);
if let Some(rs) = register_row_fp(
bi,
&diff,
&basis,
&simplifications,
&mut row_store,
&mut row_cache,
) {
round_rows.push(rs);
for (mexp, _) in &row_store[rs].terms {
if seen_monomials.insert(mexp.clone()) {
worklist.push(mexp.clone());
}
}
}
}
}
if round_rows.is_empty() {
continue;
}
t_pre += t1.elapsed();
let t2 = std::time::Instant::now();
for &rs in &round_rows {
let poly = &row_store[rs];
let mut row: Vec<(i64, usize)> = Vec::with_capacity(poly.terms.len());
for (exp, coeff) in &poly.terms {
let col = all_monomials.entry(exp.clone()).or_insert_with(|| {
let idx = monomial_list.len();
monomial_list.push(exp.clone());
MonomialData { column: idx }
});
row.push((*coeff, col.column));
}
if !row.is_empty() {
matrix.push(row);
}
}
let ncols = monomial_list.len();
let mut col_order: Vec<usize> = (0..ncols).collect();
col_order.sort_unstable_by(|&a, &b| order.cmp(&monomial_list[b], &monomial_list[a]));
let mut col_inv = vec![0usize; ncols];
for (new_col, &old_col) in col_order.iter().enumerate() {
col_inv[old_col] = new_col;
}
for row in &mut matrix {
for (_, col) in row.iter_mut() {
*col = col_inv[*col];
}
}
let mut sorted_monomials: Vec<SmallVec<[usize; 4]>> = vec![SmallVec::new(); ncols];
for (new_col, &old_col) in col_order.iter().enumerate() {
sorted_monomials[new_col] = monomial_list[old_col].clone();
}
echelonize_fp(&mut matrix, ncols, prime, &mut pivots);
if round_stats {
let nnz: usize = matrix.iter().map(Vec::len).sum();
let max_len = matrix.iter().map(Vec::len).max().unwrap_or(0);
eprintln!(
" round {rounds}: rows={} cols={ncols} nnz={nnz} maxlen={max_len} sel={}",
matrix.len(),
selected.len()
);
}
t_ech += t2.elapsed();
let t3 = std::time::Instant::now();
for row in &matrix {
if row.is_empty() {
continue;
}
let row_lm = &sorted_monomials[row[0].1];
if input_heads.contains(row_lm) {
continue;
}
let mut poly = FpPoly::zero(n_vars);
for &(c, col) in row {
let v = norm_mod(c, prime);
if v != 0 {
poly.terms.push((sorted_monomials[col].clone(), v));
}
}
if poly.is_zero() {
continue;
}
let new_lm = poly.leading_monomial().unwrap().clone();
if basis_lm_set.contains(&new_lm) {
continue;
}
update_pairs(&mut basis, &mut pairs, &mut simplifications, poly);
let idx = basis.len() - 1;
if let Some(lm) = basis[idx].leading_monomial() {
div_index.push(lm, idx);
basis_lm_set.insert(lm.clone());
}
added += 1;
}
t_ext += t3.elapsed();
}
if stats {
eprintln!(
"f4_fp stats: rounds={rounds} added={added} | build={t_build:.2?} pre={t_pre:.2?} echelon={t_ech:.2?} extract={t_ext:.2?}"
);
}
let domain = ideal[0].domain().clone();
let basis_d: Vec<SparseMultivariatePolynomial<D, O>> = basis
.iter()
.map(|p| p.to_domain::<D, O>(&domain, prime))
.collect();
GroebnerBasis { basis: basis_d }.minimize().auto_reduce()
}
#[allow(clippy::needless_range_loop)]
fn echelonize_fp(
matrix: &mut Vec<Vec<(i64, usize)>>,
ncols: usize,
prime: i64,
pivots: &mut Vec<Option<usize>>,
) {
let p = prime;
sort_rows(matrix);
pivots.clear();
pivots.resize(ncols, None);
for r in 0..matrix.len() {
if matrix[r].is_empty() {
continue;
}
let col = matrix[r][0].1;
if pivots[col].is_none() {
pivots[col] = Some(r);
if matrix[r][0].0 != 1 {
let inv = mod_inv(matrix[r][0].0, p);
for (c, _) in &mut matrix[r] {
*c = (*c * inv) % p;
}
}
}
}
let mut scratch: Vec<(i64, usize)> = Vec::new();
for r in 0..matrix.len() {
if matrix[r].is_empty() {
continue;
}
if pivots[matrix[r][0].1] == Some(r) {
continue;
}
let mut row = std::mem::take(&mut matrix[r]);
loop {
if row.is_empty() {
break;
}
let head_col = row[0].1;
match pivots[head_col] {
Some(pi) if pi != r => {
let c = row[0].0;
sub_scaled_fp(&mut row, &matrix[pi], c, p, &mut scratch);
}
Some(_) => break,
None => {
if row[0].0 != 1 {
let inv = mod_inv(row[0].0, p);
for (c, _) in &mut row {
*c = (*c * inv) % p;
}
}
pivots[head_col] = Some(r);
break;
}
}
}
matrix[r] = row;
}
matrix.retain(|r| !r.is_empty());
}
fn sub_scaled_fp(
row: &mut Vec<(i64, usize)>,
pivot: &[(i64, usize)],
c: i64,
p: i64,
scratch: &mut Vec<(i64, usize)>,
) {
scratch.clear();
scratch.reserve(row.len() + pivot.len());
let mut i = 1;
let mut j = 1;
while i < row.len() && j < pivot.len() {
let (rc, rcol) = row[i];
let (pc, pcol) = pivot[j];
if rcol < pcol {
scratch.push((rc, rcol));
i += 1;
} else if rcol > pcol {
let v = norm_mod(-c * pc, p);
if v != 0 {
scratch.push((v, pcol));
}
j += 1;
} else {
let v = norm_mod(rc - c * pc, p);
if v != 0 {
scratch.push((v, rcol));
}
i += 1;
j += 1;
}
}
scratch.extend_from_slice(&row[i..]);
for &(pc, pcol) in &pivot[j..] {
let v = norm_mod(-c * pc, p);
if v != 0 {
scratch.push((v, pcol));
}
}
std::mem::swap(row, scratch);
}
#[allow(clippy::needless_range_loop)]
fn echelonize_generic<D: Domain>(
matrix: &mut Vec<Vec<(D::Element, usize)>>,
ncols: usize,
domain: &D,
pivots: &mut Vec<Option<usize>>,
) {
sort_rows(matrix);
pivots.clear();
pivots.resize(ncols, None);
for r in 0..matrix.len() {
if matrix[r].is_empty() {
continue;
}
let col = matrix[r][0].1;
if pivots[col].is_none() {
pivots[col] = Some(r);
let lc = matrix[r][0].0.clone();
if !domain.is_one(&lc)
&& let Some(inv) = domain.inv(&lc)
{
for (c, _) in &mut matrix[r] {
*c = domain.mul(c, &inv);
}
}
}
}
let mut scratch: Vec<(D::Element, usize)> = Vec::new();
for r in 0..matrix.len() {
if matrix[r].is_empty() {
continue;
}
if pivots[matrix[r][0].1] == Some(r) {
continue;
}
let mut row = std::mem::take(&mut matrix[r]);
loop {
if row.is_empty() {
break;
}
let head_col = row[0].1;
match pivots[head_col] {
Some(pi) if pi != r => {
let c = row[0].0.clone();
sub_scaled_generic(domain, &mut row, &matrix[pi], &c, &mut scratch);
}
Some(_) => break,
None => {
let lc = row[0].0.clone();
if !domain.is_one(&lc)
&& let Some(inv) = domain.inv(&lc)
{
for (c, _) in &mut row {
*c = domain.mul(c, &inv);
}
}
pivots[head_col] = Some(r);
break;
}
}
}
matrix[r] = row;
}
matrix.retain(|r| !r.is_empty());
}
fn sub_scaled_generic<D: Domain>(
domain: &D,
row: &mut Vec<(D::Element, usize)>,
pivot: &[(D::Element, usize)],
c: &D::Element,
scratch: &mut Vec<(D::Element, usize)>,
) {
scratch.clear();
let mut i = 1;
let mut j = 1;
while i < row.len() && j < pivot.len() {
if row[i].1 < pivot[j].1 {
scratch.push(row[i].clone());
i += 1;
} else if row[i].1 > pivot[j].1 {
let prod = domain.mul(&pivot[j].0, c);
let v = domain.sub(&domain.zero(), &prod);
if !domain.is_zero(&v) {
scratch.push((v, pivot[j].1));
}
j += 1;
} else {
let prod = domain.mul(&pivot[j].0, c);
let v = domain.sub(&row[i].0, &prod);
if !domain.is_zero(&v) {
scratch.push((v, row[i].1));
}
i += 1;
j += 1;
}
}
scratch.extend_from_slice(&row[i..]);
for (pc, pcol) in &pivot[j..] {
let prod = domain.mul(pc, c);
let v = domain.sub(&domain.zero(), &prod);
if !domain.is_zero(&v) {
scratch.push((v, *pcol));
}
}
std::mem::swap(row, scratch);
}
fn sort_rows<T>(matrix: &mut [Vec<(T, usize)>]) {
matrix.sort_unstable_by(|a, b| match (a.first(), b.first()) {
(Some((_, ca)), Some((_, cb))) => ca.cmp(cb).then(a.len().cmp(&b.len())),
(Some(_), None) => std::cmp::Ordering::Less,
(None, Some(_)) => std::cmp::Ordering::Greater,
(None, None) => std::cmp::Ordering::Equal,
});
}
pub(super) fn mod_inv(a: i64, p: i64) -> i64 {
let a = ((a % p) + p) % p;
if a == 0 {
return 0;
}
let (mut old_r, mut r) = (a, p);
let (mut old_s, mut s) = (1i64, 0i64);
while r != 0 {
let q = old_r / r;
let tmp = r;
r = old_r - q * r;
old_r = tmp;
let tmp = s;
s = old_s - q * s;
old_s = tmp;
}
((old_s % p) + p) % p
}
#[allow(clippy::collapsible_if)]
fn make_monic<D: Domain, O: MonomialOrder>(p: &mut SparseMultivariatePolynomial<D, O>) {
if p.is_zero() {
return;
}
if let Some(lc) = p.leading_coeff().cloned()
&& let Some(inv_lc) = p.domain().inv(&lc)
{
let terms: Vec<(Vec<usize>, D::Element)> = p
.terms_ref()
.iter()
.map(|(exp, coeff)| (exp.to_vec(), p.domain().mul(coeff, &inv_lc)))
.collect();
*p = SparseMultivariatePolynomial::from_terms(p.domain().clone(), p.n_vars(), terms);
}
}
pub(super) fn domain_to_i64_fp<D: Domain + 'static>(elem: &D::Element, prime: i64) -> i64 {
if std::any::TypeId::of::<D>() == std::any::TypeId::of::<FiniteField>() {
let ff_elem =
unsafe { &*(elem as *const D::Element as *const <FiniteField as Domain>::Element) };
let val = ff_elem.value();
let (_, digits) = val.to_u64_digits();
if digits.is_empty() {
0
} else {
(digits[0] as i64) % prime
}
} else {
0
}
}
pub(super) fn i64_to_domain_fp<D: Domain + 'static>(
domain: &D,
val: i64,
prime: i64,
) -> D::Element {
if std::any::TypeId::of::<D>() == std::any::TypeId::of::<FiniteField>() {
let ff_domain = unsafe { &*(domain as *const D as *const FiniteField) };
let v = ((val % prime) + prime) % prime;
let elem = ff_domain.element(num_bigint::BigInt::from(v));
unsafe {
(&*(&elem as *const <FiniteField as Domain>::Element as *const D::Element)).clone()
}
} else {
domain.zero()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::Lex;
use num_bigint::BigInt;
use ocas_domain::{FiniteField, Rational, RationalDomain};
fn rat(n: i64, d: i64) -> Rational {
Rational::new(n, d)
}
#[test]
fn f4_empty_ideal() {
let gb = f4::<RationalDomain, Lex>(&[]);
assert!(gb.basis.is_empty());
}
#[test]
fn f4_single_polynomial() {
let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
RationalDomain,
2,
vec![(vec![2, 0], rat(1, 1)), (vec![0, 0], rat(-1, 1))],
);
let gb = f4(&[f]);
assert_eq!(gb.basis.len(), 1);
}
#[test]
fn f4_linear_system() {
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
RationalDomain,
2,
vec![(vec![1, 0], rat(1, 1)), (vec![0, 1], rat(1, 1))],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
RationalDomain,
2,
vec![(vec![1, 0], rat(1, 1)), (vec![0, 1], rat(-1, 1))],
);
let gb = f4(&[f1, f2]);
assert!(gb.basis.len() >= 2, "expected >= 2, got {}", gb.basis.len());
assert!(gb.is_groebner_basis());
}
#[test]
fn f4_two_variable_ideal() {
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
RationalDomain,
2,
vec![(vec![2, 0], rat(1, 1)), (vec![0, 1], rat(-1, 1))],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
RationalDomain,
2,
vec![(vec![3, 0], rat(1, 1)), (vec![1, 0], rat(-1, 1))],
);
let gb = f4(&[f1, f2]);
assert!(gb.is_groebner_basis());
}
#[test]
fn f4_cyclic_3_zp() {
let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
3,
vec![
(vec![1, 0, 0], rat(1, 1)),
(vec![0, 1, 0], rat(1, 1)),
(vec![0, 0, 1], rat(1, 1)),
],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
3,
vec![
(vec![1, 1, 0], rat(1, 1)),
(vec![0, 1, 1], rat(1, 1)),
(vec![1, 0, 1], rat(1, 1)),
],
);
let f3 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
3,
vec![(vec![1, 1, 1], rat(1, 1)), (vec![0, 0, 0], rat(-1, 1))],
);
let gb = f4(&[f1, f2, f3]);
assert!(!gb.basis.is_empty());
assert!(gb.is_groebner_basis());
}
#[test]
fn f4_cyclic_3_fp13_matches_q() {
let field = FiniteField::new(BigInt::from(13u32));
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
3,
vec![
(vec![1, 0, 0], field.element(1)),
(vec![0, 1, 0], field.element(1)),
(vec![0, 0, 1], field.element(1)),
],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
3,
vec![
(vec![1, 1, 0], field.element(1)),
(vec![0, 1, 1], field.element(1)),
(vec![1, 0, 1], field.element(1)),
],
);
let f3 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
3,
vec![
(vec![1, 1, 1], field.element(1)),
(vec![0, 0, 0], field.element(12)),
],
);
let gb = f4(&[f1, f2, f3]);
assert!(!gb.basis.is_empty());
assert!(gb.is_groebner_basis());
assert_eq!(gb.basis.len(), 3);
}
#[test]
fn f4_cyclic_4_zp() {
let field = FiniteField::new(BigInt::from(13u32));
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
4,
vec![
(vec![1, 0, 0, 0], field.element(1)),
(vec![0, 1, 0, 0], field.element(1)),
(vec![0, 0, 1, 0], field.element(1)),
(vec![0, 0, 0, 1], field.element(1)),
],
);
let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
4,
vec![
(vec![1, 1, 0, 0], field.element(1)),
(vec![0, 1, 1, 0], field.element(1)),
(vec![0, 0, 1, 1], field.element(1)),
(vec![1, 0, 0, 1], field.element(1)),
],
);
let f3 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
4,
vec![
(vec![1, 1, 1, 0], field.element(1)),
(vec![0, 1, 1, 1], field.element(1)),
(vec![1, 0, 1, 1], field.element(1)),
(vec![1, 1, 0, 1], field.element(1)),
],
);
let f4_poly = SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
4,
vec![
(vec![1, 1, 1, 1], field.element(1)),
(vec![0, 0, 0, 0], field.element(12)),
],
);
let gb = f4(&[f1, f2, f3, f4_poly]);
assert!(!gb.basis.is_empty());
assert!(gb.is_groebner_basis());
}
#[test]
#[ignore = "timing test: ~55s per run"]
fn f4_cyclic_5_fp13_timing() {
let field = FiniteField::new(BigInt::from(13u32));
let n = 5;
let mut gens = Vec::new();
for k in 1..n {
let mut terms = Vec::new();
for start in 0..n {
let mut exps = vec![0usize; n];
for j in 0..k {
exps[(start + j) % n] = 1;
}
terms.push((exps, field.element(1)));
}
gens.push(SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
n,
terms,
));
}
let full_exps = vec![1usize; n];
gens.push(SparseMultivariatePolynomial::<_, Lex>::from_terms(
field.clone(),
n,
vec![
(full_exps, field.element(1)),
(vec![0usize; n], field.element(12)),
],
));
let start = std::time::Instant::now();
let gb = f4(&gens);
let elapsed = start.elapsed();
eprintln!("cyclic-5 Fp13: {:.2?}, basis={}", elapsed, gb.basis.len());
assert!(gb.is_groebner_basis());
}
#[test]
fn mod_inv_basic() {
assert_eq!(mod_inv(3, 7), 5);
assert_eq!(mod_inv(2, 7), 4);
assert_eq!(mod_inv(1, 13), 1);
}
#[test]
fn grlex_ordering() {
use crate::sparse::Grlex;
let ord = Grlex;
assert_eq!(ord.cmp(&[2, 0], &[1, 1]), std::cmp::Ordering::Greater);
assert_eq!(ord.cmp(&[1, 1], &[0, 2]), std::cmp::Ordering::Greater);
assert_eq!(ord.cmp(&[0, 2], &[1, 0]), std::cmp::Ordering::Less);
assert_eq!(ord.cmp(&[1, 0], &[0, 2]), std::cmp::Ordering::Greater);
}
}