use std::fmt;
use num_bigint::BigInt;
use num_rational::Ratio;
use num_traits::Zero;
use crate::api::context::Context;
use crate::api::expr::Ex;
use crate::api::poly_ex::Poly;
use crate::base::errors::SymplexError;
use crate::domains::linprog::{Feasibility, LpProblem, LpStatus, nonneg_combination};
use crate::output::lean::{LeanOpts, lean_ident};
type Q = Ratio<BigInt>;
fn invalid(reason: impl Into<String>) -> SymplexError {
SymplexError::InvalidArgument {
operation: "prove_nonnegative_on_box",
reason: reason.into(),
}
}
#[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 Certificate {
goal: Poly,
bounds: Vec<BoxBound>,
terms: Vec<HandelmanTerm>,
}
impl Certificate {
pub fn goal(&self) -> &Poly {
&self.goal
}
pub fn bounds(&self) -> &[BoxBound] {
&self.bounds
}
pub fn terms(&self) -> &[HandelmanTerm] {
&self.terms
}
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;
}
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) -> (Ex, Ex) {
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);
}
(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 mut hints: Vec<String> = Vec::new();
let mut max_factors = 0usize;
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; };
max_factors = max_factors.max(factors.len());
let mut acc = first.clone();
for f in rest {
acc = format!("mul_nonneg ({acc}) ({f})");
}
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(format!("{sig}{tactic}\n"))
}
}
impl fmt::Display for Certificate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let (lhs, rhs) = self.identity();
write!(f, "{lhs} = {rhs}")?;
for b in &self.bounds {
write!(f, ", {} ≤ {} ≤ {}", b.lo, b.var, b.hi)?;
}
Ok(())
}
}
#[derive(Clone, Debug)]
pub enum BoxOutcome {
Proved(Certificate),
Refuted {
point: Vec<Q>,
value: Q,
},
Unknown {
farkas: Option<Vec<Q>>,
degree: u32,
},
}
impl BoxOutcome {
pub fn certificate(&self) -> Option<&Certificate> {
match self {
BoxOutcome::Proved(c) => Some(c),
_ => None,
}
}
pub fn is_proved(&self) -> bool {
matches!(self, BoxOutcome::Proved(_))
}
}
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: &[(Q, 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, (lo, hi))| lo + (hi - lo) * 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 => nonneg_combination(columns, target),
}
}
pub fn prove_nonnegative_on_box(
goal: &Ex,
bounds: &[(Ex, Ex, Ex)],
degree: u32,
) -> Result<BoxOutcome, SymplexError> {
if bounds.is_empty() {
return Err(invalid("at least one bounded variable is required"));
}
let ctx: Context = goal.context();
let mut vars: Vec<&Ex> = Vec::with_capacity(bounds.len());
let mut q_bounds: Vec<(Q, Q)> = Vec::with_capacity(bounds.len());
let mut box_bounds: Vec<BoxBound> = Vec::with_capacity(bounds.len());
for (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((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(BoxOutcome::Refuted { point, value });
}
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 cert = Certificate {
goal: goal_poly,
bounds: box_bounds,
terms,
};
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(BoxOutcome::Proved(cert))
}
Feasibility::Infeasible { farkas } => {
if let Some((point, value)) = find_counterexample(&goal_poly, &q_bounds, 32) {
return Ok(BoxOutcome::Refuted { point, value });
}
Ok(BoxOutcome::Unknown { farkas, degree })
}
}
}
pub fn is_nonnegative_on_box(goal: &Ex, bounds: &[(Ex, Ex, Ex)], max_degree: u32) -> Option<bool> {
for d in 1..=max_degree.max(1) {
match prove_nonnegative_on_box(goal, bounds, d) {
Ok(BoxOutcome::Proved(_)) => return Some(true),
Ok(BoxOutcome::Refuted { .. }) => return Some(false),
Ok(BoxOutcome::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: &[(Ex, Ex, Ex)],
degree: u32,
) -> Result<BoxOutcome, SymplexError> {
prove_nonnegative_on_box(self, bounds, degree)
}
}