1use ocas_domain::Domain;
15use ocas_domain::{Rational, RationalDomain};
16
17use crate::groebner::{Algorithm, GroebnerBasis, groebner_basis};
18use crate::sparse::{Lex, SparseMultivariatePolynomial};
19
20pub fn ideal_contains<D: Domain + 'static>(
45 generators: &[SparseMultivariatePolynomial<D, Lex>],
46 f: &SparseMultivariatePolynomial<D, Lex>,
47 algo: Algorithm,
48) -> bool {
49 if generators.is_empty() {
50 return f.is_zero();
51 }
52 let gb = groebner_basis(generators, algo);
53 let remainder = f.reduce(&gb.basis);
54 remainder.is_zero()
55}
56
57pub fn ideal_sum<D: Domain + 'static>(
79 generators_a: &[SparseMultivariatePolynomial<D, Lex>],
80 generators_b: &[SparseMultivariatePolynomial<D, Lex>],
81) -> GroebnerBasis<D, Lex> {
82 let mut combined = generators_a.to_vec();
83 combined.extend(generators_b.iter().cloned());
84 groebner_basis(&combined, Algorithm::Auto)
85}
86
87pub fn ideal_product<D: Domain + 'static>(
109 generators_a: &[SparseMultivariatePolynomial<D, Lex>],
110 generators_b: &[SparseMultivariatePolynomial<D, Lex>],
111) -> GroebnerBasis<D, Lex> {
112 let products: Vec<SparseMultivariatePolynomial<D, Lex>> = generators_a
113 .iter()
114 .flat_map(|f| generators_b.iter().map(move |g| f.mul(g)))
115 .collect();
116 groebner_basis(&products, Algorithm::Auto)
117}
118
119pub fn ideal_quotient<D: Domain + 'static>(
151 generators_i: &[SparseMultivariatePolynomial<D, Lex>],
152 generators_j: &[SparseMultivariatePolynomial<D, Lex>],
153) -> GroebnerBasis<D, Lex> {
154 if generators_i.is_empty() || generators_j.is_empty() {
155 return GroebnerBasis { basis: vec![] };
156 }
157
158 let mut result: Option<Vec<SparseMultivariatePolynomial<D, Lex>>> = None;
160
161 for g in generators_j {
162 if g.is_zero() {
163 continue;
164 }
165 let i_colon_g = quotient_single_generator(generators_i, g);
166
167 if let Some(current) = result.take() {
168 result = Some(intersect_generators(¤t, &i_colon_g));
169 } else {
170 result = Some(i_colon_g);
171 }
172 }
173
174 match result {
175 None => GroebnerBasis { basis: vec![] },
176 Some(gens) => {
177 if gens.is_empty() {
178 GroebnerBasis { basis: vec![] }
179 } else {
180 groebner_basis(&gens, Algorithm::Auto)
181 }
182 }
183 }
184}
185
186fn quotient_single_generator<D: Domain + 'static>(
191 generators_i: &[SparseMultivariatePolynomial<D, Lex>],
192 g: &SparseMultivariatePolynomial<D, Lex>,
193) -> Vec<SparseMultivariatePolynomial<D, Lex>> {
194 let n_vars = g.n_vars();
195 let domain = g.domain().clone();
196
197 let embedded: Vec<SparseMultivariatePolynomial<D, Lex>> = generators_i
199 .iter()
200 .map(|p| p.embed_new_main())
201 .collect();
202
203 let g_embedded = g.embed_new_main();
205 let w = {
206 let mut exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
207 exp[0] = 1;
208 SparseMultivariatePolynomial::from_terms(domain.clone(), n_vars + 1, vec![
209 (exp.to_vec(), domain.one()),
210 ])
211 };
212 let wg = w.mul(&g_embedded);
213 let one_minus_wg = {
214 let one_exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
215 let one = SparseMultivariatePolynomial::from_terms(domain.clone(), n_vars + 1, vec![
216 (one_exp.to_vec(), domain.one()),
217 ]);
218 one.sub(&wg)
219 };
220
221 let mut combined = embedded;
222 combined.push(one_minus_wg);
223
224 let elim_gb = crate::groebner::eliminate(&combined, 1, Algorithm::Auto);
226 elim_gb.basis.into_iter().map(|p| p.drop_variable(0)).collect()
227}
228
229fn intersect_generators<D: Domain + 'static>(
234 generators_a: &[SparseMultivariatePolynomial<D, Lex>],
235 generators_b: &[SparseMultivariatePolynomial<D, Lex>],
236) -> Vec<SparseMultivariatePolynomial<D, Lex>> {
237 let n_vars = generators_a
238 .first()
239 .or(generators_b.first())
240 .map(|p| p.n_vars())
241 .unwrap_or(0);
242 let domain = generators_a
243 .first()
244 .or(generators_b.first())
245 .map(|p| p.domain().clone())
246 .unwrap_or_else(|| {
247 unreachable!("intersect_generators called with empty inputs")
249 });
250
251 if generators_a.is_empty() || generators_b.is_empty() {
252 return vec![];
253 }
254
255 let t = {
257 let mut exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
258 exp[0] = 1;
259 SparseMultivariatePolynomial::from_terms(domain.clone(), n_vars + 1, vec![
260 (exp.to_vec(), domain.one()),
261 ])
262 };
263 let one_minus_t = {
264 let one_exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
265 let one = SparseMultivariatePolynomial::from_terms(domain.clone(), n_vars + 1, vec![
266 (one_exp.to_vec(), domain.one()),
267 ]);
268 one.sub(&t)
269 };
270
271 let mut combined: Vec<SparseMultivariatePolynomial<D, Lex>> = Vec::new();
272
273 for f in generators_a {
275 let f_emb = f.embed_new_main();
276 combined.push(t.mul(&f_emb));
277 }
278 for g in generators_b {
280 let g_emb = g.embed_new_main();
281 combined.push(one_minus_t.mul(&g_emb));
282 }
283
284 let elim_gb = crate::groebner::eliminate(&combined, 1, Algorithm::Auto);
286 elim_gb.basis.into_iter().map(|p| p.drop_variable(0)).collect()
287}
288
289pub fn ideal_intersection<D: Domain + 'static>(
314 generators_a: &[SparseMultivariatePolynomial<D, Lex>],
315 generators_b: &[SparseMultivariatePolynomial<D, Lex>],
316) -> GroebnerBasis<D, Lex> {
317 if generators_a.is_empty() || generators_b.is_empty() {
318 return GroebnerBasis { basis: vec![] };
319 }
320 let gens = intersect_generators(generators_a, generators_b);
321 if gens.is_empty() {
322 GroebnerBasis { basis: vec![] }
323 } else {
324 groebner_basis(&gens, Algorithm::Auto)
325 }
326}
327
328pub fn ideal_saturate<D: Domain + 'static>(
355 generators_i: &[SparseMultivariatePolynomial<D, Lex>],
356 generators_j: &[SparseMultivariatePolynomial<D, Lex>],
357) -> GroebnerBasis<D, Lex> {
358 if generators_i.is_empty() {
359 return GroebnerBasis { basis: vec![] };
360 }
361 if generators_j.is_empty() {
362 return groebner_basis(generators_i, Algorithm::Auto);
363 }
364
365 let mut current_gens = generators_i.to_vec();
366 let max_iter = 20;
367
368 for _ in 0..max_iter {
369 let current_gb = groebner_basis(¤t_gens, Algorithm::Auto);
370 let next = ideal_quotient(¤t_gb.basis, generators_j);
371 let next_gb = groebner_basis(&next.basis, Algorithm::Auto);
372
373 let all_in_current = next_gb
375 .basis
376 .iter()
377 .all(|p| p.reduce(¤t_gb.basis).is_zero());
378 let all_in_next = current_gb
379 .basis
380 .iter()
381 .all(|p| p.reduce(&next_gb.basis).is_zero());
382
383 if all_in_current && all_in_next {
384 return current_gb;
385 }
386 current_gens = next_gb.basis;
387 }
388
389 groebner_basis(¤t_gens, Algorithm::Auto)
390}
391
392#[derive(Debug, Clone)]
398pub struct RealSolution {
399 pub values: Vec<f64>,
401 pub multiplicity: usize,
403}
404
405#[derive(Debug, Clone)]
407pub struct ZeroDimSolutions {
408 pub solutions: Vec<RealSolution>,
410 pub vector_space_dimension: usize,
413}
414
415#[derive(Debug, Clone)]
417pub enum PolynomialSystemSolution {
418 ZeroDimensional(ZeroDimSolutions),
420 PositiveDimensional(GroebnerBasis<RationalDomain, Lex>),
422 Empty,
424}
425
426pub fn is_zero_dimensional(gb: &GroebnerBasis<RationalDomain, Lex>) -> bool {
454 let n_vars = match gb.basis.first() {
455 Some(p) => p.n_vars(),
456 None => return false, };
458
459 for var in 0..n_vars {
461 let has_pure_power = gb.basis.iter().any(|p| {
462 match p.leading_monomial() {
463 Some(lm) => {
464 lm.iter()
465 .enumerate()
466 .all(|(i, &e)| if i == var { e > 0 } else { e == 0 })
467 }
468 None => false,
469 }
470 });
471 if !has_pure_power {
472 return false;
473 }
474 }
475 true
476}
477
478fn extract_univariate(
482 poly: &SparseMultivariatePolynomial<RationalDomain, Lex>,
483 var_index: usize,
484) -> crate::dense::DenseUnivariatePolynomial<RationalDomain> {
485 let d = RationalDomain;
486 let deg = poly.degree_in(var_index);
487 let mut coeffs = vec![ocas_domain::Rational::new(0, 1); deg + 1];
488 for (exp, coeff) in poly.terms_ref() {
489 let power = exp.get(var_index).copied().unwrap_or(0);
490 coeffs[power] = coeffs[power].clone() + coeff.clone();
491 }
492 crate::dense::DenseUnivariatePolynomial::from_coeffs(d, coeffs)
493}
494
495fn solve_univariate_f64(
498 poly: &SparseMultivariatePolynomial<RationalDomain, Lex>,
499 var_index: usize,
500 substituted_values: &[f64], ) -> Vec<f64> {
502 let d = RationalDomain;
505 let deg = poly.degree_in(var_index);
506 let mut coeffs_f64 = vec![0.0f64; deg + 1];
507
508 for (exp, coeff) in poly.terms_ref() {
509 let mut coeff_f = format!("{}", coeff).parse::<f64>().unwrap_or(0.0);
511 for (i, &e) in exp.iter().enumerate() {
512 if i > var_index && e > 0 {
513 let sub_idx = i - var_index - 1;
515 if sub_idx < substituted_values.len() {
516 coeff_f *= substituted_values[sub_idx].powi(e as i32);
517 }
518 }
519 }
520 let power = exp.get(var_index).copied().unwrap_or(0);
521 coeffs_f64[power] += coeff_f;
522 }
523
524 let rational_coeffs: Vec<ocas_domain::Rational> = coeffs_f64
526 .iter()
527 .map(|&c| rational_approx(c))
528 .collect();
529 let unipoly =
530 crate::dense::DenseUnivariatePolynomial::from_coeffs(d, rational_coeffs);
531 let intervals = unipoly.isolate_real_roots();
532 intervals
533 .iter()
534 .map(|iv| {
535 let refined = unipoly.refine_root(iv, 1e-14);
536 (refined.low + refined.high) / 2.0
537 })
538 .collect()
539}
540
541fn rational_approx(x: f64) -> ocas_domain::Rational {
543 if x == 0.0 {
544 return ocas_domain::Rational::new(0, 1);
545 }
546 let sign: i64 = if x < 0.0 { -1 } else { 1 };
547 let x_abs = x.abs();
548 let mut a = x_abs.floor() as i64;
549 let mut frac = x_abs - a as f64;
550 let mut prev_num = 1i64;
551 let mut prev_den = 0i64;
552 let mut num = a;
553 let mut den = 1i64;
554
555 for _ in 0..50 {
556 if frac.abs() < 1e-12 {
557 break;
558 }
559 let r = 1.0 / frac;
560 a = r.floor() as i64;
561 frac = r - a as f64;
562 let new_num = a * num + prev_num;
563 let new_den = a * den + prev_den;
564 prev_num = num;
565 prev_den = den;
566 num = new_num;
567 den = new_den;
568 if den > 1_000_000 {
569 break;
570 }
571 }
572 ocas_domain::Rational::new(sign * num, den)
573}
574
575pub fn solve_polynomial_system(
608 equations: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
609 algo: Algorithm,
610) -> PolynomialSystemSolution {
611 if equations.is_empty() {
612 return PolynomialSystemSolution::PositiveDimensional(GroebnerBasis { basis: vec![] });
613 }
614
615 let gb = groebner_basis(equations, algo);
616
617 if gb.basis.len() == 1
619 && gb.basis[0].terms_ref().len() == 1
620 && gb.basis[0]
621 .leading_monomial()
622 .map(|lm| lm.iter().all(|&e| e == 0))
623 .unwrap_or(false)
624 {
625 return PolynomialSystemSolution::Empty;
626 }
627
628 let gb_lex: GroebnerBasis<RationalDomain, Lex> = gb;
630 if !is_zero_dimensional(&gb_lex) {
634 return PolynomialSystemSolution::PositiveDimensional(gb_lex);
635 }
636
637 let solutions = solve_triangular(&gb_lex);
638 let dim = compute_vector_space_dim(&gb_lex).unwrap_or(solutions.len());
642
643 PolynomialSystemSolution::ZeroDimensional(ZeroDimSolutions {
644 solutions,
645 vector_space_dimension: dim,
646 })
647}
648
649fn compute_vector_space_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> Option<usize> {
653 let n_vars = gb.basis.first()?.n_vars();
654 let mut dim = 1usize;
655 for var in 0..n_vars {
656 let max_deg = gb
657 .basis
658 .iter()
659 .filter(|p| {
660 p.terms_ref()
661 .keys()
662 .all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
663 })
664 .map(|p| p.degree_in(var))
665 .max()
666 .unwrap_or(1);
667 dim = dim.checked_mul(max_deg)?;
668 }
669 Some(dim)
670}
671
672fn solve_triangular(
675 gb: &GroebnerBasis<RationalDomain, Lex>,
676) -> Vec<RealSolution> {
677 let n_vars = match gb.basis.first() {
678 Some(p) => p.n_vars(),
679 None => return vec![],
680 };
681
682 let raw = solve_recursive(gb, n_vars - 1, &[]);
684 raw.into_iter()
686 .map(|mut s| {
687 s.values.reverse();
688 s
689 })
690 .collect()
691}
692
693fn solve_recursive(
697 gb: &GroebnerBasis<RationalDomain, Lex>,
698 var_index: usize,
699 higher_values: &[f64],
700) -> Vec<RealSolution> {
701 let univariate_poly = gb.basis.iter().find(|p| {
704 p.degree_in(var_index) > 0
705 && p.terms_ref().keys().all(|e| {
706 e.iter()
707 .enumerate()
708 .all(|(i, &v)| i <= var_index || v == 0)
709 })
710 });
711
712 let roots = if let Some(poly) = univariate_poly {
713 let unipoly = extract_univariate(poly, var_index);
715 let intervals = unipoly.isolate_real_roots();
716 let r: Vec<f64> = intervals
717 .iter()
718 .map(|iv| {
719 let refined = unipoly.refine_root(iv, 1e-14);
720 (refined.low + refined.high) / 2.0
721 })
722 .collect();
723 r
724 } else {
725 let poly = gb.basis.iter().find(|p| p.degree_in(var_index) > 0);
728 let Some(poly) = poly else {
729 return vec![];
730 };
731 solve_univariate_f64(poly, var_index, higher_values)
732 };
733
734 if var_index == 0 {
735 roots
737 .into_iter()
738 .map(|v| RealSolution {
739 values: vec![v],
740 multiplicity: 1,
741 })
742 .collect()
743 } else {
744 let mut results = Vec::new();
746 for root in &roots {
747 let mut new_higher = Vec::with_capacity(higher_values.len() + 1);
748 new_higher.push(*root);
749 new_higher.extend_from_slice(higher_values);
750 let sub_solutions = solve_recursive(gb, var_index - 1, &new_higher);
751 for mut sol in sub_solutions {
752 sol.values.push(*root);
753 results.push(sol);
754 }
755 }
756 results
757 }
758}
759
760#[derive(Debug, Clone)]
766pub struct PrimaryComponent {
767 pub primary: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
769 pub prime: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
771}
772
773pub fn ideal_radical(
799 generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
800) -> GroebnerBasis<RationalDomain, Lex> {
801 if generators.is_empty() {
802 return GroebnerBasis { basis: vec![] };
803 }
804
805 let gb = groebner_basis(generators, Algorithm::Auto);
806
807 if is_zero_dimensional(&gb) {
808 radical_zero_dim(&gb)
809 } else {
810 radical_via_jacobian(&gb)
815 }
816}
817
818fn radical_zero_dim(
821 gb: &GroebnerBasis<RationalDomain, Lex>,
822) -> GroebnerBasis<RationalDomain, Lex> {
823 let n_vars = match gb.basis.first() {
824 Some(p) => p.n_vars(),
825 None => return GroebnerBasis { basis: vec![] },
826 };
827
828 let domain = RationalDomain;
829 let mut radical_gens: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> =
830 Vec::new();
831
832 for var in 0..n_vars {
834 let univariate = gb.basis.iter().find(|p| {
835 p.terms_ref()
836 .keys()
837 .all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
838 && p.degree_in(var) > 0
839 });
840
841 if let Some(poly) = univariate {
842 let unipoly = extract_univariate(poly, var);
843 let deriv = unipoly.derivative();
845 let g = unipoly.gcd(&deriv);
846 let sqf = unipoly.div_rem(&g).map(|(q, _)| q).unwrap_or(unipoly);
847
848 let terms: Vec<(Vec<usize>, ocas_domain::Rational)> = sqf
850 .coeffs()
851 .iter()
852 .enumerate()
853 .filter(|(_, c)| !domain.is_zero(c))
854 .map(|(i, c)| {
855 let mut exp = vec![0usize; n_vars];
856 exp[var] = i;
857 (exp, c.clone())
858 })
859 .collect();
860 if !terms.is_empty() {
861 radical_gens
862 .push(SparseMultivariatePolynomial::from_terms(domain, n_vars, terms));
863 }
864 }
865 }
866
867 for p in &gb.basis {
869 let is_univariate = p
870 .terms_ref()
871 .keys()
872 .any(|e| e.iter().filter(|&&v| v > 0).count() <= 1);
873 if !is_univariate {
874 radical_gens.push(p.clone());
875 }
876 }
877
878 groebner_basis(&radical_gens, Algorithm::Auto)
879}
880
881fn radical_via_jacobian(
893 gb: &GroebnerBasis<RationalDomain, Lex>,
894) -> GroebnerBasis<RationalDomain, Lex> {
895 let n_vars = match gb.basis.first() {
896 Some(p) => p.n_vars(),
897 None => return gb.clone(),
898 };
899
900 if n_vars == 0 || gb.basis.is_empty() {
901 return gb.clone();
902 }
903
904 let mut derivatives: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = Vec::new();
906 for f in &gb.basis {
907 for var in 0..n_vars {
908 let df = f.derivative(var);
909 if df.total_degree().is_some_and(|d| d > 0) {
910 derivatives.push(df);
911 }
912 }
913 }
914
915 if derivatives.is_empty() {
916 return gb.clone();
919 }
920
921 let h = derivatives.into_iter().reduce(|a, b| {
932 if a.total_degree() <= b.total_degree() {
935 a
936 } else {
937 b
938 }
939 });
940
941 let Some(h) = h else {
942 return gb.clone();
943 };
944
945 if h.total_degree() == Some(0) || h.total_degree().is_none() {
947 return gb.clone();
948 }
949
950 ideal_saturate(&gb.basis, std::slice::from_ref(&h))
952}
953
954pub fn primary_decomposition(
979 generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
980) -> Vec<PrimaryComponent> {
981 if generators.is_empty() {
982 return vec![];
983 }
984
985 let gb = groebner_basis(generators, Algorithm::Auto);
986
987 if is_zero_dimensional(&gb) {
988 primary_decomp_zero_dim(&gb)
989 } else {
990 vec![PrimaryComponent {
992 primary: gb.basis.clone(),
993 prime: gb.basis.clone(), }]
995 }
996}
997
998fn primary_decomp_zero_dim(
1003 gb: &GroebnerBasis<RationalDomain, Lex>,
1004) -> Vec<PrimaryComponent> {
1005 if gb.basis.is_empty() {
1006 return vec![];
1007 }
1008
1009 let n_vars = match gb.basis.first() {
1010 Some(p) => p.n_vars(),
1011 None => return vec![],
1012 };
1013
1014 let univariate = gb.basis.iter().find(|p| {
1016 p.terms_ref()
1017 .keys()
1018 .all(|e| e.iter().enumerate().all(|(i, &v)| i == 0 || v == 0))
1019 && p.degree_in(0) > 0
1020 });
1021
1022 let Some(poly) = univariate else {
1023 return vec![PrimaryComponent {
1025 primary: gb.basis.clone(),
1026 prime: ideal_radical(&gb.basis).basis,
1027 }];
1028 };
1029
1030 let unipoly = extract_univariate(poly, 0);
1031
1032 let deriv = unipoly.derivative();
1034 let g = unipoly.gcd(&deriv);
1035 let sqf = match unipoly.div_rem(&g) {
1036 Some((q, _)) => q,
1037 None => unipoly.clone(),
1038 };
1039
1040 let factors = crate::factor::algebraic::factor_square_free_rationals(&sqf);
1042
1043 if factors.len() <= 1 {
1044 return vec![PrimaryComponent {
1046 primary: gb.basis.clone(),
1047 prime: ideal_radical(&gb.basis).basis,
1048 }];
1049 }
1050
1051 let domain = RationalDomain;
1053 let factor_polys: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = factors
1054 .iter()
1055 .map(|f| {
1056 let terms: Vec<(Vec<usize>, Rational)> = f
1057 .coeffs()
1058 .iter()
1059 .enumerate()
1060 .filter(|(_, c)| !domain.is_zero(c))
1061 .map(|(i, c)| {
1062 let mut exp = vec![0usize; n_vars];
1063 exp[0] = i;
1064 (exp, c.clone())
1065 })
1066 .collect();
1067 SparseMultivariatePolynomial::from_terms(domain, n_vars, terms)
1068 })
1069 .collect();
1070
1071 let mut components = Vec::new();
1074 for (i, fi) in factor_polys.iter().enumerate() {
1075 let mut saturated = GroebnerBasis { basis: gb.basis.clone() };
1077 for (j, fj) in factor_polys.iter().enumerate() {
1078 if i == j {
1079 continue;
1080 }
1081 saturated = ideal_saturate(&saturated.basis, std::slice::from_ref(fj));
1082 }
1083
1084 let mut prime_gens = gb.basis.clone();
1086 prime_gens.push(fi.clone());
1087 let prime_gb = groebner_basis(&prime_gens, Algorithm::Auto);
1088
1089 components.push(PrimaryComponent {
1090 primary: saturated.basis,
1091 prime: prime_gb.basis,
1092 });
1093 }
1094
1095 components
1096}
1097
1098pub fn is_prime_ideal(
1109 generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
1110) -> bool {
1111 if generators.is_empty() {
1112 return false;
1113 }
1114 let gb = groebner_basis(generators, Algorithm::Auto);
1115 if gb.basis.is_empty() {
1116 return false;
1117 }
1118 if gb.basis.len() == 1
1120 && gb.basis[0]
1121 .leading_monomial()
1122 .map(|lm| lm.iter().all(|&e| e == 0))
1123 .unwrap_or(false)
1124 {
1125 return false;
1126 }
1127 if is_zero_dimensional(&gb) {
1128 is_prime_zero_dim(&gb)
1129 } else {
1130 false
1133 }
1134}
1135
1136fn is_prime_zero_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> bool {
1137 let n_vars = match gb.basis.first() {
1138 Some(p) => p.n_vars(),
1139 None => return false,
1140 };
1141
1142 for var in 0..n_vars {
1145 let univariate = gb.basis.iter().find(|p| {
1146 p.terms_ref()
1147 .keys()
1148 .all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
1149 && p.degree_in(var) > 0
1150 });
1151
1152 if let Some(poly) = univariate {
1153 let unipoly = extract_univariate(poly, var);
1154 if let Some(deg) = unipoly.degree() && deg <= 3 {
1159 let has_rational_root = check_rational_roots(&unipoly);
1160 if has_rational_root && deg > 1 {
1161 return false;
1162 }
1163 }
1164 }
1165 }
1166 true
1167}
1168
1169fn divisors_of(n: i64) -> Vec<i64> {
1171 if n <= 0 {
1172 return vec![];
1173 }
1174 let mut divs = Vec::new();
1175 let mut i = 1i64;
1176 while i * i <= n {
1177 if n % i == 0 {
1178 divs.push(i);
1179 if i != n / i {
1180 divs.push(n / i);
1181 }
1182 }
1183 i += 1;
1184 }
1185 divs.sort_unstable();
1186 divs
1187}
1188
1189fn check_rational_roots(
1193 poly: &crate::dense::DenseUnivariatePolynomial<RationalDomain>,
1194) -> bool {
1195 let Some(deg) = poly.degree() else {
1196 return false;
1197 };
1198 if deg == 0 {
1199 return false;
1200 }
1201
1202 let coeffs = poly.coeffs();
1203 let constant = &coeffs[0];
1204
1205 if RationalDomain.is_zero(constant) {
1206 return true; }
1208
1209 let Some(lc) = poly.leading_coeff() else {
1210 return false;
1211 };
1212
1213 let p_divs = divisors_of(constant.numer().to_i64().unwrap_or(0).unsigned_abs() as i64);
1215 let q_divs = divisors_of(lc.numer().to_i64().unwrap_or(0).unsigned_abs() as i64);
1216
1217 if p_divs.is_empty() || q_divs.is_empty() {
1218 let one = ocas_domain::Rational::new(1, 1);
1220 let neg_one = ocas_domain::Rational::new(-1, 1);
1221 return RationalDomain.is_zero(&poly.eval(&one))
1222 || RationalDomain.is_zero(&poly.eval(&neg_one));
1223 }
1224
1225 for &p in &p_divs {
1226 for &q in &q_divs {
1227 let candidate = ocas_domain::Rational::new(p, q);
1228 if RationalDomain.is_zero(&poly.eval(&candidate)) {
1229 return true;
1230 }
1231 let neg_candidate = ocas_domain::Rational::new(-p, q);
1232 if RationalDomain.is_zero(&poly.eval(&neg_candidate)) {
1233 return true;
1234 }
1235 }
1236 }
1237 false
1238}
1239
1240pub fn is_primary_ideal(
1244 generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
1245) -> bool {
1246 let decomp = primary_decomposition(generators);
1247 decomp.len() <= 1
1248}
1249
1250#[cfg(test)]
1251mod tests {
1252 use super::*;
1253 use crate::groebner_basis;
1254 use ocas_domain::{Rational, RationalDomain};
1255
1256 fn r(n: i64, d: i64) -> Rational {
1257 Rational::new(n, d)
1258 }
1259
1260 fn x() -> SparseMultivariatePolynomial<RationalDomain, Lex> {
1261 SparseMultivariatePolynomial::from_terms(RationalDomain, 2, vec![(vec![1, 0], r(1, 1))])
1262 }
1263
1264 fn y() -> SparseMultivariatePolynomial<RationalDomain, Lex> {
1265 SparseMultivariatePolynomial::from_terms(RationalDomain, 2, vec![(vec![0, 1], r(1, 1))])
1266 }
1267
1268 #[test]
1269 fn contains_basic() {
1270 assert!(ideal_contains(&[x(), y()], &x(), Algorithm::Auto));
1272 }
1273
1274 #[test]
1275 fn contains_negative() {
1276 assert!(!ideal_contains(&[y()], &x(), Algorithm::Auto));
1278 }
1279
1280 #[test]
1281 fn sum_xy() {
1282 let gb = ideal_sum(&[x()], &[y()]);
1284 assert!(gb.basis.len() >= 2);
1285 }
1286
1287 #[test]
1288 fn product_xy() {
1289 let gb = ideal_product(&[x()], &[y()]);
1291 assert_eq!(gb.basis.len(), 1);
1292 }
1293
1294 #[test]
1295 fn quotient_x2_xy_by_x() {
1296 let d = RationalDomain;
1298 let x2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1299 (vec![2, 0], r(1, 1)),
1300 ]);
1301 let xy = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1302 (vec![1, 1], r(1, 1)),
1303 ]);
1304 let g = x();
1305 let gb = ideal_quotient(&[x2, xy], &[g]);
1306 assert!(!gb.basis.is_empty());
1308 assert!(ideal_contains(&gb.basis, &x(), Algorithm::Auto));
1309 }
1310
1311 #[test]
1312 fn intersection_x_y() {
1313 let gb = ideal_intersection(&[x()], &[y()]);
1315 assert_eq!(gb.basis.len(), 1);
1316 let xy_exp = vec![1usize, 1];
1318 let has_xy = gb.basis.iter().any(|p| {
1319 p.terms_ref().len() == 1 && p.terms_ref().contains_key(xy_exp.as_slice())
1320 });
1321 assert!(has_xy, "expected xy in intersection basis");
1322 }
1323
1324 #[test]
1325 fn saturate_x2y_xy2_by_x() {
1326 let d = RationalDomain;
1328 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1329 (vec![2, 1], r(1, 1)),
1330 ]);
1331 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1332 (vec![1, 2], r(1, 1)),
1333 ]);
1334 let g = x();
1335 let gb = ideal_saturate(&[f1, f2], &[g]);
1336 assert!(!gb.basis.is_empty());
1338 assert!(ideal_contains(&gb.basis, &y(), Algorithm::Auto));
1339 }
1340
1341 #[test]
1344 fn is_zero_dim_positive() {
1345 let d = RationalDomain;
1347 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1348 (vec![2, 0], r(1, 1)),
1349 (vec![0, 0], r(-1, 1)),
1350 ]);
1351 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1352 (vec![0, 1], r(1, 1)),
1353 (vec![1, 0], r(-1, 1)),
1354 ]);
1355 let gb = groebner_basis(&[f1, f2], Algorithm::F4);
1356 assert!(is_zero_dimensional(&gb));
1357 }
1358
1359 #[test]
1360 fn is_zero_dim_negative() {
1361 let d = RationalDomain;
1363 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1364 (vec![1, 0], r(1, 1)),
1365 (vec![0, 1], r(-1, 1)),
1366 ]);
1367 let gb = groebner_basis(&[f], Algorithm::F4);
1368 assert!(!is_zero_dimensional(&gb));
1369 }
1370
1371 #[test]
1372 fn solve_circle_line() {
1373 let d = RationalDomain;
1375 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1376 (vec![2, 0], r(1, 1)),
1377 (vec![0, 2], r(1, 1)),
1378 (vec![0, 0], r(-1, 1)),
1379 ]);
1380 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1381 (vec![1, 0], r(1, 1)),
1382 (vec![0, 1], r(-1, 1)),
1383 ]);
1384 let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
1385 match sol {
1386 PolynomialSystemSolution::ZeroDimensional(z) => {
1387 assert_eq!(z.solutions.len(), 2);
1388 for s in &z.solutions {
1390 let x_val = s.values[0];
1391 let y_val = s.values[1];
1392 assert!((x_val - y_val).abs() < 1e-10, "x should equal y");
1393 assert!(
1394 (x_val * x_val + y_val * y_val - 1.0).abs() < 1e-10,
1395 "x² + y² should be 1"
1396 );
1397 }
1398 }
1399 _ => panic!("expected zero-dimensional"),
1400 }
1401 }
1402
1403 #[test]
1404 fn solve_empty_variety() {
1405 let d = RationalDomain;
1407 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1408 (vec![2, 0], r(1, 1)),
1409 (vec![0, 2], r(1, 1)),
1410 (vec![0, 0], r(-1, 1)),
1411 ]);
1412 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1413 (vec![2, 0], r(1, 1)),
1414 (vec![0, 2], r(1, 1)),
1415 (vec![0, 0], r(-2, 1)),
1416 ]);
1417 let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
1418 assert!(matches!(sol, PolynomialSystemSolution::Empty));
1419 }
1420
1421 #[test]
1424 fn radical_x2_y2() {
1425 let d = RationalDomain;
1427 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1428 (vec![2, 0], r(1, 1)),
1429 ]);
1430 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1431 (vec![0, 2], r(1, 1)),
1432 ]);
1433 let rad = ideal_radical(&[f1, f2]);
1434 let x_poly = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1436 (vec![1, 0], r(1, 1)),
1437 ]);
1438 let y_poly = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1439 (vec![0, 1], r(1, 1)),
1440 ]);
1441 assert!(ideal_contains(&rad.basis, &x_poly, Algorithm::Auto));
1442 assert!(ideal_contains(&rad.basis, &y_poly, Algorithm::Auto));
1443 }
1444
1445 #[test]
1446 fn radical_of_prime_is_self() {
1447 let d = RationalDomain;
1449 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 1, vec![
1450 (vec![2], r(1, 1)),
1451 (vec![0], r(-2, 1)),
1452 ]);
1453 let rad = ideal_radical(&[f.clone()]);
1454 assert!(ideal_contains(&rad.basis, &f, Algorithm::Auto));
1456 }
1457
1458 #[test]
1459 fn primary_decomp_x2_xy() {
1460 let d = RationalDomain;
1462 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1463 (vec![2, 0], r(1, 1)),
1464 ]);
1465 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![
1466 (vec![1, 1], r(1, 1)),
1467 ]);
1468 let decomp = primary_decomposition(&[f1, f2]);
1469 assert!(!decomp.is_empty());
1470 for comp in &decomp {
1472 assert!(!comp.primary.is_empty());
1473 assert!(!comp.prime.is_empty());
1474 }
1475 }
1476
1477 #[test]
1478 fn is_prime_x2_minus_2() {
1479 let d = RationalDomain;
1481 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 1, vec![
1482 (vec![2], r(1, 1)),
1483 (vec![0], r(-2, 1)),
1484 ]);
1485 assert!(is_prime_ideal(&[f]));
1486 }
1487
1488 #[test]
1489 fn is_primary_x2() {
1490 let d = RationalDomain;
1492 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 1, vec![
1493 (vec![2], r(1, 1)),
1494 ]);
1495 assert!(is_primary_ideal(&[f]));
1496 }
1497}