#![allow(clippy::needless_range_loop)]
mod common;
use common::{Rng, U2};
use multidouble::dd::{two_prod, two_sum, DD};
use multidouble::exact::Dy;
use multidouble::linalg::{dd_axpy, ld_for, DDLu};
use multidouble::native::{DotAcc, DV};
const N: usize = 20_000;
fn exact(x: DD) -> Dy {
Dy::from_f64(x.hi).add(&Dy::from_f64(x.lo))
}
fn err_u2(got: DD, want: &Dy, scale: &Dy) -> f64 {
exact(got).sub(want).ratio(scale) / U2
}
fn normalized(x: DD) -> bool {
x.hi + x.lo == x.hi
}
fn dd_in(r: &mut Rng, emin: i32, emax: i32) -> DD {
let e = emin + (r.next_u64() % (emax - emin + 1) as u64) as i32;
let hi = (1.0 + r.unit()) * 2f64.powi(e) * if r.next_u64() & 1 == 0 { 1.0 } else { -1.0 };
let lo = r.sym() * hi.abs() * 2f64.powi(-53);
DD::new(hi, lo)
}
fn axpy_no_cross(a: DD, l: DD, nt: DD) -> DD {
let (p, pe) = two_prod(l.hi, nt.hi);
let pe = l.hi.mul_add(nt.lo, pe);
let (s, t) = two_sum(a.hi, p);
let e = a.lo + (t + pe);
let (h, lo) = two_sum(s, e);
DD { hi: h, lo }
}
fn axpy_worst(f: fn(DD, DD, DD) -> DD, seed: u64) -> f64 {
let mut r = Rng(seed);
let mut worst: f64 = 0.0;
for i in 0..N {
let (l, t) = (r.dd(20), r.dd(20));
let lt = exact(l).mul(&exact(t));
let a = if i % 4 == 0 {
let p = -(l * t);
DD::new(p.hi, p.lo + r.sym() * p.hi.abs() * 2f64.powi(-60))
} else {
r.dd(20)
};
let got = f(a, l, t);
assert!(normalized(got), "dd_axpy({a:?}, {l:?}, {t:?}) = {got:?}");
let want = exact(a).add(<);
worst = worst.max(err_u2(got, &want, &exact(a).abs().add(<.abs())));
}
worst
}
#[test]
fn dd_axpy_within_bound() {
let worst = axpy_worst(dd_axpy, 101);
println!("dd_axpy: worst {worst:.3} u^2 of |a| + |l t| (bound 7)");
assert!(worst <= 7.0, "dd_axpy: {worst} u^2");
let broken = axpy_worst(axpy_no_cross, 101);
assert!(
broken > 7.0,
"control: dd_axpy without the cross term passes ({broken} u^2)"
);
}
fn dot_worst(len: usize, cat: &str, seed: u64, drop_e: bool) -> f64 {
let mut r = Rng(seed);
let mut worst: f64 = 0.0;
for _ in 0..(4096 / len).max(16) {
let mut a = vec![[DD::ZERO; 8]; len];
let mut b = vec![[DD::ZERO; 8]; len];
for lane in 0..8 {
for i in 0..len {
let (x, y) = match cat {
"wide" => (dd_in(&mut r, -60, 60), dd_in(&mut r, -60, 60)),
"cancel" if i >= len / 2 && len > 1 => {
let k = i - len / 2;
let ak = a[k][lane];
let d = DD::from_f64(1.0 + r.sym() * 2f64.powi(-30));
(-(ak * d), b[k][lane])
}
_ => (dd_in(&mut r, -3, 3), dd_in(&mut r, -3, 3)),
};
a[i][lane] = x;
b[i][lane] = y;
}
}
let lanes = |v: &[DD; 8]| DV::load(&v.map(|d| d.hi), &v.map(|d| d.lo));
let mut acc = DotAcc::zero();
for i in 0..len {
acc = acc.add_prod(lanes(&a[i]), lanes(&b[i]));
}
let got = if drop_e {
DV {
hi: acc.s,
lo: multidouble::native::V8::zero(),
}
} else {
acc.finish()
};
for lane in 0..8 {
let (mut s, mut m) = (Dy::zero(), Dy::zero());
for i in 0..len {
let t = exact(a[i][lane]).mul(&exact(b[i][lane]));
m = m.add(&t.abs());
s = s.add(&t);
}
let g = got.lane(lane);
if !drop_e {
assert!(normalized(g), "DotAcc len {len} {cat}: {g:?} not normalized");
}
worst = worst.max(err_u2(g, &s, &m));
}
}
worst
}
#[test]
fn dot_acc_within_bound() {
for (ci, cat) in ["random", "wide", "cancel"].into_iter().enumerate() {
for len in [1usize, 2, 7, 16, 64, 256] {
let seed = 200 + 10 * ci as u64 + len as u64;
let thr = len as f64 + 4.0;
let worst = dot_worst(len, cat, seed, false);
println!("DotAcc {cat:6} len {len:3}: worst {worst:.3} u^2 of sum |a b| (bound {thr})");
assert!(worst <= thr, "DotAcc {cat} len {len}: {worst} u^2 (bound {thr})");
let broken = dot_worst(len, cat, seed, true);
assert!(
broken > thr,
"control: DotAcc without e passes {cat} len {len} ({broken} u^2)"
);
}
}
}
#[test]
fn recip_within_bound() {
let mut r = Rng(301);
let mut worst: f64 = 0.0;
let mut caught = false;
for _ in 0..N {
let b = r.dd(20);
let q = b.recip();
assert!(normalized(q));
let one = Dy::from_f64(1.0);
let e = exact(q).mul(&exact(b)).sub(&one).ratio(&one) / U2;
worst = worst.max(e);
assert!(e <= 8.0, "recip({b:?}): {e} u^2");
let c = DD::from_f64(1.0 / b.hi);
caught |= exact(c).mul(&exact(b)).sub(&one).ratio(&one) / U2 > 8.0;
}
println!("recip: worst {worst:.3} u^2 (bound 8)");
assert!(caught, "control: the f64 reciprocal passes the recip bound");
}
#[test]
fn mul_pwr2_is_exact() {
let mut r = Rng(302);
for _ in 0..N {
let x = r.dd(100);
let k = (r.next_u64() % 401) as i32 - 200;
let p = 2f64.powi(k);
let got = x.mul_pwr2(p);
let want = exact(x).mul(&Dy::from_f64(p));
assert!(exact(got).sub(&want).is_zero(), "{x:?} * 2^{k} = {got:?}");
assert_eq!(got.hi, x.hi * p);
assert_eq!(got.lo, x.lo * p);
}
let x = DD::new(1.5, 2f64.powi(-60));
let off = DD::new(x.hi * 4.0, (x.lo * 4.0) * (1.0 + f64::EPSILON));
assert!(!exact(off).sub(&exact(x).mul(&Dy::from_f64(4.0))).is_zero());
}
#[test]
fn bounds_hold_over_the_exponent_range() {
let mut r = Rng(401);
let (mut wa, mut wm, mut wd) = (0f64, 0f64, 0f64);
let mut caught = [false; 3];
for i in 0..N {
let a = dd_in(&mut r, -960, 1000);
let b = if i % 4 == 0 {
DD::new(-a.hi, r.sym() * a.hi.abs() * 2f64.powi(-60))
} else {
let e = a.hi.abs().log2() as i32;
dd_in(&mut r, (e - 60).max(-960), (e + 60).min(1000))
};
let want = exact(a).add(&exact(b));
let got = a + b;
assert!(normalized(got));
let e = err_u2(got, &want, &want);
wa = wa.max(e);
assert!(e <= 3.0, "{a:?} + {b:?}: {e} u^2");
caught[0] |= err_u2(DD::from_f64(a.hi + b.hi), &want, &want) > 3.0;
let (a, b) = (dd_in(&mut r, -450, 450), dd_in(&mut r, -450, 450));
let want = exact(a).mul(&exact(b));
let got = a * b;
assert!(normalized(got));
let e = err_u2(got, &want, &want);
wm = wm.max(e);
assert!(e <= 5.0, "{a:?} * {b:?}: {e} u^2");
caught[1] |= err_u2(DD::from_f64(a.hi * b.hi), &want, &want) > 5.0;
let q = a / b;
assert!(normalized(q));
let res = |q: DD| exact(q).mul(&exact(b)).sub(&exact(a)).ratio(&exact(a)) / U2;
let e = res(q);
wd = wd.max(e);
assert!(e <= 8.0, "{a:?} / {b:?}: {e} u^2");
caught[2] |= res(DD::from_f64(a.hi / b.hi)) > 8.0;
}
println!("exponent range: add/sub {wa:.3}, mul {wm:.3}, div {wd:.3} u^2 (bounds 3, 5, 8)");
assert_eq!(caught, [true; 3], "controls (f64 add, mul, div) pass the bounds");
}
fn dd_matrix(n: usize, mut entry: impl FnMut(usize, usize) -> DD) -> (usize, Vec<f64>, Vec<f64>) {
let ld = ld_for(n);
let (mut hi, mut lo) = (vec![0.0; ld * n], vec![0.0; ld * n]);
for j in 0..n {
for i in 0..n {
let v = entry(i, j);
hi[j * ld + i] = v.hi;
lo[j * ld + i] = v.lo;
}
}
(ld, hi, lo)
}
fn backward_error(n: usize, ld: usize, hi: &[f64], lo: &[f64], x: &[DD], b: &[DD]) -> f64 {
let mut worst: f64 = 0.0;
for i in 0..n {
let (mut res, mut scale) = (exact(b[i]), exact(b[i]).abs());
for j in 0..n {
let t = exact(DD::new(hi[j * ld + i], lo[j * ld + i])).mul(&exact(x[j]));
scale = scale.add(&t.abs());
res = res.sub(&t);
}
worst = worst.max(res.ratio(&scale));
}
worst
}
#[test]
fn solve_is_backward_stable() {
let mut r = Rng(501);
for n in [3usize, 8, 31, 64, 100] {
let (ld, hi, lo) = dd_matrix(n, |_, _| DD::new(r.sym(), r.sym() * 1e-17));
let b: Vec<DD> = (0..n).map(|_| DD::new(r.sym(), r.sym() * 1e-17)).collect();
let lu = DDLu::factor(n, ld, hi.clone(), lo.clone(), false).unwrap();
let mut x = b.clone();
lu.solve(&mut x);
let e = backward_error(n, ld, &hi, &lo, &x, &b) / U2;
println!("solve n {n:3}: backward error {e:.3} u^2 (bound n = {n})");
assert!(e <= n as f64, "n {n}: backward error {e} u^2");
let mut bad = lu.clone();
let k = n / 2;
bad.lo[k * ld + k] += bad.hi[k * ld + k].abs() * 2f64.powi(-80);
let mut x = b.clone();
bad.solve(&mut x);
let e = backward_error(n, ld, &hi, &lo, &x, &b) / U2;
assert!(e > n as f64, "control: perturbed pivot passes n {n} ({e} u^2)");
}
}
#[test]
fn hilbert_forward_error_within_cond_bound() {
let mut r = Rng(502);
for n in [2usize, 4, 6, 8, 10, 12, 13] {
let (ld, hi, lo) = dd_matrix(n, |i, j| DD::ONE / DD::from_f64((i + j + 1) as f64));
let lu = DDLu::factor(n, ld, hi.clone(), lo.clone(), false).unwrap();
let a_norm = (0..n)
.map(|i| (0..n).map(|j| (hi[j * ld + i] + lo[j * ld + i]).abs()).sum::<f64>())
.fold(0.0, f64::max);
let mut rows = vec![0.0f64; n];
for j in 0..n {
let mut e = vec![DD::ZERO; n];
e[j] = DD::ONE;
lu.solve(&mut e);
for i in 0..n {
rows[i] += e[i].to_f64().abs();
}
}
let cond = a_norm * rows.iter().fold(0.0f64, |m, &v| m.max(v));
let x: Vec<DD> = (0..n).map(|_| DD::new(r.sym(), r.sym() * 1e-17)).collect();
let b: Vec<DD> = (0..n)
.map(|i| {
let mut s = Dy::zero();
for j in 0..n {
s = s.add(&exact(DD::new(hi[j * ld + i], lo[j * ld + i])).mul(&exact(x[j])));
}
let h = s.to_f64();
DD::new(h, s.sub(&Dy::from_f64(h)).to_f64())
})
.collect();
let fwd = |lu: &DDLu| {
let mut y = b.clone();
lu.solve(&mut y);
let xn = x.iter().map(|v| v.hi.abs()).fold(0.0, f64::max);
(0..n).map(|i| (y[i] - x[i]).to_f64().abs()).fold(0.0, f64::max) / xn
};
let bound = n as f64 * cond * U2;
let e = fwd(&lu);
println!(
"Hilbert n {n:2}: cond {cond:.2e}, forward error {e:.2e} = {:.3} of the bound n cond u^2",
e / bound
);
assert!(e <= bound, "Hilbert n {n}: forward error {e:e} above {bound:e}");
if bound < 1e-22 {
let mut bad = lu.clone();
let k = n / 2;
bad.lo[k * ld + k] += bad.hi[k * ld + k].abs() * 2f64.powi(-60);
let e = fwd(&bad);
assert!(e > bound, "control: perturbed pivot passes Hilbert n {n} ({e:e})");
}
}
}