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>> =
199 generators_i.iter().map(|p| p.embed_new_main()).collect();
200
201 let g_embedded = g.embed_new_main();
203 let w = {
204 let mut exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
205 exp[0] = 1;
206 SparseMultivariatePolynomial::from_terms(
207 domain.clone(),
208 n_vars + 1,
209 vec![(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(
216 domain.clone(),
217 n_vars + 1,
218 vec![(one_exp.to_vec(), domain.one())],
219 );
220 one.sub(&wg)
221 };
222
223 let mut combined = embedded;
224 combined.push(one_minus_wg);
225
226 let elim_gb = crate::groebner::eliminate(&combined, 1, Algorithm::Auto);
228 elim_gb
229 .basis
230 .into_iter()
231 .map(|p| p.drop_variable(0))
232 .collect()
233}
234
235fn intersect_generators<D: Domain + 'static>(
240 generators_a: &[SparseMultivariatePolynomial<D, Lex>],
241 generators_b: &[SparseMultivariatePolynomial<D, Lex>],
242) -> Vec<SparseMultivariatePolynomial<D, Lex>> {
243 let n_vars = generators_a
244 .first()
245 .or(generators_b.first())
246 .map(|p| p.n_vars())
247 .unwrap_or(0);
248 let domain = generators_a
249 .first()
250 .or(generators_b.first())
251 .map(|p| p.domain().clone())
252 .unwrap_or_else(|| {
253 unreachable!("intersect_generators called with empty inputs")
255 });
256
257 if generators_a.is_empty() || generators_b.is_empty() {
258 return vec![];
259 }
260
261 let t = {
263 let mut exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
264 exp[0] = 1;
265 SparseMultivariatePolynomial::from_terms(
266 domain.clone(),
267 n_vars + 1,
268 vec![(exp.to_vec(), domain.one())],
269 )
270 };
271 let one_minus_t = {
272 let one_exp = smallvec::SmallVec::<[usize; 4]>::from_elem(0, n_vars + 1);
273 let one = SparseMultivariatePolynomial::from_terms(
274 domain.clone(),
275 n_vars + 1,
276 vec![(one_exp.to_vec(), domain.one())],
277 );
278 one.sub(&t)
279 };
280
281 let mut combined: Vec<SparseMultivariatePolynomial<D, Lex>> = Vec::new();
282
283 for f in generators_a {
285 let f_emb = f.embed_new_main();
286 combined.push(t.mul(&f_emb));
287 }
288 for g in generators_b {
290 let g_emb = g.embed_new_main();
291 combined.push(one_minus_t.mul(&g_emb));
292 }
293
294 let elim_gb = crate::groebner::eliminate(&combined, 1, Algorithm::Auto);
296 elim_gb
297 .basis
298 .into_iter()
299 .map(|p| p.drop_variable(0))
300 .collect()
301}
302
303pub fn ideal_intersection<D: Domain + 'static>(
328 generators_a: &[SparseMultivariatePolynomial<D, Lex>],
329 generators_b: &[SparseMultivariatePolynomial<D, Lex>],
330) -> GroebnerBasis<D, Lex> {
331 if generators_a.is_empty() || generators_b.is_empty() {
332 return GroebnerBasis { basis: vec![] };
333 }
334 let gens = intersect_generators(generators_a, generators_b);
335 if gens.is_empty() {
336 GroebnerBasis { basis: vec![] }
337 } else {
338 groebner_basis(&gens, Algorithm::Auto)
339 }
340}
341
342pub fn ideal_saturate<D: Domain + 'static>(
369 generators_i: &[SparseMultivariatePolynomial<D, Lex>],
370 generators_j: &[SparseMultivariatePolynomial<D, Lex>],
371) -> GroebnerBasis<D, Lex> {
372 if generators_i.is_empty() {
373 return GroebnerBasis { basis: vec![] };
374 }
375 if generators_j.is_empty() {
376 return groebner_basis(generators_i, Algorithm::Auto);
377 }
378
379 let mut current_gens = generators_i.to_vec();
380 let max_iter = 20;
381
382 for _ in 0..max_iter {
383 let current_gb = groebner_basis(¤t_gens, Algorithm::Auto);
384 let next = ideal_quotient(¤t_gb.basis, generators_j);
385 let next_gb = groebner_basis(&next.basis, Algorithm::Auto);
386
387 let all_in_current = next_gb
389 .basis
390 .iter()
391 .all(|p| p.reduce(¤t_gb.basis).is_zero());
392 let all_in_next = current_gb
393 .basis
394 .iter()
395 .all(|p| p.reduce(&next_gb.basis).is_zero());
396
397 if all_in_current && all_in_next {
398 return current_gb;
399 }
400 current_gens = next_gb.basis;
401 }
402
403 groebner_basis(¤t_gens, Algorithm::Auto)
404}
405
406#[derive(Debug, Clone)]
412pub struct RealSolution {
413 pub values: Vec<f64>,
415 pub multiplicity: usize,
417}
418
419#[derive(Debug, Clone)]
421pub struct ZeroDimSolutions {
422 pub solutions: Vec<RealSolution>,
424 pub vector_space_dimension: usize,
427}
428
429#[derive(Debug, Clone)]
431pub enum PolynomialSystemSolution {
432 ZeroDimensional(ZeroDimSolutions),
434 PositiveDimensional(GroebnerBasis<RationalDomain, Lex>),
436 Empty,
438}
439
440pub fn is_zero_dimensional(gb: &GroebnerBasis<RationalDomain, Lex>) -> bool {
468 let n_vars = match gb.basis.first() {
469 Some(p) => p.n_vars(),
470 None => return false, };
472
473 for var in 0..n_vars {
475 let has_pure_power = gb.basis.iter().any(|p| match p.leading_monomial() {
476 Some(lm) => lm
477 .iter()
478 .enumerate()
479 .all(|(i, &e)| if i == var { e > 0 } else { e == 0 }),
480 None => false,
481 });
482 if !has_pure_power {
483 return false;
484 }
485 }
486 true
487}
488
489fn extract_univariate(
493 poly: &SparseMultivariatePolynomial<RationalDomain, Lex>,
494 var_index: usize,
495) -> crate::dense::DenseUnivariatePolynomial<RationalDomain> {
496 let d = RationalDomain;
497 let deg = poly.degree_in(var_index);
498 let mut coeffs = vec![ocas_domain::Rational::new(0, 1); deg + 1];
499 for (exp, coeff) in poly.terms_ref() {
500 let power = exp.get(var_index).copied().unwrap_or(0);
501 coeffs[power] = coeffs[power].clone() + coeff.clone();
502 }
503 crate::dense::DenseUnivariatePolynomial::from_coeffs(d, coeffs)
504}
505
506fn solve_univariate_f64(
509 poly: &SparseMultivariatePolynomial<RationalDomain, Lex>,
510 var_index: usize,
511 substituted_values: &[f64], ) -> Vec<f64> {
513 let d = RationalDomain;
516 let deg = poly.degree_in(var_index);
517 let mut coeffs_f64 = vec![0.0f64; deg + 1];
518
519 for (exp, coeff) in poly.terms_ref() {
520 let mut coeff_f = format!("{}", coeff).parse::<f64>().unwrap_or(0.0);
522 for (i, &e) in exp.iter().enumerate() {
523 if i > var_index && e > 0 {
524 let sub_idx = i - var_index - 1;
526 if sub_idx < substituted_values.len() {
527 coeff_f *= substituted_values[sub_idx].powi(e as i32);
528 }
529 }
530 }
531 let power = exp.get(var_index).copied().unwrap_or(0);
532 coeffs_f64[power] += coeff_f;
533 }
534
535 let rational_coeffs: Vec<ocas_domain::Rational> =
537 coeffs_f64.iter().map(|&c| rational_approx(c)).collect();
538 let unipoly = crate::dense::DenseUnivariatePolynomial::from_coeffs(d, rational_coeffs);
539 let intervals = unipoly.isolate_real_roots();
540 intervals
541 .iter()
542 .map(|iv| {
543 let refined = unipoly.refine_root(iv, 1e-14);
544 (refined.low + refined.high) / 2.0
545 })
546 .collect()
547}
548
549fn rational_approx(x: f64) -> ocas_domain::Rational {
551 if x == 0.0 {
552 return ocas_domain::Rational::new(0, 1);
553 }
554 let sign: i64 = if x < 0.0 { -1 } else { 1 };
555 let x_abs = x.abs();
556 let mut a = x_abs.floor() as i64;
557 let mut frac = x_abs - a as f64;
558 let mut prev_num = 1i64;
559 let mut prev_den = 0i64;
560 let mut num = a;
561 let mut den = 1i64;
562
563 for _ in 0..50 {
564 if frac.abs() < 1e-12 {
565 break;
566 }
567 let r = 1.0 / frac;
568 a = r.floor() as i64;
569 frac = r - a as f64;
570 let new_num = a * num + prev_num;
571 let new_den = a * den + prev_den;
572 prev_num = num;
573 prev_den = den;
574 num = new_num;
575 den = new_den;
576 if den > 1_000_000 {
577 break;
578 }
579 }
580 ocas_domain::Rational::new(sign * num, den)
581}
582
583pub fn solve_polynomial_system(
616 equations: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
617 algo: Algorithm,
618) -> PolynomialSystemSolution {
619 if equations.is_empty() {
620 return PolynomialSystemSolution::PositiveDimensional(GroebnerBasis { basis: vec![] });
621 }
622
623 let gb = groebner_basis(equations, algo);
624
625 if gb.basis.len() == 1
627 && gb.basis[0].terms_ref().len() == 1
628 && gb.basis[0]
629 .leading_monomial()
630 .map(|lm| lm.iter().all(|&e| e == 0))
631 .unwrap_or(false)
632 {
633 return PolynomialSystemSolution::Empty;
634 }
635
636 let gb_lex: GroebnerBasis<RationalDomain, Lex> = gb;
638 if !is_zero_dimensional(&gb_lex) {
642 return PolynomialSystemSolution::PositiveDimensional(gb_lex);
643 }
644
645 let solutions = solve_triangular(&gb_lex);
646 let dim = compute_vector_space_dim(&gb_lex).unwrap_or(solutions.len());
650
651 PolynomialSystemSolution::ZeroDimensional(ZeroDimSolutions {
652 solutions,
653 vector_space_dimension: dim,
654 })
655}
656
657fn compute_vector_space_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> Option<usize> {
661 let n_vars = gb.basis.first()?.n_vars();
662 let mut dim = 1usize;
663 for var in 0..n_vars {
664 let max_deg = gb
665 .basis
666 .iter()
667 .filter(|p| {
668 p.terms_ref()
669 .keys()
670 .all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
671 })
672 .map(|p| p.degree_in(var))
673 .max()
674 .unwrap_or(1);
675 dim = dim.checked_mul(max_deg)?;
676 }
677 Some(dim)
678}
679
680fn solve_triangular(gb: &GroebnerBasis<RationalDomain, Lex>) -> Vec<RealSolution> {
683 let n_vars = match gb.basis.first() {
684 Some(p) => p.n_vars(),
685 None => return vec![],
686 };
687
688 let raw = solve_recursive(gb, n_vars - 1, &[]);
690 raw.into_iter()
692 .map(|mut s| {
693 s.values.reverse();
694 s
695 })
696 .collect()
697}
698
699fn solve_recursive(
703 gb: &GroebnerBasis<RationalDomain, Lex>,
704 var_index: usize,
705 higher_values: &[f64],
706) -> Vec<RealSolution> {
707 let univariate_poly = gb.basis.iter().find(|p| {
710 p.degree_in(var_index) > 0
711 && p.terms_ref()
712 .keys()
713 .all(|e| e.iter().enumerate().all(|(i, &v)| i <= var_index || v == 0))
714 });
715
716 let roots = if let Some(poly) = univariate_poly {
717 let unipoly = extract_univariate(poly, var_index);
719 let intervals = unipoly.isolate_real_roots();
720 let r: Vec<f64> = intervals
721 .iter()
722 .map(|iv| {
723 let refined = unipoly.refine_root(iv, 1e-14);
724 (refined.low + refined.high) / 2.0
725 })
726 .collect();
727 r
728 } else {
729 let poly = gb.basis.iter().find(|p| p.degree_in(var_index) > 0);
732 let Some(poly) = poly else {
733 return vec![];
734 };
735 solve_univariate_f64(poly, var_index, higher_values)
736 };
737
738 if var_index == 0 {
739 roots
741 .into_iter()
742 .map(|v| RealSolution {
743 values: vec![v],
744 multiplicity: 1,
745 })
746 .collect()
747 } else {
748 let mut results = Vec::new();
750 for root in &roots {
751 let mut new_higher = Vec::with_capacity(higher_values.len() + 1);
752 new_higher.push(*root);
753 new_higher.extend_from_slice(higher_values);
754 let sub_solutions = solve_recursive(gb, var_index - 1, &new_higher);
755 for mut sol in sub_solutions {
756 sol.values.push(*root);
757 results.push(sol);
758 }
759 }
760 results
761 }
762}
763
764#[derive(Debug, Clone)]
770pub struct PrimaryComponent {
771 pub primary: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
773 pub prime: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>>,
775}
776
777pub fn ideal_radical(
803 generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
804) -> GroebnerBasis<RationalDomain, Lex> {
805 if generators.is_empty() {
806 return GroebnerBasis { basis: vec![] };
807 }
808
809 let gb = groebner_basis(generators, Algorithm::Auto);
810
811 if is_zero_dimensional(&gb) {
812 radical_zero_dim(&gb)
813 } else {
814 radical_via_jacobian(&gb)
819 }
820}
821
822fn radical_zero_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> GroebnerBasis<RationalDomain, Lex> {
825 let n_vars = match gb.basis.first() {
826 Some(p) => p.n_vars(),
827 None => return GroebnerBasis { basis: vec![] },
828 };
829
830 let domain = RationalDomain;
831 let mut radical_gens: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = Vec::new();
832
833 for var in 0..n_vars {
835 let univariate = gb.basis.iter().find(|p| {
836 p.terms_ref()
837 .keys()
838 .all(|e| e.iter().enumerate().all(|(i, &v)| i == var || v == 0))
839 && p.degree_in(var) > 0
840 });
841
842 if let Some(poly) = univariate {
843 let unipoly = extract_univariate(poly, var);
844 let deriv = unipoly.derivative();
846 let g = unipoly.gcd(&deriv);
847 let sqf = unipoly.div_rem(&g).map(|(q, _)| q).unwrap_or(unipoly);
848
849 let terms: Vec<(Vec<usize>, ocas_domain::Rational)> = sqf
851 .coeffs()
852 .iter()
853 .enumerate()
854 .filter(|(_, c)| !domain.is_zero(c))
855 .map(|(i, c)| {
856 let mut exp = vec![0usize; n_vars];
857 exp[var] = i;
858 (exp, c.clone())
859 })
860 .collect();
861 if !terms.is_empty() {
862 radical_gens.push(SparseMultivariatePolynomial::from_terms(
863 domain, n_vars, terms,
864 ));
865 }
866 }
867 }
868
869 for p in &gb.basis {
871 let is_univariate = p
872 .terms_ref()
873 .keys()
874 .any(|e| e.iter().filter(|&&v| v > 0).count() <= 1);
875 if !is_univariate {
876 radical_gens.push(p.clone());
877 }
878 }
879
880 groebner_basis(&radical_gens, Algorithm::Auto)
881}
882
883fn radical_via_jacobian(
895 gb: &GroebnerBasis<RationalDomain, Lex>,
896) -> GroebnerBasis<RationalDomain, Lex> {
897 let n_vars = match gb.basis.first() {
898 Some(p) => p.n_vars(),
899 None => return gb.clone(),
900 };
901
902 if n_vars == 0 || gb.basis.is_empty() {
903 return gb.clone();
904 }
905
906 let mut derivatives: Vec<SparseMultivariatePolynomial<RationalDomain, Lex>> = Vec::new();
908 for f in &gb.basis {
909 for var in 0..n_vars {
910 let df = f.derivative(var);
911 if df.total_degree().is_some_and(|d| d > 0) {
912 derivatives.push(df);
913 }
914 }
915 }
916
917 if derivatives.is_empty() {
918 return gb.clone();
921 }
922
923 let h = derivatives.into_iter().reduce(|a, b| {
934 if a.total_degree() <= b.total_degree() {
937 a
938 } else {
939 b
940 }
941 });
942
943 let Some(h) = h else {
944 return gb.clone();
945 };
946
947 if h.total_degree() == Some(0) || h.total_degree().is_none() {
949 return gb.clone();
950 }
951
952 ideal_saturate(&gb.basis, std::slice::from_ref(&h))
954}
955
956pub fn primary_decomposition(
981 generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>],
982) -> Vec<PrimaryComponent> {
983 if generators.is_empty() {
984 return vec![];
985 }
986
987 let gb = groebner_basis(generators, Algorithm::Auto);
988
989 if is_zero_dimensional(&gb) {
990 primary_decomp_zero_dim(&gb)
991 } else {
992 vec![PrimaryComponent {
994 primary: gb.basis.clone(),
995 prime: gb.basis.clone(), }]
997 }
998}
999
1000fn primary_decomp_zero_dim(gb: &GroebnerBasis<RationalDomain, Lex>) -> 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 {
1077 basis: gb.basis.clone(),
1078 };
1079 for (j, fj) in factor_polys.iter().enumerate() {
1080 if i == j {
1081 continue;
1082 }
1083 saturated = ideal_saturate(&saturated.basis, std::slice::from_ref(fj));
1084 }
1085
1086 let mut prime_gens = gb.basis.clone();
1088 prime_gens.push(fi.clone());
1089 let prime_gb = groebner_basis(&prime_gens, Algorithm::Auto);
1090
1091 components.push(PrimaryComponent {
1092 primary: saturated.basis,
1093 prime: prime_gb.basis,
1094 });
1095 }
1096
1097 components
1098}
1099
1100pub fn is_prime_ideal(generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>]) -> 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()
1159 && deg <= 3
1160 {
1161 let has_rational_root = check_rational_roots(&unipoly);
1162 if has_rational_root && deg > 1 {
1163 return false;
1164 }
1165 }
1166 }
1167 }
1168 true
1169}
1170
1171fn divisors_of(n: i64) -> Vec<i64> {
1173 if n <= 0 {
1174 return vec![];
1175 }
1176 let mut divs = Vec::new();
1177 let mut i = 1i64;
1178 while i * i <= n {
1179 if n % i == 0 {
1180 divs.push(i);
1181 if i != n / i {
1182 divs.push(n / i);
1183 }
1184 }
1185 i += 1;
1186 }
1187 divs.sort_unstable();
1188 divs
1189}
1190
1191fn check_rational_roots(poly: &crate::dense::DenseUnivariatePolynomial<RationalDomain>) -> 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(generators: &[SparseMultivariatePolynomial<RationalDomain, Lex>]) -> bool {
1244 let decomp = primary_decomposition(generators);
1245 decomp.len() <= 1
1246}
1247
1248#[cfg(test)]
1249mod tests {
1250 use super::*;
1251 use crate::groebner_basis;
1252 use ocas_domain::{Rational, RationalDomain};
1253
1254 fn r(n: i64, d: i64) -> Rational {
1255 Rational::new(n, d)
1256 }
1257
1258 fn x() -> SparseMultivariatePolynomial<RationalDomain, Lex> {
1259 SparseMultivariatePolynomial::from_terms(RationalDomain, 2, vec![(vec![1, 0], r(1, 1))])
1260 }
1261
1262 fn y() -> SparseMultivariatePolynomial<RationalDomain, Lex> {
1263 SparseMultivariatePolynomial::from_terms(RationalDomain, 2, vec![(vec![0, 1], r(1, 1))])
1264 }
1265
1266 #[test]
1267 fn contains_basic() {
1268 assert!(ideal_contains(&[x(), y()], &x(), Algorithm::Auto));
1270 }
1271
1272 #[test]
1273 fn contains_negative() {
1274 assert!(!ideal_contains(&[y()], &x(), Algorithm::Auto));
1276 }
1277
1278 #[test]
1279 fn sum_xy() {
1280 let gb = ideal_sum(&[x()], &[y()]);
1282 assert!(gb.basis.len() >= 2);
1283 }
1284
1285 #[test]
1286 fn product_xy() {
1287 let gb = ideal_product(&[x()], &[y()]);
1289 assert_eq!(gb.basis.len(), 1);
1290 }
1291
1292 #[test]
1293 fn quotient_x2_xy_by_x() {
1294 let d = RationalDomain;
1296 let x2 =
1297 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 0], r(1, 1))]);
1298 let xy =
1299 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 1], r(1, 1))]);
1300 let g = x();
1301 let gb = ideal_quotient(&[x2, xy], &[g]);
1302 assert!(!gb.basis.is_empty());
1304 assert!(ideal_contains(&gb.basis, &x(), Algorithm::Auto));
1305 }
1306
1307 #[test]
1308 fn intersection_x_y() {
1309 let gb = ideal_intersection(&[x()], &[y()]);
1311 assert_eq!(gb.basis.len(), 1);
1312 let xy_exp = vec![1usize, 1];
1314 let has_xy = gb
1315 .basis
1316 .iter()
1317 .any(|p| p.terms_ref().len() == 1 && p.terms_ref().contains_key(xy_exp.as_slice()));
1318 assert!(has_xy, "expected xy in intersection basis");
1319 }
1320
1321 #[test]
1322 fn saturate_x2y_xy2_by_x() {
1323 let d = RationalDomain;
1325 let f1 =
1326 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 1], r(1, 1))]);
1327 let f2 =
1328 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 2], r(1, 1))]);
1329 let g = x();
1330 let gb = ideal_saturate(&[f1, f2], &[g]);
1331 assert!(!gb.basis.is_empty());
1333 assert!(ideal_contains(&gb.basis, &y(), Algorithm::Auto));
1334 }
1335
1336 #[test]
1339 fn is_zero_dim_positive() {
1340 let d = RationalDomain;
1342 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1343 d,
1344 2,
1345 vec![(vec![2, 0], r(1, 1)), (vec![0, 0], r(-1, 1))],
1346 );
1347 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1348 d,
1349 2,
1350 vec![(vec![0, 1], r(1, 1)), (vec![1, 0], r(-1, 1))],
1351 );
1352 let gb = groebner_basis(&[f1, f2], Algorithm::F4);
1353 assert!(is_zero_dimensional(&gb));
1354 }
1355
1356 #[test]
1357 fn is_zero_dim_negative() {
1358 let d = RationalDomain;
1360 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1361 d,
1362 2,
1363 vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))],
1364 );
1365 let gb = groebner_basis(&[f], Algorithm::F4);
1366 assert!(!is_zero_dimensional(&gb));
1367 }
1368
1369 #[test]
1370 fn solve_circle_line() {
1371 let d = RationalDomain;
1373 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1374 d,
1375 2,
1376 vec![
1377 (vec![2, 0], r(1, 1)),
1378 (vec![0, 2], r(1, 1)),
1379 (vec![0, 0], r(-1, 1)),
1380 ],
1381 );
1382 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1383 d,
1384 2,
1385 vec![(vec![1, 0], r(1, 1)), (vec![0, 1], r(-1, 1))],
1386 );
1387 let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
1388 match sol {
1389 PolynomialSystemSolution::ZeroDimensional(z) => {
1390 assert_eq!(z.solutions.len(), 2);
1391 for s in &z.solutions {
1393 let x_val = s.values[0];
1394 let y_val = s.values[1];
1395 assert!((x_val - y_val).abs() < 1e-10, "x should equal y");
1396 assert!(
1397 (x_val * x_val + y_val * y_val - 1.0).abs() < 1e-10,
1398 "x² + y² should be 1"
1399 );
1400 }
1401 }
1402 _ => panic!("expected zero-dimensional"),
1403 }
1404 }
1405
1406 #[test]
1407 fn solve_empty_variety() {
1408 let d = RationalDomain;
1410 let f1 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1411 d,
1412 2,
1413 vec![
1414 (vec![2, 0], r(1, 1)),
1415 (vec![0, 2], r(1, 1)),
1416 (vec![0, 0], r(-1, 1)),
1417 ],
1418 );
1419 let f2 = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1420 d,
1421 2,
1422 vec![
1423 (vec![2, 0], r(1, 1)),
1424 (vec![0, 2], r(1, 1)),
1425 (vec![0, 0], r(-2, 1)),
1426 ],
1427 );
1428 let sol = solve_polynomial_system(&[f1, f2], Algorithm::Auto);
1429 assert!(matches!(sol, PolynomialSystemSolution::Empty));
1430 }
1431
1432 #[test]
1435 fn radical_x2_y2() {
1436 let d = RationalDomain;
1438 let f1 =
1439 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 0], r(1, 1))]);
1440 let f2 =
1441 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![0, 2], r(1, 1))]);
1442 let rad = ideal_radical(&[f1, f2]);
1443 let x_poly =
1445 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 0], r(1, 1))]);
1446 let y_poly =
1447 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![0, 1], r(1, 1))]);
1448 assert!(ideal_contains(&rad.basis, &x_poly, Algorithm::Auto));
1449 assert!(ideal_contains(&rad.basis, &y_poly, Algorithm::Auto));
1450 }
1451
1452 #[test]
1453 fn radical_of_prime_is_self() {
1454 let d = RationalDomain;
1456 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1457 d,
1458 1,
1459 vec![(vec![2], r(1, 1)), (vec![0], r(-2, 1))],
1460 );
1461 let rad = ideal_radical(std::slice::from_ref(&f));
1462 assert!(ideal_contains(&rad.basis, &f, Algorithm::Auto));
1464 }
1465
1466 #[test]
1467 fn primary_decomp_x2_xy() {
1468 let d = RationalDomain;
1470 let f1 =
1471 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![2, 0], r(1, 1))]);
1472 let f2 =
1473 SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 2, vec![(vec![1, 1], r(1, 1))]);
1474 let decomp = primary_decomposition(&[f1, f2]);
1475 assert!(!decomp.is_empty());
1476 for comp in &decomp {
1478 assert!(!comp.primary.is_empty());
1479 assert!(!comp.prime.is_empty());
1480 }
1481 }
1482
1483 #[test]
1484 fn is_prime_x2_minus_2() {
1485 let d = RationalDomain;
1487 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(
1488 d,
1489 1,
1490 vec![(vec![2], r(1, 1)), (vec![0], r(-2, 1))],
1491 );
1492 assert!(is_prime_ideal(&[f]));
1493 }
1494
1495 #[test]
1496 fn is_primary_x2() {
1497 let d = RationalDomain;
1499 let f = SparseMultivariatePolynomial::<_, Lex>::from_terms(d, 1, vec![(vec![2], r(1, 1))]);
1500 assert!(is_primary_ideal(&[f]));
1501 }
1502}