#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
use core::f64::consts::PI;
pub mod prelude;
pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
pub const PLANCK_CONSTANT: f64 = 6.626_070_15e-34;
pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
pub const JOULES_PER_MEV: f64 = 1.602_176_634e-13;
fn is_non_negative_finite(value: f64) -> bool {
value.is_finite() && value >= 0.0
}
fn is_positive_finite(value: f64) -> bool {
value.is_finite() && value > 0.0
}
fn normalize_zero(value: f64) -> f64 {
if value == 0.0 { 0.0 } else { value }
}
fn finite_non_negative(value: f64) -> Option<f64> {
(value.is_finite() && value >= 0.0).then_some(normalize_zero(value))
}
fn finite_unit_interval(value: f64) -> Option<f64> {
(value.is_finite() && (0.0..=1.0).contains(&value)).then_some(normalize_zero(value))
}
fn divide_non_negative(numerator: f64, denominator: f64) -> Option<f64> {
if !is_non_negative_finite(numerator) || !is_positive_finite(denominator) {
return None;
}
finite_non_negative(numerator / denominator)
}
fn multiply_non_negative(left: f64, right: f64) -> Option<f64> {
if !is_non_negative_finite(left) || !is_non_negative_finite(right) {
return None;
}
finite_non_negative(left * right)
}
#[must_use]
pub fn photon_energy_from_frequency(frequency: f64) -> Option<f64> {
if !is_non_negative_finite(frequency) {
return None;
}
finite_non_negative(PLANCK_CONSTANT * frequency)
}
#[must_use]
pub fn photon_energy_from_wavelength(wavelength: f64) -> Option<f64> {
if !is_positive_finite(wavelength) {
return None;
}
finite_non_negative((PLANCK_CONSTANT * SPEED_OF_LIGHT) / wavelength)
}
#[must_use]
pub fn photon_flux_from_power(power: f64, photon_energy: f64) -> Option<f64> {
divide_non_negative(power, photon_energy)
}
#[must_use]
pub fn photon_flux_density(photon_flux: f64, area: f64) -> Option<f64> {
divide_non_negative(photon_flux, area)
}
#[must_use]
pub fn intensity(power: f64, area: f64) -> Option<f64> {
divide_non_negative(power, area)
}
#[must_use]
pub fn isotropic_intensity(power: f64, distance: f64) -> Option<f64> {
if !is_non_negative_finite(power) || !is_positive_finite(distance) {
return None;
}
intensity(power, 4.0 * PI * distance * distance)
}
#[must_use]
pub fn inverse_square_intensity(
reference_intensity: f64,
reference_distance: f64,
target_distance: f64,
) -> Option<f64> {
if !is_non_negative_finite(reference_intensity)
|| !is_positive_finite(reference_distance)
|| !is_positive_finite(target_distance)
{
return None;
}
let ratio = reference_distance / target_distance;
finite_non_negative(reference_intensity * ratio * ratio)
}
#[must_use]
pub fn fluence(particle_count: f64, area: f64) -> Option<f64> {
divide_non_negative(particle_count, area)
}
#[must_use]
pub fn energy_fluence(energy: f64, area: f64) -> Option<f64> {
divide_non_negative(energy, area)
}
#[must_use]
pub fn fluence_rate(fluence: f64, time: f64) -> Option<f64> {
divide_non_negative(fluence, time)
}
#[must_use]
pub fn absorbed_dose(energy_absorbed: f64, mass: f64) -> Option<f64> {
divide_non_negative(energy_absorbed, mass)
}
#[must_use]
pub fn absorbed_energy_from_dose(dose: f64, mass: f64) -> Option<f64> {
multiply_non_negative(dose, mass)
}
#[must_use]
pub fn dose_rate(dose: f64, time: f64) -> Option<f64> {
divide_non_negative(dose, time)
}
#[must_use]
pub fn accumulated_dose(dose_rate: f64, time: f64) -> Option<f64> {
multiply_non_negative(dose_rate, time)
}
#[must_use]
pub fn equivalent_dose(absorbed_dose: f64, radiation_weighting_factor: f64) -> Option<f64> {
multiply_non_negative(absorbed_dose, radiation_weighting_factor)
}
#[must_use]
pub fn effective_dose(equivalent_dose: f64, tissue_weighting_factor: f64) -> Option<f64> {
multiply_non_negative(equivalent_dose, tissue_weighting_factor)
}
#[must_use]
pub fn total_effective_dose(weighted_equivalent_doses: &[f64]) -> Option<f64> {
weighted_equivalent_doses
.iter()
.try_fold(0.0, |sum, value| {
if !is_non_negative_finite(*value) {
return None;
}
finite_non_negative(sum + *value)
})
}
#[must_use]
pub fn attenuated_intensity(
initial_intensity: f64,
linear_attenuation_coefficient: f64,
thickness: f64,
) -> Option<f64> {
if !is_non_negative_finite(initial_intensity)
|| !is_non_negative_finite(linear_attenuation_coefficient)
|| !is_non_negative_finite(thickness)
{
return None;
}
finite_non_negative(
initial_intensity * transmitted_fraction(linear_attenuation_coefficient, thickness)?,
)
}
#[must_use]
pub fn transmitted_fraction(linear_attenuation_coefficient: f64, thickness: f64) -> Option<f64> {
if !is_non_negative_finite(linear_attenuation_coefficient) || !is_non_negative_finite(thickness)
{
return None;
}
finite_unit_interval((-(linear_attenuation_coefficient * thickness)).exp())
}
#[must_use]
pub fn required_shield_thickness(
linear_attenuation_coefficient: f64,
initial_intensity: f64,
target_intensity: f64,
) -> Option<f64> {
if !is_positive_finite(linear_attenuation_coefficient)
|| !is_positive_finite(initial_intensity)
|| !is_positive_finite(target_intensity)
|| target_intensity > initial_intensity
{
return None;
}
finite_non_negative(
(initial_intensity / target_intensity).ln() / linear_attenuation_coefficient,
)
}
#[must_use]
pub fn half_value_layer(linear_attenuation_coefficient: f64) -> Option<f64> {
if !is_positive_finite(linear_attenuation_coefficient) {
return None;
}
finite_non_negative(core::f64::consts::LN_2 / linear_attenuation_coefficient)
}
#[must_use]
pub fn tenth_value_layer(linear_attenuation_coefficient: f64) -> Option<f64> {
if !is_positive_finite(linear_attenuation_coefficient) {
return None;
}
finite_non_negative(core::f64::consts::LN_10 / linear_attenuation_coefficient)
}
#[must_use]
pub fn linear_attenuation_from_mass_attenuation(
mass_attenuation_coefficient: f64,
density: f64,
) -> Option<f64> {
multiply_non_negative(mass_attenuation_coefficient, density)
}
#[must_use]
pub fn mass_attenuation_from_linear_attenuation(
linear_attenuation_coefficient: f64,
density: f64,
) -> Option<f64> {
divide_non_negative(linear_attenuation_coefficient, density)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RadiationKind {
Alpha,
BetaMinus,
BetaPlus,
Gamma,
XRay,
Neutron,
Proton,
Electron,
Photon,
}
#[must_use]
pub const fn is_ionizing(_kind: RadiationKind) -> bool {
true
}
#[must_use]
pub const fn is_photon_radiation(kind: RadiationKind) -> bool {
matches!(
kind,
RadiationKind::Gamma | RadiationKind::XRay | RadiationKind::Photon
)
}
#[must_use]
pub const fn is_particle_radiation(kind: RadiationKind) -> bool {
!is_photon_radiation(kind)
}
#[must_use]
pub const fn default_radiation_weighting_factor(kind: RadiationKind) -> Option<f64> {
match kind {
RadiationKind::Gamma
| RadiationKind::XRay
| RadiationKind::BetaMinus
| RadiationKind::BetaPlus
| RadiationKind::Electron
| RadiationKind::Photon => Some(1.0),
RadiationKind::Proton => Some(2.0),
RadiationKind::Alpha => Some(20.0),
RadiationKind::Neutron => None,
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct RadiationBeam {
pub power: f64,
pub area: f64,
}
impl RadiationBeam {
#[must_use]
pub fn new(power: f64, area: f64) -> Option<Self> {
if !is_non_negative_finite(power) || !is_positive_finite(area) {
return None;
}
Some(Self { power, area })
}
#[must_use]
pub fn intensity(&self) -> Option<f64> {
intensity(self.power, self.area)
}
#[must_use]
pub fn photon_flux(&self, photon_energy: f64) -> Option<f64> {
photon_flux_from_power(self.power, photon_energy)
}
#[must_use]
pub fn photon_flux_density(&self, photon_energy: f64) -> Option<f64> {
photon_flux_density(self.photon_flux(photon_energy)?, self.area)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Shield {
pub linear_attenuation_coefficient: f64,
pub thickness: f64,
}
impl Shield {
#[must_use]
pub fn new(linear_attenuation_coefficient: f64, thickness: f64) -> Option<Self> {
if !is_non_negative_finite(linear_attenuation_coefficient)
|| !is_non_negative_finite(thickness)
{
return None;
}
Some(Self {
linear_attenuation_coefficient,
thickness,
})
}
#[must_use]
pub fn transmitted_fraction(&self) -> Option<f64> {
transmitted_fraction(self.linear_attenuation_coefficient, self.thickness)
}
#[must_use]
pub fn attenuated_intensity(&self, initial_intensity: f64) -> Option<f64> {
attenuated_intensity(
initial_intensity,
self.linear_attenuation_coefficient,
self.thickness,
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Dose {
pub absorbed_dose: f64,
}
impl Dose {
#[must_use]
pub fn new(absorbed_dose: f64) -> Option<Self> {
if !is_non_negative_finite(absorbed_dose) {
return None;
}
Some(Self { absorbed_dose })
}
#[must_use]
pub fn equivalent(self, radiation_weighting_factor: f64) -> Option<f64> {
equivalent_dose(self.absorbed_dose, radiation_weighting_factor)
}
#[must_use]
pub fn rate_over(self, time: f64) -> Option<f64> {
dose_rate(self.absorbed_dose, time)
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::{
Dose, ELEMENTARY_CHARGE, JOULES_PER_MEV, PLANCK_CONSTANT, RadiationBeam, RadiationKind,
SPEED_OF_LIGHT, Shield, absorbed_dose, absorbed_energy_from_dose, accumulated_dose,
attenuated_intensity, default_radiation_weighting_factor, dose_rate, effective_dose,
energy_fluence, equivalent_dose, fluence, fluence_rate, half_value_layer, intensity,
inverse_square_intensity, is_ionizing, is_particle_radiation, is_photon_radiation,
isotropic_intensity, linear_attenuation_from_mass_attenuation,
mass_attenuation_from_linear_attenuation, photon_energy_from_frequency,
photon_energy_from_wavelength, photon_flux_density, photon_flux_from_power,
required_shield_thickness, tenth_value_layer, total_effective_dose, transmitted_fraction,
};
fn assert_approx_eq(left: f64, right: f64) {
let tolerance = 1.0e-12 * left.abs().max(right.abs()).max(1.0);
assert!(
(left - right).abs() <= tolerance,
"left={left}, right={right}, tolerance={tolerance}"
);
}
fn assert_some_approx(actual: Option<f64>, expected: f64) {
let Some(actual) = actual else {
panic!("expected Some({expected})");
};
assert_approx_eq(actual, expected);
}
#[test]
fn photon_energy_helpers_validate_inputs() {
assert_eq!(photon_energy_from_frequency(1.0), Some(PLANCK_CONSTANT));
assert_eq!(photon_energy_from_frequency(-1.0), None);
assert_eq!(photon_energy_from_frequency(f64::NAN), None);
assert_some_approx(
photon_energy_from_wavelength(SPEED_OF_LIGHT),
PLANCK_CONSTANT,
);
assert_eq!(photon_energy_from_wavelength(0.0), None);
assert_eq!(photon_energy_from_wavelength(f64::INFINITY), None);
}
#[test]
fn photon_flux_helpers_cover_common_cases() {
assert_eq!(photon_flux_from_power(10.0, 2.0), Some(5.0));
assert_eq!(photon_flux_from_power(10.0, 0.0), None);
assert_eq!(photon_flux_from_power(-1.0, 2.0), None);
assert_eq!(photon_flux_density(10.0, 2.0), Some(5.0));
assert_eq!(photon_flux_density(10.0, 0.0), None);
}
#[test]
fn intensity_helpers_cover_planar_and_inverse_square_cases() {
assert_eq!(intensity(10.0, 2.0), Some(5.0));
assert_eq!(intensity(-10.0, 2.0), None);
assert_eq!(intensity(10.0, 0.0), None);
assert_some_approx(isotropic_intensity(4.0 * core::f64::consts::PI, 1.0), 1.0);
assert_eq!(isotropic_intensity(1.0, 0.0), None);
assert_eq!(inverse_square_intensity(100.0, 1.0, 2.0), Some(25.0));
assert_eq!(inverse_square_intensity(100.0, 1.0, 0.0), None);
}
#[test]
fn fluence_helpers_validate_inputs() {
assert_eq!(fluence(100.0, 2.0), Some(50.0));
assert_eq!(fluence(-100.0, 2.0), None);
assert_eq!(energy_fluence(100.0, 2.0), Some(50.0));
assert_eq!(fluence_rate(50.0, 10.0), Some(5.0));
assert_eq!(fluence_rate(50.0, 0.0), None);
}
#[test]
fn absorbed_dose_helpers_cover_forward_and_inverse_relations() {
assert_eq!(absorbed_dose(20.0, 4.0), Some(5.0));
assert_eq!(absorbed_dose(20.0, 0.0), None);
assert_eq!(absorbed_energy_from_dose(5.0, 4.0), Some(20.0));
assert_eq!(absorbed_energy_from_dose(-5.0, 4.0), None);
assert_eq!(dose_rate(10.0, 2.0), Some(5.0));
assert_eq!(dose_rate(10.0, 0.0), None);
assert_eq!(accumulated_dose(5.0, 2.0), Some(10.0));
assert_eq!(accumulated_dose(5.0, -1.0), None);
}
#[test]
fn equivalent_and_effective_dose_helpers_cover_common_cases() {
assert_eq!(equivalent_dose(2.0, 20.0), Some(40.0));
assert_eq!(equivalent_dose(-2.0, 20.0), None);
assert_some_approx(effective_dose(10.0, 0.12), 1.2);
assert_eq!(effective_dose(10.0, -0.12), None);
assert_eq!(total_effective_dose(&[1.0, 2.0, 3.0]), Some(6.0));
assert_eq!(total_effective_dose(&[]), Some(0.0));
assert_eq!(total_effective_dose(&[1.0, -2.0]), None);
}
#[test]
fn attenuation_helpers_cover_common_shielding_cases() {
assert_some_approx(
attenuated_intensity(100.0, core::f64::consts::LN_2, 1.0),
50.0,
);
assert_eq!(attenuated_intensity(100.0, -1.0, 1.0), None);
assert_some_approx(transmitted_fraction(core::f64::consts::LN_2, 1.0), 0.5);
assert_eq!(transmitted_fraction(-1.0, 1.0), None);
assert_some_approx(
required_shield_thickness(core::f64::consts::LN_2, 100.0, 50.0),
1.0,
);
assert_eq!(
required_shield_thickness(core::f64::consts::LN_2, 100.0, 200.0),
None,
);
assert_some_approx(half_value_layer(core::f64::consts::LN_2), 1.0);
assert_eq!(half_value_layer(0.0), None);
assert_some_approx(tenth_value_layer(core::f64::consts::LN_10), 1.0);
assert_eq!(tenth_value_layer(0.0), None);
}
#[test]
fn attenuation_conversion_helpers_cover_mass_and_linear_forms() {
assert_eq!(
linear_attenuation_from_mass_attenuation(2.0, 3.0),
Some(6.0)
);
assert_eq!(linear_attenuation_from_mass_attenuation(-2.0, 3.0), None);
assert_eq!(
mass_attenuation_from_linear_attenuation(6.0, 3.0),
Some(2.0)
);
assert_eq!(mass_attenuation_from_linear_attenuation(6.0, 0.0), None);
}
#[test]
fn radiation_kind_helpers_cover_simple_classification() {
assert!(is_ionizing(RadiationKind::Alpha));
assert!(is_photon_radiation(RadiationKind::Gamma));
assert!(!is_photon_radiation(RadiationKind::Alpha));
assert!(is_particle_radiation(RadiationKind::Alpha));
assert!(!is_particle_radiation(RadiationKind::Photon));
assert_eq!(
default_radiation_weighting_factor(RadiationKind::Gamma),
Some(1.0)
);
assert_eq!(
default_radiation_weighting_factor(RadiationKind::Alpha),
Some(20.0)
);
assert_eq!(
default_radiation_weighting_factor(RadiationKind::Neutron),
None
);
}
#[test]
fn simple_types_delegate_to_public_helpers() {
let Some(beam) = RadiationBeam::new(10.0, 2.0) else {
panic!("expected valid beam");
};
assert_eq!(beam.intensity(), Some(5.0));
assert_eq!(beam.photon_flux(2.0), Some(5.0));
assert_eq!(RadiationBeam::new(10.0, 0.0), None);
let Some(shield) = Shield::new(core::f64::consts::LN_2, 1.0) else {
panic!("expected valid shield");
};
assert_some_approx(shield.transmitted_fraction(), 0.5);
assert_some_approx(shield.attenuated_intensity(100.0), 50.0);
assert_eq!(Shield::new(-1.0, 1.0), None);
let Some(dose) = Dose::new(2.0) else {
panic!("expected valid dose");
};
assert_eq!(dose.equivalent(20.0), Some(40.0));
assert_eq!(
Dose::new(10.0).and_then(|value| value.rate_over(2.0)),
Some(5.0)
);
assert_eq!(Dose::new(-1.0), None);
}
#[test]
fn local_constants_match_expected_values() {
assert_eq!(SPEED_OF_LIGHT, 299_792_458.0);
assert_eq!(PLANCK_CONSTANT, 6.626_070_15e-34);
assert_eq!(ELEMENTARY_CHARGE, 1.602_176_634e-19);
assert_eq!(JOULES_PER_MEV, 1.602_176_634e-13);
}
#[test]
fn non_finite_inputs_return_none() {
assert_eq!(intensity(f64::NAN, 2.0), None);
assert_eq!(photon_flux_from_power(1.0, f64::INFINITY), None);
assert_eq!(required_shield_thickness(1.0, f64::INFINITY, 1.0), None);
}
}