use core::cmp::Ordering;
use dashu_base::Approximation::*;
use crate::{
fbig::FBig,
repr::{Context, Repr},
round::ErrorBounds,
round::Rounded,
};
use dashu_int::Word;
const MAX_ZIV_RETRIES: usize = 32;
#[cfg(all(test, feature = "std"))]
thread_local! {
pub(crate) static LAST_ZIV_RETRIES: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
}
impl<R: ErrorBounds> Context<R> {
pub(crate) fn ziv<const B: Word>(
&self,
initial_guard: usize,
mut approx: impl FnMut(usize) -> (FBig<R, B>, FBig<R, B>),
) -> Rounded<FBig<R, B>> {
if !self.is_limited() {
let (value, _err) = approx(0);
return Exact(value);
}
let mut guard = initial_guard;
let mut last = None;
#[cfg(all(test, feature = "std"))]
LAST_ZIV_RETRIES.with(|c| c.set(0));
for _ in 0..MAX_ZIV_RETRIES {
let (a, e) = approx(guard);
let candidate = a.clone().with_precision(self.precision);
if Self::contained::<B>(&a.repr, &e.repr, candidate.value_ref()) {
return candidate;
}
last = Some(candidate);
let step = core::cmp::max(guard, self.precision / 2).max(1);
guard += step;
#[cfg(all(test, feature = "std"))]
LAST_ZIV_RETRIES.with(|c| c.set(c.get() + 1));
}
last.expect("MAX_ZIV_RETRIES is non-zero")
}
pub(crate) fn ziv_pair<const B: Word>(
&self,
initial_guard: usize,
mut approx: impl FnMut(usize) -> ((FBig<R, B>, FBig<R, B>), (FBig<R, B>, FBig<R, B>)),
) -> (Rounded<FBig<R, B>>, Rounded<FBig<R, B>>) {
if !self.is_limited() {
let ((v1, _), (v2, _)) = approx(0);
return (Exact(v1), Exact(v2));
}
let mut guard = initial_guard;
let mut last = None;
#[cfg(all(test, feature = "std"))]
LAST_ZIV_RETRIES.with(|c| c.set(0));
for _ in 0..MAX_ZIV_RETRIES {
let ((a1, e1), (a2, e2)) = approx(guard);
let c1 = a1.clone().with_precision(self.precision);
let c2 = a2.clone().with_precision(self.precision);
if Self::contained::<B>(&a1.repr, &e1.repr, c1.value_ref())
&& Self::contained::<B>(&a2.repr, &e2.repr, c2.value_ref())
{
return (c1, c2);
}
last = Some((c1, c2));
let step = core::cmp::max(guard, self.precision / 2).max(1);
guard += step;
#[cfg(all(test, feature = "std"))]
LAST_ZIV_RETRIES.with(|c| c.set(c.get() + 1));
}
last.expect("MAX_ZIV_RETRIES is non-zero")
}
fn contained<const B: Word>(a: &Repr<B>, e: &Repr<B>, y: &FBig<R, B>) -> bool {
let (lb, rb, incl_l, incl_r) = R::error_bounds::<B>(y);
let y = &y.repr;
let lb = lb.into_repr();
let rb = rb.into_repr();
let left = (a + &lb).cmp(&(y + e));
let right = (y + &rb).cmp(&(a + e));
let left_ok = if incl_l {
left != Ordering::Less
} else {
left == Ordering::Greater
};
let right_ok = if incl_r {
right != Ordering::Less
} else {
right == Ordering::Greater
};
left_ok && right_ok
}
}
#[cfg(all(test, feature = "std"))]
mod tests {
use super::*;
use crate::round::mode;
type F = crate::FBig<mode::HalfEven>;
#[test]
fn ziv_accepts_exact_first_attempt() {
let ctx: Context<mode::HalfEven> = Context::new(10);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let r = ctx.ziv(4, |_| (F::ONE, F::ZERO));
assert!(matches!(r, Exact(_)));
assert_eq!(LAST_ZIV_RETRIES.with(|c| c.get()), 0);
}
#[test]
fn ziv_retries_until_contained() {
let ctx: Context<mode::HalfEven> = Context::new(4);
let r = ctx.ziv(2, |guard| {
(F::ONE, F::ONE >> guard as isize)
});
let _ = r.value();
assert!(LAST_ZIV_RETRIES.with(|c| c.get()) >= 1);
}
#[test]
fn ziv_unlimited_short_circuits() {
let ctx: Context<mode::HalfEven> = Context::new(0);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let r = ctx.ziv(4, |_| (F::from(7u8), F::ZERO));
assert!(matches!(r, Exact(_)));
assert_eq!(LAST_ZIV_RETRIES.with(|c| c.get()), usize::MAX);
}
#[test]
fn ziv_pair_accepts_exact_first_attempt() {
let ctx: Context<mode::HalfEven> = Context::new(10);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let (r1, r2) = ctx.ziv_pair(4, |_| ((F::ONE, F::ZERO), (F::from(2u8), F::ZERO)));
assert!(matches!(r1, Exact(_)));
assert!(matches!(r2, Exact(_)));
assert_eq!(LAST_ZIV_RETRIES.with(|c| c.get()), 0);
}
#[test]
fn ziv_pair_retries_until_both_contained() {
let ctx: Context<mode::HalfEven> = Context::new(4);
let (r1, r2) = ctx.ziv_pair(2, |guard| {
let radius = F::ONE >> guard as isize;
((F::ONE, F::ZERO), (F::ONE, radius))
});
let _ = (r1.value(), r2.value());
assert!(LAST_ZIV_RETRIES.with(|c| c.get()) >= 1);
}
#[test]
fn ziv_few_retries_for_typical_inputs() {
let cases = [
F::try_from(0.5f64).unwrap(),
F::try_from(1.5f64).unwrap(),
F::try_from(2.0f64).unwrap(),
F::try_from(10.0f64).unwrap(),
F::try_from(1000.0f64).unwrap(),
F::try_from(1e-6f64).unwrap(),
];
const MAX_RETRIES: usize = 1;
for p in [10usize, 24, 53, 100, 200] {
for x in &cases {
let x = x.clone().with_precision(p).value();
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let _ = x.ln();
let ln_retries = LAST_ZIV_RETRIES.with(|c| c.get());
assert!(
ln_retries <= MAX_RETRIES,
"ln({x}) at p={p} took {ln_retries} retries (expected <= {MAX_RETRIES})"
);
LAST_ZIV_RETRIES.with(|c| c.set(usize::MAX));
let _ = x.exp();
let exp_retries = LAST_ZIV_RETRIES.with(|c| c.get());
assert!(
exp_retries <= MAX_RETRIES,
"exp({x}) at p={p} took {exp_retries} retries (expected <= {MAX_RETRIES})"
);
}
}
}
}