use alloc::vec::Vec;
use core::cmp::Ordering;
use puremp::Rational;
use crate::math::interval::{Bound, Interval};
use crate::math::polynomial::{Polynomial, Var};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Rel {
Lt,
Le,
Eq,
Ne,
Ge,
Gt,
}
#[derive(Clone, Debug)]
pub struct Constraint {
pub poly: Polynomial,
pub rel: Rel,
}
impl Constraint {
pub fn new(poly: Polynomial, rel: Rel) -> Constraint {
Constraint { poly, rel }
}
}
pub fn eval_interval(poly: &Polynomial, box_: &[Interval]) -> Interval {
let mut acc = Interval::point(Rational::from_integer(0.into()));
for (coeff, mono) in poly.terms() {
let mut term: Option<Interval> = None;
for v in mono.vars() {
let e = mono.degree_of(v);
let vi = box_.get(v as usize).cloned().unwrap_or_else(Interval::all);
let p = ipow(&vi, e);
term = Some(match term {
None => p,
Some(t) => t.mul(&p),
});
}
let term = term.unwrap_or_else(|| Interval::point(Rational::from_integer(1.into())));
acc = acc.add(&scale(&term, coeff));
}
acc
}
fn scale(iv: &Interval, c: &Rational) -> Interval {
if c.is_zero() {
return Interval::point(Rational::from_integer(0.into()));
}
let Some((lo, hi)) = iv.bounds() else {
return Interval::Empty;
};
let scaled = |b: &Bound| -> Bound {
match b {
Bound::Infinite => Bound::infinite(),
Bound::Finite { value, open } => Bound::Finite {
value: value.mul(c),
open: *open,
},
}
};
let (nlo, nhi) = (scaled(lo), scaled(hi));
if c.is_negative() {
Interval::new(nhi, nlo)
} else {
Interval::new(nlo, nhi)
}
}
#[derive(Clone)]
enum Ext {
NegInf,
PosInf,
Val(Rational),
}
impl Ext {
fn lower_of(b: &Bound) -> Ext {
match b {
Bound::Infinite => Ext::NegInf,
Bound::Finite { value, .. } => Ext::Val(value.clone()),
}
}
fn upper_of(b: &Bound) -> Ext {
match b {
Bound::Infinite => Ext::PosInf,
Bound::Finite { value, .. } => Ext::Val(value.clone()),
}
}
fn pow(&self, e: u32) -> Ext {
match self {
Ext::NegInf => {
if e.is_multiple_of(2) {
Ext::PosInf
} else {
Ext::NegInf
}
}
Ext::PosInf => Ext::PosInf,
Ext::Val(v) => Ext::Val(v.pow(e as i32)),
}
}
fn sign(&self) -> i32 {
match self {
Ext::NegInf => -1,
Ext::PosInf => 1,
Ext::Val(v) => v.signum(),
}
}
fn max(a: Ext, b: Ext) -> Ext {
match (&a, &b) {
(Ext::PosInf, _) | (_, Ext::PosInf) => Ext::PosInf,
(Ext::NegInf, other) | (other, Ext::NegInf) => other.clone(),
(Ext::Val(x), Ext::Val(y)) => {
if x >= y {
Ext::Val(x.clone())
} else {
Ext::Val(y.clone())
}
}
}
}
fn to_lower_bound(&self) -> Bound {
match self {
Ext::NegInf | Ext::PosInf => Bound::infinite(),
Ext::Val(v) => Bound::closed(v.clone()),
}
}
fn to_upper_bound(&self) -> Bound {
match self {
Ext::NegInf | Ext::PosInf => Bound::infinite(),
Ext::Val(v) => Bound::closed(v.clone()),
}
}
}
pub fn ipow(vi: &Interval, e: u32) -> Interval {
if e == 0 {
return Interval::point(Rational::from_integer(1.into()));
}
if e == 1 {
return vi.clone(); }
let Some((lo, hi)) = vi.bounds() else {
return Interval::Empty;
};
let powv = |v: &Rational| -> Rational {
let mut p = Rational::from_integer(1.into());
for _ in 0..e {
p = p.mul(v);
}
p
};
let powb = |b: &Bound| -> Bound {
match b {
Bound::Infinite => Bound::Infinite,
Bound::Finite { value, open } => Bound::Finite {
value: powv(value),
open: *open,
},
}
};
if e % 2 == 1 {
return Interval::new(powb(lo), powb(hi));
}
let lo_nonneg = matches!(lo, Bound::Finite { value, .. } if !value.is_negative());
let hi_nonpos = matches!(hi, Bound::Finite { value, .. } if !value.is_positive());
if lo_nonneg {
Interval::new(powb(lo), powb(hi)) } else if hi_nonpos {
Interval::new(powb(hi), powb(lo)) } else {
let up = match (lo, hi) {
(Bound::Infinite, _) | (_, Bound::Infinite) => Bound::Infinite,
(
Bound::Finite {
value: lv,
open: lo_open,
},
Bound::Finite {
value: hv,
open: hi_open,
},
) => {
let (pl, ph) = (powv(lv), powv(hv));
match pl.cmp(&ph) {
Ordering::Greater => Bound::Finite {
value: pl,
open: *lo_open,
},
Ordering::Less => Bound::Finite {
value: ph,
open: *hi_open,
},
Ordering::Equal => Bound::Finite {
value: ph,
open: *lo_open && *hi_open,
},
}
}
};
Interval::new(Bound::closed(Rational::from_integer(0.into())), up)
}
}
fn relation_feasible(i: &Interval, rel: Rel) -> bool {
let Some((lo, hi)) = i.bounds() else {
return false; };
let lo_cmp0 = bound_cmp_zero_lower(lo); let hi_cmp0 = bound_cmp_zero_upper(hi);
match rel {
Rel::Lt => lo_cmp0 == SignPos::BelowStrict,
Rel::Le => matches!(lo_cmp0, SignPos::BelowStrict | SignPos::AtZero),
Rel::Gt => hi_cmp0 == SignPos::AboveStrict,
Rel::Ge => matches!(hi_cmp0, SignPos::AboveStrict | SignPos::AtZero),
Rel::Eq => i.contains(&Rational::from_integer(0.into())),
Rel::Ne => !is_point_zero(i),
}
}
#[derive(PartialEq, Eq)]
enum SignPos {
BelowStrict,
AtZero,
AboveStrict,
}
fn bound_cmp_zero_lower(lo: &Bound) -> SignPos {
match lo {
Bound::Infinite => SignPos::BelowStrict, Bound::Finite { value, open } => match value.signum().cmp(&0) {
Ordering::Less => SignPos::BelowStrict,
Ordering::Greater => SignPos::AboveStrict,
Ordering::Equal => {
if *open {
SignPos::AboveStrict
} else {
SignPos::AtZero
}
}
},
}
}
fn bound_cmp_zero_upper(hi: &Bound) -> SignPos {
match hi {
Bound::Infinite => SignPos::AboveStrict, Bound::Finite { value, open } => match value.signum().cmp(&0) {
Ordering::Greater => SignPos::AboveStrict,
Ordering::Less => SignPos::BelowStrict,
Ordering::Equal => {
if *open {
SignPos::BelowStrict
} else {
SignPos::AtZero
}
}
},
}
}
fn is_point_zero(i: &Interval) -> bool {
match i.bounds() {
Some((
Bound::Finite {
value: l,
open: false,
},
Bound::Finite {
value: h,
open: false,
},
)) => l.is_zero() && h.is_zero(),
_ => false,
}
}
fn linear_bound(c: &Constraint) -> Option<(Var, Interval)> {
let p = &c.poly;
let vars = p.vars();
if vars.len() != 1 || p.total_degree() != 1 {
return None;
}
let x = vars[0];
let a = p
.terms()
.iter()
.find(|(_, m)| m.degree_of(x) == 1)
.map(|(co, _)| co.clone())?;
let b = p
.terms()
.iter()
.find(|(_, m)| m.is_one())
.map(|(co, _)| co.clone())
.unwrap_or_else(|| Rational::from_integer(0.into()));
let bound_val = b.neg().div(&a);
let flip = a.is_negative();
let rel = if flip { flip_rel(c.rel) } else { c.rel };
let iv = match rel {
Rel::Lt => Interval::new(Bound::infinite(), Bound::open(bound_val)),
Rel::Le => Interval::new(Bound::infinite(), Bound::closed(bound_val)),
Rel::Gt => Interval::new(Bound::open(bound_val), Bound::infinite()),
Rel::Ge => Interval::new(Bound::closed(bound_val), Bound::infinite()),
Rel::Eq => Interval::point(bound_val),
Rel::Ne => return None, };
Some((x, iv))
}
fn flip_rel(r: Rel) -> Rel {
match r {
Rel::Lt => Rel::Gt,
Rel::Le => Rel::Ge,
Rel::Gt => Rel::Lt,
Rel::Ge => Rel::Le,
Rel::Eq => Rel::Eq,
Rel::Ne => Rel::Ne,
}
}
fn sqrt_upper(b: &Rational) -> Rational {
if !b.is_positive() {
return Rational::from_integer(0.into());
}
let two = Rational::from_integer(2.into());
let mut lo = Rational::from_integer(0.into());
let mut hi = if *b > Rational::from_integer(1.into()) {
b.clone()
} else {
Rational::from_integer(1.into())
};
for _ in 0..48 {
let mid = lo.add(&hi).div(&two);
if mid.mul(&mid) >= *b {
hi = mid;
} else {
lo = mid;
}
}
hi
}
fn interval_inf(iv: &Interval) -> Option<Rational> {
match iv.bounds()?.0 {
Bound::Finite { value, .. } => Some(value.clone()),
Bound::Infinite => None,
}
}
fn square_bound(c: &Constraint, v: Var, box_: &[Interval]) -> Option<Interval> {
let strict = match c.rel {
Rel::Lt => true,
Rel::Le | Rel::Eq => false,
_ => return None,
};
let mut a = Rational::from_integer(0.into());
let mut rest_terms = alloc::vec::Vec::new();
for (coeff, mono) in c.poly.terms() {
match mono.degree_of(v) {
0 => rest_terms.push((coeff.clone(), mono.clone())),
2 if mono.total_degree() == 2 => a = a.add(coeff), _ => return None, }
}
let mut rest = Polynomial::from_terms(rest_terms);
if a.is_negative() && c.rel == Rel::Eq {
a = a.neg();
rest = rest.neg();
}
if !a.is_positive() {
return None;
}
let c_iv = eval_interval(&rest, box_);
let inf = interval_inf(&c_iv)?; let b = inf.neg().div(&a); if b.is_negative() {
return Some(Interval::Empty);
}
if b.is_zero() {
return Some(if strict {
Interval::Empty } else {
Interval::point(Rational::from_integer(0.into())) });
}
let u = sqrt_upper(&b);
Some(Interval::closed(u.neg(), u))
}
pub fn narrow_box(constraints: &[Constraint], num_vars: usize) -> Option<Vec<Interval>> {
let mut box_: Vec<Interval> = alloc::vec![Interval::all(); num_vars];
let mut changed = true;
let mut rounds = 0;
while changed && rounds < 2 * num_vars + 6 {
changed = false;
rounds += 1;
for c in constraints {
if let Some((x, iv)) = linear_bound(c) {
let tightened = box_[x as usize].intersect(&iv);
if tightened != box_[x as usize] {
box_[x as usize] = tightened;
changed = true;
}
}
for v in 0..num_vars as Var {
if let Some(iv) = square_bound(c, v, &box_) {
let tightened = box_[v as usize].intersect(&iv);
if tightened != box_[v as usize] {
box_[v as usize] = tightened;
changed = true;
}
}
if let Some(iv) = product_bound(c, v, &box_) {
let tightened = box_[v as usize].intersect(&iv);
if tightened != box_[v as usize] {
box_[v as usize] = tightened;
changed = true;
}
}
}
}
if box_.iter().any(Interval::is_empty) {
return None;
}
}
Some(box_)
}
fn bound_recip(b: &Bound) -> Bound {
match b {
Bound::Infinite => Bound::open(Rational::from_integer(0.into())),
Bound::Finite { value, open } => {
if value.is_zero() {
Bound::infinite()
} else {
Bound::Finite {
value: value.recip(),
open: *open,
}
}
}
}
}
fn bound_gt_zero(b: &Bound) -> bool {
matches!(b, Bound::Finite { value, open } if value.is_positive() || (value.is_zero() && *open))
}
fn bound_lt_zero(b: &Bound) -> bool {
matches!(b, Bound::Finite { value, open } if value.is_negative() || (value.is_zero() && *open))
}
fn interval_recip(iv: &Interval) -> Option<Interval> {
let (lo, hi) = iv.bounds()?;
if !bound_gt_zero(lo) && !bound_lt_zero(hi) {
return None; }
Some(Interval::new(bound_recip(hi), bound_recip(lo)))
}
fn product_bound(c: &Constraint, v: Var, box_: &[Interval]) -> Option<Interval> {
if c.poly.degree_of(v) != 1 {
return None;
}
let c1 = c.poly.coeff_of_var(v, 1);
let c0 = c.poly.coeff_of_var(v, 0);
let a = eval_interval(&c1, box_);
let b = eval_interval(&c0, box_);
let (alo, ahi) = a.bounds()?;
let a_pos = bound_gt_zero(alo);
let a_neg = bound_lt_zero(ahi);
if !a_pos && !a_neg {
return None; }
let recip = interval_recip(&a)?; let b_zero = matches!(
b.bounds(),
Some((Bound::Finite { value: lv, open: false }, Bound::Finite { value: hv, open: false }))
if lv.is_zero() && hv.is_zero()
);
let sol = if b_zero {
Interval::point(Rational::from_integer(0.into()))
} else {
b.neg().mul(&recip)
};
let (slo, shi) = sol.bounds()?;
let neg_inf = Bound::infinite();
let result = match (c.rel, a_pos) {
(Rel::Eq, _) => sol.clone(),
(Rel::Le | Rel::Lt, true) | (Rel::Ge | Rel::Gt, false) => {
Interval::new(neg_inf, shi.clone())
}
(Rel::Le | Rel::Lt, false) | (Rel::Ge | Rel::Gt, true) => {
Interval::new(slo.clone(), neg_inf)
}
(Rel::Ne, _) => return None,
};
Some(result)
}
pub fn refute(constraints: &[Constraint], num_vars: usize) -> bool {
let Some(box_) = narrow_box(constraints, num_vars) else {
return true; };
for c in constraints {
let val = eval_interval(&c.poly, &box_);
if !relation_feasible(&val, c.rel) {
return true;
}
}
false
}
fn int_range(iv: &Interval) -> Option<(i64, i64)> {
let (lb, ub) = iv.bounds()?;
let lo = match lb {
Bound::Infinite => return None,
Bound::Finite { value, open } => {
let c = if *open {
value.floor().add(&1.into())
} else {
value.ceil()
};
c.to_i64()?
}
};
let hi = match ub {
Bound::Infinite => return None,
Bound::Finite { value, open } => {
let f = if *open {
value.ceil().sub(&1.into())
} else {
value.floor()
};
f.to_i64()?
}
};
Some((lo, hi))
}
pub fn decide_bounded_int(constraints: &[Constraint], num_vars: usize) -> Option<bool> {
let box_ = match narrow_box(constraints, num_vars) {
None => return Some(false), Some(b) => b,
};
let mut ranges: Vec<(i64, i64)> = Vec::with_capacity(num_vars);
let mut total: u128 = 1;
for iv in &box_ {
let (lo, hi) = int_range(iv)?;
if lo > hi {
return Some(false); }
total = total.saturating_mul((hi - lo + 1) as u128);
if total > 300_000 {
return None; }
ranges.push((lo, hi));
}
let mut point: Vec<i64> = ranges.iter().map(|&(lo, _)| lo).collect();
loop {
let sat = constraints.iter().all(|c| {
let val = c
.poly
.eval(&|v| Rational::from_integer(point[v as usize].into()));
match c.rel {
Rel::Lt => val.is_negative(),
Rel::Le => val.is_negative() || val.is_zero(),
Rel::Gt => val.is_positive(),
Rel::Ge => val.is_positive() || val.is_zero(),
Rel::Eq => val.is_zero(),
Rel::Ne => !val.is_zero(),
}
});
if sat {
return Some(true);
}
let mut i = 0;
loop {
if i == num_vars {
return Some(false); }
point[i] += 1;
if point[i] <= ranges[i].1 {
break;
}
point[i] = ranges[i].0;
i += 1;
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::math::polynomial::Monomial;
fn r(n: i64) -> Rational {
Rational::from_integer(n.into())
}
fn x_pow(v: Var, e: u32) -> Monomial {
Monomial::from_powers(&[(v, e)])
}
fn poly1(a: i64, b: i64) -> Polynomial {
poly1v(a, b, 0)
}
fn poly1v(a: i64, b: i64, var: Var) -> Polynomial {
Polynomial::from_terms(vec![
(r(a), Monomial::from_powers(&[(var, 1)])),
(r(b), Monomial::one()),
])
}
#[test]
fn square_negative_unsat() {
let p = Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 2))]);
assert!(refute(&[Constraint::new(p, Rel::Lt)], 1));
}
#[test]
fn bound_and_square_unsat() {
let c1 = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 1)), (r(-2), Monomial::one())]),
Rel::Gt,
);
let c2 = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 2)), (r(-4), Monomial::one())]),
Rel::Lt,
);
assert!(refute(&[c1, c2], 1));
}
#[test]
fn circle_and_bound_unsat() {
let c1 = Constraint::new(
Polynomial::from_terms(alloc::vec![
(r(1), x_pow(0, 2)),
(r(1), x_pow(1, 2)),
(r(-1), Monomial::one()),
]),
Rel::Lt,
);
let c2 = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 1)), (r(-2), Monomial::one())]),
Rel::Gt,
);
assert!(refute(&[c1, c2], 2));
}
#[test]
fn satisfiable_not_refuted() {
let c = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 2)), (r(-4), Monomial::one())]),
Rel::Eq,
);
assert!(!refute(&[c], 1));
}
#[test]
fn linear_box_empty_unsat() {
let c1 = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 1)), (r(-5), Monomial::one())]),
Rel::Ge,
);
let c2 = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 1)), (r(-3), Monomial::one())]),
Rel::Le,
);
assert!(refute(&[c1, c2], 1));
}
#[test]
fn bounded_int_product_sat() {
let c = vec![
Constraint::new(
Polynomial::from_terms(vec![
(r(1), Monomial::from_powers(&[(0, 1), (1, 1)])),
(r(-12), Monomial::one()),
]),
Rel::Eq,
),
Constraint::new(poly1(1, -1), Rel::Ge), Constraint::new(poly1(1, -4), Rel::Le), Constraint::new(poly1v(1, -1, 1), Rel::Ge), Constraint::new(poly1v(1, -4, 1), Rel::Le), ];
assert_eq!(decide_bounded_int(&c, 2), Some(true));
}
#[test]
fn bounded_int_xy_eq_x_unsat() {
let c = vec![
Constraint::new(
Polynomial::from_terms(vec![
(r(1), Monomial::from_powers(&[(0, 1), (1, 1)])), (r(-1), Monomial::from_powers(&[(0, 1)])), ]),
Rel::Eq,
),
Constraint::new(poly1(1, -1), Rel::Gt), Constraint::new(poly1v(1, -1, 1), Rel::Gt), ];
assert_eq!(decide_bounded_int(&c, 2), Some(false));
}
#[test]
fn bounded_int_product_unsat() {
let c = vec![
Constraint::new(
Polynomial::from_terms(vec![
(r(1), Monomial::from_powers(&[(0, 1), (1, 1)])),
(r(-7), Monomial::one()),
]),
Rel::Eq,
),
Constraint::new(poly1(1, -1), Rel::Ge),
Constraint::new(poly1(1, -3), Rel::Le),
Constraint::new(poly1v(1, -1, 1), Rel::Ge),
Constraint::new(poly1v(1, -3, 1), Rel::Le),
];
assert_eq!(decide_bounded_int(&c, 2), Some(false));
}
#[test]
fn square_narrowing_refutes() {
let c = vec![
Constraint::new(
Polynomial::from_terms(vec![
(r(1), x_pow(0, 2)),
(r(1), x_pow(1, 2)),
(r(-1), Monomial::one()),
]),
Rel::Lt,
),
Constraint::new(
Polynomial::from_terms(vec![
(r(1), Monomial::from_powers(&[(0, 1), (1, 1)])),
(r(-1), Monomial::one()),
]),
Rel::Gt,
),
];
assert!(refute(&c, 2));
}
#[test]
fn square_narrowing_no_false_refute() {
let c = vec![
Constraint::new(
Polynomial::from_terms(vec![
(r(1), x_pow(0, 2)),
(r(1), x_pow(1, 2)),
(r(-4), Monomial::one()),
]),
Rel::Lt,
),
Constraint::new(
Polynomial::from_terms(vec![
(r(1), Monomial::from_powers(&[(0, 1), (1, 1)])),
(r(-1), Monomial::one()),
]),
Rel::Gt,
),
];
assert!(!refute(&c, 2), "wrongly refuted a satisfiable system");
}
#[test]
fn unbounded_int_declined() {
let c = vec![Constraint::new(
Polynomial::from_terms(vec![(r(1), Monomial::from_powers(&[(0, 1), (1, 1)]))]),
Rel::Eq,
)];
assert_eq!(decide_bounded_int(&c, 2), None);
}
#[test]
fn feasible_linear_not_refuted() {
let c1 = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 1)), (r(-1), Monomial::one())]),
Rel::Ge,
);
let c2 = Constraint::new(
Polynomial::from_terms(alloc::vec![(r(1), x_pow(0, 1)), (r(-5), Monomial::one())]),
Rel::Le,
);
assert!(!refute(&[c1, c2], 1));
}
}