Skip to main content

use_optics/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4/// Speed of light in meters per second.
5pub const SPEED_OF_LIGHT_MPS: f64 = 299_792_458.0;
6
7/// Lower bound of the conventional visible spectrum in nanometers.
8pub const VISIBLE_MIN_NM: f64 = 380.0;
9
10/// Upper bound of the conventional visible spectrum in nanometers.
11pub const VISIBLE_MAX_NM: f64 = 780.0;
12
13/// Coarse spectral bands used by the scaffold API.
14#[derive(Clone, Copy, Debug, Eq, PartialEq)]
15pub enum SpectralBand {
16    Ultraviolet,
17    Visible,
18    Infrared,
19}
20
21impl SpectralBand {
22    /// Returns a lowercase label for the spectral band.
23    #[must_use]
24    pub const fn as_str(self) -> &'static str {
25        match self {
26            Self::Ultraviolet => "ultraviolet",
27            Self::Visible => "visible",
28            Self::Infrared => "infrared",
29        }
30    }
31}
32
33/// A simple wavelength-plus-intensity sample.
34#[derive(Clone, Copy, Debug, PartialEq)]
35pub struct SpectralSample {
36    pub wavelength_nm: f64,
37    pub intensity: f64,
38}
39
40impl SpectralSample {
41    /// Creates a new spectral sample.
42    #[must_use]
43    pub const fn new(wavelength_nm: f64, intensity: f64) -> Self {
44        Self {
45            wavelength_nm,
46            intensity,
47        }
48    }
49
50    /// Returns the coarse spectral band for the sample wavelength.
51    #[must_use]
52    pub fn band(self) -> Option<SpectralBand> {
53        classify_wavelength_nm(self.wavelength_nm)
54    }
55}
56
57/// Classifies a wavelength in nanometers into a coarse spectral band.
58#[must_use]
59pub fn classify_wavelength_nm(wavelength_nm: f64) -> Option<SpectralBand> {
60    if !wavelength_nm.is_finite() || wavelength_nm <= 0.0 {
61        return None;
62    }
63
64    if wavelength_nm < VISIBLE_MIN_NM {
65        Some(SpectralBand::Ultraviolet)
66    } else if wavelength_nm <= VISIBLE_MAX_NM {
67        Some(SpectralBand::Visible)
68    } else {
69        Some(SpectralBand::Infrared)
70    }
71}
72
73/// Returns whether the wavelength falls inside the conventional visible range.
74#[must_use]
75pub fn is_visible_wavelength_nm(wavelength_nm: f64) -> bool {
76    matches!(
77        classify_wavelength_nm(wavelength_nm),
78        Some(SpectralBand::Visible)
79    )
80}
81
82/// Clamps a valid wavelength into the visible range.
83#[must_use]
84pub fn clamp_visible_wavelength_nm(wavelength_nm: f64) -> Option<f64> {
85    if !wavelength_nm.is_finite() || wavelength_nm <= 0.0 {
86        return None;
87    }
88
89    Some(wavelength_nm.clamp(VISIBLE_MIN_NM, VISIBLE_MAX_NM))
90}
91
92/// Converts a wavelength in nanometers to frequency in hertz.
93#[must_use]
94pub fn wavelength_to_frequency_hz(wavelength_nm: f64) -> Option<f64> {
95    if !wavelength_nm.is_finite() || wavelength_nm <= 0.0 {
96        return None;
97    }
98
99    Some(SPEED_OF_LIGHT_MPS / (wavelength_nm * 1.0e-9))
100}
101
102/// Common optics primitives.
103pub mod prelude {
104    pub use super::{
105        SPEED_OF_LIGHT_MPS, SpectralBand, SpectralSample, VISIBLE_MAX_NM, VISIBLE_MIN_NM,
106        clamp_visible_wavelength_nm, classify_wavelength_nm, is_visible_wavelength_nm,
107        wavelength_to_frequency_hz,
108    };
109}
110
111#[cfg(test)]
112mod tests {
113    use super::{
114        SpectralBand, SpectralSample, VISIBLE_MAX_NM, VISIBLE_MIN_NM, clamp_visible_wavelength_nm,
115        classify_wavelength_nm, is_visible_wavelength_nm, wavelength_to_frequency_hz,
116    };
117
118    #[test]
119    fn classifies_common_wavelength_ranges() {
120        assert_eq!(
121            classify_wavelength_nm(350.0),
122            Some(SpectralBand::Ultraviolet)
123        );
124        assert_eq!(classify_wavelength_nm(532.0), Some(SpectralBand::Visible));
125        assert_eq!(classify_wavelength_nm(900.0), Some(SpectralBand::Infrared));
126        assert_eq!(classify_wavelength_nm(0.0), None);
127    }
128
129    #[test]
130    fn detects_visible_wavelengths() {
131        assert!(is_visible_wavelength_nm(VISIBLE_MIN_NM));
132        assert!(is_visible_wavelength_nm(VISIBLE_MAX_NM));
133        assert!(!is_visible_wavelength_nm(300.0));
134    }
135
136    #[test]
137    fn clamps_into_visible_range() {
138        assert_eq!(clamp_visible_wavelength_nm(300.0), Some(VISIBLE_MIN_NM));
139        assert_eq!(clamp_visible_wavelength_nm(500.0), Some(500.0));
140        assert_eq!(clamp_visible_wavelength_nm(900.0), Some(VISIBLE_MAX_NM));
141    }
142
143    #[test]
144    fn converts_wavelength_to_frequency() {
145        let frequency = wavelength_to_frequency_hz(500.0).expect("500 nm should be valid");
146
147        assert!((frequency - 5.995_849_16e14).abs() < 1.0e7);
148    }
149
150    #[test]
151    fn sample_reports_its_band() {
152        let sample = SpectralSample::new(450.0, 0.75);
153
154        assert_eq!(sample.band(), Some(SpectralBand::Visible));
155    }
156}