1use std::fmt;
114
115use num_bigint::BigInt;
116use num_traits::{One, Zero};
117
118use crate::api::context::Context;
119use crate::api::eq::Equation;
120use crate::api::expr::Ex;
121use crate::api::poly_ex::Poly;
122use crate::base::errors::SymplexError;
123use crate::base::interval::Interval;
124use crate::domains::linprog::{Feasibility, LpProblem, LpStatus, nonneg_combination};
125use crate::output::lean::{LeanOpts, MATHLIB_LINE_WIDTH, lean_ident, wrap_lean};
126
127mod outcome;
128mod polyhedron;
129mod sos;
130pub use crate::domains::linprog::BudgetHit;
133pub use outcome::{Certificate, Outcome};
134pub use polyhedron::{
135 ParamBound, ParamBoundTree, PolyhedronCertificate, PolyhedronCertificateData,
136 PolyhedronLeanNames, PolyhedronLeanSteps, PolyhedronOpts, PolyhedronOutcome, PolyhedronProver,
137 PolyhedronTerm, PolyhedronTermData, PolyhedronUnknown, prove_nonnegative_on_polyhedron,
138 prove_polyhedron_empty,
139};
140pub use sos::{
141 SosCertificate, SosCertificateData, SosOpts, SosOutcome, SosUnknown, is_sos, prove_sos,
142};
143
144pub(crate) mod serial {
146 use super::{BigInt, Q, SymplexError};
147
148 pub(crate) fn q_to_str(q: &Q) -> String {
149 format!("{}/{}", q.numer(), q.denom())
150 }
151
152 pub(crate) fn q_from_str(s: &str, operation: &'static str) -> Result<Q, SymplexError> {
153 let bad = || SymplexError::InvalidArgument {
154 operation,
155 reason: format!("malformed rational `{s}` (expected `p/q`)"),
156 };
157 let (n, d) = s.split_once('/').unwrap_or((s, "1"));
158 let n: BigInt = n.trim().parse().map_err(|_| bad())?;
159 let d: BigInt = d.trim().parse().map_err(|_| bad())?;
160 if d == BigInt::from(0) {
161 return Err(bad());
162 }
163 Ok(Q::new(n, d))
164 }
165}
166
167#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
170pub struct BoxBoundTree {
171 pub var: crate::output::tree::ExprTree,
173 pub lo: crate::output::tree::ExprTree,
175 pub hi: crate::output::tree::ExprTree,
177}
178
179#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
183pub struct HandelmanTermData {
184 pub lower_powers: Vec<u32>,
186 pub upper_powers: Vec<u32>,
188 pub weight: String,
190}
191
192#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
194pub struct BoxCertificateData {
195 pub goal: crate::output::tree::ExprTree,
197 pub bounds: Vec<BoxBoundTree>,
199 pub terms: Vec<HandelmanTermData>,
201 pub square: Option<crate::output::tree::ExprTree>,
203}
204
205#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
208pub struct HalfLineCertificateData {
209 pub goal: crate::output::tree::ExprTree,
211 pub var: crate::output::tree::ExprTree,
213 pub endpoint: crate::output::tree::ExprTree,
215 pub ray: String,
217 pub polya_power: u32,
219 pub coefficients: Vec<String>,
221 pub square: Option<crate::output::tree::ExprTree>,
223}
224
225use crate::base::numeric::Q;
226
227fn invalid(reason: impl Into<String>) -> SymplexError {
228 SymplexError::invalid_argument("prove_nonnegative_on_box", reason)
229}
230
231#[derive(Clone, Debug)]
233pub struct BoxBound {
234 pub var: Ex,
236 pub lo: Ex,
238 pub hi: Ex,
240}
241
242#[derive(Clone, Debug, PartialEq, Eq)]
244pub struct HandelmanTerm {
245 pub lower_powers: Vec<u32>,
247 pub upper_powers: Vec<u32>,
249 pub weight: Q,
251}
252
253impl HandelmanTerm {
254 pub fn degree(&self) -> u32 {
256 self.lower_powers.iter().sum::<u32>() + self.upper_powers.iter().sum::<u32>()
257 }
258}
259
260#[derive(Clone, Debug)]
265pub struct BoxCertificate {
266 goal: Poly,
267 bounds: Vec<BoxBound>,
268 terms: Vec<HandelmanTerm>,
269 square: Option<Poly>,
271}
272
273impl BoxCertificate {
274 pub fn goal(&self) -> &Poly {
276 &self.goal
277 }
278
279 pub fn bounds(&self) -> &[BoxBound] {
281 &self.bounds
282 }
283
284 pub fn terms(&self) -> &[HandelmanTerm] {
286 &self.terms
287 }
288
289 pub fn square(&self) -> Option<&Poly> {
294 self.square.as_ref()
295 }
296
297 pub fn degree(&self) -> u32 {
299 self.terms
300 .iter()
301 .map(HandelmanTerm::degree)
302 .max()
303 .unwrap_or(0)
304 }
305
306 pub fn product(&self, term: &HandelmanTerm) -> Poly {
308 let ctx = self.goal.context();
309 let gens: Vec<&Ex> = self.goal.gens().iter().collect();
310 let mut acc = Poly::one(&ctx, &gens).unwrap_or_else(|_| self.goal.clone());
311 for (i, b) in self.bounds.iter().enumerate() {
312 let lower = &b.var - &b.lo;
313 let upper = &b.hi - &b.var;
314 for _ in 0..term.lower_powers.get(i).copied().unwrap_or(0) {
315 if let Some(p) = Poly::new(&lower, &gens)
316 && let Ok(m) = acc.mul(&p)
317 {
318 acc = m;
319 }
320 }
321 for _ in 0..term.upper_powers.get(i).copied().unwrap_or(0) {
322 if let Some(p) = Poly::new(&upper, &gens)
323 && let Ok(m) = acc.mul(&p)
324 {
325 acc = m;
326 }
327 }
328 }
329 acc
330 }
331
332 pub fn verify(&self) -> bool {
338 let ctx = self.goal.context();
339 let gens: Vec<&Ex> = self.goal.gens().iter().collect();
340 let Ok(mut acc) = Poly::zero(&ctx, &gens) else {
341 return false;
342 };
343 for t in &self.terms {
344 if t.weight <= Q::zero() {
345 return false;
346 }
347 let w = ctx.from_ratio(t.weight.clone());
348 let Ok(scaled) = self.product(t).scale(&w) else {
349 return false;
350 };
351 let Ok(sum) = acc.add(&scaled) else {
352 return false;
353 };
354 acc = sum;
355 }
356 if let Some(g) = &self.square {
357 let Ok(g2) = g.mul(g) else {
358 return false;
359 };
360 let Ok(prod) = acc.mul(&g2) else {
361 return false;
362 };
363 acc = prod;
364 }
365 acc.equals(&self.goal)
366 }
367
368 pub fn product_expr(&self, term: &HandelmanTerm) -> Ex {
371 let ctx = self.goal.context();
372 let mut acc = ctx.one();
373 for (i, b) in self.bounds.iter().enumerate() {
374 let a = i64::from(term.lower_powers.get(i).copied().unwrap_or(0));
375 let e = i64::from(term.upper_powers.get(i).copied().unwrap_or(0));
376 if a > 0 {
377 acc *= (&b.var - &b.lo).powi(a);
378 }
379 if e > 0 {
380 acc *= (&b.hi - &b.var).powi(e);
381 }
382 }
383 acc
384 }
385
386 pub fn identity(&self) -> Equation {
389 let ctx = self.goal.context();
390 let mut rhs = ctx.zero();
391 for t in &self.terms {
392 rhs += ctx.from_ratio(t.weight.clone()) * self.product_expr(t);
393 }
394 if let Some(g) = &self.square {
395 rhs = g.to_ex().powi(2) * rhs;
396 }
397 Equation::new(self.goal.to_ex(), rhs)
398 }
399
400 pub fn to_lean(&self, theorem_name: &str) -> Result<String, SymplexError> {
414 self.to_lean_with(theorem_name, &LeanOpts::default())
415 }
416
417 pub fn to_lean_with(
419 &self,
420 theorem_name: &str,
421 opts: &LeanOpts,
422 ) -> Result<String, SymplexError> {
423 let real = &opts.real_type;
424 let vars: Vec<String> = self
425 .bounds
426 .iter()
427 .map(|b| lean_ident(&b.var.to_string()))
428 .collect();
429 let n = self.bounds.len();
432 let mut uses_lo = vec![false; n];
433 let mut uses_hi = vec![false; n];
434 for t in &self.terms {
435 for i in 0..n {
436 uses_lo[i] |= t.lower_powers.get(i).copied().unwrap_or(0) > 0;
437 uses_hi[i] |= t.upper_powers.get(i).copied().unwrap_or(0) > 0;
438 }
439 }
440 let mut hyps: Vec<String> = Vec::new();
442 let mut lo_names: Vec<String> = Vec::new();
443 let mut hi_names: Vec<String> = Vec::new();
444 for (i, (b, v)) in self.bounds.iter().zip(&vars).enumerate() {
445 let lo = b.lo.to_lean_with(opts)?;
446 let hi = b.hi.to_lean_with(opts)?;
447 let base = v.trim_matches(['«', '»']);
448 let lo_name = format!("{}h_{base}_lo", if uses_lo[i] { "" } else { "_" });
449 let hi_name = format!("{}h_{base}_hi", if uses_hi[i] { "" } else { "_" });
450 hyps.push(format!("({lo_name} : {lo} ≤ {v})"));
451 hyps.push(format!("({hi_name} : {v} ≤ {hi})"));
452 lo_names.push(lo_name);
453 hi_names.push(hi_name);
454 }
455 let goal = self.goal.to_ex().to_lean_with(opts)?;
456 let square_hint = match &self.square {
457 Some(g) => Some(format!("sq_nonneg ({})", g.to_ex().to_lean_with(opts)?)),
458 None => None,
459 };
460
461 let mut hints: Vec<String> = Vec::new();
465 let mut max_factors = 0usize;
466 if let Some(sq) = &square_hint
467 && self.terms.iter().any(|t| t.degree() == 0)
468 {
469 hints.push(sq.clone());
471 max_factors = max_factors.max(2);
472 }
473 for t in &self.terms {
474 let mut factors: Vec<String> = Vec::new();
475 for i in 0..n {
476 for _ in 0..t.lower_powers.get(i).copied().unwrap_or(0) {
477 factors.push(format!("sub_nonneg.mpr {}", lo_names[i]));
478 }
479 for _ in 0..t.upper_powers.get(i).copied().unwrap_or(0) {
480 factors.push(format!("sub_nonneg.mpr {}", hi_names[i]));
481 }
482 }
483 let Some((first, rest)) = factors.split_first() else {
484 continue; };
486 let mut acc = first.clone();
487 for f in rest {
488 acc = format!("mul_nonneg ({acc}) ({f})");
489 }
490 if let Some(sq) = &square_hint {
491 acc = format!("mul_nonneg ({sq}) ({acc})");
492 max_factors = max_factors.max(factors.len() + 2);
493 } else {
494 max_factors = max_factors.max(factors.len());
495 }
496 if !hints.contains(&acc) {
497 hints.push(acc);
498 }
499 }
500
501 let sig = format!(
502 "theorem {} ({} : {real}) {} :\n 0 ≤ {goal} := by\n",
503 lean_ident(theorem_name),
504 vars.join(" "),
505 hyps.join(" ")
506 );
507 let tactic = if hints.is_empty() {
511 " linarith".to_string()
512 } else if max_factors <= 1 {
513 format!(" linarith [{}]", hints.join(", "))
514 } else {
515 format!(" nlinarith [{}]", hints.join(", "))
516 };
517 Ok(wrap_lean(&format!("{sig}{tactic}\n"), MATHLIB_LINE_WIDTH))
518 }
519}
520
521impl BoxCertificate {
522 pub fn to_data(&self) -> BoxCertificateData {
524 BoxCertificateData {
525 goal: self.goal.to_ex().to_tree(),
526 bounds: self
527 .bounds
528 .iter()
529 .map(|b| BoxBoundTree {
530 var: b.var.to_tree(),
531 lo: b.lo.to_tree(),
532 hi: b.hi.to_tree(),
533 })
534 .collect(),
535 terms: self
536 .terms
537 .iter()
538 .map(|t| HandelmanTermData {
539 lower_powers: t.lower_powers.clone(),
540 upper_powers: t.upper_powers.clone(),
541 weight: serial::q_to_str(&t.weight),
542 })
543 .collect(),
544 square: self.square.as_ref().map(|g| g.to_ex().to_tree()),
545 }
546 }
547
548 pub fn from_data(ctx: &Context, data: &BoxCertificateData) -> Result<Self, SymplexError> {
556 const OP: &str = "BoxCertificate::from_data";
557 let bad = |reason: String| SymplexError::InvalidArgument {
558 operation: OP,
559 reason,
560 };
561 let bounds: Vec<BoxBound> = data
562 .bounds
563 .iter()
564 .map(|b| BoxBound {
565 var: ctx.from_tree(&b.var),
566 lo: ctx.from_tree(&b.lo),
567 hi: ctx.from_tree(&b.hi),
568 })
569 .collect();
570 if bounds.is_empty() {
571 return Err(bad(
572 "a certificate needs at least one bounded variable".into()
573 ));
574 }
575 let gens: Vec<&Ex> = bounds.iter().map(|b| &b.var).collect();
576 let goal_ex = ctx.from_tree(&data.goal);
577 let goal = Poly::new(&goal_ex, &gens).ok_or_else(|| {
578 bad(format!(
579 "goal `{goal_ex}` is not a polynomial in the box variables"
580 ))
581 })?;
582 let square = match &data.square {
583 Some(t) => {
584 let e = ctx.from_tree(t);
585 Some(
586 Poly::new(&e, &gens)
587 .ok_or_else(|| bad(format!("square factor `{e}` is not a polynomial")))?,
588 )
589 }
590 None => None,
591 };
592 let mut terms = Vec::with_capacity(data.terms.len());
593 for t in &data.terms {
594 if t.lower_powers.len() != bounds.len() || t.upper_powers.len() != bounds.len() {
595 return Err(bad(
596 "a term's power vectors must have one entry per variable".into(),
597 ));
598 }
599 terms.push(HandelmanTerm {
600 lower_powers: t.lower_powers.clone(),
601 upper_powers: t.upper_powers.clone(),
602 weight: serial::q_from_str(&t.weight, OP)?,
603 });
604 }
605 let cert = BoxCertificate {
606 goal,
607 bounds,
608 terms,
609 square,
610 };
611 if !cert.verify() {
612 return Err(bad("the certificate data does not verify".into()));
613 }
614 Ok(cert)
615 }
616
617 pub fn to_json(&self) -> Result<String, SymplexError> {
639 serde_json::to_string(&self.to_data()).map_err(|e| SymplexError::ComputationFailed {
640 operation: "BoxCertificate::to_json",
641 reason: e.to_string(),
642 })
643 }
644
645 pub fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
652 let data: BoxCertificateData =
653 serde_json::from_str(json).map_err(|e| SymplexError::InvalidArgument {
654 operation: "BoxCertificate::from_json",
655 reason: format!("malformed JSON: {e}"),
656 })?;
657 Self::from_data(ctx, &data)
658 }
659}
660
661impl fmt::Display for BoxCertificate {
662 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
663 let Equation { lhs, rhs } = self.identity();
664 write!(f, "{lhs} = {rhs}")?;
665 for b in &self.bounds {
666 write!(f, ", {} ≤ {} ≤ {}", b.lo, b.var, b.hi)?;
667 }
668 Ok(())
669 }
670}
671
672pub type BoxOutcome = Outcome<BoxCertificate, BoxUnknown>;
675
676#[derive(Clone, Debug, PartialEq)]
681#[non_exhaustive]
682pub struct BoxUnknown {
683 pub farkas: Option<Vec<Q>>,
686 pub degree: u32,
688}
689
690impl fmt::Display for BoxUnknown {
691 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
692 write!(f, "no Handelman certificate of degree ≤ {}", self.degree)?;
693 if self.farkas.is_some() {
694 write!(f, " (Farkas vector available)")?;
695 }
696 Ok(())
697 }
698}
699
700fn refutation_point(vars: &[&Ex], values: Vec<Q>) -> Vec<(Ex, Q)> {
703 vars.iter().map(|v| (*v).clone()).zip(values).collect()
704}
705
706fn products_up_to(n: usize, degree: u32) -> Vec<(Vec<u32>, Vec<u32>)> {
708 let slots = 2 * n;
709 let mut out = Vec::new();
710 let mut current = vec![0u32; slots];
711 fn rec(
713 slot: usize,
714 remaining: u32,
715 current: &mut Vec<u32>,
716 n: usize,
717 out: &mut Vec<(Vec<u32>, Vec<u32>)>,
718 ) {
719 if slot == current.len() {
720 out.push((current[..n].to_vec(), current[n..].to_vec()));
721 return;
722 }
723 for e in 0..=remaining {
724 current[slot] = e;
725 rec(slot + 1, remaining - e, current, n, out);
726 }
727 current[slot] = 0;
728 }
729 rec(0, degree, &mut current, n, &mut out);
730 out
733}
734
735fn value_at(goal: &Poly, point: &[Q]) -> Option<Q> {
737 let ctx = goal.context();
738 let vals: Vec<Ex> = point.iter().map(|q| ctx.from_ratio(q.clone())).collect();
739 let refs: Vec<&Ex> = vals.iter().collect();
740 goal.eval(&refs).ok()?.as_rational()
741}
742
743fn find_counterexample(goal: &Poly, bounds: &[Interval<Q>], steps: u32) -> Option<(Vec<Q>, Q)> {
746 let n = bounds.len();
747 let total = (u64::from(steps) + 1).checked_pow(n as u32)?;
748 if total > 200_000 {
749 return None;
750 }
751 let mut idx = vec![0u32; n];
752 loop {
753 let point: Vec<Q> = idx
754 .iter()
755 .zip(bounds)
756 .map(|(&k, iv)| {
757 &iv.lower + (&iv.upper - &iv.lower) * Q::new(BigInt::from(k), BigInt::from(steps))
758 })
759 .collect();
760 if let Some(v) = value_at(goal, &point)
761 && v < Q::zero()
762 {
763 return Some((point, v));
764 }
765 let mut pos = 0;
767 loop {
768 if pos == n {
769 return None;
770 }
771 if idx[pos] < steps {
772 idx[pos] += 1;
773 break;
774 }
775 idx[pos] = 0;
776 pos += 1;
777 }
778 }
779}
780
781fn sparse_nonneg_combination(
786 columns: &[Vec<Q>],
787 target: &[Q],
788 exponents: &[(Vec<u32>, Vec<u32>)],
789) -> Result<Feasibility, SymplexError> {
790 let m = target.len();
791 let cost: Vec<Q> = exponents
792 .iter()
793 .map(|(a, b)| {
794 let deg: u32 = a.iter().sum::<u32>() + b.iter().sum::<u32>();
795 Q::from_integer(BigInt::from(1 + u64::from(deg)))
796 })
797 .collect();
798 let mut lp = LpProblem::minimize(cost);
799 for i in 0..m {
800 let row: Vec<Q> = columns.iter().map(|c| c[i].clone()).collect();
801 lp = lp.eq(row, target[i].clone());
802 }
803 let sol = lp.solve()?;
804 match sol.status {
805 LpStatus::Optimal => Ok(Feasibility::Feasible(sol.x)),
806 LpStatus::Infeasible => Ok(Feasibility::Infeasible { farkas: sol.farkas }),
807 LpStatus::Unbounded | LpStatus::BudgetExhausted => nonneg_combination(columns, target),
810 }
811}
812
813pub fn prove_nonnegative_on_box(
847 goal: &Ex,
848 bounds: &[BoxBound],
849 degree: u32,
850) -> Result<BoxOutcome, SymplexError> {
851 if bounds.is_empty() {
852 return Err(invalid("at least one bounded variable is required"));
853 }
854 let mut vars: Vec<&Ex> = Vec::with_capacity(bounds.len());
855 let mut q_bounds: Vec<Interval<Q>> = Vec::with_capacity(bounds.len());
856 let mut box_bounds: Vec<BoxBound> = Vec::with_capacity(bounds.len());
857 for BoxBound { var, lo, hi } in bounds {
858 if vars.contains(&var) {
859 return Err(invalid(format!("variable `{var}` is bounded twice")));
860 }
861 let (Some(l), Some(h)) = (lo.eval().as_rational(), hi.eval().as_rational()) else {
862 return Err(invalid(format!(
863 "bounds of `{var}` must be rational literals, got [{lo}, {hi}]"
864 )));
865 };
866 if l >= h {
867 return Err(invalid(format!(
868 "bounds of `{var}` must satisfy lo < hi, got [{lo}, {hi}]"
869 )));
870 }
871 vars.push(var);
872 q_bounds.push(Interval::closed(l, h));
873 box_bounds.push(BoxBound {
874 var: var.clone(),
875 lo: lo.eval(),
876 hi: hi.eval(),
877 });
878 }
879 let goal_poly = Poly::new(goal, &vars).ok_or_else(|| {
880 invalid("goal must be a polynomial in the box variables (other symbols or non-polynomial operations found)")
881 })?;
882 if !goal_poly.has_rational_coeffs() {
883 return Err(invalid(
884 "goal must have rational coefficients (parameters are not supported)",
885 ));
886 }
887
888 if let Some((point, value)) = find_counterexample(&goal_poly, &q_bounds, 8) {
890 return Ok(Outcome::Refuted {
891 point: refutation_point(&vars, point),
892 value,
893 param_value: None,
894 });
895 }
896
897 match handelman_search(&goal_poly, &vars, &box_bounds, degree, None)? {
901 Ok(cert) => Ok(BoxOutcome::Proved(cert)),
902 Err(farkas) => {
903 if let Some((g, h)) = split_square_factor(&goal_poly, &vars)
904 && let Ok(cert) = handelman_search(&h, &vars, &box_bounds, degree, Some(&g))?
905 {
906 let cert = BoxCertificate {
907 goal: goal_poly.clone(),
908 ..cert
909 };
910 if cert.verify() {
911 return Ok(BoxOutcome::Proved(cert));
912 }
913 }
914 if let Some((point, value)) = find_counterexample(&goal_poly, &q_bounds, 32) {
916 return Ok(Outcome::Refuted {
917 point: refutation_point(&vars, point),
918 value,
919 param_value: None,
920 });
921 }
922 Ok(Outcome::Unknown(BoxUnknown { farkas, degree }))
923 }
924 }
925}
926
927fn split_square_factor(goal: &Poly, vars: &[&Ex]) -> Option<(Poly, Poly)> {
933 let e = goal.to_ex();
934 let (content, factors) = if vars.len() == 1 {
935 e.factor_list(vars[0])
936 } else {
937 e.factor_list_all()
938 };
939 if factors.iter().all(|(_, m)| *m < 2) {
940 return None;
941 }
942 let ctx = goal.context();
943 let mut g = ctx.one();
944 let mut h = content;
945 for (f, m) in &factors {
946 if *m >= 2 {
947 g *= f.powi(i64::from(*m / 2));
948 }
949 if *m % 2 == 1 {
950 h *= f;
951 }
952 }
953 let g = Poly::new(&g, vars)?;
954 let h = Poly::new(&h, vars)?;
955 let back = g.mul(&g).ok()?.mul(&h).ok()?;
957 if !back.equals(goal) {
958 return None;
959 }
960 Some((g, h))
961}
962
963fn handelman_search(
967 goal_poly: &Poly,
968 vars: &[&Ex],
969 box_bounds: &[BoxBound],
970 degree: u32,
971 square: Option<&Poly>,
972) -> Result<Result<BoxCertificate, Option<Vec<Q>>>, SymplexError> {
973 let ctx = goal_poly.context();
974 let n = vars.len();
975 let lower: Vec<Poly> = box_bounds
976 .iter()
977 .map(|b| {
978 Poly::new(&(&b.var - &b.lo), vars).ok_or_else(|| invalid("internal: bound factor"))
979 })
980 .collect::<Result<_, _>>()?;
981 let upper: Vec<Poly> = box_bounds
982 .iter()
983 .map(|b| {
984 Poly::new(&(&b.hi - &b.var), vars).ok_or_else(|| invalid("internal: bound factor"))
985 })
986 .collect::<Result<_, _>>()?;
987 let exponents = products_up_to(n, degree);
988 let one = Poly::one(&ctx, vars)?;
989 let mut products: Vec<Poly> = Vec::with_capacity(exponents.len());
990 for (a, b) in &exponents {
991 let mut acc = one.clone();
992 for i in 0..n {
993 for _ in 0..a[i] {
994 acc = acc.mul(&lower[i])?;
995 }
996 for _ in 0..b[i] {
997 acc = acc.mul(&upper[i])?;
998 }
999 }
1000 products.push(acc);
1001 }
1002 let mut all: Vec<&Poly> = products.iter().collect();
1003 all.push(goal_poly);
1004 let monos = Poly::monomial_basis(&all)?;
1005 let coeff_vec = |p: &Poly| -> Result<Vec<Q>, SymplexError> {
1006 monos
1007 .iter()
1008 .map(|m| {
1009 p.coeff_monomial(m)?
1010 .as_rational()
1011 .ok_or_else(|| invalid("internal: non-rational coefficient"))
1012 })
1013 .collect()
1014 };
1015 let columns: Vec<Vec<Q>> = products.iter().map(coeff_vec).collect::<Result<_, _>>()?;
1016 let target = coeff_vec(goal_poly)?;
1017
1018 match sparse_nonneg_combination(&columns, &target, &exponents)? {
1021 Feasibility::Feasible(lambda) => {
1022 let terms: Vec<HandelmanTerm> = exponents
1023 .iter()
1024 .zip(&lambda)
1025 .filter(|(_, w)| **w > Q::zero())
1026 .map(|((a, b), w)| HandelmanTerm {
1027 lower_powers: a.clone(),
1028 upper_powers: b.clone(),
1029 weight: w.clone(),
1030 })
1031 .collect();
1032 let certified = match square {
1035 Some(g) => g.mul(g)?.mul(goal_poly)?,
1036 None => goal_poly.clone(),
1037 };
1038 let cert = BoxCertificate {
1039 goal: certified,
1040 bounds: box_bounds.to_vec(),
1041 terms,
1042 square: square.cloned(),
1043 };
1044 if !cert.verify() {
1045 return Err(SymplexError::ComputationFailed {
1046 operation: "prove_nonnegative_on_box",
1047 reason:
1048 "the LP solution did not reproduce the goal under exact re-verification"
1049 .into(),
1050 });
1051 }
1052 Ok(Ok(cert))
1053 }
1054 Feasibility::Infeasible { farkas } => Ok(Err(farkas)),
1055 }
1056}
1057
1058pub fn is_nonnegative_on_box(goal: &Ex, bounds: &[BoxBound], max_degree: u32) -> Option<bool> {
1076 for d in 1..=max_degree.max(1) {
1077 match prove_nonnegative_on_box(goal, bounds, d) {
1078 Ok(Outcome::Proved(_)) => return Some(true),
1079 Ok(Outcome::Refuted { .. }) => return Some(false),
1080 Ok(Outcome::Unknown(_)) => continue,
1081 Err(_) => return None,
1082 }
1083 }
1084 None
1085}
1086
1087impl Poly {
1088 pub fn express_as_nonneg_combination(
1117 &self,
1118 basis: &[&Poly],
1119 ) -> Result<Feasibility, SymplexError> {
1120 const OP: &str = "Poly::express_as_nonneg_combination";
1121 let bad = |reason: &str| SymplexError::InvalidArgument {
1122 operation: OP,
1123 reason: reason.into(),
1124 };
1125 if basis.is_empty() {
1126 return Err(bad("basis must not be empty"));
1127 }
1128 let mut all: Vec<&Poly> = basis.to_vec();
1129 all.push(self);
1130 let monos = Poly::monomial_basis(&all).map_err(|_| bad("generators differ"))?;
1131 let coeff_vec = |p: &Poly| -> Result<Vec<Q>, SymplexError> {
1132 monos
1133 .iter()
1134 .map(|m| {
1135 p.coeff_monomial(m)?
1136 .as_rational()
1137 .ok_or_else(|| bad("coefficients must be rational"))
1138 })
1139 .collect()
1140 };
1141 let columns: Vec<Vec<Q>> = basis
1142 .iter()
1143 .map(|p| coeff_vec(p))
1144 .collect::<Result<_, _>>()?;
1145 let target = coeff_vec(self)?;
1146 nonneg_combination(&columns, &target)
1147 }
1148}
1149
1150impl Ex {
1151 pub fn prove_nonnegative_on_box(
1153 &self,
1154 bounds: &[BoxBound],
1155 degree: u32,
1156 ) -> Result<BoxOutcome, SymplexError> {
1157 prove_nonnegative_on_box(self, bounds, degree)
1158 }
1159}
1160
1161#[derive(Clone, Debug, PartialEq, Eq)]
1167pub enum Ray {
1168 AtLeast,
1170 AtMost,
1172}
1173
1174#[derive(Clone, Debug)]
1191pub struct HalfLineCertificate {
1192 goal: Poly,
1193 var: Ex,
1194 endpoint: Ex,
1195 ray: Ray,
1196 polya_power: u32,
1197 coefficients: Vec<Q>,
1198 square: Option<Poly>,
1199}
1200
1201impl HalfLineCertificate {
1202 pub fn goal(&self) -> &Poly {
1204 &self.goal
1205 }
1206
1207 pub fn var(&self) -> &Ex {
1209 &self.var
1210 }
1211
1212 pub fn endpoint(&self) -> &Ex {
1214 &self.endpoint
1215 }
1216
1217 pub fn ray(&self) -> &Ray {
1219 &self.ray
1220 }
1221
1222 pub fn polya_power(&self) -> u32 {
1224 self.polya_power
1225 }
1226
1227 pub fn coefficients(&self) -> &[Q] {
1230 &self.coefficients
1231 }
1232
1233 pub fn square(&self) -> Option<&Poly> {
1235 self.square.as_ref()
1236 }
1237
1238 pub fn shift_expr(&self) -> Ex {
1240 match self.ray {
1241 Ray::AtLeast => &self.var - &self.endpoint,
1242 Ray::AtMost => &self.endpoint - &self.var,
1243 }
1244 }
1245
1246 pub fn to_data(&self) -> HalfLineCertificateData {
1248 HalfLineCertificateData {
1249 goal: self.goal.to_ex().to_tree(),
1250 var: self.var.to_tree(),
1251 endpoint: self.endpoint.to_tree(),
1252 ray: match self.ray {
1253 Ray::AtLeast => "at_least".to_string(),
1254 Ray::AtMost => "at_most".to_string(),
1255 },
1256 polya_power: self.polya_power,
1257 coefficients: self.coefficients.iter().map(serial::q_to_str).collect(),
1258 square: self.square.as_ref().map(|g| g.to_ex().to_tree()),
1259 }
1260 }
1261
1262 pub fn from_data(ctx: &Context, data: &HalfLineCertificateData) -> Result<Self, SymplexError> {
1270 const OP: &str = "HalfLineCertificate::from_data";
1271 let bad = |reason: String| SymplexError::InvalidArgument {
1272 operation: OP,
1273 reason,
1274 };
1275 let var = ctx.from_tree(&data.var);
1276 let goal_ex = ctx.from_tree(&data.goal);
1277 let goal = Poly::new(&goal_ex, &[&var])
1278 .ok_or_else(|| bad(format!("goal `{goal_ex}` is not a polynomial in `{var}`")))?;
1279 let square = match &data.square {
1280 Some(t) => {
1281 let e = ctx.from_tree(t);
1282 Some(
1283 Poly::new(&e, &[&var])
1284 .ok_or_else(|| bad(format!("square factor `{e}` is not a polynomial")))?,
1285 )
1286 }
1287 None => None,
1288 };
1289 let ray = match data.ray.as_str() {
1290 "at_least" => Ray::AtLeast,
1291 "at_most" => Ray::AtMost,
1292 other => {
1293 return Err(bad(format!(
1294 "unknown ray `{other}` (expected `at_least` or `at_most`)"
1295 )));
1296 }
1297 };
1298 let cert = HalfLineCertificate {
1299 goal,
1300 var,
1301 endpoint: ctx.from_tree(&data.endpoint).eval(),
1302 ray,
1303 polya_power: data.polya_power,
1304 coefficients: data
1305 .coefficients
1306 .iter()
1307 .map(|s| serial::q_from_str(s, OP))
1308 .collect::<Result<_, _>>()?,
1309 square,
1310 };
1311 if !cert.verify() {
1312 return Err(bad("the certificate data does not verify".into()));
1313 }
1314 Ok(cert)
1315 }
1316
1317 pub fn to_json(&self) -> Result<String, SymplexError> {
1337 serde_json::to_string(&self.to_data()).map_err(|e| SymplexError::ComputationFailed {
1338 operation: "HalfLineCertificate::to_json",
1339 reason: e.to_string(),
1340 })
1341 }
1342
1343 pub fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
1350 let data: HalfLineCertificateData =
1351 serde_json::from_str(json).map_err(|e| SymplexError::InvalidArgument {
1352 operation: "HalfLineCertificate::from_json",
1353 reason: format!("malformed JSON: {e}"),
1354 })?;
1355 Self::from_data(ctx, &data)
1356 }
1357
1358 pub fn verify(&self) -> bool {
1361 if self.coefficients.iter().any(|c| *c < Q::zero()) {
1362 return false;
1363 }
1364 let ctx = self.goal.context();
1365 let k = self.shift_expr();
1366 let mut rhs = ctx.zero();
1367 for (i, c) in self.coefficients.iter().enumerate() {
1368 rhs += ctx.from_ratio(c.clone()) * k.powi(i as i64);
1369 }
1370 if let Some(g) = &self.square {
1371 rhs = g.to_ex().powi(2) * rhs;
1372 }
1373 let lhs = (1 + &k).powi(i64::from(self.polya_power)) * self.goal.to_ex();
1374 let vars = [&self.var];
1375 match (Poly::new(&lhs, &vars), Poly::new(&rhs, &vars)) {
1376 (Some(l), Some(r)) => l.equals(&r),
1377 _ => false,
1378 }
1379 }
1380
1381 pub fn identity(&self) -> Equation {
1384 let ctx = self.goal.context();
1385 let k = self.shift_expr();
1386 let mut rhs = ctx.zero();
1387 for (i, c) in self.coefficients.iter().enumerate() {
1388 rhs += ctx.from_ratio(c.clone()) * k.powi(i as i64);
1389 }
1390 if let Some(g) = &self.square {
1391 rhs = g.to_ex().powi(2) * rhs;
1392 }
1393 let lhs = if self.polya_power == 0 {
1394 self.goal.to_ex()
1395 } else {
1396 (1 + &k).powi(i64::from(self.polya_power)) * self.goal.to_ex()
1397 };
1398 Equation::new(lhs, rhs)
1399 }
1400
1401 pub fn lean_hints(&self, hk: &str, opts: &LeanOpts) -> Result<Vec<String>, SymplexError> {
1430 let square_hint = match &self.square {
1431 Some(g) => Some(format!("sq_nonneg ({})", g.to_ex().to_lean_with(opts)?)),
1432 None => None,
1433 };
1434 let mut hints: Vec<String> = Vec::new();
1435 for (i, c) in self.coefficients.iter().enumerate() {
1436 if *c <= Q::zero() {
1437 continue;
1438 }
1439 let h = match i {
1440 0 => None,
1441 1 => Some(hk.to_string()),
1442 _ => Some(format!("pow_nonneg {hk} {i}")),
1443 };
1444 let h = match (&square_hint, h) {
1445 (Some(sq), Some(h)) => format!("mul_nonneg ({sq}) ({h})"),
1446 (Some(sq), None) => sq.clone(),
1447 (None, Some(h)) => h,
1448 (None, None) => continue,
1449 };
1450 hints.push(h);
1451 }
1452 Ok(hints)
1453 }
1454
1455 pub fn to_lean(&self, theorem_name: &str) -> Result<String, SymplexError> {
1468 self.to_lean_with(theorem_name, &LeanOpts::default())
1469 }
1470
1471 pub fn to_lean_with(
1473 &self,
1474 theorem_name: &str,
1475 opts: &LeanOpts,
1476 ) -> Result<String, SymplexError> {
1477 let real = &opts.real_type;
1478 let v = lean_ident(&self.var.to_string());
1479 let base = v.trim_matches(['«', '»']);
1480 let a = self.endpoint.to_lean_with(opts)?;
1481 let goal = self.goal.to_ex().to_lean_with(opts)?;
1482 let (hyp_name, hyp, k) = match self.ray {
1483 Ray::AtLeast => (
1484 format!("h_{base}_lo"),
1485 format!("{a} ≤ {v}"),
1486 format!("{v} - {a}"),
1487 ),
1488 Ray::AtMost => (
1489 format!("h_{base}_hi"),
1490 format!("{v} ≤ {a}"),
1491 format!("{a} - {v}"),
1492 ),
1493 };
1494 let square_hint = self.square.is_some();
1495 let hints = self.lean_hints("hk", opts)?;
1497 let tactic_name = if square_hint || self.coefficients.len() > 2 {
1498 "nlinarith"
1499 } else {
1500 "linarith"
1501 };
1502 let hint_list = if hints.is_empty() {
1503 String::new()
1504 } else {
1505 format!(" [{}]", hints.join(", "))
1506 };
1507 let mut out = format!(
1508 "theorem {} ({v} : {real}) ({hyp_name} : {hyp}) :\n 0 ≤ {goal} := by\n have hk : 0 ≤ {k} := sub_nonneg.mpr {hyp_name}\n",
1509 lean_ident(theorem_name),
1510 );
1511 if self.polya_power == 0 {
1512 out.push_str(&format!(" {tactic_name}{hint_list}\n"));
1513 } else {
1514 let n = self.polya_power;
1515 out.push_str(&format!(
1516 " have hpos : 0 < (1 + ({k})) ^ {n} := pow_pos (by linarith) {n}\n have hprod : 0 ≤ (1 + ({k})) ^ {n} * ({goal}) := by nlinarith{hint_list}\n exact nonneg_of_mul_nonneg_right hprod hpos\n"
1517 ));
1518 }
1519 Ok(wrap_lean(&out, MATHLIB_LINE_WIDTH))
1520 }
1521}
1522
1523impl fmt::Display for HalfLineCertificate {
1524 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1525 let Equation { lhs, rhs } = self.identity();
1526 write!(f, "{lhs} = {rhs}")?;
1527 match self.ray {
1528 Ray::AtLeast => write!(f, ", {} ≥ {}", self.var, self.endpoint),
1529 Ray::AtMost => write!(f, ", {} ≤ {}", self.var, self.endpoint),
1530 }
1531 }
1532}
1533
1534pub type HalfLineOutcome = Outcome<HalfLineCertificate, HalfLineUnknown>;
1538
1539#[derive(Clone, Debug, PartialEq, Eq)]
1545#[non_exhaustive]
1546pub struct HalfLineUnknown {
1547 pub max_polya_power: u32,
1549}
1550
1551impl fmt::Display for HalfLineUnknown {
1552 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1553 write!(
1554 f,
1555 "non-negative by Sturm's theorem, but no certificate up to Pólya power {}",
1556 self.max_polya_power
1557 )
1558 }
1559}
1560
1561fn shifted_coefficients(p: &Poly, var: &Ex, a: &Ex, ray: &Ray) -> Option<Vec<Q>> {
1564 let ctx = p.context();
1565 let k = ctx.symbol("__k");
1566 let x_of_k = match ray {
1567 Ray::AtLeast => a + &k,
1568 Ray::AtMost => a - &k,
1569 };
1570 let q = p.to_ex().subs(var, &x_of_k);
1571 let qp = Poly::new(&q, &[&k])?;
1572 let coeffs = qp.all_coeffs()?; let mut asc: Vec<Q> = coeffs
1574 .iter()
1575 .rev()
1576 .map(|c| c.as_rational())
1577 .collect::<Option<_>>()?;
1578 while asc.len() > 1 && asc.last().is_some_and(Zero::is_zero) {
1579 asc.pop();
1580 }
1581 Some(asc)
1582}
1583
1584fn times_one_plus_k(c: &[Q]) -> Vec<Q> {
1586 let mut out = vec![Q::zero(); c.len() + 1];
1587 for (i, ci) in c.iter().enumerate() {
1588 out[i] += ci;
1589 out[i + 1] += ci;
1590 }
1591 out
1592}
1593
1594fn halfline_counterexample(p: &Poly, var: &Ex, a: &Q, ray: &Ray) -> Option<(Q, Q)> {
1597 let ctx = p.context();
1598 let e = p.to_ex();
1599 let eval =
1600 |x: &Q| -> Option<Q> { e.subs(var, &ctx.from_ratio(x.clone())).eval().as_rational() };
1601 let one = Q::one();
1602 let inside = |x: &Q| match ray {
1603 Ray::AtLeast => x >= a,
1604 Ray::AtMost => x <= a,
1605 };
1606 let mut candidates: Vec<Q> = vec![a.clone()];
1609 let iv = e.real_roots_isolate(var);
1610 for interval in &iv {
1611 if let (Some(l), Some(h)) = (interval.lower.as_rational(), interval.upper.as_rational()) {
1612 candidates.push((&l + &h) / Q::from_integer(BigInt::from(2)));
1613 candidates.push(&l - &one);
1614 candidates.push(&h + &one);
1615 candidates.push((&l + a) / Q::from_integer(BigInt::from(2)));
1616 }
1617 }
1618 let far = match ray {
1619 Ray::AtLeast => a + Q::from_integer(BigInt::from(1000)),
1620 Ray::AtMost => a - Q::from_integer(BigInt::from(1000)),
1621 };
1622 candidates.push(far);
1623 for x in candidates {
1624 if inside(&x)
1625 && let Some(v) = eval(&x)
1626 && v < Q::zero()
1627 {
1628 return Some((x, v));
1629 }
1630 }
1631 None
1632}
1633
1634pub fn prove_nonnegative_on_halfline(
1672 goal: &Ex,
1673 var: &Ex,
1674 a: &Ex,
1675 ray: Ray,
1676 max_polya_power: u32,
1677) -> Result<HalfLineOutcome, SymplexError> {
1678 let bad = |reason: &str| SymplexError::InvalidArgument {
1679 operation: "prove_nonnegative_on_halfline",
1680 reason: reason.into(),
1681 };
1682 let a_ex = a.eval();
1683 let a_q = a_ex
1684 .as_rational()
1685 .ok_or_else(|| bad("the endpoint must be a rational literal"))?;
1686 let goal_poly =
1687 Poly::new(goal, &[var]).ok_or_else(|| bad("goal must be a polynomial in the variable"))?;
1688 if !goal_poly.has_rational_coeffs() {
1689 return Err(bad("goal must have rational coefficients"));
1690 }
1691 let ctx: Context = goal.context();
1692
1693 let (lo, hi) = match ray {
1696 Ray::AtLeast => (a_ex.clone(), ctx.infinity()),
1697 Ray::AtMost => (ctx.neg_infinity(), a_ex.clone()),
1698 };
1699 if goal_poly.is_nonnegative_on(&lo, &hi) == Some(false)
1700 && let Some((point, value)) = halfline_counterexample(&goal_poly, var, &a_q, &ray)
1701 {
1702 return Ok(Outcome::Refuted {
1703 point: vec![(var.clone(), point)],
1704 value,
1705 param_value: None,
1706 });
1707 }
1708
1709 let try_certify = |p: &Poly, square: Option<&Poly>| -> Option<HalfLineCertificate> {
1710 let mut coeffs = shifted_coefficients(p, var, &a_ex, &ray)?;
1711 for n in 0..=max_polya_power {
1712 if coeffs.iter().all(|c| *c >= Q::zero()) {
1713 let cert = HalfLineCertificate {
1714 goal: goal_poly.clone(),
1715 var: var.clone(),
1716 endpoint: a_ex.clone(),
1717 ray: ray.clone(),
1718 polya_power: n,
1719 coefficients: coeffs,
1720 square: square.cloned(),
1721 };
1722 return cert.verify().then_some(cert);
1723 }
1724 coeffs = times_one_plus_k(&coeffs);
1725 }
1726 None
1727 };
1728
1729 if let Some(c) = try_certify(&goal_poly, None) {
1730 return Ok(HalfLineOutcome::Proved(c));
1731 }
1732 if let Some((g, h)) = split_square_factor(&goal_poly, &[var])
1733 && let Some(c) = try_certify(&h, Some(&g))
1734 {
1735 return Ok(HalfLineOutcome::Proved(c));
1736 }
1737 if let Some((point, value)) = halfline_counterexample(&goal_poly, var, &a_q, &ray) {
1738 return Ok(Outcome::Refuted {
1739 point: vec![(var.clone(), point)],
1740 value,
1741 param_value: None,
1742 });
1743 }
1744 Ok(Outcome::Unknown(HalfLineUnknown { max_polya_power }))
1745}
1746
1747#[derive(Clone, Debug)]
1750pub struct RealLineCertificate {
1751 pub upper: HalfLineCertificate,
1753 pub lower: HalfLineCertificate,
1755}
1756
1757#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
1759pub struct RealLineCertificateData {
1760 pub upper: HalfLineCertificateData,
1762 pub lower: HalfLineCertificateData,
1764}
1765
1766impl RealLineCertificate {
1767 pub fn goal(&self) -> &Poly {
1769 &self.upper.goal
1770 }
1771
1772 pub fn verify(&self) -> bool {
1774 self.upper.verify() && self.lower.verify() && self.upper.endpoint == self.lower.endpoint
1775 }
1776
1777 pub fn to_data(&self) -> RealLineCertificateData {
1779 RealLineCertificateData {
1780 upper: self.upper.to_data(),
1781 lower: self.lower.to_data(),
1782 }
1783 }
1784
1785 pub fn from_data(ctx: &Context, data: &RealLineCertificateData) -> Result<Self, SymplexError> {
1792 let cert = RealLineCertificate {
1793 upper: HalfLineCertificate::from_data(ctx, &data.upper)?,
1794 lower: HalfLineCertificate::from_data(ctx, &data.lower)?,
1795 };
1796 if !cert.verify() {
1797 return Err(SymplexError::InvalidArgument {
1798 operation: "RealLineCertificate::from_data",
1799 reason: "the two halves do not verify as one certificate".into(),
1800 });
1801 }
1802 Ok(cert)
1803 }
1804
1805 pub fn to_json(&self) -> Result<String, SymplexError> {
1811 serde_json::to_string_pretty(&self.to_data()).map_err(|e| SymplexError::ComputationFailed {
1812 operation: "RealLineCertificate::to_json",
1813 reason: e.to_string(),
1814 })
1815 }
1816
1817 pub fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
1823 let data: RealLineCertificateData =
1824 serde_json::from_str(json).map_err(|e| SymplexError::InvalidArgument {
1825 operation: "RealLineCertificate::from_json",
1826 reason: e.to_string(),
1827 })?;
1828 Self::from_data(ctx, &data)
1829 }
1830
1831 pub fn to_lean(&self, theorem_name: &str) -> Result<String, SymplexError> {
1833 self.to_lean_with(theorem_name, &LeanOpts::default())
1834 }
1835
1836 pub fn to_lean_with(
1838 &self,
1839 theorem_name: &str,
1840 opts: &LeanOpts,
1841 ) -> Result<String, SymplexError> {
1842 let up = self.upper.to_lean_with("_", opts)?;
1843 let lo = self.lower.to_lean_with("_", opts)?;
1844 let body = |text: &str| -> String {
1846 text.split_once(":= by\n")
1847 .map(|(_, b)| b.to_string())
1848 .unwrap_or_default()
1849 };
1850 let v = lean_ident(&self.upper.var.to_string());
1851 let base = v.trim_matches(['«', '»']).to_string();
1852 let a = self.upper.endpoint.to_lean_with(opts)?;
1853 let goal = self.upper.goal.to_ex().to_lean_with(opts)?;
1854 let indent = |b: String| -> String {
1855 b.lines()
1856 .map(|l| format!(" {l}"))
1857 .collect::<Vec<_>>()
1858 .join("\n")
1859 };
1860 let text = format!(
1861 "theorem {} ({v} : {}) : 0 ≤ {goal} := by\n rcases le_total {a} {v} with h_{base}_lo | h_{base}_hi\n · -- {a} ≤ {v}\n{}\n · -- {v} ≤ {a}\n{}\n",
1862 lean_ident(theorem_name),
1863 opts.real_type,
1864 indent(body(&up)),
1865 indent(body(&lo)),
1866 );
1867 Ok(wrap_lean(&text, MATHLIB_LINE_WIDTH))
1868 }
1869}
1870
1871impl fmt::Display for RealLineCertificate {
1872 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1873 write!(f, "{}; {}", self.upper, self.lower)
1874 }
1875}
1876
1877impl Certificate for BoxCertificate {
1880 fn goal(&self) -> &Poly {
1881 BoxCertificate::goal(self)
1882 }
1883 fn verify(&self) -> bool {
1884 BoxCertificate::verify(self)
1885 }
1886 fn to_lean_with(&self, theorem_name: &str, opts: &LeanOpts) -> Result<String, SymplexError> {
1887 BoxCertificate::to_lean_with(self, theorem_name, opts)
1888 }
1889 fn to_json(&self) -> Result<String, SymplexError> {
1890 BoxCertificate::to_json(self)
1891 }
1892 fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
1893 BoxCertificate::from_json(ctx, json)
1894 }
1895}
1896
1897impl Certificate for HalfLineCertificate {
1898 fn goal(&self) -> &Poly {
1899 HalfLineCertificate::goal(self)
1900 }
1901 fn verify(&self) -> bool {
1902 HalfLineCertificate::verify(self)
1903 }
1904 fn to_lean_with(&self, theorem_name: &str, opts: &LeanOpts) -> Result<String, SymplexError> {
1905 HalfLineCertificate::to_lean_with(self, theorem_name, opts)
1906 }
1907 fn to_json(&self) -> Result<String, SymplexError> {
1908 HalfLineCertificate::to_json(self)
1909 }
1910 fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
1911 HalfLineCertificate::from_json(ctx, json)
1912 }
1913}
1914
1915impl Certificate for RealLineCertificate {
1916 fn goal(&self) -> &Poly {
1917 RealLineCertificate::goal(self)
1918 }
1919 fn verify(&self) -> bool {
1920 RealLineCertificate::verify(self)
1921 }
1922 fn to_lean_with(&self, theorem_name: &str, opts: &LeanOpts) -> Result<String, SymplexError> {
1923 RealLineCertificate::to_lean_with(self, theorem_name, opts)
1924 }
1925 fn to_json(&self) -> Result<String, SymplexError> {
1926 RealLineCertificate::to_json(self)
1927 }
1928 fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
1929 RealLineCertificate::from_json(ctx, json)
1930 }
1931}
1932
1933pub fn prove_nonnegative_on_reals(
1951 goal: &Ex,
1952 var: &Ex,
1953 split: &Ex,
1954 max_polya_power: u32,
1955) -> Result<Option<RealLineCertificate>, SymplexError> {
1956 let upper = prove_nonnegative_on_halfline(goal, var, split, Ray::AtLeast, max_polya_power)?;
1957 let lower = prove_nonnegative_on_halfline(goal, var, split, Ray::AtMost, max_polya_power)?;
1958 match (upper, lower) {
1959 (HalfLineOutcome::Proved(upper), HalfLineOutcome::Proved(lower)) => {
1960 Ok(Some(RealLineCertificate { upper, lower }))
1961 }
1962 _ => Ok(None),
1963 }
1964}