Skip to main content

use_nuclear/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small nuclear physics helpers.
5
6pub mod prelude;
7
8const SPEED_OF_LIGHT_SQUARED: f64 = SPEED_OF_LIGHT * SPEED_OF_LIGHT;
9
10/// Natural logarithm of `2` for half-life conversions and decay-law helpers.
11///
12/// Broader physical constants belong in the top-level `use-constants` set.
13pub const LN_2: f64 = std::f64::consts::LN_2;
14
15/// Speed of light in vacuum in meters per second.
16///
17/// Broader physical constants belong in the top-level `use-constants` set.
18pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
19
20/// Joules in one mega-electron-volt.
21///
22/// Broader physical constants belong in the top-level `use-constants` set.
23pub const JOULES_PER_MEV: f64 = 1.602_176_634e-13;
24
25/// Atomic mass unit energy equivalent in mega-electron-volts per `c^2`.
26///
27/// Broader physical constants belong in the top-level `use-constants` set.
28pub const ATOMIC_MASS_UNIT_MEV_C2: f64 = 931.494_102_42;
29
30/// Simple exponential-decay law parameterized by a decay constant.
31#[derive(Debug, Clone, Copy, PartialEq)]
32pub struct DecayLaw {
33    /// Decay constant in inverse time units.
34    pub decay_constant: f64,
35}
36
37impl DecayLaw {
38    /// Creates a decay law from a non-negative, finite decay constant.
39    #[must_use]
40    pub fn from_decay_constant(decay_constant: f64) -> Option<Self> {
41        if is_non_negative_finite(decay_constant) {
42            Some(Self { decay_constant })
43        } else {
44            None
45        }
46    }
47
48    /// Creates a decay law from a positive, finite half-life.
49    #[must_use]
50    pub fn from_half_life(half_life: f64) -> Option<Self> {
51        Self::from_decay_constant(decay_constant_from_half_life(half_life)?)
52    }
53
54    /// Returns the half-life for this decay law.
55    #[must_use]
56    pub fn half_life(&self) -> Option<f64> {
57        half_life_from_decay_constant(self.decay_constant)
58    }
59
60    /// Returns the mean lifetime for this decay law.
61    #[must_use]
62    pub fn mean_lifetime(&self) -> Option<f64> {
63        mean_lifetime(self.decay_constant)
64    }
65
66    /// Returns the remaining fraction after the given elapsed time.
67    #[must_use]
68    pub fn remaining_fraction(&self, time: f64) -> Option<f64> {
69        remaining_fraction_from_decay_constant(self.decay_constant, time)
70    }
71
72    /// Returns the remaining quantity after the given elapsed time.
73    ///
74    /// # Examples
75    ///
76    /// ```rust
77    /// use use_nuclear::DecayLaw;
78    ///
79    /// # fn main() -> Result<(), &'static str> {
80    /// let decay_law = DecayLaw::from_half_life(10.0).ok_or("expected valid half-life")?;
81    /// let remaining = decay_law
82    ///     .remaining_quantity(100.0, 10.0)
83    ///     .ok_or("expected valid remaining quantity")?;
84    ///
85    /// assert!((remaining - 50.0).abs() < 1.0e-12);
86    /// # Ok(())
87    /// # }
88    /// ```
89    #[must_use]
90    pub fn remaining_quantity(&self, initial_quantity: f64, time: f64) -> Option<f64> {
91        remaining_quantity_from_decay_constant(initial_quantity, self.decay_constant, time)
92    }
93
94    /// Returns the decayed quantity after the given elapsed time.
95    #[must_use]
96    pub fn decayed_quantity(&self, initial_quantity: f64, time: f64) -> Option<f64> {
97        decayed_quantity_from_decay_constant(initial_quantity, self.decay_constant, time)
98    }
99
100    /// Returns the activity for the given number of nuclei.
101    #[must_use]
102    pub fn activity(&self, number_of_nuclei: f64) -> Option<f64> {
103        activity(self.decay_constant, number_of_nuclei)
104    }
105}
106
107/// Mass and atomic numbers for a nuclide.
108#[derive(Debug, Clone, Copy, PartialEq, Eq)]
109pub struct NuclideNumbers {
110    /// Mass number `A`, equal to total nucleons.
111    pub mass_number: u32,
112    /// Atomic number `Z`, equal to total protons.
113    pub atomic_number: u32,
114}
115
116impl NuclideNumbers {
117    /// Creates validated nuclide numbers.
118    ///
119    /// # Examples
120    ///
121    /// ```rust
122    /// use use_nuclear::NuclideNumbers;
123    ///
124    /// let helium = NuclideNumbers::new(4, 2);
125    ///
126    /// assert_eq!(helium, Some(NuclideNumbers { mass_number: 4, atomic_number: 2 }));
127    /// ```
128    #[must_use]
129    pub const fn new(mass_number: u32, atomic_number: u32) -> Option<Self> {
130        if is_valid_nuclide_numbers(mass_number, atomic_number) {
131            Some(Self {
132                mass_number,
133                atomic_number,
134            })
135        } else {
136            None
137        }
138    }
139
140    /// Returns the proton count for this nuclide.
141    #[must_use]
142    pub const fn proton_count(&self) -> u32 {
143        self.atomic_number
144    }
145
146    /// Returns the neutron count for this nuclide.
147    #[must_use]
148    pub const fn neutron_count(&self) -> u32 {
149        self.mass_number - self.atomic_number
150    }
151
152    /// Returns the total nucleon count for this nuclide.
153    #[must_use]
154    pub const fn nucleon_count(&self) -> u32 {
155        self.mass_number
156    }
157}
158
159/// Computes a decay constant from a half-life.
160///
161/// Formula: `lambda = ln(2) / t_half`.
162///
163/// Returns `None` when `half_life` is not positive and finite, or when the computed decay
164/// constant is not finite.
165///
166/// # Examples
167///
168/// ```rust
169/// use use_nuclear::{decay_constant_from_half_life, LN_2};
170///
171/// # fn main() -> Result<(), &'static str> {
172/// let decay_constant =
173///     decay_constant_from_half_life(10.0).ok_or("expected valid decay constant")?;
174///
175/// assert!((decay_constant - (LN_2 / 10.0)).abs() < 1.0e-12);
176/// # Ok(())
177/// # }
178/// ```
179#[must_use]
180pub fn decay_constant_from_half_life(half_life: f64) -> Option<f64> {
181    if !is_positive_finite(half_life) {
182        return None;
183    }
184
185    finite_non_negative(LN_2 / half_life)
186}
187
188/// Computes a half-life from a decay constant.
189///
190/// Formula: `t_half = ln(2) / lambda`.
191#[must_use]
192pub fn half_life_from_decay_constant(decay_constant: f64) -> Option<f64> {
193    if !is_positive_finite(decay_constant) {
194        return None;
195    }
196
197    finite_non_negative(LN_2 / decay_constant)
198}
199
200/// Computes the mean lifetime from a decay constant.
201///
202/// Formula: `tau = 1 / lambda`.
203#[must_use]
204pub fn mean_lifetime(decay_constant: f64) -> Option<f64> {
205    if !is_positive_finite(decay_constant) {
206        return None;
207    }
208
209    finite_non_negative(1.0 / decay_constant)
210}
211
212/// Computes the remaining fraction from a decay constant and elapsed time.
213///
214/// Formula: `N / N0 = exp(-lambda * t)`.
215#[must_use]
216pub fn remaining_fraction_from_decay_constant(decay_constant: f64, time: f64) -> Option<f64> {
217    if !is_non_negative_finite(decay_constant) || !is_non_negative_finite(time) {
218        return None;
219    }
220
221    finite_unit_interval((-(decay_constant * time)).exp())
222}
223
224/// Computes the remaining fraction from a half-life and elapsed time.
225///
226/// Formula: `N / N0 = 2^(-t / t_half)`.
227///
228/// # Examples
229///
230/// ```rust
231/// use use_nuclear::remaining_fraction_from_half_life;
232///
233/// # fn main() -> Result<(), &'static str> {
234/// let remaining_fraction = remaining_fraction_from_half_life(10.0, 10.0)
235///     .ok_or("expected valid remaining fraction")?;
236///
237/// assert!((remaining_fraction - 0.5).abs() < 1.0e-12);
238/// # Ok(())
239/// # }
240/// ```
241#[must_use]
242pub fn remaining_fraction_from_half_life(half_life: f64, time: f64) -> Option<f64> {
243    if !is_positive_finite(half_life) || !is_non_negative_finite(time) {
244        return None;
245    }
246
247    finite_unit_interval((-(time / half_life)).exp2())
248}
249
250/// Computes the remaining quantity from an initial quantity, decay constant, and elapsed time.
251///
252/// Formula: `N = N0 * exp(-lambda * t)`.
253#[must_use]
254pub fn remaining_quantity_from_decay_constant(
255    initial_quantity: f64,
256    decay_constant: f64,
257    time: f64,
258) -> Option<f64> {
259    if !is_non_negative_finite(initial_quantity) {
260        return None;
261    }
262
263    finite_non_negative(
264        initial_quantity * remaining_fraction_from_decay_constant(decay_constant, time)?,
265    )
266}
267
268/// Computes the remaining quantity from an initial quantity, half-life, and elapsed time.
269#[must_use]
270pub fn remaining_quantity_from_half_life(
271    initial_quantity: f64,
272    half_life: f64,
273    time: f64,
274) -> Option<f64> {
275    if !is_non_negative_finite(initial_quantity) {
276        return None;
277    }
278
279    finite_non_negative(initial_quantity * remaining_fraction_from_half_life(half_life, time)?)
280}
281
282/// Computes the decayed fraction from a decay constant and elapsed time.
283///
284/// Formula: `fraction_decayed = 1 - exp(-lambda * t)`.
285#[must_use]
286pub fn decayed_fraction_from_decay_constant(decay_constant: f64, time: f64) -> Option<f64> {
287    finite_unit_interval(1.0 - remaining_fraction_from_decay_constant(decay_constant, time)?)
288}
289
290/// Computes the decayed quantity from an initial quantity, decay constant, and elapsed time.
291///
292/// Formula: `N_decayed = N0 * (1 - exp(-lambda * t))`.
293#[must_use]
294pub fn decayed_quantity_from_decay_constant(
295    initial_quantity: f64,
296    decay_constant: f64,
297    time: f64,
298) -> Option<f64> {
299    if !is_non_negative_finite(initial_quantity) {
300        return None;
301    }
302
303    finite_non_negative(
304        initial_quantity * decayed_fraction_from_decay_constant(decay_constant, time)?,
305    )
306}
307
308/// Computes elapsed time from a remaining fraction and decay constant.
309///
310/// Formula: `t = -ln(f) / lambda`.
311#[must_use]
312pub fn elapsed_time_from_remaining_fraction(
313    decay_constant: f64,
314    remaining_fraction: f64,
315) -> Option<f64> {
316    if !is_positive_finite(decay_constant)
317        || !remaining_fraction.is_finite()
318        || remaining_fraction <= 0.0
319        || remaining_fraction > 1.0
320    {
321        return None;
322    }
323
324    finite_non_negative(normalize_zero(-remaining_fraction.ln() / decay_constant))
325}
326
327/// Computes activity from a decay constant and number of nuclei.
328///
329/// Formula: `A = lambda * N`.
330///
331/// Activity is returned in becquerels when `decay_constant` is expressed per second.
332///
333/// # Examples
334///
335/// ```rust
336/// use use_nuclear::activity;
337///
338/// assert_eq!(activity(2.0, 10.0), Some(20.0));
339/// ```
340#[must_use]
341pub fn activity(decay_constant: f64, number_of_nuclei: f64) -> Option<f64> {
342    if !is_non_negative_finite(decay_constant) || !is_non_negative_finite(number_of_nuclei) {
343        return None;
344    }
345
346    finite_non_negative(decay_constant * number_of_nuclei)
347}
348
349/// Computes the number of nuclei from activity and decay constant.
350///
351/// Formula: `N = A / lambda`.
352#[must_use]
353pub fn nuclei_from_activity(activity: f64, decay_constant: f64) -> Option<f64> {
354    if !is_non_negative_finite(activity) || !is_positive_finite(decay_constant) {
355        return None;
356    }
357
358    finite_non_negative(activity / decay_constant)
359}
360
361/// Computes specific activity from activity and mass.
362///
363/// Formula: `specific activity = A / m`.
364#[must_use]
365pub fn specific_activity(activity: f64, mass: f64) -> Option<f64> {
366    if !is_non_negative_finite(activity) || !is_positive_finite(mass) {
367        return None;
368    }
369
370    finite_non_negative(activity / mass)
371}
372
373/// Computes energy in joules from a mass defect in kilograms.
374///
375/// Formula: `E = delta_m * c^2`.
376///
377/// # Examples
378///
379/// ```rust
380/// use use_nuclear::{energy_from_mass_defect_kg, SPEED_OF_LIGHT};
381///
382/// # fn main() -> Result<(), &'static str> {
383/// let energy = energy_from_mass_defect_kg(1.0).ok_or("expected valid energy")?;
384///
385/// assert!((energy - (SPEED_OF_LIGHT * SPEED_OF_LIGHT)).abs() < 1.0e-12);
386/// # Ok(())
387/// # }
388/// ```
389#[must_use]
390pub fn energy_from_mass_defect_kg(mass_defect_kg: f64) -> Option<f64> {
391    if !is_non_negative_finite(mass_defect_kg) {
392        return None;
393    }
394
395    finite_non_negative(mass_defect_kg * SPEED_OF_LIGHT_SQUARED)
396}
397
398/// Computes mass defect in kilograms from energy in joules.
399///
400/// Formula: `delta_m = E / c^2`.
401#[must_use]
402pub fn mass_defect_kg_from_energy(energy_joules: f64) -> Option<f64> {
403    if !is_non_negative_finite(energy_joules) {
404        return None;
405    }
406
407    finite_non_negative(energy_joules / SPEED_OF_LIGHT_SQUARED)
408}
409
410/// Converts energy in joules to mega-electron-volts.
411#[must_use]
412pub fn joules_to_mev(joules: f64) -> Option<f64> {
413    if !is_non_negative_finite(joules) {
414        return None;
415    }
416
417    finite_non_negative(joules / JOULES_PER_MEV)
418}
419
420/// Converts energy in mega-electron-volts to joules.
421#[must_use]
422pub fn mev_to_joules(mev: f64) -> Option<f64> {
423    if !is_non_negative_finite(mev) {
424        return None;
425    }
426
427    finite_non_negative(mev * JOULES_PER_MEV)
428}
429
430/// Computes binding energy in mega-electron-volts from a mass defect in atomic mass units.
431///
432/// Formula: `E(MeV) = delta_m(u) * 931.49410242`.
433///
434/// # Examples
435///
436/// ```rust
437/// use use_nuclear::{binding_energy_mev_from_mass_defect_u, ATOMIC_MASS_UNIT_MEV_C2};
438///
439/// # fn main() -> Result<(), &'static str> {
440/// let binding_energy =
441///     binding_energy_mev_from_mass_defect_u(1.0).ok_or("expected valid binding energy")?;
442///
443/// assert!((binding_energy - ATOMIC_MASS_UNIT_MEV_C2).abs() < 1.0e-12);
444/// # Ok(())
445/// # }
446/// ```
447#[must_use]
448pub fn binding_energy_mev_from_mass_defect_u(mass_defect_u: f64) -> Option<f64> {
449    if !is_non_negative_finite(mass_defect_u) {
450        return None;
451    }
452
453    finite_non_negative(mass_defect_u * ATOMIC_MASS_UNIT_MEV_C2)
454}
455
456/// Computes binding energy per nucleon.
457///
458/// Formula: `E_per_nucleon = E_binding / A`.
459#[must_use]
460pub fn binding_energy_per_nucleon(binding_energy_mev: f64, nucleon_count: u32) -> Option<f64> {
461    if !is_non_negative_finite(binding_energy_mev) || nucleon_count == 0 {
462        return None;
463    }
464
465    finite_non_negative(binding_energy_mev / f64::from(nucleon_count))
466}
467
468/// Computes neutron count from mass number and atomic number.
469///
470/// Formula: `N = A - Z`.
471///
472/// # Examples
473///
474/// ```rust
475/// use use_nuclear::neutron_count;
476///
477/// assert_eq!(neutron_count(4, 2), Some(2));
478/// ```
479#[must_use]
480pub const fn neutron_count(mass_number: u32, atomic_number: u32) -> Option<u32> {
481    mass_number.checked_sub(atomic_number)
482}
483
484/// Computes nucleon count from proton and neutron counts.
485///
486/// Formula: `A = Z + N`.
487#[must_use]
488pub const fn nucleon_count(proton_count: u32, neutron_count: u32) -> Option<u32> {
489    proton_count.checked_add(neutron_count)
490}
491
492/// Validates mass and atomic numbers for a simple nuclide representation.
493#[must_use]
494pub const fn is_valid_nuclide_numbers(mass_number: u32, atomic_number: u32) -> bool {
495    mass_number != 0 && atomic_number != 0 && atomic_number <= mass_number
496}
497
498fn is_non_negative_finite(value: f64) -> bool {
499    value.is_finite() && value >= 0.0
500}
501
502fn is_positive_finite(value: f64) -> bool {
503    value.is_finite() && value > 0.0
504}
505
506fn finite_non_negative(value: f64) -> Option<f64> {
507    if value.is_finite() && value >= 0.0 {
508        Some(normalize_zero(value))
509    } else {
510        None
511    }
512}
513
514fn finite_unit_interval(value: f64) -> Option<f64> {
515    if value.is_finite() && (0.0..=1.0).contains(&value) {
516        Some(normalize_zero(value))
517    } else {
518        None
519    }
520}
521
522fn normalize_zero(value: f64) -> f64 {
523    if value == 0.0 { 0.0 } else { value }
524}
525
526#[cfg(test)]
527#[allow(clippy::float_cmp)]
528mod tests {
529    use super::{
530        ATOMIC_MASS_UNIT_MEV_C2, DecayLaw, JOULES_PER_MEV, LN_2, NuclideNumbers, SPEED_OF_LIGHT,
531        activity, binding_energy_mev_from_mass_defect_u, binding_energy_per_nucleon,
532        decay_constant_from_half_life, decayed_fraction_from_decay_constant,
533        decayed_quantity_from_decay_constant, elapsed_time_from_remaining_fraction,
534        energy_from_mass_defect_kg, half_life_from_decay_constant, is_valid_nuclide_numbers,
535        joules_to_mev, mass_defect_kg_from_energy, mean_lifetime, mev_to_joules, neutron_count,
536        nuclei_from_activity, nucleon_count, remaining_fraction_from_decay_constant,
537        remaining_fraction_from_half_life, remaining_quantity_from_decay_constant,
538        remaining_quantity_from_half_life, specific_activity,
539    };
540
541    fn assert_approx_eq(left: f64, right: f64) {
542        let tolerance = 1.0e-12 * left.abs().max(right.abs()).max(1.0);
543        assert!(
544            (left - right).abs() <= tolerance,
545            "left={left}, right={right}, tolerance={tolerance}"
546        );
547    }
548
549    fn assert_some_approx(actual: Option<f64>, expected: f64) {
550        let Some(actual) = actual else {
551            panic!("expected Some({expected})");
552        };
553
554        assert_approx_eq(actual, expected);
555    }
556
557    #[test]
558    fn decay_constant_and_half_life_helpers_cover_common_cases() {
559        assert_some_approx(decay_constant_from_half_life(10.0), LN_2 / 10.0);
560        assert_eq!(decay_constant_from_half_life(0.0), None);
561        assert_eq!(decay_constant_from_half_life(-1.0), None);
562
563        assert_some_approx(half_life_from_decay_constant(LN_2 / 10.0), 10.0);
564        assert_eq!(half_life_from_decay_constant(0.0), None);
565    }
566
567    #[test]
568    fn mean_lifetime_requires_positive_decay_constant() {
569        assert_eq!(mean_lifetime(2.0), Some(0.5));
570        assert_eq!(mean_lifetime(0.0), None);
571    }
572
573    #[test]
574    fn remaining_fraction_and_quantity_helpers_follow_decay_law() {
575        assert_eq!(remaining_fraction_from_decay_constant(0.0, 10.0), Some(1.0));
576        assert_some_approx(remaining_fraction_from_half_life(10.0, 10.0), 0.5);
577        assert_some_approx(remaining_fraction_from_half_life(10.0, 20.0), 0.25);
578        assert_eq!(remaining_fraction_from_half_life(0.0, 10.0), None);
579        assert_eq!(remaining_fraction_from_half_life(10.0, -1.0), None);
580
581        assert_some_approx(remaining_quantity_from_half_life(100.0, 10.0, 10.0), 50.0);
582        assert_some_approx(
583            remaining_quantity_from_decay_constant(100.0, LN_2 / 10.0, 10.0),
584            50.0,
585        );
586    }
587
588    #[test]
589    fn decayed_fraction_and_elapsed_time_round_trip() {
590        assert_some_approx(decayed_fraction_from_decay_constant(LN_2 / 10.0, 10.0), 0.5);
591        assert_some_approx(
592            decayed_quantity_from_decay_constant(100.0, LN_2 / 10.0, 10.0),
593            50.0,
594        );
595
596        assert_some_approx(elapsed_time_from_remaining_fraction(LN_2 / 10.0, 0.5), 10.0);
597        assert_eq!(elapsed_time_from_remaining_fraction(LN_2 / 10.0, 0.0), None);
598        assert_eq!(elapsed_time_from_remaining_fraction(LN_2 / 10.0, 1.5), None);
599    }
600
601    #[test]
602    fn activity_helpers_validate_inputs() {
603        assert_eq!(activity(2.0, 10.0), Some(20.0));
604        assert_eq!(activity(-1.0, 10.0), None);
605        assert_eq!(activity(1.0, -10.0), None);
606
607        assert_eq!(nuclei_from_activity(20.0, 2.0), Some(10.0));
608        assert_eq!(nuclei_from_activity(20.0, 0.0), None);
609
610        assert_eq!(specific_activity(100.0, 5.0), Some(20.0));
611        assert_eq!(specific_activity(100.0, 0.0), None);
612    }
613
614    #[test]
615    fn mass_energy_helpers_convert_between_common_units() {
616        assert_some_approx(
617            energy_from_mass_defect_kg(1.0),
618            SPEED_OF_LIGHT * SPEED_OF_LIGHT,
619        );
620        assert_eq!(energy_from_mass_defect_kg(-1.0), None);
621
622        assert_some_approx(
623            mass_defect_kg_from_energy(SPEED_OF_LIGHT * SPEED_OF_LIGHT),
624            1.0,
625        );
626
627        assert_some_approx(joules_to_mev(JOULES_PER_MEV), 1.0);
628        assert_some_approx(mev_to_joules(1.0), JOULES_PER_MEV);
629
630        assert_some_approx(
631            binding_energy_mev_from_mass_defect_u(1.0),
632            ATOMIC_MASS_UNIT_MEV_C2,
633        );
634        assert_eq!(binding_energy_per_nucleon(28.0, 4), Some(7.0));
635        assert_eq!(binding_energy_per_nucleon(28.0, 0), None);
636    }
637
638    #[test]
639    fn nuclear_composition_helpers_validate_counts() {
640        assert_eq!(neutron_count(4, 2), Some(2));
641        assert_eq!(neutron_count(2, 4), None);
642
643        assert_eq!(nucleon_count(2, 2), Some(4));
644
645        assert!(is_valid_nuclide_numbers(4, 2));
646        assert!(!is_valid_nuclide_numbers(0, 0));
647        assert!(!is_valid_nuclide_numbers(4, 0));
648        assert!(!is_valid_nuclide_numbers(2, 4));
649    }
650
651    #[test]
652    fn decay_law_methods_delegate_to_public_helpers() {
653        assert_some_approx(
654            DecayLaw::from_half_life(10.0).and_then(|decay_law| decay_law.remaining_fraction(10.0)),
655            0.5,
656        );
657        assert_some_approx(
658            DecayLaw::from_decay_constant(LN_2 / 10.0).and_then(|decay_law| decay_law.half_life()),
659            10.0,
660        );
661        assert_eq!(DecayLaw::from_decay_constant(-1.0), None);
662    }
663
664    #[test]
665    fn nuclide_numbers_methods_expose_component_counts() {
666        let Some(helium) = NuclideNumbers::new(4, 2) else {
667            panic!("expected helium nuclide numbers");
668        };
669
670        assert_eq!(helium.proton_count(), 2);
671        assert_eq!(helium.neutron_count(), 2);
672        assert_eq!(helium.nucleon_count(), 4);
673        assert_eq!(NuclideNumbers::new(2, 4), None);
674    }
675
676    #[test]
677    fn non_finite_inputs_return_none() {
678        assert_eq!(decay_constant_from_half_life(f64::NAN), None);
679        assert_eq!(
680            remaining_fraction_from_decay_constant(1.0, f64::INFINITY),
681            None
682        );
683        assert_eq!(activity(f64::INFINITY, 10.0), None);
684        assert_eq!(energy_from_mass_defect_kg(f64::NAN), None);
685    }
686}