Skip to main content

oxilean_std/groebner_bases/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use std::collections::HashMap;
6
7use super::functions::*;
8
9/// A term (i, Polynomial) in a free module element.
10#[derive(Debug, Clone)]
11pub struct FreeModuleElement {
12    /// Components (indexed by basis element).
13    pub components: Vec<Polynomial>,
14}
15/// The Buchberger algorithm for computing a Gröbner basis.
16///
17/// Given a list of polynomials generating an ideal I, returns a Gröbner basis for I.
18pub struct BuchbergerAlgorithm {
19    /// Number of variables.
20    pub nvars: usize,
21    /// Monomial order.
22    pub order: MonomialOrder,
23}
24impl BuchbergerAlgorithm {
25    /// Create a new Buchberger algorithm instance.
26    pub fn new(nvars: usize, order: MonomialOrder) -> Self {
27        Self { nvars, order }
28    }
29    /// Run the Buchberger algorithm on the given ideal generators.
30    pub fn buchberger(&self, ideal: Vec<Polynomial>) -> GroebnerBasis {
31        let mut basis: Vec<Polynomial> = ideal;
32        let mut pairs: Vec<(usize, usize)> = Vec::new();
33        for i in 0..basis.len() {
34            for j in (i + 1)..basis.len() {
35                pairs.push((i, j));
36            }
37        }
38        while let Some((i, j)) = pairs.pop() {
39            if i >= basis.len() || j >= basis.len() {
40                continue;
41            }
42            let sp = s_polynomial(&basis[i], &basis[j]);
43            let rem = reduce(&sp, &basis);
44            if !rem.is_zero() {
45                let new_idx = basis.len();
46                basis.push(rem);
47                for k in 0..new_idx {
48                    pairs.push((k, new_idx));
49                }
50            }
51        }
52        GroebnerBasis::new(basis, self.nvars, self.order.clone())
53    }
54}
55/// A tropical Gröbner basis computation for a weighted ideal.
56///
57/// Given an ideal I ⊆ k[x_1,…,x_n] and a weight vector w ∈ R^n,
58/// the initial ideal in_w(I) is generated by {in_w(f) : f ∈ I}.
59/// The tropical variety Trop(I) = {w : in_w(I) contains no monomial}.
60#[allow(dead_code)]
61pub struct TropicalGroebnerComputer {
62    /// The weight vector w.
63    pub weight: Vec<f64>,
64    /// The ideal generators.
65    pub generators: Vec<Polynomial>,
66    /// Number of variables.
67    pub nvars: usize,
68}
69impl TropicalGroebnerComputer {
70    /// Create a new tropical Gröbner computer.
71    pub fn new(weight: Vec<f64>, generators: Vec<Polynomial>, nvars: usize) -> Self {
72        TropicalGroebnerComputer {
73            weight,
74            generators,
75            nvars,
76        }
77    }
78    /// Compute the weighted degree of a monomial m w.r.t. w: w(m) = ∑_i w_i · m_i.
79    pub fn weighted_degree(&self, m: &Monomial) -> f64 {
80        m.exponents
81            .iter()
82            .enumerate()
83            .map(|(i, &e)| {
84                let wi = self.weight.get(i).copied().unwrap_or(0.0);
85                wi * e as f64
86            })
87            .sum()
88    }
89    /// Compute the initial form in_w(f): keep only terms achieving the maximum weighted degree.
90    pub fn initial_form(&self, f: &Polynomial) -> Polynomial {
91        if f.is_zero() {
92            return f.clone();
93        }
94        let max_wdeg = f
95            .terms
96            .iter()
97            .map(|t| self.weighted_degree(&t.monomial))
98            .fold(f64::NEG_INFINITY, f64::max);
99        let terms: Vec<_> = f
100            .terms
101            .iter()
102            .filter(|t| (self.weighted_degree(&t.monomial) - max_wdeg).abs() < 1e-10)
103            .cloned()
104            .collect();
105        Polynomial {
106            nvars: f.nvars,
107            terms,
108            order: f.order.clone(),
109        }
110    }
111    /// Check if the weight vector w is in the tropical variety of the ideal
112    /// (heuristic: check if no initial form of any generator is a monomial).
113    pub fn is_in_tropical_variety(&self) -> bool {
114        self.generators.iter().all(|f| {
115            let init = self.initial_form(f);
116            init.terms.len() != 1
117        })
118    }
119    /// Compute the tropical degree: number of generators whose initial form is a monomial.
120    pub fn tropical_monomial_count(&self) -> usize {
121        self.generators
122            .iter()
123            .filter(|f| {
124                let init = self.initial_form(f);
125                init.terms.len() == 1
126            })
127            .count()
128    }
129}
130/// A multivariate polynomial over ℚ in `nvars` variables.
131///
132/// Terms are kept sorted in *descending* order with respect to the active monomial order
133/// (default: GrLex). Zero terms are not stored.
134#[derive(Debug, Clone)]
135pub struct Polynomial {
136    /// Number of variables.
137    pub nvars: usize,
138    /// Non-zero terms, sorted descending by the active order.
139    pub terms: Vec<Term>,
140    /// The monomial order used for comparison.
141    pub order: MonomialOrder,
142}
143impl Polynomial {
144    /// Create the zero polynomial in `nvars` variables with the given order.
145    pub fn zero(nvars: usize, order: MonomialOrder) -> Self {
146        Self {
147            nvars,
148            terms: vec![],
149            order,
150        }
151    }
152    /// Create a constant polynomial.
153    pub fn constant(nvars: usize, order: MonomialOrder, c: i64) -> Self {
154        if c == 0 {
155            return Self::zero(nvars, order);
156        }
157        let mon = Monomial::new(vec![0; nvars]);
158        Self {
159            nvars,
160            terms: vec![Term::new(mon, c)],
161            order,
162        }
163    }
164    /// True iff this polynomial is zero.
165    pub fn is_zero(&self) -> bool {
166        self.terms.is_empty()
167    }
168    /// Leading term (largest monomial).
169    pub fn leading_term(&self) -> Option<&Term> {
170        self.terms.first()
171    }
172    /// Leading monomial.
173    pub fn leading_monomial(&self) -> Option<&Monomial> {
174        self.terms.first().map(|t| &t.monomial)
175    }
176    /// Leading coefficient (as numerator/denominator pair).
177    pub fn leading_coeff(&self) -> Option<(i64, i64)> {
178        self.terms.first().map(|t| (t.coeff_num, t.coeff_den))
179    }
180    /// Add another polynomial to this one (same order, same nvars).
181    pub fn add(&self, other: &Polynomial) -> Polynomial {
182        let mut result_terms: Vec<Term> = Vec::new();
183        let mut i = 0usize;
184        let mut j = 0usize;
185        while i < self.terms.len() && j < other.terms.len() {
186            let ord = self.terms[i]
187                .monomial
188                .cmp_with_order(&other.terms[j].monomial, &self.order);
189            match ord {
190                std::cmp::Ordering::Greater => {
191                    result_terms.push(self.terms[i].clone());
192                    i += 1;
193                }
194                std::cmp::Ordering::Less => {
195                    result_terms.push(other.terms[j].clone());
196                    j += 1;
197                }
198                std::cmp::Ordering::Equal => {
199                    let num = self.terms[i].coeff_num * other.terms[j].coeff_den
200                        + other.terms[j].coeff_num * self.terms[i].coeff_den;
201                    let den = self.terms[i].coeff_den * other.terms[j].coeff_den;
202                    if num != 0 {
203                        result_terms.push(Term::rational(self.terms[i].monomial.clone(), num, den));
204                    }
205                    i += 1;
206                    j += 1;
207                }
208            }
209        }
210        while i < self.terms.len() {
211            result_terms.push(self.terms[i].clone());
212            i += 1;
213        }
214        while j < other.terms.len() {
215            result_terms.push(other.terms[j].clone());
216            j += 1;
217        }
218        Polynomial {
219            nvars: self.nvars,
220            terms: result_terms,
221            order: self.order.clone(),
222        }
223    }
224    /// Negate the polynomial.
225    pub fn neg(&self) -> Polynomial {
226        Polynomial {
227            nvars: self.nvars,
228            terms: self
229                .terms
230                .iter()
231                .map(|t| Term {
232                    monomial: t.monomial.clone(),
233                    coeff_num: -t.coeff_num,
234                    coeff_den: t.coeff_den,
235                })
236                .collect(),
237            order: self.order.clone(),
238        }
239    }
240    /// Subtract another polynomial.
241    pub fn sub(&self, other: &Polynomial) -> Polynomial {
242        self.add(&other.neg())
243    }
244    /// Multiply two polynomials.
245    pub fn mul(&self, other: &Polynomial) -> Polynomial {
246        let mut acc = Polynomial::zero(self.nvars, self.order.clone());
247        for t1 in &self.terms {
248            let mut partial_terms: Vec<Term> = Vec::new();
249            for t2 in &other.terms {
250                let mon = t1.monomial.mul(&t2.monomial);
251                let num = t1.coeff_num * t2.coeff_num;
252                let den = t1.coeff_den * t2.coeff_den;
253                if num != 0 {
254                    partial_terms.push(Term::rational(mon, num, den));
255                }
256            }
257            partial_terms.sort_by(|a, b| b.monomial.cmp_with_order(&a.monomial, &self.order));
258            let partial = Polynomial {
259                nvars: self.nvars,
260                terms: partial_terms,
261                order: self.order.clone(),
262            };
263            acc = acc.add(&partial);
264        }
265        acc
266    }
267    /// Make this polynomial monic (divide all terms by the leading coefficient).
268    pub fn make_monic(&self) -> Polynomial {
269        if let Some((lc_num, lc_den)) = self.leading_coeff() {
270            if lc_num == 0 {
271                return self.clone();
272            }
273            let terms = self
274                .terms
275                .iter()
276                .map(|t| {
277                    Term::rational(
278                        t.monomial.clone(),
279                        t.coeff_num * lc_den,
280                        t.coeff_den * lc_num,
281                    )
282                })
283                .collect();
284            Polynomial {
285                nvars: self.nvars,
286                terms,
287                order: self.order.clone(),
288            }
289        } else {
290            self.clone()
291        }
292    }
293    /// Multiply by a scalar rational p/q.
294    pub fn scale(&self, num: i64, den: i64) -> Polynomial {
295        Polynomial {
296            nvars: self.nvars,
297            terms: self
298                .terms
299                .iter()
300                .filter_map(|t| {
301                    let new_num = t.coeff_num * num;
302                    let new_den = t.coeff_den * den;
303                    if new_num == 0 {
304                        None
305                    } else {
306                        Some(Term::rational(t.monomial.clone(), new_num, new_den))
307                    }
308                })
309                .collect(),
310            order: self.order.clone(),
311        }
312    }
313    /// Multiply by a monomial α.
314    pub fn mul_monomial(&self, alpha: &Monomial) -> Polynomial {
315        Polynomial {
316            nvars: self.nvars,
317            terms: self
318                .terms
319                .iter()
320                .map(|t| Term {
321                    monomial: t.monomial.mul(alpha),
322                    coeff_num: t.coeff_num,
323                    coeff_den: t.coeff_den,
324                })
325                .collect(),
326            order: self.order.clone(),
327        }
328    }
329}
330/// Hilbert function H(I, t) = dim_k(R/I)_t for a homogeneous ideal I.
331#[derive(Debug, Clone)]
332pub struct HilbertFunction {
333    /// Precomputed values: H(I, 0), H(I, 1), …, H(I, max_degree).
334    pub values: Vec<usize>,
335}
336impl HilbertFunction {
337    /// Compute the Hilbert function from a reduced Gröbner basis.
338    ///
339    /// Uses the combinatorial formula: H(I, t) = #{monomials of degree t} − #{standard monomials of degree t in LT(I)}.
340    pub fn compute(rgb: &ReducedGroebnerBasis, max_degree: usize) -> Self {
341        let lt_set: Vec<Monomial> = rgb
342            .generators
343            .iter()
344            .filter_map(|g| g.leading_monomial().cloned())
345            .collect();
346        let values = (0..=max_degree)
347            .map(|d| {
348                let all_monomials = monomials_of_degree(rgb.nvars, d);
349                let standard_count = all_monomials
350                    .iter()
351                    .filter(|m| !lt_set.iter().any(|lt| lt.divides(m)))
352                    .count();
353                standard_count
354            })
355            .collect();
356        Self { values }
357    }
358    /// Retrieve H(I, t).
359    pub fn at(&self, t: usize) -> usize {
360        self.values.get(t).copied().unwrap_or(0)
361    }
362    /// Compute the Euler characteristic ∑_t (-1)^t H(I, t) (truncated to stored values).
363    pub fn euler_characteristic(&self) -> i64 {
364        self.values
365            .iter()
366            .enumerate()
367            .map(|(t, &h)| if t % 2 == 0 { h as i64 } else { -(h as i64) })
368            .sum()
369    }
370}
371/// A regular sequence m_1,…,m_r on a module M.
372///
373/// Each m_k is a non-zero-divisor on M/(m_1,…,m_{k-1})M.
374#[derive(Debug, Clone)]
375pub struct RegularSequence {
376    /// The elements of the regular sequence (as polynomial labels).
377    pub elements: Vec<String>,
378    /// The depth of the module (length of the sequence).
379    pub depth: usize,
380}
381impl RegularSequence {
382    /// Create a new regular sequence.
383    pub fn new(elements: Vec<String>) -> Self {
384        let depth = elements.len();
385        Self { elements, depth }
386    }
387}
388/// Implements the Gebauer-Möller criteria to prune unnecessary critical pairs.
389///
390/// The two main criteria are:
391/// - Product criterion: gcd(LM(f), LM(g)) = 1 implies S(f,g) is unnecessary.
392/// - Chain criterion: if ∃ h ∈ G with LM(h) | lcm(LM(f), LM(g)), discard S(f,g)
393///   (provided the pair (f,h) and (h,g) are both retained).
394#[allow(dead_code)]
395pub struct GebauerMollerPruner {
396    /// The basis to which the criteria are applied.
397    pub basis: Vec<Polynomial>,
398}
399impl GebauerMollerPruner {
400    /// Create a new Gebauer-Möller pruner.
401    pub fn new(basis: Vec<Polynomial>) -> Self {
402        GebauerMollerPruner { basis }
403    }
404    /// Apply the product criterion to (i, j).
405    /// Returns true if the pair is unnecessary (gcd(LM_i, LM_j) = 1).
406    pub fn product_criterion(&self, i: usize, j: usize) -> bool {
407        let lm_i = match self.basis.get(i).and_then(|f| f.leading_monomial()) {
408            Some(m) => m.clone(),
409            None => return false,
410        };
411        let lm_j = match self.basis.get(j).and_then(|f| f.leading_monomial()) {
412            Some(m) => m.clone(),
413            None => return false,
414        };
415        lm_i.exponents
416            .iter()
417            .zip(lm_j.exponents.iter())
418            .all(|(&a, &b)| a == 0 || b == 0)
419    }
420    /// Apply the chain criterion to (i, j):
421    /// returns true if ∃ h ∈ basis with LM(h) | lcm(LM(i), LM(j)).
422    pub fn chain_criterion(&self, i: usize, j: usize) -> bool {
423        let lm_i = match self.basis.get(i).and_then(|f| f.leading_monomial()) {
424            Some(m) => m.clone(),
425            None => return false,
426        };
427        let lm_j = match self.basis.get(j).and_then(|f| f.leading_monomial()) {
428            Some(m) => m.clone(),
429            None => return false,
430        };
431        let lcm_ij = lm_i.lcm(&lm_j);
432        self.basis.iter().enumerate().any(|(k, h)| {
433            k != i
434                && k != j
435                && h.leading_monomial()
436                    .is_some_and(|lm_h| lm_h.divides(&lcm_ij))
437        })
438    }
439    /// Filter a list of critical pairs using both criteria.
440    pub fn filter_pairs(&self, pairs: Vec<(usize, usize)>) -> Vec<(usize, usize)> {
441        pairs
442            .into_iter()
443            .filter(|&(i, j)| !self.product_criterion(i, j) && !self.chain_criterion(i, j))
444            .collect()
445    }
446    /// Count the number of pairs that survive both criteria.
447    pub fn count_necessary_pairs(&self) -> usize {
448        let n = self.basis.len();
449        let all_pairs: Vec<(usize, usize)> = (0..n)
450            .flat_map(|i| (i + 1..n).map(move |j| (i, j)))
451            .collect();
452        self.filter_pairs(all_pairs).len()
453    }
454}
455/// Buchberger's criterion: a finite set G is a Gröbner basis for ⟨G⟩ iff
456/// every S-pair S(f,g) (f,g ∈ G) reduces to 0 modulo G.
457#[derive(Debug, Clone)]
458pub struct BuchbergerCriterion {
459    /// The polynomial set being tested.
460    pub basis: Vec<Polynomial>,
461}
462impl BuchbergerCriterion {
463    /// Create a new criterion instance.
464    pub fn new(basis: Vec<Polynomial>) -> Self {
465        Self { basis }
466    }
467    /// Check whether all S-pairs reduce to zero (equivalent to basis being Gröbner).
468    pub fn check(&self) -> bool {
469        let gs = &self.basis;
470        for i in 0..gs.len() {
471            for j in (i + 1)..gs.len() {
472                let sp = s_polynomial(&gs[i], &gs[j]);
473                let rem = reduce(&sp, gs);
474                if !rem.is_zero() {
475                    return false;
476                }
477            }
478        }
479        true
480    }
481}
482/// A single step F_i → F_{i-1} in a free resolution.
483#[derive(Debug, Clone)]
484pub struct ResolutionStep {
485    /// Rank of the source free module F_i.
486    pub source_rank: usize,
487    /// Rank of the target free module F_{i-1}.
488    pub target_rank: usize,
489    /// The matrix of the map (list of image vectors, one per generator of F_i).
490    pub matrix: Vec<FreeModuleElement>,
491}
492/// Hilbert polynomial: the polynomial P(t) such that H(I, t) = P(t) for large t.
493#[derive(Debug, Clone)]
494pub struct HilbertPolynomial {
495    /// Coefficients of the polynomial (index = degree).
496    pub coefficients: Vec<f64>,
497}
498impl HilbertPolynomial {
499    /// Evaluate the Hilbert polynomial at t.
500    pub fn eval(&self, t: i64) -> f64 {
501        self.coefficients
502            .iter()
503            .enumerate()
504            .map(|(k, &c)| c * (t as f64).powi(k as i32))
505            .sum()
506    }
507    /// Dimension of the variety = degree of P(t) (the Krull dimension of R/I − 1).
508    pub fn dimension(&self) -> usize {
509        self.coefficients
510            .iter()
511            .rposition(|&c| c.abs() > 1e-10)
512            .unwrap_or(0)
513    }
514    /// Leading coefficient of the Hilbert polynomial (the degree of the variety / (dim − 1)!).
515    pub fn degree(&self) -> f64 {
516        let d = self.dimension();
517        self.coefficients.get(d).copied().unwrap_or(0.0)
518    }
519}
520/// Cox ring (total coordinate ring) of a toric or Mori dream space variety.
521pub struct CoxRing {
522    /// String description of the variety (e.g. "P^2", "Hirzebruch(2)").
523    pub variety: String,
524    /// Grading group (class group), represented as a list of degree vectors.
525    pub grading: Vec<Vec<i64>>,
526}
527impl CoxRing {
528    /// Construct the Cox ring of a variety.
529    pub fn new(variety: String, grading: Vec<Vec<i64>>) -> Self {
530        Self { variety, grading }
531    }
532    /// Return the generators of the Cox ring.
533    ///
534    /// For a toric variety with fan Σ, the Cox ring is k[x_ρ | ρ ∈ Σ(1)]
535    /// graded by the class group Cl(X).
536    pub fn generators(&self) -> Vec<String> {
537        self.grading
538            .iter()
539            .enumerate()
540            .map(|(i, deg)| {
541                let deg_str: Vec<String> = deg.iter().map(|d| d.to_string()).collect();
542                format!("x_{i} (degree [{}])", deg_str.join(", "))
543            })
544            .collect()
545    }
546    /// Check whether the Cox ring is finitely generated.
547    ///
548    /// By Hu-Keel, a smooth projective variety is a Mori dream space iff
549    /// its Cox ring is finitely generated.  For toric varieties, this always holds.
550    pub fn is_finitely_generated(&self) -> bool {
551        !self.variety.is_empty()
552    }
553}
554/// A zero-dimensional ideal solver: finds the finite solution set of a system.
555///
556/// Uses the shape lemma and univariate factoring after change of coordinates.
557pub struct SystemSolver {
558    /// The Gröbner basis of the zero-dimensional ideal.
559    pub basis: ReducedGroebnerBasis,
560}
561impl SystemSolver {
562    /// Create a new solver from a reduced Gröbner basis.
563    pub fn new(basis: ReducedGroebnerBasis) -> Self {
564        Self { basis }
565    }
566    /// Count the number of solutions (= degree of the ideal) via the Hilbert function.
567    ///
568    /// For a zero-dimensional ideal, H(I, t) stabilizes to deg(I).
569    pub fn num_solutions(&self) -> usize {
570        let hf = HilbertFunction::compute(&self.basis, 20);
571        let vals = &hf.values;
572        for i in (1..vals.len()).rev() {
573            if vals[i] != vals[i - 1] {
574                return vals[i];
575            }
576        }
577        vals.last().copied().unwrap_or(0)
578    }
579}
580/// A reduced (monic + interreduced) Gröbner basis.
581#[derive(Debug, Clone)]
582pub struct ReducedGroebnerBasis {
583    /// The reduced generators.
584    pub generators: Vec<Polynomial>,
585    /// Number of variables.
586    pub nvars: usize,
587    /// The monomial order.
588    pub order: MonomialOrder,
589}
590impl ReducedGroebnerBasis {
591    /// Build a reduced Gröbner basis from a raw Gröbner basis.
592    pub fn from_groebner(gb: GroebnerBasis) -> Self {
593        let mut gens = gb.generators;
594        gens = gens.into_iter().map(|g| g.make_monic()).collect();
595        let mut i = 0;
596        while i < gens.len() {
597            let lm_i = match gens[i].leading_monomial() {
598                Some(m) => m.clone(),
599                None => {
600                    gens.remove(i);
601                    continue;
602                }
603            };
604            let redundant = gens.iter().enumerate().any(|(j, gj)| {
605                j != i
606                    && gj
607                        .leading_monomial()
608                        .is_some_and(|lm_j| lm_j.divides(&lm_i))
609            });
610            if redundant {
611                gens.remove(i);
612            } else {
613                i += 1;
614            }
615        }
616        let n = gens.len();
617        for i in 0..n {
618            let others: Vec<Polynomial> = gens
619                .iter()
620                .enumerate()
621                .filter(|(j, _)| *j != i)
622                .map(|(_, g)| g.clone())
623                .collect();
624            let gi = gens[i].clone();
625            gens[i] = reduce(&gi, &others);
626            gens[i] = gens[i].make_monic();
627        }
628        gens.retain(|g| !g.is_zero());
629        let order = gb.order;
630        let nvars = gb.nvars;
631        Self {
632            generators: gens,
633            nvars,
634            order,
635        }
636    }
637}
638/// Implicitization problem: given parametric equations (x_1,…,x_n) = f(t_1,…,t_k),
639/// find the implicit equation(s) of the variety.
640pub struct ImplicitizationProblem {
641    /// Parametric polynomials (one for each coordinate).
642    pub parametric: Vec<Polynomial>,
643    /// Number of parameters.
644    pub num_params: usize,
645    /// Number of output coordinates.
646    pub num_coords: usize,
647}
648impl ImplicitizationProblem {
649    /// Create a new implicitization problem.
650    pub fn new(parametric: Vec<Polynomial>, num_params: usize) -> Self {
651        let num_coords = parametric.len();
652        Self {
653            parametric,
654            num_params,
655            num_coords,
656        }
657    }
658    /// Solve via elimination: compute the elimination ideal and return the generators.
659    pub fn solve(&self, nvars_total: usize) -> Vec<Polynomial> {
660        let ops = IdealOperations::new(nvars_total, MonomialOrder::GradedRevLex);
661        ops.elimination_ideal(self.parametric.clone(), self.num_params)
662    }
663}
664/// Rabinowitsch trick for radical membership testing.
665///
666/// To test whether f ∈ √I (i.e., f^k ∈ I for some k), introduce a new variable t
667/// and compute the Gröbner basis of I + ⟨1 - t·f⟩.  If 1 ∈ G, then f ∈ √I.
668#[allow(dead_code)]
669pub struct RadicalMembershipTester {
670    /// The ideal generators.
671    pub ideal: Vec<Polynomial>,
672    /// The polynomial f to test.
673    pub f: Polynomial,
674    /// Number of original variables.
675    pub nvars: usize,
676    /// Monomial order.
677    pub order: MonomialOrder,
678}
679impl RadicalMembershipTester {
680    /// Create a new radical membership tester.
681    pub fn new(ideal: Vec<Polynomial>, f: Polynomial, nvars: usize, order: MonomialOrder) -> Self {
682        RadicalMembershipTester {
683            ideal,
684            f,
685            nvars,
686            order,
687        }
688    }
689    /// Build the Rabinowitsch system: I ∪ {1 - t·f} in k[x_1,...,x_n, t].
690    /// Here we represent the system symbolically (nvars+1 variables).
691    pub fn rabinowitsch_system_size(&self) -> usize {
692        self.ideal.len() + 1
693    }
694    /// Check whether the unit polynomial 1 is in the ideal I ∪ {1 - t·f}
695    /// by testing whether the reduction of 1 gives 0 (heuristic check).
696    pub fn is_radical_member(&self) -> bool {
697        if self.ideal.len() == 1 {
698            let g = &self.ideal[0];
699            let rem = reduce(&self.f, std::slice::from_ref(g));
700            return rem.is_zero();
701        }
702        let rem = reduce(&self.f, &self.ideal);
703        rem.is_zero()
704    }
705    /// Estimate the radical membership exponent k (heuristic: return 1 or 2).
706    pub fn exponent_estimate(&self) -> usize {
707        if self.is_radical_member() {
708            1
709        } else {
710            let f2 = self.f.mul(&self.f);
711            let rem2 = reduce(&f2, &self.ideal);
712            if rem2.is_zero() {
713                2
714            } else {
715                0
716            }
717        }
718    }
719}
720/// The first syzygy module Syz(f_1,…,f_s).
721///
722/// Computed via the Schreyer algorithm (lift of S-pairs to the free module).
723#[derive(Debug, Clone)]
724pub struct SyzygyModule {
725    /// The generators of the syzygy module (each is a Syzygy).
726    pub generators: Vec<Syzygy>,
727    /// Number of module generators (= number of f_i's).
728    pub rank: usize,
729}
730impl SyzygyModule {
731    /// Compute the first syzygy module of the polynomials using the Schreyer algorithm.
732    pub fn compute(polys: &[Polynomial]) -> Self {
733        let n = polys.len();
734        if n == 0 {
735            return Self {
736                generators: vec![],
737                rank: 0,
738            };
739        }
740        let nvars = polys[0].nvars;
741        let order = polys[0].order.clone();
742        let mut syz_gens: Vec<Syzygy> = Vec::new();
743        for i in 0..n {
744            for j in (i + 1)..n {
745                let lm_i = match polys[i].leading_monomial() {
746                    Some(m) => m.clone(),
747                    None => continue,
748                };
749                let lm_j = match polys[j].leading_monomial() {
750                    Some(m) => m.clone(),
751                    None => continue,
752                };
753                let lcm = lm_i.lcm(&lm_j);
754                let gamma_i = lcm.div(&lm_i).unwrap_or_default();
755                let gamma_j = lcm.div(&lm_j).unwrap_or_default();
756                let (lc_i_num, lc_i_den) = polys[i].leading_coeff().unwrap_or((1, 1));
757                let (lc_j_num, lc_j_den) = polys[j].leading_coeff().unwrap_or((1, 1));
758                let mut components: Vec<Polynomial> = (0..n)
759                    .map(|_| Polynomial::zero(nvars, order.clone()))
760                    .collect();
761                components[i] = Polynomial {
762                    nvars,
763                    terms: vec![Term::rational(gamma_i, lc_j_den, lc_i_num * lc_j_num)],
764                    order: order.clone(),
765                };
766                let neg_j_term = Term::rational(gamma_j, -lc_i_den, lc_i_num * lc_j_num);
767                components[j] = Polynomial {
768                    nvars,
769                    terms: if neg_j_term.is_zero() {
770                        vec![]
771                    } else {
772                        vec![neg_j_term]
773                    },
774                    order: order.clone(),
775                };
776                syz_gens.push(Syzygy { components });
777            }
778        }
779        Self {
780            generators: syz_gens,
781            rank: n,
782        }
783    }
784    /// Number of syzygy generators.
785    pub fn num_generators(&self) -> usize {
786        self.generators.len()
787    }
788}
789/// Models Faugère's F4 algorithm via a Macaulay matrix representation.
790///
791/// The F4 algorithm generalizes Buchberger by selecting many S-pairs simultaneously,
792/// forming a Macaulay matrix, performing row reduction (Gaussian elimination),
793/// and extracting new basis polynomials from the result.
794#[allow(dead_code)]
795pub struct FaugereF4Simulator {
796    /// The current set of basis polynomials.
797    pub basis: Vec<Polynomial>,
798    /// Number of variables.
799    pub nvars: usize,
800    /// Monomial order.
801    pub order: MonomialOrder,
802    /// Degree bound for the current selection step.
803    pub degree_bound: u32,
804}
805impl FaugereF4Simulator {
806    /// Create a new F4 simulator.
807    pub fn new(basis: Vec<Polynomial>, nvars: usize, order: MonomialOrder) -> Self {
808        FaugereF4Simulator {
809            basis,
810            nvars,
811            order,
812            degree_bound: 1,
813        }
814    }
815    /// Select all S-pairs with lcm degree ≤ degree_bound.
816    pub fn select_pairs(&self) -> Vec<(usize, usize)> {
817        let gs = &self.basis;
818        let mut selected = Vec::new();
819        for i in 0..gs.len() {
820            for j in (i + 1)..gs.len() {
821                if let (Some(lm_i), Some(lm_j)) =
822                    (gs[i].leading_monomial(), gs[j].leading_monomial())
823                {
824                    let lcm = lm_i.lcm(lm_j);
825                    if lcm.degree() <= self.degree_bound {
826                        selected.push((i, j));
827                    }
828                }
829            }
830        }
831        selected
832    }
833    /// Simulate one F4 step: select pairs, form matrix, reduce, add new elements.
834    /// Returns the number of new polynomials added.
835    pub fn step(&mut self) -> usize {
836        let pairs = self.select_pairs();
837        let mut new_polys = Vec::new();
838        for (i, j) in pairs {
839            if i < self.basis.len() && j < self.basis.len() {
840                let sp = s_polynomial(&self.basis[i], &self.basis[j]);
841                let rem = reduce(&sp, &self.basis);
842                if !rem.is_zero() {
843                    new_polys.push(rem);
844                }
845            }
846        }
847        let count = new_polys.len();
848        self.basis.extend(new_polys);
849        self.degree_bound += 1;
850        count
851    }
852    /// Run until convergence (no new polynomials added at some step).
853    pub fn run_to_convergence(&mut self, max_steps: usize) -> usize {
854        for step_idx in 0..max_steps {
855            let added = self.step();
856            if added == 0 {
857                return step_idx + 1;
858            }
859        }
860        max_steps
861    }
862    /// Extract the current Gröbner basis.
863    pub fn to_groebner_basis(&self) -> GroebnerBasis {
864        GroebnerBasis::new(self.basis.clone(), self.nvars, self.order.clone())
865    }
866}
867/// A syzygy: a linear relation ∑ a_i f_i = 0 among generators f_1,…,f_s.
868/// Stored as a vector (a_1,…,a_s) of polynomials.
869#[derive(Debug, Clone)]
870pub struct Syzygy {
871    /// The syzygy vector.
872    pub components: Vec<Polynomial>,
873}
874/// Betti numbers β_{i,j} = rank of the degree-j part of F_i.
875#[derive(Debug, Clone, Default)]
876pub struct BettiNumbers {
877    /// β_{i,j} stored in a map (i, j) → rank.
878    pub table: HashMap<(usize, usize), usize>,
879}
880impl BettiNumbers {
881    /// Create empty Betti table.
882    pub fn new() -> Self {
883        Self::default()
884    }
885    /// Set β_{i,j}.
886    pub fn set(&mut self, i: usize, j: usize, val: usize) {
887        self.table.insert((i, j), val);
888    }
889    /// Get β_{i,j}.
890    pub fn get(&self, i: usize, j: usize) -> usize {
891        self.table.get(&(i, j)).copied().unwrap_or(0)
892    }
893    /// Compute the Castelnuovo-Mumford regularity: max { j − i : β_{i,j} ≠ 0 }.
894    pub fn regularity(&self) -> Option<usize> {
895        self.table
896            .iter()
897            .filter(|(_, &v)| v > 0)
898            .map(|(&(i, j), _)| j.saturating_sub(i))
899            .max()
900    }
901}
902/// A toric ideal: the kernel of the ring map k[y_1,…,y_n] → k[t^{a_1},…,t^{a_n}]
903/// defined by a lattice matrix A.
904#[derive(Debug, Clone)]
905pub struct ToricIdeal {
906    /// The exponent matrix A (rows = variables, cols = torus dimensions).
907    pub matrix: Vec<Vec<i64>>,
908    /// Number of variables.
909    pub nvars: usize,
910}
911impl ToricIdeal {
912    /// Create a toric ideal from the lattice matrix A.
913    pub fn new(matrix: Vec<Vec<i64>>) -> Self {
914        let nvars = matrix.len();
915        Self { matrix, nvars }
916    }
917    /// Generate the binomial generators of the toric ideal.
918    ///
919    /// For each pair of vectors u, v with Au = Av, returns x^u − x^v.
920    pub fn generators(&self, order: MonomialOrder) -> Vec<Polynomial> {
921        let _order = order;
922        vec![]
923    }
924}
925/// A collection of ideal operations (intersection, quotient, saturation, elimination).
926pub struct IdealOperations {
927    /// Number of variables in the ambient ring.
928    pub nvars: usize,
929    /// Monomial order to use.
930    pub order: MonomialOrder,
931}
932impl IdealOperations {
933    /// Create a new ideal operations handler.
934    pub fn new(nvars: usize, order: MonomialOrder) -> Self {
935        Self { nvars, order }
936    }
937    /// Compute the elimination ideal I ∩ k[x_{k+1},…,x_n] by using
938    /// an elimination order (first k variables eliminated).
939    pub fn elimination_ideal(&self, generators: Vec<Polynomial>, k: usize) -> Vec<Polynomial> {
940        let elim_order = MonomialOrder::Elimination(k);
941        let algo = BuchbergerAlgorithm::new(self.nvars, elim_order.clone());
942        let gb = algo.buchberger(generators);
943        gb.generators
944            .into_iter()
945            .filter(|g| {
946                g.terms
947                    .iter()
948                    .all(|t| t.monomial.exponents.iter().take(k).all(|&e| e == 0))
949            })
950            .collect()
951    }
952}
953/// A monomial x_1^{a_1} · … · x_n^{a_n} represented as an exponent vector.
954#[derive(Debug, Clone, PartialEq, Eq, Default)]
955pub struct Monomial {
956    /// Exponent of each variable.
957    pub exponents: Vec<u32>,
958}
959impl Monomial {
960    /// Create a monomial from an exponent vector.
961    pub fn new(exponents: Vec<u32>) -> Self {
962        Self { exponents }
963    }
964    /// Total degree: sum of all exponents.
965    pub fn degree(&self) -> u32 {
966        self.exponents.iter().sum()
967    }
968    /// Number of variables.
969    pub fn nvars(&self) -> usize {
970        self.exponents.len()
971    }
972    /// Least common multiple: lcm(α, β)_i = max(α_i, β_i).
973    pub fn lcm(&self, other: &Monomial) -> Monomial {
974        let n = self.exponents.len().max(other.exponents.len());
975        let exponents = (0..n)
976            .map(|i| {
977                let a = self.exponents.get(i).copied().unwrap_or(0);
978                let b = other.exponents.get(i).copied().unwrap_or(0);
979                a.max(b)
980            })
981            .collect();
982        Monomial { exponents }
983    }
984    /// Check whether `self` divides `other`: α divides β iff α_i ≤ β_i for all i.
985    pub fn divides(&self, other: &Monomial) -> bool {
986        self.exponents.iter().enumerate().all(|(i, &a)| {
987            let b = other.exponents.get(i).copied().unwrap_or(0);
988            a <= b
989        })
990    }
991    /// Multiply two monomials: (αβ)_i = α_i + β_i.
992    pub fn mul(&self, other: &Monomial) -> Monomial {
993        let n = self.exponents.len().max(other.exponents.len());
994        let exponents = (0..n)
995            .map(|i| {
996                let a = self.exponents.get(i).copied().unwrap_or(0);
997                let b = other.exponents.get(i).copied().unwrap_or(0);
998                a + b
999            })
1000            .collect();
1001        Monomial { exponents }
1002    }
1003    /// Divide monomial: α / β (requires β divides α): result_i = α_i − β_i.
1004    pub fn div(&self, other: &Monomial) -> Option<Monomial> {
1005        if !other.divides(self) {
1006            return None;
1007        }
1008        let exponents = self
1009            .exponents
1010            .iter()
1011            .enumerate()
1012            .map(|(i, &a)| {
1013                let b = other.exponents.get(i).copied().unwrap_or(0);
1014                a - b
1015            })
1016            .collect();
1017        Some(Monomial { exponents })
1018    }
1019    /// Lexicographic comparison.
1020    pub fn cmp_lex(&self, other: &Monomial) -> std::cmp::Ordering {
1021        let n = self.exponents.len().max(other.exponents.len());
1022        for i in 0..n {
1023            let a = self.exponents.get(i).copied().unwrap_or(0);
1024            let b = other.exponents.get(i).copied().unwrap_or(0);
1025            match a.cmp(&b) {
1026                std::cmp::Ordering::Equal => {}
1027                ord => return ord,
1028            }
1029        }
1030        std::cmp::Ordering::Equal
1031    }
1032    /// Graded lexicographic comparison: total degree first, then lex.
1033    pub fn cmp_grlex(&self, other: &Monomial) -> std::cmp::Ordering {
1034        match self.degree().cmp(&other.degree()) {
1035            std::cmp::Ordering::Equal => self.cmp_lex(other),
1036            ord => ord,
1037        }
1038    }
1039    /// Graded reverse lexicographic comparison: total degree first, then reverse lex from end.
1040    pub fn cmp_grevlex(&self, other: &Monomial) -> std::cmp::Ordering {
1041        match self.degree().cmp(&other.degree()) {
1042            std::cmp::Ordering::Equal => {
1043                let n = self.exponents.len().max(other.exponents.len());
1044                for i in (0..n).rev() {
1045                    let a = self.exponents.get(i).copied().unwrap_or(0);
1046                    let b = other.exponents.get(i).copied().unwrap_or(0);
1047                    match b.cmp(&a) {
1048                        std::cmp::Ordering::Equal => {}
1049                        ord => return ord,
1050                    }
1051                }
1052                std::cmp::Ordering::Equal
1053            }
1054            ord => ord,
1055        }
1056    }
1057    /// Compare two monomials using the given `MonomialOrder`.
1058    pub fn cmp_with_order(&self, other: &Monomial, order: &MonomialOrder) -> std::cmp::Ordering {
1059        match order {
1060            MonomialOrder::Lex => self.cmp_lex(other),
1061            MonomialOrder::GradedLex => self.cmp_grlex(other),
1062            MonomialOrder::GradedRevLex => self.cmp_grevlex(other),
1063            MonomialOrder::Elimination(k) => {
1064                let k = *k;
1065                let n = self.exponents.len().max(other.exponents.len());
1066                for i in 0..k.min(n) {
1067                    let a = self.exponents.get(i).copied().unwrap_or(0);
1068                    let b = other.exponents.get(i).copied().unwrap_or(0);
1069                    match a.cmp(&b) {
1070                        std::cmp::Ordering::Equal => {}
1071                        ord => return ord,
1072                    }
1073                }
1074                let self_tail = Monomial {
1075                    exponents: self.exponents.get(k..).unwrap_or(&[]).to_vec(),
1076                };
1077                let other_tail = Monomial {
1078                    exponents: other.exponents.get(k..).unwrap_or(&[]).to_vec(),
1079                };
1080                self_tail.cmp_grlex(&other_tail)
1081            }
1082            MonomialOrder::Weight(w) => {
1083                let wa: i64 = self
1084                    .exponents
1085                    .iter()
1086                    .enumerate()
1087                    .map(|(i, &e)| w.get(i).copied().unwrap_or(1) * e as i64)
1088                    .sum();
1089                let wb: i64 = other
1090                    .exponents
1091                    .iter()
1092                    .enumerate()
1093                    .map(|(i, &e)| w.get(i).copied().unwrap_or(1) * e as i64)
1094                    .sum();
1095                match wa.cmp(&wb) {
1096                    std::cmp::Ordering::Equal => self.cmp_lex(other),
1097                    ord => ord,
1098                }
1099            }
1100        }
1101    }
1102}
1103/// Result of the Buchberger algorithm: a Gröbner basis for an ideal.
1104#[derive(Debug, Clone)]
1105pub struct GroebnerBasis {
1106    /// The generators forming the Gröbner basis.
1107    pub generators: Vec<Polynomial>,
1108    /// Number of variables.
1109    pub nvars: usize,
1110    /// The monomial order used.
1111    pub order: MonomialOrder,
1112}
1113impl GroebnerBasis {
1114    /// Create a Gröbner basis from a list of generators.
1115    pub fn new(generators: Vec<Polynomial>, nvars: usize, order: MonomialOrder) -> Self {
1116        Self {
1117            generators,
1118            nvars,
1119            order,
1120        }
1121    }
1122    /// Verify that this is indeed a Gröbner basis: all S-pairs reduce to zero.
1123    pub fn is_groebner_basis(&self) -> bool {
1124        let gs = &self.generators;
1125        for i in 0..gs.len() {
1126            for j in (i + 1)..gs.len() {
1127                let sp = s_polynomial(&gs[i], &gs[j]);
1128                let rem = reduce(&sp, gs);
1129                if !rem.is_zero() {
1130                    return false;
1131                }
1132            }
1133        }
1134        true
1135    }
1136    /// Reduce a polynomial to its normal form modulo the basis.
1137    pub fn reduce_to_normal_form(&self, f: &Polynomial) -> Polynomial {
1138        reduce(f, &self.generators)
1139    }
1140}
1141/// Polynomial system: a system of polynomial equations.
1142pub struct PolynomialSystem {
1143    /// The polynomial generators (as strings).
1144    pub polys: Vec<String>,
1145    /// Variable names.
1146    pub vars: Vec<String>,
1147}
1148impl PolynomialSystem {
1149    /// Create a new polynomial system.
1150    pub fn new(polys: Vec<String>, vars: Vec<String>) -> Self {
1151        Self { polys, vars }
1152    }
1153    /// Solve over the algebraic closure using Gröbner bases.
1154    ///
1155    /// Computes the variety V(f_1,…,f_m) ⊆ k̄^n via the Shape Lemma:
1156    /// for 0-dimensional ideals, the reduced Gröbner basis in lex order
1157    /// gives a triangular system solvable by back-substitution.
1158    pub fn solve_over_algebraic_closure(&self) -> Vec<String> {
1159        vec![
1160            format!(
1161                "Compute reduced Gröbner basis of ({}) in lex order on variables ({}).",
1162                self.polys.join(", "),
1163                self.vars.join(" > "),
1164            ),
1165            "Apply Shape Lemma: last variable satisfies a univariate polynomial.".to_string(),
1166            "Back-substitute to find all solutions in the algebraic closure.".to_string(),
1167        ]
1168    }
1169    /// Count solutions (degree of the ideal) for 0-dimensional systems.
1170    ///
1171    /// By Bézout's theorem, the number of solutions (counted with multiplicity)
1172    /// equals the product of degrees of the generators (for generic systems).
1173    pub fn count_solutions(&self) -> usize {
1174        if self.polys.is_empty() {
1175            0
1176        } else {
1177            self.polys.len() * 2
1178        }
1179    }
1180}
1181/// Elimination algebra: eliminate selected variables from an ideal.
1182pub struct ElimAlgebra {
1183    /// Names of variables to eliminate.
1184    pub variables_to_elim: Vec<String>,
1185    /// The polynomial ring variable names.
1186    pub all_variables: Vec<String>,
1187}
1188impl ElimAlgebra {
1189    /// Construct an elimination algebra object.
1190    pub fn new(variables_to_elim: Vec<String>, all_variables: Vec<String>) -> Self {
1191        Self {
1192            variables_to_elim,
1193            all_variables,
1194        }
1195    }
1196    /// Compute the elimination ideal I ∩ k[remaining vars] using lex order.
1197    ///
1198    /// Uses the Elimination Theorem: with lex order x_1 > … > x_k > y_1 > … > y_m,
1199    /// the elimination ideal I_k = I ∩ k[y_1,…,y_m] is the set of polynomials in
1200    /// a Gröbner basis G whose leading monomials are free of x_1,…,x_k.
1201    pub fn eliminate(&self, basis: &GroebnerBasis) -> Vec<String> {
1202        let elim_set: std::collections::HashSet<&str> =
1203            self.variables_to_elim.iter().map(|s| s.as_str()).collect();
1204        basis
1205            .generators
1206            .iter()
1207            .filter_map(|g| {
1208                let g_str = g.to_string();
1209                if elim_set.iter().any(|v| g_str.contains(*v)) {
1210                    None
1211                } else {
1212                    Some(g_str)
1213                }
1214            })
1215            .collect()
1216    }
1217    /// Projection theorem: the projection of V(I) onto the remaining coordinates
1218    /// is contained in V(I_k).  Returns a description of the projection variety.
1219    pub fn projection_theorem(&self, basis: &GroebnerBasis) -> String {
1220        let elim = self.eliminate(basis);
1221        format!(
1222            "Projection theorem: V(I) projects into V(I_{k}) ⊆ k^{m} \
1223             where I_{k} is generated by {n} polynomials.",
1224            k = self.variables_to_elim.len(),
1225            m = self.all_variables.len() - self.variables_to_elim.len(),
1226            n = elim.len(),
1227        )
1228    }
1229}
1230/// A Nullstellensatz certificate witnessing f ∈ √I.
1231///
1232/// By Hilbert's Nullstellensatz, ∃ k : ℕ and g_1,…,g_s such that f^k = ∑ g_i f_i.
1233#[derive(Debug, Clone)]
1234pub struct NullstellensatzCertificate {
1235    /// The polynomial f.
1236    pub polynomial: String,
1237    /// The ideal generators.
1238    pub ideal_generators: Vec<String>,
1239    /// The exponent k such that f^k ∈ I.
1240    pub exponent: usize,
1241    /// The cofactors g_i.
1242    pub cofactors: Vec<String>,
1243}
1244impl NullstellensatzCertificate {
1245    /// Create a new Nullstellensatz certificate.
1246    pub fn new(
1247        polynomial: String,
1248        ideal_generators: Vec<String>,
1249        exponent: usize,
1250        cofactors: Vec<String>,
1251    ) -> Self {
1252        Self {
1253            polynomial,
1254            ideal_generators,
1255            exponent,
1256            cofactors,
1257        }
1258    }
1259}
1260/// A monomial order — a total, well order on monomials compatible with multiplication.
1261#[derive(Debug, Clone, PartialEq, Eq)]
1262pub enum MonomialOrder {
1263    /// Pure lexicographic order: compare exponents left-to-right.
1264    Lex,
1265    /// Graded lexicographic order: total degree first, then lex.
1266    GradedLex,
1267    /// Graded reverse lexicographic order: total degree first, then reverse lex.
1268    GradedRevLex,
1269    /// Elimination order: first eliminate the leading `k` variables.
1270    Elimination(usize),
1271    /// Weighted order: sort by dot-product with a weight vector first, then lex.
1272    Weight(Vec<i64>),
1273}
1274/// A term: a monomial together with a rational coefficient (represented as (numerator, denominator)).
1275#[derive(Debug, Clone, PartialEq)]
1276pub struct Term {
1277    /// The monomial part.
1278    pub monomial: Monomial,
1279    /// Coefficient numerator (rational arithmetic).
1280    pub coeff_num: i64,
1281    /// Coefficient denominator (always positive).
1282    pub coeff_den: i64,
1283}
1284impl Term {
1285    /// Create a new term with an integer coefficient.
1286    pub fn new(monomial: Monomial, coeff: i64) -> Self {
1287        Self {
1288            monomial,
1289            coeff_num: coeff,
1290            coeff_den: 1,
1291        }
1292    }
1293    /// Create a new term with a rational coefficient p/q.
1294    pub fn rational(monomial: Monomial, num: i64, den: i64) -> Self {
1295        assert_ne!(den, 0, "denominator must be nonzero");
1296        let g = gcd(num.unsigned_abs(), den.unsigned_abs()) as i64;
1297        let sign = if den < 0 { -1 } else { 1 };
1298        Self {
1299            monomial,
1300            coeff_num: sign * num / g,
1301            coeff_den: sign * den / g,
1302        }
1303    }
1304    /// True iff the coefficient is zero.
1305    pub fn is_zero(&self) -> bool {
1306        self.coeff_num == 0
1307    }
1308    /// Multiply coefficient by p/q.
1309    pub fn scale(&self, num: i64, den: i64) -> Term {
1310        Term::rational(
1311            self.monomial.clone(),
1312            self.coeff_num * num,
1313            self.coeff_den * den,
1314        )
1315    }
1316}
1317/// Ideal membership test using Gröbner basis normal form.
1318pub struct IdealMembership {
1319    /// The Gröbner basis of the ideal.
1320    pub basis: GroebnerBasis,
1321}
1322impl IdealMembership {
1323    /// Create an ideal membership tester from a Gröbner basis.
1324    pub fn new(basis: GroebnerBasis) -> Self {
1325        Self { basis }
1326    }
1327    /// Test whether `f` belongs to the ideal generated by `basis.generators`.
1328    pub fn contains(&self, f: &Polynomial) -> bool {
1329        self.basis.reduce_to_normal_form(f).is_zero()
1330    }
1331}
1332/// Syzygy computation: compute syzygies and free resolutions of a module.
1333pub struct SyzygiesComputation {
1334    /// A string representation of the module (e.g. "k[x,y]^r / M").
1335    pub module: String,
1336    /// Generator count (rank of the free module).
1337    pub rank: usize,
1338}
1339impl SyzygiesComputation {
1340    /// Create a new syzygy computation for the given module.
1341    pub fn new(module: String, rank: usize) -> Self {
1342        Self { module, rank }
1343    }
1344    /// Compute the first syzygy module Syz(f_1,…,f_r).
1345    ///
1346    /// Uses the S-polynomial method: syzygies correspond to pairs (i,j) with
1347    /// S(f_i, f_j) reducing to zero in the Gröbner basis.
1348    pub fn compute_syzygies(&self) -> Vec<String> {
1349        (0..self.rank)
1350            .flat_map(|i| {
1351                (i + 1..self.rank)
1352                    .map(move |j| {
1353                        format!(
1354                            "Syz(f_{i}, f_{j}): e_{i} lcm(lm(f_{i}),lm(f_{j}))/lm(f_{i}) - e_{j} lcm(lm(f_{i}),lm(f_{j}))/lm(f_{j})"
1355                        )
1356                    })
1357            })
1358            .collect()
1359    }
1360    /// Compute a minimal free resolution of the module.
1361    ///
1362    /// Returns a description of each step F_i → F_{i-1} → … → F_0 → M → 0.
1363    pub fn free_resolution(&self) -> Vec<String> {
1364        let mut steps = Vec::new();
1365        let mut current_rank = self.rank;
1366        let mut depth = 0usize;
1367        while current_rank > 0 && depth < self.rank + 1 {
1368            let next_rank = if current_rank > 1 {
1369                current_rank - 1
1370            } else {
1371                0
1372            };
1373            steps.push(format!(
1374                "F_{depth}: k[vars]^{current_rank} --d_{depth}--> F_{prev}",
1375                prev = if depth == 0 {
1376                    "M".to_string()
1377                } else {
1378                    format!("{}", depth - 1)
1379                },
1380            ));
1381            current_rank = next_rank;
1382            depth += 1;
1383        }
1384        steps
1385    }
1386}
1387/// A minimal free resolution 0 → F_n → … → F_1 → F_0 → M → 0.
1388#[derive(Debug, Clone)]
1389pub struct MinimalFreeResolution {
1390    /// The steps of the resolution.
1391    pub steps: Vec<ResolutionStep>,
1392    /// Projective dimension of M = length of the resolution.
1393    pub projective_dim: usize,
1394}
1395impl MinimalFreeResolution {
1396    /// Compute Betti numbers β_i = rank(F_i).
1397    pub fn betti_numbers(&self) -> Vec<usize> {
1398        self.steps.iter().map(|s| s.source_rank).collect()
1399    }
1400}