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 the logarithm of `self` in the given `base`.
157    #[inline]
158    #[must_use]
159    pub fn log_strict(self, base: Self) -> Self {
160        self.log_strict_with(base, crate::support::rounding::DEFAULT_ROUNDING_MODE)
161    }
162
163    /// Logarithm in `base` under the supplied rounding mode.
164    #[inline]
165    #[must_use]
166    pub fn log_strict_with(self, base: Self, mode: crate::support::rounding::RoundingMode) -> Self {
167        Self::from_bits(crate::policy::log::dispatch::<_, SCALE>(self.to_bits(), base.to_bits(), mode))
168    }
169
170    /// Logarithm with caller-chosen guard digits. See `ln_approx`.
171    #[inline]
172    #[must_use]
173    pub fn log_approx(self, base: Self, working_digits: u32) -> Self {
174        self.log_approx_with(
175            base,
176            working_digits,
177            crate::support::rounding::DEFAULT_ROUNDING_MODE,
178        )
179    }
180
181    /// Logarithm with caller-chosen guard digits AND rounding mode.
182    #[inline]
183    #[must_use]
184    pub fn log_approx_with(
185        self,
186        base: Self,
187        working_digits: u32,
188        mode: crate::support::rounding::RoundingMode,
189    ) -> Self {
190        if working_digits == STRICT_GUARD {
191            return self.log_strict_with(base, mode);
192        }
193        Self::from_bits(crate::policy::log::dispatch_with::<_, SCALE>(self.to_bits(), base.to_bits(), working_digits, mode))
194    }
195
196    /// Returns the logarithm of `self` in the given `base`.
197    #[cfg(all(feature = "strict", not(feature = "fast")))]
198    #[inline]
199    #[must_use]
200    pub fn log(self, base: Self) -> Self {
201        self.log_strict(base)
202    }
203
204    /// Returns the base-2 logarithm of `self`.
205    #[inline]
206    #[must_use]
207    pub fn log2_strict(self) -> Self {
208        self.log2_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
209    }
210
211    /// Base-2 log under the supplied rounding mode.
212    #[inline]
213    #[must_use]
214    pub fn log2_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
215        Self::from_bits(crate::policy::ln::log2_dispatch::<_, SCALE>(self.to_bits(), mode))
216    }
217
218    /// Base-2 log with caller-chosen guard digits.
219    #[inline]
220    #[must_use]
221    pub fn log2_approx(self, working_digits: u32) -> Self {
222        self.log2_approx_with(
223            working_digits,
224            crate::support::rounding::DEFAULT_ROUNDING_MODE,
225        )
226    }
227
228    /// Base-2 log with caller-chosen guard digits AND rounding mode.
229    #[inline]
230    #[must_use]
231    pub fn log2_approx_with(
232        self,
233        working_digits: u32,
234        mode: crate::support::rounding::RoundingMode,
235    ) -> Self {
236        if working_digits == STRICT_GUARD {
237            return self.log2_strict_with(mode);
238        }
239        Self::from_bits(crate::policy::ln::log2_dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
240    }
241
242    /// Returns the base-2 logarithm of `self`.
243    #[cfg(all(feature = "strict", not(feature = "fast")))]
244    #[inline]
245    #[must_use]
246    pub fn log2(self) -> Self {
247        self.log2_strict()
248    }
249
250    /// Returns the base-10 logarithm of `self`.
251    #[inline]
252    #[must_use]
253    pub fn log10_strict(self) -> Self {
254        self.log10_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
255    }
256
257    /// Base-10 log under the supplied rounding mode.
258    #[inline]
259    #[must_use]
260    pub fn log10_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
261        Self::from_bits(crate::policy::ln::log10_dispatch::<_, SCALE>(self.to_bits(), mode))
262    }
263
264    /// Base-10 log with caller-chosen guard digits.
265    #[inline]
266    #[must_use]
267    pub fn log10_approx(self, working_digits: u32) -> Self {
268        self.log10_approx_with(
269            working_digits,
270            crate::support::rounding::DEFAULT_ROUNDING_MODE,
271        )
272    }
273
274    /// Base-10 log with caller-chosen guard digits AND rounding mode.
275    #[inline]
276    #[must_use]
277    pub fn log10_approx_with(
278        self,
279        working_digits: u32,
280        mode: crate::support::rounding::RoundingMode,
281    ) -> Self {
282        if working_digits == STRICT_GUARD {
283            return self.log10_strict_with(mode);
284        }
285        Self::from_bits(crate::policy::ln::log10_dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
286    }
287
288    /// Returns the base-10 logarithm of `self`.
289    #[cfg(all(feature = "strict", not(feature = "fast")))]
290    #[inline]
291    #[must_use]
292    pub fn log10(self) -> Self {
293        self.log10_strict()
294    }
295
296    // ── Exponentials ──────────────────────────────────────────────
297
298    /// Returns `e^self` (natural exponential).
299    #[inline]
300    #[must_use]
301    pub fn exp_strict(self) -> Self {
302        self.exp_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
303    }
304
305    /// `e^self` under the supplied rounding mode.
306    #[inline]
307    #[must_use]
308    pub fn exp_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
309        Self::from_bits(crate::policy::exp::dispatch::<_, SCALE>(self.to_bits(), mode))
310    }
311
312    /// Exponential with caller-chosen guard digits.
313    #[inline]
314    #[must_use]
315    pub fn exp_approx(self, working_digits: u32) -> Self {
316        self.exp_approx_with(
317            working_digits,
318            crate::support::rounding::DEFAULT_ROUNDING_MODE,
319        )
320    }
321
322    /// Exponential with caller-chosen guard digits AND rounding mode.
323    #[inline]
324    #[must_use]
325    pub fn exp_approx_with(
326        self,
327        working_digits: u32,
328        mode: crate::support::rounding::RoundingMode,
329    ) -> Self {
330        if working_digits == STRICT_GUARD {
331            return self.exp_strict_with(mode);
332        }
333        Self::from_bits(crate::policy::exp::dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
334    }
335
336    /// Returns `e^self` (natural exponential).
337    #[cfg(all(feature = "strict", not(feature = "fast")))]
338    #[inline]
339    #[must_use]
340    pub fn exp(self) -> Self {
341        self.exp_strict()
342    }
343
344    /// Returns `2^self` (base-2 exponential).
345    #[inline]
346    #[must_use]
347    pub fn exp2_strict(self) -> Self {
348        self.exp2_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
349    }
350
351    /// `2^self` under the supplied rounding mode.
352    #[inline]
353    #[must_use]
354    pub fn exp2_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
355        Self::from_bits(crate::policy::exp::exp2_dispatch::<_, SCALE>(self.to_bits(), mode))
356    }
357
358    /// Base-2 exponential with caller-chosen guard digits.
359    #[inline]
360    #[must_use]
361    pub fn exp2_approx(self, working_digits: u32) -> Self {
362        self.exp2_approx_with(
363            working_digits,
364            crate::support::rounding::DEFAULT_ROUNDING_MODE,
365        )
366    }
367
368    /// Base-2 exponential with caller-chosen guard digits AND rounding mode.
369    #[inline]
370    #[must_use]
371    pub fn exp2_approx_with(
372        self,
373        working_digits: u32,
374        mode: crate::support::rounding::RoundingMode,
375    ) -> Self {
376        if working_digits == STRICT_GUARD {
377            return self.exp2_strict_with(mode);
378        }
379        Self::from_bits(crate::policy::exp::exp2_dispatch_with::<_, SCALE>(self.to_bits(), working_digits, mode))
380    }
381
382    /// Returns `2^self` (base-2 exponential).
383    #[cfg(all(feature = "strict", not(feature = "fast")))]
384    #[inline]
385    #[must_use]
386    pub fn exp2(self) -> Self {
387        self.exp2_strict()
388    }
389}
390
391#[cfg(all(test, feature = "strict", not(feature = "fast")))]
392mod strict_tests {
393    use crate::types::widths::D38s12;
394
395    /// Tolerance in ULPs for the strict transcendentals. They are
396    /// correctly rounded (≤ 0.5 ULP); 2 LSB of slack absorbs the
397    /// test's own expected-value rounding.
398    const STRICT_TOLERANCE_LSB: i128 = 2;
399
400    fn within(actual: D38s12, expected_bits: i128, tolerance: i128) -> bool {
401        (actual.to_bits().as_i128() - expected_bits).abs() <= tolerance
402    }
403
404    /// ln(1) == 0 exactly (no series terms contribute).
405    #[test]
406    fn ln_of_one_is_zero() {
407        assert_eq!(D38s12::ONE.ln(), D38s12::ZERO);
408    }
409
410    /// `ln_strict` is correctly rounded: cross-check against the f64
411    /// bridge at a scale where `f64` (≈ 15–16 significant digits) is
412    /// comfortably more precise than the type's ULP, so the
413    /// correctly-rounded integer result must agree to within 1 ULP.
414    #[test]
415    fn ln_strict_is_correctly_rounded_vs_f64() {
416        fn check(raw: i128) {
417            let x = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
418            let strict = x.ln_strict().to_bits().as_i128();
419            let reference = {
420                let v = raw as f64 / 1e9;
421                (v.ln() * 1e9).round() as i128
422            };
423            assert!(
424                (strict - reference).abs() <= 1,
425                "ln_strict({raw}) = {strict}, f64 reference {reference}"
426            );
427        }
428        for &raw in &[
429            1,
430            500_000_000,
431            1_000_000_000,
432            1_500_000_000,
433            2_000_000_000,
434            2_718_281_828,
435            10_000_000_000,
436            123_456_789_012_345,
437            999_999_999_999_999_999,
438            i64::MAX as i128,
439        ] {
440            check(raw);
441        }
442    }
443
444    /// `exp_strict` / `log2_strict` / `log10_strict` agree with the f64
445    /// bridge to within 1 ULP at D38<9>.
446    #[test]
447    fn strict_log_exp_family_matches_f64() {
448        fn check_exp(raw: i128) {
449            let x = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
450            let strict = x.exp_strict().to_bits().as_i128();
451            let reference = ((raw as f64 / 1e9).exp() * 1e9).round() as i128;
452            assert!(
453                (strict - reference).abs() <= 1,
454                "exp_strict({raw}) = {strict}, f64 reference {reference}"
455            );
456        }
457        fn check_log2(raw: i128) {
458            let x = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
459            let strict = x.log2_strict().to_bits().as_i128();
460            let reference = ((raw as f64 / 1e9).log2() * 1e9).round() as i128;
461            assert!(
462                (strict - reference).abs() <= 1,
463                "log2_strict({raw}) = {strict}, f64 reference {reference}"
464            );
465        }
466        fn check_log10(raw: i128) {
467            let x = crate::D::<crate::int::types::Int<2>, 9>::from_bits(crate::int::types::Int::<2>::from_i128(raw));
468            let strict = x.log10_strict().to_bits().as_i128();
469            let reference = ((raw as f64 / 1e9).log10() * 1e9).round() as i128;
470            assert!(
471                (strict - reference).abs() <= 1,
472                "log10_strict({raw}) = {strict}, f64 reference {reference}"
473            );
474        }
475        for &raw in &[
476            -5_000_000_000,
477            -1_000_000_000,
478            -500_000_000,
479            1,
480            500_000_000,
481            1_000_000_000,
482            2_000_000_000,
483            5_000_000_000,
484            10_000_000_000,
485        ] {
486            check_exp(raw);
487        }
488        for &raw in &[
489            1,
490            500_000_000,
491            1_000_000_000,
492            2_000_000_000,
493            8_000_000_000,
494            10_000_000_000,
495            123_456_789_012_345,
496            i64::MAX as i128,
497        ] {
498            check_log2(raw);
499            check_log10(raw);
500        }
501    }
502
503    /// `exp2_strict` is exact at integer arguments: `2^10` is `1024`.
504    #[test]
505    fn strict_exp2_at_integers() {
506        for k in 0_i128..=12 {
507            let x = crate::D::<crate::int::types::Int<2>, 12>::from_bits(crate::int::types::Int::<2>::from_i128(k * 10i128.pow(12)));
508            let got = x.exp2_strict().to_bits().as_i128();
509            let expected = (1i128 << k) * 10i128.pow(12);
510            assert_eq!(got, expected, "2^{k}");
511        }
512    }
513
514    /// `ln_strict` is exact at the powers of two it can represent.
515    #[test]
516    fn ln_strict_of_powers_of_two() {
517        let ln2_s18: i128 = 693_147_180_559_945_309;
518        for k in 1_i128..=20 {
519            let x = crate::D::<crate::int::types::Int<2>, 18>::from_bits(crate::int::types::Int::<2>::from_i128((1i128 << k) * 10i128.pow(18)));
520            let got = x.ln_strict().to_bits().as_i128();
521            let expected = k * ln2_s18;
522            let tol = k / 2 + 2;
523            assert!(
524                (got - expected).abs() <= tol,
525                "ln(2^{k}) = {got}, expected ≈ {expected}"
526            );
527        }
528    }
529
530    /// ln(2) at scale 12 = 693_147_180_560 (canonical rounded to 12 places).
531    #[test]
532    fn ln_of_two_close_to_canonical() {
533        let two = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_000_000_000_000));
534        let result = two.ln();
535        assert!(
536            within(result, 693_147_180_560, STRICT_TOLERANCE_LSB),
537            "ln(2) bits = {}",
538            result.to_bits().as_i128()
539        );
540    }
541
542    /// ln(e) is approximately 1.
543    #[test]
544    fn ln_of_e_close_to_one() {
545        let e_at_s12 = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_718_281_828_459));
546        let result = e_at_s12.ln();
547        assert!(
548            within(result, 1_000_000_000_000, STRICT_TOLERANCE_LSB),
549            "ln(e) bits = {}, expected ~1_000_000_000_000",
550            result.to_bits().as_i128()
551        );
552    }
553
554    /// ln(10) at scale 12 = 2_302_585_092_994 (canonical).
555    #[test]
556    fn ln_of_ten_close_to_canonical() {
557        let ten = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(10_000_000_000_000));
558        let result = ten.ln();
559        assert!(
560            within(result, 2_302_585_092_994, STRICT_TOLERANCE_LSB),
561            "ln(10) bits = {}, expected ~2_302_585_092_994",
562            result.to_bits().as_i128()
563        );
564    }
565
566    /// ln of a value > 1 is positive.
567    #[test]
568    fn ln_above_one_is_positive() {
569        let v = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(1_500_000_000_000));
570        let result = v.ln();
571        assert!(result.to_bits().as_i128() > 0);
572    }
573
574    /// ln of a value in (0, 1) is negative.
575    #[test]
576    fn ln_below_one_is_negative() {
577        let v = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(500_000_000_000));
578        let result = v.ln();
579        assert!(result.to_bits().as_i128() < 0);
580        assert!(
581            within(result, -693_147_180_560, STRICT_TOLERANCE_LSB),
582            "ln(0.5) bits = {}, expected ~-693_147_180_560",
583            result.to_bits().as_i128()
584        );
585    }
586
587    #[test]
588    #[should_panic(expected = "argument must be positive")]
589    fn ln_of_zero_panics() {
590        let _ = D38s12::ZERO.ln();
591    }
592
593    #[test]
594    #[should_panic(expected = "argument must be positive")]
595    fn ln_of_negative_panics() {
596        let neg = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(-1_000_000_000_000));
597        let _ = neg.ln();
598    }
599
600    // log2 / log10 / log derive from ln; tolerance grows because the
601    // additional division step accumulates ~1 LSB.
602    const DERIVED_LOG_TOLERANCE_LSB: i128 = 20;
603
604    /// log2(2) ~= 1.
605    #[test]
606    fn log2_of_two_is_one() {
607        let two = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_000_000_000_000));
608        let result = two.log2();
609        assert!(
610            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
611            "log2(2) bits = {}",
612            result.to_bits().as_i128()
613        );
614    }
615
616    /// log2(8) ~= 3.
617    #[test]
618    fn log2_of_eight_is_three() {
619        let eight = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(8_000_000_000_000));
620        let result = eight.log2();
621        assert!(
622            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
623            "log2(8) bits = {}",
624            result.to_bits().as_i128()
625        );
626    }
627
628    /// log10(10) ~= 1.
629    #[test]
630    fn log10_of_ten_is_one() {
631        let ten = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(10_000_000_000_000));
632        let result = ten.log10();
633        assert!(
634            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
635            "log10(10) bits = {}",
636            result.to_bits().as_i128()
637        );
638    }
639
640    /// log10(100) ~= 2.
641    #[test]
642    fn log10_of_hundred_is_two() {
643        let hundred = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(100_000_000_000_000));
644        let result = hundred.log10();
645        assert!(
646            within(result, 2_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
647            "log10(100) bits = {}",
648            result.to_bits().as_i128()
649        );
650    }
651
652    /// log_base_b(b) == 1 for any b > 0, b != 1.
653    #[test]
654    fn log_self_is_one() {
655        let base = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(5_000_000_000_000));
656        let result = base.log(base);
657        assert!(
658            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
659            "log_5(5) bits = {}",
660            result.to_bits().as_i128()
661        );
662    }
663
664    /// log_2(8) == 3 via the generic log.
665    #[test]
666    fn log_with_base_two() {
667        let eight = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(8_000_000_000_000));
668        let two = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(2_000_000_000_000));
669        let result = eight.log(two);
670        assert!(
671            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
672            "log_2(8) bits = {}",
673            result.to_bits().as_i128()
674        );
675    }
676
677    #[test]
678    #[should_panic(expected = "base must not equal 1")]
679    fn log_base_one_panics() {
680        let x = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(5_000_000_000_000));
681        let one = D38s12::ONE;
682        let _ = x.log(one);
683    }
684
685    // exp / exp2 tolerance accounts for Taylor truncation, 2^k bit-shift
686    // exactness, and the range-reduction rounding step. ~20 LSB at D38s12.
687    const EXP_TOLERANCE_LSB: i128 = 20;
688
689    /// exp(0) == 1 exactly.
690    #[test]
691    fn exp_of_zero_is_one() {
692        assert_eq!(D38s12::ZERO.exp(), D38s12::ONE);
693    }
694
695    /// exp(1) ~= e.
696    #[test]
697    fn exp_of_one_is_e() {
698        let result = D38s12::ONE.exp();
699        assert!(
700            within(result, 2_718_281_828_459, EXP_TOLERANCE_LSB),
701            "exp(1) bits = {}",
702            result.to_bits().as_i128()
703        );
704    }
705
706    /// exp(ln(2)) ~= 2.
707    #[test]
708    fn exp_of_ln_2_is_two() {
709        let ln_2 = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(693_147_180_560));
710        let result = ln_2.exp();
711        assert!(
712            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
713            "exp(ln 2) bits = {}",
714            result.to_bits().as_i128()
715        );
716    }
717
718    /// exp(-1) ~= 1/e ~= 0.367879441171.
719    #[test]
720    fn exp_of_negative_one_is_reciprocal_e() {
721        let neg_one = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(-1_000_000_000_000));
722        let result = neg_one.exp();
723        assert!(
724            within(result, 367_879_441_171, EXP_TOLERANCE_LSB),
725            "exp(-1) bits = {}",
726            result.to_bits().as_i128()
727        );
728    }
729
730    /// exp2(0) == 1 exactly.
731    #[test]
732    fn exp2_of_zero_is_one() {
733        assert_eq!(D38s12::ZERO.exp2(), D38s12::ONE);
734    }
735
736    /// exp2(1) ~= 2.
737    #[test]
738    fn exp2_of_one_is_two() {
739        let result = D38s12::ONE.exp2();
740        assert!(
741            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
742            "exp2(1) bits = {}",
743            result.to_bits().as_i128()
744        );
745    }
746
747    /// exp2(10) ~= 1024.
748    #[test]
749    fn exp2_of_ten_is_1024() {
750        let ten = D38s12::from_bits(crate::int::types::Int::<2>::from_i128(10_000_000_000_000));
751        let result = ten.exp2();
752        assert!(
753            within(result, 1_024_000_000_000_000, EXP_TOLERANCE_LSB * 10),
754            "exp2(10) bits = {}",
755            result.to_bits().as_i128()
756        );
757    }
758}