use crate::nl_reader::{BinOp, Expr, UnaryOp};
use std::collections::{BTreeMap, BTreeSet};
pub type QuadHessian = BTreeMap<(usize, usize), f64>;
pub type QuadForm = (QuadHessian, Vec<(usize, f64)>, f64);
#[derive(Debug, Clone, Default, PartialEq)]
pub struct Quad2 {
constant: f64,
linear: BTreeMap<usize, f64>,
quadratic: QuadHessian,
lost_terms: bool,
inexact: bool,
}
impl Quad2 {
pub fn constant(&self) -> f64 {
self.constant
}
pub fn linear(&self) -> &BTreeMap<usize, f64> {
&self.linear
}
pub fn quadratic(&self) -> &QuadHessian {
&self.quadratic
}
pub fn lost_terms(&self) -> bool {
self.lost_terms
}
pub fn inexact(&self) -> bool {
self.inexact
}
pub(crate) fn of_constant(c: f64) -> Self {
Quad2 {
constant: if c != 0.0 { c } else { 0.0 },
..Quad2::default()
}
}
pub(crate) fn of_var(i: usize) -> Self {
let mut q = Quad2::default();
q.linear.insert(i, 1.0);
q
}
pub(crate) fn degree(&self) -> usize {
if !self.quadratic.is_empty() {
2
} else if !self.linear.is_empty() {
1
} else {
0
}
}
pub(crate) fn as_constant(&self) -> Option<f64> {
(self.degree() == 0).then_some(self.constant)
}
fn width(&self) -> usize {
self.linear.len() + self.quadratic.len()
}
pub(crate) fn add(a: Quad2, b: Quad2) -> Quad2 {
let (mut acc, small) = if a.width() >= b.width() {
(a, b)
} else {
(b, a)
};
let carried_inexact = acc.inexact || small.inexact;
let (mut dropped, mut inexact) = (false, false);
if small.constant != 0.0 {
let was = acc.constant;
acc.constant += small.constant;
inexact |= !add_is_exact(was, small.constant, acc.constant);
dropped |= was != 0.0 && acc.constant == 0.0;
}
for (i, c) in &small.linear {
let m = merge(&mut acc.linear, *i, *c);
dropped |= m.dropped;
inexact |= m.inexact;
}
for (k, c) in &small.quadratic {
let m = merge(&mut acc.quadratic, *k, *c);
dropped |= m.dropped;
inexact |= m.inexact;
}
acc.lost_terms |= small.lost_terms || (dropped && (carried_inexact || inexact));
acc.inexact = carried_inexact || inexact;
acc
}
pub(crate) fn neg(mut self) -> Quad2 {
self.constant = -self.constant;
for c in self.linear.values_mut() {
*c = -*c;
}
for c in self.quadratic.values_mut() {
*c = -*c;
}
self
}
pub(crate) fn scale(mut self, s: f64) -> Quad2 {
if s == 0.0 {
return Quad2 {
lost_terms: self.lost_terms,
inexact: self.inexact,
..Quad2::default()
};
}
let was = self.constant;
self.constant *= s;
if was != 0.0 {
self.lost_terms |= self.constant == 0.0;
self.inexact |= !mul_is_exact(was, s, self.constant);
}
let mut inexact = false;
for c in self.linear.values_mut() {
let was = *c;
*c *= s;
inexact |= !mul_is_exact(was, s, *c);
}
for c in self.quadratic.values_mut() {
let was = *c;
*c *= s;
inexact |= !mul_is_exact(was, s, *c);
}
self.inexact |= inexact;
self.lost_terms |= self.prune();
self
}
fn prune(&mut self) -> bool {
let before = self.width();
self.linear.retain(|_, c| is_live(*c));
self.quadratic.retain(|_, c| is_live(*c));
self.width() != before
}
pub(crate) fn div_by_constant(self, d: f64) -> Quad2 {
let r = 1.0 / d;
let exact = r.is_normal() && d.is_normal() && r.mul_add(d, -1.0) == 0.0;
let mut out = self.scale(r);
out.inexact |= !exact;
out
}
pub(crate) fn absorb_flags(&mut self, other: &Quad2) {
self.lost_terms |= other.lost_terms;
self.inexact |= other.inexact;
}
pub(crate) fn mul(&self, other: &Quad2) -> Option<Quad2> {
if self.degree() + other.degree() > 2 {
return None;
}
let mut out = Quad2::default();
let mut lost = self.lost_terms || other.lost_terms;
let carried_inexact = self.inexact || other.inexact;
let mut inexact = false;
let product = |a: f64, b: f64, lost: &mut bool, inexact: &mut bool| -> f64 {
let t = a * b;
*lost |= !is_live(t);
*inexact |= !mul_is_exact(a, b, t);
t
};
if self.constant != 0.0 && other.constant != 0.0 {
out.constant = product(self.constant, other.constant, &mut lost, &mut inexact);
}
let mut dropped = false;
for (a, b) in [(self, other), (other, self)] {
if a.constant == 0.0 {
continue;
}
for (i, c) in &b.linear {
let t = product(a.constant, *c, &mut lost, &mut inexact);
let m = accumulate(&mut out.linear, *i, t);
dropped |= m.dropped;
inexact |= m.inexact;
}
for (k, c) in &b.quadratic {
let t = product(a.constant, *c, &mut lost, &mut inexact);
let m = accumulate(&mut out.quadratic, *k, t);
dropped |= m.dropped;
inexact |= m.inexact;
}
}
for (i, a) in &self.linear {
for (j, b) in &other.linear {
let key = (*i.min(j), *i.max(j));
let t = product(*a, *b, &mut lost, &mut inexact);
let m = accumulate(&mut out.quadratic, key, t);
dropped |= m.dropped;
inexact |= m.inexact;
}
}
dropped |= out.prune();
out.lost_terms = lost || (dropped && (carried_inexact || inexact));
out.inexact = carried_inexact || inexact;
Some(out)
}
}
#[derive(Clone, Copy)]
struct Merged {
dropped: bool,
inexact: bool,
}
fn merge<K: Ord>(map: &mut BTreeMap<K, f64>, key: K, c: f64) -> Merged {
use std::collections::btree_map::Entry;
match map.entry(key) {
Entry::Occupied(mut e) => {
let a = *e.get();
let v = a + c;
let inexact = !add_is_exact(a, c, v);
if is_live(v) {
e.insert(v);
Merged {
dropped: false,
inexact,
}
} else {
e.remove();
Merged {
dropped: true,
inexact,
}
}
}
Entry::Vacant(e) => {
if is_live(c) {
e.insert(c);
Merged {
dropped: false,
inexact: false,
}
} else {
Merged {
dropped: true,
inexact: c.is_nan(),
}
}
}
}
}
fn accumulate<K: Ord>(map: &mut BTreeMap<K, f64>, key: K, t: f64) -> Merged {
let slot = map.entry(key).or_insert(0.0);
let was = *slot;
*slot = was + t;
Merged {
dropped: was != 0.0 && !is_live(*slot),
inexact: !add_is_exact(was, t, *slot),
}
}
use pounce_common::exact::{add_is_exact, is_live, mul_is_exact};
enum Step<'a> {
Visit(&'a Expr),
Apply(Op),
}
enum Op {
Neg,
Add,
Sub,
Mul,
Div,
Pow,
Sum(usize),
CacheCse(*const Expr),
}
pub fn recognize_expr(e: &Expr) -> Option<Quad2> {
let mut work: Vec<Step<'_>> = vec![Step::Visit(e)];
let mut vals: Vec<Quad2> = Vec::new();
let mut cse: std::collections::HashMap<*const Expr, Quad2> = std::collections::HashMap::new();
while let Some(step) = work.pop() {
match step {
Step::Visit(e) => match e {
Expr::Const(c) => vals.push(Quad2::of_constant(*c)),
Expr::Var(i) => vals.push(Quad2::of_var(*i)),
Expr::Cse(body) => {
let key = std::sync::Arc::as_ptr(body);
match cse.get(&key) {
Some(q) => vals.push(q.clone()),
None => {
work.push(Step::Apply(Op::CacheCse(key)));
work.push(Step::Visit(body));
}
}
}
Expr::Sum(items) => {
work.push(Step::Apply(Op::Sum(items.len())));
for it in items {
work.push(Step::Visit(it));
}
}
Expr::Unary(UnaryOp::Neg, a) => {
work.push(Step::Apply(Op::Neg));
work.push(Step::Visit(a));
}
Expr::Unary(..) => return None,
Expr::Binary(op, a, b) => {
let op = match op {
BinOp::Add => Op::Add,
BinOp::Sub => Op::Sub,
BinOp::Mul => Op::Mul,
BinOp::Div => Op::Div,
BinOp::Pow => Op::Pow,
_ => return None,
};
work.push(Step::Apply(op));
work.push(Step::Visit(b));
work.push(Step::Visit(a));
}
_ => return None,
},
Step::Apply(Op::CacheCse(key)) => {
cse.insert(key, vals.last()?.clone());
}
Step::Apply(op) => {
let combined = match op {
Op::CacheCse(_) => unreachable!("handled above"),
Op::Sum(n) => {
let at = vals.len().checked_sub(n)?;
let mut acc = Quad2::default();
for p in vals.drain(at..).rev() {
acc = Quad2::add(acc, p);
}
acc
}
Op::Neg => vals.pop()?.neg(),
Op::Add => {
let (a, b) = pop2(&mut vals)?;
Quad2::add(a, b)
}
Op::Sub => {
let (a, b) = pop2(&mut vals)?;
Quad2::add(a, b.neg())
}
Op::Mul => {
let (a, b) = pop2(&mut vals)?;
a.mul(&b)?
}
Op::Div => {
let (a, b) = pop2(&mut vals)?;
let d = b.as_constant()?;
if d == 0.0 {
return None;
}
let mut out = a.div_by_constant(d);
out.absorb_flags(&b);
out
}
Op::Pow => {
let (a, b) = pop2(&mut vals)?;
let exp = b.as_constant()?;
let mut out = if exp == 0.0 {
Quad2::of_constant(1.0)
} else if exp == 1.0 {
a
} else if exp == 2.0 {
a.mul(&a)?
} else {
return None;
};
out.absorb_flags(&b);
out
}
};
vals.push(combined);
}
}
}
debug_assert_eq!(vals.len(), 1, "one value per lowered expression");
vals.pop()
}
fn pop2(vals: &mut Vec<Quad2>) -> Option<(Quad2, Quad2)> {
let b = vals.pop()?;
let a = vals.pop()?;
Some((a, b))
}
pub fn analyze_quadratic(e: &Expr) -> Option<QuadHessian> {
analyze_quadratic_full(e).map(|(h, _, _)| h)
}
pub fn analyze_quadratic_full(e: &Expr) -> Option<QuadForm> {
Some(quad_form_readout(&recognize_expr(e)?))
}
pub fn quad_form_readout(q: &Quad2) -> QuadForm {
let mut h: QuadHessian = q
.quadratic
.iter()
.map(|(&(i, j), c)| ((i, j), if i == j { 2.0 * c } else { *c }))
.collect();
h.retain(|_, v| v.abs() > 0.0);
let lin: Vec<(usize, f64)> = q.linear.iter().map(|(i, c)| (*i, *c)).collect();
(h, lin, 0.0 + q.constant)
}
pub fn is_expanded_quadratic(e: &Expr) -> bool {
let mut seen: BTreeSet<(*const Expr, bool)> = BTreeSet::new();
let mut spine: Vec<&Expr> = vec![e];
while let Some(e) = spine.pop() {
match e {
Expr::Sum(items) => spine.extend(items.iter()),
Expr::Binary(BinOp::Add | BinOp::Sub, a, b) => {
spine.push(a);
spine.push(b);
}
Expr::Unary(UnaryOp::Neg, a) => spine.push(a),
Expr::Cse(body) => {
if seen.insert((std::sync::Arc::as_ptr(body), false)) {
spine.push(body);
}
}
other => {
if !is_monomial(other, &mut seen) {
return false;
}
}
}
}
true
}
pub fn is_monomial_expr(e: &Expr) -> bool {
let mut seen: BTreeSet<(*const Expr, bool)> = BTreeSet::new();
is_monomial(e, &mut seen)
}
fn is_monomial(e: &Expr, seen: &mut BTreeSet<(*const Expr, bool)>) -> bool {
let mut work: Vec<&Expr> = vec![e];
while let Some(e) = work.pop() {
match e {
Expr::Const(_) | Expr::Var(_) => {}
Expr::Cse(body) => {
if seen.insert((std::sync::Arc::as_ptr(body), true)) {
work.push(body);
}
}
Expr::Unary(UnaryOp::Neg, a) => work.push(a),
Expr::Binary(BinOp::Mul | BinOp::Div, a, b) => {
work.push(a);
work.push(b);
}
Expr::Binary(BinOp::Pow, a, b) => {
if !matches!(
a.as_ref(),
Expr::Const(_) | Expr::Var(_) | Expr::Unary(UnaryOp::Neg, _)
) {
return false;
}
work.push(a);
work.push(b);
}
_ => return false,
}
}
true
}
pub fn is_trivially_zero(e: &Expr) -> bool {
matches!(e, Expr::Const(c) if *c == 0.0)
}
#[derive(Debug, Clone, PartialEq)]
pub struct SquaredAffine {
pub weight: f64,
pub coefs: Vec<(usize, f64)>,
pub constant: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FactoredQuadratic {
pub squares: Vec<SquaredAffine>,
pub linear: Vec<(usize, f64)>,
pub constant: f64,
}
pub fn recognize_factored_quadratic(e: &Expr) -> Option<FactoredQuadratic> {
let mut seen: BTreeSet<(*const Expr, bool)> = BTreeSet::new();
let mut spine: Vec<(&Expr, f64)> = vec![(e, 1.0)];
let mut squares: Vec<SquaredAffine> = Vec::new();
let mut rest = Quad2::default();
while let Some((e, sign)) = spine.pop() {
match e {
Expr::Sum(items) => spine.extend(items.iter().rev().map(|it| (it, sign))),
Expr::Binary(BinOp::Add, a, b) => {
spine.push((b, sign));
spine.push((a, sign));
}
Expr::Binary(BinOp::Sub, a, b) => {
spine.push((b, -sign));
spine.push((a, sign));
}
Expr::Unary(UnaryOp::Neg, a) => spine.push((a, -sign)),
Expr::Cse(_) => return None,
leaf => {
if let Some((weight, base)) = peel_square(leaf) {
squares.push(admit_square(sign * weight, base)?);
continue;
}
if !is_monomial(leaf, &mut seen) {
return None;
}
let q = recognize_expr(leaf)?;
match diagonal_square(&q) {
Some((i, c)) => squares.push(SquaredAffine {
weight: sign * c,
coefs: vec![(i, 1.0)],
constant: 0.0,
}),
None if !q.quadratic().is_empty() => return None,
None => rest = Quad2::add(rest, if sign < 0.0 { q.neg() } else { q }),
}
}
}
}
if !squares.iter().any(|t| !t.coefs.is_empty()) {
return None;
}
if !rest.quadratic().is_empty() || rest.lost_terms() {
return None;
}
Some(FactoredQuadratic {
squares,
linear: rest.linear().iter().map(|(&i, &c)| (i, c)).collect(),
constant: 0.0 + rest.constant(),
})
}
fn diagonal_square(q: &Quad2) -> Option<(usize, f64)> {
if q.lost_terms() || !q.linear().is_empty() || q.constant() != 0.0 {
return None;
}
match q.quadratic().iter().next() {
Some((&(i, j), &c)) if i == j && q.quadratic().len() == 1 => Some((i, c)),
_ => None,
}
}
fn admit_square(weight: f64, base: &Expr) -> Option<SquaredAffine> {
if !is_expanded_quadratic(base) {
return None;
}
let q = recognize_expr(base)?;
if !q.quadratic().is_empty() || q.lost_terms() {
return None;
}
Some(SquaredAffine {
weight,
coefs: q.linear().iter().map(|(&i, &c)| (i, c)).collect(),
constant: 0.0 + q.constant(),
})
}
fn peel_square(e: &Expr) -> Option<(f64, &Expr)> {
let mut work: Vec<(&Expr, bool)> = vec![(e, false)];
let mut weight = 1.0f64;
let mut base: Option<&Expr> = None;
while let Some((e, recip)) = work.pop() {
match e {
Expr::Const(c) => weight = if recip { weight / c } else { weight * c },
Expr::Unary(UnaryOp::Neg, a) => {
weight = -weight;
work.push((a, recip));
}
Expr::Binary(BinOp::Mul, a, b) => {
work.push((a, recip));
work.push((b, recip));
}
Expr::Binary(BinOp::Div, a, b) => {
work.push((a, recip));
work.push((b, !recip));
}
Expr::Binary(BinOp::Pow, a, b) if matches!(b.as_ref(), Expr::Const(c) if *c == 2.0) => {
if recip || base.is_some() {
return None;
}
base = Some(a);
}
_ => return None,
}
}
let base = base?;
(weight.is_finite() && weight != 0.0).then_some((weight, base))
}
#[cfg(test)]
mod tests {
use super::*;
fn sq(i: usize) -> Expr {
Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(i)),
Box::new(Expr::Const(2.0)),
)
}
#[test]
fn quadratic_diagonal() {
let e = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Binary(
BinOp::Sub,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(1.0)),
)),
Box::new(Expr::Const(2.0)),
);
let (h, lin, c) = analyze_quadratic_full(&e).expect("degree-2 polynomial");
assert_eq!(h.get(&(0, 0)), Some(&2.0));
assert_eq!(lin, vec![(0, -2.0)]);
assert_eq!(c, 1.0);
}
#[test]
fn cross_term_hessian() {
let e = Expr::Binary(BinOp::Mul, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
let h = analyze_quadratic(&e).expect("degree-2");
assert_eq!(h.get(&(0, 1)), Some(&1.0));
}
#[test]
fn rejects_transcendental_and_cubic() {
assert!(analyze_quadratic(&Expr::Unary(UnaryOp::Sin, Box::new(Expr::Var(0)))).is_none());
let cubic = Expr::Binary(
BinOp::Pow,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(3.0)),
);
assert!(analyze_quadratic(&cubic).is_none());
let deg3 = Expr::Binary(BinOp::Mul, Box::new(sq(0)), Box::new(Expr::Var(1)));
assert!(analyze_quadratic(°3).is_none());
}
#[test]
fn division_by_a_constant_scales_by_the_reciprocal() {
let e = Expr::Binary(BinOp::Div, Box::new(sq(0)), Box::new(Expr::Const(3.0)));
let h = analyze_quadratic(&e).expect("degree-2");
assert_eq!(h.get(&(0, 0)), Some(&(2.0 * (1.0 / 3.0))));
let e = Expr::Binary(BinOp::Div, Box::new(sq(0)), Box::new(Expr::Var(1)));
assert!(analyze_quadratic(&e).is_none());
}
#[test]
fn scaling_a_coefficient_to_zero_drops_it_like_cancellation_does() {
let tiny = Expr::Binary(BinOp::Mul, Box::new(Expr::Const(1e-300)), Box::new(sq(0)));
let flushed = Expr::Binary(BinOp::Div, Box::new(tiny), Box::new(Expr::Const(1e300)));
let h = analyze_quadratic(&flushed).expect("degree-2 at worst");
assert!(
h.is_empty(),
"underflowed coefficient was kept as a structural nonzero: {h:?}"
);
let q = recognize_expr(&flushed).expect("degree-2 at worst");
assert!(
q.lost_terms(),
"a coefficient that underflowed in `scale` was dropped silently"
);
let times_x1 = Expr::Binary(BinOp::Mul, Box::new(flushed), Box::new(Expr::Var(1)));
assert!(
analyze_quadratic(×_x1).is_some(),
"a form scaled to nothing was refused as degree 3"
);
}
#[test]
fn cancellation_drops_the_term_and_the_degree_with_it() {
let zero = Expr::Binary(BinOp::Sub, Box::new(sq(0)), Box::new(sq(0)));
let h = analyze_quadratic(&zero).expect("degree-2 at worst");
assert!(h.is_empty());
let times_x1 = Expr::Binary(BinOp::Mul, Box::new(zero), Box::new(Expr::Var(1)));
assert!(analyze_quadratic(×_x1).is_some());
}
#[test]
fn an_exact_cancellation_is_not_a_lost_term() {
let zero = Expr::Binary(BinOp::Sub, Box::new(sq(0)), Box::new(sq(0)));
let q = recognize_expr(&zero).expect("degree-2 at worst");
assert!(q.quadratic().is_empty());
assert!(!q.inexact(), "an exact fold reported rounding");
assert!(
!q.lost_terms(),
"x0² − x0² was refused a fast path it is entitled to",
);
let big = (1u64 << 53) as f64;
let scaled = |c: f64| Expr::Binary(BinOp::Mul, Box::new(Expr::Const(c)), Box::new(sq(0)));
let absorbing = Expr::Sum(vec![scaled(big), sq(0), scaled(-big)]);
let q = recognize_expr(&absorbing).expect("degree-2 at worst");
assert!(q.quadratic().is_empty());
assert!(q.inexact(), "the absorbing add was not seen to round");
assert!(
q.lost_terms(),
"a body whose x0² was absorbed was reported complete",
);
}
#[test]
fn the_inexact_fold_is_flagged_before_anything_drops() {
let big = (1u64 << 53) as f64;
let scaled = |c: f64| Expr::Binary(BinOp::Mul, Box::new(Expr::Const(c)), Box::new(sq(0)));
let e = Expr::Binary(BinOp::Add, Box::new(scaled(big)), Box::new(sq(0)));
let q = recognize_expr(&e).expect("degree-2");
assert_eq!(q.quadratic().get(&(0, 0)), Some(&big));
assert!(q.inexact(), "fl(2⁵³ + 1) = 2⁵³ was called exact");
assert!(!q.lost_terms());
}
#[test]
fn rounding_carried_from_a_subexpression_makes_a_later_drop_a_loss() {
let big = (1u64 << 53) as f64;
let scaled =
|c: f64| Expr::Binary(BinOp::Mul, Box::new(Expr::Const(c)), Box::new(Expr::Var(0)));
let absorbed = Expr::Binary(BinOp::Add, Box::new(scaled(big)), Box::new(Expr::Var(0)));
let e = Expr::Binary(BinOp::Sub, Box::new(absorbed), Box::new(scaled(big)));
let q = recognize_expr(&e).expect("degree-1 at worst");
assert!(q.linear().is_empty());
assert!(
q.lost_terms(),
"the x0 absorbed two additions ago is no less missing now",
);
}
#[test]
fn an_exact_cancellation_in_a_product_is_not_a_lost_term() {
let add = |a: Expr, b: Expr| Expr::Binary(BinOp::Add, Box::new(a), Box::new(b));
let sub = |a: Expr, b: Expr| Expr::Binary(BinOp::Sub, Box::new(a), Box::new(b));
let e = Expr::Binary(
BinOp::Mul,
Box::new(add(Expr::Var(0), Expr::Var(1))),
Box::new(sub(Expr::Var(0), Expr::Var(1))),
);
let q = recognize_expr(&e).expect("degree-2");
assert_eq!(q.quadratic().get(&(0, 0)), Some(&1.0));
assert_eq!(q.quadratic().get(&(1, 1)), Some(&-1.0));
assert_eq!(q.quadratic().get(&(0, 1)), None);
assert!(!q.lost_terms(), "x0² − x1² was reported incomplete");
}
#[test]
fn an_underflowing_product_is_still_a_lost_term() {
let tiny = |i: usize| {
Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(1e-200)),
Box::new(Expr::Var(i)),
)
};
let e = Expr::Binary(BinOp::Mul, Box::new(tiny(0)), Box::new(tiny(0)));
let q = recognize_expr(&e).expect("degree-2 at worst");
assert!(q.quadratic().is_empty());
assert!(
q.lost_terms(),
"a coefficient that underflowed on the multiply was reported absent",
);
}
#[test]
fn an_exactly_cancelled_constant_is_not_a_lost_term() {
let e = Expr::Binary(
BinOp::Sub,
Box::new(Expr::Const(3.0)),
Box::new(Expr::Const(3.0)),
);
let q = recognize_expr(&e).expect("degree 0");
assert_eq!(q.as_constant(), Some(0.0));
assert!(!q.lost_terms());
let big = (1u64 << 53) as f64;
let absorbed = Expr::Binary(
BinOp::Add,
Box::new(Expr::Const(big)),
Box::new(Expr::Const(1.0)),
);
let e = Expr::Binary(BinOp::Sub, Box::new(absorbed), Box::new(Expr::Const(big)));
let q = recognize_expr(&e).expect("degree 0");
assert_eq!(q.as_constant(), Some(0.0));
assert!(q.lost_terms(), "an absorbed constant was reported absent");
}
#[test]
fn a_rounded_scale_is_inexact_but_loses_nothing() {
let e = Expr::Binary(BinOp::Div, Box::new(sq(0)), Box::new(Expr::Const(3.0)));
let q = recognize_expr(&e).expect("degree-2");
assert_eq!(q.quadratic().get(&(0, 0)), Some(&(1.0 / 3.0)));
assert!(q.inexact(), "1 · (1/3) was called exact");
assert!(
!q.lost_terms(),
"a rounded coefficient is not a missing one"
);
let e = Expr::Binary(BinOp::Div, Box::new(sq(0)), Box::new(Expr::Const(2.0)));
let q = recognize_expr(&e).expect("degree-2");
assert!(!q.inexact(), "x0²/2 rounds nothing");
}
#[test]
fn the_exactness_tests_agree_with_the_arithmetic() {
let big = (1u64 << 53) as f64;
assert!(add_is_exact(big, -big, big + -big));
assert!(add_is_exact(1.0, 2.0, 3.0));
assert!(!add_is_exact(big, 1.0, big + 1.0), "2⁵³ + 1 loses the 1");
assert!(!add_is_exact(0.1, 0.2, 0.1 + 0.2));
assert!(!add_is_exact(f64::INFINITY, 1.0, f64::INFINITY));
assert!(!add_is_exact(f64::NAN, 1.0, f64::NAN + 1.0));
assert!(mul_is_exact(3.0, 1.0, 3.0), "the ±1 shortcut");
assert!(mul_is_exact(3.0, 0.5, 1.5));
assert!(mul_is_exact(0.1, 4.0, 0.4));
assert!(!mul_is_exact(3.0, 1.0 / 3.0, 3.0 * (1.0 / 3.0)));
assert!(!mul_is_exact(1e-200, 1e-200, 1e-200 * 1e-200), "underflow");
assert!(!mul_is_exact(1e300, 1e300, 1e300 * 1e300), "overflow");
}
#[test]
fn cse_bodies_are_inlined_at_every_reference() {
let body = std::sync::Arc::new(Expr::Var(0));
let e = Expr::Binary(
BinOp::Mul,
Box::new(Expr::Cse(body.clone())),
Box::new(Expr::Cse(body)),
);
let h = analyze_quadratic(&e).expect("degree-2");
assert_eq!(h.get(&(0, 0)), Some(&2.0));
}
#[test]
fn a_wide_nary_sum_does_not_recurse() {
const N: usize = 5000;
let e = Expr::Sum((0..N).map(sq).collect());
let h = analyze_quadratic(&e).expect("sum of squares is a QP");
assert_eq!(h.len(), N);
assert_eq!(h.get(&(N - 1, N - 1)), Some(&2.0));
}
#[test]
fn a_repeated_monomial_in_an_nary_sum_accumulates_front_to_back() {
let scaled =
|c: f64, i: usize| Expr::Binary(BinOp::Mul, Box::new(Expr::Const(c)), Box::new(sq(i)));
let e = Expr::Sum(vec![scaled(1.0e16, 0), scaled(1.0, 0), scaled(1.0, 0)]);
let h = analyze_quadratic(&e).expect("sum of squares is a QP");
let got = h[&(0, 0)];
assert_eq!(
got.to_bits(),
(2.0 * 1.0e16_f64).to_bits(),
"expected the front-to-back fold, got {got:e}"
);
assert_ne!(got.to_bits(), (2.0 * (1.0e16_f64 + 2.0)).to_bits());
}
#[test]
fn only_already_expanded_forms_are_admitted() {
let monomial = |c: f64, i: usize, j: usize| {
Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(0.5)),
Box::new(Expr::Binary(
BinOp::Mul,
Box::new(Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(c)),
Box::new(Expr::Var(i)),
)),
Box::new(Expr::Var(j)),
)),
)
};
let row = Expr::Sum(vec![monomial(2.0, 0, 0), monomial(3.0, 0, 1)]);
assert!(is_expanded_quadratic(&row));
assert!(is_expanded_quadratic(&Expr::Sum(vec![sq(0), sq(1)])));
let chain = Expr::Binary(BinOp::Add, Box::new(sq(0)), Box::new(sq(1)));
assert!(is_expanded_quadratic(&chain));
assert!(is_expanded_quadratic(&Expr::Binary(
BinOp::Div,
Box::new(sq(0)),
Box::new(Expr::Const(3.0)),
)));
assert!(is_expanded_quadratic(&Expr::Unary(
UnaryOp::Neg,
Box::new(monomial(1.0, 0, 1))
)));
let diff = Expr::Binary(BinOp::Sub, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
let factored = Expr::Binary(BinOp::Pow, Box::new(diff), Box::new(Expr::Const(2.0)));
assert!(analyze_quadratic(&factored).is_some(), "it is quadratic");
assert!(
!is_expanded_quadratic(&factored),
"but not already expanded"
);
let product = Expr::Binary(
BinOp::Mul,
Box::new(Expr::Binary(
BinOp::Add,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(1.0)),
)),
Box::new(Expr::Var(1)),
);
assert!(!is_expanded_quadratic(&product));
let mixed = Expr::Sum(vec![monomial(1.0, 0, 0), factored]);
assert!(!is_expanded_quadratic(&mixed));
let once = Expr::Binary(
BinOp::Mul,
Box::new(Expr::Cse(std::sync::Arc::new(Expr::Var(0)))),
Box::new(Expr::Var(1)),
);
assert!(is_expanded_quadratic(&once));
}
#[test]
fn a_shared_cse_body_is_walked_once_and_admitted() {
let mut e = Expr::Var(0);
for _ in 0..60 {
let shared = std::sync::Arc::new(e);
e = Expr::Binary(
BinOp::Add,
Box::new(Expr::Cse(std::sync::Arc::clone(&shared))),
Box::new(Expr::Cse(shared)),
);
}
assert!(is_expanded_quadratic(&e));
let q = recognize_expr(&e).expect("a sum of one monomial is degree 1");
assert_eq!(q.linear().get(&0).copied(), Some(2.0_f64.powi(60)));
std::mem::forget(e);
}
#[test]
fn a_shared_body_is_judged_per_context_not_once_and_for_all() {
let sum = std::sync::Arc::new(Expr::Binary(
BinOp::Add,
Box::new(Expr::Var(0)),
Box::new(Expr::Var(1)),
));
let spine = Expr::Binary(
BinOp::Add,
Box::new(Expr::Cse(std::sync::Arc::clone(&sum))),
Box::new(Expr::Cse(std::sync::Arc::clone(&sum))),
);
assert!(is_expanded_quadratic(&spine));
let mixed = Expr::Binary(
BinOp::Add,
Box::new(Expr::Cse(std::sync::Arc::clone(&sum))),
Box::new(Expr::Binary(
BinOp::Mul,
Box::new(Expr::Cse(sum)),
Box::new(Expr::Var(1)),
)),
);
assert!(!is_expanded_quadratic(&mixed));
}
#[test]
fn the_expansion_gate_does_not_overflow_the_stack() {
const K: usize = 250_000;
let mut e = sq(0);
for i in 1..K {
e = Expr::Binary(BinOp::Add, Box::new(e), Box::new(sq(i)));
}
assert!(is_expanded_quadratic(&e));
std::mem::forget(e);
}
#[test]
fn deep_add_chain_does_not_overflow_the_stack() {
const K: usize = 250_000;
let mut e = sq(0);
for i in 1..K {
e = Expr::Binary(BinOp::Add, Box::new(e), Box::new(sq(i)));
}
let h = analyze_quadratic(&e).expect("a sum of squares is a QP at any depth");
assert_eq!(h.len(), K, "every xᵢ² contributes one diagonal entry");
assert_eq!(h.get(&(K - 1, K - 1)), Some(&2.0));
std::mem::forget(e);
}
#[test]
fn deep_right_leaning_chain_does_not_overflow_the_stack() {
const K: usize = 250_000;
let mut e = sq(K - 1);
for i in (0..K - 1).rev() {
e = Expr::Binary(BinOp::Add, Box::new(sq(i)), Box::new(e));
}
let h = analyze_quadratic(&e).expect("a sum of squares is a QP at any depth");
assert_eq!(h.len(), K);
std::mem::forget(e);
}
#[test]
fn deep_chain_with_a_transcendental_bails_without_overflowing() {
const K: usize = 250_000;
let mut e = Expr::Unary(UnaryOp::Sin, Box::new(Expr::Var(0)));
for i in 1..K {
e = Expr::Binary(BinOp::Add, Box::new(e), Box::new(sq(i)));
}
assert!(analyze_quadratic(&e).is_none());
std::mem::forget(e);
}
fn sq_of(base: Expr) -> Expr {
Expr::Binary(BinOp::Pow, Box::new(base), Box::new(Expr::Const(2.0)))
}
fn var_minus(i: usize, c: f64) -> Expr {
Expr::Binary(BinOp::Sub, Box::new(Expr::Var(i)), Box::new(Expr::Const(c)))
}
#[test]
fn a_shifted_square_is_kept_factored() {
let e = sq_of(var_minus(0, 500_000.0));
assert!(
!is_expanded_quadratic(&e),
"the expanded gate must refuse it"
);
let f = recognize_factored_quadratic(&e).expect("a square of an affine form");
assert_eq!(f.squares.len(), 1);
assert_eq!(f.squares[0].weight, 1.0);
assert_eq!(f.squares[0].coefs, vec![(0, 1.0)]);
assert_eq!(f.squares[0].constant, -500_000.0);
assert!(f.linear.is_empty());
assert_eq!(f.constant, 0.0);
}
#[test]
fn the_factored_read_out_does_not_cancel_where_the_expansion_does() {
let e = sq_of(var_minus(0, 500_000.0));
let x = 500_000.0 + 1e-4;
let r = x - 500_000.0;
let taped = r * r;
let (h, lin, c) = analyze_quadratic_full(&e).expect("degree 2");
let expanded = 0.5 * h[&(0, 0)] * x * x + lin[0].1 * x + c;
assert!(
(expanded - taped).abs() / taped > 1e-6,
"the expansion is supposed to cancel here, got {expanded} for {taped}"
);
let f = recognize_factored_quadratic(&e).expect("a square");
let t = &f.squares[0];
let l = t.constant + t.coefs[0].1 * x;
assert_eq!(t.weight * l * l, taped);
}
#[test]
fn a_sum_of_squared_differences_is_admitted() {
let diff = |i: usize, j: usize| {
Expr::Binary(BinOp::Sub, Box::new(Expr::Var(i)), Box::new(Expr::Var(j)))
};
let e = Expr::Binary(
BinOp::Add,
Box::new(sq_of(diff(0, 1))),
Box::new(sq_of(diff(2, 3))),
);
let f = recognize_factored_quadratic(&e).expect("two squares");
assert_eq!(f.squares.len(), 2);
let mut sup: Vec<Vec<(usize, f64)>> = f.squares.iter().map(|t| t.coefs.clone()).collect();
sup.sort_by_key(|c| c[0].0);
assert_eq!(
sup,
vec![vec![(0, 1.0), (1, -1.0)], vec![(2, 1.0), (3, -1.0)]]
);
assert!(
f.squares
.iter()
.all(|t| t.weight == 1.0 && t.constant == 0.0)
);
}
#[test]
fn spine_signs_and_constant_factors_fold_into_the_weight() {
let a = Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(3.0)),
Box::new(sq_of(var_minus(0, 1.0))),
);
let b = Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(0.5)),
Box::new(sq_of(Expr::Binary(
BinOp::Add,
Box::new(Expr::Var(1)),
Box::new(Expr::Const(2.0)),
))),
);
let f = recognize_factored_quadratic(&Expr::Binary(BinOp::Sub, Box::new(a), Box::new(b)))
.expect("two squares");
let mut got: Vec<(f64, f64)> = f.squares.iter().map(|t| (t.weight, t.constant)).collect();
got.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
assert_eq!(got, vec![(-0.5, 2.0), (3.0, -1.0)]);
}
#[test]
fn degree_one_leftovers_fold_into_the_linear_part() {
let e = Expr::Sum(vec![
sq_of(var_minus(0, 1.0)),
Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(3.0)),
Box::new(Expr::Var(1)),
),
Expr::Const(7.0),
]);
let f = recognize_factored_quadratic(&e).expect("a square plus leftovers");
assert_eq!(f.squares.len(), 1);
assert_eq!(f.linear, vec![(1, 3.0)]);
assert_eq!(f.constant, 7.0);
}
#[test]
fn a_diagonal_monomial_is_stored_as_a_square() {
let e = Expr::Binary(
BinOp::Add,
Box::new(sq_of(var_minus(0, 1.0))),
Box::new(Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(4.0)),
Box::new(Expr::Binary(
BinOp::Mul,
Box::new(Expr::Var(1)),
Box::new(Expr::Var(1)),
)),
)),
);
let f = recognize_factored_quadratic(&e).expect("square + diagonal monomial");
assert_eq!(f.squares.len(), 2);
let diag = f
.squares
.iter()
.find(|t| t.coefs == vec![(1, 1.0)])
.unwrap();
assert_eq!((diag.weight, diag.constant), (4.0, 0.0));
}
#[test]
fn a_cross_monomial_alongside_a_square_is_refused() {
let e = Expr::Binary(
BinOp::Add,
Box::new(sq_of(var_minus(0, 1.0))),
Box::new(Expr::Binary(
BinOp::Mul,
Box::new(Expr::Var(1)),
Box::new(Expr::Var(2)),
)),
);
assert!(recognize_factored_quadratic(&e).is_none());
}
#[test]
fn an_expanded_body_is_not_claimed_here() {
let e = Expr::Binary(BinOp::Add, Box::new(sq(0)), Box::new(sq(1)));
assert!(is_expanded_quadratic(&e));
assert!(recognize_factored_quadratic(&e).is_some());
let cross = Expr::Binary(BinOp::Mul, Box::new(Expr::Var(0)), Box::new(Expr::Var(1)));
assert!(recognize_factored_quadratic(&cross).is_none());
assert!(recognize_factored_quadratic(&Expr::Var(0)).is_none());
}
#[test]
fn a_product_of_two_different_affine_forms_is_refused() {
let e = Expr::Binary(
BinOp::Mul,
Box::new(var_minus(0, 1.0)),
Box::new(var_minus(1, 2.0)),
);
assert!(recognize_factored_quadratic(&e).is_none());
}
#[test]
fn quartics_and_transcendentals_are_refused() {
assert!(recognize_factored_quadratic(&sq_of(sq(0))).is_none());
let s = Expr::Unary(UnaryOp::Sin, Box::new(Expr::Var(0)));
assert!(recognize_factored_quadratic(&sq_of(s)).is_none());
}
#[test]
fn a_base_the_expanded_gate_refuses_is_refused_here_too() {
let inner = Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(2.0)),
Box::new(Expr::Binary(
BinOp::Add,
Box::new(Expr::Var(0)),
Box::new(Expr::Const(1.0)),
)),
);
assert!(!is_expanded_quadratic(&inner));
assert!(recognize_factored_quadratic(&sq_of(inner)).is_none());
}
#[test]
fn a_lost_leftover_refuses_the_body() {
let big = 9_007_199_254_740_992.0f64; let scaled = |c: f64, i: usize| {
Expr::Binary(BinOp::Mul, Box::new(Expr::Const(c)), Box::new(Expr::Var(i)))
};
let e = Expr::Sum(vec![
sq_of(var_minus(0, 1.0)),
scaled(big, 1),
Expr::Var(1),
scaled(-big, 1),
]);
assert!(recognize_factored_quadratic(&e).is_none());
}
#[test]
fn a_shared_body_on_the_spine_is_refused() {
let shared = std::sync::Arc::new(sq_of(var_minus(0, 1.0)));
let e = Expr::Binary(
BinOp::Add,
Box::new(Expr::Cse(shared.clone())),
Box::new(Expr::Cse(shared)),
);
assert!(recognize_factored_quadratic(&e).is_none());
}
#[test]
fn degenerate_weights_are_refused() {
let zero = Expr::Binary(
BinOp::Mul,
Box::new(Expr::Const(0.0)),
Box::new(sq_of(var_minus(0, 1.0))),
);
assert!(recognize_factored_quadratic(&zero).is_none());
let div0 = Expr::Binary(
BinOp::Div,
Box::new(sq_of(var_minus(0, 1.0))),
Box::new(Expr::Const(0.0)),
);
assert!(recognize_factored_quadratic(&div0).is_none());
}
#[test]
fn a_deep_chain_of_squares_does_not_overflow_the_stack() {
const K: usize = 250_000;
let mut e = sq_of(var_minus(0, 1.0));
for i in 1..K {
e = Expr::Binary(
BinOp::Add,
Box::new(e),
Box::new(sq_of(var_minus(i, i as f64))),
);
}
let f = recognize_factored_quadratic(&e).expect("K squares");
assert_eq!(f.squares.len(), K);
std::mem::forget(e);
}
}