Skip to main content

decimal_scaled/
log_exp_strict.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 `*_strict` dual API
9//!
10//! Each method has an integer-only `<method>_strict` form and an
11//! f64-bridge form:
12//!
13//! - `<method>_strict` — always compiled (unless the `fast`
14//! feature is set), `no_std`-compatible, platform-deterministic.
15//! `ln_strict` uses range reduction plus a Mercator series;
16//! `exp_strict` uses range reduction plus a Taylor series; the
17//! remaining methods compose those two.
18//! - The f64-bridge form is gated on `std` and calls the inherent
19//! `f64` intrinsic.
20//!
21//! The plain `<method>` is a dispatcher: with the `strict` feature it
22//! calls `<method>_strict`, otherwise the f64 bridge. See
23//! `docs/strict-mode.md` for the full dual-API and feature rules.
24//!
25//! # Precision
26//!
27//! The f64-bridge forms are **Lossy** — `self` round-trips through
28//! `f64`. The `*_strict` forms are **correctly rounded**: the result
29//! is within 0.5 ULP of the exact value (IEEE-754 round-to-nearest).
30//! They evaluate the series in the `d_w128_kernels::Fixed` guard-digit
31//! intermediate and round once at the end.
32//!
33//! # Domain handling
34//!
35//! `f64::ln`, `f64::log2`, `f64::log10`, and `f64::log` return `-Infinity`
36//! for `0.0` and `NaN` for negative inputs. The f64 bridge maps `NaN` to
37//! `D38::ZERO` and saturates infinities to `D38::MAX` or `D38::MIN`.
38//! The `*_strict` forms panic on out-of-domain inputs (`self <= 0`).
39
40use crate::core_type::D38;
41
42// ─────────────────────────────────────────────────────────────────────
43// Correctly-rounded strict log / exp core.
44//
45// The strict `ln` / `log` / `log2` / `log10` / `exp` / `exp2` all run
46// on a 256-bit `Fixed` intermediate at `SCALE + GUARD` working digits.
47// The 30 guard digits bound the total accumulated rounding error far
48// below 0.5 ULP of the output, so each result — rounded once,
49// half-to-even, back to `SCALE` — is correctly rounded.
50//
51// `GUARD = 30` keeps the working scale `W = SCALE + 30 <= 68` for
52// `SCALE <= 38`, which is small enough that the 64-digit constants
53// cover it, `r · 10^GUARD` fits `U256`, and the 512-bit mul/div
54// intermediates never overflow.
55// ─────────────────────────────────────────────────────────────────────
56
57pub(crate) const STRICT_GUARD: u32 = 30;
58
59/// `ln(2)` as a `Fixed` at working scale `w` (`w <= 64`). The constant
60/// is embedded to 64 fractional digits and narrowed to `w`.
61pub(crate) fn wide_ln2(w: u32) -> crate::d_w128_kernels::Fixed {
62    // ln 2 = 0.693147180559945309417232121458176568075500134360255254120680 0094
63    crate::d_w128_kernels::Fixed::from_decimal_split(
64        69_314_718_055_994_530_941_723_212_145_817_u128,
65        65_680_755_001_343_602_552_541_206_800_094_u128,
66    )
67    .rescale_down(64, w)
68}
69
70/// `ln(10)` as a `Fixed` at working scale `w` (`w <= 63`). Embedded to
71/// 63 fractional digits (`ln 10 ≈ 2.30…` has an integer digit) and
72/// narrowed to `w`.
73fn wide_ln10(w: u32) -> crate::d_w128_kernels::Fixed {
74    // ln 10 = 2.302585092994045684017991454684364207601101488628772976033327 901
75    crate::d_w128_kernels::Fixed::from_decimal_split(
76        23_025_850_929_940_456_840_179_914_546_843_u128,
77        64_207_601_101_488_628_772_976_033_327_901_u128,
78    )
79    .rescale_down(63, w)
80}
81
82/// Natural logarithm of a positive working-scale value `v_w`, returned
83/// at the same working scale `w`.
84///
85/// Range-reduces `v = 2^k · m` with `m ∈ [1,2)` — the mantissa is
86/// recomputed exactly from `v_w` once `k` is known — then evaluates
87/// `ln(m) = 2·artanh((m-1)/(m+1))` (`t ∈ [0,1/3]`, fast convergence)
88/// and returns `k·ln(2) + ln(m)`.
89pub(crate) fn ln_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
90    use crate::d_w128_kernels::Fixed;
91    let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
92    let two_w = one_w.double();
93
94    // Range reduction: find k with v ∈ [2^k, 2^(k+1)); m_w = v_w / 2^k.
95    let mut k: i32 = v_w.bit_length() as i32 - one_w.bit_length() as i32;
96    let m_w = loop {
97        let m = if k >= 0 {
98            v_w.shr(k as u32)
99        } else {
100            v_w.shl((-k) as u32)
101        };
102        if m.ge_mag(two_w) {
103            k += 1;
104        } else if !m.ge_mag(one_w) {
105            k -= 1;
106        } else {
107            break m;
108        }
109    };
110
111    // t = (m - 1) / (m + 1) ∈ [0, 1/3]; artanh(t) = t + t³/3 + t⁵/5 + …
112    let t = m_w.sub(one_w).div(m_w.add(one_w), w);
113    let t2 = t.mul(t, w);
114    let mut sum = t;
115    let mut term = t;
116    let mut j: u128 = 1;
117    loop {
118        term = term.mul(t2, w);
119        let contrib = term.div_small(2 * j + 1);
120        if contrib.is_zero() {
121            break;
122        }
123        sum = sum.add(contrib);
124        j += 1;
125        if j > 400 {
126            break;
127        }
128    }
129    let ln_m = sum.double();
130
131    let ln2 = wide_ln2(w);
132    let k_ln2 = if k >= 0 {
133        ln2.mul_u128(k as u128)
134    } else {
135        ln2.mul_u128((-k) as u128).neg()
136    };
137    k_ln2.add(ln_m)
138}
139
140/// `e` raised to a working-scale value `v_w`, returned at the same
141/// working scale `w`.
142///
143/// Range-reduces `v = k·ln(2) + s` with `|s| ≤ ln(2)/2`, evaluates the
144/// Taylor series for `exp(s)`, then reassembles `2^k · exp(s)` by
145/// shifting the working-scale value (so the `2^k` factor never
146/// amplifies a rounding error).
147///
148/// # Panics
149///
150/// Panics if `2^k · exp(s)` cannot fit a 256-bit working value — i.e.
151/// the caller's result would overflow its representable range.
152pub(crate) fn exp_fixed(v_w: crate::d_w128_kernels::Fixed, w: u32) -> crate::d_w128_kernels::Fixed {
153    use crate::d_w128_kernels::Fixed;
154    let one_w = Fixed { negative: false, mag: Fixed::pow10(w) };
155    let ln2 = wide_ln2(w);
156
157    // k = round(v / ln 2); s = v - k·ln(2), |s| <= ln(2)/2.
158    let k = v_w.div(ln2, w).round_to_nearest_int(w);
159    let k_ln2 = if k >= 0 {
160        ln2.mul_u128(k as u128)
161    } else {
162        ln2.mul_u128((-k) as u128).neg()
163    };
164    let s = v_w.sub(k_ln2);
165
166    // Taylor series exp(s) = 1 + s + s²/2! + … — `term` carries sⁿ/n!.
167    let mut sum = one_w;
168    let mut term = one_w;
169    let mut n: u128 = 1;
170    loop {
171        term = term.mul(s, w).div_small(n);
172        if term.is_zero() {
173            break;
174        }
175        sum = sum.add(term);
176        n += 1;
177        if n > 400 {
178            break;
179        }
180    }
181
182    // exp(v) = 2^k · exp(s).
183    if k >= 0 {
184        let shift = k as u32;
185        assert!(sum.bit_length() + shift <= 256, "D38::exp: result overflows the representable range");
186        sum.shl(shift)
187    } else {
188        sum.shr((-k) as u32)
189    }
190}
191
192impl<const SCALE: u32> D38<SCALE> {
193    // Logarithms
194
195    /// Returns the natural logarithm (base e) of `self`.
196    ///
197    /// # Algorithm
198    ///
199    /// Range reduction `x = 2^k * m` with `m ∈ [1, 2)`, then a Mercator
200    /// reduction `x = 2^k * m` with `m ∈ [1, 2)`, then the
201    /// area-hyperbolic-tangent series
202    /// `ln(m) = 2·artanh(t)`, `t = (m-1)/(m+1) ∈ [0, 1/3]`,
203    /// `artanh(t) = t + t³/3 + t⁵/5 + …`, evaluated in a 256-bit
204    /// fixed-point intermediate at `SCALE + 20` working digits. The 20
205    /// guard digits bound the total accumulated rounding error far
206    /// below 0.5 ULP of the output, so the result — `k·ln(2) + ln(m)`,
207    /// rounded once at the end — is correctly rounded.
208    ///
209    /// # Precision
210    ///
211    /// Strict: integer-only, and **correctly rounded** — the result is
212    /// within 0.5 ULP of the exact natural logarithm (IEEE-754
213    /// round-to-nearest).
214    ///
215    /// # Panics
216    ///
217    /// Panics if `self <= 0`, or if the result overflows the type's
218    /// representable range (only possible for `ln` of a near-`MAX`
219    /// value at `SCALE >= 37`).
220    ///
221    /// Always available, regardless of the `strict` feature. When
222    /// `strict` is enabled, the plain [`Self::ln`] delegates here.
223    #[inline]
224    #[must_use]
225    pub fn ln_strict(self) -> Self {
226        use crate::d_w128_kernels::Fixed;
227        assert!(self.0 > 0, "D38::ln: argument must be positive");
228        let w = SCALE + STRICT_GUARD;
229        let v_w =
230            Fixed::from_u128_mag(self.0 as u128, false).mul_u128(10u128.pow(STRICT_GUARD));
231        let raw = ln_fixed(v_w, w)
232            .round_to_i128(w, SCALE)
233            .expect("D38::ln: result out of range");
234        Self::from_bits(raw)
235    }
236
237    /// Returns the natural logarithm (base e) of `self`.
238    ///
239    /// With the `strict` feature enabled this is the integer-only
240    /// [`Self::ln_strict`]; without it, the f64-bridge form.
241    #[cfg(all(feature = "strict", not(feature = "fast")))]
242    #[inline]
243    #[must_use]
244    pub fn ln(self) -> Self {
245        self.ln_strict()
246    }
247
248    /// Returns the logarithm of `self` in the given `base`, computed
249    /// integer-only as `ln(self) / ln(base)` — both logarithms and the
250    /// division are carried in the wide guard-digit intermediate, so
251    /// the result is correctly rounded.
252    ///
253    /// Always available, regardless of the `strict` feature.
254    ///
255    /// # Panics
256    ///
257    /// Panics if `self <= 0` or `base <= 0`, or if `base == 1`
258    /// (division by `ln(1) = 0`).
259    #[inline]
260    #[must_use]
261    pub fn log_strict(self, base: Self) -> Self {
262        use crate::d_w128_kernels::Fixed;
263        assert!(self.0 > 0, "D38::log: argument must be positive");
264        assert!(base.0 > 0, "D38::log: base must be positive");
265        let w = SCALE + STRICT_GUARD;
266        let pow = 10u128.pow(STRICT_GUARD);
267        let v_w = Fixed::from_u128_mag(self.0 as u128, false).mul_u128(pow);
268        let b_w = Fixed::from_u128_mag(base.0 as u128, false).mul_u128(pow);
269        let ln_b = ln_fixed(b_w, w);
270        assert!(!ln_b.is_zero(), "D38::log: base must not equal 1 (ln(1) is zero)");
271        let raw = ln_fixed(v_w, w)
272            .div(ln_b, w)
273            .round_to_i128(w, SCALE)
274            .expect("D38::log: result out of range");
275        Self::from_bits(raw)
276    }
277
278    /// Returns the logarithm of `self` in the given `base`.
279    ///
280    /// With the `strict` feature enabled this is the integer-only
281    /// [`Self::log_strict`]; without it, the f64-bridge form.
282    #[cfg(all(feature = "strict", not(feature = "fast")))]
283    #[inline]
284    #[must_use]
285    pub fn log(self, base: Self) -> Self {
286        self.log_strict(base)
287    }
288
289    /// Returns the base-2 logarithm of `self`, computed integer-only as
290    /// `ln(self) / ln(2)` in the wide guard-digit intermediate — the
291    /// result is correctly rounded.
292    ///
293    /// Always available, regardless of the `strict` feature.
294    ///
295    /// # Panics
296    ///
297    /// Panics if `self <= 0`.
298    #[inline]
299    #[must_use]
300    pub fn log2_strict(self) -> Self {
301        use crate::d_w128_kernels::Fixed;
302        assert!(self.0 > 0, "D38::log2: argument must be positive");
303        let w = SCALE + STRICT_GUARD;
304        let v_w =
305            Fixed::from_u128_mag(self.0 as u128, false).mul_u128(10u128.pow(STRICT_GUARD));
306        let raw = ln_fixed(v_w, w)
307            .div(wide_ln2(w), w)
308            .round_to_i128(w, SCALE)
309            .expect("D38::log2: result out of range");
310        Self::from_bits(raw)
311    }
312
313    /// Returns the base-2 logarithm of `self`.
314    ///
315    /// With the `strict` feature enabled this is the integer-only
316    /// [`Self::log2_strict`]; without it, the f64-bridge form.
317    #[cfg(all(feature = "strict", not(feature = "fast")))]
318    #[inline]
319    #[must_use]
320    pub fn log2(self) -> Self {
321        self.log2_strict()
322    }
323
324    /// Returns the base-10 logarithm of `self`, computed integer-only
325    /// as `ln(self) / ln(10)` in the wide guard-digit intermediate —
326    /// the result is correctly rounded.
327    ///
328    /// Always available, regardless of the `strict` feature.
329    ///
330    /// # Panics
331    ///
332    /// Panics if `self <= 0`.
333    #[inline]
334    #[must_use]
335    pub fn log10_strict(self) -> Self {
336        use crate::d_w128_kernels::Fixed;
337        assert!(self.0 > 0, "D38::log10: argument must be positive");
338        let w = SCALE + STRICT_GUARD;
339        let v_w =
340            Fixed::from_u128_mag(self.0 as u128, false).mul_u128(10u128.pow(STRICT_GUARD));
341        let raw = ln_fixed(v_w, w)
342            .div(wide_ln10(w), w)
343            .round_to_i128(w, SCALE)
344            .expect("D38::log10: result out of range");
345        Self::from_bits(raw)
346    }
347
348    /// Returns the base-10 logarithm of `self`.
349    ///
350    /// With the `strict` feature enabled this is the integer-only
351    /// [`Self::log10_strict`]; without it, the f64-bridge form.
352    #[cfg(all(feature = "strict", not(feature = "fast")))]
353    #[inline]
354    #[must_use]
355    pub fn log10(self) -> Self {
356        self.log10_strict()
357    }
358
359    // Exponentials
360
361    /// Returns `e^self` (natural exponential).
362    ///
363    /// # Algorithm
364    ///
365    /// Range reduction `x = k·ln(2) + s` with `k = round(x / ln 2)` and
366    /// `|s| ≤ ln(2)/2 ≈ 0.347`, then the Taylor series
367    /// `exp(s) = 1 + s + s²/2! + …` evaluated in a 256-bit `Fixed`
368    /// intermediate at `SCALE + 30` working digits. Reassembly is
369    /// `exp(x) = 2^k · exp(s)`, applied as a shift on the working-scale
370    /// value *before* the final rounding, so the `2^k` factor never
371    /// amplifies a rounding error. The result is rounded once,
372    /// half-to-even, back to `SCALE`.
373    ///
374    /// # Precision
375    ///
376    /// Strict: integer-only, and **correctly rounded** — the result is
377    /// within 0.5 ULP of the exact exponential (IEEE-754
378    /// round-to-nearest).
379    ///
380    /// # Panics
381    ///
382    /// Panics if the result overflows the type's representable range.
383    #[inline]
384    #[must_use]
385    pub fn exp_strict(self) -> Self {
386        use crate::d_w128_kernels::Fixed;
387        if self.0 == 0 {
388            return Self::ONE;
389        }
390        let w = SCALE + STRICT_GUARD;
391        let negative_input = self.0 < 0;
392        let v_w = Fixed::from_u128_mag(self.0.unsigned_abs(), false)
393            .mul_u128(10u128.pow(STRICT_GUARD));
394        let v_w = if negative_input { v_w.neg() } else { v_w };
395        let raw = exp_fixed(v_w, w)
396            .round_to_i128(w, SCALE)
397            .expect("D38::exp: result overflows the representable range");
398        Self::from_bits(raw)
399    }
400
401    /// Returns `e^self` (natural exponential).
402    ///
403    /// With the `strict` feature enabled this is the integer-only
404    /// [`Self::exp_strict`]; without it, the f64-bridge form.
405    #[cfg(all(feature = "strict", not(feature = "fast")))]
406    #[inline]
407    #[must_use]
408    pub fn exp(self) -> Self {
409        self.exp_strict()
410    }
411
412    /// Returns `2^self` (base-2 exponential), computed integer-only as
413    /// `exp(self · ln(2))` — the `self · ln(2)` product is formed in
414    /// the wide guard-digit intermediate (not at the type's own scale),
415    /// so the result is correctly rounded.
416    ///
417    /// Always available, regardless of the `strict` feature.
418    ///
419    /// # Panics
420    ///
421    /// Panics if the result overflows D38's representable range.
422    #[inline]
423    #[must_use]
424    pub fn exp2_strict(self) -> Self {
425        use crate::d_w128_kernels::Fixed;
426        if self.0 == 0 {
427            return Self::ONE;
428        }
429        let w = SCALE + STRICT_GUARD;
430        let negative_input = self.0 < 0;
431        let v_w = Fixed::from_u128_mag(self.0.unsigned_abs(), false)
432            .mul_u128(10u128.pow(STRICT_GUARD));
433        let v_w = if negative_input { v_w.neg() } else { v_w };
434        // arg = self · ln(2), carried at the wide working scale.
435        let arg_w = v_w.mul(wide_ln2(w), w);
436        let raw = exp_fixed(arg_w, w)
437            .round_to_i128(w, SCALE)
438            .expect("D38::exp2: result overflows the representable range");
439        Self::from_bits(raw)
440    }
441
442    /// Returns `2^self` (base-2 exponential).
443    ///
444    /// With the `strict` feature enabled this is the integer-only
445    /// [`Self::exp2_strict`]; without it, the f64-bridge form.
446    #[cfg(all(feature = "strict", not(feature = "fast")))]
447    #[inline]
448    #[must_use]
449    pub fn exp2(self) -> Self {
450        self.exp2_strict()
451    }
452}
453
454
455
456#[cfg(all(test, feature = "strict", not(feature = "fast")))]
457mod strict_tests {
458    use crate::core_type::D38s12;
459
460    /// Tolerance in ULPs for the strict transcendentals. They are
461    /// correctly rounded (≤ 0.5 ULP); 2 LSB of slack absorbs the
462    /// test's own expected-value rounding.
463    const STRICT_TOLERANCE_LSB: i128 = 2;
464
465    fn within(actual: D38s12, expected_bits: i128, tolerance: i128) -> bool {
466        (actual.to_bits() - expected_bits).abs() <= tolerance
467    }
468
469    /// ln(1) == 0 exactly (no series terms contribute).
470    #[test]
471    fn ln_of_one_is_zero() {
472        assert_eq!(D38s12::ONE.ln(), D38s12::ZERO);
473    }
474
475    /// `ln_strict` is correctly rounded: cross-check against the f64
476    /// bridge at a scale where `f64` (≈ 15–16 significant digits) is
477    /// comfortably more precise than the type's ULP, so the
478    /// correctly-rounded integer result must agree to within 1 ULP.
479    #[test]
480    fn ln_strict_is_correctly_rounded_vs_f64() {
481        use crate::core_type::D38;
482        // D38<9>: ULP is 1e-9; f64 ln is good to ~1e-15 over this
483        // range, so the correctly-rounded result is within 1 ULP of the
484        // f64 reference (allow 1 for the f64 reference's own rounding).
485        fn check(raw: i128) {
486            let x = D38::<9>::from_bits(raw);
487            let strict = x.ln_strict().to_bits();
488            let reference = {
489                let v = raw as f64 / 1e9;
490                (v.ln() * 1e9).round() as i128
491            };
492            assert!(
493                (strict - reference).abs() <= 1,
494                "ln_strict({raw}) = {strict}, f64 reference {reference}"
495            );
496        }
497        for &raw in &[
498            1,
499            500_000_000,            // 0.5
500            1_000_000_000,          // 1.0
501            1_500_000_000,          // 1.5
502            2_000_000_000,          // 2.0
503            2_718_281_828,          // ≈ e
504            10_000_000_000,         // 10
505            123_456_789_012_345,    // ≈ 123456.78…
506            999_999_999_999_999_999,// ≈ 1e9
507            i64::MAX as i128,
508        ] {
509            check(raw);
510        }
511    }
512
513    /// `exp_strict` / `log2_strict` / `log10_strict` agree with the f64
514    /// bridge to within 1 ULP at D38<9>, where f64 is comfortably more
515    /// precise than the type's ULP — strong evidence of correct
516    /// rounding for the whole log/exp family.
517    #[test]
518    fn strict_log_exp_family_matches_f64() {
519        use crate::core_type::D38;
520        fn check_exp(raw: i128) {
521            let x = D38::<9>::from_bits(raw);
522            let strict = x.exp_strict().to_bits();
523            let reference = ((raw as f64 / 1e9).exp() * 1e9).round() as i128;
524            assert!(
525                (strict - reference).abs() <= 1,
526                "exp_strict({raw}) = {strict}, f64 reference {reference}"
527            );
528        }
529        fn check_log2(raw: i128) {
530            let x = D38::<9>::from_bits(raw);
531            let strict = x.log2_strict().to_bits();
532            let reference = ((raw as f64 / 1e9).log2() * 1e9).round() as i128;
533            assert!(
534                (strict - reference).abs() <= 1,
535                "log2_strict({raw}) = {strict}, f64 reference {reference}"
536            );
537        }
538        fn check_log10(raw: i128) {
539            let x = D38::<9>::from_bits(raw);
540            let strict = x.log10_strict().to_bits();
541            let reference = ((raw as f64 / 1e9).log10() * 1e9).round() as i128;
542            assert!(
543                (strict - reference).abs() <= 1,
544                "log10_strict({raw}) = {strict}, f64 reference {reference}"
545            );
546        }
547        // exp: keep the argument modest so the result stays in range.
548        for &raw in &[
549            -5_000_000_000, -1_000_000_000, -500_000_000, 1, 500_000_000,
550            1_000_000_000, 2_000_000_000, 5_000_000_000, 10_000_000_000,
551        ] {
552            check_exp(raw);
553        }
554        // log2 / log10: positive arguments across the range.
555        for &raw in &[
556            1, 500_000_000, 1_000_000_000, 2_000_000_000, 8_000_000_000,
557            10_000_000_000, 123_456_789_012_345, i64::MAX as i128,
558        ] {
559            check_log2(raw);
560            check_log10(raw);
561        }
562    }
563
564    /// `exp2_strict` is exact at integer arguments: `2^10` is `1024`.
565    #[test]
566    fn strict_exp2_at_integers() {
567        use crate::core_type::D38;
568        for k in 0_i128..=12 {
569            let x = D38::<12>::from_bits(k * 10i128.pow(12));
570            let got = x.exp2_strict().to_bits();
571            let expected = (1i128 << k) * 10i128.pow(12);
572            // Correctly rounded: exactly the integer power of two.
573            assert_eq!(got, expected, "2^{k}");
574        }
575    }
576
577    /// `ln_strict` is exact at the powers of two it can represent:
578    /// `ln(2^k)` rounds to `k · ln(2)` at the type's scale.
579    #[test]
580    fn ln_strict_of_powers_of_two() {
581        use crate::core_type::D38;
582        // ln(2) at scale 18, correctly rounded:
583        // 0.693147180559945309… -> 693147180559945309.
584        let ln2_s18: i128 = 693_147_180_559_945_309;
585        for k in 1_i128..=20 {
586            let x = D38::<18>::from_bits((1i128 << k) * 10i128.pow(18));
587            let got = x.ln_strict().to_bits();
588            let expected = k * ln2_s18;
589            // k·ln(2) accumulates k roundings of the scale-18 ln(2);
590            // the correctly-rounded result is within ⌈k/2⌉+1 of the
591            // naive k·(rounded ln2).
592            let tol = k / 2 + 2;
593            assert!(
594                (got - expected).abs() <= tol,
595                "ln(2^{k}) = {got}, expected ≈ {expected}"
596            );
597        }
598    }
599
600    /// ln(2) at scale 12 = 693_147_180_560 (canonical rounded to 12 places).
601    #[test]
602    fn ln_of_two_close_to_canonical() {
603        let two = D38s12::from_bits(2_000_000_000_000);
604        let result = two.ln();
605        // ln(2) = 0.693147180559945... so at scale 12, bits = 693_147_180_560.
606        assert!(
607            within(result, 693_147_180_560, STRICT_TOLERANCE_LSB),
608            "ln(2) bits = {}",
609            result.to_bits()
610        );
611    }
612
613    /// ln(e) is approximately 1. Uses the existing pi/e constants via DecimalConsts.
614    #[test]
615    fn ln_of_e_close_to_one() {
616        // e at scale 12 = 2_718_281_828_459 (canonical 35-digit reference rescaled).
617        let e_at_s12 = D38s12::from_bits(2_718_281_828_459);
618        let result = e_at_s12.ln();
619        // ln(e) = 1.0 -> bits = 1_000_000_000_000 at scale 12.
620        assert!(
621            within(result, 1_000_000_000_000, STRICT_TOLERANCE_LSB),
622            "ln(e) bits = {}, expected ~1_000_000_000_000",
623            result.to_bits()
624        );
625    }
626
627    /// ln(10) at scale 12 = 2_302_585_092_994 (canonical).
628    #[test]
629    fn ln_of_ten_close_to_canonical() {
630        let ten = D38s12::from_bits(10_000_000_000_000);
631        let result = ten.ln();
632        assert!(
633            within(result, 2_302_585_092_994, STRICT_TOLERANCE_LSB),
634            "ln(10) bits = {}, expected ~2_302_585_092_994",
635            result.to_bits()
636        );
637    }
638
639    /// ln of a value > 1 is positive.
640    #[test]
641    fn ln_above_one_is_positive() {
642        let v = D38s12::from_bits(1_500_000_000_000); // 1.5
643        let result = v.ln();
644        assert!(result.to_bits() > 0);
645    }
646
647    /// ln of a value in (0, 1) is negative.
648    #[test]
649    fn ln_below_one_is_negative() {
650        let v = D38s12::from_bits(500_000_000_000); // 0.5
651        let result = v.ln();
652        assert!(result.to_bits() < 0);
653        // ln(0.5) = -ln(2) ~= -0.693147...
654        assert!(
655            within(result, -693_147_180_560, STRICT_TOLERANCE_LSB),
656            "ln(0.5) bits = {}, expected ~-693_147_180_560",
657            result.to_bits()
658        );
659    }
660
661    #[test]
662    #[should_panic(expected = "argument must be positive")]
663    fn ln_of_zero_panics() {
664        let _ = D38s12::ZERO.ln();
665    }
666
667    #[test]
668    #[should_panic(expected = "argument must be positive")]
669    fn ln_of_negative_panics() {
670        let neg = D38s12::from_bits(-1_000_000_000_000);
671        let _ = neg.ln();
672    }
673
674    // log2 / log10 / log derive from ln; tolerance grows because the
675    // additional division step accumulates ~1 LSB.
676    const DERIVED_LOG_TOLERANCE_LSB: i128 = 20;
677
678    /// log2(2) ~= 1.
679    #[test]
680    fn log2_of_two_is_one() {
681        let two = D38s12::from_bits(2_000_000_000_000);
682        let result = two.log2();
683        assert!(
684            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
685            "log2(2) bits = {}",
686            result.to_bits()
687        );
688    }
689
690    /// log2(8) ~= 3.
691    #[test]
692    fn log2_of_eight_is_three() {
693        let eight = D38s12::from_bits(8_000_000_000_000);
694        let result = eight.log2();
695        assert!(
696            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
697            "log2(8) bits = {}",
698            result.to_bits()
699        );
700    }
701
702    /// log10(10) ~= 1.
703    #[test]
704    fn log10_of_ten_is_one() {
705        let ten = D38s12::from_bits(10_000_000_000_000);
706        let result = ten.log10();
707        assert!(
708            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
709            "log10(10) bits = {}",
710            result.to_bits()
711        );
712    }
713
714    /// log10(100) ~= 2.
715    #[test]
716    fn log10_of_hundred_is_two() {
717        let hundred = D38s12::from_bits(100_000_000_000_000);
718        let result = hundred.log10();
719        assert!(
720            within(result, 2_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
721            "log10(100) bits = {}",
722            result.to_bits()
723        );
724    }
725
726    /// log_base_b(b) == 1 for any b > 0, b != 1.
727    #[test]
728    fn log_self_is_one() {
729        let base = D38s12::from_bits(5_000_000_000_000); // 5
730        let result = base.log(base);
731        assert!(
732            within(result, 1_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
733            "log_5(5) bits = {}",
734            result.to_bits()
735        );
736    }
737
738    /// log_2(8) == 3 via the generic log.
739    #[test]
740    fn log_with_base_two() {
741        let eight = D38s12::from_bits(8_000_000_000_000);
742        let two = D38s12::from_bits(2_000_000_000_000);
743        let result = eight.log(two);
744        assert!(
745            within(result, 3_000_000_000_000, DERIVED_LOG_TOLERANCE_LSB),
746            "log_2(8) bits = {}",
747            result.to_bits()
748        );
749    }
750
751    #[test]
752    #[should_panic(expected = "base must not equal 1")]
753    fn log_base_one_panics() {
754        let x = D38s12::from_bits(5_000_000_000_000);
755        let one = D38s12::ONE;
756        let _ = x.log(one);
757    }
758
759    // exp / exp2: tolerance accounts for Taylor truncation, 2^k bit-shift
760    // exactness, and the range-reduction rounding step. ~20 LSB at D38s12.
761    const EXP_TOLERANCE_LSB: i128 = 20;
762
763    /// exp(0) == 1 exactly.
764    #[test]
765    fn exp_of_zero_is_one() {
766        assert_eq!(D38s12::ZERO.exp(), D38s12::ONE);
767    }
768
769    /// exp(1) ~= e.
770    #[test]
771    fn exp_of_one_is_e() {
772        let result = D38s12::ONE.exp();
773        // e ~= 2.718281828459 at D38s12.
774        assert!(
775            within(result, 2_718_281_828_459, EXP_TOLERANCE_LSB),
776            "exp(1) bits = {}",
777            result.to_bits()
778        );
779    }
780
781    /// exp(ln(2)) ~= 2.
782    #[test]
783    fn exp_of_ln_2_is_two() {
784        let ln_2 = D38s12::from_bits(693_147_180_560);
785        let result = ln_2.exp();
786        assert!(
787            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
788            "exp(ln 2) bits = {}",
789            result.to_bits()
790        );
791    }
792
793    /// exp(-1) ~= 1/e ~= 0.367879441171.
794    #[test]
795    fn exp_of_negative_one_is_reciprocal_e() {
796        let neg_one = D38s12::from_bits(-1_000_000_000_000);
797        let result = neg_one.exp();
798        // 1/e ~= 0.367879441171 at D38s12 -> bits ~= 367_879_441_171.
799        assert!(
800            within(result, 367_879_441_171, EXP_TOLERANCE_LSB),
801            "exp(-1) bits = {}",
802            result.to_bits()
803        );
804    }
805
806    /// exp2(0) == 1 exactly.
807    #[test]
808    fn exp2_of_zero_is_one() {
809        assert_eq!(D38s12::ZERO.exp2(), D38s12::ONE);
810    }
811
812    /// exp2(1) ~= 2.
813    #[test]
814    fn exp2_of_one_is_two() {
815        let result = D38s12::ONE.exp2();
816        assert!(
817            within(result, 2_000_000_000_000, EXP_TOLERANCE_LSB),
818            "exp2(1) bits = {}",
819            result.to_bits()
820        );
821    }
822
823    /// exp2(10) ~= 1024.
824    #[test]
825    fn exp2_of_ten_is_1024() {
826        let ten = D38s12::from_bits(10_000_000_000_000);
827        let result = ten.exp2();
828        assert!(
829            within(result, 1_024_000_000_000_000, EXP_TOLERANCE_LSB * 10),
830            "exp2(10) bits = {}",
831            result.to_bits()
832        );
833    }
834}
835