ocas_poly/groebner/mod.rs
1//! Gröbner basis computation for multivariate polynomial ideals.
2//!
3//! Provides three algorithms, all reachable through the unified
4//! [`groebner_basis`] entry point with an [`Algorithm`] selector:
5//!
6//! - **Buchberger** ([`buchberger`]) — classic S-polynomial iteration with
7//! Gebauer-Moeller optimization. Suitable for small ideals.
8//! - **F4** ([`f4::f4`]) — matrix-based algorithm from Faugère (1999).
9//! Dramatically faster for larger ideals by batching S-polynomial
10//! reductions into sparse matrix row operations.
11//! - **F5** ([`f5::f5`]) — signature-based algorithm from Faugère (2002).
12//! Rejects zero-reducers *before* matrix construction via syzygy
13//! criteria, targeting order-of-magnitude speedups on difficult ideals
14//! (e.g. cyclic-n). Production-grade since 0.19.0.
15//! - **MultiModular** ([`multi_modular`]) — multi-prime strategy for ℚ
16//! ideals since 0.25.0: parallel F5 over lucky primes, CRT + rational
17//! reconstruction, exact ℚ verification, and a p-adic Hensel-lift
18//! shortcut.
19//!
20//! All algorithms produce a reduced Gröbner basis. [`Algorithm::Auto`]
21//! routes ℚ ideals through the multi-modular pipeline and other domains
22//! through F4.
23
24pub mod f4;
25pub mod f5;
26pub mod fglm;
27pub mod hilbert;
28pub mod multi_modular;
29pub(crate) mod packed;
30
31use ocas_core::FastHashSet as HashSet;
32use ocas_domain::Domain;
33
34use crate::sparse::{
35 MonomialOrder, SparseMultivariatePolynomial, monomial_are_coprime, monomial_divides,
36};
37
38/// A Gröbner basis for a polynomial ideal.
39#[derive(Debug, Clone, PartialEq, Eq)]
40pub struct GroebnerBasis<D: Domain, O: MonomialOrder> {
41 /// The polynomials forming the basis.
42 pub basis: Vec<SparseMultivariatePolynomial<D, O>>,
43}
44
45impl<D: Domain, O: MonomialOrder> GroebnerBasis<D, O> {
46 /// Compute a Gröbner basis from a set of generators using Buchberger's algorithm.
47 ///
48 /// Requires that the coefficient domain supports exact division (i.e., is
49 /// effectively a field). The algorithm will panic if division fails.
50 ///
51 /// # Example
52 ///
53 /// ```
54 /// use ocas_domain::{RationalDomain, Rational};
55 /// use ocas_poly::sparse::Lex;
56 /// use ocas_poly::GroebnerBasis;
57 /// use ocas_poly::SparseMultivariatePolynomial;
58 ///
59 /// let d = RationalDomain;
60 /// // ideal: x + y, x - y
61 /// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
62 /// (vec![1, 0], Rational::new(1, 1)),
63 /// (vec![0, 1], Rational::new(1, 1)),
64 /// ]);
65 /// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
66 /// (vec![1, 0], Rational::new(1, 1)),
67 /// (vec![0, 1], Rational::new(-1, 1)),
68 /// ]);
69 /// let gb = GroebnerBasis::buchberger(&[f1, f2]);
70 /// assert!(gb.basis.len() >= 2);
71 /// ```
72 pub fn buchberger(ideal: &[SparseMultivariatePolynomial<D, O>]) -> Self {
73 // Filter out zero polynomials.
74 let mut basis: Vec<SparseMultivariatePolynomial<D, O>> =
75 ideal.iter().filter(|p| !p.is_zero()).cloned().collect();
76 if basis.is_empty() {
77 return Self { basis };
78 }
79
80 // Collect critical pairs: all unordered pairs (i, j) with i < j.
81 let mut pairs: HashSet<(usize, usize)> = HashSet::default();
82 for i in 0..basis.len() {
83 for j in i + 1..basis.len() {
84 pairs.insert((i, j));
85 }
86 }
87
88 let max_iter = 10000;
89
90 for _ in 0..max_iter {
91 if pairs.is_empty() {
92 break;
93 }
94 let (i, j) = *pairs.iter().next().unwrap();
95 pairs.remove(&(i, j));
96
97 // Buchberger's first criterion: if the leading monomials are
98 // coprime, the S-polynomial reduces to zero, so skip.
99 let lm_i = basis[i].leading_monomial();
100 let lm_j = basis[j].leading_monomial();
101 if let (Some(mi), Some(mj)) = (&lm_i, &lm_j)
102 && monomial_are_coprime(mi, mj)
103 {
104 continue;
105 }
106
107 // Compute S-polynomial and reduce by current basis.
108 let s = basis[i].spoly(&basis[j]);
109 let r = s.reduce(&basis);
110
111 if !r.is_zero() {
112 let new_idx = basis.len();
113 basis.push(r);
114 for k in 0..new_idx {
115 pairs.insert((k, new_idx));
116 }
117 }
118 }
119
120 Self { basis }
121 }
122
123 /// Minimize the basis: remove polynomials whose leading monomial is
124 /// divisible by another element's leading monomial.
125 pub fn minimize(mut self) -> Self {
126 let lms: Vec<_> = self
127 .basis
128 .iter()
129 .filter_map(|p| p.leading_monomial().cloned())
130 .collect();
131
132 let mut keep = vec![true; self.basis.len()];
133 for i in 0..self.basis.len() {
134 for j in 0..self.basis.len() {
135 // Remove i if lms[j] divides lms[i] (i.e., lms[i] is a
136 // multiple of lms[j], making i redundant).
137 // monomial_divides(big, small) returns true when small divides big.
138 if i != j && keep[i] && keep[j] && monomial_divides(&lms[i], &lms[j]) {
139 keep[i] = false;
140 break;
141 }
142 }
143 }
144
145 self.basis = self
146 .basis
147 .into_iter()
148 .enumerate()
149 .filter(|(i, _)| keep[*i])
150 .map(|(_, p)| p)
151 .collect();
152
153 self
154 }
155
156 /// Inter-reduce the basis: reduce each element by the others and make
157 /// each polynomial monic.
158 ///
159 /// The algorithm processes elements in ascending order of leading
160 /// monomial. Each element is reduced by all elements with strictly
161 /// smaller leading monomials (those already in the result set).
162 /// This ensures the standard reduced Gröbner basis property:
163 /// no monomial of any basis element is divisible by the leading
164 /// monomial of any other basis element.
165 pub fn auto_reduce(mut self) -> Self {
166 let order = self
167 .basis
168 .first()
169 .map(|p| p.order.clone())
170 .unwrap_or_default();
171 // Sort basis in ascending order of leading monomial (smallest first).
172 self.basis
173 .sort_by(|a, b| match (a.leading_monomial(), b.leading_monomial()) {
174 (Some(ma), Some(mb)) => order.cmp(ma, mb),
175 (Some(_), None) => std::cmp::Ordering::Greater,
176 (None, Some(_)) => std::cmp::Ordering::Less,
177 (None, None) => std::cmp::Ordering::Equal,
178 });
179
180 let mut reduced: Vec<SparseMultivariatePolynomial<D, O>> = Vec::new();
181
182 for poly in &self.basis {
183 // Reduce `poly` by all elements already in `reduced`
184 // (which have smaller leading monomials).
185 let mut r = poly.reduce(&reduced);
186 if !r.is_zero() {
187 if let Some(lc) = r.leading_coeff().cloned()
188 && let Some(inv) = r.domain().inv(&lc)
189 {
190 r = r.mul_scalar(&inv);
191 }
192 reduced.push(r);
193 }
194 }
195
196 self.basis = reduced;
197 self
198 }
199
200 /// Verify that this is indeed a Gröbner basis by checking that all
201 /// S-polynomials reduce to zero.
202 pub fn is_groebner_basis(&self) -> bool {
203 for i in 0..self.basis.len() {
204 for j in i + 1..self.basis.len() {
205 let s = self.basis[i].spoly(&self.basis[j]);
206 let r = s.reduce(&self.basis);
207 if !r.is_zero() {
208 return false;
209 }
210 }
211 }
212 true
213 }
214
215 /// Change the monomial order of this Gröbner basis.
216 ///
217 /// The polynomials are re-interpreted under the target order `O2`
218 /// and the F4 algorithm is re-run. This is the simple reorder path
219 /// (Symbolica's `reorder::<Order>()`). For zero-dimensional ideals,
220 /// use [`crate::groebner::fglm::fglm`] for a much faster conversion.
221 ///
222 /// # Example
223 ///
224 /// ```
225 /// use ocas_domain::{RationalDomain, Rational};
226 /// use ocas_poly::sparse::{Grevlex, Lex};
227 /// use ocas_poly::{GroebnerBasis, SparseMultivariatePolynomial, f4};
228 ///
229 /// let d = RationalDomain;
230 /// // ideal: x + y, x - y → basis {y, x} under Lex
231 /// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
232 /// (vec![1, 0], Rational::new(1, 1)),
233 /// (vec![0, 1], Rational::new(1, 1)),
234 /// ]);
235 /// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
236 /// (vec![1, 0], Rational::new(1, 1)),
237 /// (vec![0, 1], Rational::new(-1, 1)),
238 /// ]);
239 /// let gb_lex = f4::f4(&[f1, f2]);
240 /// let gb_grevlex = gb_lex.reorder::<Grevlex>();
241 /// assert!(gb_grevlex.is_groebner_basis());
242 /// ```
243 pub fn reorder<O2: MonomialOrder>(&self) -> GroebnerBasis<D, O2>
244 where
245 D: 'static,
246 {
247 let converted: Vec<SparseMultivariatePolynomial<D, O2>> = self
248 .basis
249 .iter()
250 .map(|p| {
251 SparseMultivariatePolynomial::from_terms(
252 p.domain().clone(),
253 p.n_vars(),
254 p.terms_ref()
255 .iter()
256 .map(|(e, c)| (e.to_vec(), c.clone()))
257 .collect(),
258 )
259 })
260 .collect();
261 crate::groebner::f4::f4(&converted)
262 }
263}
264
265/// Convenience: compute a Gröbner basis and inter-reduce it.
266pub fn buchberger<D: Domain, O: MonomialOrder>(
267 ideal: &[SparseMultivariatePolynomial<D, O>],
268) -> GroebnerBasis<D, O> {
269 GroebnerBasis::buchberger(ideal).minimize().auto_reduce()
270}
271
272/// Algorithm selector for [`groebner_basis`].
273///
274/// `Auto` picks a backend based on the ideal's size and structure; the
275/// other variants force a specific algorithm. See [`groebner_basis`] for
276/// the unified entry point.
277#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
278pub enum Algorithm {
279 /// Automatically select the most suitable algorithm: the multi-modular
280 /// pipeline for ℚ ideals (fast path since 0.25.0), F4 otherwise.
281 #[default]
282 Auto,
283 /// Force the F4 matrix algorithm (Faugère 1999).
284 F4,
285 /// Force the F5 signature-based algorithm (Faugère 2002).
286 F5,
287 /// Force Buchberger's classic S-polynomial iteration.
288 Buchberger,
289 /// Force the multi-modular pipeline ([`crate::groebner::multi_modular`]):
290 /// parallel F5 over lucky primes + CRT/rational reconstruction + exact
291 /// ℚ verification, with a Hensel-lift shortcut. Only applies to ℚ
292 /// coefficients; other domains fall back to F4.
293 MultiModular,
294}
295
296/// Compute a Gröbner basis using the requested [`Algorithm`].
297///
298/// This is the unified entry point for Gröbner basis computation. Zero
299/// polynomials in `ideal` are filtered internally by each backend.
300///
301/// [`Algorithm::Auto`] routes ℚ ideals through the multi-modular pipeline
302/// (the fast path for rational coefficients since 0.25.0) and other
303/// domains through F4.
304///
305/// # Example
306///
307/// ```
308/// use ocas_domain::{RationalDomain, Rational};
309/// use ocas_poly::sparse::Lex;
310/// use ocas_poly::{Algorithm, groebner_basis, SparseMultivariatePolynomial};
311///
312/// let d = RationalDomain;
313/// // ideal: x + y, x - y
314/// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
315/// (vec![1, 0], Rational::new(1, 1)),
316/// (vec![0, 1], Rational::new(1, 1)),
317/// ]);
318/// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
319/// (vec![1, 0], Rational::new(1, 1)),
320/// (vec![0, 1], Rational::new(-1, 1)),
321/// ]);
322/// let gb = groebner_basis(&[f1, f2], Algorithm::Auto);
323/// assert!(gb.is_groebner_basis());
324/// ```
325pub fn groebner_basis<D: Domain + 'static, O: MonomialOrder + Send + Sync>(
326 ideal: &[SparseMultivariatePolynomial<D, O>],
327 algo: Algorithm,
328) -> GroebnerBasis<D, O> {
329 match algo {
330 // Auto: multi-modular for ℚ ideals (the internal Any check returns
331 // None for other domains, which then take the F4 path).
332 Algorithm::Auto => match multi_modular::groebner_basis_mm(ideal) {
333 Some(gb) => gb,
334 None => f4::f4(ideal),
335 },
336 Algorithm::F4 => f4::f4(ideal),
337 Algorithm::F5 => f5::f5(ideal),
338 Algorithm::Buchberger => buchberger(ideal),
339 Algorithm::MultiModular => match multi_modular::groebner_basis_mm(ideal) {
340 Some(gb) => gb,
341 None => f4::f4(ideal),
342 },
343 }
344}
345
346/// Eliminate variables from an ideal.
347///
348/// Returns the Gröbner basis of `I ∩ k[x_{elim_vars}, ..., x_{n-1}]`, i.e.,
349/// the polynomials in the basis that do not involve the first `elim_vars`
350/// variables. Uses Lex ordering which is a natural elimination order:
351/// under Lex, the reduced GB of an ideal automatically contains the
352/// elimination ideal's generators.
353///
354/// # Example
355///
356/// ```
357/// use ocas_domain::{RationalDomain, Rational};
358/// use ocas_poly::sparse::Lex;
359/// use ocas_poly::{SparseMultivariatePolynomial, eliminate, Algorithm};
360///
361/// let d = RationalDomain;
362/// // Ideal: x + y + z, x*y + x*z in k[x,y,z]; eliminate x.
363/// let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 3, vec![
364/// (vec![1, 0, 0], Rational::new(1, 1)),
365/// (vec![0, 1, 0], Rational::new(1, 1)),
366/// (vec![0, 0, 1], Rational::new(1, 1)),
367/// ]);
368/// let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 3, vec![
369/// (vec![1, 1, 0], Rational::new(1, 1)),
370/// (vec![1, 0, 1], Rational::new(1, 1)),
371/// ]);
372/// let elim = eliminate(&[f1, f2], 1, Algorithm::Auto);
373/// // Result should be in k[y,z]
374/// for p in &elim.basis {
375/// assert!(p.degree_in(0) == 0, "eliminated variable x should not appear");
376/// }
377/// ```
378pub fn eliminate<D: Domain + 'static>(
379 ideal: &[SparseMultivariatePolynomial<D, crate::sparse::Lex>],
380 elim_vars: usize,
381 algo: Algorithm,
382) -> GroebnerBasis<D, crate::sparse::Lex> {
383 let n_vars = ideal.first().map(|p| p.n_vars()).unwrap_or(0);
384 assert!(
385 elim_vars <= n_vars,
386 "elim_vars ({elim_vars}) must be <= n_vars ({n_vars})"
387 );
388 if ideal.is_empty() {
389 return GroebnerBasis { basis: vec![] };
390 }
391
392 // Compute Gröbner basis under Lex ordering.
393 // Lex is a natural elimination order: polynomials in the GB that
394 // don't involve x_0,...,x_{s-1} form a GB of the elimination ideal.
395 let gb = groebner_basis(ideal, algo);
396
397 // Filter: keep only polynomials that don't involve the eliminated variables.
398 let filtered: Vec<SparseMultivariatePolynomial<D, crate::sparse::Lex>> = gb
399 .basis
400 .into_iter()
401 .filter(|p| {
402 p.terms_ref()
403 .keys()
404 .all(|exp| exp.iter().take(elim_vars).all(|&e| e == 0))
405 })
406 .collect();
407
408 GroebnerBasis { basis: filtered }
409}
410
411#[cfg(test)]
412mod tests {
413 use super::*;
414 use crate::sparse::Lex;
415 use ocas_domain::{Rational, RationalDomain};
416
417 fn r(n: i64, d: i64) -> Rational {
418 Rational::new(n, d)
419 }
420
421 fn make_poly(
422 terms: Vec<(Vec<usize>, Rational)>,
423 ) -> SparseMultivariatePolynomial<RationalDomain, Lex> {
424 SparseMultivariatePolynomial::from_terms(RationalDomain, 2, terms)
425 }
426
427 #[test]
428 fn empty_ideal() {
429 let gb = buchberger::<RationalDomain, Lex>(&[]);
430 assert!(gb.basis.is_empty());
431 }
432
433 #[test]
434 fn single_polynomial() {
435 // f = x^2 - 1
436 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
437 RationalDomain,
438 1,
439 vec![(vec![2], r(1, 1)), (vec![0], r(-1, 1))],
440 );
441 let gb = buchberger(&[f]);
442 assert_eq!(gb.basis.len(), 1);
443 assert!(gb.is_groebner_basis());
444 }
445
446 #[test]
447 fn linear_system() {
448 // x + y = 0, x - y = 0 → basis = {x, y}
449 let f1 = make_poly(vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(1, 1))]);
450 let f2 = make_poly(vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))]);
451 let gb = buchberger(&[f1, f2]);
452 assert!(gb.is_groebner_basis());
453 // After auto-reduce, we expect {x, y} (monic leading terms)
454 assert!(gb.basis.len() >= 2);
455 }
456
457 #[test]
458 fn two_variable_ideal() {
459 // x^2 - y, x^3 - x (elimination ideal: y = x^2, x^3 = x → x ∈ {0, ±1})
460 let f1 = make_poly(vec![(vec![2, 0], r(1, 1)), (vec![0, 1], r(-1, 1))]);
461 let f2 = make_poly(vec![(vec![3, 0], r(1, 1)), (vec![1, 0], r(-1, 1))]);
462 let gb = buchberger(&[f1, f2]);
463 assert!(gb.is_groebner_basis());
464 assert!(!gb.basis.is_empty());
465 }
466
467 // --- Step 1a: Lex order verification ---
468
469 #[test]
470 fn lex_cyclic_3() {
471 // Cyclic-3: x+y+z, xy+yz+zx, xyz-1 under Lex.
472 let d = RationalDomain;
473 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
474 d,
475 3,
476 vec![
477 (vec![1, 0, 0], r(1, 1)),
478 (vec![0, 1, 0], r(1, 1)),
479 (vec![0, 0, 1], r(1, 1)),
480 ],
481 );
482 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
483 d,
484 3,
485 vec![
486 (vec![1, 1, 0], r(1, 1)),
487 (vec![0, 1, 1], r(1, 1)),
488 (vec![1, 0, 1], r(1, 1)),
489 ],
490 );
491 let f3 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
492 d,
493 3,
494 vec![(vec![1, 1, 1], r(1, 1)), (vec![0, 0, 0], r(-1, 1))],
495 );
496 let gb = groebner_basis(&[f1, f2, f3], Algorithm::F4);
497 assert!(gb.is_groebner_basis());
498 // Print basis for debugging.
499 for (i, p) in gb.basis.iter().enumerate() {
500 eprintln!("lex_cyclic_3 gb[{i}]: {p:?}");
501 }
502 // Under Lex, the GB should be triangular (each poly introduces
503 // one fewer variable). The smallest variable (z) should appear
504 // in a univariate polynomial.
505 let has_univariate_in_z = gb
506 .basis
507 .iter()
508 .any(|p| p.terms_ref().keys().all(|e| e[0] == 0 && e[1] == 0));
509 assert!(
510 has_univariate_in_z,
511 "Lex GB should contain a univariate poly in z"
512 );
513 }
514
515 #[test]
516 fn lex_two_variable_elimination() {
517 // Ideal: x^2 - y, x^3 - x in k[x,y] under Lex.
518 // Lex GB should eliminate x: expect y^2 - y, xy - x, x^2 - y.
519 let d = RationalDomain;
520 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
521 d,
522 2,
523 vec![(vec![2, 0], r(1, 1)), (vec![0, 1], r(-1, 1))],
524 );
525 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
526 d,
527 2,
528 vec![(vec![3, 0], r(1, 1)), (vec![1, 0], r(-1, 1))],
529 );
530 let gb = groebner_basis(&[f1, f2], Algorithm::F4);
531 assert!(gb.is_groebner_basis());
532 // The GB should be triangular: first poly in y only, then xy, then x^2.
533 // Find the univariate poly in y.
534 let y_poly = gb
535 .basis
536 .iter()
537 .find(|p| p.terms_ref().keys().all(|e| e[0] == 0));
538 assert!(
539 y_poly.is_some(),
540 "Lex GB should contain a univariate poly in y"
541 );
542 }
543
544 // --- Step 1c: eliminate() tests ---
545
546 #[test]
547 fn eliminate_simple() {
548 // Eliminate x from {x + y, x - y} in k[x,y].
549 // x + y = 0 and x - y = 0 ⟹ x = 0, y = 0.
550 // Eliminating x should give {y} (or just y = 0).
551 let d = RationalDomain;
552 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
553 d,
554 2,
555 vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(1, 1))],
556 );
557 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
558 d,
559 2,
560 vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))],
561 );
562 let elim = eliminate(&[f1, f2], 1, Algorithm::F4);
563 assert!(!elim.basis.is_empty());
564 // All result polynomials should be in y only.
565 for p in &elim.basis {
566 assert_eq!(p.degree_in(0), 0, "eliminated var x should not appear");
567 }
568 }
569
570 #[test]
571 fn eliminate_cox_little_oshea() {
572 // Cox-Little-O'Shea §3.1: eliminate x from {x+y+z-1, xy+xz, xyz}.
573 let d = RationalDomain;
574 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
575 d,
576 3,
577 vec![
578 (vec![1, 0, 0], r(1, 1)),
579 (vec![0, 1, 0], r(1, 1)),
580 (vec![0, 0, 1], r(1, 1)),
581 (vec![0, 0, 0], r(-1, 1)),
582 ],
583 );
584 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
585 d,
586 3,
587 vec![(vec![1, 1, 0], r(1, 1)), (vec![1, 0, 1], r(1, 1))],
588 );
589 let f3 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
590 d,
591 3,
592 vec![(vec![1, 1, 1], r(1, 1))],
593 );
594 let elim = eliminate(&[f1, f2, f3], 1, Algorithm::F4);
595 // All result polynomials should be in y, z only.
596 for p in &elim.basis {
597 assert_eq!(p.degree_in(0), 0, "x should be eliminated");
598 }
599 // Should contain y^2 + z^2 - y - z and yz + z^2 - z (or equivalent).
600 assert!(
601 elim.basis.len() >= 2,
602 "expected at least 2 generators, got {}",
603 elim.basis.len()
604 );
605 }
606}