use crate::equations::{Equations, State, Work};
use crate::lane::{Lane, DV};
use crate::prec::nan_min;
use multidouble::dd::DD;
use multidouble::field::{horner, Fl};
use multidouble::native::V8;
#[derive(Clone, Copy, Debug)]
pub struct Plan {
pub kt: usize,
pub tol: f64,
pub hmax: f64,
}
pub fn taylor_step<E: Equations<B>, T: Lane, const B: usize>(
eq: &E,
s: &[Fl<T, B>],
x0: f64,
x1: f64,
g: &[T],
w: &mut Work<T, B>,
) -> State<T, B> {
for (f, sf) in s.iter().enumerate().take(E::NF) {
w.c[f][0] = *sf;
}
eq.taylor_coeffs(x0, g, w);
let h = DD::from_f64(x1) - DD::from_f64(x0);
let kt = w.kt;
w.c.iter().map(|cf| horner(&cf[..=kt], h)).collect()
}
pub fn taylor_path<E: Equations<B>, T: Lane, const B: usize>(
eq: &E,
s0: &[Fl<T, B>],
xs: &[f64],
g: &[T],
w: &mut Work<T, B>,
) -> State<T, B> {
let mut s = s0.to_vec();
for i in 0..xs.len().saturating_sub(1) {
s = taylor_step(eq, &s, xs[i], xs[i + 1], g, w);
}
s
}
pub fn all_finite<const B: usize>(f: &Fl<DV, B>) -> bool {
let zero = V8::zero();
f.0.iter()
.all(|v| v.hi.sub(v.hi).eq(zero).bits() == 0xff && v.lo.sub(v.lo).eq(zero).bits() == 0xff)
}
pub fn coef_norm<const B: usize>(w: &Work<DV, B>, k: usize) -> f64 {
if w.c.iter().any(|cf| !all_finite(&cf[k])) {
return f64::NAN;
}
w.c.iter().map(|cf| cf[k].amax()).fold(0.0, f64::max)
}
fn pow2(n: i32) -> f64 {
debug_assert!((-1022..=1023).contains(&n));
f64::from_bits(((n + 1023) as u64) << 52)
}
fn scale2(x: f64, n: i32) -> f64 {
let h = n / 2;
x * pow2(h) * pow2(n - h)
}
fn ilog2(a: f64) -> i32 {
let (a, off) = if a < f64::MIN_POSITIVE {
(a * pow2(64), -64)
} else {
(a, 0)
};
((a.to_bits() >> 52) & 0x7ff) as i32 - 1023 + off
}
fn powi_dd(t: DD, n: u32) -> DD {
let (mut r, mut b, mut n) = (DD::ONE, t, n);
while n > 0 {
if n & 1 == 1 {
r = r * b;
}
b = b * b;
n >>= 1;
}
r
}
fn powi_f64(t: f64, n: u32) -> f64 {
(0..n).fold(1.0, |r, _| r * t)
}
pub fn kth_root(a: f64, k: u32) -> f64 {
assert!(a > 0.0 && a.is_finite() && k >= 1, "kth_root: a = {a:e}, k = {k}");
if k == 1 {
return a;
}
let q = ilog2(a).div_euclid(k as i32);
let b = scale2(a, -(k as i32) * q);
let (mut lo, mut hi) = (1.0f64, 2.0f64);
for _ in 0..12 {
let mid = 0.5 * (lo + hi);
if powi_f64(mid, k) > b {
hi = mid;
} else {
lo = mid;
}
}
let bd = DD::from_f64(b);
let kd = DD::from_f64(k as f64);
let mut t = DD::from_f64(0.5 * (lo + hi));
for _ in 0..5 {
t = (t.mul_f((k - 1) as f64) + bd / powi_dd(t, k - 1)) / kd;
}
let mut y = t.hi;
for _ in 0..3 {
let (up, dn) = (y.next_up(), y.next_down());
let above = (powi_dd(
DD {
hi: y,
lo: 0.5 * (up - y),
},
k,
) - bd)
.hi;
let below = (powi_dd(
DD {
hi: y,
lo: -0.5 * (y - dn),
},
k,
) - bd)
.hi;
if above <= 0.0 {
y = up;
} else if below > 0.0 {
y = dn;
} else {
break;
}
}
scale2(y, q)
}
pub fn step_size<const B: usize>(w: &Work<DV, B>, plan: &Plan) -> f64 {
let kt = w.kt;
let floor = |e: f64| if e.is_nan() { e } else { e.max(1e-300) };
let e1 = floor(coef_norm(w, kt - 1));
let e2 = floor(coef_norm(w, kt));
let root = |a: f64, k: usize| {
if a == 0.0 {
0.0
} else if a.is_finite() {
kth_root(a, k as u32)
} else if a.is_nan() {
a
} else {
f64::INFINITY
}
};
let h1 = root(plan.tol / e1, kt - 1);
let h2 = root(plan.tol / e2, kt);
nan_min(nan_min(h1, h2), plan.hmax)
}
pub fn plan_path<E: Equations<B>, const B: usize>(
eq: &E,
s0: &[Fl<DV, B>],
xa: f64,
xb: f64,
g: &[DV],
plan: &Plan,
) -> Vec<f64> {
assert!(
xa.is_finite() && xb.is_finite(),
"plan_path: ends {xa} and {xb} must be finite"
);
assert!(plan.kt >= 2, "plan_path: Taylor order {} < 2", plan.kt);
assert!(
plan.tol > 0.0 && plan.hmax > 0.0,
"plan_path: tol {:e} and hmax {:e} must be positive",
plan.tol,
plan.hmax
);
let mut w = Work::<DV, B>::new(E::NF, E::NAUX, plan.kt);
let mut xs = vec![xa];
let mut x = xa;
let mut s = s0.to_vec();
let dir = if xb > xa { 1.0 } else { -1.0 };
while (xb - x) * dir > 0.0 {
for (f, sf) in s.iter().enumerate() {
w.c[f][0] = *sf;
}
eq.taylor_coeffs(x, g, &mut w);
for (f, cf) in w.c.iter().enumerate() {
if let Some(k) = cf[..=plan.kt].iter().position(|c| !all_finite(c)) {
panic!("plan_path: Taylor coefficient {k} of field {f} at x = {x} is not finite");
}
}
let mut h = step_size(&w, plan);
assert!(h > 0.0, "plan_path: step {h:e} at x = {x}");
let rest = (xb - x) * dir;
if h >= rest {
h = rest;
} else if h > 0.6 * rest {
h = 0.5 * rest;
}
let xn = if h == rest { xb } else { x + dir * h };
assert!(xn != x, "plan_path: step {h:e} does not move x = {x}");
let hd = DD::from_f64(xn) - DD::from_f64(x);
s = w.c.iter().map(|cf| horner(&cf[..=plan.kt], hd)).collect();
x = xn;
xs.push(x);
}
xs
}
#[cfg(test)]
mod tests {
use super::*;
use multidouble::qd::QD;
fn sample() -> Vec<(f64, u32)> {
let mut v = Vec::new();
for e in [-1000, -500, -77, -30, -1, 0, 1, 52, 300, 900] {
for j in [0.0, 1.0, 13.0, 50.0, 96.0] {
for k in [1u32, 2, 3, 7, 19, 29, 35, 36, 60] {
v.push((scale2(1.0 + j / 97.0, e), k));
}
}
}
v
}
fn powi_qd(q: QD, k: u32) -> QD {
(0..k).fold(QD::from_dd(DD::ONE), |r, _| r * q)
}
fn correctly_rounded(y: f64, a: f64, k: u32) -> bool {
let (up, dn) = (y.next_up(), y.next_down());
let hi = QD::new(y, 0.5 * (up - y), 0.0, 0.0);
let lo = QD::new(y, -0.5 * (y - dn), 0.0, 0.0);
let aq = QD::from_dd(DD::from_f64(a));
(powi_qd(lo, k) - aq).to_dd().hi <= 0.0 && (powi_qd(hi, k) - aq).to_dd().hi > 0.0
}
#[test]
fn kth_root_is_correctly_rounded() {
for (a, k) in sample() {
let y = kth_root(a, k);
assert!(correctly_rounded(y, a, k), "a = {a:e}, k = {k}: {y:e}");
}
}
#[test]
fn kth_root_control_neighbours_fail() {
for (a, k) in sample() {
let y = kth_root(a, k);
assert!(!correctly_rounded(y.next_up(), a, k) && !correctly_rounded(y.next_down(), a, k));
}
}
#[test]
fn kth_root_agrees_with_powf() {
let mut worst = 0.0f64;
for (a, k) in sample() {
let y = kth_root(a, k);
let p = a.powf(1.0 / k as f64);
let ulp = y.next_up() - y;
let bound = ulp + y * (ilog2(a).abs() + 1) as f64 * std::f64::consts::LN_2 * 2f64.powi(-53) / k as f64;
worst = worst.max((y - p).abs() / ulp);
assert!((y - p).abs() <= bound, "a = {a:e}, k = {k}: {y:e} against powf {p:e}");
}
println!("largest |kth_root - powf| {worst:.1} ulp");
}
#[test]
fn kth_root_follows_its_input() {
for (a, k) in sample() {
let mut b = a;
for _ in 0..(if k == 1 { 1 } else { 2 * k }) {
b = b.next_up();
}
assert_ne!(kth_root(b, k), kth_root(a, k), "a = {a:e}, k = {k}");
}
}
}