nereids_core/constants.rs
1//! Physical constants used throughout NEREIDS.
2//!
3//! Values from CODATA 2018 recommended values.
4//! Reference: <https://physics.nist.gov/cuu/Constants/>
5
6/// Neutron mass in kg.
7pub const NEUTRON_MASS_KG: f64 = 1.674_927_498_04e-27;
8
9/// Neutron mass in atomic mass units (u).
10pub const NEUTRON_MASS_AMU: f64 = 1.008_664_915_95;
11
12/// Neutron mass in MeV/c².
13pub const NEUTRON_MASS_MEV: f64 = 939.565_420_52;
14
15/// Boltzmann constant in eV/K.
16pub const BOLTZMANN_EV_PER_K: f64 = 8.617_333_262e-5;
17
18/// Planck constant (reduced, ħ) in eV·s.
19///
20/// Derived from h = 6.626_070_15e-34 J·s (exact, 2019 SI) and
21/// e = 1.602_176_634e-19 C (exact): ħ = h / (2π·e).
22/// Rounded to 10 significant figures.
23pub const HBAR_EV_S: f64 = 6.582_119_569e-16;
24
25/// Speed of light in m/s.
26pub const SPEED_OF_LIGHT: f64 = 2.997_924_58e8;
27
28/// Fine-structure constant α.
29pub const FINE_STRUCTURE: f64 = 7.297_352_569_3e-3;
30
31/// 1 eV in joules.
32pub const EV_TO_JOULES: f64 = 1.602_176_634e-19;
33
34/// Avogadro's number in mol⁻¹.
35pub const AVOGADRO: f64 = 6.022_140_76e23;
36
37/// Convert neutron energy (eV) to wavelength (Å).
38///
39/// λ = h / √(2·m·E), result in angstroms.
40pub fn energy_to_wavelength_angstrom(energy_ev: f64) -> f64 {
41 // λ(Å) = 0.2860 / √(E in eV) (standard neutron relation)
42 0.286_014_3 / energy_ev.sqrt()
43}
44
45/// Convert neutron time-of-flight (μs) and flight path (m) to energy (eV).
46///
47/// E = ½·m_n·(L/t)²
48///
49/// # Domain contract
50/// Returns [`f64::NAN`] for out-of-domain input rather than a misleading
51/// value. Both arguments must be finite and strictly positive:
52/// * a non-positive `tof_us` is unphysical — the unguarded formula squares
53/// the velocity, so a *negative* TOF would return a *positive* energy, and
54/// `tof_us == 0` would return `+∞`;
55/// * a non-positive `flight_path_m` is equally unphysical — `flight_path_m
56/// == 0` makes the velocity (and energy) `0`, and a *negative* flight path
57/// would (after squaring) return a *positive* energy, masking a bad
58/// detector geometry the same way a negative TOF would;
59/// * a non-finite `tof_us` or `flight_path_m` cannot map to a real energy.
60///
61/// Callers that need to surface bad input as an error (e.g. the PyO3
62/// boundary) check the input before calling or test the result with
63/// `is_finite()`; the in-crate callers (`nereids_io::tof`, the GUI) already
64/// guard `tof > 0 && finite` up-front, so this NaN sentinel never fires on
65/// valid data.
66pub fn tof_to_energy(tof_us: f64, flight_path_m: f64) -> f64 {
67 // `is_finite()` first excludes NaN, so the `<= 0.0` total-order
68 // comparisons that follow are well-defined (clippy's
69 // `neg_cmp_op_on_partial_ord`).
70 if !tof_us.is_finite() || tof_us <= 0.0 || !flight_path_m.is_finite() || flight_path_m <= 0.0 {
71 return f64::NAN;
72 }
73 let t_s = tof_us * 1.0e-6;
74 let v = flight_path_m / t_s;
75 0.5 * NEUTRON_MASS_KG * v * v / EV_TO_JOULES
76}
77
78/// Convert neutron energy (eV) to time-of-flight (μs) given flight path (m).
79///
80/// # Domain contract
81/// Mirrors [`tof_to_energy`]: returns [`f64::NAN`] unless *both* arguments are
82/// finite and strictly positive. A non-positive or non-finite `energy_ev` (the
83/// unguarded formula takes `√energy`, so a negative energy would yield a `NaN`
84/// velocity and `energy_ev == 0` would yield `+∞`), and a non-positive or
85/// non-finite `flight_path_m` (which would yield a zero or *negative* TOF — a
86/// physically impossible time), are all rejected. This keeps the two
87/// directions consistent — both refuse out-of-domain input instead of
88/// returning a plausible-looking number.
89pub fn energy_to_tof(energy_ev: f64, flight_path_m: f64) -> f64 {
90 if !energy_ev.is_finite()
91 || energy_ev <= 0.0
92 || !flight_path_m.is_finite()
93 || flight_path_m <= 0.0
94 {
95 return f64::NAN;
96 }
97 let v = (2.0 * energy_ev * EV_TO_JOULES / NEUTRON_MASS_KG).sqrt();
98 (flight_path_m / v) * 1.0e6
99}
100
101// ── Numerical tolerances ─────────────────────────────────────────────
102// Named constants for magic-number epsilons scattered across physics code.
103
104/// Epsilon for floating-point comparison of quantum numbers (J, L, spin).
105pub const QUANTUM_NUMBER_EPS: f64 = 1e-10;
106
107/// Floor for Poisson model values to avoid log(0) in NLL computation.
108pub const POISSON_EPSILON: f64 = 1e-10;
109
110/// Floor for denominators in physics evaluations (penetrability, shift, etc.)
111/// to avoid division by zero.
112pub const DIVISION_FLOOR: f64 = 1e-50;
113
114/// Generic tiny positive floor used as a near-zero tolerance across physics
115/// calculations (e.g., cross-sections in barns, energies in eV, widths,
116/// dimensionless parameters). Values below this are treated as negligible.
117pub const NEAR_ZERO_FLOOR: f64 = 1e-60;
118
119/// Floor for pivot detection and division safety in numerical linear algebra
120/// (LM solver, Gaussian elimination). Values below this indicate a
121/// (near-)singular system.
122pub const PIVOT_FLOOR: f64 = 1e-30;
123
124/// Floor for Levenberg-Marquardt diagonal elements to ensure damping stability.
125/// Intentionally much larger than PIVOT_FLOOR — LM requires a meaningful
126/// minimum curvature for numerical stability of the trust-region step.
127pub const LM_DIAGONAL_FLOOR: f64 = 1e-10;
128
129/// Floor for avoiding log(0) or division by zero in general computations.
130pub const LOG_FLOOR: f64 = 1e-300;
131
132#[cfg(test)]
133mod tests {
134 use super::*;
135
136 #[test]
137 fn test_tof_energy_roundtrip() {
138 let energy = 6.67; // eV (first U-238 resonance)
139 let flight_path = 25.0; // meters (VENUS)
140 let tof = energy_to_tof(energy, flight_path);
141 let energy_back = tof_to_energy(tof, flight_path);
142 assert!((energy - energy_back).abs() < 1e-10);
143 }
144
145 #[test]
146 fn test_wavelength_thermal() {
147 // Thermal neutrons at 0.0253 eV should have λ ≈ 1.8 Å
148 let lambda = energy_to_wavelength_angstrom(0.0253);
149 assert!((lambda - 1.8).abs() < 0.1);
150 }
151
152 #[test]
153 fn test_tof_to_energy_rejects_non_positive_and_non_finite() {
154 let l = 25.0;
155 // sign), masking an upstream sign/loader bug.
156 assert!(
157 tof_to_energy(-100.0, l).is_nan(),
158 "negative TOF must be NaN"
159 );
160 assert!(tof_to_energy(0.0, l).is_nan(), "zero TOF must be NaN");
161 assert!(tof_to_energy(f64::NAN, l).is_nan(), "NaN TOF must stay NaN");
162 assert!(
163 tof_to_energy(f64::INFINITY, l).is_nan(),
164 "Inf TOF must be NaN"
165 );
166 assert!(
167 tof_to_energy(100.0, f64::NAN).is_nan(),
168 "NaN flight path must be NaN"
169 );
170 assert!(
171 tof_to_energy(100.0, f64::INFINITY).is_nan(),
172 "Inf flight path must be NaN"
173 );
174 // masking a bad detector geometry.
175 assert!(
176 tof_to_energy(100.0, 0.0).is_nan(),
177 "zero flight path must be NaN"
178 );
179 // the sign), the same trap as a negative TOF.
180 assert!(
181 tof_to_energy(100.0, -25.0).is_nan(),
182 "negative flight path must be NaN"
183 );
184 // Valid input still produces a finite, positive energy.
185 assert!(tof_to_energy(100.0, l).is_finite());
186 assert!(tof_to_energy(100.0, l) > 0.0);
187 }
188
189 #[test]
190 fn test_energy_to_tof_rejects_non_positive_and_non_finite() {
191 let l = 25.0;
192 // zero energy returned +∞ — both are now an explicit NaN contract.
193 assert!(
194 energy_to_tof(-1.0, l).is_nan(),
195 "negative energy must be NaN"
196 );
197 assert!(energy_to_tof(0.0, l).is_nan(), "zero energy must be NaN");
198 assert!(
199 energy_to_tof(f64::NAN, l).is_nan(),
200 "NaN energy must stay NaN"
201 );
202 assert!(
203 energy_to_tof(f64::INFINITY, l).is_nan(),
204 "Inf energy must be NaN"
205 );
206 assert!(
207 energy_to_tof(10.0, f64::NAN).is_nan(),
208 "NaN flight path must be NaN"
209 );
210 assert!(
211 energy_to_tof(10.0, f64::INFINITY).is_nan(),
212 "Inf flight path must be NaN"
213 );
214 // a physically impossible time.
215 assert!(
216 energy_to_tof(10.0, 0.0).is_nan(),
217 "zero flight path must be NaN"
218 );
219 assert!(
220 energy_to_tof(10.0, -25.0).is_nan(),
221 "negative flight path must be NaN"
222 );
223 // Valid input still produces a finite, positive TOF.
224 assert!(energy_to_tof(10.0, l).is_finite());
225 assert!(energy_to_tof(10.0, l) > 0.0);
226 }
227}