Skip to main content

decimal_scaled/types/
log_exp.rs

1//! Logarithm and exponential methods for [`D38`].
2//!
3//! # Methods
4//!
5//! - **Logarithms:** [`D38::ln`] / [`D38::log`] / [`D38::log2`] / [`D38::log10`].
6//! - **Exponentials:** [`D38::exp`] / [`D38::exp2`].
7//!
8//! # The four-variant matrix
9//!
10//! Each function ships four entry points so a single name covers
11//! every (precision × rounding) combination:
12//!
13//! | Method            | Guard width    | Rounding mode               |
14//! |-------------------|----------------|------------------------------|
15//! | `<fn>_strict`     | crate default  | crate default               |
16//! | `<fn>_strict_with`| crate default  | caller-supplied              |
17//! | `<fn>_approx`     | caller-chosen  | crate default               |
18//! | `<fn>_approx_with`| caller-chosen  | caller-supplied              |
19//!
20//! `_strict` runs at `SCALE + STRICT_GUARD` (const-folded so LLVM
21//! specialises one optimal kernel per `SCALE`). `_approx` runs at
22//! `SCALE + working_digits` chosen at call time — drop below
23//! `STRICT_GUARD` to trade precision for latency (the Mercator /
24//! Taylor series shortens proportionally), raise above for more
25//! headroom on chained compositions. When `working_digits ==
26//! STRICT_GUARD` the `_approx_with` body redirects to `_strict_with`
27//! so the const-folded path is never displaced.
28//!
29//! `ln_strict` uses range reduction plus a Mercator series;
30//! `exp_strict` uses range reduction plus a Taylor series; the
31//! remaining methods compose those two. All four variants are
32//! integer-only, `no_std`-compatible, and correctly rounded under
33//! the selected mode.
34//!
35//! Without the `strict` feature, the plain `<fn>` is an f64-bridge
36//! (calls the inherent `f64` intrinsic, gated on `std`). With
37//! `strict` it dispatches to `<fn>_strict`. See `docs/strict-mode.md`
38//! for the full dual-API and feature rules.
39//!
40//! # Layering
41//!
42//! Every public method on this file is a one-line delegate into
43//! `policy::ln::LnPolicy` or `policy::exp::ExpPolicy`. The
44//! correctly-rounded kernels (`ln_fixed`, `exp_fixed`,
45//! `STRICT_GUARD`, the `wide_ln2` / `wide_ln10` constants, and the
46//! per-variant `ln_strict` / `ln_with` / `log_strict` / `log_with` /
47//! `log2_*` / `log10_*` / `exp_strict` / `exp_with` / `exp2_*`
48//! `Fixed`-shape functions) live in
49//! [`crate::algos::ln::fixed_d38`] and
50//! [`crate::algos::exp::fixed_d38`]. This file is a typed-shell
51//! surface; there are zero `crate::algos::*` or
52//! `crate::algos::fixed_d38::*` references in it.
53//!
54//! # Precision
55//!
56//! The f64-bridge forms are **Lossy** — `self` round-trips through
57//! `f64`. Every `_strict` / `_strict_with` / `_approx` /
58//! `_approx_with` form is **correctly rounded** under the selected
59//! [`RoundingMode`]: the result is within 0.5 ULP of the exact
60//! value. They evaluate the series in the `algos::fixed_d38::Fixed`
61//! guard-digit intermediate and round once at the end.
62//!
63//! [`RoundingMode`]: crate::RoundingMode
64//!
65//! # Domain handling
66//!
67//! `f64::ln`, `f64::log2`, `f64::log10`, and `f64::log` return `-Infinity`
68//! for `0.0` and `NaN` for negative inputs. The f64 bridge maps `NaN` to
69//! `D38::ZERO` and saturates infinities to `D38::MAX` or `D38::MIN`.
70//! The `*_strict` forms panic on out-of-domain inputs (`self <= 0`).
71
72use crate::types::widths::D38;
73
74/// Re-export of the D38 strict-mode guard-digit constant for in-crate
75/// callers that branch on the strict-vs-approx working-scale match.
76/// The authoritative definition lives in
77/// [`crate::algos::ln::fixed_d38::STRICT_GUARD`].
78pub(crate) use crate::algos::ln::fixed_d38::STRICT_GUARD;
79
80impl<const SCALE: u32> D38<SCALE> {
81    // ── Logarithms ────────────────────────────────────────────────
82
83    /// Returns the natural logarithm (base e) of `self`.
84    ///
85    /// # Algorithm
86    ///
87    /// Range reduction `x = 2^k * m` with `m ∈ [1, 2)`, then the
88    /// area-hyperbolic-tangent series
89    /// `ln(m) = 2·artanh(t)`, `t = (m-1)/(m+1) ∈ [0, 1/3]`,
90    /// `artanh(t) = t + t³/3 + t⁵/5 + …`, evaluated in a 256-bit
91    /// fixed-point intermediate at `SCALE + STRICT_GUARD` working
92    /// digits. The guard digits bound the total accumulated rounding
93    /// error far below 0.5 ULP of the output, so the result —
94    /// `k·ln(2) + ln(m)`, rounded once at the end — is correctly
95    /// rounded.
96    ///
97    /// # Precision
98    ///
99    /// Strict: integer-only, and **correctly rounded** — the result is
100    /// within 0.5 ULP of the exact natural logarithm.
101    ///
102    /// # Panics
103    ///
104    /// Panics if `self <= 0`, or if the result overflows the type's
105    /// representable range (only possible for `ln` of a near-`MAX`
106    /// value at `SCALE >= 37`).
107    #[inline]
108    #[must_use]
109    pub fn ln_strict(self) -> Self {
110        self.ln_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
111    }
112
113    /// Natural log under the supplied rounding mode. See [`Self::ln_strict`].
114    #[inline]
115    #[must_use]
116    pub fn ln_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
117        <Self as crate::policy::ln::LnPolicy>::ln_impl(self, mode)
118    }
119
120    /// Natural logarithm with a caller-chosen number of guard digits
121    /// above the storage scale, trading away the strict 0.5-ULP
122    /// guarantee for proportionally faster evaluation.
123    #[inline]
124    #[must_use]
125    pub fn ln_approx(self, working_digits: u32) -> Self {
126        self.ln_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
127    }
128
129    /// Natural log with caller-chosen guard digits AND rounding mode.
130    #[inline]
131    #[must_use]
132    pub fn ln_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
133        if working_digits == STRICT_GUARD {
134            return self.ln_strict_with(mode);
135        }
136        <Self as crate::policy::ln::LnPolicy>::ln_with_impl(self, working_digits, mode)
137    }
138
139    /// Returns the natural logarithm (base e) of `self`.
140    #[cfg(all(feature = "strict", not(feature = "fast")))]
141    #[inline]
142    #[must_use]
143    pub fn ln(self) -> Self {
144        self.ln_strict()
145    }
146
147    /// Returns the logarithm of `self` in the given `base`.
148    #[inline]
149    #[must_use]
150    pub fn log_strict(self, base: Self) -> Self {
151        self.log_strict_with(base, crate::support::rounding::DEFAULT_ROUNDING_MODE)
152    }
153
154    /// Logarithm in `base` under the supplied rounding mode.
155    #[inline]
156    #[must_use]
157    pub fn log_strict_with(self, base: Self, mode: crate::support::rounding::RoundingMode) -> Self {
158        <Self as crate::policy::ln::LnPolicy>::log_impl(self, base, mode)
159    }
160
161    /// Logarithm with caller-chosen guard digits. See `ln_approx`.
162    #[inline]
163    #[must_use]
164    pub fn log_approx(self, base: Self, working_digits: u32) -> Self {
165        self.log_approx_with(base, working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
166    }
167
168    /// Logarithm with caller-chosen guard digits AND rounding mode.
169    #[inline]
170    #[must_use]
171    pub fn log_approx_with(self, base: Self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
172        if working_digits == STRICT_GUARD {
173            return self.log_strict_with(base, mode);
174        }
175        <Self as crate::policy::ln::LnPolicy>::log_with_impl(self, base, working_digits, mode)
176    }
177
178    /// Returns the logarithm of `self` in the given `base`.
179    #[cfg(all(feature = "strict", not(feature = "fast")))]
180    #[inline]
181    #[must_use]
182    pub fn log(self, base: Self) -> Self {
183        self.log_strict(base)
184    }
185
186    /// Returns the base-2 logarithm of `self`.
187    #[inline]
188    #[must_use]
189    pub fn log2_strict(self) -> Self {
190        self.log2_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
191    }
192
193    /// Base-2 log under the supplied rounding mode.
194    #[inline]
195    #[must_use]
196    pub fn log2_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
197        <Self as crate::policy::ln::LnPolicy>::log2_impl(self, mode)
198    }
199
200    /// Base-2 log with caller-chosen guard digits.
201    #[inline]
202    #[must_use]
203    pub fn log2_approx(self, working_digits: u32) -> Self {
204        self.log2_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
205    }
206
207    /// Base-2 log with caller-chosen guard digits AND rounding mode.
208    #[inline]
209    #[must_use]
210    pub fn log2_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
211        if working_digits == STRICT_GUARD {
212            return self.log2_strict_with(mode);
213        }
214        <Self as crate::policy::ln::LnPolicy>::log2_with_impl(self, working_digits, mode)
215    }
216
217    /// Returns the base-2 logarithm of `self`.
218    #[cfg(all(feature = "strict", not(feature = "fast")))]
219    #[inline]
220    #[must_use]
221    pub fn log2(self) -> Self {
222        self.log2_strict()
223    }
224
225    /// Returns the base-10 logarithm of `self`.
226    #[inline]
227    #[must_use]
228    pub fn log10_strict(self) -> Self {
229        self.log10_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
230    }
231
232    /// Base-10 log under the supplied rounding mode.
233    #[inline]
234    #[must_use]
235    pub fn log10_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
236        <Self as crate::policy::ln::LnPolicy>::log10_impl(self, mode)
237    }
238
239    /// Base-10 log with caller-chosen guard digits.
240    #[inline]
241    #[must_use]
242    pub fn log10_approx(self, working_digits: u32) -> Self {
243        self.log10_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
244    }
245
246    /// Base-10 log with caller-chosen guard digits AND rounding mode.
247    #[inline]
248    #[must_use]
249    pub fn log10_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
250        if working_digits == STRICT_GUARD {
251            return self.log10_strict_with(mode);
252        }
253        <Self as crate::policy::ln::LnPolicy>::log10_with_impl(self, working_digits, mode)
254    }
255
256    /// Returns the base-10 logarithm of `self`.
257    #[cfg(all(feature = "strict", not(feature = "fast")))]
258    #[inline]
259    #[must_use]
260    pub fn log10(self) -> Self {
261        self.log10_strict()
262    }
263
264    // ── Exponentials ──────────────────────────────────────────────
265
266    /// Returns `e^self` (natural exponential).
267    #[inline]
268    #[must_use]
269    pub fn exp_strict(self) -> Self {
270        self.exp_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
271    }
272
273    /// `e^self` under the supplied rounding mode.
274    #[inline]
275    #[must_use]
276    pub fn exp_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
277        <Self as crate::policy::exp::ExpPolicy>::exp_impl(self, mode)
278    }
279
280    /// Exponential with caller-chosen guard digits.
281    #[inline]
282    #[must_use]
283    pub fn exp_approx(self, working_digits: u32) -> Self {
284        self.exp_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
285    }
286
287    /// Exponential with caller-chosen guard digits AND rounding mode.
288    #[inline]
289    #[must_use]
290    pub fn exp_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
291        if working_digits == STRICT_GUARD {
292            return self.exp_strict_with(mode);
293        }
294        <Self as crate::policy::exp::ExpPolicy>::exp_with_impl(self, working_digits, mode)
295    }
296
297    /// Returns `e^self` (natural exponential).
298    #[cfg(all(feature = "strict", not(feature = "fast")))]
299    #[inline]
300    #[must_use]
301    pub fn exp(self) -> Self {
302        self.exp_strict()
303    }
304
305    /// Returns `2^self` (base-2 exponential).
306    #[inline]
307    #[must_use]
308    pub fn exp2_strict(self) -> Self {
309        self.exp2_strict_with(crate::support::rounding::DEFAULT_ROUNDING_MODE)
310    }
311
312    /// `2^self` under the supplied rounding mode.
313    #[inline]
314    #[must_use]
315    pub fn exp2_strict_with(self, mode: crate::support::rounding::RoundingMode) -> Self {
316        <Self as crate::policy::exp::ExpPolicy>::exp2_impl(self, mode)
317    }
318
319    /// Base-2 exponential with caller-chosen guard digits.
320    #[inline]
321    #[must_use]
322    pub fn exp2_approx(self, working_digits: u32) -> Self {
323        self.exp2_approx_with(working_digits, crate::support::rounding::DEFAULT_ROUNDING_MODE)
324    }
325
326    /// Base-2 exponential with caller-chosen guard digits AND rounding mode.
327    #[inline]
328    #[must_use]
329    pub fn exp2_approx_with(self, working_digits: u32, mode: crate::support::rounding::RoundingMode) -> Self {
330        if working_digits == STRICT_GUARD {
331            return self.exp2_strict_with(mode);
332        }
333        <Self as crate::policy::exp::ExpPolicy>::exp2_with_impl(self, working_digits, mode)
334    }
335
336    /// Returns `2^self` (base-2 exponential).
337    #[cfg(all(feature = "strict", not(feature = "fast")))]
338    #[inline]
339    #[must_use]
340    pub fn exp2(self) -> Self {
341        self.exp2_strict()
342    }
343}
344
345#[cfg(all(test, feature = "strict", not(feature = "fast")))]
346mod strict_tests {
347    use crate::types::widths::D38s12;
348
349    /// Tolerance in ULPs for the strict transcendentals. They are
350    /// correctly rounded (≤ 0.5 ULP); 2 LSB of slack absorbs the
351    /// test's own expected-value rounding.
352    const STRICT_TOLERANCE_LSB: i128 = 2;
353
354    fn within(actual: D38s12, expected_bits: i128, tolerance: i128) -> bool {
355        (actual.to_bits() - expected_bits).abs() <= tolerance
356    }
357
358    /// ln(1) == 0 exactly (no series terms contribute).
359    #[test]
360    fn ln_of_one_is_zero() {
361        assert_eq!(D38s12::ONE.ln(), D38s12::ZERO);
362    }
363
364    /// `ln_strict` is correctly rounded: cross-check against the f64
365    /// bridge at a scale where `f64` (≈ 15–16 significant digits) is
366    /// comfortably more precise than the type's ULP, so the
367    /// correctly-rounded integer result must agree to within 1 ULP.
368    #[test]
369    fn ln_strict_is_correctly_rounded_vs_f64() {
370        use crate::types::widths::D38;
371        fn check(raw: i128) {
372            let x = D38::<9>::from_bits(raw);
373            let strict = x.ln_strict().to_bits();
374            let reference = {
375                let v = raw as f64 / 1e9;
376                (v.ln() * 1e9).round() as i128
377            };
378            assert!(
379                (strict - reference).abs() <= 1,
380                "ln_strict({raw}) = {strict}, f64 reference {reference}"
381            );
382        }
383        for &raw in &[
384            1,
385            500_000_000,
386            1_000_000_000,
387            1_500_000_000,
388            2_000_000_000,
389            2_718_281_828,
390            10_000_000_000,
391            123_456_789_012_345,
392            999_999_999_999_999_999,
393            i64::MAX as i128,
394        ] {
395            check(raw);
396        }
397    }
398
399    /// `exp_strict` / `log2_strict` / `log10_strict` agree with the f64
400    /// bridge to within 1 ULP at D38<9>.
401    #[test]
402    fn strict_log_exp_family_matches_f64() {
403        use crate::types::widths::D38;
404        fn check_exp(raw: i128) {
405            let x = D38::<9>::from_bits(raw);
406            let strict = x.exp_strict().to_bits();
407            let reference = ((raw as f64 / 1e9).exp() * 1e9).round() as i128;
408            assert!(
409                (strict - reference).abs() <= 1,
410                "exp_strict({raw}) = {strict}, f64 reference {reference}"
411            );
412        }
413        fn check_log2(raw: i128) {
414            let x = D38::<9>::from_bits(raw);
415            let strict = x.log2_strict().to_bits();
416            let reference = ((raw as f64 / 1e9).log2() * 1e9).round() as i128;
417            assert!(
418                (strict - reference).abs() <= 1,
419                "log2_strict({raw}) = {strict}, f64 reference {reference}"
420            );
421        }
422        fn check_log10(raw: i128) {
423            let x = D38::<9>::from_bits(raw);
424            let strict = x.log10_strict().to_bits();
425            let reference = ((raw as f64 / 1e9).log10() * 1e9).round() as i128;
426            assert!(
427                (strict - reference).abs() <= 1,
428                "log10_strict({raw}) = {strict}, f64 reference {reference}"
429            );
430        }
431        for &raw in &[
432            -5_000_000_000, -1_000_000_000, -500_000_000, 1, 500_000_000,
433            1_000_000_000, 2_000_000_000, 5_000_000_000, 10_000_000_000,
434        ] {
435            check_exp(raw);
436        }
437        for &raw in &[
438            1, 500_000_000, 1_000_000_000, 2_000_000_000, 8_000_000_000,
439            10_000_000_000, 123_456_789_012_345, i64::MAX as i128,
440        ] {
441            check_log2(raw);
442            check_log10(raw);
443        }
444    }
445
446    /// `exp2_strict` is exact at integer arguments: `2^10` is `1024`.
447    #[test]
448    fn strict_exp2_at_integers() {
449        use crate::types::widths::D38;
450        for k in 0_i128..=12 {
451            let x = D38::<12>::from_bits(k * 10i128.pow(12));
452            let got = x.exp2_strict().to_bits();
453            let expected = (1i128 << k) * 10i128.pow(12);
454            assert_eq!(got, expected, "2^{k}");
455        }
456    }
457
458    /// `ln_strict` is exact at the powers of two it can represent.
459    #[test]
460    fn ln_strict_of_powers_of_two() {
461        use crate::types::widths::D38;
462        let ln2_s18: i128 = 693_147_180_559_945_309;
463        for k in 1_i128..=20 {
464            let x = D38::<18>::from_bits((1i128 << k) * 10i128.pow(18));
465            let got = x.ln_strict().to_bits();
466            let expected = k * ln2_s18;
467            let tol = k / 2 + 2;
468            assert!(
469                (got - expected).abs() <= tol,
470                "ln(2^{k}) = {got}, expected ≈ {expected}"
471            );
472        }
473    }
474
475    /// ln(2) at scale 12 = 693_147_180_560 (canonical rounded to 12 places).
476    #[test]
477    fn ln_of_two_close_to_canonical() {
478        let two = D38s12::from_bits(2_000_000_000_000);
479        let result = two.ln();
480        assert!(
481            within(result, 693_147_180_560, STRICT_TOLERANCE_LSB),
482            "ln(2) bits = {}",
483            result.to_bits()
484        );
485    }
486
487    /// ln(e) is approximately 1.
488    #[test]
489    fn ln_of_e_close_to_one() {
490        let e_at_s12 = D38s12::from_bits(2_718_281_828_459);
491        let result = e_at_s12.ln();
492        assert!(
493            within(result, 1_000_000_000_000, STRICT_TOLERANCE_LSB),
494            "ln(e) bits = {}, expected ~1_000_000_000_000",
495            result.to_bits()
496        );
497    }
498
499    /// ln(10) at scale 12 = 2_302_585_092_994 (canonical).
500    #[test]
501    fn ln_of_ten_close_to_canonical() {
502        let ten = D38s12::from_bits(10_000_000_000_000);
503        let result = ten.ln();
504        assert!(
505            within(result, 2_302_585_092_994, STRICT_TOLERANCE_LSB),
506            "ln(10) bits = {}, expected ~2_302_585_092_994",
507            result.to_bits()
508        );
509    }
510
511    /// ln of a value > 1 is positive.
512    #[test]
513    fn ln_above_one_is_positive() {
514        let v = D38s12::from_bits(1_500_000_000_000);
515        let result = v.ln();
516        assert!(result.to_bits() > 0);
517    }
518
519    /// ln of a value in (0, 1) is negative.
520    #[test]
521    fn ln_below_one_is_negative() {
522        let v = D38s12::from_bits(500_000_000_000);
523        let result = v.ln();
524        assert!(result.to_bits() < 0);
525        assert!(
526            within(result, -693_147_180_560, STRICT_TOLERANCE_LSB),
527            "ln(0.5) bits = {}, expected ~-693_147_180_560",
528            result.to_bits()
529        );
530    }
531
532    #[test]
533    #[should_panic(expected = "argument must be positive")]
534    fn ln_of_zero_panics() {
535        let _ = D38s12::ZERO.ln();
536    }
537
538    #[test]
539    #[should_panic(expected = "argument must be positive")]
540    fn ln_of_negative_panics() {
541        let neg = D38s12::from_bits(-1_000_000_000_000);
542        let _ = neg.ln();
543    }
544
545    // log2 / log10 / log derive from ln; tolerance grows because the
546    // additional division step accumulates ~1 LSB.
547    const DERIVED_LOG_TOLERANCE_LSB: i128 = 20;
548
549    /// log2(2) ~= 1.
550    #[test]
551    fn log2_of_two_is_one() {
552        let two = D38s12::from_bits(2_000_000_000_000);
553        let result = two.log2();
554        assert!(
555            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
556            "log2(2) bits = {}",
557            result.to_bits()
558        );
559    }
560
561    /// log2(8) ~= 3.
562    #[test]
563    fn log2_of_eight_is_three() {
564        let eight = D38s12::from_bits(8_000_000_000_000);
565        let result = eight.log2();
566        assert!(
567            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
568            "log2(8) bits = {}",
569            result.to_bits()
570        );
571    }
572
573    /// log10(10) ~= 1.
574    #[test]
575    fn log10_of_ten_is_one() {
576        let ten = D38s12::from_bits(10_000_000_000_000);
577        let result = ten.log10();
578        assert!(
579            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
580            "log10(10) bits = {}",
581            result.to_bits()
582        );
583    }
584
585    /// log10(100) ~= 2.
586    #[test]
587    fn log10_of_hundred_is_two() {
588        let hundred = D38s12::from_bits(100_000_000_000_000);
589        let result = hundred.log10();
590        assert!(
591            within(result, 2_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
592            "log10(100) bits = {}",
593            result.to_bits()
594        );
595    }
596
597    /// log_base_b(b) == 1 for any b > 0, b != 1.
598    #[test]
599    fn log_self_is_one() {
600        let base = D38s12::from_bits(5_000_000_000_000);
601        let result = base.log(base);
602        assert!(
603            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
604            "log_5(5) bits = {}",
605            result.to_bits()
606        );
607    }
608
609    /// log_2(8) == 3 via the generic log.
610    #[test]
611    fn log_with_base_two() {
612        let eight = D38s12::from_bits(8_000_000_000_000);
613        let two = D38s12::from_bits(2_000_000_000_000);
614        let result = eight.log(two);
615        assert!(
616            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
617            "log_2(8) bits = {}",
618            result.to_bits()
619        );
620    }
621
622    #[test]
623    #[should_panic(expected = "base must not equal 1")]
624    fn log_base_one_panics() {
625        let x = D38s12::from_bits(5_000_000_000_000);
626        let one = D38s12::ONE;
627        let _ = x.log(one);
628    }
629
630    // exp / exp2 tolerance accounts for Taylor truncation, 2^k bit-shift
631    // exactness, and the range-reduction rounding step. ~20 LSB at D38s12.
632    const EXP_TOLERANCE_LSB: i128 = 20;
633
634    /// exp(0) == 1 exactly.
635    #[test]
636    fn exp_of_zero_is_one() {
637        assert_eq!(D38s12::ZERO.exp(), D38s12::ONE);
638    }
639
640    /// exp(1) ~= e.
641    #[test]
642    fn exp_of_one_is_e() {
643        let result = D38s12::ONE.exp();
644        assert!(
645            within(result, 2_718_281_828_459, EXP_TOLERANCE_LSB),
646            "exp(1) bits = {}",
647            result.to_bits()
648        );
649    }
650
651    /// exp(ln(2)) ~= 2.
652    #[test]
653    fn exp_of_ln_2_is_two() {
654        let ln_2 = D38s12::from_bits(693_147_180_560);
655        let result = ln_2.exp();
656        assert!(
657            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
658            "exp(ln 2) bits = {}",
659            result.to_bits()
660        );
661    }
662
663    /// exp(-1) ~= 1/e ~= 0.367879441171.
664    #[test]
665    fn exp_of_negative_one_is_reciprocal_e() {
666        let neg_one = D38s12::from_bits(-1_000_000_000_000);
667        let result = neg_one.exp();
668        assert!(
669            within(result, 367_879_441_171, EXP_TOLERANCE_LSB),
670            "exp(-1) bits = {}",
671            result.to_bits()
672        );
673    }
674
675    /// exp2(0) == 1 exactly.
676    #[test]
677    fn exp2_of_zero_is_one() {
678        assert_eq!(D38s12::ZERO.exp2(), D38s12::ONE);
679    }
680
681    /// exp2(1) ~= 2.
682    #[test]
683    fn exp2_of_one_is_two() {
684        let result = D38s12::ONE.exp2();
685        assert!(
686            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
687            "exp2(1) bits = {}",
688            result.to_bits()
689        );
690    }
691
692    /// exp2(10) ~= 1024.
693    #[test]
694    fn exp2_of_ten_is_1024() {
695        let ten = D38s12::from_bits(10_000_000_000_000);
696        let result = ten.exp2();
697        assert!(
698            within(result, 1_024_000_000_000_000, EXP_TOLERANCE_LSB * 10),
699            "exp2(10) bits = {}",
700            result.to_bits()
701        );
702    }
703}