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}