use std::collections::HashMap;
use ocas_domain::Domain;
use crate::groebner::GroebnerBasis;
use crate::sparse::{MonomialOrder, SparseMultivariatePolynomial};
pub fn fglm<D: Domain, O2: MonomialOrder>(
gb: &GroebnerBasis<D, impl MonomialOrder>,
) -> Option<GroebnerBasis<D, O2>> {
let n_vars = gb.basis.first()?.n_vars();
let domain = gb.basis.first()?.domain().clone();
let lms: Vec<Vec<usize>> = gb
.basis
.iter()
.filter_map(|p| p.leading_monomial().map(|m| m.to_vec()))
.collect();
let staircase = compute_staircase(&lms, n_vars)?;
let dim = staircase.len();
let mut mult_matrices: Vec<Vec<Vec<D::Element>>> = vec![Vec::new(); n_vars];
for var in 0..n_vars {
let mut mat = vec![vec![domain.zero(); dim]; dim];
for (col, m) in staircase.iter().enumerate() {
let mut xm = m.clone();
xm[var] += 1;
let nf = normal_form_monomial(&xm, gb, &staircase, &domain);
for (row, coeff) in nf.into_iter().enumerate() {
mat[row][col] = coeff;
}
}
mult_matrices[var] = mat;
}
let mut new_basis: Vec<SparseMultivariatePolynomial<D, O2>> = Vec::new();
let mut seen_nfs: Vec<Vec<D::Element>> = Vec::new(); let mut seen_mons: Vec<Vec<usize>> = Vec::new(); let mut boundary: Vec<Vec<usize>> = vec![vec![0; n_vars]]; let mut visited: HashMap<Vec<usize>, bool> = HashMap::new();
let max_steps = dim * (dim + 1) + n_vars * 4;
let mut steps = 0;
while !boundary.is_empty() && steps < max_steps {
steps += 1;
let (pos, m) = boundary
.iter()
.enumerate()
.min_by(|(_, a), (_, b)| O2::cmp(a, b))
.map(|(i, a)| (i, a.clone()))?;
boundary.remove(pos);
if visited.contains_key(&m) {
continue;
}
visited.insert(m.clone(), true);
let nf = normal_form_monomial(&m, gb, &staircase, &domain);
if let Some(relation) = find_relation(&seen_nfs, &nf, &domain) {
let mut terms: Vec<(Vec<usize>, D::Element)> = vec![(m.clone(), domain.one())];
for (coeff, mon) in relation.into_iter().zip(seen_mons.iter()) {
if !domain.is_zero(&coeff) {
terms.push((mon.clone(), coeff));
}
}
new_basis.push(SparseMultivariatePolynomial::from_terms(
domain.clone(),
n_vars,
terms,
));
mark_multiples(&mut visited, &m, n_vars, dim * 2);
} else {
seen_nfs.push(nf);
seen_mons.push(m.clone());
for var in 0..n_vars {
let mut next = m.clone();
next[var] += 1;
if !visited.contains_key(&next) {
boundary.push(next);
}
}
}
}
if new_basis.is_empty() {
return None;
}
let out = GroebnerBasis { basis: new_basis };
Some(out.minimize().auto_reduce())
}
fn compute_staircase(lms: &[Vec<usize>], n_vars: usize) -> Option<Vec<Vec<usize>>> {
let mut staircase = Vec::new();
let mut queue = vec![vec![0usize; n_vars]];
let mut seen: HashMap<Vec<usize>, ()> = HashMap::new();
let limit = 100_000; while let Some(m) = queue.pop() {
if seen.contains_key(&m) {
continue;
}
seen.insert(m.clone(), ());
if seen.len() > limit {
return None; }
if lms.iter().any(|lm| monomial_divides_big(lm, &m)) {
continue; }
for var in 0..n_vars {
let mut next = m.clone();
next[var] += 1;
queue.push(next);
}
staircase.push(m);
}
Some(staircase)
}
fn monomial_divides_big(lm: &[usize], big: &[usize]) -> bool {
lm.iter().zip(big.iter()).all(|(a, b)| a <= b)
}
fn normal_form_monomial<D: Domain>(
m: &[usize],
gb: &GroebnerBasis<D, impl MonomialOrder>,
staircase: &[Vec<usize>],
domain: &D,
) -> Vec<D::Element> {
let poly = SparseMultivariatePolynomial::from_terms(
domain.clone(),
m.len(),
vec![(m.to_vec(), domain.one())],
);
let nf = poly.reduce(&gb.basis);
let mut coords = vec![domain.zero(); staircase.len()];
for (exp, coeff) in nf.terms_ref() {
if let Some(pos) = staircase
.iter()
.position(|s| s.as_slice() == exp.as_slice())
{
coords[pos] = coeff.clone();
}
}
coords
}
fn find_relation<D: Domain>(
seen: &[Vec<D::Element>],
nf: &[D::Element],
domain: &D,
) -> Option<Vec<D::Element>> {
if seen.is_empty() {
return None;
}
let rows = seen.len();
let cols = nf.len();
let mut mat: Vec<Vec<D::Element>> = (0..cols)
.map(|r| {
let mut row: Vec<D::Element> = seen.iter().map(|s| s[r].clone()).collect();
row.push(nf[r].clone());
row
})
.collect();
let mut pivot_cols = Vec::new();
let mut r = 0;
for c in 0..rows {
let mut piv = None;
for (rr, row) in mat.iter().enumerate().skip(r) {
if !domain.is_zero(&row[c]) {
piv = Some(rr);
break;
}
}
let Some(piv) = piv else { continue };
mat.swap(r, piv);
let inv = domain.inv(&mat[r][c].clone())?;
for cc in c..=rows {
mat[r][cc] = domain.mul(&mat[r][cc], &inv);
}
for rr in 0..cols {
if rr != r && !domain.is_zero(&mat[rr][c]) {
let factor = mat[rr][c].clone();
for cc in c..=rows {
let sub = domain.mul(&factor, &mat[r][cc]);
mat[rr][cc] = domain.sub(&mat[rr][cc], &sub);
}
}
}
pivot_cols.push(c);
r += 1;
if r == cols {
break;
}
}
for row in &mat {
let seen_zero = (0..rows).all(|c| domain.is_zero(&row[c]));
if seen_zero && !domain.is_zero(&row[rows]) {
return None;
}
}
let mut c_vec = vec![domain.zero(); rows];
for (i, &pc) in pivot_cols.iter().enumerate() {
c_vec[pc] = mat[i][rows].clone();
}
Some(c_vec)
}
fn mark_multiples(
visited: &mut HashMap<Vec<usize>, bool>,
m: &[usize],
n_vars: usize,
max_deg: usize,
) {
let mut queue = vec![m.to_vec()];
while let Some(cur) = queue.pop() {
for var in 0..n_vars {
let mut next = cur.clone();
next[var] += 1;
if next.iter().sum::<usize>() > max_deg {
continue;
}
if visited.insert(next.clone(), true).is_none() {
queue.push(next);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sparse::{Grevlex, Lex};
use ocas_domain::{Rational, RationalDomain};
fn r(n: i64, d: i64) -> Rational {
Rational::new(n, d)
}
#[test]
fn fglm_linear_ideal() {
let d = RationalDomain;
let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
d,
2,
vec![(vec![1, 0], r(1, 1)), (vec![0, 1], 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 gb_lex = crate::groebner::f4::f4(&[f1, f2]);
let gb_grevlex = fglm::<_, Grevlex>(&gb_lex).expect("zero-dimensional");
assert!(gb_grevlex.is_groebner_basis());
}
#[test]
fn fglm_zero_dim_quadratic() {
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_lex = crate::groebner::f4::f4(&[f1, f2]);
let gb_grevlex = fglm::<_, Grevlex>(&gb_lex).expect("zero-dimensional");
assert!(gb_grevlex.is_groebner_basis());
}
}