Skip to main content

decimal_scaled/types/
log_exp.rs

1// SPDX-FileCopyrightText: 2026 John Moxley
2// SPDX-License-Identifier: MIT OR Apache-2.0
3
4//! Logarithm and exponential methods for [`D38`].
5//!
6//! # Methods
7//!
8//! - **Logarithms:** [`D38::ln`] / [`D38::log`] / [`D38::log2`] / [`D38::log10`].
9//! - **Exponentials:** [`D38::exp`] / [`D38::exp2`].
10//!
11//! # The four-variant matrix
12//!
13//! Each function ships four entry points so a single name covers
14//! every (precision × rounding) combination:
15//!
16//! | Method            | Guard width    | Rounding mode               |
17//! |-------------------|----------------|------------------------------|
18//! | `<fn>_strict`     | crate default  | crate default               |
19//! | `<fn>_strict_with`| crate default  | caller-supplied              |
20//! | `<fn>_approx`     | caller-chosen  | crate default               |
21//! | `<fn>_approx_with`| caller-chosen  | caller-supplied              |
22//!
23//! `_strict` runs at `SCALE + STRICT_GUARD` (const-folded so LLVM
24//! specialises one optimal kernel per `SCALE`). `_approx` runs at
25//! `SCALE + working_digits` chosen at call time — drop below
26//! `STRICT_GUARD` to trade precision for latency (the Mercator /
27//! Taylor series shortens proportionally), raise above for more
28//! headroom on chained compositions. When `working_digits ==
29//! STRICT_GUARD` the `_approx_with` body redirects to `_strict_with`
30//! so the const-folded path is never displaced.
31//!
32//! `ln_strict` uses range reduction plus a Mercator series;
33//! `exp_strict` uses range reduction plus a Taylor series; the
34//! remaining methods compose those two. All four variants are
35//! integer-only, `no_std`-compatible, and correctly rounded under
36//! the selected mode.
37//!
38//! Without the `strict` feature, the plain `<fn>` is an f64-bridge
39//! (calls the inherent `f64` intrinsic, gated on `std`). With
40//! `strict` it dispatches to `<fn>_strict`. See `docs/strict-mode.md`
41//! for the full dual-API and feature rules.
42//!
43//! # Layering
44//!
45//! Every public method on this file is a one-line delegate into
46//! `policy::ln` or `policy::exp`. The
47//! correctly-rounded kernels (`ln_fixed`, `exp_fixed`,
48//! `STRICT_GUARD`, the `wide_ln2` / `wide_ln10` constants, and the
49//! per-variant `ln_strict` / `ln_with` / `log_strict` / `log_with` /
50//! `log2_*` / `log10_*` / `exp_strict` / `exp_with` / `exp2_*`
51//! `Fixed`-shape functions) live in
52//! [`crate::algos::ln::ln_series_2limb`] and
53//! [`crate::algos::exp::exp_series_2limb`]. This file is a typed-shell
54//! surface; there are zero `crate::algos::*` or
55//! `crate::algos::support::fixed::*` references in it.
56//!
57//! # Precision
58//!
59//! The f64-bridge forms are **Lossy** — `self` round-trips through
60//! `f64`. Every `_strict` / `_strict_with` / `_approx` /
61//! `_approx_with` form is **correctly rounded** under the selected
62//! [`RoundingMode`]: the result is within 0.5 ULP of the exact
63//! value. They evaluate the series in the `algos::support::fixed::Fixed`
64//! guard-digit intermediate and round once at the end.
65//!
66//! [`RoundingMode`]: crate::RoundingMode
67//!
68//! # Domain handling
69//!
70//! `f64::ln`, `f64::log2`, `f64::log10`, and `f64::log` return `-Infinity`
71//! for `0.0` and `NaN` for negative inputs. The f64 bridge maps `NaN` to
72//! `D38::ZERO` and saturates infinities to `D38::MAX` or `D38::MIN`.
73//! The `*_strict` forms panic on out-of-domain inputs (`self <= 0`).
74
75
76/// Re-export of the D38 strict-mode guard-digit constant for in-crate
77/// callers that branch on the strict-vs-approx working-scale match.
78/// The authoritative definition lives in
79/// [`crate::algos::ln::ln_series_2limb::STRICT_GUARD`].
80pub(crate) use crate::algos::ln::ln_series_2limb::STRICT_GUARD;
81
82impl<const SCALE: u32> crate::D<crate::int::types::Int<2>, SCALE> {
83    // ── Logarithms ────────────────────────────────────────────────
84
85    /// Returns the natural logarithm (base e) of `self`.
86    ///
87    /// # Algorithm
88    ///
89    /// Range reduction `x = 2^k * m` with `m ∈ [1, 2)`, then the
90    /// area-hyperbolic-tangent series
91    /// `ln(m) = 2·artanh(t)`, `t = (m-1)/(m+1) ∈ [0, 1/3]`,
92    /// `artanh(t) = t + t³/3 + t⁵/5 + …`, evaluated in a 256-bit
93    /// fixed-point intermediate at `SCALE + STRICT_GUARD` working
94    /// digits. The guard digits bound the total accumulated rounding
95    /// error far below 0.5 ULP of the output, so the result —
96    /// `k·ln(2) + ln(m)`, rounded once at the end — is correctly
97    /// rounded.
98    ///
99    /// # Precision
100    ///
101    /// Strict: integer-only, and **correctly rounded** — the result is
102    /// within 0.5 ULP of the exact natural logarithm.
103    ///
104    /// # Panics
105    ///
106    /// Panics if `self <= 0`, or if the result overflows the type's
107    /// representable range (only possible for `ln` of a near-`MAX`
108    /// value at `SCALE >= 37`).
109    #[inline]
110    #[must_use]
111    pub fn ln_strict(self) -> Self {
112        self.ln_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
113    }
114
115    /// Natural log under the supplied rounding mode. See [`Self::ln_strict`].
116    #[inline]
117    #[must_use]
118    pub fn ln_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
119        Self::from_bits(crate::policy::ln::dispatch::<_, SCALE>(self.to_bits(), mode))
120    }
121
122    /// Natural logarithm with a caller-chosen number of guard digits
123    /// above the storage scale, trading away the strict 0.5-ULP
124    /// guarantee for proportionally faster evaluation.
125    #[inline]
126    #[must_use]
127    pub fn ln_approx(self, working_digits: u32) -> Self {
128        self.ln_approx_with(
129            working_digits,
130            crate::support::rounding::DEFAULT_ROUNDING_MODE,
131        )
132    }
133
134    /// Natural log with caller-chosen guard digits AND rounding mode.
135    #[inline]
136    #[must_use]
137    pub fn ln_approx_with(
138        self,
139        working_digits: u32,
140        mode: crate::support::rounding::RoundingMode,
141    ) -> Self {
142        if working_digits == STRICT_GUARD {
143            return self.ln_strict_with(mode);
144        }
145        Self::from_bits(crate::policy::ln::dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
146    }
147
148    /// Returns the natural logarithm (base e) of `self`.
149    #[cfg(all(feature = "strict", not(feature = "fast")))]
150    #[inline]
151    #[must_use]
152    pub fn ln(self) -> Self {
153        self.ln_strict()
154    }
155
156    /// Returns `ln(1 + self)`.
157    ///
158    /// # Why it exists
159    ///
160    /// For API parity and standards conformance (C `log1p`, IEEE
161    /// 754-2019 `logp1`). In this crate's fixed-point representation it
162    /// is numerically **equivalent** to `(1 + self).ln_strict()` at the
163    /// same scale — `1 + self` is exactly representable, so the binary
164    /// floating-point cancellation that motivates a separate `log1p`
165    /// does not arise here. It is not more accurate than
166    /// [`Self::ln_strict`] of `1 + self`; both are correctly rounded.
167    ///
168    /// # Algorithm
169    ///
170    /// The Goldberg/Higham reformulation
171    /// `log1p(t) = 2·artanh(t / (2 + t))`, evaluated at
172    /// `SCALE + STRICT_GUARD` working digits and rounded once at the
173    /// end, so the result is correctly rounded. See `policy::log1p`.
174    ///
175    /// # Precision
176    ///
177    /// Strict: integer-only, and **correctly rounded** — within 0.5 ULP.
178    ///
179    /// # Panics
180    ///
181    /// Panics if `self <= -1` (the domain is `t > -1`, mirroring
182    /// [`Self::ln_strict`]'s positive-argument requirement on `1 + t`),
183    /// or if the result overflows the type's representable range.
184    #[inline]
185    #[must_use]
186    pub fn log1p_strict(self) -> Self {
187        self.log1p_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
188    }
189
190    /// `ln(1 + self)` under the supplied rounding mode. See
191    /// [`Self::log1p_strict`].
192    #[inline]
193    #[must_use]
194    pub fn log1p_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
195        Self::from_bits(crate::policy::log1p::dispatch::<_, SCALE>(self.to_bits(), mode))
196    }
197
198    /// `ln(1 + self)` with a caller-chosen number of guard digits above
199    /// the storage scale, trading away the strict 0.5-ULP guarantee for
200    /// proportionally faster evaluation.
201    #[inline]
202    #[must_use]
203    pub fn log1p_approx(self, working_digits: u32) -> Self {
204        self.log1p_approx_with(
205            working_digits,
206            crate::support::rounding::DEFAULT_ROUNDING_MODE,
207        )
208    }
209
210    /// `ln(1 + self)` with caller-chosen guard digits AND rounding mode.
211    #[inline]
212    #[must_use]
213    pub fn log1p_approx_with(
214        self,
215        working_digits: u32,
216        mode: crate::support::rounding::RoundingMode,
217    ) -> Self {
218        if working_digits == STRICT_GUARD {
219            return self.log1p_strict_with(mode);
220        }
221        Self::from_bits(crate::policy::log1p::dispatch_with::<_, SCALE>(
222            self.to_bits(),
223            working_digits,
224            mode,
225        ))
226    }
227
228    /// Returns `ln(1 + self)`.
229    #[cfg(all(feature = "strict", not(feature = "fast")))]
230    #[inline]
231    #[must_use]
232    pub fn log1p(self) -> Self {
233        self.log1p_strict()
234    }
235
236    /// Returns the logarithm of `self` in the given `base`.
237    #[inline]
238    #[must_use]
239    pub fn log_strict(self, base: Self) -> Self {
240        self.log_strict_with(base, crate::support::rounding::DEFAULT_ROUNDING_MODE)
241    }
242
243    /// Logarithm in `base` under the supplied rounding mode.
244    #[inline]
245    #[must_use]
246    pub fn log_strict_with(self, base: Self, mode: crate::support::rounding::RoundingMode) -> Self {
247        Self::from_bits(crate::policy::log::dispatch::<_, SCALE>(self.to_bits(), base.to_bits(), mode))
248    }
249
250    /// Logarithm with caller-chosen guard digits. See `ln_approx`.
251    #[inline]
252    #[must_use]
253    pub fn log_approx(self, base: Self, working_digits: u32) -> Self {
254        self.log_approx_with(
255            base,
256            working_digits,
257            crate::support::rounding::DEFAULT_ROUNDING_MODE,
258        )
259    }
260
261    /// Logarithm with caller-chosen guard digits AND rounding mode.
262    #[inline]
263    #[must_use]
264    pub fn log_approx_with(
265        self,
266        base: Self,
267        working_digits: u32,
268        mode: crate::support::rounding::RoundingMode,
269    ) -> Self {
270        if working_digits == STRICT_GUARD {
271            return self.log_strict_with(base, mode);
272        }
273        Self::from_bits(crate::policy::log::dispatch_with::<_, SCALE>(self.to_bits(), base.to_bits(), working_digits, mode))
274    }
275
276    /// Returns the logarithm of `self` in the given `base`.
277    #[cfg(all(feature = "strict", not(feature = "fast")))]
278    #[inline]
279    #[must_use]
280    pub fn log(self, base: Self) -> Self {
281        self.log_strict(base)
282    }
283
284    /// Returns the base-2 logarithm of `self`.
285    #[inline]
286    #[must_use]
287    pub fn log2_strict(self) -> Self {
288        self.log2_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
289    }
290
291    /// Base-2 log under the supplied rounding mode.
292    #[inline]
293    #[must_use]
294    pub fn log2_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
295        Self::from_bits(crate::policy::ln::log2_dispatch::<_, SCALE>(self.to_bits(), mode))
296    }
297
298    /// Base-2 log with caller-chosen guard digits.
299    #[inline]
300    #[must_use]
301    pub fn log2_approx(self, working_digits: u32) -> Self {
302        self.log2_approx_with(
303            working_digits,
304            crate::support::rounding::DEFAULT_ROUNDING_MODE,
305        )
306    }
307
308    /// Base-2 log with caller-chosen guard digits AND rounding mode.
309    #[inline]
310    #[must_use]
311    pub fn log2_approx_with(
312        self,
313        working_digits: u32,
314        mode: crate::support::rounding::RoundingMode,
315    ) -> Self {
316        if working_digits == STRICT_GUARD {
317            return self.log2_strict_with(mode);
318        }
319        Self::from_bits(crate::policy::ln::log2_dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
320    }
321
322    /// Returns the base-2 logarithm of `self`.
323    #[cfg(all(feature = "strict", not(feature = "fast")))]
324    #[inline]
325    #[must_use]
326    pub fn log2(self) -> Self {
327        self.log2_strict()
328    }
329
330    /// Returns the base-10 logarithm of `self`.
331    #[inline]
332    #[must_use]
333    pub fn log10_strict(self) -> Self {
334        self.log10_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
335    }
336
337    /// Base-10 log under the supplied rounding mode.
338    #[inline]
339    #[must_use]
340    pub fn log10_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
341        Self::from_bits(crate::policy::ln::log10_dispatch::<_, SCALE>(self.to_bits(), mode))
342    }
343
344    /// Base-10 log with caller-chosen guard digits.
345    #[inline]
346    #[must_use]
347    pub fn log10_approx(self, working_digits: u32) -> Self {
348        self.log10_approx_with(
349            working_digits,
350            crate::support::rounding::DEFAULT_ROUNDING_MODE,
351        )
352    }
353
354    /// Base-10 log with caller-chosen guard digits AND rounding mode.
355    #[inline]
356    #[must_use]
357    pub fn log10_approx_with(
358        self,
359        working_digits: u32,
360        mode: crate::support::rounding::RoundingMode,
361    ) -> Self {
362        if working_digits == STRICT_GUARD {
363            return self.log10_strict_with(mode);
364        }
365        Self::from_bits(crate::policy::ln::log10_dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
366    }
367
368    /// Returns the base-10 logarithm of `self`.
369    #[cfg(all(feature = "strict", not(feature = "fast")))]
370    #[inline]
371    #[must_use]
372    pub fn log10(self) -> Self {
373        self.log10_strict()
374    }
375
376    // ── Exponentials ──────────────────────────────────────────────
377
378    /// Returns `e^self` (natural exponential).
379    #[inline]
380    #[must_use]
381    pub fn exp_strict(self) -> Self {
382        self.exp_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
383    }
384
385    /// `e^self` under the supplied rounding mode.
386    #[inline]
387    #[must_use]
388    pub fn exp_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
389        Self::from_bits(crate::policy::exp::dispatch::<_, SCALE>(self.to_bits(), mode))
390    }
391
392    /// Exponential with caller-chosen guard digits.
393    #[inline]
394    #[must_use]
395    pub fn exp_approx(self, working_digits: u32) -> Self {
396        self.exp_approx_with(
397            working_digits,
398            crate::support::rounding::DEFAULT_ROUNDING_MODE,
399        )
400    }
401
402    /// Exponential with caller-chosen guard digits AND rounding mode.
403    #[inline]
404    #[must_use]
405    pub fn exp_approx_with(
406        self,
407        working_digits: u32,
408        mode: crate::support::rounding::RoundingMode,
409    ) -> Self {
410        if working_digits == STRICT_GUARD {
411            return self.exp_strict_with(mode);
412        }
413        Self::from_bits(crate::policy::exp::dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
414    }
415
416    /// Returns `e^self` (natural exponential).
417    #[cfg(all(feature = "strict", not(feature = "fast")))]
418    #[inline]
419    #[must_use]
420    pub fn exp(self) -> Self {
421        self.exp_strict()
422    }
423
424    /// Returns `e^self - 1`.
425    ///
426    /// # Why it exists
427    ///
428    /// For API parity and standards conformance (C `expm1`, IEEE
429    /// 754-2019 `expm1`) — and, unlike its `log1p` sibling, for a
430    /// concrete capability `exp` cannot provide:
431    ///
432    /// **Domain reach.** The `- 1` happens at the WORKING scale, ahead of
433    /// the storage range check, so the representable argument range is
434    /// `self <= ln(1 + MAX)` where [`Self::exp_strict`] stops at
435    /// `ln(MAX)` — exactly the arguments whose `e^self` lands in
436    /// `(MAX, MAX + 1]`. The extra band is `ln(1 + 1/MAX)` wide: with
437    /// `MAX ≈ 17` at this width's maximum scale that is about `0.057`,
438    /// narrowing at lower scales. Small, but real — there are arguments
439    /// this answers and `exp_strict` panics on.
440    ///
441    /// It is NOT more accurate than `exp_strict(self) - 1` where both are
442    /// representable: in this crate's fixed-point representation `1` is
443    /// exactly `10^SCALE` raw units, so subtracting it is an exact grid
444    /// translation and rounding commutes with it — both are correctly
445    /// rounded and agree bit-for-bit. The binary floating-point
446    /// cancellation that motivates a separate `expm1` does not arise
447    /// here.
448    ///
449    /// # Algorithm
450    ///
451    /// For `|self| <= 1`, the leading-term-dropped Taylor series
452    /// `x + x²/2! + x³/3! + …`, which needs no range reduction and keeps
453    /// every digit of a tiny argument; outside that band, `e^x - 1`
454    /// formed at the working scale. Evaluated at `SCALE + STRICT_GUARD`
455    /// working digits with Ziv escalation, so the result is correctly
456    /// rounded. See `policy::expm1`.
457    ///
458    /// # Precision
459    ///
460    /// Strict: integer-only, and **correctly rounded** — within 0.5 ULP.
461    ///
462    /// # Panics
463    ///
464    /// Panics if the result overflows the type's representable range.
465    /// There is no lower domain limit: `expm1` tends to `-1` as `self`
466    /// tends to `-∞`, which is representable at every scale.
467    #[inline]
468    #[must_use]
469    pub fn expm1_strict(self) -> Self {
470        self.expm1_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
471    }
472
473    /// `e^self - 1` under the supplied rounding mode. See
474    /// [`Self::expm1_strict`].
475    #[inline]
476    #[must_use]
477    pub fn expm1_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
478        Self::from_bits(crate::policy::expm1::dispatch::<_, SCALE>(self.to_bits(), mode))
479    }
480
481    /// `e^self - 1` with a caller-chosen number of guard digits above the
482    /// storage scale, trading away the strict 0.5-ULP guarantee for
483    /// proportionally faster evaluation.
484    #[inline]
485    #[must_use]
486    pub fn expm1_approx(self, working_digits: u32) -> Self {
487        self.expm1_approx_with(
488            working_digits,
489            crate::support::rounding::DEFAULT_ROUNDING_MODE,
490        )
491    }
492
493    /// `e^self - 1` with caller-chosen guard digits AND rounding mode.
494    #[inline]
495    #[must_use]
496    pub fn expm1_approx_with(
497        self,
498        working_digits: u32,
499        mode: crate::support::rounding::RoundingMode,
500    ) -> Self {
501        if working_digits == STRICT_GUARD {
502            return self.expm1_strict_with(mode);
503        }
504        Self::from_bits(crate::policy::expm1::dispatch_with::<_, SCALE>(
505            self.to_bits(),
506            working_digits,
507            mode,
508        ))
509    }
510
511    /// Returns `e^self - 1`.
512    #[cfg(all(feature = "strict", not(feature = "fast")))]
513    #[inline]
514    #[must_use]
515    pub fn expm1(self) -> Self {
516        self.expm1_strict()
517    }
518
519    /// Returns `2^self` (base-2 exponential).
520    #[inline]
521    #[must_use]
522    pub fn exp2_strict(self) -> Self {
523        self.exp2_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
524    }
525
526    /// `2^self` under the supplied rounding mode.
527    #[inline]
528    #[must_use]
529    pub fn exp2_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
530        Self::from_bits(crate::policy::exp::exp2_dispatch::<_, SCALE>(self.to_bits(), mode))
531    }
532
533    /// Base-2 exponential with caller-chosen guard digits.
534    #[inline]
535    #[must_use]
536    pub fn exp2_approx(self, working_digits: u32) -> Self {
537        self.exp2_approx_with(
538            working_digits,
539            crate::support::rounding::DEFAULT_ROUNDING_MODE,
540        )
541    }
542
543    /// Base-2 exponential with caller-chosen guard digits AND rounding mode.
544    #[inline]
545    #[must_use]
546    pub fn exp2_approx_with(
547        self,
548        working_digits: u32,
549        mode: crate::support::rounding::RoundingMode,
550    ) -> Self {
551        if working_digits == STRICT_GUARD {
552            return self.exp2_strict_with(mode);
553        }
554        Self::from_bits(crate::policy::exp::exp2_dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
555    }
556
557    /// Returns `2^self` (base-2 exponential).
558    #[cfg(all(feature = "strict", not(feature = "fast")))]
559    #[inline]
560    #[must_use]
561    pub fn exp2(self) -> Self {
562        self.exp2_strict()
563    }
564}
565
566#[cfg(all(test, feature = "strict", not(feature = "fast")))]
567mod strict_tests {
568    use crate::types::widths::D38s12;
569
570    /// Tolerance in ULPs for the strict transcendentals. They are
571    /// correctly rounded (≤ 0.5 ULP); 2 LSB of slack absorbs the
572    /// test's own expected-value rounding.
573    const STRICT_TOLERANCE_LSB: i128 = 2;
574
575    fn within(actual: D38s12, expected_bits: i128, tolerance: i128) -> bool {
576        (actual.to_bits().as_i128() - expected_bits).abs() <= tolerance
577    }
578
579    /// ln(1) == 0 exactly (no series terms contribute).
580    #[test]
581    fn ln_of_one_is_zero() {
582        assert_eq!(D38s12::ONE.ln(), D38s12::ZERO);
583    }
584
585    /// `ln_strict` is correctly rounded: cross-check against the f64
586    /// bridge at a scale where `f64` (≈ 15–16 significant digits) is
587    /// comfortably more precise than the type's ULP, so the
588    /// correctly-rounded integer result must agree to within 1 ULP.
589    #[test]
590    fn ln_strict_is_correctly_rounded_vs_f64() {
591        fn check(raw: i128) {
592            let value = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
593            let strict = value.ln_strict().to_bits().as_i128();
594            let reference = {
595                let as_float = raw as f64 / 1e9;
596                (as_float.ln() * 1e9).round() as i128
597            };
598            assert!(
599                (strict - reference).abs() <= 1,
600                "ln_strict({raw}) = {strict}, f64 reference {reference}"
601            );
602        }
603        for &raw in &[
604            1,
605            500_000_000,
606            1_000_000_000,
607            1_500_000_000,
608            2_000_000_000,
609            2_718_281_828,
610            10_000_000_000,
611            123_456_789_012_345,
612            999_999_999_999_999_999,
613            i64::MAX as i128,
614        ] {
615            check(raw);
616        }
617    }
618
619    /// `exp_strict` / `log2_strict` / `log10_strict` agree with the f64
620    /// bridge to within 1 ULP at D38<9>.
621    #[test]
622    fn strict_log_exp_family_matches_f64() {
623        fn check_exp(raw: i128) {
624            let value = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
625            let strict = value.exp_strict().to_bits().as_i128();
626            let reference = ((raw as f64 / 1e9).exp() * 1e9).round() as i128;
627            assert!(
628                (strict - reference).abs() <= 1,
629                "exp_strict({raw}) = {strict}, f64 reference {reference}"
630            );
631        }
632        fn check_log2(raw: i128) {
633            let value = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
634            let strict = value.log2_strict().to_bits().as_i128();
635            let reference = ((raw as f64 / 1e9).log2() * 1e9).round() as i128;
636            assert!(
637                (strict - reference).abs() <= 1,
638                "log2_strict({raw}) = {strict}, f64 reference {reference}"
639            );
640        }
641        fn check_log10(raw: i128) {
642            let value = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
643            let strict = value.log10_strict().to_bits().as_i128();
644            let reference = ((raw as f64 / 1e9).log10() * 1e9).round() as i128;
645            assert!(
646                (strict - reference).abs() <= 1,
647                "log10_strict({raw}) = {strict}, f64 reference {reference}"
648            );
649        }
650        for &raw in &[
651            -5_000_000_000,
652            -1_000_000_000,
653            -500_000_000,
654            1,
655            500_000_000,
656            1_000_000_000,
657            2_000_000_000,
658            5_000_000_000,
659            10_000_000_000,
660        ] {
661            check_exp(raw);
662        }
663        for &raw in &[
664            1,
665            500_000_000,
666            1_000_000_000,
667            2_000_000_000,
668            8_000_000_000,
669            10_000_000_000,
670            123_456_789_012_345,
671            i64::MAX as i128,
672        ] {
673            check_log2(raw);
674            check_log10(raw);
675        }
676    }
677
678    /// `exp2_strict` is exact at integer arguments: `2^10` is `1024`.
679    #[test]
680    fn strict_exp2_at_integers() {
681        for k in 0_i128..=12 {
682            let value = crate::D::<crate::int::types::Int<2>, 12>::from_bits(crate::int::types::Int::<2>::from_i128(k * 10i128.pow(12)));
683            let got = value.exp2_strict().to_bits().as_i128();
684            let expected = (1i128 << k) * 10i128.pow(12);
685            assert_eq!(got, expected, "2^{k}");
686        }
687    }
688
689    /// `ln_strict` is exact at the powers of two it can represent.
690    #[test]
691    fn ln_strict_of_powers_of_two() {
692        let ln2_s18: i128 = 693_147_180_559_945_309;
693        for k in 1_i128..=20 {
694            let value = crate::D::<crate::int::types::Int<2>, 18>::from_bits(crate::int::types::Int::<2>::from_i128((1i128 << k) * 10i128.pow(18)));
695            let got = value.ln_strict().to_bits().as_i128();
696            let expected = k * ln2_s18;
697            let tolerance = k / 2 + 2;
698            assert!(
699                (got - expected).abs() <= tolerance,
700                "ln(2^{k}) = {got}, expected ≈ {expected}"
701            );
702        }
703    }
704
705    /// ln(2) at scale 12 = 693_147_180_560 (canonical rounded to 12 places).
706    #[test]
707    fn ln_of_two_close_to_canonical() {
708        let two = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_000_000_000_000));
709        let result = two.ln();
710        assert!(
711            within(result, 693_147_180_560, STRICT_TOLERANCE_LSB),
712            "ln(2) bits = {}",
713            result.to_bits().as_i128()
714        );
715    }
716
717    /// ln(e) is approximately 1.
718    #[test]
719    fn ln_of_e_close_to_one() {
720        let e_at_s12 = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_718_281_828_459));
721        let result = e_at_s12.ln();
722        assert!(
723            within(result, 1_000_000_000_000, STRICT_TOLERANCE_LSB),
724            "ln(e) bits = {}, expected ~1_000_000_000_000",
725            result.to_bits().as_i128()
726        );
727    }
728
729    /// ln(10) at scale 12 = 2_302_585_092_994 (canonical).
730    #[test]
731    fn ln_of_ten_close_to_canonical() {
732        let ten = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(10_000_000_000_000));
733        let result = ten.ln();
734        assert!(
735            within(result, 2_302_585_092_994, STRICT_TOLERANCE_LSB),
736            "ln(10) bits = {}, expected ~2_302_585_092_994",
737            result.to_bits().as_i128()
738        );
739    }
740
741    /// ln of a value > 1 is positive.
742    #[test]
743    fn ln_above_one_is_positive() {
744        let value = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(1_500_000_000_000));
745        let result = value.ln();
746        assert!(result.to_bits().as_i128() > 0);
747    }
748
749    /// ln of a value in (0, 1) is negative.
750    #[test]
751    fn ln_below_one_is_negative() {
752        let value = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(500_000_000_000));
753        let result = value.ln();
754        assert!(result.to_bits().as_i128() < 0);
755        assert!(
756            within(result, -693_147_180_560, STRICT_TOLERANCE_LSB),
757            "ln(0.5) bits = {}, expected ~-693_147_180_560",
758            result.to_bits().as_i128()
759        );
760    }
761
762    #[test]
763    #[should_panic(expected = "argument must be positive")]
764    fn ln_of_zero_panics() {
765        let _ = D38s12::ZERO.ln();
766    }
767
768    #[test]
769    #[should_panic(expected = "argument must be positive")]
770    fn ln_of_negative_panics() {
771        let negative_value = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(-1_000_000_000_000));
772        let _ = negative_value.ln();
773    }
774
775    // log2 / log10 / log derive from ln; tolerance grows because the
776    // additional division step accumulates ~1 LSB.
777    const DERIVED_LOG_TOLERANCE_LSB: i128 = 20;
778
779    /// log2(2) ~= 1.
780    #[test]
781    fn log2_of_two_is_one() {
782        let two = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_000_000_000_000));
783        let result = two.log2();
784        assert!(
785            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
786            "log2(2) bits = {}",
787            result.to_bits().as_i128()
788        );
789    }
790
791    /// log2(8) ~= 3.
792    #[test]
793    fn log2_of_eight_is_three() {
794        let eight = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(8_000_000_000_000));
795        let result = eight.log2();
796        assert!(
797            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
798            "log2(8) bits = {}",
799            result.to_bits().as_i128()
800        );
801    }
802
803    /// log10(10) ~= 1.
804    #[test]
805    fn log10_of_ten_is_one() {
806        let ten = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(10_000_000_000_000));
807        let result = ten.log10();
808        assert!(
809            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
810            "log10(10) bits = {}",
811            result.to_bits().as_i128()
812        );
813    }
814
815    /// log10(100) ~= 2.
816    #[test]
817    fn log10_of_hundred_is_two() {
818        let hundred = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(100_000_000_000_000));
819        let result = hundred.log10();
820        assert!(
821            within(result, 2_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
822            "log10(100) bits = {}",
823            result.to_bits().as_i128()
824        );
825    }
826
827    /// log_base_b(b) == 1 for any b > 0, b != 1.
828    #[test]
829    fn log_self_is_one() {
830        let base = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(5_000_000_000_000));
831        let result = base.log(base);
832        assert!(
833            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
834            "log_5(5) bits = {}",
835            result.to_bits().as_i128()
836        );
837    }
838
839    /// log_2(8) == 3 via the generic log.
840    #[test]
841    fn log_with_base_two() {
842        let eight = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(8_000_000_000_000));
843        let two = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_000_000_000_000));
844        let result = eight.log(two);
845        assert!(
846            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
847            "log_2(8) bits = {}",
848            result.to_bits().as_i128()
849        );
850    }
851
852    #[test]
853    #[should_panic(expected = "base must not equal 1")]
854    fn log_base_one_panics() {
855        let value = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(5_000_000_000_000));
856        let one = D38s12::ONE;
857        let _ = value.log(one);
858    }
859
860    // exp / exp2 tolerance accounts for Taylor truncation, 2^k bit-shift
861    // exactness, and the range-reduction rounding step. ~20 LSB at D38s12.
862    const EXP_TOLERANCE_LSB: i128 = 20;
863
864    /// exp(0) == 1 exactly.
865    #[test]
866    fn exp_of_zero_is_one() {
867        assert_eq!(D38s12::ZERO.exp(), D38s12::ONE);
868    }
869
870    /// exp(1) ~= e.
871    #[test]
872    fn exp_of_one_is_e() {
873        let result = D38s12::ONE.exp();
874        assert!(
875            within(result, 2_718_281_828_459, EXP_TOLERANCE_LSB),
876            "exp(1) bits = {}",
877            result.to_bits().as_i128()
878        );
879    }
880
881    /// exp(ln(2)) ~= 2.
882    #[test]
883    fn exp_of_ln_2_is_two() {
884        let ln_2 = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(693_147_180_560));
885        let result = ln_2.exp();
886        assert!(
887            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
888            "exp(ln 2) bits = {}",
889            result.to_bits().as_i128()
890        );
891    }
892
893    /// exp(-1) ~= 1/e ~= 0.367879441171.
894    #[test]
895    fn exp_of_negative_one_is_reciprocal_e() {
896        let neg_one = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(-1_000_000_000_000));
897        let result = neg_one.exp();
898        assert!(
899            within(result, 367_879_441_171, EXP_TOLERANCE_LSB),
900            "exp(-1) bits = {}",
901            result.to_bits().as_i128()
902        );
903    }
904
905    /// exp2(0) == 1 exactly.
906    #[test]
907    fn exp2_of_zero_is_one() {
908        assert_eq!(D38s12::ZERO.exp2(), D38s12::ONE);
909    }
910
911    /// exp2(1) ~= 2.
912    #[test]
913    fn exp2_of_one_is_two() {
914        let result = D38s12::ONE.exp2();
915        assert!(
916            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
917            "exp2(1) bits = {}",
918            result.to_bits().as_i128()
919        );
920    }
921
922    /// exp2(10) ~= 1024.
923    #[test]
924    fn exp2_of_ten_is_1024() {
925        let ten = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(10_000_000_000_000));
926        let result = ten.exp2();
927        assert!(
928            within(result, 1_024_000_000_000_000, EXP_TOLERANCE_LSB * 10),
929            "exp2(10) bits = {}",
930            result.to_bits().as_i128()
931        );
932    }
933}