#![forbid(unsafe_code)]
#![doc = include_str!("../README.md")]
pub mod prelude;
pub const PLANCK_CONSTANT: f64 = 6.626_070_15e-34;
pub const REDUCED_PLANCK_CONSTANT: f64 = 1.054_571_817e-34;
pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
pub const ELECTRON_MASS: f64 = 9.109_383_701_5e-31;
pub const BOHR_RADIUS: f64 = 5.291_772_109_03e-11;
pub const RYDBERG_ENERGY_EV: f64 = 13.605_693_122_994;
fn is_nonnegative_finite(value: f64) -> bool {
value.is_finite() && value >= 0.0
}
fn is_positive_finite(value: f64) -> bool {
value.is_finite() && value > 0.0
}
fn finite_result(value: f64) -> Option<f64> {
value.is_finite().then_some(value)
}
fn principal_squared(principal: u32) -> Option<f64> {
if principal == 0 {
return None;
}
let principal = f64::from(principal);
finite_result(principal * principal)
}
fn momentum_magnitude_from_mass_velocity(mass: f64, velocity: f64) -> Option<f64> {
if !is_positive_finite(mass) || !velocity.is_finite() {
return None;
}
let speed = velocity.abs();
if speed == 0.0 {
return None;
}
let momentum = mass * speed;
(momentum.is_finite() && momentum > 0.0).then_some(momentum)
}
#[must_use]
pub fn photon_energy_from_frequency(frequency: f64) -> Option<f64> {
if !is_nonnegative_finite(frequency) {
return None;
}
finite_result(PLANCK_CONSTANT * frequency)
}
#[must_use]
pub fn photon_energy_from_wavelength(wavelength: f64) -> Option<f64> {
if !is_positive_finite(wavelength) {
return None;
}
finite_result((PLANCK_CONSTANT * SPEED_OF_LIGHT) / wavelength)
}
#[must_use]
pub fn frequency_from_photon_energy(energy: f64) -> Option<f64> {
if !is_nonnegative_finite(energy) {
return None;
}
finite_result(energy / PLANCK_CONSTANT)
}
#[must_use]
pub fn wavelength_from_photon_energy(energy: f64) -> Option<f64> {
if !is_positive_finite(energy) {
return None;
}
finite_result((PLANCK_CONSTANT * SPEED_OF_LIGHT) / energy)
}
#[must_use]
pub fn photon_momentum_from_wavelength(wavelength: f64) -> Option<f64> {
if !is_positive_finite(wavelength) {
return None;
}
finite_result(PLANCK_CONSTANT / wavelength)
}
#[must_use]
pub fn photon_momentum_from_energy(energy: f64) -> Option<f64> {
if !is_nonnegative_finite(energy) {
return None;
}
finite_result(energy / SPEED_OF_LIGHT)
}
#[must_use]
pub fn joules_to_electron_volts(joules: f64) -> Option<f64> {
if !is_nonnegative_finite(joules) {
return None;
}
finite_result(joules / ELEMENTARY_CHARGE)
}
#[must_use]
pub fn electron_volts_to_joules(electron_volts: f64) -> Option<f64> {
if !is_nonnegative_finite(electron_volts) {
return None;
}
finite_result(electron_volts * ELEMENTARY_CHARGE)
}
#[must_use]
pub fn de_broglie_wavelength(momentum: f64) -> Option<f64> {
if !is_positive_finite(momentum) {
return None;
}
finite_result(PLANCK_CONSTANT / momentum)
}
#[must_use]
pub fn de_broglie_wavelength_from_mass_velocity(mass: f64, velocity: f64) -> Option<f64> {
de_broglie_wavelength(momentum_magnitude_from_mass_velocity(mass, velocity)?)
}
#[must_use]
pub fn momentum_from_de_broglie_wavelength(wavelength: f64) -> Option<f64> {
photon_momentum_from_wavelength(wavelength)
}
#[must_use]
pub fn angular_frequency_from_energy(energy: f64) -> Option<f64> {
if !is_nonnegative_finite(energy) {
return None;
}
finite_result(energy / REDUCED_PLANCK_CONSTANT)
}
#[must_use]
pub fn energy_from_angular_frequency(angular_frequency: f64) -> Option<f64> {
if !is_nonnegative_finite(angular_frequency) {
return None;
}
finite_result(REDUCED_PLANCK_CONSTANT * angular_frequency)
}
fn minimum_conjugate_uncertainty(uncertainty: f64) -> Option<f64> {
if !is_positive_finite(uncertainty) {
return None;
}
finite_result(REDUCED_PLANCK_CONSTANT / (2.0 * uncertainty))
}
#[must_use]
pub fn minimum_position_uncertainty(momentum_uncertainty: f64) -> Option<f64> {
minimum_conjugate_uncertainty(momentum_uncertainty)
}
#[must_use]
pub fn minimum_momentum_uncertainty(position_uncertainty: f64) -> Option<f64> {
minimum_conjugate_uncertainty(position_uncertainty)
}
#[must_use]
pub fn minimum_energy_uncertainty(time_uncertainty: f64) -> Option<f64> {
minimum_conjugate_uncertainty(time_uncertainty)
}
#[must_use]
pub fn minimum_time_uncertainty(energy_uncertainty: f64) -> Option<f64> {
minimum_conjugate_uncertainty(energy_uncertainty)
}
#[must_use]
pub fn bohr_orbit_radius(n: u32) -> Option<f64> {
finite_result(BOHR_RADIUS * principal_squared(n)?)
}
#[must_use]
pub fn hydrogen_energy_level_ev(n: u32) -> Option<f64> {
finite_result(-RYDBERG_ENERGY_EV / principal_squared(n)?)
}
#[must_use]
pub fn hydrogen_transition_energy_ev(initial_n: u32, final_n: u32) -> Option<f64> {
if initial_n == 0 || final_n == 0 {
return None;
}
if initial_n == final_n {
return Some(0.0);
}
let initial = hydrogen_energy_level_ev(initial_n)?;
let final_ = hydrogen_energy_level_ev(final_n)?;
finite_result((final_ - initial).abs())
}
#[must_use]
pub fn hydrogen_transition_wavelength(initial_n: u32, final_n: u32) -> Option<f64> {
let transition_energy_ev = hydrogen_transition_energy_ev(initial_n, final_n)?;
if transition_energy_ev == 0.0 {
return None;
}
wavelength_from_photon_energy(electron_volts_to_joules(transition_energy_ev)?)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct QuantumNumbers {
pub principal: u32,
pub azimuthal: u32,
pub magnetic: i32,
pub spin_twice: i8,
}
#[must_use]
pub const fn is_valid_principal_quantum_number(n: u32) -> bool {
n >= 1
}
#[must_use]
pub const fn is_valid_azimuthal_quantum_number(n: u32, l: u32) -> bool {
is_valid_principal_quantum_number(n) && l < n
}
#[must_use]
pub fn is_valid_magnetic_quantum_number(l: u32, m_l: i32) -> bool {
let l = i64::from(l);
let magnetic = i64::from(m_l);
(-l..=l).contains(&magnetic)
}
#[must_use]
pub const fn is_valid_spin_twice(spin_twice: i8) -> bool {
matches!(spin_twice, -1 | 1)
}
#[must_use]
pub fn is_valid_quantum_numbers(
principal: u32,
azimuthal: u32,
magnetic: i32,
spin_twice: i8,
) -> bool {
is_valid_azimuthal_quantum_number(principal, azimuthal)
&& is_valid_magnetic_quantum_number(azimuthal, magnetic)
&& is_valid_spin_twice(spin_twice)
}
impl QuantumNumbers {
#[must_use]
pub fn new(principal: u32, azimuthal: u32, magnetic: i32, spin_twice: i8) -> Option<Self> {
is_valid_quantum_numbers(principal, azimuthal, magnetic, spin_twice).then_some(Self {
principal,
azimuthal,
magnetic,
spin_twice,
})
}
#[must_use]
pub fn spin_projection(&self) -> f64 {
f64::from(self.spin_twice) / 2.0
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Photon {
pub energy_joules: f64,
}
impl Photon {
#[must_use]
pub fn from_energy_joules(energy_joules: f64) -> Option<Self> {
is_nonnegative_finite(energy_joules).then_some(Self { energy_joules })
}
#[must_use]
pub fn from_frequency(frequency: f64) -> Option<Self> {
Self::from_energy_joules(photon_energy_from_frequency(frequency)?)
}
#[must_use]
pub fn from_wavelength(wavelength: f64) -> Option<Self> {
Self::from_energy_joules(photon_energy_from_wavelength(wavelength)?)
}
#[must_use]
pub const fn energy_joules(&self) -> f64 {
self.energy_joules
}
#[must_use]
pub fn energy_ev(&self) -> Option<f64> {
joules_to_electron_volts(self.energy_joules)
}
#[must_use]
pub fn frequency(&self) -> Option<f64> {
frequency_from_photon_energy(self.energy_joules)
}
#[must_use]
pub fn wavelength(&self) -> Option<f64> {
wavelength_from_photon_energy(self.energy_joules)
}
#[must_use]
pub fn momentum(&self) -> Option<f64> {
photon_momentum_from_energy(self.energy_joules)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct MatterWave {
pub momentum: f64,
}
impl MatterWave {
#[must_use]
pub fn from_momentum(momentum: f64) -> Option<Self> {
is_positive_finite(momentum).then_some(Self { momentum })
}
#[must_use]
pub fn from_mass_velocity(mass: f64, velocity: f64) -> Option<Self> {
Self::from_momentum(momentum_magnitude_from_mass_velocity(mass, velocity)?)
}
#[must_use]
pub fn wavelength(&self) -> Option<f64> {
de_broglie_wavelength(self.momentum)
}
}
#[cfg(test)]
#[allow(clippy::float_cmp)]
mod tests {
use super::*;
fn approx_eq(left: f64, right: f64) -> bool {
let scale = left.abs().max(right.abs()).max(1.0);
(left - right).abs() <= 1.0e-12 * scale
}
fn assert_approx_eq(left: f64, right: f64) {
assert!(
approx_eq(left, right),
"left={left:e} right={right:e} delta={:e}",
(left - right).abs()
);
}
fn assert_some_approx_eq(value: Option<f64>, expected: f64) {
match value {
Some(actual) => assert_approx_eq(actual, expected),
None => panic!("expected Some({expected:e})"),
}
}
#[test]
fn photon_energy_helpers_cover_frequency_and_wavelength() {
assert_eq!(photon_energy_from_frequency(1.0), Some(PLANCK_CONSTANT));
assert_eq!(photon_energy_from_frequency(-1.0), None);
assert_some_approx_eq(
photon_energy_from_wavelength(SPEED_OF_LIGHT),
PLANCK_CONSTANT,
);
assert_eq!(photon_energy_from_wavelength(0.0), None);
}
#[test]
fn photon_frequency_and_wavelength_helpers_invert_energy() {
assert_some_approx_eq(frequency_from_photon_energy(PLANCK_CONSTANT), 1.0);
assert_eq!(frequency_from_photon_energy(-1.0), None);
assert_some_approx_eq(
wavelength_from_photon_energy(PLANCK_CONSTANT),
SPEED_OF_LIGHT,
);
assert_eq!(wavelength_from_photon_energy(0.0), None);
}
#[test]
fn photon_momentum_and_energy_conversion_helpers_work() {
assert_some_approx_eq(photon_momentum_from_wavelength(PLANCK_CONSTANT), 1.0);
assert_some_approx_eq(photon_momentum_from_energy(SPEED_OF_LIGHT), 1.0);
assert_some_approx_eq(joules_to_electron_volts(ELEMENTARY_CHARGE), 1.0);
assert_some_approx_eq(electron_volts_to_joules(1.0), ELEMENTARY_CHARGE);
}
#[test]
fn matter_wave_helpers_cover_momentum_and_mass_velocity() {
assert_some_approx_eq(de_broglie_wavelength(PLANCK_CONSTANT), 1.0);
assert_eq!(de_broglie_wavelength(0.0), None);
assert_some_approx_eq(
de_broglie_wavelength_from_mass_velocity(2.0, 3.0),
PLANCK_CONSTANT / 6.0,
);
assert_eq!(de_broglie_wavelength_from_mass_velocity(2.0, 0.0), None);
assert_eq!(de_broglie_wavelength_from_mass_velocity(0.0, 3.0), None);
assert_some_approx_eq(momentum_from_de_broglie_wavelength(PLANCK_CONSTANT), 1.0);
}
#[test]
fn reduced_planck_and_uncertainty_helpers_work() {
assert_some_approx_eq(angular_frequency_from_energy(REDUCED_PLANCK_CONSTANT), 1.0);
assert_some_approx_eq(energy_from_angular_frequency(1.0), REDUCED_PLANCK_CONSTANT);
assert_some_approx_eq(minimum_position_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
assert_eq!(minimum_position_uncertainty(0.0), None);
assert_some_approx_eq(minimum_momentum_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
assert_eq!(minimum_momentum_uncertainty(0.0), None);
assert_some_approx_eq(minimum_energy_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
assert_eq!(minimum_energy_uncertainty(0.0), None);
assert_some_approx_eq(minimum_time_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
assert_eq!(minimum_time_uncertainty(0.0), None);
}
#[test]
fn bohr_model_helpers_cover_levels_and_transitions() {
assert_some_approx_eq(bohr_orbit_radius(1), BOHR_RADIUS);
assert_some_approx_eq(bohr_orbit_radius(2), 4.0 * BOHR_RADIUS);
assert_eq!(bohr_orbit_radius(0), None);
assert_some_approx_eq(hydrogen_energy_level_ev(1), -RYDBERG_ENERGY_EV);
assert_some_approx_eq(hydrogen_energy_level_ev(2), -RYDBERG_ENERGY_EV / 4.0);
assert_eq!(hydrogen_energy_level_ev(0), None);
assert_some_approx_eq(hydrogen_transition_energy_ev(2, 1), 10.204_269_842_245_5);
assert_eq!(hydrogen_transition_energy_ev(1, 1), Some(0.0));
assert_eq!(hydrogen_transition_energy_ev(0, 1), None);
match hydrogen_transition_wavelength(2, 1) {
Some(wavelength) => assert!(wavelength.is_finite() && wavelength > 0.0),
None => panic!("expected a valid transition wavelength"),
}
assert_eq!(hydrogen_transition_wavelength(1, 1), None);
}
#[test]
fn quantum_number_helpers_validate_expected_ranges() {
assert!(is_valid_principal_quantum_number(1));
assert!(!is_valid_principal_quantum_number(0));
assert!(is_valid_azimuthal_quantum_number(1, 0));
assert!(!is_valid_azimuthal_quantum_number(1, 1));
assert!(is_valid_magnetic_quantum_number(1, -1));
assert!(is_valid_magnetic_quantum_number(1, 0));
assert!(is_valid_magnetic_quantum_number(1, 1));
assert!(!is_valid_magnetic_quantum_number(1, 2));
assert!(is_valid_spin_twice(1));
assert!(is_valid_spin_twice(-1));
assert!(!is_valid_spin_twice(0));
assert!(is_valid_quantum_numbers(2, 1, 0, 1));
assert!(!is_valid_quantum_numbers(2, 2, 0, 1));
match QuantumNumbers::new(2, 1, 0, 1) {
Some(quantum_numbers) => assert_eq!(quantum_numbers.spin_projection(), 0.5),
None => panic!("expected valid quantum numbers"),
}
assert_eq!(QuantumNumbers::new(2, 2, 0, 1), None);
}
#[test]
fn photon_wrapper_delegates_to_public_helpers() {
match Photon::from_frequency(1.0) {
Some(photon) => assert_eq!(photon.energy_joules(), PLANCK_CONSTANT),
None => panic!("expected a valid photon from frequency"),
}
match Photon::from_wavelength(SPEED_OF_LIGHT) {
Some(photon) => assert_some_approx_eq(photon.frequency(), 1.0),
None => panic!("expected a valid photon from wavelength"),
}
match Photon::from_energy_joules(PLANCK_CONSTANT) {
Some(photon) => assert_some_approx_eq(photon.wavelength(), SPEED_OF_LIGHT),
None => panic!("expected a valid photon from energy"),
}
assert_eq!(Photon::from_energy_joules(-1.0), None);
}
#[test]
fn matter_wave_wrapper_delegates_to_public_helpers() {
match MatterWave::from_momentum(PLANCK_CONSTANT) {
Some(wave) => assert_some_approx_eq(wave.wavelength(), 1.0),
None => panic!("expected a valid matter wave from momentum"),
}
match MatterWave::from_mass_velocity(2.0, 3.0) {
Some(wave) => assert_some_approx_eq(wave.wavelength(), PLANCK_CONSTANT / 6.0),
None => panic!("expected a valid matter wave from mass and velocity"),
}
assert_eq!(MatterWave::from_momentum(0.0), None);
}
}