mod common;
use common::{perturbed, Fnv, Planted};
use multishoot::prec::{max_abs, nan_max, parts, widen};
use multishoot::{Double, Native, NewtonOpts, Octo, Precision, Quad, Shooting};
const B: usize = 2;
const NODES: [f64; 5] = [0.0, 0.5, 1.0, 1.5, 2.0];
fn planted() -> (Shooting<Planted<B>, B>, Vec<Vec<f64>>) {
Planted::<B>::shooting(&NODES, 30, 1e-32)
}
fn opts(tol: f64, max_iter: usize) -> NewtonOpts {
NewtonOpts {
tol,
max_iter,
..Default::default()
}
}
fn sci(v: &[f64]) -> String {
v.iter().map(|x| format!("{x:.2e}")).collect::<Vec<_>>().join(" ")
}
fn state_diff<P: Precision<Native>>(a: &[Vec<f64>], b: &[Vec<f64>]) -> f64 {
let pick = |z: &[Vec<f64>], i: usize| -> P::Scalar {
let p: Vec<f64> = z.iter().map(|q| q[i]).collect();
P::new(&p)
};
let d: Vec<P::Scalar> = (0..a[0].len()).map(|i| pick(a, i) - pick(b, i)).collect();
max_abs::<P>(&d)
}
fn chain(sh: &Shooting<Planted<B>, B>, zs: &[Vec<f64>]) -> [multishoot::NewtonReport; 3] {
let mut z = perturbed(zs, 1e-3);
let dd = sh.newton::<Double>(&mut z, &opts(1e-28, 10)).unwrap();
let mut zq = widen(&z, 4);
let qd = sh.newton::<Quad>(&mut zq, &opts(1e-60, 4)).unwrap();
let mut zo = widen(&zq, 8);
let od = sh.newton::<Octo>(&mut zo, &opts(1e-110, 4)).unwrap();
println!(
"max |F|: dd {}, qd {}, od {}; max |z_od - z*| {:.3e}",
sci(&dd.fnorm),
sci(&qd.fnorm),
sci(&od.fnorm),
state_diff::<Octo>(&zo, &widen(zs, 8))
);
assert!(
state_diff::<Octo>(&zo, &widen(zs, 8)) < 1e-27,
"the octo-double root is not z*"
);
[dd, qd, od]
}
#[test]
fn planted_converges_in_quad_and_octo() {
let (sh, zs) = planted();
let [dd, qd, od] = chain(&sh, &zs);
assert!(dd.converged, "double-double: {:?}", dd.fnorm);
assert!(
qd.converged && *qd.fnorm.last().unwrap() < 1e-60,
"quad-double: {:?}",
qd.fnorm
);
assert!(
od.converged && *od.fnorm.last().unwrap() < 1e-110,
"octo-double: {:?}",
od.fnorm
);
}
#[test]
fn precisions_agree_on_the_residual() {
let (sh, zs) = planted();
let z = perturbed(&zs, 1e-3);
let fd = sh.residual::<Double>(&parts(&z), true).unwrap();
let fq = sh.residual::<Quad>(&parts(&widen(&z, 4)), true).unwrap();
let fo = sh.residual::<Octo>(&parts(&widen(&z, 8)), true).unwrap();
let qo: Vec<_> = fq
.iter()
.zip(&fo)
.map(|(q, o)| <Octo as Precision<Native>>::new(&q.x) - *o)
.collect();
let qo = max_abs::<Octo>(&qo);
let dq = fd
.iter()
.zip(&fq)
.map(|(d, q)| (*d - q.to_dd()).hi.abs())
.fold(0.0, nan_max);
println!("|F_qd - F_od| = {qo:.3e}, |F_dd - F_qd| = {dq:.3e}");
assert!(qo < 1e-60 && dq < 1e-29, "{qo:e} {dq:e}");
}
fn fnv<P: Precision<Native>>(f: &[P::Scalar]) -> u64 {
let mut h = Fnv::new();
for &v in f {
for q in 0..P::PARTS {
h.f64(P::part(v, q));
}
}
h.0
}
fn full_parts_z(zs: &[Vec<f64>]) -> Vec<Vec<f64>> {
let z = perturbed(zs, 1e-3);
let l = z[0].len();
let mut out: Vec<Vec<f64>> = (0..8).map(|_| vec![0.0; l]).collect();
for i in 0..l {
let x = <Octo as Precision<Native>>::new(&[z[0][i]]);
let r = <Octo as Precision<Native>>::ONE
+ <Octo as Precision<Native>>::new(&[1e-3]) * <Octo as Precision<Native>>::new(&[1.0 / (3.0 + i as f64)]);
let v = x * r * r;
for (q, o) in out.iter_mut().enumerate() {
o[i] = <Octo as Precision<Native>>::part(v, q);
}
}
out
}
fn hashes(sh: &Shooting<Planted<B>, B>, z8: &[Vec<f64>]) -> [u64; 4] {
let p = parts(z8);
let fd = sh.residual::<Double>(&p[..2], true).unwrap();
let fq = sh.residual::<Quad>(&p[..4], true).unwrap();
let fo = sh.residual::<Octo>(&p, true).unwrap();
let st = sh.newton_step::<Quad>(&p[..4], true).unwrap();
let dz: Vec<_> = (0..sh.len())
.map(|i| <Quad as Precision<Native>>::from_raw(&[st.dz[0][i], st.dz[1][i], st.dz[2][i], st.dz[3][i]]))
.collect();
[fnv::<Double>(&fd), fnv::<Quad>(&fq), fnv::<Octo>(&fo), fnv::<Quad>(&dz)]
}
const REFERENCE: [u64; 4] = [
0xe6e87e1b5a179335,
0x71f836f4c615fda7,
0x2a443f3e943b018b,
0x8d90a6bb492d64e3,
];
#[test]
fn residual_bytes_match_reference() {
let (sh, zs) = planted();
let h = hashes(&sh, &full_parts_z(&zs));
println!(
"hashes [{}]",
h.iter().map(|v| format!("{v:#018x}")).collect::<Vec<_>>().join(", ")
);
assert_eq!(h, REFERENCE, "residual bytes differ from the reference");
}
#[test]
fn residual_bytes_control_one_ulp_differs() {
let (sh, zs) = planted();
let z = full_parts_z(&zs);
let base = hashes(&sh, &z);
let i = sh.off_node(1) + 3;
for q in [0usize, 1, 3] {
let mut zm = z.clone();
zm[q][i] = f64::from_bits(zm[q][i].to_bits() + 1);
let h = hashes(&sh, &zm);
for (k, parts) in [2usize, 4, 8, 4].iter().enumerate() {
if q < *parts {
assert_ne!(h[k], base[k], "hash {k} unchanged by part {q}");
}
}
}
}
#[cfg(feature = "mutate")]
mod controls {
use super::*;
use multishoot::control::Control;
#[test]
fn half_parts_stall_above_the_floors() {
let (mut sh, zs) = planted();
let mut z = perturbed(&zs, 1e-3);
sh.newton::<Double>(&mut z, &opts(1e-28, 10)).unwrap();
sh.set_control(Control::HalfParts);
let mut zq = widen(&z, 4);
let qd = sh.newton::<Quad>(&mut zq, &opts(1e-60, 4)).unwrap();
sh.set_control(Control::None);
let mut zq = widen(&z, 4);
sh.newton::<Quad>(&mut zq, &opts(1e-60, 4)).unwrap();
sh.set_control(Control::HalfParts);
let mut zo = widen(&zq, 8);
let od = sh.newton::<Octo>(&mut zo, &opts(1e-110, 4)).unwrap();
println!("control, half parts: qd {}, od {}", sci(&qd.fnorm), sci(&od.fnorm));
assert!(!qd.converged && !od.converged, "control did not fire");
}
}