1use crate::diff::{diff, DiffError};
18use crate::errors::AlkahestError;
19use crate::kernel::expr::PredicateKind;
20use crate::kernel::Domain;
21use crate::kernel::{ExprId, ExprPool};
22use crate::logic::{formula_from_expr, Formula, LogicError};
23use crate::poly::resultant::{self, resultant};
24use crate::poly::{
25 poly_normal, real_roots, ConversionError, RealRootError, ResultantError, RootInterval, UniPoly,
26};
27use std::collections::{BTreeSet, HashMap};
28use std::fmt;
29
30#[derive(Debug, Clone, PartialEq, Eq)]
36pub enum CadError {
37 NotPolynomial(ConversionError),
38 Diff(DiffError),
39 Resultant(ResultantError),
40 RealRoots(RealRootError),
41 Logic(LogicError),
42 Unsupported(&'static str),
44}
45
46impl fmt::Display for CadError {
47 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
48 match self {
49 CadError::NotPolynomial(e) => write!(f, "{e}"),
50 CadError::Diff(e) => write!(f, "{e}"),
51 CadError::Resultant(e) => write!(f, "{e}"),
52 CadError::RealRoots(e) => write!(f, "{e}"),
53 CadError::Logic(e) => write!(f, "{e}"),
54 CadError::Unsupported(s) => write!(f, "CAD: {s}"),
55 }
56 }
57}
58
59impl std::error::Error for CadError {}
60
61impl AlkahestError for CadError {
62 fn code(&self) -> &'static str {
63 match self {
64 CadError::NotPolynomial(e) => e.code(),
65 CadError::Diff(e) => e.code(),
66 CadError::Resultant(e) => e.code(),
67 CadError::RealRoots(e) => e.code(),
68 CadError::Logic(e) => e.code(),
69 CadError::Unsupported(_) => "E-CAD-001",
70 }
71 }
72
73 fn remediation(&self) -> Option<&'static str> {
74 match self {
75 CadError::NotPolynomial(e) => e.remediation(),
76 CadError::Diff(e) => e.remediation(),
77 CadError::Resultant(e) => e.remediation(),
78 CadError::RealRoots(e) => e.remediation(),
79 CadError::Logic(e) => e.remediation(),
80 CadError::Unsupported(_) => Some(
81 "use a purely polynomial constraint in one real variable without nested quantifiers; multivariate QE is incremental",
82 ),
83 }
84 }
85}
86
87impl From<ConversionError> for CadError {
88 fn from(value: ConversionError) -> Self {
89 CadError::NotPolynomial(value)
90 }
91}
92
93impl From<DiffError> for CadError {
94 fn from(value: DiffError) -> Self {
95 CadError::Diff(value)
96 }
97}
98
99impl From<ResultantError> for CadError {
100 fn from(value: ResultantError) -> Self {
101 CadError::Resultant(value)
102 }
103}
104
105impl From<RealRootError> for CadError {
106 fn from(value: RealRootError) -> Self {
107 CadError::RealRoots(value)
108 }
109}
110
111impl From<LogicError> for CadError {
112 fn from(value: LogicError) -> Self {
113 CadError::Logic(value)
114 }
115}
116
117#[derive(Debug, Clone, PartialEq, Eq)]
119pub struct QeResult {
120 pub truth: bool,
121 pub witness: Option<HashMap<ExprId, rug::Rational>>,
122}
123
124fn dual_kind(kind: PredicateKind) -> PredicateKind {
129 use PredicateKind::{Eq, Ge, Gt, Le, Lt, Ne};
130 match kind {
131 Lt => Ge,
132 Le => Gt,
133 Gt => Le,
134 Ge => Lt,
135 Eq => Ne,
136 Ne => Eq,
137 other => other,
138 }
139}
140
141fn is_rel(kind: &PredicateKind) -> bool {
142 use PredicateKind::*;
143 matches!(kind, Lt | Le | Gt | Ge | Eq | Ne)
144}
145
146fn simplify_formula_constants(f: Formula) -> Formula {
147 match f {
148 Formula::And(a, b) => {
149 let la = simplify_formula_constants(*a);
150 let lb = simplify_formula_constants(*b);
151 match (&la, &lb) {
152 (Formula::False, _) | (_, Formula::False) => Formula::False,
153 (Formula::True, x) => x.clone(),
154 (x, Formula::True) => x.clone(),
155 _ => Formula::and(la, lb),
156 }
157 }
158 Formula::Or(a, b) => {
159 let la = simplify_formula_constants(*a);
160 let lb = simplify_formula_constants(*b);
161 match (&la, &lb) {
162 (Formula::True, _) | (_, Formula::True) => Formula::True,
163 (Formula::False, x) => x.clone(),
164 (x, Formula::False) => x.clone(),
165 _ => Formula::or(la, lb),
166 }
167 }
168 Formula::Not(x) => Formula::not(simplify_formula_constants(*x)),
169 Formula::Forall { var, body } => Formula::Forall {
170 var,
171 body: Box::new(simplify_formula_constants(*body)),
172 },
173 Formula::Exists { var, body } => Formula::Exists {
174 var,
175 body: Box::new(simplify_formula_constants(*body)),
176 },
177 other => other,
178 }
179}
180
181fn nnf_formula(f: Formula) -> Formula {
182 match f {
183 Formula::Not(inner) => match *inner {
184 Formula::True => Formula::False,
185 Formula::False => Formula::True,
186 Formula::Not(g) => nnf_formula(*g),
187 Formula::And(a, b) => nnf_formula(Formula::or(Formula::not(*a), Formula::not(*b))),
188 Formula::Or(a, b) => nnf_formula(Formula::and(Formula::not(*a), Formula::not(*b))),
189 Formula::Forall { var, body } => nnf_formula(Formula::Exists {
190 var,
191 body: Box::new(Formula::not(*body)),
192 }),
193 Formula::Exists { var, body } => nnf_formula(Formula::Forall {
194 var,
195 body: Box::new(Formula::not(*body)),
196 }),
197 Formula::Atom {
198 kind: PredicateKind::True,
199 ..
200 } => Formula::False,
201 Formula::Atom {
202 kind: PredicateKind::False,
203 ..
204 } => Formula::True,
205 Formula::Atom { kind, args } if is_rel(&kind) => Formula::Atom {
206 kind: dual_kind(kind),
207 args,
208 },
209 inner => Formula::Not(Box::new(inner)),
210 },
211 Formula::And(a, b) => Formula::and(nnf_formula(*a), nnf_formula(*b)),
212 Formula::Or(a, b) => Formula::or(nnf_formula(*a), nnf_formula(*b)),
213 Formula::Forall { var, body } => Formula::Forall {
214 var,
215 body: Box::new(nnf_formula(*body)),
216 },
217 Formula::Exists { var, body } => Formula::Exists {
218 var,
219 body: Box::new(nnf_formula(*body)),
220 },
221 other => other,
222 }
223}
224
225fn insert_formula_vars(pool: &ExprPool, expr: ExprId, out: &mut BTreeSet<ExprId>) {
230 for v in resultant::collect_free_vars(expr, pool) {
231 out.insert(v);
232 }
233}
234
235fn free_vars_formula(f: &Formula, pool: &ExprPool) -> BTreeSet<ExprId> {
236 match f {
237 Formula::Atom { args, .. } => {
238 let mut s = BTreeSet::new();
239 for &a in args {
240 insert_formula_vars(pool, a, &mut s);
241 }
242 s
243 }
244 Formula::And(a, b) | Formula::Or(a, b) => {
245 let mut s = free_vars_formula(a, pool);
246 s.extend(free_vars_formula(b, pool));
247 s
248 }
249 Formula::Not(x) => free_vars_formula(x, pool),
250 Formula::Exists { var, body } => {
251 let mut s = free_vars_formula(body, pool);
252 s.insert(*var);
253 s
254 }
255 Formula::Forall { var, body } => {
256 let mut s = free_vars_formula(body, pool);
257 s.insert(*var);
258 s
259 }
260 Formula::True | Formula::False => BTreeSet::new(),
261 }
262}
263
264fn contains_quantifier(f: &Formula) -> bool {
265 match f {
266 Formula::Exists { .. } | Formula::Forall { .. } => true,
267 Formula::And(a, b) | Formula::Or(a, b) => contains_quantifier(a) || contains_quantifier(b),
268 Formula::Not(x) => contains_quantifier(x),
269 Formula::True | Formula::False | Formula::Atom { .. } => false,
270 }
271}
272
273fn is_quantifier_free(f: &Formula) -> bool {
274 !contains_quantifier(f)
275}
276
277fn free_vars_subset_of_binding(pool: &ExprPool, f: &Formula, allowed: &BTreeSet<ExprId>) -> bool {
278 free_vars_formula(f, pool).is_subset(allowed)
279}
280
281fn poly_exprs_from_atom(
282 pool: &ExprPool,
283 kind: &PredicateKind,
284 args: &[ExprId],
285 _quant_var: ExprId,
286) -> Result<Vec<ExprId>, CadError> {
287 use PredicateKind::{False, True};
288 if matches!(kind, True | False) {
289 return Ok(vec![]);
290 }
291 if !is_rel(kind) {
292 return Err(CadError::Unsupported(
293 "only relation atoms are supported in CAD QE",
294 ));
295 }
296 if args.len() != 2 {
297 return Err(CadError::Logic(LogicError::UnsupportedExpr(
298 "relational predicate arity must be 2",
299 )));
300 }
301 let lhs = args[0];
302 let rhs = args[1];
303 let lhs_mrhs = poly_diff(pool, lhs, rhs)?;
304 Ok(vec![lhs_mrhs])
305}
306
307fn poly_exprs_from_formula(
308 pool: &ExprPool,
309 f: &Formula,
310 var: ExprId,
311) -> Result<Vec<ExprId>, CadError> {
312 match f {
313 Formula::True | Formula::False => Ok(vec![]),
314 Formula::Atom { kind, args } => poly_exprs_from_atom(pool, kind, args, var),
315 Formula::Not(inner) => {
316 if let Formula::Atom { kind, args } = inner.as_ref() {
317 if is_rel(kind) {
318 poly_exprs_from_atom(pool, kind, args, var)
319 } else {
320 Err(CadError::Unsupported(
321 "NOT is only supported on relation atoms",
322 ))
323 }
324 } else {
325 Err(CadError::Unsupported(
326 "`Not` expects a relational atom underneath in this CAD fragment",
327 ))
328 }
329 }
330 Formula::And(a, b) | Formula::Or(a, b) => {
331 let mut v = poly_exprs_from_formula(pool, a, var)?;
332 v.extend(poly_exprs_from_formula(pool, b, var)?);
333 Ok(v)
334 }
335 _ => Err(CadError::Unsupported(
336 "expected quantifier-free Boolean combination of polynomials",
337 )),
338 }
339}
340
341fn eq_polynomials_for_sampling(
342 pool: &ExprPool,
343 f: &Formula,
344 var: ExprId,
345) -> Result<Vec<UniPoly>, CadError> {
346 fn rec(
347 pool: &ExprPool,
348 f: &Formula,
349 var: ExprId,
350 out: &mut Vec<UniPoly>,
351 ) -> Result<(), CadError> {
352 match f {
353 Formula::Atom {
354 kind: PredicateKind::Eq,
355 args,
356 } => {
357 if args.len() != 2 {
358 return Err(CadError::Logic(LogicError::UnsupportedExpr(
359 "Eq arity must be 2",
360 )));
361 }
362 let d = UniPoly::from_symbolic(poly_diff(pool, args[0], args[1])?, var, pool)?;
363 if !d.is_zero() {
364 out.push(d);
365 }
366 Ok(())
367 }
368 Formula::And(a, b) | Formula::Or(a, b) => {
369 rec(pool, a, var, out)?;
370 rec(pool, b, var, out)
371 }
372 Formula::Not(x) => {
373 if let Formula::Atom {
374 kind: PredicateKind::Eq,
375 args,
376 } = x.as_ref()
377 {
378 let _ = args;
380 Ok(())
381 } else {
382 Err(CadError::Unsupported(
383 "NOT over non-Eq unsupported for sampling roots",
384 ))
385 }
386 }
387 _ => Ok(()),
388 }
389 }
390
391 let mut out = Vec::new();
392 rec(pool, f, var, &mut out)?;
393 Ok(out)
394}
395
396fn poly_diff(pool: &ExprPool, lhs: ExprId, rhs: ExprId) -> Result<ExprId, CadError> {
401 let minus_one = pool.integer(-1_i32);
402 let neg_rhs = pool.mul(vec![minus_one, rhs]);
403 Ok(pool.add(vec![lhs, neg_rhs]))
404}
405
406fn combine_algebraic_master(main_var: ExprId, polys: &[UniPoly]) -> UniPoly {
407 let mut nz: Vec<UniPoly> = polys
408 .iter()
409 .filter(|p| !p.is_zero())
410 .map(|p| p.squarefree_part())
411 .collect();
412 if nz.is_empty() {
413 UniPoly::constant(main_var, 1)
414 } else {
415 let mut m = nz.swap_remove(0);
416 for q in nz {
417 m = UniPoly::lcm_poly(&m, &q);
418 }
419 m.squarefree_part()
420 }
421}
422
423fn cauchy_bound(p: &UniPoly) -> rug::Rational {
424 let coeffs = p.coefficients();
425 if coeffs.is_empty() || p.degree() <= 0 {
426 return rug::Rational::from((1_u32, 1_u32));
427 }
428 let n = coeffs.len() - 1;
429 let lead = coeffs[n].clone().abs();
430 if lead.is_zero() {
431 return rug::Rational::from((1_u32, 1_u32));
432 }
433 let mut num = rug::Integer::from(0);
434 for c in coeffs.iter().take(n) {
435 num += c.clone().abs();
436 }
437 let frac = rug::Rational::from((num, lead)) + rug::Rational::from(1);
438 frac + rug::Rational::from(1)
439}
440
441fn iv_midpoint(lo: &rug::Rational, hi: &rug::Rational) -> rug::Rational {
442 (lo.clone() + hi.clone()) / rug::Rational::from((2_u32, 1_u32))
443}
444
445fn cmp_atom(
450 pool: &ExprPool,
451 kind: &PredicateKind,
452 args: &[ExprId],
453 var: ExprId,
454 pt: &rug::Rational,
455) -> Result<bool, CadError> {
456 use PredicateKind::{Eq, False, Ge, Gt, Le, Lt, Ne, True};
457 if matches!(kind, True) {
458 return Ok(true);
459 }
460 if matches!(kind, False) {
461 return Ok(false);
462 }
463 let diff = UniPoly::from_symbolic(poly_diff(pool, args[0], args[1])?, var, pool)?;
464 let v = diff.eval_rational(pt);
465 let z = rug::Rational::from(0);
466 Ok(match kind {
467 Eq => v == z,
468 Ne => v != z,
469 Lt => v < z,
470 Le => v <= z,
471 Gt => v > z,
472 Ge => v >= z,
473 _ => {
474 return Err(CadError::Unsupported("non-relational predicate in atom"));
475 }
476 })
477}
478
479fn eval_qf_formula(
480 pool: &ExprPool,
481 var: ExprId,
482 f: &Formula,
483 pt: &rug::Rational,
484) -> Result<bool, CadError> {
485 match f {
486 Formula::True => Ok(true),
487 Formula::False => Ok(false),
488 Formula::Atom { kind, args } => cmp_atom(pool, kind, args.as_slice(), var, pt),
489 Formula::And(a, b) => {
490 Ok(eval_qf_formula(pool, var, a, pt)? && eval_qf_formula(pool, var, b, pt)?)
491 }
492 Formula::Or(a, b) => {
493 Ok(eval_qf_formula(pool, var, a, pt)? || eval_qf_formula(pool, var, b, pt)?)
494 }
495 Formula::Not(x) => Ok(!eval_qf_formula(pool, var, x, pt)?),
496 _ => Err(CadError::Unsupported(
497 "quantifiers not allowed inside QF eval",
498 )),
499 }
500}
501
502fn intervals_overlap(a: &RootInterval, b: &RootInterval) -> bool {
503 !(a.hi < b.lo || b.hi < a.lo)
504}
505
506fn gcd_interval_shares_root_iv(g: &UniPoly, iv: &RootInterval) -> Result<bool, CadError> {
508 if g.is_zero() || g.degree() <= 0 {
509 return Ok(false);
510 }
511 let sg = g.squarefree_part();
512 let roots_g = real_roots(&sg)?;
513 Ok(roots_g.into_iter().any(|rj| intervals_overlap(iv, &rj)))
514}
515
516fn eval_qf_formula_on_iv(
517 pool: &ExprPool,
518 var: ExprId,
519 phi: &Formula,
520 iv: &RootInterval,
521 focus_sf: &UniPoly,
522) -> Result<bool, CadError> {
523 match phi {
524 Formula::True => Ok(true),
525 Formula::False => Ok(false),
526 Formula::Atom { kind, args } => {
527 use PredicateKind::{Eq, False, Ne, True};
528 if matches!(kind, True) {
529 return Ok(true);
530 }
531 if matches!(kind, False) {
532 return Ok(false);
533 }
534 let d_poly = UniPoly::from_symbolic(poly_diff(pool, args[0], args[1])?, var, pool)?;
535 if matches!(kind, Eq) {
536 let gx = focus_sf.gcd(&d_poly).unwrap_or_else(|| UniPoly::zero(var));
537 return gcd_interval_shares_root_iv(&gx, iv);
538 }
539 if matches!(kind, Ne) {
540 let gx = focus_sf.gcd(&d_poly).unwrap_or_else(|| UniPoly::zero(var));
541 return Ok(!gcd_interval_shares_root_iv(&gx, iv)?);
542 }
543 let mid = iv_midpoint(&iv.lo, &iv.hi);
544 eval_qf_formula(pool, var, phi, &mid)
545 }
546 Formula::And(a, b) => Ok(eval_qf_formula_on_iv(pool, var, a, iv, focus_sf)?
547 && eval_qf_formula_on_iv(pool, var, b, iv, focus_sf)?),
548 Formula::Or(a, b) => Ok(eval_qf_formula_on_iv(pool, var, a, iv, focus_sf)?
549 || eval_qf_formula_on_iv(pool, var, b, iv, focus_sf)?),
550 Formula::Not(x) => Ok(!eval_qf_formula_on_iv(pool, var, x, iv, focus_sf)?),
551 _ => Err(CadError::Unsupported(
552 "unexpected quantifier during CAD sample refinement",
553 )),
554 }
555}
556
557fn decide_exists_univariate(
562 pool: &ExprPool,
563 var: ExprId,
564 phi: Formula,
565) -> Result<QeResult, CadError> {
566 let allowed: BTreeSet<ExprId> = [var].into_iter().collect();
567 if !free_vars_subset_of_binding(pool, &phi, &allowed) {
568 return Err(CadError::Unsupported(
569 "quantifier-free body may only reference the bound variable (constants allowed)",
570 ));
571 }
572
573 let poly_exprs = poly_exprs_from_formula(pool, &phi, var)?;
574 let mut polys_uni = Vec::<UniPoly>::new();
575 for e in poly_exprs.iter().copied() {
576 match UniPoly::from_symbolic(e, var, pool) {
577 Ok(p) => {
578 if !p.is_zero() {
579 polys_uni.push(p.clone());
580 }
581 }
582 Err(err) => return Err(CadError::NotPolynomial(err)),
583 }
584 }
585
586 let mut candidates: BTreeSet<rug::Rational> = BTreeSet::new();
587 let master = combine_algebraic_master(var, &polys_uni);
588 let br = cauchy_bound(&master);
589 let roots_iv = real_roots(&master)?;
590
591 let mut breakpoints: Vec<rug::Rational> = Vec::new();
592 breakpoints.push(-br.clone());
593 for iv in roots_iv.iter() {
594 breakpoints.push(iv.lo.clone());
595 breakpoints.push(iv.hi.clone());
596 }
597 breakpoints.push(br.clone());
598
599 breakpoints.sort();
600 breakpoints.dedup_by(|a, b| *a == *b);
601 for w in breakpoints.windows(2) {
603 let lo = &w[0];
604 let hi = &w[1];
605 if lo < hi {
606 candidates.insert(iv_midpoint(lo, hi));
607 }
608 }
609
610 for p in eq_polynomials_for_sampling(pool, &phi, var)? {
611 let sf = p.squarefree_part();
612 let riv = real_roots(&sf)?;
613 for iv in riv {
614 candidates.insert(iv_midpoint(&iv.lo, &iv.hi));
615 }
616 }
617
618 for pt in candidates {
619 if eval_qf_formula(pool, var, &phi, &pt)? {
620 let mut wm = HashMap::new();
621 wm.insert(var, pt.clone());
622 return Ok(QeResult {
623 truth: true,
624 witness: Some(wm),
625 });
626 }
627 }
628
629 for p_focus in eq_polynomials_for_sampling(pool, &phi, var)? {
632 let sf = p_focus.squarefree_part();
633 if sf.is_zero() {
634 continue;
635 }
636 for iv in real_roots(&sf)? {
637 if eval_qf_formula_on_iv(pool, var, &phi, &iv, &sf)? {
638 let mid = iv_midpoint(&iv.lo, &iv.hi);
639 let mut wm = HashMap::new();
640 wm.insert(var, mid);
641 return Ok(QeResult {
642 truth: true,
643 witness: Some(wm),
644 });
645 }
646 }
647 }
648
649 Ok(QeResult {
650 truth: false,
651 witness: None,
652 })
653}
654
655fn decide_closed_qf(pool: &ExprPool, phi: Formula) -> Result<QeResult, CadError> {
656 if !free_vars_formula(&phi, pool).is_empty() {
657 return Err(CadError::Unsupported(
658 "closed formula unexpectedly contains free symbols",
659 ));
660 }
661 let zero = rug::Rational::from(0);
662 let dummy = pool.symbol("__cad_iv_local", Domain::Real);
663 Ok(QeResult {
664 truth: eval_qf_formula(pool, dummy, &phi, &zero)?,
665 witness: None,
666 })
667}
668
669fn decide_formula_inner(pool: &ExprPool, phi: Formula) -> Result<QeResult, CadError> {
670 let phi = simplify_formula_constants(nnf_formula(phi));
671 if is_quantifier_free(&phi) {
672 return decide_closed_qf(pool, phi);
673 }
674 match phi {
675 Formula::Exists { var, body } => {
676 if contains_quantifier(&body) {
677 return Err(CadError::Unsupported(
678 "nested quantifiers are not implemented",
679 ));
680 }
681 decide_exists_univariate(pool, var, *body)
682 }
683 Formula::Forall { var, body } => {
684 if contains_quantifier(&body) {
685 return Err(CadError::Unsupported(
686 "nested quantifiers are not implemented",
687 ));
688 }
689 let neg_body = nnf_formula(Formula::Not(body));
690 let inner = decide_exists_univariate(pool, var, neg_body)?;
691 Ok(QeResult {
692 truth: !inner.truth,
693 witness: None,
694 })
695 }
696 Formula::True => Ok(QeResult {
697 truth: true,
698 witness: None,
699 }),
700 Formula::False => Ok(QeResult {
701 truth: false,
702 witness: None,
703 }),
704 _ => Err(CadError::Unsupported(
705 "sentence must begin with forall/exists after quantifiers are outermost",
706 )),
707 }
708}
709
710pub fn decide(formula: &Formula, pool: &ExprPool) -> Result<QeResult, CadError> {
713 decide_formula_inner(pool, formula.clone())
714}
715
716pub fn decide_expr(expr: ExprId, pool: &ExprPool) -> Result<QeResult, CadError> {
718 let fm = formula_from_expr(expr, pool)?;
719 decide(&fm, pool)
720}
721
722pub fn cad_project(
730 polynomials: &[ExprId],
731 elim_var: ExprId,
732 pool: &ExprPool,
733) -> Result<Vec<ExprId>, CadError> {
734 if polynomials.is_empty() {
735 return Ok(Vec::new());
736 }
737 let mut all_vars = BTreeSet::new();
738 all_vars.insert(elim_var);
739 for &p in polynomials {
740 all_vars.extend(resultant::collect_free_vars(p, pool));
741 }
742 let vars_no_elim: Vec<ExprId> = all_vars
743 .iter()
744 .copied()
745 .filter(|&v| v != elim_var)
746 .collect();
747
748 let mut uniq: Vec<ExprId> = Vec::new();
749 let mut seen: BTreeSet<ExprId> = BTreeSet::new();
750
751 for i in 0..polynomials.len() {
752 let f_expr = polynomials[i];
753 let df = diff(f_expr, elim_var, pool)?.value;
754
755 let is_zero_f = UniPoly::from_symbolic(f_expr, elim_var, pool)
756 .map(|u| u.is_zero())
757 .unwrap_or(false);
758 let is_zero_df = UniPoly::from_symbolic(df, elim_var, pool)
759 .map(|u| u.is_zero())
760 .unwrap_or(true);
761
762 if !is_zero_f && !is_zero_df {
764 let rp = resultant(f_expr, df, elim_var, pool)?.value;
765 if seen.insert(rp) {
766 uniq.push(rp);
767 }
768 }
769
770 for &g_expr in polynomials.iter().skip(i + 1) {
772 let is_zero_g = UniPoly::from_symbolic(g_expr, elim_var, pool)
773 .map(|u| u.is_zero())
774 .unwrap_or(false);
775 if is_zero_f || is_zero_g {
776 continue;
777 }
778 let r = resultant(f_expr, g_expr, elim_var, pool)?.value;
779 if seen.insert(r) {
780 uniq.push(r);
781 }
782 }
783 }
784
785 let mut normed = Vec::<ExprId>::new();
786 for e in uniq {
787 let simplified = if vars_no_elim.is_empty() {
788 e
789 } else {
790 poly_normal(e, vars_no_elim.clone(), pool)?
791 };
792 normed.push(simplified);
793 }
794
795 normed.sort_unstable();
796 normed.dedup();
797 Ok(normed)
798}
799
800pub fn cad_lift(
803 polynomials: &[ExprId],
804 main_var: ExprId,
805 pool: &ExprPool,
806) -> Result<Vec<RootInterval>, CadError> {
807 let mut polys_uni = Vec::new();
808 for &e in polynomials {
809 match UniPoly::from_symbolic(e, main_var, pool) {
810 Ok(u) => {
811 if !u.is_zero() {
812 polys_uni.push(u);
813 }
814 }
815 Err(e) => return Err(CadError::NotPolynomial(e)),
816 }
817 }
818 let m = combine_algebraic_master(main_var, &polys_uni);
819 Ok(real_roots(&m)?)
820}
821
822#[cfg(test)]
827mod tests {
828 use super::*;
829 use crate::kernel::Domain;
830
831 #[test]
832 fn forall_x_squared_plus_one_positive() {
833 let p = ExprPool::new();
834 let x = p.symbol("x", Domain::Real);
835 let one = p.integer(1_i32);
836 let x_sq = p.pow(x, p.integer(2_i32));
837 let body = p.pred_gt(p.add(vec![x_sq, one]), p.integer(0_i32));
838
839 let f = Formula::Forall {
840 var: x,
841 body: Box::new(formula_from_expr(body, &p).unwrap()),
842 };
843 let r = decide(&f, &p).unwrap();
844 assert!(r.truth);
845 assert!(r.witness.is_none());
846 }
847
848 #[test]
849 fn exists_roots_x_squared_minus_two() {
850 let p = ExprPool::new();
851 let x = p.symbol("x", Domain::Real);
852 let two = p.integer(2_i32);
853 let xs = p.pow(x, p.integer(2_i32));
854 let body = p.pred_eq(xs, two);
855 let f = Formula::Exists {
856 var: x,
857 body: Box::new(formula_from_expr(body, &p).unwrap()),
858 };
859 let r = decide(&f, &p).unwrap();
860 assert!(r.truth);
861 assert!(r.witness.is_some());
862 }
863
864 #[test]
865 fn cad_lift_univariate_quadratic() {
866 let p = ExprPool::new();
867 let x = p.symbol("x", Domain::Real);
868 let xs = p.add(vec![p.pow(x, p.integer(2_i32)), p.integer(-2_i32)]);
869 let ivs = cad_lift(&[xs], x, &p).unwrap();
870 assert_eq!(ivs.len(), 2);
871 assert!(ivs.iter().all(|iv| iv.lo <= iv.hi));
872 }
873
874 #[test]
875 fn cad_project_circle_eliminates_y() {
876 let p = ExprPool::new();
877 let x = p.symbol("x", Domain::Real);
878 let y = p.symbol("y", Domain::Real);
879 let circle = p.add(vec![
880 p.pow(x, p.integer(2_i32)),
881 p.pow(y, p.integer(2_i32)),
882 p.integer(-1_i32),
883 ]);
884 let line = p.add(vec![y, pool_neg_x(&p, x)]); let pr = cad_project(&[circle, line], y, &p).unwrap();
886 assert!(!pr.is_empty());
887 }
888
889 fn pool_neg_x(pool: &ExprPool, x: ExprId) -> ExprId {
890 pool.mul(vec![pool.integer(-1_i32), x])
891 }
892
893 #[test]
894 fn unipoly_eval_rational_zero() {
895 let p = ExprPool::new();
896 let x = p.symbol("x", Domain::Real);
897 let qp = UniPoly::from_symbolic(p.add(vec![x, p.integer(2_i32)]), x, &p).unwrap();
898 let z = qp.eval_rational(&rug::Rational::from(-2));
899 assert_eq!(z, 0);
900 }
901}