Skip to main content

ni_number/
lib.rs

1//! # ni-number
2//!
3//! High-precision computation of the **Ni constant** (η_ν) —
4//! the quantum energy scattering constant.
5//!
6//! ## Definition
7//!
8//! The Ni constant is defined by the series:
9//!
10//! ```text
11//! η_ν = Σ (n=1..∞)  πⁿ / (n! · 2^(n²))
12//! ```
13//!
14//! Value: **1.88937666040491913115597775087642096081019761538215...**
15//!
16//! ## Backends
17//!
18//! | Feature           | Backend   | Requires               | Precision  |
19//! |-------------------|-----------|------------------------|------------|
20//! | `backend-dashu`   | pure Rust | nothing **(default)**  | arbitrary  |
21//! | `backend-rug`     | GNU MPFR  | MSYS2 on Windows       | arbitrary  |
22//!
23//! ## Quick start
24//!
25//! ```toml
26//! # Cargo.toml — pure Rust, works on every platform
27//! [dependencies]
28//! ni-number = "1.0"
29//!
30//! # Maximum performance via GNU MPFR
31//! ni-number = { version = "1.0", features = ["backend-rug"] }
32//! ```
33//!
34//! ```rust,ignore
35//! use ni_number::{NI_F64, ni_number_digits};
36//!
37//! println!("η_ν ≈ {}", NI_F64);                // fast — pre-computed constant
38//! println!("η_ν = {}", ni_number_digits(100));  // 100 decimal digits
39//! ```
40
41#![warn(missing_docs)]
42
43pub mod backend;
44pub mod cache;
45pub mod compute;
46pub mod constants;
47pub mod series;
48
49pub use compute::digits_to_bits as bits_for_digits;
50pub use constants::{NI_50_DIGITS, NI_F32, NI_F64};
51
52// ─── Select active backend at compile time ───────────────────────────────────
53
54// `backend-rug` wins when explicitly requested AND `backend-dashu` is NOT also
55// the only default.  In practice: if the user adds `features = ["backend-rug"]`
56// without disabling defaults they get rug (which supersedes dashu in our cfg).
57
58#[cfg(all(feature = "backend-rug", not(feature = "backend-dashu")))]
59type ActiveBackend = backend::rug_backend::RugBackend;
60
61#[cfg(feature = "backend-dashu")]
62type ActiveBackend = backend::dashu::DashuBackend;
63
64#[cfg(all(
65    feature = "backend-f64",
66    not(feature = "backend-dashu"),
67    not(feature = "backend-rug")
68))]
69type ActiveBackend = backend::f64_backend::F64Backend;
70
71// ─── Global cache ─────────────────────────────────────────────────────────────
72
73use cache::NiCache;
74
75// SAFETY: NiCache uses an internal RwLock and is Send + Sync.
76static CACHE: NiCache<ActiveBackend> = NiCache::new();
77
78// ─── Public API ──────────────────────────────────────────────────────────────
79
80/// Compute η_ν and return a decimal string with `decimal_digits` digits
81/// after the decimal point.
82///
83/// The result is cached — repeated calls at the same (or lower) precision
84/// return instantly without recomputing.
85///
86/// # Example
87///
88/// ```rust,ignore
89/// use ni_number::ni_number_digits;
90///
91/// let s = ni_number_digits(50);
92/// assert!(s.starts_with("1.889376660404919"));
93/// ```
94pub fn ni_number_digits(decimal_digits: u32) -> String {
95    let bits = compute::digits_to_bits(decimal_digits);
96    let val = CACHE.get_or_compute(bits);
97    use backend::NiFloat;
98    val.to_decimal_string(decimal_digits)
99}
100
101/// Compute η_ν to `precision_bits` of internal bit precision.
102///
103/// Returns the backend's native float type (opaque outside this crate).
104/// Use [`bits_for_digits`] to convert from decimal digit count to bits.
105///
106/// Results are cached — repeated calls at the same precision are instant.
107pub fn ni_number(precision_bits: u32) -> <ActiveBackend as backend::NiBackend>::Float {
108    CACHE.get_or_compute(precision_bits)
109}
110
111/// Return a lazy iterator over the individual series terms.
112///
113/// Each item is a [`series::NiStep`] with fields `n`, `term`, and `sum`.
114/// Useful for visualising convergence or stopping at a custom threshold.
115///
116/// # Example
117///
118/// ```rust,ignore
119/// use ni_number::ni_series;
120/// use ni_number::backend::NiFloat;
121///
122/// for step in ni_series(128).take(10) {
123///     println!("n={:2}  sum={:.15}", step.n, step.sum.to_f64());
124/// }
125/// ```
126pub fn ni_series(precision_bits: u32) -> series::NiSeries<ActiveBackend> {
127    series::NiSeries::new(precision_bits)
128}
129
130/// Clear the internal cache and free its memory.
131///
132/// The next call to [`ni_number`] or [`ni_number_digits`] will recompute
133/// from scratch.  Useful in long-running applications that need to reclaim
134/// memory after a high-precision computation is no longer needed.
135pub fn clear_cache() {
136    CACHE.clear();
137}
138
139// ─── Tests ────────────────────────────────────────────────────────────────────
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use backend::NiFloat;
145    use serial_test::serial;
146
147    #[test]
148    #[serial]
149    fn digits_prefix_is_correct() {
150        let s = ni_number_digits(20);
151        assert!(s.starts_with("1.88937666040491913"), "wrong prefix: {}", s);
152    }
153
154    #[test]
155    #[serial]
156    fn f64_matches_precomputed_constant() {
157        let computed = ni_number(128).to_f64();
158        assert!(
159            (computed - NI_F64).abs() < 1e-13,
160            "drift: {:.2e}",
161            (computed - NI_F64).abs()
162        );
163    }
164
165    #[test]
166    #[serial]
167    fn cache_is_idempotent() {
168        let a = ni_number(128).to_f64();
169        let b = ni_number(128).to_f64();
170        assert_eq!(a, b, "cache returned different values");
171    }
172
173    #[test]
174    #[serial]
175    fn series_converges_to_constant() {
176        use crate::series::NiSeries;
177        let sum = NiSeries::<ActiveBackend>::new(256)
178            .take(20)
179            .last()
180            .unwrap()
181            .sum
182            .to_f64();
183        assert!(
184            (sum - NI_F64).abs() < 1e-13,
185            "series drift: {:.2e}",
186            (sum - NI_F64).abs()
187        );
188    }
189
190    #[test]
191    #[serial]
192    fn clear_and_recompute_is_stable() {
193        let _ = ni_number(64);
194        clear_cache();
195        let val = ni_number(64).to_f64();
196        assert!((val - NI_F64).abs() < 1e-12);
197    }
198
199    mod edge_cases {
200        use super::*;
201        use serial_test::serial;
202
203        #[test]
204        #[serial]
205        fn test_zero_digits() {
206            // Check: 0 digits after the decimal point, 1.889... must be correctly rounded to 2.
207            let s = ni_number_digits(0);
208            assert_eq!(s, "2", "0 decimal digits should round to 2");
209        }
210
211        #[test]
212        #[serial]
213        fn test_massive_cache_jump() {
214            // Check: A sharp jump in accuracy should not break the cache
215            clear_cache();
216            let _ = ni_number_digits(10);
217            let high = ni_number_digits(1000);
218            assert!(high.starts_with("1.88937666040491913115597775087642096081019761538215"));
219        }
220    }
221}