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;
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
122// `ln2`/`ln10`/`iacoth`/`ln_base`/`ln_compute` are the near-correct logarithm primitives: they
123// evaluate the series at a working precision and round once, without a Ziv certification step.
124// They live on `R: Round` so that base conversion (`with_base_and_precision`, which only needs a
125// near-correct constant `ln(B)`) can use them without inheriting the `ErrorBounds` bound. The
126// correctly-rounded public `ln`/`ln_1p` (in the `ErrorBounds` impl below) wrap `ln_compute` in a
127// Ziv loop.
128impl<R: Round> Context<R> {
129 /// Calculate log(2)
130 ///
131 /// The precision of the output will be larger than self.precision
132 #[inline]
133 fn ln2<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
134 if let Some(c) = cache {
135 return c.ln2::<B, R>(self.precision);
136 }
137 // log(2) = 4L(6) + 2L(99)
138 // see formula (24) from Gourdon, Xavier, and Pascal Sebah.
139 // "The Logarithmic Constant: Log 2." (2004)
140 4 * self.iacoth(6.into()) + 2 * self.iacoth(99.into())
141 }
142
143 /// Calculate log(10)
144 ///
145 /// The precision of the output will be larger than self.precision
146 #[inline]
147 fn ln10<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
148 if let Some(c) = cache {
149 return c.ln10::<B, R>(self.precision);
150 }
151 // log(10) = log(2) + log(5) = 3log(2) + 2L(9)
152 3 * self.ln2(None) + 2 * self.iacoth(9.into())
153 }
154
155 /// Calculate log(B), for internal use only
156 ///
157 /// The precision of the output will be larger than self.precision
158 #[inline]
159 pub(crate) fn ln_base<const B: Word>(&self, cache: Option<&mut ConstCache>) -> FBig<R, B> {
160 if let Some(c) = cache {
161 return c.ln_base::<B, R>(self.precision);
162 }
163 match B {
164 2 => self.ln2(None),
165 10 => self.ln10(None),
166 i if i.is_power_of_two() => self.ln2(None) * i.trailing_zeros(),
167 _ => {
168 // Near-correct ln(B) via the atanh series (no Ziv certification — base conversion
169 // only needs a near-correct constant). `ln_compute` is on `R: Round`, so this keeps
170 // `ln_base` callable from `R: Round` contexts (base conversion).
171 let guard = self.base_guard_digits::<B>() + 2;
172 self.ln_compute::<B>(
173 &Repr::new(Repr::<B>::BASE.into(), 0),
174 self.precision + guard,
175 false,
176 None,
177 )
178 .to_value_radius::<R>()
179 .0
180 }
181 }
182 }
183
184 /// `ln(B)` as a [`Ball`], carrying the mechanical radius.
185 ///
186 /// The cached bases (2, 10, powers of 2) evaluate correctly-rounded constants (error ≤ ½ ulp at
187 /// the working precision), so the fixed `8` ulps is a sound loose bound. A generic base falls
188 /// back to `ln_compute`'s atanh series, whose radius is ~(series terms + B) ulps — far larger
189 /// than 8 — so its mechanical radius is kept instead (a hard-coded `8` would under-bind the
190 /// `s·ln(B)` reconstruction error in `exp_compute`'s reduction).
191 pub(crate) fn ln_base_ball<const B: Word>(
192 &self,
193 mut cache: Option<&mut ConstCache>,
194 ) -> Ball<B> {
195 let ctx = Context::<mode::HalfEven>::new(self.precision);
196 match B {
197 10 => {
198 let logb = ctx.ln_base::<B>(reborrow_cache(&mut cache));
199 Ball::with_error(logb, IBig::from(8))
200 }
201 i if i.is_power_of_two() => {
202 let logb = ctx.ln_base::<B>(reborrow_cache(&mut cache));
203 Ball::with_error(logb, IBig::from(8))
204 }
205 _ => {
206 // Generic base: no cached sub-series applies, so compute ln(B) directly at the
207 // requested precision and keep `ln_compute`'s ball error.
208 ctx.ln_compute::<B>(
209 &Repr::new(Repr::<B>::BASE.into(), 0),
210 self.precision,
211 false,
212 reborrow_cache(&mut cache),
213 )
214 }
215 }
216 }
217
218 /// Calculate L(n) = acoth(n) = atanh(1/n) = 1/2 log((n+1)/(n-1)), given by the
219 /// series
220 ///
221 /// ```text
222 /// 1 n + 1 1
223 /// atanh(1/n) = — log(—————) = Σ ——————————————————
224 /// 2 n - 1 i≥0 n^(2i+1) · (2i+1)
225 /// ```
226 ///
227 /// This method is intended to be used in logarithm calculation,
228 /// so the precision of the output will be larger than desired precision.
229 ///
230 /// Evaluated by binary splitting (see [`iacoth_bs`][crate::math::cache::iacoth_bs]):
231 /// the exact integer tree state `(P, Q, T)` over `[1, N)` satisfies
232 /// `L(n) = (Q + T)/(n·Q)`, with `Q` kept at O(p) digits by the ratio-form
233 /// term recurrence.
234 fn iacoth<const B: Word>(&self, n: IBig) -> FBig<R, B> {
235 let n: u32 = (&n).try_into().expect("iacoth argument must fit in u32");
236
237 // number of series terms until r_k < B^{-p}: (2k+1)·log_B(n) > p.
238 // The count is generously over-provisioned, so a truncating cast stands in
239 // for a ceiling.
240 let log_b_n = n.log2_est() / B.log2_est();
241 let num_terms = (self.precision as f32 / (2.0 * log_b_n)) as usize + 10;
242
243 let (_p, q, t) = crate::math::cache::iacoth_bs(n, 1, num_terms + 1);
244
245 // L(n) = (Q + T) / (n·Q). Extra guard digits absorb the division's rounding
246 // (the binary-splitting state is exact, so only this single round loses anything).
247 let guard_digits = self.base_guard_digits::<B>();
248 let work_context = Self::new(self.precision + guard_digits + 2);
249
250 let num = work_context.convert_int::<B>(q.as_ibig() + &t).value();
251 let denom = work_context.convert_int::<B>(IBig::from(n) * &q).value();
252 num / denom
253 }
254
255 /// Evaluate `ln(x)` (or `ln(x+1)` when `one_plus`) at `work_precision` via the atanh series,
256 /// returning a [`Ball`] whose radius is derived mechanically by Ball arithmetic (error
257 /// propagates term-by-term through the series; cancellation and the `s·ln(B)` reconstruction
258 /// flow through the Ball scale factors).
259 ///
260 /// This is the near-correct computation core shared by the public Ziv-backed `ln`/`ln_1p`
261 /// (which wrap it in a retry loop) and by `ln_base` (which only needs a near-correct constant
262 /// `ln(B)`). It lives on `R: Round` so those near-correct callers don't inherit the
263 /// `ErrorBounds` bound.
264 pub(crate) fn ln_compute<const B: Word>(
265 &self,
266 x: &Repr<B>,
267 mut work_precision: usize,
268 one_plus: bool,
269 mut cache: Option<&mut ConstCache>,
270 ) -> Ball<B> {
271 // Round the input to the working precision; the input's own rounding is the only error
272 // introduced here.
273 let context = Context::<mode::HalfEven>::new(work_precision);
274 let x_ball = Ball::from_rounded(context.repr_round_ref(x).map(|r| FBig::new(r, context)));
275
276 // When one_plus is true and |x| < 1/B, the input is fed into the Maclaurin without scaling.
277 let no_scaling = one_plus && x_ball.mid.log2_est() < -B.log2_est();
278
279 let (s, mut x_scaled) = if no_scaling {
280 (0, x_ball)
281 } else {
282 let x_ball = if one_plus {
283 x_ball.add(&Ball::exact_int(work_precision, IBig::ONE))
284 } else {
285 x_ball
286 };
287
288 let log2 = x_ball.mid.log2_bounds().0;
289 let s = log2 as isize - (log2 < 0.) as isize; // floor(log2(x))
290
291 let mut exact = x_ball.n.is_zero();
292 let x_scaled = if B == 2 {
293 x_ball.shift(s) // exact (power-of-base shift)
294 } else if s > 0 {
295 // Exact divisor 2^s: the error shrinks by it directly (no general-division rational).
296 x_ball.div_exact(&(IBig::ONE << s as usize))
297 } else {
298 // Scaling by 2^|s| is exact (finite decimal × power of two). Use the tracking
299 // variant so an exact operand keeps n = 0 — otherwise the unconditional `+1`
300 // would be amplified by `rescale_precision` into a `B^precision`-sized error
301 // count that never shrinks across Ziv retries (the powf-of-base-<1 hang).
302 x_ball.scale_int_tracking(&(IBig::ONE << (-s) as usize), &mut exact)
303 };
304 debug_assert!(x_scaled.mid >= FBig::<mode::HalfEven, B>::ONE);
305 (s, x_scaled)
306 };
307
308 // The reconstruction 2·sum + s·ln(B) *cancels* for x < 1 (s < 0), so the series runs at
309 // double precision to keep the pre-cancellation sum accurate. The finer ulp rescales `n`.
310 if s < 0 || x_scaled.mid.repr().sign() == Sign::Negative {
311 work_precision += self.precision;
312 x_scaled.rescale_precision(self.precision);
313 }
314 let work_context = Context::<mode::HalfEven>::new(work_precision);
315
316 // after the number is scaled to nearly one, use Maclaurin series on log(x) = 2atanh(z):
317 // let z = (x-1)/(x+1) < 1, log(x) = 2atanh(z) = 2Σ(z²ⁱ⁺¹/(2i+1)) for i = 1,3,5,...
318 let z = if no_scaling {
319 let two = Ball::exact_int(work_precision, IBig::from(2));
320 let den = x_scaled.add(&two);
321 x_scaled.div(&den)
322 } else {
323 let one = Ball::exact_int(work_precision, IBig::ONE);
324 let num = x_scaled.sub(&one);
325 let den = x_scaled.add(&one);
326 num.div(&den)
327 };
328 let z2 = z.mul(&z);
329 let mut pow = z.clone();
330 let mut sum = z;
331 let mut k: usize = 3;
332 loop {
333 pow = pow.mul(&z2);
334
335 let increase = pow.div_int(k);
336 if increase.mid.abs_cmp(&sum.mid.ulp_lb()).is_le() {
337 break;
338 }
339
340 sum = sum.add(&increase);
341 k += 2;
342 }
343
344 // Omitted series tail: the first omitted term is ≤ sum.ulp_lb(), and the tail of the
345 // atanh series shrinks by z² per step with 1/(1−z²) < B for x_scaled ∈ [1, B), so the
346 // tail is < B·sum.ulp_lb() < B ulps of sum.
347 sum.inflate(&IBig::from(B));
348
349 // compose the logarithm of the original number
350 let sum2 = sum.scale_int(&IBig::from(2));
351 if no_scaling {
352 sum2
353 } else {
354 // ln(2) as a ball. The constant evaluates the atanh series via binary splitting at
355 // work + guard digits and rounds once to `work_precision`, so its error is a handful
356 // of work-precision ulps; 8 is a conservative sound bound for every code path
357 // (cached and uncached).
358 let ln2 = work_context.ln2::<B>(reborrow_cache(&mut cache));
359 let ln2 = Ball::with_error(ln2, IBig::from(8));
360 sum2.add(&ln2.scale_int(&IBig::from(s)))
361 }
362 }
363
364 /// `ln(1 + arg)` of a *ball* input. [`ln_compute`](Self::ln_compute) evaluates the series on
365 /// `arg.mid`; the input ball's own error `|θ| ≤ arg.n·ulp(arg)` then contributes `|θ|/(1+arg)`
366 /// to the log. Bound via `(1+arg)`'s ball magnitude: for a mostly-correct argument the factor
367 /// `1/(1−|θ|/(1+arg)) ≤ 2` is sound, so the adjustment is `⌈2·n_arg·ulp_arg/((1+arg)·ulp_ln)⌉`.
368 pub(crate) fn ln_1p_ball<const B: Word>(
369 &self,
370 arg: &Ball<B>,
371 mut cache: Option<&mut ConstCache>,
372 ) -> Ball<B> {
373 let mut ln_ball =
374 self.ln_compute::<B>(arg.mid.repr(), self.precision, true, reborrow_cache(&mut cache));
375 let den = arg.add(&Ball::exact_int(self.precision, IBig::ONE));
376 let e_d = den.mid.repr().exponent; // (1+arg) = sig_d·B^(e_d)
377 let sig_d = den.mid.repr().significand.clone().abs();
378 // `lead_*` is the leading position (`lead_exp`), so `ulp_arg = B^(lead_arg − p_arg)` and
379 // `ulp_ln = B^(lead_ln − p_ln)`. The input error propagates as
380 // n_arg·ulp_arg / ((1+arg)·ulp_ln) = n_arg·B^(lead_arg − p_arg − e_d − lead_ln + p_ln)/sig_d;
381 // ×2 for the 1/(1−|θ|/(1+arg)) factor.
382 // The precision difference is essential: `ln_compute`'s s<0 path runs at double precision,
383 // so `ln_ball` sits at 2·self.precision while `arg` stays at self.precision — dropping the
384 // `−p_arg+p_ln` term under-bounds the adjust by B^(p_ln−p_arg) (atanh(x<0) near the pole
385 // then mis-certifies, e.g. off by 2^13 ulps).
386 let lead_arg = Ball::lead_exp(&arg.mid);
387 let p_arg = arg.mid.precision();
388 let lead_ln = Ball::lead_exp(&ln_ball.mid);
389 let p_ln = ln_ball.mid.precision();
390 let shift = lead_arg - p_arg as isize - e_d - lead_ln + p_ln as isize;
391 let num = ceil_shift::<B>(2 * &arg.n, shift);
392 let adjust = (num + &sig_d - IBig::ONE) / sig_d;
393 ln_ball.inflate(&adjust);
394 ln_ball
395 }
396}
397
398// `ln`/`ln_1p` are correctly rounded via the Ziv loop, whose containment test needs the rounding
399// preimage (`R: ErrorBounds`). They delegate the series to `ln_compute`.
400impl<R: ErrorBounds> Context<R> {
401 /// Calculate the natural logarithm function (`log(x)`) on the float number under this context.
402 ///
403 /// # Examples
404 ///
405 /// ```
406 /// # use core::str::FromStr;
407 /// # use dashu_base::ParseError;
408 /// # use dashu_float::DBig;
409 /// use dashu_base::Approximation::*;
410 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
411 ///
412 /// let context = Context::<HalfAway>::new(2);
413 /// let a = DBig::from_str("1.234")?;
414 /// assert_eq!(context.ln(&a.repr(), None), Ok(Inexact(DBig::from_str("0.21")?, NoOp)));
415 /// # Ok::<(), ParseError>(())
416 /// ```
417 #[inline]
418 pub fn ln<const B: Word>(
419 &self,
420 x: &Repr<B>,
421 cache: Option<&mut ConstCache>,
422 ) -> FpResult<FBig<R, B>> {
423 if x.is_infinite() {
424 return Err(FpError::InfiniteInput);
425 }
426 if x.significand.is_zero() {
427 // ln(±0) = -inf (a value, not an error)
428 return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
429 }
430 if x.sign() == Sign::Negative {
431 return Err(FpError::OutOfDomain);
432 }
433 self.ln_internal(x, false, cache)
434 }
435
436 /// Calculate the natural logarithm function (`log(x+1)`) on the float number under this context.
437 ///
438 /// # Examples
439 ///
440 /// ```
441 /// # use core::str::FromStr;
442 /// # use dashu_base::ParseError;
443 /// # use dashu_float::DBig;
444 /// use dashu_base::Approximation::*;
445 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
446 ///
447 /// let context = Context::<HalfAway>::new(2);
448 /// let a = DBig::from_str("0.1234")?;
449 /// assert_eq!(context.ln_1p(&a.repr(), None), Ok(Inexact(DBig::from_str("0.12")?, AddOne)));
450 /// # Ok::<(), ParseError>(())
451 /// ```
452 #[inline]
453 pub fn ln_1p<const B: Word>(
454 &self,
455 x: &Repr<B>,
456 cache: Option<&mut ConstCache>,
457 ) -> FpResult<FBig<R, B>> {
458 if x.is_infinite() {
459 return Err(FpError::InfiniteInput);
460 }
461 // Domain of ln_1p is x > -1. x == -1 gives -inf; x < -1 is out of domain.
462 if x.sign() == Sign::Negative && !x.significand.is_zero() {
463 match FBig::<R, B>::new(x.clone(), *self).abs_cmp(&FBig::ONE) {
464 Ordering::Greater => return Err(FpError::OutOfDomain), // x < -1
465 Ordering::Equal => return Ok(Exact(FBig::new(Repr::neg_infinity(), *self))),
466 _ => {}
467 }
468 }
469 self.ln_internal(x, true, cache)
470 }
471
472 fn ln_internal<const B: Word>(
473 &self,
474 x: &Repr<B>,
475 one_plus: bool,
476 mut cache: Option<&mut ConstCache>,
477 ) -> FpResult<FBig<R, B>> {
478 assert_finite(x);
479
480 // Exact special cases first: they need no rounding, so a precision-0 (unlimited)
481 // value such as `FBig::ONE` or the one from `try_from(0.0)` must still resolve
482 // ln/ln_1p exactly rather than tripping the limited-precision assertion below.
483 if !one_plus && x.is_one() {
484 return Ok(Exact(FBig::ZERO)); // ln(1) = +0
485 }
486 if one_plus && x.significand.is_zero() {
487 // ln_1p(±0) = ±0
488 let zero = if x.is_neg_zero() {
489 FBig::new(Repr::neg_zero(), *self)
490 } else {
491 FBig::ZERO
492 };
493 return Ok(Exact(zero));
494 }
495
496 assert_limited_precision(self.precision);
497
498 // Correct rounding via the Ziv loop: `ln_compute` evaluates the atanh series at `p + guard`
499 // and reports a provable error radius; the driver retries with more guard digits until the
500 // approximation's error interval lies entirely inside one rounding bin. The guard is a
501 // *performance* knob (first-attempt hit rate), not a correctness backstop — Ziv certifies
502 // the result. (The pre-Ziv `+ 2` is retained: with the conservative radius below it is still
503 // needed for the first attempt to clear the half-ulp preimage at typical precisions.)
504 let base_guard = self.base_guard_digits::<B>() + 2;
505 self.ziv(base_guard + one_plus as usize, |guard| {
506 Ok(self
507 .ln_compute::<B>(x, self.precision + guard, one_plus, reborrow_cache(&mut cache))
508 .to_value_radius::<R>())
509 })
510 }
511
512 /// Calculate the base-2 logarithm (`log2(x)`) on the float number under this context.
513 ///
514 /// Correctly rounded to the context's precision under any rounding mode; for an exact power
515 /// of two the result is the exact integer `log2(x)`.
516 ///
517 /// # Domain
518 ///
519 /// `log2(±0) = −∞` and a negative (non-zero) input is out of domain; an infinite input is an
520 /// error (a finite context cannot produce the infinite `log2(+∞) = +∞` exactly).
521 ///
522 /// # Examples
523 ///
524 /// ```
525 /// # use core::str::FromStr;
526 /// # use dashu_base::ParseError;
527 /// # use dashu_float::DBig;
528 /// use dashu_base::Approximation::*;
529 /// use dashu_float::{Context, round::{mode::HalfAway, Rounding::*}};
530 ///
531 /// let context = Context::<HalfAway>::new(4);
532 /// let a = DBig::from_str("10")?;
533 /// assert_eq!(context.log2(&a.repr(), None), Ok(Inexact(DBig::from_str("3.322")?, AddOne)));
534 /// # Ok::<(), ParseError>(())
535 /// ```
536 #[inline]
537 pub fn log2<const B: Word>(
538 &self,
539 x: &Repr<B>,
540 cache: Option<&mut ConstCache>,
541 ) -> FpResult<FBig<R, B>> {
542 if x.is_infinite() {
543 return Err(FpError::InfiniteInput);
544 }
545 if x.significand.is_zero() {
546 // log2(±0) = -inf (a value, not an error)
547 return Ok(Exact(FBig::new(Repr::neg_infinity(), *self)));
548 }
549 if x.sign() == Sign::Negative {
550 return Err(FpError::OutOfDomain);
551 }
552 self.log2_internal(x, cache)
553 }
554
555 fn log2_internal<const B: Word>(
556 &self,
557 x: &Repr<B>,
558 mut cache: Option<&mut ConstCache>,
559 ) -> FpResult<FBig<R, B>> {
560 assert_finite(x);
561
562 // Exact shortcuts first — they also cover unlimited precision, which the Ziv loop below
563 // rejects via its limited-precision assertion.
564 if x.is_one() {
565 return Ok(Exact(FBig::ZERO)); // log2(1) = +0
566 }
567
568 // Exact power-of-two shortcut: if x = 2^k for an integer k, log2(x) = k. This is *required*
569 // for directed rounding — the Ziv loop below cannot certify an exactly-representable
570 // result whose true value sits on a rounding boundary (its shrinking error interval
571 // always straddles the boundary), so without this shortcut log2(2^-159) under `Up` would
572 // exhaust the retry cap and return k + 1 ulp instead of the exact k.
573 //
574 // log2(x) = log2(significand) + exponent·log2(B). With significand = 2^m this is an exact
575 // integer whenever log2(B) is integral (B a power of two), or — for a non-power-of-two
576 // base — when the exponent is zero.
577 let mag = (&x.significand).unsigned_abs();
578 if mag.is_power_of_two() && (x.exponent == 0 || B.is_power_of_two()) {
579 let m = mag.trailing_zeros().unwrap(); // = log2(significand)
580 let log2_b = B.trailing_zeros() as isize;
581 let k = IBig::from(m) + IBig::from(x.exponent) * IBig::from(log2_b);
582 return Ok(self.convert_int::<B>(k));
583 }
584
585 assert_limited_precision(self.precision);
586
587 // log2(x) = ln(x)/ln(2), correctly rounded via the Ziv loop. Both logarithms come from the
588 // Ball-based `ln_compute`, and dividing them as Balls composes the radius mechanically:
589 // the quotient's error is bounded from the two logarithms' relative errors, with no
590 // directed-interval bookkeeping or guard-digit constant. The driver certifies the result
591 // against the rounding preimage exactly as before.
592 let initial_guard = self.base_guard_digits::<B>() + 4;
593 self.ziv(initial_guard, |guard| {
594 let work_precision = self.precision + guard;
595 let lx = self.ln_compute::<B>(x, work_precision, false, reborrow_cache(&mut cache));
596 let two = Repr::new(IBig::from(2), 0);
597 let l2 = self.ln_compute::<B>(&two, work_precision, false, reborrow_cache(&mut cache));
598 Ok(lx.div(&l2).to_value_radius::<R>())
599 })
600 }
601}
602
603#[cfg(test)]
604mod tests {
605 use super::*;
606 use crate::round::mode;
607 use alloc::vec::Vec;
608 use dashu_base::BitTest;
609
610 #[test]
611 fn test_ln_zero_is_neg_infinity() {
612 let ctx = Context::<mode::HalfEven>::new(53);
613 let r = ctx.ln::<2>(&Repr::<2>::zero(), None).unwrap().value();
614 assert!(r.repr().is_infinite());
615 assert_eq!(r.repr().sign(), Sign::Negative);
616 }
617
618 #[test]
619 fn test_iacoth() {
620 let context = Context::<mode::Zero>::new(10);
621 let binary_6 = context.iacoth::<2>(6.into()).with_precision(10).value();
622 assert_eq!(binary_6.repr.significand, IBig::from(689));
623 let decimal_6 = context.iacoth::<10>(6.into()).with_precision(10).value();
624 assert_eq!(decimal_6.repr.significand, IBig::from(1682361183));
625
626 let context = Context::<mode::Zero>::new(40);
627 let decimal_6 = context.iacoth::<10>(6.into()).with_precision(40).value();
628 assert_eq!(
629 decimal_6.repr.significand,
630 IBig::from_str_radix("1682361183106064652522967051084960450557", 10).unwrap()
631 );
632
633 let context = Context::<mode::Zero>::new(201);
634 let binary_6 = context.iacoth::<2>(6.into()).with_precision(201).value();
635 assert_eq!(
636 binary_6.repr.significand,
637 IBig::from_str_radix(
638 "2162760151454160450909229890833066944953539957685348083415205",
639 10
640 )
641 .unwrap()
642 );
643 }
644
645 #[test]
646 fn test_ln2_ln10() {
647 let context = Context::<mode::Zero>::new(45);
648 let decimal_ln2 = context.ln2::<10>(None).with_precision(45).value();
649 assert_eq!(
650 decimal_ln2.repr.significand,
651 IBig::from_str_radix("693147180559945309417232121458176568075500134", 10).unwrap()
652 );
653 let decimal_ln10 = context.ln10::<10>(None).with_precision(45).value();
654 assert_eq!(
655 decimal_ln10.repr.significand,
656 IBig::from_str_radix("230258509299404568401799145468436420760110148", 10).unwrap()
657 );
658
659 let context = Context::<mode::Zero>::new(180);
660 let binary_ln2 = context.ln2::<2>(None).with_precision(180).value();
661 assert_eq!(
662 binary_ln2.repr.significand,
663 IBig::from_str_radix("1062244963371879310175186301324412638028404515790072203", 10)
664 .unwrap()
665 );
666 let binary_ln10 = context.ln10::<2>(None).with_precision(180).value();
667 assert_eq!(
668 binary_ln10.repr.significand,
669 IBig::from_str_radix("882175346869410758689845931257775553286341791676474847", 10)
670 .unwrap()
671 );
672 }
673
674 #[test]
675 fn test_log2_domain() {
676 let ctx = Context::<mode::HalfEven>::new(53);
677 // log2(±0) = -inf (a value, not an error)
678 let r = ctx.log2::<2>(&Repr::<2>::zero(), None).unwrap().value();
679 assert!(r.repr.is_infinite());
680 assert_eq!(r.repr.sign(), Sign::Negative);
681 // log2(negative) is out of domain
682 assert!(matches!(
683 ctx.log2::<2>(&Repr::new((-1).into(), 0), None),
684 Err(FpError::OutOfDomain)
685 ));
686 // an infinite input is rejected
687 assert!(matches!(ctx.log2::<2>(&Repr::infinity(), None), Err(FpError::InfiniteInput)));
688 }
689
690 #[test]
691 fn test_log2_exact_power_of_two() {
692 // log2(2^k) = k exactly under every rounding mode. Regression for the directed-rounding
693 // defect: rounding ln(x) and ln(2) each toward the mode and dividing once does not bound
694 // the quotient, so previously log2(2^-159) under `Up` returned -159 + 1 ulp.
695 let p = 53;
696 for k in [0isize, 1, -1, 5, 159, -159, 1000, -1000] {
697 let x = Repr::<2>::new(IBig::from(1), k); // 2^k
698 let r_down = Context::<mode::Down>::new(p)
699 .log2::<2>(&x, None)
700 .unwrap()
701 .value();
702 let r_up = Context::<mode::Up>::new(p)
703 .log2::<2>(&x, None)
704 .unwrap()
705 .value();
706 let r_zero = Context::<mode::Zero>::new(p)
707 .log2::<2>(&x, None)
708 .unwrap()
709 .value();
710 let r_he = Context::<mode::HalfEven>::new(p)
711 .log2::<2>(&x, None)
712 .unwrap()
713 .value();
714 // Every directed mode produces the identical value — no mode-dependent ulp.
715 assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log2(2^{k})");
716 assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log2(2^{k})");
717 assert_eq!(r_zero.repr, r_he.repr, "Zero != HalfEven for log2(2^{k})");
718 // And that value is exactly k.
719 assert_eq!(r_he.to_int().value(), IBig::from(k), "value for log2(2^{k})");
720 }
721 }
722
723 #[test]
724 fn test_log2_exact_power_of_two_decimal_base() {
725 // In a non-power-of-two base the shortcut still fires when the exponent is zero: a
726 // significand that is itself a power of two makes x = 2^m exactly.
727 let p = 53;
728 for (sig, want) in [(8i32, 3isize), (1024, 10), (2, 1), (32, 5)] {
729 let x = Repr::<10>::new(IBig::from(sig), 0);
730 let r_down = Context::<mode::Down>::new(p)
731 .log2::<10>(&x, None)
732 .unwrap()
733 .value();
734 let r_up = Context::<mode::Up>::new(p)
735 .log2::<10>(&x, None)
736 .unwrap()
737 .value();
738 let r_he = Context::<mode::HalfEven>::new(p)
739 .log2::<10>(&x, None)
740 .unwrap()
741 .value();
742 assert_eq!(r_down.repr, r_he.repr, "Down != HalfEven for log2({sig}) base 10");
743 assert_eq!(r_up.repr, r_he.repr, "Up != HalfEven for log2({sig}) base 10");
744 assert_eq!(r_he.to_int().value(), IBig::from(want), "value for log2({sig}) base 10");
745 }
746 }
747
748 /// For a non-power-of-two significand `sig` (so `log2` is irrational and never lands on a
749 /// rounding boundary), each directed result must equal a high-precision oracle rounded to the
750 /// target precision under the same mode — the definition of correct rounding.
751 fn check_log2_directed_matches_oracle<const B: Word>(sig: u32, p: usize) {
752 let oracle_ctx = Context::<mode::HalfEven>::new(p + 40);
753 let x = Repr::<B>::new(IBig::from(sig), 0);
754 let oracle = oracle_ctx.log2::<B>(&x, None).unwrap().value();
755
756 let want_down = Context::<mode::Down>::new(p)
757 .repr_round_ref(&oracle.repr)
758 .value();
759 let want_up = Context::<mode::Up>::new(p)
760 .repr_round_ref(&oracle.repr)
761 .value();
762 let want_he = Context::<mode::HalfEven>::new(p)
763 .repr_round_ref(&oracle.repr)
764 .value();
765
766 let got_down = Context::<mode::Down>::new(p)
767 .log2::<B>(&x, None)
768 .unwrap()
769 .value();
770 let got_up = Context::<mode::Up>::new(p)
771 .log2::<B>(&x, None)
772 .unwrap()
773 .value();
774 let got_he = Context::<mode::HalfEven>::new(p)
775 .log2::<B>(&x, None)
776 .unwrap()
777 .value();
778
779 assert_eq!(got_down.repr, want_down, "log2({sig}) base {B} under Down");
780 assert_eq!(got_up.repr, want_up, "log2({sig}) base {B} under Up");
781 assert_eq!(got_he.repr, want_he, "log2({sig}) base {B} under HalfEven");
782 }
783
784 #[test]
785 fn test_log2_directed_matches_oracle() {
786 let p = 24;
787 for sig in [3u32, 7, 10, 12345, 65537] {
788 check_log2_directed_matches_oracle::<2>(sig, p);
789 }
790 // Exercise a non-power-of-two base through the Ziv interval path too.
791 for sig in [3u32, 7, 10, 12345] {
792 check_log2_directed_matches_oracle::<10>(sig, p);
793 }
794 }
795
796 // log2 of a value whose result sits within ~1 work-ulp of a power of two must still round to
797 // the correct neighbor under directed modes. log2(f64::MAX) ≈ 1024 − 2^-53/ln2 sits just below
798 // 1024; under Down at p=53 the answer is 1024 − 2^-42 (the largest p=53 value ≤ it), but an
799 // unsound radius previously let Ziv certify 1024 on the first attempt.
800 #[test]
801 fn test_log2_just_below_power_of_two_directed() {
802 let x = FBig::<mode::HalfEven, 2>::try_from(f64::MAX).unwrap();
803 // High-precision oracle, then re-rounded to the target precision under each mode.
804 let oracle = Context::<mode::HalfEven>::new(200)
805 .log2::<2>(x.repr(), None)
806 .unwrap()
807 .value();
808 for p in [24usize, 40, 53, 64] {
809 let want_down = Context::<mode::Down>::new(p)
810 .repr_round_ref(&oracle.repr)
811 .value();
812 let want_up = Context::<mode::Up>::new(p)
813 .repr_round_ref(&oracle.repr)
814 .value();
815 let got_down = Context::<mode::Down>::new(p)
816 .log2::<2>(x.repr(), None)
817 .unwrap()
818 .value();
819 let got_up = Context::<mode::Up>::new(p)
820 .log2::<2>(x.repr(), None)
821 .unwrap()
822 .value();
823 assert_eq!(got_down.repr(), &want_down, "p={p} Down");
824 assert_eq!(got_up.repr(), &want_up, "p={p} Up");
825 // Directed invariant: Up ≥ Down.
826 assert!(got_up.repr() >= got_down.repr(), "p={p} Up < Down");
827 }
828 }
829
830 /// Directed `ln` of `x ∈ [1, 2)` must match a high-precision oracle re-rounded under the same
831 /// mode. This binade (s = 0) is where the radius under-estimated the error: `result` inherits
832 /// `ln_base`'s over-delivered context, and for `x` just above 1 the scaling even classifies
833 /// `s = −1`, so `2·sum + s·ln2` cancels and the error stays at `sum`'s magnitude while
834 /// `result`'s collapses — both make `result.ulp()` the wrong scale for the radius.
835 fn check_ln_directed_in_unit_binade(k: usize, p: usize) {
836 // x = (2^k + 1) * 2^-k = 1 + 2^-k, exactly representable at precision p when k < p.
837 let x = Repr::<2>::new(IBig::from(1i64 << k) + IBig::ONE, -(k as isize));
838 let oracle = Context::<mode::HalfEven>::new(p + 60)
839 .ln::<2>(&x, None)
840 .unwrap()
841 .value();
842 let want_down = Context::<mode::Down>::new(p)
843 .repr_round_ref(&oracle.repr)
844 .value();
845 let want_up = Context::<mode::Up>::new(p)
846 .repr_round_ref(&oracle.repr)
847 .value();
848 let got_down = Context::<mode::Down>::new(p)
849 .ln::<2>(&x, None)
850 .unwrap()
851 .value();
852 let got_up = Context::<mode::Up>::new(p)
853 .ln::<2>(&x, None)
854 .unwrap()
855 .value();
856 assert_eq!(got_down.repr(), &want_down, "ln(1+2^-{k}) p={p} Down");
857 assert_eq!(got_up.repr(), &want_up, "ln(1+2^-{k}) p={p} Up");
858 assert!(got_up.repr() >= got_down.repr(), "ln(1+2^-{k}) p={p} Up < Down");
859 }
860
861 #[test]
862 fn test_ln_directed_near_one() {
863 // Sweep the near-1 binade at low precision, including the k close to p cases that
864 // classify as s = −1 and cancel.
865 for p in [24usize, 40, 53] {
866 for k in 1..p.saturating_sub(1) {
867 check_ln_directed_in_unit_binade(k, p);
868 }
869 }
870 }
871
872 /// Fixed inputs for the `log2` oracle differential: moderate magnitudes and the
873 /// near-boundary regimes the legacy directed-interval implementation was specifically sized for.
874 fn log2_diff_inputs() -> Vec<Repr<2>> {
875 let mut v = Vec::new();
876 for x in [0.5f64, 1.5, 2.0, 3.0, 10.0, 1000.0, 1e-6, 123.456, 2.5e-10] {
877 v.push(FBig::<mode::HalfEven, 2>::try_from(x).unwrap().into_repr());
878 }
879 // Exact powers of two.
880 for k in [-100isize, -50, -10, -1, 0, 1, 10, 50, 100] {
881 v.push(Repr::new(IBig::ONE, k));
882 }
883 // Just below the largest f64 (log2 ≈ 1024, the directed-regime case in the old comment).
884 v.push(
885 FBig::<mode::HalfEven, 2>::try_from(f64::MAX)
886 .unwrap()
887 .into_repr(),
888 );
889 // The [1, 2) unit binade and its mirror below 1: 1 ± 2^-k and 2 − 2^-k exercise the
890 // s = −1 cancellation (the second-classified-s-−1 case the doubling compensates).
891 for k in 1usize..=60 {
892 v.push(Repr::new(IBig::from(1u64 << k) + IBig::ONE, -(k as isize))); // 1 + 2^-k
893 v.push(Repr::new(IBig::from((1u64 << k) - 1), -(k as isize))); // 1 − 2^-k
894 v.push(Repr::new(IBig::from((1u64 << (k + 1)) - 1), -(k as isize)));
895 // 2 − 2^-k
896 }
897 v
898 }
899
900 /// The Ball-based `log2` must round exactly like a high-precision oracle (the definition of
901 /// correct rounding) across precisions, modes, and the near-boundary inputs.
902 ///
903 /// The legacy directed-interval implementation is *not* used as the oracle: it has its own
904 /// residual 1-ulp bug under directed rounding for `log2(1 − 2^-k)` at p=50 (verified against
905 /// an independent high-precision computation) — exactly the class of defect this pilot
906 /// replaces.
907 fn check_log2_differential<R: ErrorBounds>(p: usize, x: &Repr<2>, oracle: &Repr<2>) {
908 let ctx = Context::<R>::new(p);
909 let want = ctx.repr_round_ref(oracle).value();
910 let got = ctx.log2_internal::<2>(x, None).unwrap().value();
911 assert_eq!(got.repr, want, "p={p} {} x={x:?}", core::any::type_name::<R>(),);
912 }
913
914 /// Regression: `ln_compute`'s s<0 path (base < 1) must NOT inflate its error count with the
915 /// working precision. An exactly-representable input scaled by a power of two is exact, so the
916 /// radius must shrink monotonically as the work precision grows — otherwise the composed
917 /// `pow_exp_log` chain's radius stays constant and the Ziv loop hangs (powf of a base < 1).
918 #[test]
919 fn ln_small_base_radius_shrinks_with_guard() {
920 let ctx = Context::<mode::HalfEven>::new(50);
921 // 0.2668 (base 10): s = floor(log2(0.2668)) = -2, the s < 0 path.
922 let x = Repr::<10>::new(IBig::from(2668), -4);
923 for guard in [4usize, 12, 40, 120] {
924 let ball = ctx.ln_compute::<10>(&x, 50 + guard, false, None);
925 // The regression: n must be O(series terms) (~10^5, bit_len < 30), NOT inflated to
926 // ~B^50 ≈ 10^50 (bit_len ~166) by the s<0 reduction's spurious +1.
927 assert!(
928 ball.n.bit_len() < 30,
929 "n = {} ({} bits) too large at guard={guard}: the s<0 reduction inflated it",
930 ball.n,
931 ball.n.bit_len()
932 );
933 // The radius in target (precision 50) ulps must fit a preimage so Ziv certifies on the
934 // first attempt: n·B^(E−p_ball)·B^(50−E) ≤ 1.
935 let radius_target = crate::ball::ceil_shift::<10>(
936 ball.n.clone(),
937 Ball::lead_exp(&ball.mid) - ball.mid.precision() as isize + 50,
938 );
939 assert!(
940 radius_target <= IBig::ONE,
941 "radius {radius_target} ulps at guard={guard} does not certify (n={})",
942 ball.n
943 );
944 }
945 }
946
947 #[test]
948 fn log2_ball_matches_oracle() {
949 let inputs = log2_diff_inputs();
950 // Moderate precisions: full input sweep, all five modes.
951 for p in [20usize, 50, 100] {
952 for x in &inputs {
953 // The oracle is mode-independent: a high-precision HalfEven value re-rounded
954 // under each target mode.
955 let oracle = Context::<mode::HalfEven>::new(p + 60)
956 .log2::<2>(x, None)
957 .unwrap()
958 .value();
959 check_log2_differential::<mode::HalfEven>(p, x, &oracle.repr);
960 check_log2_differential::<mode::Down>(p, x, &oracle.repr);
961 check_log2_differential::<mode::Up>(p, x, &oracle.repr);
962 check_log2_differential::<mode::Zero>(p, x, &oracle.repr);
963 check_log2_differential::<mode::Away>(p, x, &oracle.repr);
964 }
965 }
966 // The arbitrary-precision regime: a reduced sweep (directed modes still exercised).
967 for x in inputs.iter().step_by(9) {
968 let oracle = Context::<mode::HalfEven>::new(560)
969 .log2::<2>(x, None)
970 .unwrap()
971 .value();
972 check_log2_differential::<mode::HalfEven>(500, x, &oracle.repr);
973 check_log2_differential::<mode::Down>(500, x, &oracle.repr);
974 check_log2_differential::<mode::Up>(500, x, &oracle.repr);
975 }
976 }
977
978 #[test]
979 fn ln_1p_ball_bounds_negative_arg() {
980 // Regression: `ln_1p_ball`'s input-error adjust dropped the precision-difference term
981 // (−p_arg+p_ln). For an arg with 1+arg ∈ (0, 1) (e.g. atanh(x<0) near the pole),
982 // `ln_compute` doubles the work precision (the s<0 path), so `ln_ball` sits at 2p while
983 // `arg` stays at p — the missing +p under-bounded the adjust by B^p and the radius no
984 // longer covered the true value.
985 use crate::fbig::FBig;
986 use crate::repr::Context;
987 type F = FBig<mode::HalfEven, 10>;
988 let ctx = Context::<mode::HalfEven>::new(10);
989 // arg mid = −0.9999 at precision 10 (ulp = 1e-10), n = 5 ⇒ true arg = −0.9999000005.
990 let mid = F::from_parts(IBig::from(-9999000000i64), -10)
991 .with_precision(10)
992 .value();
993 let arg = Ball::<10>::with_error(mid, IBig::from(5));
994 let ln_ball = ctx.ln_1p_ball::<10>(&arg, None);
995 // true ln(1+arg) = ln(1 − 0.9999000005) = ln(9.99995e-5), oracle at precision 60.
996 let one_plus_true = F::from_parts(IBig::from(999995i64), -10)
997 .with_precision(0)
998 .value();
999 let true_ln = one_plus_true
1000 .with_precision(60)
1001 .value()
1002 .ln()
1003 .with_precision(0)
1004 .value();
1005 let diff = (ln_ball.mid.clone().with_precision(0).value() - true_ln).abs();
1006 let bound = F::from(ln_ball.n.clone()) * ln_ball.mid.ulp().with_precision(0).value();
1007 assert!(
1008 diff <= bound,
1009 "ln_1p_ball: |mid − true| = {diff} > n·ulp = {bound} (n = {}, missing −p_arg+p_ln?)",
1010 ln_ball.n
1011 );
1012 }
1013}