1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4pub mod prelude;
7
8pub const PLANCK_CONSTANT: f64 = 6.626_070_15e-34;
12
13pub const REDUCED_PLANCK_CONSTANT: f64 = 1.054_571_817e-34;
17
18pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
22
23pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
27
28pub const ELECTRON_MASS: f64 = 9.109_383_701_5e-31;
32
33pub const BOHR_RADIUS: f64 = 5.291_772_109_03e-11;
37
38pub const RYDBERG_ENERGY_EV: f64 = 13.605_693_122_994;
42
43fn is_nonnegative_finite(value: f64) -> bool {
44 value.is_finite() && value >= 0.0
45}
46
47fn is_positive_finite(value: f64) -> bool {
48 value.is_finite() && value > 0.0
49}
50
51fn finite_result(value: f64) -> Option<f64> {
52 value.is_finite().then_some(value)
53}
54
55fn principal_squared(principal: u32) -> Option<f64> {
56 if principal == 0 {
57 return None;
58 }
59
60 let principal = f64::from(principal);
61 finite_result(principal * principal)
62}
63
64fn momentum_magnitude_from_mass_velocity(mass: f64, velocity: f64) -> Option<f64> {
65 if !is_positive_finite(mass) || !velocity.is_finite() {
66 return None;
67 }
68
69 let speed = velocity.abs();
70 if speed == 0.0 {
71 return None;
72 }
73
74 let momentum = mass * speed;
75 (momentum.is_finite() && momentum > 0.0).then_some(momentum)
76}
77
78#[must_use]
94pub fn photon_energy_from_frequency(frequency: f64) -> Option<f64> {
95 if !is_nonnegative_finite(frequency) {
96 return None;
97 }
98
99 finite_result(PLANCK_CONSTANT * frequency)
100}
101
102#[must_use]
118pub fn photon_energy_from_wavelength(wavelength: f64) -> Option<f64> {
119 if !is_positive_finite(wavelength) {
120 return None;
121 }
122
123 finite_result((PLANCK_CONSTANT * SPEED_OF_LIGHT) / wavelength)
124}
125
126#[must_use]
131pub fn frequency_from_photon_energy(energy: f64) -> Option<f64> {
132 if !is_nonnegative_finite(energy) {
133 return None;
134 }
135
136 finite_result(energy / PLANCK_CONSTANT)
137}
138
139#[must_use]
144pub fn wavelength_from_photon_energy(energy: f64) -> Option<f64> {
145 if !is_positive_finite(energy) {
146 return None;
147 }
148
149 finite_result((PLANCK_CONSTANT * SPEED_OF_LIGHT) / energy)
150}
151
152#[must_use]
157pub fn photon_momentum_from_wavelength(wavelength: f64) -> Option<f64> {
158 if !is_positive_finite(wavelength) {
159 return None;
160 }
161
162 finite_result(PLANCK_CONSTANT / wavelength)
163}
164
165#[must_use]
170pub fn photon_momentum_from_energy(energy: f64) -> Option<f64> {
171 if !is_nonnegative_finite(energy) {
172 return None;
173 }
174
175 finite_result(energy / SPEED_OF_LIGHT)
176}
177
178#[must_use]
183pub fn joules_to_electron_volts(joules: f64) -> Option<f64> {
184 if !is_nonnegative_finite(joules) {
185 return None;
186 }
187
188 finite_result(joules / ELEMENTARY_CHARGE)
189}
190
191#[must_use]
196pub fn electron_volts_to_joules(electron_volts: f64) -> Option<f64> {
197 if !is_nonnegative_finite(electron_volts) {
198 return None;
199 }
200
201 finite_result(electron_volts * ELEMENTARY_CHARGE)
202}
203
204#[must_use]
209pub fn de_broglie_wavelength(momentum: f64) -> Option<f64> {
210 if !is_positive_finite(momentum) {
211 return None;
212 }
213
214 finite_result(PLANCK_CONSTANT / momentum)
215}
216
217#[must_use]
234pub fn de_broglie_wavelength_from_mass_velocity(mass: f64, velocity: f64) -> Option<f64> {
235 de_broglie_wavelength(momentum_magnitude_from_mass_velocity(mass, velocity)?)
236}
237
238#[must_use]
243pub fn momentum_from_de_broglie_wavelength(wavelength: f64) -> Option<f64> {
244 photon_momentum_from_wavelength(wavelength)
245}
246
247#[must_use]
252pub fn angular_frequency_from_energy(energy: f64) -> Option<f64> {
253 if !is_nonnegative_finite(energy) {
254 return None;
255 }
256
257 finite_result(energy / REDUCED_PLANCK_CONSTANT)
258}
259
260#[must_use]
265pub fn energy_from_angular_frequency(angular_frequency: f64) -> Option<f64> {
266 if !is_nonnegative_finite(angular_frequency) {
267 return None;
268 }
269
270 finite_result(REDUCED_PLANCK_CONSTANT * angular_frequency)
271}
272
273fn minimum_conjugate_uncertainty(uncertainty: f64) -> Option<f64> {
274 if !is_positive_finite(uncertainty) {
275 return None;
276 }
277
278 finite_result(REDUCED_PLANCK_CONSTANT / (2.0 * uncertainty))
279}
280
281#[must_use]
298pub fn minimum_position_uncertainty(momentum_uncertainty: f64) -> Option<f64> {
299 minimum_conjugate_uncertainty(momentum_uncertainty)
300}
301
302#[must_use]
307pub fn minimum_momentum_uncertainty(position_uncertainty: f64) -> Option<f64> {
308 minimum_conjugate_uncertainty(position_uncertainty)
309}
310
311#[must_use]
316pub fn minimum_energy_uncertainty(time_uncertainty: f64) -> Option<f64> {
317 minimum_conjugate_uncertainty(time_uncertainty)
318}
319
320#[must_use]
325pub fn minimum_time_uncertainty(energy_uncertainty: f64) -> Option<f64> {
326 minimum_conjugate_uncertainty(energy_uncertainty)
327}
328
329#[must_use]
333pub fn bohr_orbit_radius(n: u32) -> Option<f64> {
334 finite_result(BOHR_RADIUS * principal_squared(n)?)
335}
336
337#[must_use]
349pub fn hydrogen_energy_level_ev(n: u32) -> Option<f64> {
350 finite_result(-RYDBERG_ENERGY_EV / principal_squared(n)?)
351}
352
353#[must_use]
358pub fn hydrogen_transition_energy_ev(initial_n: u32, final_n: u32) -> Option<f64> {
359 if initial_n == 0 || final_n == 0 {
360 return None;
361 }
362
363 if initial_n == final_n {
364 return Some(0.0);
365 }
366
367 let initial = hydrogen_energy_level_ev(initial_n)?;
368 let final_ = hydrogen_energy_level_ev(final_n)?;
369
370 finite_result((final_ - initial).abs())
371}
372
373#[must_use]
390pub fn hydrogen_transition_wavelength(initial_n: u32, final_n: u32) -> Option<f64> {
391 let transition_energy_ev = hydrogen_transition_energy_ev(initial_n, final_n)?;
392 if transition_energy_ev == 0.0 {
393 return None;
394 }
395
396 wavelength_from_photon_energy(electron_volts_to_joules(transition_energy_ev)?)
397}
398
399#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
401pub struct QuantumNumbers {
402 pub principal: u32,
404 pub azimuthal: u32,
406 pub magnetic: i32,
408 pub spin_twice: i8,
410}
411
412#[must_use]
414pub const fn is_valid_principal_quantum_number(n: u32) -> bool {
415 n >= 1
416}
417
418#[must_use]
420pub const fn is_valid_azimuthal_quantum_number(n: u32, l: u32) -> bool {
421 is_valid_principal_quantum_number(n) && l < n
422}
423
424#[must_use]
426pub fn is_valid_magnetic_quantum_number(l: u32, m_l: i32) -> bool {
427 let l = i64::from(l);
428 let magnetic = i64::from(m_l);
429
430 (-l..=l).contains(&magnetic)
431}
432
433#[must_use]
435pub const fn is_valid_spin_twice(spin_twice: i8) -> bool {
436 matches!(spin_twice, -1 | 1)
437}
438
439#[must_use]
441pub fn is_valid_quantum_numbers(
442 principal: u32,
443 azimuthal: u32,
444 magnetic: i32,
445 spin_twice: i8,
446) -> bool {
447 is_valid_azimuthal_quantum_number(principal, azimuthal)
448 && is_valid_magnetic_quantum_number(azimuthal, magnetic)
449 && is_valid_spin_twice(spin_twice)
450}
451
452impl QuantumNumbers {
453 #[must_use]
473 pub fn new(principal: u32, azimuthal: u32, magnetic: i32, spin_twice: i8) -> Option<Self> {
474 is_valid_quantum_numbers(principal, azimuthal, magnetic, spin_twice).then_some(Self {
475 principal,
476 azimuthal,
477 magnetic,
478 spin_twice,
479 })
480 }
481
482 #[must_use]
484 pub fn spin_projection(&self) -> f64 {
485 f64::from(self.spin_twice) / 2.0
486 }
487}
488
489#[derive(Debug, Clone, Copy, PartialEq)]
491pub struct Photon {
492 pub energy_joules: f64,
494}
495
496impl Photon {
497 #[must_use]
499 pub fn from_energy_joules(energy_joules: f64) -> Option<Self> {
500 is_nonnegative_finite(energy_joules).then_some(Self { energy_joules })
501 }
502
503 #[must_use]
505 pub fn from_frequency(frequency: f64) -> Option<Self> {
506 Self::from_energy_joules(photon_energy_from_frequency(frequency)?)
507 }
508
509 #[must_use]
522 pub fn from_wavelength(wavelength: f64) -> Option<Self> {
523 Self::from_energy_joules(photon_energy_from_wavelength(wavelength)?)
524 }
525
526 #[must_use]
528 pub const fn energy_joules(&self) -> f64 {
529 self.energy_joules
530 }
531
532 #[must_use]
534 pub fn energy_ev(&self) -> Option<f64> {
535 joules_to_electron_volts(self.energy_joules)
536 }
537
538 #[must_use]
540 pub fn frequency(&self) -> Option<f64> {
541 frequency_from_photon_energy(self.energy_joules)
542 }
543
544 #[must_use]
546 pub fn wavelength(&self) -> Option<f64> {
547 wavelength_from_photon_energy(self.energy_joules)
548 }
549
550 #[must_use]
552 pub fn momentum(&self) -> Option<f64> {
553 photon_momentum_from_energy(self.energy_joules)
554 }
555}
556
557#[derive(Debug, Clone, Copy, PartialEq)]
559pub struct MatterWave {
560 pub momentum: f64,
562}
563
564impl MatterWave {
565 #[must_use]
567 pub fn from_momentum(momentum: f64) -> Option<Self> {
568 is_positive_finite(momentum).then_some(Self { momentum })
569 }
570
571 #[must_use]
584 pub fn from_mass_velocity(mass: f64, velocity: f64) -> Option<Self> {
585 Self::from_momentum(momentum_magnitude_from_mass_velocity(mass, velocity)?)
586 }
587
588 #[must_use]
590 pub fn wavelength(&self) -> Option<f64> {
591 de_broglie_wavelength(self.momentum)
592 }
593}
594
595#[cfg(test)]
596#[allow(clippy::float_cmp)]
597mod tests {
598 use super::*;
599
600 fn approx_eq(left: f64, right: f64) -> bool {
601 let scale = left.abs().max(right.abs()).max(1.0);
602 (left - right).abs() <= 1.0e-12 * scale
603 }
604
605 fn assert_approx_eq(left: f64, right: f64) {
606 assert!(
607 approx_eq(left, right),
608 "left={left:e} right={right:e} delta={:e}",
609 (left - right).abs()
610 );
611 }
612
613 fn assert_some_approx_eq(value: Option<f64>, expected: f64) {
614 match value {
615 Some(actual) => assert_approx_eq(actual, expected),
616 None => panic!("expected Some({expected:e})"),
617 }
618 }
619
620 #[test]
621 fn photon_energy_helpers_cover_frequency_and_wavelength() {
622 assert_eq!(photon_energy_from_frequency(1.0), Some(PLANCK_CONSTANT));
623 assert_eq!(photon_energy_from_frequency(-1.0), None);
624
625 assert_some_approx_eq(
626 photon_energy_from_wavelength(SPEED_OF_LIGHT),
627 PLANCK_CONSTANT,
628 );
629 assert_eq!(photon_energy_from_wavelength(0.0), None);
630 }
631
632 #[test]
633 fn photon_frequency_and_wavelength_helpers_invert_energy() {
634 assert_some_approx_eq(frequency_from_photon_energy(PLANCK_CONSTANT), 1.0);
635 assert_eq!(frequency_from_photon_energy(-1.0), None);
636
637 assert_some_approx_eq(
638 wavelength_from_photon_energy(PLANCK_CONSTANT),
639 SPEED_OF_LIGHT,
640 );
641 assert_eq!(wavelength_from_photon_energy(0.0), None);
642 }
643
644 #[test]
645 fn photon_momentum_and_energy_conversion_helpers_work() {
646 assert_some_approx_eq(photon_momentum_from_wavelength(PLANCK_CONSTANT), 1.0);
647 assert_some_approx_eq(photon_momentum_from_energy(SPEED_OF_LIGHT), 1.0);
648
649 assert_some_approx_eq(joules_to_electron_volts(ELEMENTARY_CHARGE), 1.0);
650 assert_some_approx_eq(electron_volts_to_joules(1.0), ELEMENTARY_CHARGE);
651 }
652
653 #[test]
654 fn matter_wave_helpers_cover_momentum_and_mass_velocity() {
655 assert_some_approx_eq(de_broglie_wavelength(PLANCK_CONSTANT), 1.0);
656 assert_eq!(de_broglie_wavelength(0.0), None);
657
658 assert_some_approx_eq(
659 de_broglie_wavelength_from_mass_velocity(2.0, 3.0),
660 PLANCK_CONSTANT / 6.0,
661 );
662 assert_eq!(de_broglie_wavelength_from_mass_velocity(2.0, 0.0), None);
663 assert_eq!(de_broglie_wavelength_from_mass_velocity(0.0, 3.0), None);
664
665 assert_some_approx_eq(momentum_from_de_broglie_wavelength(PLANCK_CONSTANT), 1.0);
666 }
667
668 #[test]
669 fn reduced_planck_and_uncertainty_helpers_work() {
670 assert_some_approx_eq(angular_frequency_from_energy(REDUCED_PLANCK_CONSTANT), 1.0);
671 assert_some_approx_eq(energy_from_angular_frequency(1.0), REDUCED_PLANCK_CONSTANT);
672
673 assert_some_approx_eq(minimum_position_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
674 assert_eq!(minimum_position_uncertainty(0.0), None);
675
676 assert_some_approx_eq(minimum_momentum_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
677 assert_eq!(minimum_momentum_uncertainty(0.0), None);
678
679 assert_some_approx_eq(minimum_energy_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
680 assert_eq!(minimum_energy_uncertainty(0.0), None);
681
682 assert_some_approx_eq(minimum_time_uncertainty(REDUCED_PLANCK_CONSTANT), 0.5);
683 assert_eq!(minimum_time_uncertainty(0.0), None);
684 }
685
686 #[test]
687 fn bohr_model_helpers_cover_levels_and_transitions() {
688 assert_some_approx_eq(bohr_orbit_radius(1), BOHR_RADIUS);
689 assert_some_approx_eq(bohr_orbit_radius(2), 4.0 * BOHR_RADIUS);
690 assert_eq!(bohr_orbit_radius(0), None);
691
692 assert_some_approx_eq(hydrogen_energy_level_ev(1), -RYDBERG_ENERGY_EV);
693 assert_some_approx_eq(hydrogen_energy_level_ev(2), -RYDBERG_ENERGY_EV / 4.0);
694 assert_eq!(hydrogen_energy_level_ev(0), None);
695
696 assert_some_approx_eq(hydrogen_transition_energy_ev(2, 1), 10.204_269_842_245_5);
697 assert_eq!(hydrogen_transition_energy_ev(1, 1), Some(0.0));
698 assert_eq!(hydrogen_transition_energy_ev(0, 1), None);
699
700 match hydrogen_transition_wavelength(2, 1) {
701 Some(wavelength) => assert!(wavelength.is_finite() && wavelength > 0.0),
702 None => panic!("expected a valid transition wavelength"),
703 }
704 assert_eq!(hydrogen_transition_wavelength(1, 1), None);
705 }
706
707 #[test]
708 fn quantum_number_helpers_validate_expected_ranges() {
709 assert!(is_valid_principal_quantum_number(1));
710 assert!(!is_valid_principal_quantum_number(0));
711
712 assert!(is_valid_azimuthal_quantum_number(1, 0));
713 assert!(!is_valid_azimuthal_quantum_number(1, 1));
714
715 assert!(is_valid_magnetic_quantum_number(1, -1));
716 assert!(is_valid_magnetic_quantum_number(1, 0));
717 assert!(is_valid_magnetic_quantum_number(1, 1));
718 assert!(!is_valid_magnetic_quantum_number(1, 2));
719
720 assert!(is_valid_spin_twice(1));
721 assert!(is_valid_spin_twice(-1));
722 assert!(!is_valid_spin_twice(0));
723
724 assert!(is_valid_quantum_numbers(2, 1, 0, 1));
725 assert!(!is_valid_quantum_numbers(2, 2, 0, 1));
726
727 match QuantumNumbers::new(2, 1, 0, 1) {
728 Some(quantum_numbers) => assert_eq!(quantum_numbers.spin_projection(), 0.5),
729 None => panic!("expected valid quantum numbers"),
730 }
731 assert_eq!(QuantumNumbers::new(2, 2, 0, 1), None);
732 }
733
734 #[test]
735 fn photon_wrapper_delegates_to_public_helpers() {
736 match Photon::from_frequency(1.0) {
737 Some(photon) => assert_eq!(photon.energy_joules(), PLANCK_CONSTANT),
738 None => panic!("expected a valid photon from frequency"),
739 }
740
741 match Photon::from_wavelength(SPEED_OF_LIGHT) {
742 Some(photon) => assert_some_approx_eq(photon.frequency(), 1.0),
743 None => panic!("expected a valid photon from wavelength"),
744 }
745
746 match Photon::from_energy_joules(PLANCK_CONSTANT) {
747 Some(photon) => assert_some_approx_eq(photon.wavelength(), SPEED_OF_LIGHT),
748 None => panic!("expected a valid photon from energy"),
749 }
750
751 assert_eq!(Photon::from_energy_joules(-1.0), None);
752 }
753
754 #[test]
755 fn matter_wave_wrapper_delegates_to_public_helpers() {
756 match MatterWave::from_momentum(PLANCK_CONSTANT) {
757 Some(wave) => assert_some_approx_eq(wave.wavelength(), 1.0),
758 None => panic!("expected a valid matter wave from momentum"),
759 }
760
761 match MatterWave::from_mass_velocity(2.0, 3.0) {
762 Some(wave) => assert_some_approx_eq(wave.wavelength(), PLANCK_CONSTANT / 6.0),
763 None => panic!("expected a valid matter wave from mass and velocity"),
764 }
765
766 assert_eq!(MatterWave::from_momentum(0.0), None);
767 }
768}