Skip to main content

use_radiation/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small scalar helpers for radiation physics calculations.
5
6use core::f64::consts::PI;
7
8pub mod prelude;
9
10/// Speed of light in vacuum, in meters per second.
11///
12/// Broader physical constants belong in the top-level `use-constants` set.
13pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
14
15/// Planck's constant in joule-seconds.
16///
17/// Broader physical constants belong in the top-level `use-constants` set.
18pub const PLANCK_CONSTANT: f64 = 6.626_070_15e-34;
19
20/// Elementary charge in coulombs.
21///
22/// Broader physical constants belong in the top-level `use-constants` set.
23pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
24
25/// Joules in one mega-electron-volt.
26///
27/// Broader physical constants belong in the top-level `use-constants` set.
28pub const JOULES_PER_MEV: f64 = 1.602_176_634e-13;
29
30fn is_non_negative_finite(value: f64) -> bool {
31    value.is_finite() && value >= 0.0
32}
33
34fn is_positive_finite(value: f64) -> bool {
35    value.is_finite() && value > 0.0
36}
37
38fn normalize_zero(value: f64) -> f64 {
39    if value == 0.0 { 0.0 } else { value }
40}
41
42fn finite_non_negative(value: f64) -> Option<f64> {
43    (value.is_finite() && value >= 0.0).then_some(normalize_zero(value))
44}
45
46fn finite_unit_interval(value: f64) -> Option<f64> {
47    (value.is_finite() && (0.0..=1.0).contains(&value)).then_some(normalize_zero(value))
48}
49
50fn divide_non_negative(numerator: f64, denominator: f64) -> Option<f64> {
51    if !is_non_negative_finite(numerator) || !is_positive_finite(denominator) {
52        return None;
53    }
54
55    finite_non_negative(numerator / denominator)
56}
57
58fn multiply_non_negative(left: f64, right: f64) -> Option<f64> {
59    if !is_non_negative_finite(left) || !is_non_negative_finite(right) {
60        return None;
61    }
62
63    finite_non_negative(left * right)
64}
65
66/// Computes photon energy from frequency with `E = h * f`.
67///
68/// Returns joules.
69///
70/// Broader photon helpers also exist in `use-quantum`.
71#[must_use]
72pub fn photon_energy_from_frequency(frequency: f64) -> Option<f64> {
73    if !is_non_negative_finite(frequency) {
74        return None;
75    }
76
77    finite_non_negative(PLANCK_CONSTANT * frequency)
78}
79
80/// Computes photon energy from wavelength with `E = h * c / lambda`.
81///
82/// Returns joules.
83#[must_use]
84pub fn photon_energy_from_wavelength(wavelength: f64) -> Option<f64> {
85    if !is_positive_finite(wavelength) {
86        return None;
87    }
88
89    finite_non_negative((PLANCK_CONSTANT * SPEED_OF_LIGHT) / wavelength)
90}
91
92/// Computes photon flux from power with `Phi = P / E_photon`.
93///
94/// Returns photons per second.
95///
96/// # Examples
97///
98/// ```rust
99/// use use_radiation::photon_flux_from_power;
100///
101/// assert_eq!(photon_flux_from_power(10.0, 2.0), Some(5.0));
102/// ```
103#[must_use]
104pub fn photon_flux_from_power(power: f64, photon_energy: f64) -> Option<f64> {
105    divide_non_negative(power, photon_energy)
106}
107
108/// Computes photon flux density with `phi = Phi / A`.
109///
110/// Returns photons per square meter per second.
111#[must_use]
112pub fn photon_flux_density(photon_flux: f64, area: f64) -> Option<f64> {
113    divide_non_negative(photon_flux, area)
114}
115
116/// Computes intensity from power and area with `I = P / A`.
117///
118/// Returns watts per square meter.
119///
120/// # Examples
121///
122/// ```rust
123/// use use_radiation::intensity;
124///
125/// assert_eq!(intensity(10.0, 2.0), Some(5.0));
126/// ```
127#[must_use]
128pub fn intensity(power: f64, area: f64) -> Option<f64> {
129    divide_non_negative(power, area)
130}
131
132/// Computes isotropic intensity from power and distance with `I = P / (4 * pi * r^2)`.
133///
134/// # Examples
135///
136/// ```rust
137/// use use_radiation::isotropic_intensity;
138///
139/// let Some(value) = isotropic_intensity(4.0 * core::f64::consts::PI, 1.0) else {
140///     panic!("expected isotropic intensity");
141/// };
142///
143/// assert!((value - 1.0).abs() < 1.0e-12);
144/// ```
145#[must_use]
146pub fn isotropic_intensity(power: f64, distance: f64) -> Option<f64> {
147    if !is_non_negative_finite(power) || !is_positive_finite(distance) {
148        return None;
149    }
150
151    intensity(power, 4.0 * PI * distance * distance)
152}
153
154/// Computes inverse-square intensity from a reference value.
155///
156/// Formula: `I2 = I1 * (r1 / r2)^2`.
157#[must_use]
158pub fn inverse_square_intensity(
159    reference_intensity: f64,
160    reference_distance: f64,
161    target_distance: f64,
162) -> Option<f64> {
163    if !is_non_negative_finite(reference_intensity)
164        || !is_positive_finite(reference_distance)
165        || !is_positive_finite(target_distance)
166    {
167        return None;
168    }
169
170    let ratio = reference_distance / target_distance;
171    finite_non_negative(reference_intensity * ratio * ratio)
172}
173
174/// Computes fluence with `F = N / A`.
175///
176/// Returns particles per square meter.
177#[must_use]
178pub fn fluence(particle_count: f64, area: f64) -> Option<f64> {
179    divide_non_negative(particle_count, area)
180}
181
182/// Computes energy fluence with `Psi = E / A`.
183///
184/// Returns joules per square meter.
185#[must_use]
186pub fn energy_fluence(energy: f64, area: f64) -> Option<f64> {
187    divide_non_negative(energy, area)
188}
189
190/// Computes fluence rate with `fluence_rate = F / t`.
191#[must_use]
192pub fn fluence_rate(fluence: f64, time: f64) -> Option<f64> {
193    divide_non_negative(fluence, time)
194}
195
196/// Computes absorbed dose with `D = E / m`.
197///
198/// Returns gray, equivalent to joules per kilogram.
199///
200/// # Examples
201///
202/// ```rust
203/// use use_radiation::absorbed_dose;
204///
205/// assert_eq!(absorbed_dose(20.0, 4.0), Some(5.0));
206/// ```
207#[must_use]
208pub fn absorbed_dose(energy_absorbed: f64, mass: f64) -> Option<f64> {
209    divide_non_negative(energy_absorbed, mass)
210}
211
212/// Computes absorbed energy from dose with `E = D * m`.
213#[must_use]
214pub fn absorbed_energy_from_dose(dose: f64, mass: f64) -> Option<f64> {
215    multiply_non_negative(dose, mass)
216}
217
218/// Computes dose rate with `dose_rate = D / t`.
219#[must_use]
220pub fn dose_rate(dose: f64, time: f64) -> Option<f64> {
221    divide_non_negative(dose, time)
222}
223
224/// Computes accumulated dose with `D = dose_rate * t`.
225#[must_use]
226pub fn accumulated_dose(dose_rate: f64, time: f64) -> Option<f64> {
227    multiply_non_negative(dose_rate, time)
228}
229
230/// Computes equivalent dose with `H = D * w_R`.
231///
232/// Returns sieverts.
233///
234/// # Examples
235///
236/// ```rust
237/// use use_radiation::equivalent_dose;
238///
239/// assert_eq!(equivalent_dose(2.0, 20.0), Some(40.0));
240/// ```
241#[must_use]
242pub fn equivalent_dose(absorbed_dose: f64, radiation_weighting_factor: f64) -> Option<f64> {
243    multiply_non_negative(absorbed_dose, radiation_weighting_factor)
244}
245
246/// Computes effective dose with `E = H * w_T`.
247///
248/// Returns sieverts.
249#[must_use]
250pub fn effective_dose(equivalent_dose: f64, tissue_weighting_factor: f64) -> Option<f64> {
251    multiply_non_negative(equivalent_dose, tissue_weighting_factor)
252}
253
254/// Sums pre-weighted equivalent doses into a total effective dose.
255#[must_use]
256pub fn total_effective_dose(weighted_equivalent_doses: &[f64]) -> Option<f64> {
257    weighted_equivalent_doses
258        .iter()
259        .try_fold(0.0, |sum, value| {
260            if !is_non_negative_finite(*value) {
261                return None;
262            }
263
264            finite_non_negative(sum + *value)
265        })
266}
267
268/// Computes attenuated intensity with `I = I0 * e^(-mu * x)`.
269///
270/// # Examples
271///
272/// ```rust
273/// use use_radiation::attenuated_intensity;
274///
275/// let Some(value) = attenuated_intensity(100.0, core::f64::consts::LN_2, 1.0) else {
276///     panic!("expected attenuated intensity");
277/// };
278///
279/// assert!((value - 50.0).abs() < 1.0e-12);
280/// ```
281#[must_use]
282pub fn attenuated_intensity(
283    initial_intensity: f64,
284    linear_attenuation_coefficient: f64,
285    thickness: f64,
286) -> Option<f64> {
287    if !is_non_negative_finite(initial_intensity)
288        || !is_non_negative_finite(linear_attenuation_coefficient)
289        || !is_non_negative_finite(thickness)
290    {
291        return None;
292    }
293
294    finite_non_negative(
295        initial_intensity * transmitted_fraction(linear_attenuation_coefficient, thickness)?,
296    )
297}
298
299/// Computes transmitted fraction with `T = e^(-mu * x)`.
300#[must_use]
301pub fn transmitted_fraction(linear_attenuation_coefficient: f64, thickness: f64) -> Option<f64> {
302    if !is_non_negative_finite(linear_attenuation_coefficient) || !is_non_negative_finite(thickness)
303    {
304        return None;
305    }
306
307    finite_unit_interval((-(linear_attenuation_coefficient * thickness)).exp())
308}
309
310/// Computes required shield thickness with `x = ln(I0 / I) / mu`.
311#[must_use]
312pub fn required_shield_thickness(
313    linear_attenuation_coefficient: f64,
314    initial_intensity: f64,
315    target_intensity: f64,
316) -> Option<f64> {
317    if !is_positive_finite(linear_attenuation_coefficient)
318        || !is_positive_finite(initial_intensity)
319        || !is_positive_finite(target_intensity)
320        || target_intensity > initial_intensity
321    {
322        return None;
323    }
324
325    finite_non_negative(
326        (initial_intensity / target_intensity).ln() / linear_attenuation_coefficient,
327    )
328}
329
330/// Computes half-value layer with `HVL = ln(2) / mu`.
331///
332/// # Examples
333///
334/// ```rust
335/// use use_radiation::half_value_layer;
336///
337/// let Some(value) = half_value_layer(core::f64::consts::LN_2) else {
338///     panic!("expected half-value layer");
339/// };
340///
341/// assert!((value - 1.0).abs() < 1.0e-12);
342/// ```
343#[must_use]
344pub fn half_value_layer(linear_attenuation_coefficient: f64) -> Option<f64> {
345    if !is_positive_finite(linear_attenuation_coefficient) {
346        return None;
347    }
348
349    finite_non_negative(core::f64::consts::LN_2 / linear_attenuation_coefficient)
350}
351
352/// Computes tenth-value layer with `TVL = ln(10) / mu`.
353#[must_use]
354pub fn tenth_value_layer(linear_attenuation_coefficient: f64) -> Option<f64> {
355    if !is_positive_finite(linear_attenuation_coefficient) {
356        return None;
357    }
358
359    finite_non_negative(core::f64::consts::LN_10 / linear_attenuation_coefficient)
360}
361
362/// Computes linear attenuation coefficient from mass attenuation and density.
363///
364/// Formula: `mu = (mu / rho) * rho`.
365#[must_use]
366pub fn linear_attenuation_from_mass_attenuation(
367    mass_attenuation_coefficient: f64,
368    density: f64,
369) -> Option<f64> {
370    multiply_non_negative(mass_attenuation_coefficient, density)
371}
372
373/// Computes mass attenuation coefficient from linear attenuation and density.
374///
375/// Formula: `mu / rho = mu / rho`.
376#[must_use]
377pub fn mass_attenuation_from_linear_attenuation(
378    linear_attenuation_coefficient: f64,
379    density: f64,
380) -> Option<f64> {
381    divide_non_negative(linear_attenuation_coefficient, density)
382}
383
384/// Simple radiation categories for scalar helper selection.
385#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
386pub enum RadiationKind {
387    /// Alpha-particle radiation.
388    Alpha,
389    /// Beta-minus radiation.
390    BetaMinus,
391    /// Beta-plus radiation.
392    BetaPlus,
393    /// Gamma radiation.
394    Gamma,
395    /// X-ray radiation.
396    XRay,
397    /// Neutron radiation.
398    Neutron,
399    /// Proton radiation.
400    Proton,
401    /// Electron radiation.
402    Electron,
403    /// Generic photon radiation.
404    Photon,
405}
406
407/// Returns whether the listed radiation kind is ionizing in this simple crate.
408#[must_use]
409pub const fn is_ionizing(_kind: RadiationKind) -> bool {
410    true
411}
412
413/// Returns whether the listed radiation kind is photon radiation.
414#[must_use]
415pub const fn is_photon_radiation(kind: RadiationKind) -> bool {
416    matches!(
417        kind,
418        RadiationKind::Gamma | RadiationKind::XRay | RadiationKind::Photon
419    )
420}
421
422/// Returns whether the listed radiation kind is particle radiation.
423#[must_use]
424pub const fn is_particle_radiation(kind: RadiationKind) -> bool {
425    !is_photon_radiation(kind)
426}
427
428/// Returns a simple conventional radiation weighting factor for the given kind.
429///
430/// These values are example conveniences, not safety guidance.
431/// Neutron weighting depends on energy and is intentionally omitted in v1.
432#[must_use]
433pub const fn default_radiation_weighting_factor(kind: RadiationKind) -> Option<f64> {
434    match kind {
435        RadiationKind::Gamma
436        | RadiationKind::XRay
437        | RadiationKind::BetaMinus
438        | RadiationKind::BetaPlus
439        | RadiationKind::Electron
440        | RadiationKind::Photon => Some(1.0),
441        RadiationKind::Proton => Some(2.0),
442        RadiationKind::Alpha => Some(20.0),
443        RadiationKind::Neutron => None,
444    }
445}
446
447/// A simple beam characterized by total power and illuminated area.
448#[derive(Debug, Clone, Copy, PartialEq)]
449pub struct RadiationBeam {
450    /// Beam power in watts.
451    pub power: f64,
452    /// Beam cross-sectional area in square meters.
453    pub area: f64,
454}
455
456impl RadiationBeam {
457    /// Creates a beam from non-negative finite power and positive finite area.
458    #[must_use]
459    pub fn new(power: f64, area: f64) -> Option<Self> {
460        if !is_non_negative_finite(power) || !is_positive_finite(area) {
461            return None;
462        }
463
464        Some(Self { power, area })
465    }
466
467    /// Computes beam intensity.
468    ///
469    /// # Examples
470    ///
471    /// ```rust
472    /// use use_radiation::RadiationBeam;
473    ///
474    /// # fn main() -> Result<(), &'static str> {
475    /// let beam = RadiationBeam::new(10.0, 2.0).ok_or("expected beam")?;
476    ///
477    /// assert_eq!(beam.intensity(), Some(5.0));
478    /// # Ok(())
479    /// # }
480    /// ```
481    #[must_use]
482    pub fn intensity(&self) -> Option<f64> {
483        intensity(self.power, self.area)
484    }
485
486    /// Computes photon flux for this beam.
487    #[must_use]
488    pub fn photon_flux(&self, photon_energy: f64) -> Option<f64> {
489        photon_flux_from_power(self.power, photon_energy)
490    }
491
492    /// Computes photon flux density for this beam.
493    #[must_use]
494    pub fn photon_flux_density(&self, photon_energy: f64) -> Option<f64> {
495        photon_flux_density(self.photon_flux(photon_energy)?, self.area)
496    }
497}
498
499/// A simple slab shield with a linear attenuation coefficient and thickness.
500#[derive(Debug, Clone, Copy, PartialEq)]
501pub struct Shield {
502    /// Linear attenuation coefficient in inverse meters.
503    pub linear_attenuation_coefficient: f64,
504    /// Shield thickness in meters.
505    pub thickness: f64,
506}
507
508impl Shield {
509    /// Creates a shield from non-negative finite attenuation and thickness values.
510    #[must_use]
511    pub fn new(linear_attenuation_coefficient: f64, thickness: f64) -> Option<Self> {
512        if !is_non_negative_finite(linear_attenuation_coefficient)
513            || !is_non_negative_finite(thickness)
514        {
515            return None;
516        }
517
518        Some(Self {
519            linear_attenuation_coefficient,
520            thickness,
521        })
522    }
523
524    /// Computes transmitted fraction for this shield.
525    #[must_use]
526    pub fn transmitted_fraction(&self) -> Option<f64> {
527        transmitted_fraction(self.linear_attenuation_coefficient, self.thickness)
528    }
529
530    /// Computes attenuated intensity through this shield.
531    ///
532    /// # Examples
533    ///
534    /// ```rust
535    /// use use_radiation::Shield;
536    ///
537    /// # fn main() -> Result<(), &'static str> {
538    /// let shield = Shield::new(core::f64::consts::LN_2, 1.0).ok_or("expected shield")?;
539    /// let attenuated = shield
540    ///     .attenuated_intensity(100.0)
541    ///     .ok_or("expected attenuated intensity")?;
542    ///
543    /// assert!((attenuated - 50.0).abs() < 1.0e-12);
544    /// # Ok(())
545    /// # }
546    /// ```
547    #[must_use]
548    pub fn attenuated_intensity(&self, initial_intensity: f64) -> Option<f64> {
549        attenuated_intensity(
550            initial_intensity,
551            self.linear_attenuation_coefficient,
552            self.thickness,
553        )
554    }
555}
556
557/// A simple absorbed dose wrapper in gray.
558#[derive(Debug, Clone, Copy, PartialEq)]
559pub struct Dose {
560    /// Absorbed dose in gray.
561    pub absorbed_dose: f64,
562}
563
564impl Dose {
565    /// Creates a dose from a non-negative finite absorbed dose.
566    #[must_use]
567    pub fn new(absorbed_dose: f64) -> Option<Self> {
568        if !is_non_negative_finite(absorbed_dose) {
569            return None;
570        }
571
572        Some(Self { absorbed_dose })
573    }
574
575    /// Computes equivalent dose from this absorbed dose.
576    ///
577    /// # Examples
578    ///
579    /// ```rust
580    /// use use_radiation::Dose;
581    ///
582    /// # fn main() -> Result<(), &'static str> {
583    /// let dose = Dose::new(2.0).ok_or("expected dose")?;
584    ///
585    /// assert_eq!(dose.equivalent(20.0), Some(40.0));
586    /// # Ok(())
587    /// # }
588    /// ```
589    #[must_use]
590    pub fn equivalent(self, radiation_weighting_factor: f64) -> Option<f64> {
591        equivalent_dose(self.absorbed_dose, radiation_weighting_factor)
592    }
593
594    /// Computes dose rate over the given time.
595    #[must_use]
596    pub fn rate_over(self, time: f64) -> Option<f64> {
597        dose_rate(self.absorbed_dose, time)
598    }
599}
600
601#[cfg(test)]
602#[allow(clippy::float_cmp)]
603mod tests {
604    use super::{
605        Dose, ELEMENTARY_CHARGE, JOULES_PER_MEV, PLANCK_CONSTANT, RadiationBeam, RadiationKind,
606        SPEED_OF_LIGHT, Shield, absorbed_dose, absorbed_energy_from_dose, accumulated_dose,
607        attenuated_intensity, default_radiation_weighting_factor, dose_rate, effective_dose,
608        energy_fluence, equivalent_dose, fluence, fluence_rate, half_value_layer, intensity,
609        inverse_square_intensity, is_ionizing, is_particle_radiation, is_photon_radiation,
610        isotropic_intensity, linear_attenuation_from_mass_attenuation,
611        mass_attenuation_from_linear_attenuation, photon_energy_from_frequency,
612        photon_energy_from_wavelength, photon_flux_density, photon_flux_from_power,
613        required_shield_thickness, tenth_value_layer, total_effective_dose, transmitted_fraction,
614    };
615
616    fn assert_approx_eq(left: f64, right: f64) {
617        let tolerance = 1.0e-12 * left.abs().max(right.abs()).max(1.0);
618        assert!(
619            (left - right).abs() <= tolerance,
620            "left={left}, right={right}, tolerance={tolerance}"
621        );
622    }
623
624    fn assert_some_approx(actual: Option<f64>, expected: f64) {
625        let Some(actual) = actual else {
626            panic!("expected Some({expected})");
627        };
628
629        assert_approx_eq(actual, expected);
630    }
631
632    #[test]
633    fn photon_energy_helpers_validate_inputs() {
634        assert_eq!(photon_energy_from_frequency(1.0), Some(PLANCK_CONSTANT));
635        assert_eq!(photon_energy_from_frequency(-1.0), None);
636        assert_eq!(photon_energy_from_frequency(f64::NAN), None);
637
638        assert_some_approx(
639            photon_energy_from_wavelength(SPEED_OF_LIGHT),
640            PLANCK_CONSTANT,
641        );
642        assert_eq!(photon_energy_from_wavelength(0.0), None);
643        assert_eq!(photon_energy_from_wavelength(f64::INFINITY), None);
644    }
645
646    #[test]
647    fn photon_flux_helpers_cover_common_cases() {
648        assert_eq!(photon_flux_from_power(10.0, 2.0), Some(5.0));
649        assert_eq!(photon_flux_from_power(10.0, 0.0), None);
650        assert_eq!(photon_flux_from_power(-1.0, 2.0), None);
651
652        assert_eq!(photon_flux_density(10.0, 2.0), Some(5.0));
653        assert_eq!(photon_flux_density(10.0, 0.0), None);
654    }
655
656    #[test]
657    fn intensity_helpers_cover_planar_and_inverse_square_cases() {
658        assert_eq!(intensity(10.0, 2.0), Some(5.0));
659        assert_eq!(intensity(-10.0, 2.0), None);
660        assert_eq!(intensity(10.0, 0.0), None);
661
662        assert_some_approx(isotropic_intensity(4.0 * core::f64::consts::PI, 1.0), 1.0);
663        assert_eq!(isotropic_intensity(1.0, 0.0), None);
664
665        assert_eq!(inverse_square_intensity(100.0, 1.0, 2.0), Some(25.0));
666        assert_eq!(inverse_square_intensity(100.0, 1.0, 0.0), None);
667    }
668
669    #[test]
670    fn fluence_helpers_validate_inputs() {
671        assert_eq!(fluence(100.0, 2.0), Some(50.0));
672        assert_eq!(fluence(-100.0, 2.0), None);
673
674        assert_eq!(energy_fluence(100.0, 2.0), Some(50.0));
675        assert_eq!(fluence_rate(50.0, 10.0), Some(5.0));
676        assert_eq!(fluence_rate(50.0, 0.0), None);
677    }
678
679    #[test]
680    fn absorbed_dose_helpers_cover_forward_and_inverse_relations() {
681        assert_eq!(absorbed_dose(20.0, 4.0), Some(5.0));
682        assert_eq!(absorbed_dose(20.0, 0.0), None);
683
684        assert_eq!(absorbed_energy_from_dose(5.0, 4.0), Some(20.0));
685        assert_eq!(absorbed_energy_from_dose(-5.0, 4.0), None);
686
687        assert_eq!(dose_rate(10.0, 2.0), Some(5.0));
688        assert_eq!(dose_rate(10.0, 0.0), None);
689
690        assert_eq!(accumulated_dose(5.0, 2.0), Some(10.0));
691        assert_eq!(accumulated_dose(5.0, -1.0), None);
692    }
693
694    #[test]
695    fn equivalent_and_effective_dose_helpers_cover_common_cases() {
696        assert_eq!(equivalent_dose(2.0, 20.0), Some(40.0));
697        assert_eq!(equivalent_dose(-2.0, 20.0), None);
698
699        assert_some_approx(effective_dose(10.0, 0.12), 1.2);
700        assert_eq!(effective_dose(10.0, -0.12), None);
701
702        assert_eq!(total_effective_dose(&[1.0, 2.0, 3.0]), Some(6.0));
703        assert_eq!(total_effective_dose(&[]), Some(0.0));
704        assert_eq!(total_effective_dose(&[1.0, -2.0]), None);
705    }
706
707    #[test]
708    fn attenuation_helpers_cover_common_shielding_cases() {
709        assert_some_approx(
710            attenuated_intensity(100.0, core::f64::consts::LN_2, 1.0),
711            50.0,
712        );
713        assert_eq!(attenuated_intensity(100.0, -1.0, 1.0), None);
714
715        assert_some_approx(transmitted_fraction(core::f64::consts::LN_2, 1.0), 0.5);
716        assert_eq!(transmitted_fraction(-1.0, 1.0), None);
717
718        assert_some_approx(
719            required_shield_thickness(core::f64::consts::LN_2, 100.0, 50.0),
720            1.0,
721        );
722        assert_eq!(
723            required_shield_thickness(core::f64::consts::LN_2, 100.0, 200.0),
724            None,
725        );
726
727        assert_some_approx(half_value_layer(core::f64::consts::LN_2), 1.0);
728        assert_eq!(half_value_layer(0.0), None);
729
730        assert_some_approx(tenth_value_layer(core::f64::consts::LN_10), 1.0);
731        assert_eq!(tenth_value_layer(0.0), None);
732    }
733
734    #[test]
735    fn attenuation_conversion_helpers_cover_mass_and_linear_forms() {
736        assert_eq!(
737            linear_attenuation_from_mass_attenuation(2.0, 3.0),
738            Some(6.0)
739        );
740        assert_eq!(linear_attenuation_from_mass_attenuation(-2.0, 3.0), None);
741
742        assert_eq!(
743            mass_attenuation_from_linear_attenuation(6.0, 3.0),
744            Some(2.0)
745        );
746        assert_eq!(mass_attenuation_from_linear_attenuation(6.0, 0.0), None);
747    }
748
749    #[test]
750    fn radiation_kind_helpers_cover_simple_classification() {
751        assert!(is_ionizing(RadiationKind::Alpha));
752        assert!(is_photon_radiation(RadiationKind::Gamma));
753        assert!(!is_photon_radiation(RadiationKind::Alpha));
754        assert!(is_particle_radiation(RadiationKind::Alpha));
755        assert!(!is_particle_radiation(RadiationKind::Photon));
756
757        assert_eq!(
758            default_radiation_weighting_factor(RadiationKind::Gamma),
759            Some(1.0)
760        );
761        assert_eq!(
762            default_radiation_weighting_factor(RadiationKind::Alpha),
763            Some(20.0)
764        );
765        assert_eq!(
766            default_radiation_weighting_factor(RadiationKind::Neutron),
767            None
768        );
769    }
770
771    #[test]
772    fn simple_types_delegate_to_public_helpers() {
773        let Some(beam) = RadiationBeam::new(10.0, 2.0) else {
774            panic!("expected valid beam");
775        };
776        assert_eq!(beam.intensity(), Some(5.0));
777        assert_eq!(beam.photon_flux(2.0), Some(5.0));
778        assert_eq!(RadiationBeam::new(10.0, 0.0), None);
779
780        let Some(shield) = Shield::new(core::f64::consts::LN_2, 1.0) else {
781            panic!("expected valid shield");
782        };
783        assert_some_approx(shield.transmitted_fraction(), 0.5);
784        assert_some_approx(shield.attenuated_intensity(100.0), 50.0);
785        assert_eq!(Shield::new(-1.0, 1.0), None);
786
787        let Some(dose) = Dose::new(2.0) else {
788            panic!("expected valid dose");
789        };
790        assert_eq!(dose.equivalent(20.0), Some(40.0));
791        assert_eq!(
792            Dose::new(10.0).and_then(|value| value.rate_over(2.0)),
793            Some(5.0)
794        );
795        assert_eq!(Dose::new(-1.0), None);
796    }
797
798    #[test]
799    fn local_constants_match_expected_values() {
800        assert_eq!(SPEED_OF_LIGHT, 299_792_458.0);
801        assert_eq!(PLANCK_CONSTANT, 6.626_070_15e-34);
802        assert_eq!(ELEMENTARY_CHARGE, 1.602_176_634e-19);
803        assert_eq!(JOULES_PER_MEV, 1.602_176_634e-13);
804    }
805
806    #[test]
807    fn non_finite_inputs_return_none() {
808        assert_eq!(intensity(f64::NAN, 2.0), None);
809        assert_eq!(photon_flux_from_power(1.0, f64::INFINITY), None);
810        assert_eq!(required_shield_thickness(1.0, f64::INFINITY, 1.0), None);
811    }
812}