use std::fmt;
use num_bigint::BigInt;
use num_traits::{One, Zero};
use crate::api::context::Context;
use crate::api::eq::Equation;
use crate::api::expr::Ex;
use crate::api::poly_ex::Poly;
use crate::base::errors::SymplexError;
use crate::base::interval::Interval;
use crate::domains::linprog::{Feasibility, LpProblem, LpStatus, nonneg_combination};
use crate::output::lean::{LeanOpts, MATHLIB_LINE_WIDTH, lean_ident, wrap_lean};
mod outcome;
mod polyhedron;
mod sos;
pub use crate::domains::linprog::BudgetHit;
pub use outcome::{Certificate, Outcome};
pub use polyhedron::{
ParamBound, ParamBoundTree, PolyhedronCertificate, PolyhedronCertificateData,
PolyhedronLeanNames, PolyhedronLeanSteps, PolyhedronOpts, PolyhedronOutcome, PolyhedronProver,
PolyhedronTerm, PolyhedronTermData, PolyhedronUnknown, prove_nonnegative_on_polyhedron,
prove_polyhedron_empty,
};
pub use sos::{
SosCertificate, SosCertificateData, SosOpts, SosOutcome, SosUnknown, is_sos, prove_sos,
};
pub(crate) mod serial {
use super::{BigInt, Q, SymplexError};
pub(crate) fn q_to_str(q: &Q) -> String {
format!("{}/{}", q.numer(), q.denom())
}
pub(crate) fn q_from_str(s: &str, operation: &'static str) -> Result<Q, SymplexError> {
let bad = || SymplexError::InvalidArgument {
operation,
reason: format!("malformed rational `{s}` (expected `p/q`)"),
};
let (n, d) = s.split_once('/').unwrap_or((s, "1"));
let n: BigInt = n.trim().parse().map_err(|_| bad())?;
let d: BigInt = d.trim().parse().map_err(|_| bad())?;
if d == BigInt::from(0) {
return Err(bad());
}
Ok(Q::new(n, d))
}
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BoxBoundTree {
pub var: crate::output::tree::ExprTree,
pub lo: crate::output::tree::ExprTree,
pub hi: crate::output::tree::ExprTree,
}
#[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
pub struct HandelmanTermData {
pub lower_powers: Vec<u32>,
pub upper_powers: Vec<u32>,
pub weight: String,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct BoxCertificateData {
pub goal: crate::output::tree::ExprTree,
pub bounds: Vec<BoxBoundTree>,
pub terms: Vec<HandelmanTermData>,
pub square: Option<crate::output::tree::ExprTree>,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct HalfLineCertificateData {
pub goal: crate::output::tree::ExprTree,
pub var: crate::output::tree::ExprTree,
pub endpoint: crate::output::tree::ExprTree,
pub ray: String,
pub polya_power: u32,
pub coefficients: Vec<String>,
pub square: Option<crate::output::tree::ExprTree>,
}
use crate::base::numeric::Q;
fn invalid(reason: impl Into<String>) -> SymplexError {
SymplexError::invalid_argument("prove_nonnegative_on_box", reason)
}
#[derive(Clone, Debug)]
pub struct BoxBound {
pub var: Ex,
pub lo: Ex,
pub hi: Ex,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct HandelmanTerm {
pub lower_powers: Vec<u32>,
pub upper_powers: Vec<u32>,
pub weight: Q,
}
impl HandelmanTerm {
pub fn degree(&self) -> u32 {
self.lower_powers.iter().sum::<u32>() + self.upper_powers.iter().sum::<u32>()
}
}
#[derive(Clone, Debug)]
pub struct BoxCertificate {
goal: Poly,
bounds: Vec<BoxBound>,
terms: Vec<HandelmanTerm>,
square: Option<Poly>,
}
impl BoxCertificate {
pub fn goal(&self) -> &Poly {
&self.goal
}
pub fn bounds(&self) -> &[BoxBound] {
&self.bounds
}
pub fn terms(&self) -> &[HandelmanTerm] {
&self.terms
}
pub fn square(&self) -> Option<&Poly> {
self.square.as_ref()
}
pub fn degree(&self) -> u32 {
self.terms
.iter()
.map(HandelmanTerm::degree)
.max()
.unwrap_or(0)
}
pub fn product(&self, term: &HandelmanTerm) -> Poly {
let ctx = self.goal.context();
let gens: Vec<&Ex> = self.goal.gens().iter().collect();
let mut acc = Poly::one(&ctx, &gens).unwrap_or_else(|_| self.goal.clone());
for (i, b) in self.bounds.iter().enumerate() {
let lower = &b.var - &b.lo;
let upper = &b.hi - &b.var;
for _ in 0..term.lower_powers.get(i).copied().unwrap_or(0) {
if let Some(p) = Poly::new(&lower, &gens)
&& let Ok(m) = acc.mul(&p)
{
acc = m;
}
}
for _ in 0..term.upper_powers.get(i).copied().unwrap_or(0) {
if let Some(p) = Poly::new(&upper, &gens)
&& let Ok(m) = acc.mul(&p)
{
acc = m;
}
}
}
acc
}
pub fn verify(&self) -> bool {
let ctx = self.goal.context();
let gens: Vec<&Ex> = self.goal.gens().iter().collect();
let Ok(mut acc) = Poly::zero(&ctx, &gens) else {
return false;
};
for t in &self.terms {
if t.weight <= Q::zero() {
return false;
}
let w = ctx.from_ratio(t.weight.clone());
let Ok(scaled) = self.product(t).scale(&w) else {
return false;
};
let Ok(sum) = acc.add(&scaled) else {
return false;
};
acc = sum;
}
if let Some(g) = &self.square {
let Ok(g2) = g.mul(g) else {
return false;
};
let Ok(prod) = acc.mul(&g2) else {
return false;
};
acc = prod;
}
acc.equals(&self.goal)
}
pub fn product_expr(&self, term: &HandelmanTerm) -> Ex {
let ctx = self.goal.context();
let mut acc = ctx.one();
for (i, b) in self.bounds.iter().enumerate() {
let a = i64::from(term.lower_powers.get(i).copied().unwrap_or(0));
let e = i64::from(term.upper_powers.get(i).copied().unwrap_or(0));
if a > 0 {
acc *= (&b.var - &b.lo).powi(a);
}
if e > 0 {
acc *= (&b.hi - &b.var).powi(e);
}
}
acc
}
pub fn identity(&self) -> Equation {
let ctx = self.goal.context();
let mut rhs = ctx.zero();
for t in &self.terms {
rhs += ctx.from_ratio(t.weight.clone()) * self.product_expr(t);
}
if let Some(g) = &self.square {
rhs = g.to_ex().powi(2) * rhs;
}
Equation::new(self.goal.to_ex(), rhs)
}
pub fn to_lean(&self, theorem_name: &str) -> Result<String, SymplexError> {
self.to_lean_with(theorem_name, &LeanOpts::default())
}
pub fn to_lean_with(
&self,
theorem_name: &str,
opts: &LeanOpts,
) -> Result<String, SymplexError> {
let real = &opts.real_type;
let vars: Vec<String> = self
.bounds
.iter()
.map(|b| lean_ident(&b.var.to_string()))
.collect();
let n = self.bounds.len();
let mut uses_lo = vec![false; n];
let mut uses_hi = vec![false; n];
for t in &self.terms {
for i in 0..n {
uses_lo[i] |= t.lower_powers.get(i).copied().unwrap_or(0) > 0;
uses_hi[i] |= t.upper_powers.get(i).copied().unwrap_or(0) > 0;
}
}
let mut hyps: Vec<String> = Vec::new();
let mut lo_names: Vec<String> = Vec::new();
let mut hi_names: Vec<String> = Vec::new();
for (i, (b, v)) in self.bounds.iter().zip(&vars).enumerate() {
let lo = b.lo.to_lean_with(opts)?;
let hi = b.hi.to_lean_with(opts)?;
let base = v.trim_matches(['«', '»']);
let lo_name = format!("{}h_{base}_lo", if uses_lo[i] { "" } else { "_" });
let hi_name = format!("{}h_{base}_hi", if uses_hi[i] { "" } else { "_" });
hyps.push(format!("({lo_name} : {lo} ≤ {v})"));
hyps.push(format!("({hi_name} : {v} ≤ {hi})"));
lo_names.push(lo_name);
hi_names.push(hi_name);
}
let goal = self.goal.to_ex().to_lean_with(opts)?;
let square_hint = match &self.square {
Some(g) => Some(format!("sq_nonneg ({})", g.to_ex().to_lean_with(opts)?)),
None => None,
};
let mut hints: Vec<String> = Vec::new();
let mut max_factors = 0usize;
if let Some(sq) = &square_hint
&& self.terms.iter().any(|t| t.degree() == 0)
{
hints.push(sq.clone());
max_factors = max_factors.max(2);
}
for t in &self.terms {
let mut factors: Vec<String> = Vec::new();
for i in 0..n {
for _ in 0..t.lower_powers.get(i).copied().unwrap_or(0) {
factors.push(format!("sub_nonneg.mpr {}", lo_names[i]));
}
for _ in 0..t.upper_powers.get(i).copied().unwrap_or(0) {
factors.push(format!("sub_nonneg.mpr {}", hi_names[i]));
}
}
let Some((first, rest)) = factors.split_first() else {
continue; };
let mut acc = first.clone();
for f in rest {
acc = format!("mul_nonneg ({acc}) ({f})");
}
if let Some(sq) = &square_hint {
acc = format!("mul_nonneg ({sq}) ({acc})");
max_factors = max_factors.max(factors.len() + 2);
} else {
max_factors = max_factors.max(factors.len());
}
if !hints.contains(&acc) {
hints.push(acc);
}
}
let sig = format!(
"theorem {} ({} : {real}) {} :\n 0 ≤ {goal} := by\n",
lean_ident(theorem_name),
vars.join(" "),
hyps.join(" ")
);
let tactic = if hints.is_empty() {
" linarith".to_string()
} else if max_factors <= 1 {
format!(" linarith [{}]", hints.join(", "))
} else {
format!(" nlinarith [{}]", hints.join(", "))
};
Ok(wrap_lean(&format!("{sig}{tactic}\n"), MATHLIB_LINE_WIDTH))
}
}
impl BoxCertificate {
pub fn to_data(&self) -> BoxCertificateData {
BoxCertificateData {
goal: self.goal.to_ex().to_tree(),
bounds: self
.bounds
.iter()
.map(|b| BoxBoundTree {
var: b.var.to_tree(),
lo: b.lo.to_tree(),
hi: b.hi.to_tree(),
})
.collect(),
terms: self
.terms
.iter()
.map(|t| HandelmanTermData {
lower_powers: t.lower_powers.clone(),
upper_powers: t.upper_powers.clone(),
weight: serial::q_to_str(&t.weight),
})
.collect(),
square: self.square.as_ref().map(|g| g.to_ex().to_tree()),
}
}
pub fn from_data(ctx: &Context, data: &BoxCertificateData) -> Result<Self, SymplexError> {
const OP: &str = "BoxCertificate::from_data";
let bad = |reason: String| SymplexError::InvalidArgument {
operation: OP,
reason,
};
let bounds: Vec<BoxBound> = data
.bounds
.iter()
.map(|b| BoxBound {
var: ctx.from_tree(&b.var),
lo: ctx.from_tree(&b.lo),
hi: ctx.from_tree(&b.hi),
})
.collect();
if bounds.is_empty() {
return Err(bad(
"a certificate needs at least one bounded variable".into()
));
}
let gens: Vec<&Ex> = bounds.iter().map(|b| &b.var).collect();
let goal_ex = ctx.from_tree(&data.goal);
let goal = Poly::new(&goal_ex, &gens).ok_or_else(|| {
bad(format!(
"goal `{goal_ex}` is not a polynomial in the box variables"
))
})?;
let square = match &data.square {
Some(t) => {
let e = ctx.from_tree(t);
Some(
Poly::new(&e, &gens)
.ok_or_else(|| bad(format!("square factor `{e}` is not a polynomial")))?,
)
}
None => None,
};
let mut terms = Vec::with_capacity(data.terms.len());
for t in &data.terms {
if t.lower_powers.len() != bounds.len() || t.upper_powers.len() != bounds.len() {
return Err(bad(
"a term's power vectors must have one entry per variable".into(),
));
}
terms.push(HandelmanTerm {
lower_powers: t.lower_powers.clone(),
upper_powers: t.upper_powers.clone(),
weight: serial::q_from_str(&t.weight, OP)?,
});
}
let cert = BoxCertificate {
goal,
bounds,
terms,
square,
};
if !cert.verify() {
return Err(bad("the certificate data does not verify".into()));
}
Ok(cert)
}
pub fn to_json(&self) -> Result<String, SymplexError> {
serde_json::to_string(&self.to_data()).map_err(|e| SymplexError::ComputationFailed {
operation: "BoxCertificate::to_json",
reason: e.to_string(),
})
}
pub fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
let data: BoxCertificateData =
serde_json::from_str(json).map_err(|e| SymplexError::InvalidArgument {
operation: "BoxCertificate::from_json",
reason: format!("malformed JSON: {e}"),
})?;
Self::from_data(ctx, &data)
}
}
impl fmt::Display for BoxCertificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Equation { lhs, rhs } = self.identity();
write!(f, "{lhs} = {rhs}")?;
for b in &self.bounds {
write!(f, ", {} ≤ {} ≤ {}", b.lo, b.var, b.hi)?;
}
Ok(())
}
}
pub type BoxOutcome = Outcome<BoxCertificate, BoxUnknown>;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct BoxUnknown {
pub farkas: Option<Vec<Q>>,
pub degree: u32,
}
impl fmt::Display for BoxUnknown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "no Handelman certificate of degree ≤ {}", self.degree)?;
if self.farkas.is_some() {
write!(f, " (Farkas vector available)")?;
}
Ok(())
}
}
fn refutation_point(vars: &[&Ex], values: Vec<Q>) -> Vec<(Ex, Q)> {
vars.iter().map(|v| (*v).clone()).zip(values).collect()
}
fn products_up_to(n: usize, degree: u32) -> Vec<(Vec<u32>, Vec<u32>)> {
let slots = 2 * n;
let mut out = Vec::new();
let mut current = vec![0u32; slots];
fn rec(
slot: usize,
remaining: u32,
current: &mut Vec<u32>,
n: usize,
out: &mut Vec<(Vec<u32>, Vec<u32>)>,
) {
if slot == current.len() {
out.push((current[..n].to_vec(), current[n..].to_vec()));
return;
}
for e in 0..=remaining {
current[slot] = e;
rec(slot + 1, remaining - e, current, n, out);
}
current[slot] = 0;
}
rec(0, degree, &mut current, n, &mut out);
out
}
fn value_at(goal: &Poly, point: &[Q]) -> Option<Q> {
let ctx = goal.context();
let vals: Vec<Ex> = point.iter().map(|q| ctx.from_ratio(q.clone())).collect();
let refs: Vec<&Ex> = vals.iter().collect();
goal.eval(&refs).ok()?.as_rational()
}
fn find_counterexample(goal: &Poly, bounds: &[Interval<Q>], steps: u32) -> Option<(Vec<Q>, Q)> {
let n = bounds.len();
let total = (u64::from(steps) + 1).checked_pow(n as u32)?;
if total > 200_000 {
return None;
}
let mut idx = vec![0u32; n];
loop {
let point: Vec<Q> = idx
.iter()
.zip(bounds)
.map(|(&k, iv)| {
&iv.lower + (&iv.upper - &iv.lower) * Q::new(BigInt::from(k), BigInt::from(steps))
})
.collect();
if let Some(v) = value_at(goal, &point)
&& v < Q::zero()
{
return Some((point, v));
}
let mut pos = 0;
loop {
if pos == n {
return None;
}
if idx[pos] < steps {
idx[pos] += 1;
break;
}
idx[pos] = 0;
pos += 1;
}
}
}
fn sparse_nonneg_combination(
columns: &[Vec<Q>],
target: &[Q],
exponents: &[(Vec<u32>, Vec<u32>)],
) -> Result<Feasibility, SymplexError> {
let m = target.len();
let cost: Vec<Q> = exponents
.iter()
.map(|(a, b)| {
let deg: u32 = a.iter().sum::<u32>() + b.iter().sum::<u32>();
Q::from_integer(BigInt::from(1 + u64::from(deg)))
})
.collect();
let mut lp = LpProblem::minimize(cost);
for i in 0..m {
let row: Vec<Q> = columns.iter().map(|c| c[i].clone()).collect();
lp = lp.eq(row, target[i].clone());
}
let sol = lp.solve()?;
match sol.status {
LpStatus::Optimal => Ok(Feasibility::Feasible(sol.x)),
LpStatus::Infeasible => Ok(Feasibility::Infeasible { farkas: sol.farkas }),
LpStatus::Unbounded | LpStatus::BudgetExhausted => nonneg_combination(columns, target),
}
}
pub fn prove_nonnegative_on_box(
goal: &Ex,
bounds: &[BoxBound],
degree: u32,
) -> Result<BoxOutcome, SymplexError> {
if bounds.is_empty() {
return Err(invalid("at least one bounded variable is required"));
}
let mut vars: Vec<&Ex> = Vec::with_capacity(bounds.len());
let mut q_bounds: Vec<Interval<Q>> = Vec::with_capacity(bounds.len());
let mut box_bounds: Vec<BoxBound> = Vec::with_capacity(bounds.len());
for BoxBound { var, lo, hi } in bounds {
if vars.contains(&var) {
return Err(invalid(format!("variable `{var}` is bounded twice")));
}
let (Some(l), Some(h)) = (lo.eval().as_rational(), hi.eval().as_rational()) else {
return Err(invalid(format!(
"bounds of `{var}` must be rational literals, got [{lo}, {hi}]"
)));
};
if l >= h {
return Err(invalid(format!(
"bounds of `{var}` must satisfy lo < hi, got [{lo}, {hi}]"
)));
}
vars.push(var);
q_bounds.push(Interval::closed(l, h));
box_bounds.push(BoxBound {
var: var.clone(),
lo: lo.eval(),
hi: hi.eval(),
});
}
let goal_poly = Poly::new(goal, &vars).ok_or_else(|| {
invalid("goal must be a polynomial in the box variables (other symbols or non-polynomial operations found)")
})?;
if !goal_poly.has_rational_coeffs() {
return Err(invalid(
"goal must have rational coefficients (parameters are not supported)",
));
}
if let Some((point, value)) = find_counterexample(&goal_poly, &q_bounds, 8) {
return Ok(Outcome::Refuted {
point: refutation_point(&vars, point),
value,
param_value: None,
});
}
match handelman_search(&goal_poly, &vars, &box_bounds, degree, None)? {
Ok(cert) => Ok(BoxOutcome::Proved(cert)),
Err(farkas) => {
if let Some((g, h)) = split_square_factor(&goal_poly, &vars)
&& let Ok(cert) = handelman_search(&h, &vars, &box_bounds, degree, Some(&g))?
{
let cert = BoxCertificate {
goal: goal_poly.clone(),
..cert
};
if cert.verify() {
return Ok(BoxOutcome::Proved(cert));
}
}
if let Some((point, value)) = find_counterexample(&goal_poly, &q_bounds, 32) {
return Ok(Outcome::Refuted {
point: refutation_point(&vars, point),
value,
param_value: None,
});
}
Ok(Outcome::Unknown(BoxUnknown { farkas, degree }))
}
}
}
fn split_square_factor(goal: &Poly, vars: &[&Ex]) -> Option<(Poly, Poly)> {
let e = goal.to_ex();
let (content, factors) = if vars.len() == 1 {
e.factor_list(vars[0])
} else {
e.factor_list_all()
};
if factors.iter().all(|(_, m)| *m < 2) {
return None;
}
let ctx = goal.context();
let mut g = ctx.one();
let mut h = content;
for (f, m) in &factors {
if *m >= 2 {
g *= f.powi(i64::from(*m / 2));
}
if *m % 2 == 1 {
h *= f;
}
}
let g = Poly::new(&g, vars)?;
let h = Poly::new(&h, vars)?;
let back = g.mul(&g).ok()?.mul(&h).ok()?;
if !back.equals(goal) {
return None;
}
Some((g, h))
}
fn handelman_search(
goal_poly: &Poly,
vars: &[&Ex],
box_bounds: &[BoxBound],
degree: u32,
square: Option<&Poly>,
) -> Result<Result<BoxCertificate, Option<Vec<Q>>>, SymplexError> {
let ctx = goal_poly.context();
let n = vars.len();
let lower: Vec<Poly> = box_bounds
.iter()
.map(|b| {
Poly::new(&(&b.var - &b.lo), vars).ok_or_else(|| invalid("internal: bound factor"))
})
.collect::<Result<_, _>>()?;
let upper: Vec<Poly> = box_bounds
.iter()
.map(|b| {
Poly::new(&(&b.hi - &b.var), vars).ok_or_else(|| invalid("internal: bound factor"))
})
.collect::<Result<_, _>>()?;
let exponents = products_up_to(n, degree);
let one = Poly::one(&ctx, vars)?;
let mut products: Vec<Poly> = Vec::with_capacity(exponents.len());
for (a, b) in &exponents {
let mut acc = one.clone();
for i in 0..n {
for _ in 0..a[i] {
acc = acc.mul(&lower[i])?;
}
for _ in 0..b[i] {
acc = acc.mul(&upper[i])?;
}
}
products.push(acc);
}
let mut all: Vec<&Poly> = products.iter().collect();
all.push(goal_poly);
let monos = Poly::monomial_basis(&all)?;
let coeff_vec = |p: &Poly| -> Result<Vec<Q>, SymplexError> {
monos
.iter()
.map(|m| {
p.coeff_monomial(m)?
.as_rational()
.ok_or_else(|| invalid("internal: non-rational coefficient"))
})
.collect()
};
let columns: Vec<Vec<Q>> = products.iter().map(coeff_vec).collect::<Result<_, _>>()?;
let target = coeff_vec(goal_poly)?;
match sparse_nonneg_combination(&columns, &target, &exponents)? {
Feasibility::Feasible(lambda) => {
let terms: Vec<HandelmanTerm> = exponents
.iter()
.zip(&lambda)
.filter(|(_, w)| **w > Q::zero())
.map(|((a, b), w)| HandelmanTerm {
lower_powers: a.clone(),
upper_powers: b.clone(),
weight: w.clone(),
})
.collect();
let certified = match square {
Some(g) => g.mul(g)?.mul(goal_poly)?,
None => goal_poly.clone(),
};
let cert = BoxCertificate {
goal: certified,
bounds: box_bounds.to_vec(),
terms,
square: square.cloned(),
};
if !cert.verify() {
return Err(SymplexError::ComputationFailed {
operation: "prove_nonnegative_on_box",
reason:
"the LP solution did not reproduce the goal under exact re-verification"
.into(),
});
}
Ok(Ok(cert))
}
Feasibility::Infeasible { farkas } => Ok(Err(farkas)),
}
}
pub fn is_nonnegative_on_box(goal: &Ex, bounds: &[BoxBound], max_degree: u32) -> Option<bool> {
for d in 1..=max_degree.max(1) {
match prove_nonnegative_on_box(goal, bounds, d) {
Ok(Outcome::Proved(_)) => return Some(true),
Ok(Outcome::Refuted { .. }) => return Some(false),
Ok(Outcome::Unknown(_)) => continue,
Err(_) => return None,
}
}
None
}
impl Poly {
pub fn express_as_nonneg_combination(
&self,
basis: &[&Poly],
) -> Result<Feasibility, SymplexError> {
const OP: &str = "Poly::express_as_nonneg_combination";
let bad = |reason: &str| SymplexError::InvalidArgument {
operation: OP,
reason: reason.into(),
};
if basis.is_empty() {
return Err(bad("basis must not be empty"));
}
let mut all: Vec<&Poly> = basis.to_vec();
all.push(self);
let monos = Poly::monomial_basis(&all).map_err(|_| bad("generators differ"))?;
let coeff_vec = |p: &Poly| -> Result<Vec<Q>, SymplexError> {
monos
.iter()
.map(|m| {
p.coeff_monomial(m)?
.as_rational()
.ok_or_else(|| bad("coefficients must be rational"))
})
.collect()
};
let columns: Vec<Vec<Q>> = basis
.iter()
.map(|p| coeff_vec(p))
.collect::<Result<_, _>>()?;
let target = coeff_vec(self)?;
nonneg_combination(&columns, &target)
}
}
impl Ex {
pub fn prove_nonnegative_on_box(
&self,
bounds: &[BoxBound],
degree: u32,
) -> Result<BoxOutcome, SymplexError> {
prove_nonnegative_on_box(self, bounds, degree)
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Ray {
AtLeast,
AtMost,
}
#[derive(Clone, Debug)]
pub struct HalfLineCertificate {
goal: Poly,
var: Ex,
endpoint: Ex,
ray: Ray,
polya_power: u32,
coefficients: Vec<Q>,
square: Option<Poly>,
}
impl HalfLineCertificate {
pub fn goal(&self) -> &Poly {
&self.goal
}
pub fn var(&self) -> &Ex {
&self.var
}
pub fn endpoint(&self) -> &Ex {
&self.endpoint
}
pub fn ray(&self) -> &Ray {
&self.ray
}
pub fn polya_power(&self) -> u32 {
self.polya_power
}
pub fn coefficients(&self) -> &[Q] {
&self.coefficients
}
pub fn square(&self) -> Option<&Poly> {
self.square.as_ref()
}
pub fn shift_expr(&self) -> Ex {
match self.ray {
Ray::AtLeast => &self.var - &self.endpoint,
Ray::AtMost => &self.endpoint - &self.var,
}
}
pub fn to_data(&self) -> HalfLineCertificateData {
HalfLineCertificateData {
goal: self.goal.to_ex().to_tree(),
var: self.var.to_tree(),
endpoint: self.endpoint.to_tree(),
ray: match self.ray {
Ray::AtLeast => "at_least".to_string(),
Ray::AtMost => "at_most".to_string(),
},
polya_power: self.polya_power,
coefficients: self.coefficients.iter().map(serial::q_to_str).collect(),
square: self.square.as_ref().map(|g| g.to_ex().to_tree()),
}
}
pub fn from_data(ctx: &Context, data: &HalfLineCertificateData) -> Result<Self, SymplexError> {
const OP: &str = "HalfLineCertificate::from_data";
let bad = |reason: String| SymplexError::InvalidArgument {
operation: OP,
reason,
};
let var = ctx.from_tree(&data.var);
let goal_ex = ctx.from_tree(&data.goal);
let goal = Poly::new(&goal_ex, &[&var])
.ok_or_else(|| bad(format!("goal `{goal_ex}` is not a polynomial in `{var}`")))?;
let square = match &data.square {
Some(t) => {
let e = ctx.from_tree(t);
Some(
Poly::new(&e, &[&var])
.ok_or_else(|| bad(format!("square factor `{e}` is not a polynomial")))?,
)
}
None => None,
};
let ray = match data.ray.as_str() {
"at_least" => Ray::AtLeast,
"at_most" => Ray::AtMost,
other => {
return Err(bad(format!(
"unknown ray `{other}` (expected `at_least` or `at_most`)"
)));
}
};
let cert = HalfLineCertificate {
goal,
var,
endpoint: ctx.from_tree(&data.endpoint).eval(),
ray,
polya_power: data.polya_power,
coefficients: data
.coefficients
.iter()
.map(|s| serial::q_from_str(s, OP))
.collect::<Result<_, _>>()?,
square,
};
if !cert.verify() {
return Err(bad("the certificate data does not verify".into()));
}
Ok(cert)
}
pub fn to_json(&self) -> Result<String, SymplexError> {
serde_json::to_string(&self.to_data()).map_err(|e| SymplexError::ComputationFailed {
operation: "HalfLineCertificate::to_json",
reason: e.to_string(),
})
}
pub fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
let data: HalfLineCertificateData =
serde_json::from_str(json).map_err(|e| SymplexError::InvalidArgument {
operation: "HalfLineCertificate::from_json",
reason: format!("malformed JSON: {e}"),
})?;
Self::from_data(ctx, &data)
}
pub fn verify(&self) -> bool {
if self.coefficients.iter().any(|c| *c < Q::zero()) {
return false;
}
let ctx = self.goal.context();
let k = self.shift_expr();
let mut rhs = ctx.zero();
for (i, c) in self.coefficients.iter().enumerate() {
rhs += ctx.from_ratio(c.clone()) * k.powi(i as i64);
}
if let Some(g) = &self.square {
rhs = g.to_ex().powi(2) * rhs;
}
let lhs = (1 + &k).powi(i64::from(self.polya_power)) * self.goal.to_ex();
let vars = [&self.var];
match (Poly::new(&lhs, &vars), Poly::new(&rhs, &vars)) {
(Some(l), Some(r)) => l.equals(&r),
_ => false,
}
}
pub fn identity(&self) -> Equation {
let ctx = self.goal.context();
let k = self.shift_expr();
let mut rhs = ctx.zero();
for (i, c) in self.coefficients.iter().enumerate() {
rhs += ctx.from_ratio(c.clone()) * k.powi(i as i64);
}
if let Some(g) = &self.square {
rhs = g.to_ex().powi(2) * rhs;
}
let lhs = if self.polya_power == 0 {
self.goal.to_ex()
} else {
(1 + &k).powi(i64::from(self.polya_power)) * self.goal.to_ex()
};
Equation::new(lhs, rhs)
}
pub fn lean_hints(&self, hk: &str, opts: &LeanOpts) -> Result<Vec<String>, SymplexError> {
let square_hint = match &self.square {
Some(g) => Some(format!("sq_nonneg ({})", g.to_ex().to_lean_with(opts)?)),
None => None,
};
let mut hints: Vec<String> = Vec::new();
for (i, c) in self.coefficients.iter().enumerate() {
if *c <= Q::zero() {
continue;
}
let h = match i {
0 => None,
1 => Some(hk.to_string()),
_ => Some(format!("pow_nonneg {hk} {i}")),
};
let h = match (&square_hint, h) {
(Some(sq), Some(h)) => format!("mul_nonneg ({sq}) ({h})"),
(Some(sq), None) => sq.clone(),
(None, Some(h)) => h,
(None, None) => continue,
};
hints.push(h);
}
Ok(hints)
}
pub fn to_lean(&self, theorem_name: &str) -> Result<String, SymplexError> {
self.to_lean_with(theorem_name, &LeanOpts::default())
}
pub fn to_lean_with(
&self,
theorem_name: &str,
opts: &LeanOpts,
) -> Result<String, SymplexError> {
let real = &opts.real_type;
let v = lean_ident(&self.var.to_string());
let base = v.trim_matches(['«', '»']);
let a = self.endpoint.to_lean_with(opts)?;
let goal = self.goal.to_ex().to_lean_with(opts)?;
let (hyp_name, hyp, k) = match self.ray {
Ray::AtLeast => (
format!("h_{base}_lo"),
format!("{a} ≤ {v}"),
format!("{v} - {a}"),
),
Ray::AtMost => (
format!("h_{base}_hi"),
format!("{v} ≤ {a}"),
format!("{a} - {v}"),
),
};
let square_hint = self.square.is_some();
let hints = self.lean_hints("hk", opts)?;
let tactic_name = if square_hint || self.coefficients.len() > 2 {
"nlinarith"
} else {
"linarith"
};
let hint_list = if hints.is_empty() {
String::new()
} else {
format!(" [{}]", hints.join(", "))
};
let mut out = format!(
"theorem {} ({v} : {real}) ({hyp_name} : {hyp}) :\n 0 ≤ {goal} := by\n have hk : 0 ≤ {k} := sub_nonneg.mpr {hyp_name}\n",
lean_ident(theorem_name),
);
if self.polya_power == 0 {
out.push_str(&format!(" {tactic_name}{hint_list}\n"));
} else {
let n = self.polya_power;
out.push_str(&format!(
" 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"
));
}
Ok(wrap_lean(&out, MATHLIB_LINE_WIDTH))
}
}
impl fmt::Display for HalfLineCertificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let Equation { lhs, rhs } = self.identity();
write!(f, "{lhs} = {rhs}")?;
match self.ray {
Ray::AtLeast => write!(f, ", {} ≥ {}", self.var, self.endpoint),
Ray::AtMost => write!(f, ", {} ≤ {}", self.var, self.endpoint),
}
}
}
pub type HalfLineOutcome = Outcome<HalfLineCertificate, HalfLineUnknown>;
#[derive(Clone, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub struct HalfLineUnknown {
pub max_polya_power: u32,
}
impl fmt::Display for HalfLineUnknown {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"non-negative by Sturm's theorem, but no certificate up to Pólya power {}",
self.max_polya_power
)
}
}
fn shifted_coefficients(p: &Poly, var: &Ex, a: &Ex, ray: &Ray) -> Option<Vec<Q>> {
let ctx = p.context();
let k = ctx.symbol("__k");
let x_of_k = match ray {
Ray::AtLeast => a + &k,
Ray::AtMost => a - &k,
};
let q = p.to_ex().subs(var, &x_of_k);
let qp = Poly::new(&q, &[&k])?;
let coeffs = qp.all_coeffs()?; let mut asc: Vec<Q> = coeffs
.iter()
.rev()
.map(|c| c.as_rational())
.collect::<Option<_>>()?;
while asc.len() > 1 && asc.last().is_some_and(Zero::is_zero) {
asc.pop();
}
Some(asc)
}
fn times_one_plus_k(c: &[Q]) -> Vec<Q> {
let mut out = vec![Q::zero(); c.len() + 1];
for (i, ci) in c.iter().enumerate() {
out[i] += ci;
out[i + 1] += ci;
}
out
}
fn halfline_counterexample(p: &Poly, var: &Ex, a: &Q, ray: &Ray) -> Option<(Q, Q)> {
let ctx = p.context();
let e = p.to_ex();
let eval =
|x: &Q| -> Option<Q> { e.subs(var, &ctx.from_ratio(x.clone())).eval().as_rational() };
let one = Q::one();
let inside = |x: &Q| match ray {
Ray::AtLeast => x >= a,
Ray::AtMost => x <= a,
};
let mut candidates: Vec<Q> = vec![a.clone()];
let iv = e.real_roots_isolate(var);
for interval in &iv {
if let (Some(l), Some(h)) = (interval.lower.as_rational(), interval.upper.as_rational()) {
candidates.push((&l + &h) / Q::from_integer(BigInt::from(2)));
candidates.push(&l - &one);
candidates.push(&h + &one);
candidates.push((&l + a) / Q::from_integer(BigInt::from(2)));
}
}
let far = match ray {
Ray::AtLeast => a + Q::from_integer(BigInt::from(1000)),
Ray::AtMost => a - Q::from_integer(BigInt::from(1000)),
};
candidates.push(far);
for x in candidates {
if inside(&x)
&& let Some(v) = eval(&x)
&& v < Q::zero()
{
return Some((x, v));
}
}
None
}
pub fn prove_nonnegative_on_halfline(
goal: &Ex,
var: &Ex,
a: &Ex,
ray: Ray,
max_polya_power: u32,
) -> Result<HalfLineOutcome, SymplexError> {
let bad = |reason: &str| SymplexError::InvalidArgument {
operation: "prove_nonnegative_on_halfline",
reason: reason.into(),
};
let a_ex = a.eval();
let a_q = a_ex
.as_rational()
.ok_or_else(|| bad("the endpoint must be a rational literal"))?;
let goal_poly =
Poly::new(goal, &[var]).ok_or_else(|| bad("goal must be a polynomial in the variable"))?;
if !goal_poly.has_rational_coeffs() {
return Err(bad("goal must have rational coefficients"));
}
let ctx: Context = goal.context();
let (lo, hi) = match ray {
Ray::AtLeast => (a_ex.clone(), ctx.infinity()),
Ray::AtMost => (ctx.neg_infinity(), a_ex.clone()),
};
if goal_poly.is_nonnegative_on(&lo, &hi) == Some(false)
&& let Some((point, value)) = halfline_counterexample(&goal_poly, var, &a_q, &ray)
{
return Ok(Outcome::Refuted {
point: vec![(var.clone(), point)],
value,
param_value: None,
});
}
let try_certify = |p: &Poly, square: Option<&Poly>| -> Option<HalfLineCertificate> {
let mut coeffs = shifted_coefficients(p, var, &a_ex, &ray)?;
for n in 0..=max_polya_power {
if coeffs.iter().all(|c| *c >= Q::zero()) {
let cert = HalfLineCertificate {
goal: goal_poly.clone(),
var: var.clone(),
endpoint: a_ex.clone(),
ray: ray.clone(),
polya_power: n,
coefficients: coeffs,
square: square.cloned(),
};
return cert.verify().then_some(cert);
}
coeffs = times_one_plus_k(&coeffs);
}
None
};
if let Some(c) = try_certify(&goal_poly, None) {
return Ok(HalfLineOutcome::Proved(c));
}
if let Some((g, h)) = split_square_factor(&goal_poly, &[var])
&& let Some(c) = try_certify(&h, Some(&g))
{
return Ok(HalfLineOutcome::Proved(c));
}
if let Some((point, value)) = halfline_counterexample(&goal_poly, var, &a_q, &ray) {
return Ok(Outcome::Refuted {
point: vec![(var.clone(), point)],
value,
param_value: None,
});
}
Ok(Outcome::Unknown(HalfLineUnknown { max_polya_power }))
}
#[derive(Clone, Debug)]
pub struct RealLineCertificate {
pub upper: HalfLineCertificate,
pub lower: HalfLineCertificate,
}
#[derive(Clone, Debug, PartialEq, serde::Serialize, serde::Deserialize)]
pub struct RealLineCertificateData {
pub upper: HalfLineCertificateData,
pub lower: HalfLineCertificateData,
}
impl RealLineCertificate {
pub fn goal(&self) -> &Poly {
&self.upper.goal
}
pub fn verify(&self) -> bool {
self.upper.verify() && self.lower.verify() && self.upper.endpoint == self.lower.endpoint
}
pub fn to_data(&self) -> RealLineCertificateData {
RealLineCertificateData {
upper: self.upper.to_data(),
lower: self.lower.to_data(),
}
}
pub fn from_data(ctx: &Context, data: &RealLineCertificateData) -> Result<Self, SymplexError> {
let cert = RealLineCertificate {
upper: HalfLineCertificate::from_data(ctx, &data.upper)?,
lower: HalfLineCertificate::from_data(ctx, &data.lower)?,
};
if !cert.verify() {
return Err(SymplexError::InvalidArgument {
operation: "RealLineCertificate::from_data",
reason: "the two halves do not verify as one certificate".into(),
});
}
Ok(cert)
}
pub fn to_json(&self) -> Result<String, SymplexError> {
serde_json::to_string_pretty(&self.to_data()).map_err(|e| SymplexError::ComputationFailed {
operation: "RealLineCertificate::to_json",
reason: e.to_string(),
})
}
pub fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
let data: RealLineCertificateData =
serde_json::from_str(json).map_err(|e| SymplexError::InvalidArgument {
operation: "RealLineCertificate::from_json",
reason: e.to_string(),
})?;
Self::from_data(ctx, &data)
}
pub fn to_lean(&self, theorem_name: &str) -> Result<String, SymplexError> {
self.to_lean_with(theorem_name, &LeanOpts::default())
}
pub fn to_lean_with(
&self,
theorem_name: &str,
opts: &LeanOpts,
) -> Result<String, SymplexError> {
let up = self.upper.to_lean_with("_", opts)?;
let lo = self.lower.to_lean_with("_", opts)?;
let body = |text: &str| -> String {
text.split_once(":= by\n")
.map(|(_, b)| b.to_string())
.unwrap_or_default()
};
let v = lean_ident(&self.upper.var.to_string());
let base = v.trim_matches(['«', '»']).to_string();
let a = self.upper.endpoint.to_lean_with(opts)?;
let goal = self.upper.goal.to_ex().to_lean_with(opts)?;
let indent = |b: String| -> String {
b.lines()
.map(|l| format!(" {l}"))
.collect::<Vec<_>>()
.join("\n")
};
let text = format!(
"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",
lean_ident(theorem_name),
opts.real_type,
indent(body(&up)),
indent(body(&lo)),
);
Ok(wrap_lean(&text, MATHLIB_LINE_WIDTH))
}
}
impl fmt::Display for RealLineCertificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}; {}", self.upper, self.lower)
}
}
impl Certificate for BoxCertificate {
fn goal(&self) -> &Poly {
BoxCertificate::goal(self)
}
fn verify(&self) -> bool {
BoxCertificate::verify(self)
}
fn to_lean_with(&self, theorem_name: &str, opts: &LeanOpts) -> Result<String, SymplexError> {
BoxCertificate::to_lean_with(self, theorem_name, opts)
}
fn to_json(&self) -> Result<String, SymplexError> {
BoxCertificate::to_json(self)
}
fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
BoxCertificate::from_json(ctx, json)
}
}
impl Certificate for HalfLineCertificate {
fn goal(&self) -> &Poly {
HalfLineCertificate::goal(self)
}
fn verify(&self) -> bool {
HalfLineCertificate::verify(self)
}
fn to_lean_with(&self, theorem_name: &str, opts: &LeanOpts) -> Result<String, SymplexError> {
HalfLineCertificate::to_lean_with(self, theorem_name, opts)
}
fn to_json(&self) -> Result<String, SymplexError> {
HalfLineCertificate::to_json(self)
}
fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
HalfLineCertificate::from_json(ctx, json)
}
}
impl Certificate for RealLineCertificate {
fn goal(&self) -> &Poly {
RealLineCertificate::goal(self)
}
fn verify(&self) -> bool {
RealLineCertificate::verify(self)
}
fn to_lean_with(&self, theorem_name: &str, opts: &LeanOpts) -> Result<String, SymplexError> {
RealLineCertificate::to_lean_with(self, theorem_name, opts)
}
fn to_json(&self) -> Result<String, SymplexError> {
RealLineCertificate::to_json(self)
}
fn from_json(ctx: &Context, json: &str) -> Result<Self, SymplexError> {
RealLineCertificate::from_json(ctx, json)
}
}
pub fn prove_nonnegative_on_reals(
goal: &Ex,
var: &Ex,
split: &Ex,
max_polya_power: u32,
) -> Result<Option<RealLineCertificate>, SymplexError> {
let upper = prove_nonnegative_on_halfline(goal, var, split, Ray::AtLeast, max_polya_power)?;
let lower = prove_nonnegative_on_halfline(goal, var, split, Ray::AtMost, max_polya_power)?;
match (upper, lower) {
(HalfLineOutcome::Proved(upper), HalfLineOutcome::Proved(lower)) => {
Ok(Some(RealLineCertificate { upper, lower }))
}
_ => Ok(None),
}
}