Skip to main content

dashu_float/
log.rs

1use dashu_base::{
2    utils::{next_down, next_up},
3    Abs, AbsOrd,
4    Approximation::*,
5    EstimatedLog2, PowerOfTwo, Sign, UnsignedAbs,
6};
7use dashu_int::{IBig, UBig};
8
9use crate::{
10    ball::{ceil_shift, Ball},
11    error::{assert_finite, assert_limited_precision, FpError, FpResult},
12    fbig::FBig,
13    math::cache::{reborrow_cache, ConstCache},
14    repr::{Context, Repr, Word},
15    round::{mode, ErrorBounds, Round},
16};
17use core::cmp::Ordering;
18
19impl<const B: Word> EstimatedLog2 for Repr<B> {
20    // currently a Word has at most 64 bits, so log2() < f32::MAX
21    fn log2_bounds(&self) -> (f32, f32) {
22        if self.significand.is_zero() {
23            return (f32::NEG_INFINITY, f32::NEG_INFINITY);
24        }
25
26        // log(s*B^e) = log(s) + e*log(B)
27        let (logs_lb, logs_ub) = self.significand.log2_bounds();
28        let (logb_lb, logb_ub) = if B.is_power_of_two() {
29            let log = B.trailing_zeros() as f32;
30            (log, log)
31        } else {
32            B.log2_bounds()
33        };
34        let e = self.exponent as f32;
35        let (lb, ub) = if self.exponent >= 0 {
36            (logs_lb + e * logb_lb, logs_ub + e * logb_ub)
37        } else {
38            (logs_lb + e * logb_ub, logs_ub + e * logb_lb)
39        };
40        (next_down(lb), next_up(ub))
41    }
42
43    fn log2_est(&self) -> f32 {
44        let logs = self.significand.log2_est();
45        let logb = if B.is_power_of_two() {
46            B.trailing_zeros() as f32
47        } else {
48            B.log2_est()
49        };
50        logs + self.exponent as f32 * logb
51    }
52}
53
54impl<R: Round, const B: Word> EstimatedLog2 for FBig<R, B> {
55    #[inline]
56    fn log2_bounds(&self) -> (f32, f32) {
57        self.repr.log2_bounds()
58    }
59
60    #[inline]
61    fn log2_est(&self) -> f32 {
62        self.repr.log2_est()
63    }
64}
65
66impl<R: ErrorBounds, const B: Word> FBig<R, B> {
67    /// Calculate the natural logarithm function (`log(x)`) on the float number.
68    ///
69    /// # Examples
70    ///
71    /// ```
72    /// # use core::str::FromStr;
73    /// # use dashu_base::ParseError;
74    /// # use dashu_float::DBig;
75    /// let a = DBig::from_str("1.234")?;
76    /// assert_eq!(a.ln(), DBig::from_str("0.2103")?);
77    /// # Ok::<(), ParseError>(())
78    /// ```
79    #[inline]
80    pub fn ln(&self) -> Self {
81        self.context.unwrap_fp(self.context.ln(&self.repr, None))
82    }
83
84    /// Calculate the natural logarithm function (`log(x+1)`) on the float number
85    ///
86    /// # Examples
87    ///
88    /// ```
89    /// # use core::str::FromStr;
90    /// # use dashu_base::ParseError;
91    /// # use dashu_float::DBig;
92    /// let a = DBig::from_str("0.1234")?;
93    /// assert_eq!(a.ln_1p(), DBig::from_str("0.11636")?);
94    /// # Ok::<(), ParseError>(())
95    /// ```
96    #[inline]
97    pub fn ln_1p(&self) -> Self {
98        self.context.unwrap_fp(self.context.ln_1p(&self.repr, None))
99    }
100
101    /// Calculate the base-2 logarithm (`log2(x)`) on the float number.
102    ///
103    /// Correctly rounded to the context's precision under any rounding mode. For an exact power
104    /// of two the result is the exact integer `log2(x)`.
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// # use core::str::FromStr;
110    /// # use dashu_base::ParseError;
111    /// # use dashu_float::DBig;
112    /// let a = DBig::from_str("8")?;
113    /// assert_eq!(a.log2(), DBig::from_str("3")?);
114    /// # Ok::<(), ParseError>(())
115    /// ```
116    #[inline]
117    pub fn log2(&self) -> Self {
118        self.context.unwrap_fp(self.context.log2(&self.repr, None))
119    }
120
121    /// Calculate the base-10 logarithm (`log10(x)`) on the float number.
122    ///
123    /// Correctly rounded to the context's precision under any rounding mode. For an exact power
124    /// of ten the result is the exact integer `log10(x)`.
125    ///
126    /// # Examples
127    ///
128    /// ```
129    /// # use core::str::FromStr;
130    /// # use dashu_base::ParseError;
131    /// # use dashu_float::DBig;
132    /// let a = DBig::from_str("1000")?;
133    /// assert_eq!(a.log10(), DBig::from_str("3")?);
134    /// # Ok::<(), ParseError>(())
135    /// ```
136    #[inline]
137    pub fn log10(&self) -> Self {
138        self.context.unwrap_fp(self.context.log10(&self.repr, None))
139    }
140}
141
142// `ln2`/`ln10`/`iacoth`/`ln_base`/`ln_compute` are the near-correct logarithm primitives: they
143// evaluate the series at a working precision and round once, without a Ziv certification step.
144// They live on `R: Round` so that base conversion (`with_base_and_precision`, which only needs a
145// near-correct constant `ln(B)`) can use them without inheriting the `ErrorBounds` bound. The
146// correctly-rounded public `ln`/`ln_1p` (in the `ErrorBounds` impl below) wrap `ln_compute` in a
147// Ziv loop.
148impl<R: Round> Context<R> {
149    /// Calculate log(2)
150    ///
151    /// The precision of the output will be larger than self.precision
152    #[inline]
153    fn ln2<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
154        if let Some(c) = cache {
155            return c.ln2::<B, R>(self.precision);
156        }
157        // log(2) = 4L(6) + 2L(99)
158        // see formula (24) from Gourdon, Xavier, and Pascal Sebah.
159        // "The Logarithmic Constant: Log 2." (2004)
160        4 * self.iacoth(6.into()) + 2 * self.iacoth(99.into())
161    }
162
163    /// Calculate log(10)
164    ///
165    /// The precision of the output will be larger than self.precision
166    #[inline]
167    fn ln10<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
168        if let Some(c) = cache {
169            return c.ln10::<B, R>(self.precision);
170        }
171        // log(10) = log(2) + log(5) = 3log(2) + 2L(9)
172        3 * self.ln2(None) + 2 * self.iacoth(9.into())
173    }
174
175    /// Calculate log(B), for internal use only
176    ///
177    /// The precision of the output will be larger than self.precision
178    #[inline]
179    pub(crate) fn ln_base<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
180        if let Some(c) = cache {
181            return c.ln_base::<B, R>(self.precision);
182        }
183        match B {
184            2 => self.ln2(None),
185            10 => self.ln10(None),
186            i if i.is_power_of_two() => self.ln2(None) * i.trailing_zeros(),
187            _ => {
188                // Near-correct ln(B) via the atanh series (no Ziv certification — base conversion
189                // only needs a near-correct constant). `ln_compute` is on `R: Round`, so this keeps
190                // `ln_base` callable from `R: Round` contexts (base conversion).
191                let guard = self.base_guard_digits::<B>() + 2;
192                self.ln_compute::<B>(
193                    &Repr::new(Repr::<B>::BASE.into(), 0),
194                    self.precision + guard,
195                    false,
196                    None,
197                )
198                .to_value_radius::<R>()
199                .0
200            }
201        }
202    }
203
204    /// `ln(B)` as a [`Ball`], carrying the mechanical radius.
205    ///
206    /// The cached bases (2, 10, powers of 2) evaluate correctly-rounded constants (error ≤ ½ ulp at
207    /// the working precision), so the fixed `8` ulps is a sound loose bound. A generic base falls
208    /// back to `ln_compute`'s atanh series, whose radius is ~(series terms + B) ulps — far larger
209    /// than 8 — so its mechanical radius is kept instead (a hard-coded `8` would under-bind the
210    /// `s·ln(B)` reconstruction error in `exp_compute`'s reduction).
211    pub(crate) fn ln_base_ball<const B: Word>(
212        &self,
213        mut cache: Option<&mut ConstCache>,
214    ) -> Ball<B> {
215        let ctx = Context::<mode::HalfEven>::new(self.precision);
216        match B {
217            10 => {
218                let logb = ctx.ln_base::<B>(reborrow_cache(&mut cache));
219                Ball::with_error(logb, IBig::from(8))
220            }
221            i if i.is_power_of_two() => {
222                let logb = ctx.ln_base::<B>(reborrow_cache(&mut cache));
223                Ball::with_error(logb, IBig::from(8))
224            }
225            _ => {
226                // Generic base: no cached sub-series applies, so compute ln(B) directly at the
227                // requested precision and keep `ln_compute`'s ball error.
228                ctx.ln_compute::<B>(
229                    &Repr::new(Repr::<B>::BASE.into(), 0),
230                    self.precision,
231                    false,
232                    reborrow_cache(&mut cache),
233                )
234            }
235        }
236    }
237
238    /// Calculate L(n) = acoth(n) = atanh(1/n) = 1/2 log((n+1)/(n-1)), given by the
239    /// series
240    ///
241    /// ```text
242    ///                1     n + 1              1
243    ///   atanh(1/n) = — log(—————) = Σ   ——————————————————
244    ///                2     n - 1   i≥0 n^(2i+1) · (2i+1)
245    /// ```
246    ///
247    /// This method is intended to be used in logarithm calculation,
248    /// so the precision of the output will be larger than desired precision.
249    ///
250    /// Evaluated by binary splitting (see [`iacoth_bs`][crate::math::cache::iacoth_bs]):
251    /// the exact integer tree state `(P, Q, T)` over `[1, N)` satisfies
252    /// `L(n) = (Q + T)/(n·Q)`, with `Q` kept at O(p) digits by the ratio-form
253    /// term recurrence.
254    fn iacoth<const B: Word>(&self, n: IBig) -> FBig<R, B> {
255        let n: u32 = (&n).try_into().expect("iacoth argument must fit in u32");
256
257        // number of series terms until r_k < B^{-p}:  (2k+1)·log_B(n) > p.
258        // The count is generously over-provisioned, so a truncating cast stands in
259        // for a ceiling.
260        let log_b_n = n.log2_est() / B.log2_est();
261        let num_terms = (self.precision as f32 / (2.0 * log_b_n)) as usize + 10;
262
263        let (_p, q, t) = crate::math::cache::iacoth_bs(n, 1, num_terms + 1);
264
265        // L(n) = (Q + T) / (n·Q). Extra guard digits absorb the division's rounding
266        // (the binary-splitting state is exact, so only this single round loses anything).
267        let guard_digits = self.base_guard_digits::<B>();
268        let work_context = Self::new(self.precision + guard_digits + 2);
269
270        let num = work_context.convert_int::<B>(q.as_ibig() + &t).value();
271        let denom = work_context.convert_int::<B>(IBig::from(n) * &q).value();
272        num / denom
273    }
274
275    /// Evaluate `ln(x)` (or `ln(x+1)` when `one_plus`) at `work_precision` via the atanh series,
276    /// returning a [`Ball`] whose radius is derived mechanically by Ball arithmetic (error
277    /// propagates term-by-term through the series; cancellation and the `s·ln(B)` reconstruction
278    /// flow through the Ball scale factors).
279    ///
280    /// This is the near-correct computation core shared by the public Ziv-backed `ln`/`ln_1p`
281    /// (which wrap it in a retry loop) and by `ln_base` (which only needs a near-correct constant
282    /// `ln(B)`). It lives on `R: Round` so those near-correct callers don't inherit the
283    /// `ErrorBounds` bound.
284    pub(crate) fn ln_compute<const B: Word>(
285        &self,
286        x: &Repr<B>,
287        mut work_precision: usize,
288        one_plus: bool,
289        mut cache: Option<&mut ConstCache>,
290    ) -> Ball<B> {
291        // Round the input to the working precision; the input's own rounding is the only error
292        // introduced here.
293        let context = Context::<mode::HalfEven>::new(work_precision);
294        let x_ball = Ball::from_rounded(context.repr_round_ref(x).map(|r| FBig::new(r, context)));
295
296        // When one_plus is true and |x| < 1/B, the input is fed into the Maclaurin without scaling.
297        let no_scaling = one_plus && x_ball.mid.log2_est() < -B.log2_est();
298
299        let (s, mut x_scaled) = if no_scaling {
300            (0, x_ball)
301        } else {
302            let x_ball = if one_plus {
303                x_ball.add(&Ball::exact_int(work_precision, IBig::ONE))
304            } else {
305                x_ball
306            };
307
308            let log2 = x_ball.mid.log2_bounds().0;
309            let s = log2 as isize - (log2 < 0.) as isize; // floor(log2(x))
310
311            let mut exact = x_ball.n.is_zero();
312            let x_scaled = if B == 2 {
313                x_ball.shift(s) // exact (power-of-base shift)
314            } else if s > 0 {
315                // Exact divisor 2^s: the error shrinks by it directly (no general-division rational).
316                x_ball.div_exact(&(IBig::ONE << s as usize))
317            } else {
318                // Scaling by 2^|s| is exact (finite decimal × power of two). Use the tracking
319                // variant so an exact operand keeps n = 0 — otherwise the unconditional `+1`
320                // would be amplified by `rescale_precision` into a `B^precision`-sized error
321                // count that never shrinks across Ziv retries (the powf-of-base-<1 hang).
322                x_ball.scale_int_tracking(&(IBig::ONE << (-s) as usize), &mut exact)
323            };
324            debug_assert!(x_scaled.mid >= FBig::<mode::HalfEven, B>::ONE);
325            (s, x_scaled)
326        };
327
328        // The reconstruction 2·sum + s·ln(B) *cancels* for x < 1 (s < 0), so the series runs at
329        // double precision to keep the pre-cancellation sum accurate. The finer ulp rescales `n`.
330        if s < 0 || x_scaled.mid.repr().sign() == Sign::Negative {
331            work_precision += self.precision;
332            x_scaled.rescale_precision(self.precision);
333        }
334        let work_context = Context::<mode::HalfEven>::new(work_precision);
335
336        // after the number is scaled to nearly one, use Maclaurin series on log(x) = 2atanh(z):
337        // let z = (x-1)/(x+1) < 1, log(x) = 2atanh(z) = 2Σ(z²ⁱ⁺¹/(2i+1)) for i = 1,3,5,...
338        let z = if no_scaling {
339            let two = Ball::exact_int(work_precision, IBig::from(2));
340            let den = x_scaled.add(&two);
341            x_scaled.div(&den)
342        } else {
343            let one = Ball::exact_int(work_precision, IBig::ONE);
344            let num = x_scaled.sub(&one);
345            let den = x_scaled.add(&one);
346            num.div(&den)
347        };
348        let z2 = z.mul(&z);
349        let mut pow = z.clone();
350        let mut sum = z;
351        let mut k: usize = 3;
352        loop {
353            pow = pow.mul(&z2);
354
355            let increase = pow.div_int(k);
356            if increase.mid.abs_cmp(&sum.mid.ulp_lb()).is_le() {
357                break;
358            }
359
360            sum = sum.add(&increase);
361            k += 2;
362        }
363
364        // Omitted series tail: the first omitted term is ≤ sum.ulp_lb(), and the tail of the
365        // atanh series shrinks by z² per step with 1/(1−z²) < B for x_scaled ∈ [1, B), so the
366        // tail is < B·sum.ulp_lb() < B ulps of sum.
367        sum.inflate(&IBig::from(B));
368
369        // compose the logarithm of the original number
370        let sum2 = sum.scale_int(&IBig::from(2));
371        if no_scaling {
372            sum2
373        } else {
374            // ln(2) as a ball. The constant evaluates the atanh series via binary splitting at
375            // work + guard digits and rounds once to `work_precision`, so its error is a handful
376            // of work-precision ulps; 8 is a conservative sound bound for every code path
377            // (cached and uncached).
378            let ln2 = work_context.ln2::<B>(reborrow_cache(&mut cache));
379            let ln2 = Ball::with_error(ln2, IBig::from(8));
380            sum2.add(&ln2.scale_int(&IBig::from(s)))
381        }
382    }
383
384    /// `ln(1 + arg)` of a *ball* input. [`ln_compute`](Self::ln_compute) evaluates the series on
385    /// `arg.mid`; the input ball's own error `|θ| ≤ arg.n·ulp(arg)` then contributes `|θ|/(1+arg)`
386    /// to the log. Bound via `(1+arg)`'s ball magnitude: for a mostly-correct argument the factor
387    /// `1/(1−|θ|/(1+arg)) ≤ 2` is sound, so the adjustment is `⌈2·n_arg·ulp_arg/((1+arg)·ulp_ln)⌉`.
388    pub(crate) fn ln_1p_ball<const B: Word>(
389        &self,
390        arg: &Ball<B>,
391        mut cache: Option<&mut ConstCache>,
392    ) -> Ball<B> {
393        let mut ln_ball =
394            self.ln_compute::<B>(arg.mid.repr(), self.precision, true, reborrow_cache(&mut cache));
395        let den = arg.add(&Ball::exact_int(self.precision, IBig::ONE));
396        let e_d = den.mid.repr().exponent; // (1+arg) = sig_d·B^(e_d)
397        let sig_d = den.mid.repr().significand.clone().abs();
398        // `lead_*` is the leading position (`lead_exp`), so `ulp_arg = B^(lead_arg − p_arg)` and
399        // `ulp_ln = B^(lead_ln − p_ln)`. The input error propagates as
400        //   n_arg·ulp_arg / ((1+arg)·ulp_ln) = n_arg·B^(lead_arg − p_arg − e_d − lead_ln + p_ln)/sig_d;
401        // ×2 for the 1/(1−|θ|/(1+arg)) factor.
402        // The precision difference is essential: `ln_compute`'s s<0 path runs at double precision,
403        // so `ln_ball` sits at 2·self.precision while `arg` stays at self.precision — dropping the
404        // `−p_arg+p_ln` term under-bounds the adjust by B^(p_ln−p_arg) (atanh(x<0) near the pole
405        // then mis-certifies, e.g. off by 2^13 ulps).
406        let lead_arg = Ball::lead_exp(&arg.mid);
407        let p_arg = arg.mid.precision();
408        let lead_ln = Ball::lead_exp(&ln_ball.mid);
409        let p_ln = ln_ball.mid.precision();
410        let shift = lead_arg - p_arg as isize - e_d - lead_ln + p_ln as isize;
411        let num = ceil_shift::<B>(2 * &arg.n, shift);
412        let adjust = (num + &sig_d - IBig::ONE) / sig_d;
413        ln_ball.inflate(&adjust);
414        ln_ball
415    }
416}
417
418// `ln`/`ln_1p` are correctly rounded via the Ziv loop, whose containment test needs the rounding
419// preimage (`R: ErrorBounds`). They delegate the series to `ln_compute`.
420impl<R: ErrorBounds> Context<R> {
421    /// Calculate the natural logarithm function (`log(x)`) on the float number under this context.
422    ///
423    /// # Examples
424    ///
425    /// ```
426    /// # use core::str::FromStr;
427    /// # use dashu_base::ParseError;
428    /// # use dashu_float::DBig;
429    /// use dashu_base::Approximation::*;
430    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
431    ///
432    /// let context = Context::<HalfAway>::new(2);
433    /// let a = DBig::from_str("1.234")?;
434    /// assert_eq!(context.ln(&a.repr(), None), Ok(Inexact(DBig::from_str("0.21")?, NoOp)));
435    /// # Ok::<(), ParseError>(())
436    /// ```
437    #[inline]
438    pub fn ln<const B: Word>(
439        &self,
440        x: &Repr<B>,
441        cache: Option<&mut ConstCache>,
442    ) -> FpResult<FBig<R, B>> {
443        if x.is_infinite() {
444            return Err(FpError::InfiniteInput);
445        }
446        if x.significand.is_zero() {
447            // ln(±0) = -inf (a value, not an error)
448            return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
449        }
450        if x.sign() == Sign::Negative {
451            return Err(FpError::OutOfDomain);
452        }
453        self.ln_internal(x, false, cache)
454    }
455
456    /// Calculate the natural logarithm function (`log(x+1)`) on the float number under this context.
457    ///
458    /// # Examples
459    ///
460    /// ```
461    /// # use core::str::FromStr;
462    /// # use dashu_base::ParseError;
463    /// # use dashu_float::DBig;
464    /// use dashu_base::Approximation::*;
465    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
466    ///
467    /// let context = Context::<HalfAway>::new(2);
468    /// let a = DBig::from_str("0.1234")?;
469    /// assert_eq!(context.ln_1p(&a.repr(), None), Ok(Inexact(DBig::from_str("0.12")?, AddOne)));
470    /// # Ok::<(), ParseError>(())
471    /// ```
472    #[inline]
473    pub fn ln_1p<const B: Word>(
474        &self,
475        x: &Repr<B>,
476        cache: Option<&mut ConstCache>,
477    ) -> FpResult<FBig<R, B>> {
478        if x.is_infinite() {
479            return Err(FpError::InfiniteInput);
480        }
481        // Domain of ln_1p is x > -1. x == -1 gives -inf; x < -1 is out of domain.
482        if x.sign() == Sign::Negative && !x.significand.is_zero() {
483            match FBig::<R, B>::new(x.clone(), *self).abs_cmp(&FBig::ONE) {
484                Ordering::Greater => return Err(FpError::OutOfDomain), // x < -1
485                Ordering::Equal => return Ok(Exact(FBig::new(Repr::neg_infinity(), *self))),
486                _ => {}
487            }
488        }
489        self.ln_internal(x, true, cache)
490    }
491
492    fn ln_internal<const B: Word>(
493        &self,
494        x: &Repr<B>,
495        one_plus: bool,
496        mut cache: Option<&mut ConstCache>,
497    ) -> FpResult<FBig<R, B>> {
498        assert_finite(x);
499
500        // Exact special cases first: they need no rounding, so a precision-0 (unlimited)
501        // value such as `FBig::ONE` or the one from `try_from(0.0)` must still resolve
502        // ln/ln_1p exactly rather than tripping the limited-precision assertion below.
503        if !one_plus && x.is_one() {
504            return Ok(Exact(FBig::ZERO)); // ln(1) = +0
505        }
506        if one_plus && x.significand.is_zero() {
507            // ln_1p(±0) = ±0
508            let zero = if x.is_neg_zero() {
509                FBig::new(Repr::neg_zero(), *self)
510            } else {
511                FBig::ZERO
512            };
513            return Ok(Exact(zero));
514        }
515
516        assert_limited_precision(self.precision);
517
518        // Correct rounding via the Ziv loop: `ln_compute` evaluates the atanh series at `p + guard`
519        // and reports a provable error radius; the driver retries with more guard digits until the
520        // approximation's error interval lies entirely inside one rounding bin. The guard is a
521        // *performance* knob (first-attempt hit rate), not a correctness backstop — Ziv certifies
522        // the result. (The pre-Ziv `+ 2` is retained: with the conservative radius below it is still
523        // needed for the first attempt to clear the half-ulp preimage at typical precisions.)
524        let base_guard = self.base_guard_digits::<B>() + 2;
525        self.ziv(base_guard + one_plus as usize, |guard| {
526            Ok(self
527                .ln_compute::<B>(x, self.precision + guard, one_plus, reborrow_cache(&mut cache))
528                .to_value_radius::<R>())
529        })
530    }
531
532    /// Calculate the base-2 logarithm (`log2(x)`) on the float number under this context.
533    ///
534    /// Correctly rounded to the context's precision under any rounding mode; for an exact power
535    /// of two the result is the exact integer `log2(x)`.
536    ///
537    /// # Domain
538    ///
539    /// `log2(±0) = −∞` and a negative (non-zero) input is out of domain; an infinite input is an
540    /// error (a finite context cannot produce the infinite `log2(+∞) = +∞` exactly).
541    ///
542    /// # Examples
543    ///
544    /// ```
545    /// # use core::str::FromStr;
546    /// # use dashu_base::ParseError;
547    /// # use dashu_float::DBig;
548    /// use dashu_base::Approximation::*;
549    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
550    ///
551    /// let context = Context::<HalfAway>::new(4);
552    /// let a = DBig::from_str("10")?;
553    /// assert_eq!(context.log2(&a.repr(), None), Ok(Inexact(DBig::from_str("3.322")?, AddOne)));
554    /// # Ok::<(), ParseError>(())
555    /// ```
556    #[inline]
557    pub fn log2<const B: Word>(
558        &self,
559        x: &Repr<B>,
560        cache: Option<&mut ConstCache>,
561    ) -> FpResult<FBig<R, B>> {
562        if x.is_infinite() {
563            return Err(FpError::InfiniteInput);
564        }
565        if x.significand.is_zero() {
566            // log2(±0) = -inf (a value, not an error)
567            return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
568        }
569        if x.sign() == Sign::Negative {
570            return Err(FpError::OutOfDomain);
571        }
572        self.log2_internal(x, cache)
573    }
574
575    fn log2_internal<const B: Word>(
576        &self,
577        x: &Repr<B>,
578        mut cache: Option<&mut ConstCache>,
579    ) -> FpResult<FBig<R, B>> {
580        assert_finite(x);
581
582        // Exact shortcuts first — they also cover unlimited precision, which the Ziv loop below
583        // rejects via its limited-precision assertion.
584        if x.is_one() {
585            return Ok(Exact(FBig::ZERO)); // log2(1) = +0
586        }
587
588        // Exact power-of-two shortcut: if x = 2^k for an integer k, log2(x) = k. This is *required*
589        // for directed rounding — the Ziv loop below cannot certify an exactly-representable
590        // result whose true value sits on a rounding boundary (its shrinking error interval
591        // always straddles the boundary), so without this shortcut log2(2^-159) under `Up` would
592        // exhaust the retry cap and return k + 1 ulp instead of the exact k.
593        //
594        // log2(x) = log2(significand) + exponent·log2(B). With significand = 2^m this is an exact
595        // integer whenever log2(B) is integral (B a power of two), or — for a non-power-of-two
596        // base — when the exponent is zero.
597        let mag = (&x.significand).unsigned_abs();
598        if mag.is_power_of_two() && (x.exponent == 0 || B.is_power_of_two()) {
599            let m = mag.trailing_zeros().unwrap(); // = log2(significand)
600            let log2_b = B.trailing_zeros() as isize;
601            let k = IBig::from(m) + IBig::from(x.exponent) * IBig::from(log2_b);
602            return Ok(self.convert_int::<B>(k));
603        }
604
605        assert_limited_precision(self.precision);
606
607        // log2(x) = ln(x)/ln(2), correctly rounded via the Ziv loop. Both logarithms come from the
608        // Ball-based `ln_compute`, and dividing them as Balls composes the radius mechanically:
609        // the quotient's error is bounded from the two logarithms' relative errors, with no
610        // directed-interval bookkeeping or guard-digit constant. The driver certifies the result
611        // against the rounding preimage exactly as before.
612        let initial_guard = self.base_guard_digits::<B>() + 4;
613        self.ziv(initial_guard, |guard| {
614            let work_precision = self.precision + guard;
615            let lx = self.ln_compute::<B>(x, work_precision, false, reborrow_cache(&mut cache));
616            let two = Repr::new(IBig::from(2), 0);
617            let l2 = self.ln_compute::<B>(&two, work_precision, false, reborrow_cache(&mut cache));
618            Ok(lx.div(&l2).to_value_radius::<R>())
619        })
620    }
621
622    /// Calculate the base-10 logarithm (`log10(x)`) on the float number under this context.
623    ///
624    /// Correctly rounded to the context's precision under any rounding mode; for an exact power
625    /// of ten the result is the exact integer `log10(x)`.
626    ///
627    /// # Domain
628    ///
629    /// `log10(±0) = −∞` and a negative (non-zero) input is out of domain; an infinite input is an
630    /// error (a finite context cannot produce the infinite `log10(+∞) = +∞` exactly).
631    ///
632    /// # Examples
633    ///
634    /// ```
635    /// # use core::str::FromStr;
636    /// # use dashu_base::ParseError;
637    /// # use dashu_float::DBig;
638    /// use dashu_base::Approximation::*;
639    /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
640    ///
641    /// let context = Context::<HalfAway>::new(4);
642    /// let a = DBig::from_str("100")?;
643    /// assert_eq!(context.log10(&a.repr(), None), Ok(Exact(DBig::from_str("2")?)));
644    /// # Ok::<(), ParseError>(())
645    /// ```
646    #[inline]
647    pub fn log10<const B: Word>(
648        &self,
649        x: &Repr<B>,
650        cache: Option<&mut ConstCache>,
651    ) -> FpResult<FBig<R, B>> {
652        if x.is_infinite() {
653            return Err(FpError::InfiniteInput);
654        }
655        if x.significand.is_zero() {
656            // log10(±0) = -inf (a value, not an error)
657            return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
658        }
659        if x.sign() == Sign::Negative {
660            return Err(FpError::OutOfDomain);
661        }
662        self.log10_internal(x, cache)
663    }
664
665    fn log10_internal<const B: Word>(
666        &self,
667        x: &Repr<B>,
668        mut cache: Option<&mut ConstCache>,
669    ) -> FpResult<FBig<R, B>> {
670        assert_finite(x);
671
672        // Exact shortcuts first — they also cover unlimited precision, which the Ziv loop below
673        // rejects via its limited-precision assertion.
674        if x.is_one() {
675            return Ok(Exact(FBig::ZERO)); // log10(1) = +0
676        }
677
678        // Exact power-of-ten shortcut: if x = 10^m for an integer m, log10(x) = m. This is *required*
679        // for directed rounding — the Ziv loop below cannot certify an exactly-representable result
680        // whose true value sits on a rounding boundary (its shrinking error interval always
681        // straddles the one-sided preimage), so without this shortcut log10(10^-159) under `Up`
682        // would exhaust the retry cap and return m + 1 ulp instead of the exact m.
683        if let Some(m) = exact_pow10_log::<B>(&x.significand, x.exponent) {
684            return Ok(self.convert_int::<B>(IBig::from(m)));
685        }
686
687        assert_limited_precision(self.precision);
688
689        // log10(x) = ln(x)/ln(10), correctly rounded via the Ziv loop. Both logarithms come from the
690        // Ball-based `ln_compute`, and dividing them as Balls composes the radius mechanically:
691        // the quotient's error is bounded from the two logarithms' relative errors, with no
692        // directed-interval bookkeeping or guard-digit constant. The driver certifies the result
693        // against the rounding preimage exactly as before.
694        let initial_guard = self.base_guard_digits::<B>() + 4;
695        self.ziv(initial_guard, |guard| {
696            let work_precision = self.precision + guard;
697            let lx = self.ln_compute::<B>(x, work_precision, false, reborrow_cache(&mut cache));
698            let ten = Repr::new(IBig::from(10), 0);
699            let l10 = self.ln_compute::<B>(&ten, work_precision, false, reborrow_cache(&mut cache));
700            Ok(lx.div(&l10).to_value_radius::<R>())
701        })
702    }
703}
704
705/// If `x = sig·B^e` is exactly `10^m` for some integer `m`, return `m`; otherwise `None`.
706///
707/// `10^m = 2^m·5^m`, so `x` is a power of ten iff the 2-valuation and 5-valuation of `sig·B^e`
708/// coincide and `x` has no other prime factor. [`UBig::remove_word`] divides out all 2s and 5s from
709/// both the base and the significand, returning each valuation as the removed exponent (and leaving
710/// any non-{2,5} prime factor behind as a cofactor ≠ 1). The base `B = 2^p·5^q·s` (with `s` coprime
711/// to 10) contributes `p·e` to the 2-valuation and `q·e` to the 5-valuation, and `s` must not
712/// appear (unless `e = 0`). The valuations may be negative (`x` a negative power of ten).
713fn exact_pow10_log<const B: Word>(sig: &IBig, e: isize) -> Option<isize> {
714    // base: divide out all 2s and 5s, getting (p, q, leftover)
715    let mut rest = UBig::from_word(B);
716    let p = rest.remove_word(2)? as isize; // B ≥ 2, so never None
717    let q = rest.remove_word(5).unwrap() as isize;
718    // the leftover would give `x` a non-{2,5} prime factor when e ≠ 0
719    if !rest.is_one() && e != 0 {
720        return None;
721    }
722
723    // significand: divide out all 2s and 5s, getting (v2, v5, cofactor)
724    let mut sig_abs = sig.unsigned_abs();
725    let v2_sig = sig_abs.remove_word(2)? as isize; // non-zero upstream, so never None
726    let v5 = sig_abs.remove_word(5).unwrap() as isize;
727    // after removing every 2 and 5 the significand must be 1 (no other prime factor)
728    if sig_abs != UBig::ONE {
729        return None;
730    }
731
732    // `p*e` / `q*e` can overflow `isize` for an exotic base (B a large power of 2) at extreme
733    // exponents on 32-bit; the true log10 would not fit `isize` then, so bail rather than compare
734    // wrapped values that could falsely match.
735    let v2 = v2_sig.checked_add(p.checked_mul(e)?)?;
736    let v5 = v5.checked_add(q.checked_mul(e)?)?;
737    (v2 == v5).then_some(v2)
738}
739
740#[cfg(test)]
741mod tests {
742    use super::*;
743    use crate::round::mode;
744    use alloc::vec::Vec;
745    use dashu_base::BitTest;
746
747    #[test]
748    fn test_log10_domain() {
749        let ctx = Context::<mode::HalfEven>::new(53);
750        // log10(±0) = -inf (a value, not an error)
751        let r = ctx.log10::<2>(&Repr::<2>::zero(), None).unwrap().value();
752        assert!(r.repr.is_infinite());
753        assert_eq!(r.repr.sign(), Sign::Negative);
754        // log10(negative) is out of domain
755        assert!(matches!(
756            ctx.log10::<2>(&Repr::new((-1).into(), 0), None),
757            Err(FpError::OutOfDomain)
758        ));
759        // an infinite input is rejected
760        assert!(matches!(ctx.log10::<2>(&Repr::infinity(), None), Err(FpError::InfiniteInput)));
761    }
762
763    #[test]
764    fn test_log10_exact_power_of_ten() {
765        // log10(10^k) = k exactly under every rounding mode. Regression for the directed-rounding
766        // defect the power-of-ten shortcut exists for: rounding ln(x) and ln(10) each toward the
767        // mode and dividing once does not bound the quotient, so previously log10(10^-159) under
768        // `Up` returned -159 + 1 ulp.
769        let p = 53;
770        for k in [0isize, 1, -1, 5, -159, 1000, -1000] {
771            let x = Repr::<10>::new(IBig::from(1), k); // 10^k (base 10: significand 1)
772            let r_down = Context::<mode::Down>::new(p)
773                .log10::<10>(&x, None)
774                .unwrap()
775                .value();
776            let r_up = Context::<mode::Up>::new(p)
777                .log10::<10>(&x, None)
778                .unwrap()
779                .value();
780            let r_zero = Context::<mode::Zero>::new(p)
781                .log10::<10>(&x, None)
782                .unwrap()
783                .value();
784            let r_he = Context::<mode::HalfEven>::new(p)
785                .log10::<10>(&x, None)
786                .unwrap()
787                .value();
788            assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log10(10^{k})");
789            assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log10(10^{k})");
790            assert_eq!(r_zero.repr, r_he.repr, "Zero != HalfEven for log10(10^{k})");
791            assert_eq!(r_he.to_int().value(), IBig::from(k), "value for log10(10^{k})");
792        }
793    }
794
795    #[test]
796    fn test_exact_pow10_log_overflow() {
797        // `p*e` overflows isize for a base that is a large power of two at an extreme exponent
798        // (on 32-bit and 64-bit alike); the exact log wouldn't fit isize, so it must be None, not
799        // a wrapped value that could falsely compare equal and return a wrong `Some`.
800        assert_eq!(exact_pow10_log::<256>(&IBig::ONE, isize::MAX), None);
801        assert_eq!(exact_pow10_log::<256>(&IBig::ONE, isize::MIN), None);
802        // sanity: the normal path is unaffected.
803        assert_eq!(exact_pow10_log::<10>(&IBig::ONE, 5), Some(5));
804    }
805
806    #[test]
807    fn test_log10_exact_power_of_ten_binary_base() {
808        // In base 2, 10^k is a float only via a 5^k-significand (e.g. 100 = 25·2^2); the
809        // valuation-based shortcut must still detect the exact log10.
810        let p = 53;
811        for (sig, e, want) in [(25i32, 2isize, 2i64), (5, 1, 1), (125, 3, 3), (50, 1, 2)] {
812            // 50·2^1 = 100 = 10^2 too (a non-normalized significand)
813            let x = Repr::<2>::new(IBig::from(sig), e);
814            let r_down = Context::<mode::Down>::new(p)
815                .log10::<2>(&x, None)
816                .unwrap()
817                .value();
818            let r_up = Context::<mode::Up>::new(p)
819                .log10::<2>(&x, None)
820                .unwrap()
821                .value();
822            let r_zero = Context::<mode::Zero>::new(p)
823                .log10::<2>(&x, None)
824                .unwrap()
825                .value();
826            let r_he = Context::<mode::HalfEven>::new(p)
827                .log10::<2>(&x, None)
828                .unwrap()
829                .value();
830            assert_eq!(r_down.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) Down");
831            assert_eq!(r_up.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) Up");
832            assert_eq!(r_zero.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) Zero");
833            assert_eq!(r_he.to_int().value(), IBig::from(want), "log10({sig}·2^{e}) HalfEven");
834        }
835    }
836
837    #[test]
838    fn test_log10_fbig_convenience() {
839        // FBig::log10 convenience layer: exact powers of ten in both bases.
840        let x = FBig::<mode::HalfEven, 10>::from_repr(Repr::new(1000.into(), 0), Context::new(50));
841        assert_eq!(x.log10(), FBig::<mode::HalfEven, 10>::from(3u8));
842        // base-2 float 100 = 25·2^2
843        let x = FBig::<mode::HalfEven, 2>::from_repr(Repr::new(25.into(), 2), Context::new(50));
844        assert_eq!(x.log10(), FBig::<mode::HalfEven, 2>::from(2u8));
845    }
846
847    /// Fixed inputs for the `log10` oracle differential.
848    fn log10_diff_inputs() -> Vec<Repr<2>> {
849        let mut v = Vec::new();
850        for x in [0.5f64, 1.5, 2.0, 3.0, 10.0, 1000.0, 1e-6, 123.456, 2.5e-10] {
851            v.push(FBig::<mode::HalfEven, 2>::try_from(x).unwrap().into_repr());
852        }
853        for k in [-100isize, -50, -10, -1, 0, 1, 10, 50, 100] {
854            v.push(Repr::new(IBig::ONE, k)); // 2^k
855        }
856        v.push(
857            FBig::<mode::HalfEven, 2>::try_from(f64::MAX)
858                .unwrap()
859                .into_repr(),
860        );
861        v
862    }
863
864    fn check_log10_differential<R: ErrorBounds>(p: usize, x: &Repr<2>, oracle: &Repr<2>) {
865        let ctx = Context::<R>::new(p);
866        let want = ctx.repr_round_ref(oracle).value();
867        let got = ctx.log10_internal::<2>(x, None).unwrap().value();
868        assert_eq!(got.repr, want, "p={p} {} x={x:?}", core::any::type_name::<R>(),);
869    }
870
871    #[test]
872    fn log10_ball_matches_oracle() {
873        let inputs = log10_diff_inputs();
874        for p in [20usize, 50, 100] {
875            for x in &inputs {
876                let oracle = Context::<mode::HalfEven>::new(p + 60)
877                    .log10::<2>(x, None)
878                    .unwrap()
879                    .value();
880                check_log10_differential::<mode::HalfEven>(p, x, &oracle.repr);
881                check_log10_differential::<mode::Down>(p, x, &oracle.repr);
882                check_log10_differential::<mode::Up>(p, x, &oracle.repr);
883                check_log10_differential::<mode::Zero>(p, x, &oracle.repr);
884                check_log10_differential::<mode::Away>(p, x, &oracle.repr);
885            }
886        }
887        // the arbitrary-precision regime: a reduced sweep (directed modes still exercised).
888        for x in inputs.iter().step_by(9) {
889            let oracle = Context::<mode::HalfEven>::new(560)
890                .log10::<2>(x, None)
891                .unwrap()
892                .value();
893            check_log10_differential::<mode::HalfEven>(500, x, &oracle.repr);
894            check_log10_differential::<mode::Down>(500, x, &oracle.repr);
895            check_log10_differential::<mode::Up>(500, x, &oracle.repr);
896        }
897    }
898
899    #[test]
900    fn test_ln_zero_is_neg_infinity() {
901        let ctx = Context::<mode::HalfEven>::new(53);
902        let r = ctx.ln::<2>(&Repr::<2>::zero(), None).unwrap().value();
903        assert!(r.repr().is_infinite());
904        assert_eq!(r.repr().sign(), Sign::Negative);
905    }
906
907    #[test]
908    fn test_iacoth() {
909        let context = Context::<mode::Zero>::new(10);
910        let binary_6 = context.iacoth::<2>(6.into()).with_precision(10).value();
911        assert_eq!(binary_6.repr.significand, IBig::from(689));
912        let decimal_6 = context.iacoth::<10>(6.into()).with_precision(10).value();
913        assert_eq!(decimal_6.repr.significand, IBig::from(1682361183));
914
915        let context = Context::<mode::Zero>::new(40);
916        let decimal_6 = context.iacoth::<10>(6.into()).with_precision(40).value();
917        assert_eq!(
918            decimal_6.repr.significand,
919            IBig::from_str_radix("1682361183106064652522967051084960450557", 10).unwrap()
920        );
921
922        let context = Context::<mode::Zero>::new(201);
923        let binary_6 = context.iacoth::<2>(6.into()).with_precision(201).value();
924        assert_eq!(
925            binary_6.repr.significand,
926            IBig::from_str_radix(
927                "2162760151454160450909229890833066944953539957685348083415205",
928                10
929            )
930            .unwrap()
931        );
932    }
933
934    #[test]
935    fn test_ln2_ln10() {
936        let context = Context::<mode::Zero>::new(45);
937        let decimal_ln2 = context.ln2::<10>(None).with_precision(45).value();
938        assert_eq!(
939            decimal_ln2.repr.significand,
940            IBig::from_str_radix("693147180559945309417232121458176568075500134", 10).unwrap()
941        );
942        let decimal_ln10 = context.ln10::<10>(None).with_precision(45).value();
943        assert_eq!(
944            decimal_ln10.repr.significand,
945            IBig::from_str_radix("230258509299404568401799145468436420760110148", 10).unwrap()
946        );
947
948        let context = Context::<mode::Zero>::new(180);
949        let binary_ln2 = context.ln2::<2>(None).with_precision(180).value();
950        assert_eq!(
951            binary_ln2.repr.significand,
952            IBig::from_str_radix("1062244963371879310175186301324412638028404515790072203", 10)
953                .unwrap()
954        );
955        let binary_ln10 = context.ln10::<2>(None).with_precision(180).value();
956        assert_eq!(
957            binary_ln10.repr.significand,
958            IBig::from_str_radix("882175346869410758689845931257775553286341791676474847", 10)
959                .unwrap()
960        );
961    }
962
963    #[test]
964    fn test_log2_domain() {
965        let ctx = Context::<mode::HalfEven>::new(53);
966        // log2(±0) = -inf (a value, not an error)
967        let r = ctx.log2::<2>(&Repr::<2>::zero(), None).unwrap().value();
968        assert!(r.repr.is_infinite());
969        assert_eq!(r.repr.sign(), Sign::Negative);
970        // log2(negative) is out of domain
971        assert!(matches!(
972            ctx.log2::<2>(&Repr::new((-1).into(), 0), None),
973            Err(FpError::OutOfDomain)
974        ));
975        // an infinite input is rejected
976        assert!(matches!(ctx.log2::<2>(&Repr::infinity(), None), Err(FpError::InfiniteInput)));
977    }
978
979    #[test]
980    fn test_log2_exact_power_of_two() {
981        // log2(2^k) = k exactly under every rounding mode. Regression for the directed-rounding
982        // defect: rounding ln(x) and ln(2) each toward the mode and dividing once does not bound
983        // the quotient, so previously log2(2^-159) under `Up` returned -159 + 1 ulp.
984        let p = 53;
985        for k in [0isize, 1, -1, 5, 159, -159, 1000, -1000] {
986            let x = Repr::<2>::new(IBig::from(1), k); // 2^k
987            let r_down = Context::<mode::Down>::new(p)
988                .log2::<2>(&x, None)
989                .unwrap()
990                .value();
991            let r_up = Context::<mode::Up>::new(p)
992                .log2::<2>(&x, None)
993                .unwrap()
994                .value();
995            let r_zero = Context::<mode::Zero>::new(p)
996                .log2::<2>(&x, None)
997                .unwrap()
998                .value();
999            let r_he = Context::<mode::HalfEven>::new(p)
1000                .log2::<2>(&x, None)
1001                .unwrap()
1002                .value();
1003            // Every directed mode produces the identical value — no mode-dependent ulp.
1004            assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log2(2^{k})");
1005            assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log2(2^{k})");
1006            assert_eq!(r_zero.repr, r_he.repr, "Zero != HalfEven for log2(2^{k})");
1007            // And that value is exactly k.
1008            assert_eq!(r_he.to_int().value(), IBig::from(k), "value for log2(2^{k})");
1009        }
1010    }
1011
1012    #[test]
1013    fn test_log2_exact_power_of_two_decimal_base() {
1014        // In a non-power-of-two base the shortcut still fires when the exponent is zero: a
1015        // significand that is itself a power of two makes x = 2^m exactly.
1016        let p = 53;
1017        for (sig, want) in [(8i32, 3isize), (1024, 10), (2, 1), (32, 5)] {
1018            let x = Repr::<10>::new(IBig::from(sig), 0);
1019            let r_down = Context::<mode::Down>::new(p)
1020                .log2::<10>(&x, None)
1021                .unwrap()
1022                .value();
1023            let r_up = Context::<mode::Up>::new(p)
1024                .log2::<10>(&x, None)
1025                .unwrap()
1026                .value();
1027            let r_he = Context::<mode::HalfEven>::new(p)
1028                .log2::<10>(&x, None)
1029                .unwrap()
1030                .value();
1031            assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log2({sig}) base 10");
1032            assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log2({sig}) base 10");
1033            assert_eq!(r_he.to_int().value(), IBig::from(want), "value for log2({sig}) base 10");
1034        }
1035    }
1036
1037    /// For a non-power-of-two significand `sig` (so `log2` is irrational and never lands on a
1038    /// rounding boundary), each directed result must equal a high-precision oracle rounded to the
1039    /// target precision under the same mode — the definition of correct rounding.
1040    fn check_log2_directed_matches_oracle<const B: Word>(sig: u32, p: usize) {
1041        let oracle_ctx = Context::<mode::HalfEven>::new(p + 40);
1042        let x = Repr::<B>::new(IBig::from(sig), 0);
1043        let oracle = oracle_ctx.log2::<B>(&x, None).unwrap().value();
1044
1045        let want_down = Context::<mode::Down>::new(p)
1046            .repr_round_ref(&oracle.repr)
1047            .value();
1048        let want_up = Context::<mode::Up>::new(p)
1049            .repr_round_ref(&oracle.repr)
1050            .value();
1051        let want_he = Context::<mode::HalfEven>::new(p)
1052            .repr_round_ref(&oracle.repr)
1053            .value();
1054
1055        let got_down = Context::<mode::Down>::new(p)
1056            .log2::<B>(&x, None)
1057            .unwrap()
1058            .value();
1059        let got_up = Context::<mode::Up>::new(p)
1060            .log2::<B>(&x, None)
1061            .unwrap()
1062            .value();
1063        let got_he = Context::<mode::HalfEven>::new(p)
1064            .log2::<B>(&x, None)
1065            .unwrap()
1066            .value();
1067
1068        assert_eq!(got_down.repr, want_down, "log2({sig}) base {B} under Down");
1069        assert_eq!(got_up.repr, want_up, "log2({sig}) base {B} under Up");
1070        assert_eq!(got_he.repr, want_he, "log2({sig}) base {B} under HalfEven");
1071    }
1072
1073    #[test]
1074    fn test_log2_directed_matches_oracle() {
1075        let p = 24;
1076        for sig in [3u32, 7, 10, 12345, 65537] {
1077            check_log2_directed_matches_oracle::<2>(sig, p);
1078        }
1079        // Exercise a non-power-of-two base through the Ziv interval path too.
1080        for sig in [3u32, 7, 10, 12345] {
1081            check_log2_directed_matches_oracle::<10>(sig, p);
1082        }
1083    }
1084
1085    // log2 of a value whose result sits within ~1 work-ulp of a power of two must still round to
1086    // the correct neighbor under directed modes. log2(f64::MAX) ≈ 1024 − 2^-53/ln2 sits just below
1087    // 1024; under Down at p=53 the answer is 1024 − 2^-42 (the largest p=53 value ≤ it), but an
1088    // unsound radius previously let Ziv certify 1024 on the first attempt.
1089    #[test]
1090    fn test_log2_just_below_power_of_two_directed() {
1091        let x = FBig::<mode::HalfEven, 2>::try_from(f64::MAX).unwrap();
1092        // High-precision oracle, then re-rounded to the target precision under each mode.
1093        let oracle = Context::<mode::HalfEven>::new(200)
1094            .log2::<2>(x.repr(), None)
1095            .unwrap()
1096            .value();
1097        for p in [24usize, 40, 53, 64] {
1098            let want_down = Context::<mode::Down>::new(p)
1099                .repr_round_ref(&oracle.repr)
1100                .value();
1101            let want_up = Context::<mode::Up>::new(p)
1102                .repr_round_ref(&oracle.repr)
1103                .value();
1104            let got_down = Context::<mode::Down>::new(p)
1105                .log2::<2>(x.repr(), None)
1106                .unwrap()
1107                .value();
1108            let got_up = Context::<mode::Up>::new(p)
1109                .log2::<2>(x.repr(), None)
1110                .unwrap()
1111                .value();
1112            assert_eq!(got_down.repr(), &want_down, "p={p} Down");
1113            assert_eq!(got_up.repr(), &want_up, "p={p} Up");
1114            // Directed invariant: Up ≥ Down.
1115            assert!(got_up.repr() >= got_down.repr(), "p={p} Up < Down");
1116        }
1117    }
1118
1119    /// Directed `ln` of `x ∈ [1, 2)` must match a high-precision oracle re-rounded under the same
1120    /// mode. This binade (s = 0) is where the radius under-estimated the error: `result` inherits
1121    /// `ln_base`'s over-delivered context, and for `x` just above 1 the scaling even classifies
1122    /// `s = −1`, so `2·sum + s·ln2` cancels and the error stays at `sum`'s magnitude while
1123    /// `result`'s collapses — both make `result.ulp()` the wrong scale for the radius.
1124    fn check_ln_directed_in_unit_binade(k: usize, p: usize) {
1125        // x = (2^k + 1) * 2^-k = 1 + 2^-k, exactly representable at precision p when k < p.
1126        let x = Repr::<2>::new(IBig::from(1i64 << k) + IBig::ONE, -(k as isize));
1127        let oracle = Context::<mode::HalfEven>::new(p + 60)
1128            .ln::<2>(&x, None)
1129            .unwrap()
1130            .value();
1131        let want_down = Context::<mode::Down>::new(p)
1132            .repr_round_ref(&oracle.repr)
1133            .value();
1134        let want_up = Context::<mode::Up>::new(p)
1135            .repr_round_ref(&oracle.repr)
1136            .value();
1137        let got_down = Context::<mode::Down>::new(p)
1138            .ln::<2>(&x, None)
1139            .unwrap()
1140            .value();
1141        let got_up = Context::<mode::Up>::new(p)
1142            .ln::<2>(&x, None)
1143            .unwrap()
1144            .value();
1145        assert_eq!(got_down.repr(), &want_down, "ln(1+2^-{k}) p={p} Down");
1146        assert_eq!(got_up.repr(), &want_up, "ln(1+2^-{k}) p={p} Up");
1147        assert!(got_up.repr() >= got_down.repr(), "ln(1+2^-{k}) p={p} Up < Down");
1148    }
1149
1150    #[test]
1151    fn test_ln_directed_near_one() {
1152        // Sweep the near-1 binade at low precision, including the k close to p cases that
1153        // classify as s = −1 and cancel.
1154        for p in [24usize, 40, 53] {
1155            for k in 1..p.saturating_sub(1) {
1156                check_ln_directed_in_unit_binade(k, p);
1157            }
1158        }
1159    }
1160
1161    /// Fixed inputs for the `log2` oracle differential: moderate magnitudes and the
1162    /// near-boundary regimes the legacy directed-interval implementation was specifically sized for.
1163    fn log2_diff_inputs() -> Vec<Repr<2>> {
1164        let mut v = Vec::new();
1165        for x in [0.5f64, 1.5, 2.0, 3.0, 10.0, 1000.0, 1e-6, 123.456, 2.5e-10] {
1166            v.push(FBig::<mode::HalfEven, 2>::try_from(x).unwrap().into_repr());
1167        }
1168        // Exact powers of two.
1169        for k in [-100isize, -50, -10, -1, 0, 1, 10, 50, 100] {
1170            v.push(Repr::new(IBig::ONE, k));
1171        }
1172        // Just below the largest f64 (log2 ≈ 1024, the directed-regime case in the old comment).
1173        v.push(
1174            FBig::<mode::HalfEven, 2>::try_from(f64::MAX)
1175                .unwrap()
1176                .into_repr(),
1177        );
1178        // The [1, 2) unit binade and its mirror below 1: 1 ± 2^-k and 2 − 2^-k exercise the
1179        // s = −1 cancellation (the second-classified-s-−1 case the doubling compensates).
1180        for k in 1usize..=60 {
1181            v.push(Repr::new(IBig::from(1u64 << k) + IBig::ONE, -(k as isize))); // 1 + 2^-k
1182            v.push(Repr::new(IBig::from((1u64 << k) - 1), -(k as isize))); // 1 − 2^-k
1183            v.push(Repr::new(IBig::from((1u64 << (k + 1)) - 1), -(k as isize)));
1184            // 2 − 2^-k
1185        }
1186        v
1187    }
1188
1189    /// The Ball-based `log2` must round exactly like a high-precision oracle (the definition of
1190    /// correct rounding) across precisions, modes, and the near-boundary inputs.
1191    ///
1192    /// The legacy directed-interval implementation is *not* used as the oracle: it has its own
1193    /// residual 1-ulp bug under directed rounding for `log2(1 − 2^-k)` at p=50 (verified against
1194    /// an independent high-precision computation) — exactly the class of defect this pilot
1195    /// replaces.
1196    fn check_log2_differential<R: ErrorBounds>(p: usize, x: &Repr<2>, oracle: &Repr<2>) {
1197        let ctx = Context::<R>::new(p);
1198        let want = ctx.repr_round_ref(oracle).value();
1199        let got = ctx.log2_internal::<2>(x, None).unwrap().value();
1200        assert_eq!(got.repr, want, "p={p} {} x={x:?}", core::any::type_name::<R>(),);
1201    }
1202
1203    /// Regression: `ln_compute`'s s<0 path (base < 1) must NOT inflate its error count with the
1204    /// working precision. An exactly-representable input scaled by a power of two is exact, so the
1205    /// radius must shrink monotonically as the work precision grows — otherwise the composed
1206    /// `pow_exp_log` chain's radius stays constant and the Ziv loop hangs (powf of a base < 1).
1207    #[test]
1208    fn ln_small_base_radius_shrinks_with_guard() {
1209        let ctx = Context::<mode::HalfEven>::new(50);
1210        // 0.2668 (base 10): s = floor(log2(0.2668)) = -2, the s < 0 path.
1211        let x = Repr::<10>::new(IBig::from(2668), -4);
1212        for guard in [4usize, 12, 40, 120] {
1213            let ball = ctx.ln_compute::<10>(&x, 50 + guard, false, None);
1214            // The regression: n must be O(series terms) (~10^5, bit_len < 30), NOT inflated to
1215            // ~B^50 ≈ 10^50 (bit_len ~166) by the s<0 reduction's spurious +1.
1216            assert!(
1217                ball.n.bit_len() < 30,
1218                "n = {} ({} bits) too large at guard={guard}: the s<0 reduction inflated it",
1219                ball.n,
1220                ball.n.bit_len()
1221            );
1222            // The radius in target (precision 50) ulps must fit a preimage so Ziv certifies on the
1223            // first attempt: n·B^(E−p_ball)·B^(50−E) ≤ 1.
1224            let radius_target = crate::ball::ceil_shift::<10>(
1225                ball.n.clone(),
1226                Ball::lead_exp(&ball.mid) - ball.mid.precision() as isize + 50,
1227            );
1228            assert!(
1229                radius_target <= IBig::ONE,
1230                "radius {radius_target} ulps at guard={guard} does not certify (n={})",
1231                ball.n
1232            );
1233        }
1234    }
1235
1236    #[test]
1237    fn log2_ball_matches_oracle() {
1238        let inputs = log2_diff_inputs();
1239        // Moderate precisions: full input sweep, all five modes.
1240        for p in [20usize, 50, 100] {
1241            for x in &inputs {
1242                // The oracle is mode-independent: a high-precision HalfEven value re-rounded
1243                // under each target mode.
1244                let oracle = Context::<mode::HalfEven>::new(p + 60)
1245                    .log2::<2>(x, None)
1246                    .unwrap()
1247                    .value();
1248                check_log2_differential::<mode::HalfEven>(p, x, &oracle.repr);
1249                check_log2_differential::<mode::Down>(p, x, &oracle.repr);
1250                check_log2_differential::<mode::Up>(p, x, &oracle.repr);
1251                check_log2_differential::<mode::Zero>(p, x, &oracle.repr);
1252                check_log2_differential::<mode::Away>(p, x, &oracle.repr);
1253            }
1254        }
1255        // The arbitrary-precision regime: a reduced sweep (directed modes still exercised).
1256        for x in inputs.iter().step_by(9) {
1257            let oracle = Context::<mode::HalfEven>::new(560)
1258                .log2::<2>(x, None)
1259                .unwrap()
1260                .value();
1261            check_log2_differential::<mode::HalfEven>(500, x, &oracle.repr);
1262            check_log2_differential::<mode::Down>(500, x, &oracle.repr);
1263            check_log2_differential::<mode::Up>(500, x, &oracle.repr);
1264        }
1265    }
1266
1267    #[test]
1268    fn ln_1p_ball_bounds_negative_arg() {
1269        // Regression: `ln_1p_ball`'s input-error adjust dropped the precision-difference term
1270        // (−p_arg+p_ln). For an arg with 1+arg ∈ (0, 1) (e.g. atanh(x<0) near the pole),
1271        // `ln_compute` doubles the work precision (the s<0 path), so `ln_ball` sits at 2p while
1272        // `arg` stays at p — the missing +p under-bounded the adjust by B^p and the radius no
1273        // longer covered the true value.
1274        use crate::fbig::FBig;
1275        use crate::repr::Context;
1276        type F = FBig<mode::HalfEven, 10>;
1277        let ctx = Context::<mode::HalfEven>::new(10);
1278        // arg mid = −0.9999 at precision 10 (ulp = 1e-10), n = 5 ⇒ true arg = −0.9999000005.
1279        let mid = F::from_parts(IBig::from(-9999000000i64), -10)
1280            .with_precision(10)
1281            .value();
1282        let arg = Ball::<10>::with_error(mid, IBig::from(5));
1283        let ln_ball = ctx.ln_1p_ball::<10>(&arg, None);
1284        // true ln(1+arg) = ln(1 − 0.9999000005) = ln(9.99995e-5), oracle at precision 60.
1285        let one_plus_true = F::from_parts(IBig::from(999995i64), -10)
1286            .with_precision(0)
1287            .value();
1288        let true_ln = one_plus_true
1289            .with_precision(60)
1290            .value()
1291            .ln()
1292            .with_precision(0)
1293            .value();
1294        let diff = (ln_ball.mid.clone().with_precision(0).value() - true_ln).abs();
1295        let bound = F::from(ln_ball.n.clone()) * ln_ball.mid.ulp().with_precision(0).value();
1296        assert!(
1297            diff <= bound,
1298            "ln_1p_ball: |mid − true| = {diff} > n·ulp = {bound} (n = {}, missing −p_arg+p_ln?)",
1299            ln_ball.n
1300        );
1301    }
1302}