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