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}