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