Skip to main content

decimal_scaled/
log_exp_fast.rs

1//! Lossy (f64-bridge) `log_exp` methods for D38.
2//!
3//! Companion to `log_exp_strict.rs`. The plain methods here are the
4//! f64-bridge variants, gated on std + (no strict feature or
5//! fast set). When strict is on, the dispatcher in the
6//! _strict file shadows these.
7
8use crate::core_type::D38;
9
10impl<const SCALE: u32> D38<SCALE> {
11
12    /// Returns the natural logarithm (base e) of `self`.
13    ///
14    /// # Precision
15    ///
16    /// Lossy: converts to f64, calls `f64::ln`, converts back. `f64::ln`
17    /// returns `-Infinity` for `0.0` (saturates to `D38::MIN`) and `NaN`
18    /// for negative inputs (maps to `D38::ZERO`).
19    ///
20    /// # Examples
21    ///
22    /// ```ignore
23    /// use decimal_scaled::D38s12;
24    /// // ln(1) == 0 (f64::ln(1.0) == 0.0 exactly).
25    /// assert_eq!(D38s12::ONE.ln(), D38s12::ZERO);
26    /// ```
27    #[cfg(feature = "std")]
28    #[inline]
29    #[must_use]
30    pub fn ln_fast(self) -> Self {
31        Self::from_f64(self.to_f64().ln())
32    }
33
34    /// Returns the logarithm of `self` in the given `base`.
35    ///
36    /// Implemented via a single `f64::log(self_f64, base_f64)` call, which
37    /// avoids the extra quantisation that would come from computing
38    /// `ln(self) / ln(base)` with two separate f64 round-trips.
39    ///
40    /// # Precision
41    ///
42    /// Lossy: involves f64 at some point; result may lose precision.
43    ///
44    /// # Examples
45    ///
46    /// ```ignore
47    /// use decimal_scaled::D38s12;
48    /// // log_2(8) is approximately 3 within f64 precision.
49    /// let eight = D38s12::from_int(8);
50    /// let two = D38s12::from_int(2);
51    /// let result = eight.log(two);
52    /// ```
53    #[cfg(feature = "std")]
54    #[inline]
55    #[must_use]
56    pub fn log_fast(self, base: Self) -> Self {
57        Self::from_f64(self.to_f64().log(base.to_f64()))
58    }
59
60    /// Returns the base-2 logarithm of `self`.
61    ///
62    /// # Precision
63    ///
64    /// Lossy: involves f64 at some point; result may lose precision.
65    /// On IEEE-754 platforms, `f64::log2` is exact for integer powers
66    /// of two (e.g. `log2(8.0) == 3.0`). Out-of-domain inputs follow
67    /// the same saturation policy as [`Self::ln`].
68    ///
69    /// # Examples
70    ///
71    /// ```ignore
72    /// use decimal_scaled::D38s12;
73    /// // log2(1) == 0 (f64::log2(1.0) == 0.0 exactly).
74    /// assert_eq!(D38s12::ONE.log2(), D38s12::ZERO);
75    /// ```
76    #[cfg(feature = "std")]
77    #[inline]
78    #[must_use]
79    pub fn log2_fast(self) -> Self {
80        Self::from_f64(self.to_f64().log2())
81    }
82
83    /// Returns the base-10 logarithm of `self`.
84    ///
85    /// # Precision
86    ///
87    /// Lossy: involves f64 at some point; result may lose precision.
88    /// Out-of-domain inputs follow the same saturation policy as [`Self::ln`].
89    ///
90    /// # Examples
91    ///
92    /// ```ignore
93    /// use decimal_scaled::D38s12;
94    /// // log10(1) == 0 (f64::log10(1.0) == 0.0 exactly).
95    /// assert_eq!(D38s12::ONE.log10(), D38s12::ZERO);
96    /// ```
97    #[cfg(feature = "std")]
98    #[inline]
99    #[must_use]
100    pub fn log10_fast(self) -> Self {
101        Self::from_f64(self.to_f64().log10())
102    }
103
104    /// Returns `e^self` (natural exponential).
105    ///
106    /// # Precision
107    ///
108    /// Lossy: involves f64 at some point; result may lose precision.
109    /// Large positive inputs overflow f64 to `+Infinity`, which saturates
110    /// to `D38::MAX`. Large negative inputs underflow to `0.0` in f64,
111    /// which maps to `D38::ZERO`.
112    ///
113    /// # Examples
114    ///
115    /// ```ignore
116    /// use decimal_scaled::D38s12;
117    /// // exp(0) == 1 (f64::exp(0.0) == 1.0 exactly).
118    /// assert_eq!(D38s12::ZERO.exp(), D38s12::ONE);
119    /// ```
120    #[cfg(feature = "std")]
121    #[inline]
122    #[must_use]
123    pub fn exp_fast(self) -> Self {
124        Self::from_f64(self.to_f64().exp())
125    }
126
127    /// Returns `2^self` (base-2 exponential).
128    ///
129    /// # Precision
130    ///
131    /// Lossy: involves f64 at some point; result may lose precision.
132    /// Saturation behaviour is analogous to [`Self::exp`] but at different
133    /// magnitudes (inputs beyond approximately 1024 overflow to `+Infinity`).
134    ///
135    /// # Examples
136    ///
137    /// ```ignore
138    /// use decimal_scaled::D38s12;
139    /// // exp2(0) == 1 (f64::exp2(0.0) == 1.0 exactly).
140    /// assert_eq!(D38s12::ZERO.exp2(), D38s12::ONE);
141    /// ```
142    #[cfg(feature = "std")]
143    #[inline]
144    #[must_use]
145    pub fn exp2_fast(self) -> Self {
146        Self::from_f64(self.to_f64().exp2())
147    }
148}
149
150#[cfg(all(feature = "std", any(not(feature = "strict"), feature = "fast")))]
151impl<const SCALE: u32> D38<SCALE> {
152    /// Plain dispatcher: forwards to [`Self::ln_fast`] in this feature mode.
153    #[inline] #[must_use] pub fn ln(self) -> Self { self.ln_fast() }
154    /// Plain dispatcher: forwards to [`Self::log_fast`] in this feature mode.
155    #[inline] #[must_use] pub fn log(self, base: Self) -> Self { self.log_fast(base) }
156    /// Plain dispatcher: forwards to [`Self::log2_fast`] in this feature mode.
157    #[inline] #[must_use] pub fn log2(self) -> Self { self.log2_fast() }
158    /// Plain dispatcher: forwards to [`Self::log10_fast`] in this feature mode.
159    #[inline] #[must_use] pub fn log10(self) -> Self { self.log10_fast() }
160    /// Plain dispatcher: forwards to [`Self::exp_fast`] in this feature mode.
161    #[inline] #[must_use] pub fn exp(self) -> Self { self.exp_fast() }
162    /// Plain dispatcher: forwards to [`Self::exp2_fast`] in this feature mode.
163    #[inline] #[must_use] pub fn exp2(self) -> Self { self.exp2_fast() }
164}
165
166#[cfg(all(test, any(not(feature = "strict"), feature = "fast")))]
167mod tests {
168    use crate::consts::DecimalConsts;
169    use crate::core_type::D38s12;
170
171    /// Tolerance for f64-bridge log/exp tests against integer-valued
172    /// expectations.
173    ///
174    /// The f64 round-trip introduces roughly 1 LSB of quantisation noise.
175    /// Log and exp then amplify that noise in proportion to input magnitude.
176    /// For the test inputs (powers of 10 and powers of 2 up to 2^16) the
177    /// worst-case slack is around 16 LSB; 32 gives comfortable margin.
178    /// At SCALE=12 this is 32 picometers, nine orders of magnitude below
179    /// any physical measurement. The test margin reflects f64 arithmetic
180    /// noise, not D38 imprecision.
181    const LOG_EXP_TOLERANCE_LSB: i128 = 32;
182
183    /// Looser tolerance for round-trips like `exp(ln(x)) ~= x`.
184    ///
185    /// An epsilon-LSB error in `ln(x)` becomes a `~|x| * epsilon`-LSB
186    /// error after `exp` (because `exp(ln(x) + eps) ~= x * (1 + eps)`).
187    /// For `|x|` up to ~80 the worst observed slack is ~56 LSB; 128 LSB
188    /// gives margin while staying well under 1 nanometer at SCALE=12.
189    const ROUND_TRIP_TOLERANCE_LSB: i128 = 128;
190
191    /// Tighter tolerance for moderate-magnitude round-trips where `|x| < 10`.
192    /// Each f64 step adds up to ~1 LSB; 4 LSB absorbs two quantisation steps.
193    const FOUR_LSB: i128 = 4;
194
195    fn within_lsb(actual: D38s12, expected: D38s12, lsb: i128) -> bool {
196        let diff = (actual.to_bits() - expected.to_bits()).abs();
197        diff <= lsb
198    }
199
200    // Bit-exact identity tests
201
202    /// `exp(0) == 1` -- bit-exact via `f64::exp(0.0) == 1.0`.
203    #[test]
204    fn exp_zero_is_one() {
205        assert_eq!(D38s12::ZERO.exp(), D38s12::ONE);
206    }
207
208    /// `exp2(0) == 1` -- bit-exact via `f64::exp2(0.0) == 1.0`.
209    #[test]
210    fn exp2_zero_is_one() {
211        assert_eq!(D38s12::ZERO.exp2(), D38s12::ONE);
212    }
213
214    /// `ln(1) == 0` -- bit-exact via `f64::ln(1.0) == 0.0`.
215    #[test]
216    fn ln_one_is_zero() {
217        assert_eq!(D38s12::ONE.ln(), D38s12::ZERO);
218    }
219
220    /// `log2(1) == 0` -- bit-exact via `f64::log2(1.0) == 0.0`.
221    #[test]
222    fn log2_one_is_zero() {
223        assert_eq!(D38s12::ONE.log2(), D38s12::ZERO);
224    }
225
226    /// `log10(1) == 0` -- bit-exact via `f64::log10(1.0) == 0.0`.
227    #[test]
228    fn log10_one_is_zero() {
229        assert_eq!(D38s12::ONE.log10(), D38s12::ZERO);
230    }
231
232    // Integer-power identities (within tolerance)
233
234    /// `log2(8) ~= 3` within tolerance.
235    #[test]
236    fn log2_of_eight_is_three() {
237        let eight = D38s12::from_int(8);
238        let result = eight.log2();
239        let expected = D38s12::from_int(3);
240        assert!(
241            within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
242            "log2(8) bits {}, expected 3 bits {} (delta {})",
243            result.to_bits(),
244            expected.to_bits(),
245            (result.to_bits() - expected.to_bits()).abs(),
246        );
247    }
248
249    /// `log10(1000) ~= 3` within tolerance.
250    #[test]
251    fn log10_of_thousand_is_three() {
252        let thousand = D38s12::from_int(1000);
253        let result = thousand.log10();
254        let expected = D38s12::from_int(3);
255        assert!(
256            within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
257            "log10(1000) bits {}, expected 3 bits {} (delta {})",
258            result.to_bits(),
259            expected.to_bits(),
260            (result.to_bits() - expected.to_bits()).abs(),
261        );
262    }
263
264    /// `log10(10^n) ~= n` for representative n.
265    #[test]
266    fn log10_of_power_of_ten() {
267        // n = 1, 2, 4, 6 chosen to stay well within f64's range at SCALE=12.
268        for n in [1_i64, 2, 4, 6] {
269            let pow_of_ten = D38s12::from_int(10_i64.pow(n as u32));
270            let result = pow_of_ten.log10();
271            let expected = D38s12::from_int(n);
272            assert!(
273                within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
274                "log10(10^{n}) bits {}, expected {n} bits {} (delta {})",
275                result.to_bits(),
276                expected.to_bits(),
277                (result.to_bits() - expected.to_bits()).abs(),
278            );
279        }
280    }
281
282    /// `log2(2^n) ~= n` for representative n.
283    #[test]
284    fn log2_of_power_of_two() {
285        for n in [1_i64, 2, 4, 8, 16] {
286            let pow_of_two = D38s12::from_int(2_i64.pow(n as u32));
287            let result = pow_of_two.log2();
288            let expected = D38s12::from_int(n);
289            assert!(
290                within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
291                "log2(2^{n}) bits {}, expected {n} bits {} (delta {})",
292                result.to_bits(),
293                expected.to_bits(),
294                (result.to_bits() - expected.to_bits()).abs(),
295            );
296        }
297    }
298
299    // Round-trip identities
300
301    /// `exp(ln(x)) ~= x` for `x` in `[0.1, 100]` within tolerance.
302    ///
303    /// Each f64 transcendental introduces ~1 LSB of quantisation noise;
304    /// that noise is amplified by `~|x|` after the `exp` step.
305    #[test]
306    fn exp_of_ln_round_trip() {
307        // Raw bit-patterns at SCALE=12 spanning [0.1, ~80].
308        for raw in [
309            100_000_000_000_i128,    // 0.1
310            500_000_000_000_i128,    // 0.5
311            1_234_567_890_123_i128,  // ~1.234567
312            4_567_891_234_567_i128,  // ~4.567891
313            7_890_123_456_789_i128,  // ~7.890123
314            45_678_912_345_679_i128, // ~45.678912
315            78_901_234_567_890_i128, // ~78.901234
316        ] {
317            let x = D38s12::from_bits(raw);
318            let recovered = x.ln().exp();
319            assert!(
320                within_lsb(recovered, x, ROUND_TRIP_TOLERANCE_LSB),
321                "exp(ln(x)) != x for raw={raw}: got bits {} (delta {})",
322                recovered.to_bits(),
323                (recovered.to_bits() - x.to_bits()).abs(),
324            );
325        }
326    }
327
328    /// `exp(D38::e().ln()) ~= D38::e()` round-trip within tolerance.
329    ///
330    /// `e ~= 2.718`, so the error stays inside `LOG_EXP_TOLERANCE_LSB`.
331    #[test]
332    fn exp_of_ln_e_round_trip() {
333        let e = D38s12::e();
334        let recovered = e.ln().exp();
335        assert!(
336            within_lsb(recovered, e, LOG_EXP_TOLERANCE_LSB),
337            "exp(ln(e)) != e: got bits {} (delta {})",
338            recovered.to_bits(),
339            (recovered.to_bits() - e.to_bits()).abs(),
340        );
341    }
342
343    /// `ln(exp(x)) ~= x` for moderate `x` -- the inverse round-trip.
344    #[test]
345    fn ln_of_exp_round_trip() {
346        if !crate::rounding::DEFAULT_IS_HALF_TO_EVEN { return; }
347        // Moderate inputs; large positive inputs approach D38s12 magnitude limit.
348        for raw in [
349            -2_345_678_901_234_i128, // ~-2.345678
350            -500_000_000_000_i128,   // -0.5
351            500_000_000_000_i128,    // 0.5
352            1_234_567_890_123_i128,  // ~1.234567
353            7_890_123_456_789_i128,  // ~7.890123
354        ] {
355            let x = D38s12::from_bits(raw);
356            let recovered = x.exp().ln();
357            assert!(
358                within_lsb(recovered, x, FOUR_LSB),
359                "ln(exp(x)) != x for raw={raw}: got bits {} (delta {})",
360                recovered.to_bits(),
361                (recovered.to_bits() - x.to_bits()).abs(),
362            );
363        }
364    }
365
366    // Cross-method consistency
367
368    /// `log(self, e) ~= ln(self)` -- base-aware form is consistent with `ln`.
369    #[test]
370    fn log_base_e_matches_ln() {
371        let e = D38s12::e();
372        for raw in [
373            500_000_000_000_i128,    // 0.5
374            1_234_567_890_123_i128,  // ~1.234567
375            4_567_891_234_567_i128,  // ~4.567891
376            7_890_123_456_789_i128,  // ~7.890123
377        ] {
378            let x = D38s12::from_bits(raw);
379            let via_log = x.log(e);
380            let via_ln = x.ln();
381            assert!(
382                within_lsb(via_log, via_ln, FOUR_LSB),
383                "log(x, e) != ln(x) for raw={raw}: log bits {}, ln bits {}",
384                via_log.to_bits(),
385                via_ln.to_bits(),
386            );
387        }
388    }
389
390    /// `log(self, 2) ~= log2(self)` -- consistency check for base 2.
391    #[test]
392    fn log_base_two_matches_log2() {
393        let two = D38s12::from_int(2);
394        for raw in [
395            500_000_000_000_i128,    // 0.5
396            1_234_567_890_123_i128,  // ~1.234567
397            4_567_891_234_567_i128,  // ~4.567891
398            7_890_123_456_789_i128,  // ~7.890123
399        ] {
400            let x = D38s12::from_bits(raw);
401            let via_log = x.log(two);
402            let via_log2 = x.log2();
403            assert!(
404                within_lsb(via_log, via_log2, FOUR_LSB),
405                "log(x, 2) != log2(x) for raw={raw}: log bits {}, log2 bits {}",
406                via_log.to_bits(),
407                via_log2.to_bits(),
408            );
409        }
410    }
411
412    /// `log(self, 10) ~= log10(self)` -- consistency check for base 10.
413    #[test]
414    fn log_base_ten_matches_log10() {
415        let ten = D38s12::from_int(10);
416        for raw in [
417            500_000_000_000_i128,    // 0.5
418            1_234_567_890_123_i128,  // ~1.234567
419            4_567_891_234_567_i128,  // ~4.567891
420            7_890_123_456_789_i128,  // ~7.890123
421        ] {
422            let x = D38s12::from_bits(raw);
423            let via_log = x.log(ten);
424            let via_log10 = x.log10();
425            assert!(
426                within_lsb(via_log, via_log10, FOUR_LSB),
427                "log(x, 10) != log10(x) for raw={raw}: log bits {}, log10 bits {}",
428                via_log.to_bits(),
429                via_log10.to_bits(),
430            );
431        }
432    }
433
434    /// `exp2(n) ~= 2^n` for small integer n -- cross-check exp2 against
435    /// the integer pow surface.
436    #[test]
437    fn exp2_matches_integer_power_of_two() {
438        for n in [0_i64, 1, 2, 4, 8] {
439            let result = D38s12::from_int(n).exp2();
440            let expected = D38s12::from_int(2_i64.pow(n as u32));
441            assert!(
442                within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
443                "exp2({n}) bits {}, expected 2^{n} bits {} (delta {})",
444                result.to_bits(),
445                expected.to_bits(),
446                (result.to_bits() - expected.to_bits()).abs(),
447            );
448        }
449    }
450}
451