1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::{PI, TAU};
7
8pub mod prelude;
9
10pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
14
15pub const ELECTRON_MASS: f64 = 9.109_383_701_5e-31;
19
20pub const PROTON_MASS: f64 = 1.672_621_923_69e-27;
24
25pub const VACUUM_PERMITTIVITY: f64 = 8.854_187_812_8e-12;
29
30pub const VACUUM_PERMEABILITY: f64 = 1.256_637_062_12e-6;
34
35pub const BOLTZMANN_CONSTANT: f64 = 1.380_649e-23;
39
40fn all_finite(values: &[f64]) -> bool {
41 values.iter().all(|value| value.is_finite())
42}
43
44fn finite_result(value: f64) -> Option<f64> {
45 value.is_finite().then_some(value)
46}
47
48#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct PlasmaSpecies {
51 pub number_density: f64,
53 pub temperature_kelvin: f64,
55 pub charge_state: f64,
57 pub mass: f64,
59}
60
61impl PlasmaSpecies {
62 #[must_use]
64 pub fn new(
65 number_density: f64,
66 temperature_kelvin: f64,
67 charge_state: f64,
68 mass: f64,
69 ) -> Option<Self> {
70 if !all_finite(&[number_density, temperature_kelvin, charge_state, mass])
71 || number_density < 0.0
72 || temperature_kelvin < 0.0
73 || mass <= 0.0
74 {
75 return None;
76 }
77
78 Some(Self {
79 number_density,
80 temperature_kelvin,
81 charge_state,
82 mass,
83 })
84 }
85
86 #[must_use]
98 pub fn pressure(&self) -> Option<f64> {
99 plasma_pressure(self.number_density, self.temperature_kelvin)
100 }
101
102 #[must_use]
104 pub fn thermal_speed(&self) -> Option<f64> {
105 particle_thermal_speed(self.temperature_kelvin, self.mass)
106 }
107}
108
109#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct ElectronPlasma {
112 pub electron_number_density: f64,
114 pub electron_temperature_kelvin: f64,
116}
117
118impl ElectronPlasma {
119 #[must_use]
121 pub fn new(electron_number_density: f64, electron_temperature_kelvin: f64) -> Option<Self> {
122 if !all_finite(&[electron_number_density, electron_temperature_kelvin])
123 || electron_number_density < 0.0
124 || electron_temperature_kelvin < 0.0
125 {
126 return None;
127 }
128
129 Some(Self {
130 electron_number_density,
131 electron_temperature_kelvin,
132 })
133 }
134
135 #[must_use]
137 pub fn plasma_angular_frequency(&self) -> Option<f64> {
138 electron_plasma_angular_frequency(self.electron_number_density)
139 }
140
141 #[must_use]
143 pub fn plasma_frequency(&self) -> Option<f64> {
144 electron_plasma_frequency(self.electron_number_density)
145 }
146
147 #[must_use]
159 pub fn debye_length(&self) -> Option<f64> {
160 debye_length(
161 self.electron_temperature_kelvin,
162 self.electron_number_density,
163 )
164 }
165
166 #[must_use]
168 pub fn debye_number(&self) -> Option<f64> {
169 debye_number(self.electron_number_density, self.debye_length()?)
170 }
171
172 #[must_use]
174 pub fn thermal_speed(&self) -> Option<f64> {
175 electron_thermal_speed(self.electron_temperature_kelvin)
176 }
177
178 #[must_use]
180 pub fn pressure(&self) -> Option<f64> {
181 plasma_pressure(
182 self.electron_number_density,
183 self.electron_temperature_kelvin,
184 )
185 }
186}
187
188#[must_use]
203pub fn electron_plasma_angular_frequency(electron_number_density: f64) -> Option<f64> {
204 if !electron_number_density.is_finite() || electron_number_density < 0.0 {
205 return None;
206 }
207
208 let numerator = electron_number_density * ELEMENTARY_CHARGE * ELEMENTARY_CHARGE;
209 let denominator = VACUUM_PERMITTIVITY * ELECTRON_MASS;
210
211 finite_result((numerator / denominator).sqrt())
212}
213
214#[must_use]
226pub fn electron_plasma_frequency(electron_number_density: f64) -> Option<f64> {
227 finite_result(electron_plasma_angular_frequency(electron_number_density)? / TAU)
228}
229
230#[must_use]
234pub fn ion_plasma_angular_frequency(
235 ion_number_density: f64,
236 ion_charge_state: f64,
237 ion_mass: f64,
238) -> Option<f64> {
239 if !all_finite(&[ion_number_density, ion_charge_state, ion_mass])
240 || ion_number_density < 0.0
241 || ion_charge_state < 0.0
242 || ion_mass <= 0.0
243 {
244 return None;
245 }
246
247 let ion_charge = ion_charge_state * ELEMENTARY_CHARGE;
248 let numerator = ion_number_density * ion_charge * ion_charge;
249 let denominator = VACUUM_PERMITTIVITY * ion_mass;
250
251 finite_result((numerator / denominator).sqrt())
252}
253
254#[must_use]
266pub fn debye_length(electron_temperature_kelvin: f64, electron_number_density: f64) -> Option<f64> {
267 if !all_finite(&[electron_temperature_kelvin, electron_number_density])
268 || electron_temperature_kelvin < 0.0
269 || electron_number_density <= 0.0
270 {
271 return None;
272 }
273
274 let numerator = VACUUM_PERMITTIVITY * BOLTZMANN_CONSTANT * electron_temperature_kelvin;
275 let denominator = electron_number_density * ELEMENTARY_CHARGE * ELEMENTARY_CHARGE;
276
277 finite_result((numerator / denominator).sqrt())
278}
279
280#[must_use]
284pub fn debye_sphere_volume(debye_length: f64) -> Option<f64> {
285 if !debye_length.is_finite() || debye_length < 0.0 {
286 return None;
287 }
288
289 finite_result((4.0 / 3.0) * PI * debye_length.powi(3))
290}
291
292#[must_use]
296pub fn debye_number(electron_number_density: f64, debye_length: f64) -> Option<f64> {
297 if !all_finite(&[electron_number_density, debye_length])
298 || electron_number_density < 0.0
299 || debye_length < 0.0
300 {
301 return None;
302 }
303
304 finite_result(electron_number_density * debye_sphere_volume(debye_length)?)
305}
306
307#[must_use]
319pub fn electron_thermal_speed(electron_temperature_kelvin: f64) -> Option<f64> {
320 particle_thermal_speed(electron_temperature_kelvin, ELECTRON_MASS)
321}
322
323#[must_use]
327pub fn particle_thermal_speed(temperature_kelvin: f64, particle_mass: f64) -> Option<f64> {
328 if !all_finite(&[temperature_kelvin, particle_mass])
329 || temperature_kelvin < 0.0
330 || particle_mass <= 0.0
331 {
332 return None;
333 }
334
335 finite_result((BOLTZMANN_CONSTANT * temperature_kelvin / particle_mass).sqrt())
336}
337
338#[must_use]
342pub fn gyro_angular_frequency(charge: f64, magnetic_flux_density: f64, mass: f64) -> Option<f64> {
343 if !all_finite(&[charge, magnetic_flux_density, mass])
344 || charge == 0.0
345 || magnetic_flux_density < 0.0
346 || mass <= 0.0
347 {
348 return None;
349 }
350
351 finite_result(charge.abs() * magnetic_flux_density / mass)
352}
353
354#[must_use]
366pub fn gyrofrequency(charge: f64, magnetic_flux_density: f64, mass: f64) -> Option<f64> {
367 finite_result(gyro_angular_frequency(charge, magnetic_flux_density, mass)? / TAU)
368}
369
370#[must_use]
383pub fn gyroradius(
384 mass: f64,
385 perpendicular_speed: f64,
386 charge: f64,
387 magnetic_flux_density: f64,
388) -> Option<f64> {
389 if !all_finite(&[mass, perpendicular_speed, charge, magnetic_flux_density])
390 || mass <= 0.0
391 || perpendicular_speed < 0.0
392 || charge == 0.0
393 || magnetic_flux_density <= 0.0
394 {
395 return None;
396 }
397
398 finite_result(mass * perpendicular_speed / (charge.abs() * magnetic_flux_density))
399}
400
401#[must_use]
403pub fn electron_gyrofrequency(magnetic_flux_density: f64) -> Option<f64> {
404 gyrofrequency(ELEMENTARY_CHARGE, magnetic_flux_density, ELECTRON_MASS)
405}
406
407#[must_use]
409pub fn electron_gyroradius(perpendicular_speed: f64, magnetic_flux_density: f64) -> Option<f64> {
410 gyroradius(
411 ELECTRON_MASS,
412 perpendicular_speed,
413 ELEMENTARY_CHARGE,
414 magnetic_flux_density,
415 )
416}
417
418#[must_use]
422pub fn charge_density(
423 ion_number_density: f64,
424 ion_charge_state: f64,
425 electron_number_density: f64,
426) -> Option<f64> {
427 if !all_finite(&[
428 ion_number_density,
429 ion_charge_state,
430 electron_number_density,
431 ]) || ion_number_density < 0.0
432 || ion_charge_state < 0.0
433 || electron_number_density < 0.0
434 {
435 return None;
436 }
437
438 let charge_imbalance = ion_charge_state.mul_add(ion_number_density, -electron_number_density);
439 if !charge_imbalance.is_finite() {
440 return None;
441 }
442
443 finite_result(ELEMENTARY_CHARGE * charge_imbalance)
444}
445
446#[must_use]
456pub fn is_quasi_neutral(
457 ion_number_density: f64,
458 ion_charge_state: f64,
459 electron_number_density: f64,
460 relative_tolerance: f64,
461) -> Option<bool> {
462 if !all_finite(&[
463 ion_number_density,
464 ion_charge_state,
465 electron_number_density,
466 relative_tolerance,
467 ]) || ion_number_density < 0.0
468 || ion_charge_state < 0.0
469 || electron_number_density < 0.0
470 || relative_tolerance < 0.0
471 {
472 return None;
473 }
474
475 let ion_equivalent_density = ion_charge_state * ion_number_density;
476 if !ion_equivalent_density.is_finite() {
477 return None;
478 }
479
480 if ion_equivalent_density == 0.0 && electron_number_density == 0.0 {
481 return Some(true);
482 }
483
484 let scale = ion_equivalent_density.max(electron_number_density);
485 if scale == 0.0 || !scale.is_finite() {
486 return None;
487 }
488
489 let relative_difference = (ion_equivalent_density - electron_number_density).abs() / scale;
490 relative_difference
491 .is_finite()
492 .then_some(relative_difference <= relative_tolerance)
493}
494
495#[must_use]
499pub fn plasma_pressure(number_density: f64, temperature_kelvin: f64) -> Option<f64> {
500 if !all_finite(&[number_density, temperature_kelvin])
501 || number_density < 0.0
502 || temperature_kelvin < 0.0
503 {
504 return None;
505 }
506
507 finite_result(number_density * BOLTZMANN_CONSTANT * temperature_kelvin)
508}
509
510#[must_use]
514pub fn total_plasma_pressure(
515 electron_number_density: f64,
516 electron_temperature_kelvin: f64,
517 ion_number_density: f64,
518 ion_temperature_kelvin: f64,
519) -> Option<f64> {
520 let electron_pressure = plasma_pressure(electron_number_density, electron_temperature_kelvin)?;
521 let ion_pressure = plasma_pressure(ion_number_density, ion_temperature_kelvin)?;
522
523 finite_result(electron_pressure + ion_pressure)
524}
525
526#[must_use]
530pub fn magnetic_pressure(magnetic_flux_density: f64) -> Option<f64> {
531 if !magnetic_flux_density.is_finite() || magnetic_flux_density < 0.0 {
532 return None;
533 }
534
535 finite_result((magnetic_flux_density * magnetic_flux_density) / (2.0 * VACUUM_PERMEABILITY))
536}
537
538#[must_use]
550pub fn plasma_beta(plasma_pressure: f64, magnetic_flux_density: f64) -> Option<f64> {
551 if !all_finite(&[plasma_pressure, magnetic_flux_density])
552 || plasma_pressure < 0.0
553 || magnetic_flux_density <= 0.0
554 {
555 return None;
556 }
557
558 finite_result(plasma_pressure / magnetic_pressure(magnetic_flux_density)?)
559}
560
561#[must_use]
573pub fn alfven_speed(magnetic_flux_density: f64, mass_density: f64) -> Option<f64> {
574 if !all_finite(&[magnetic_flux_density, mass_density])
575 || magnetic_flux_density < 0.0
576 || mass_density <= 0.0
577 {
578 return None;
579 }
580
581 finite_result(magnetic_flux_density / (VACUUM_PERMEABILITY * mass_density).sqrt())
582}
583
584#[must_use]
589pub fn is_valid_coulomb_logarithm(coulomb_logarithm: f64) -> bool {
590 coulomb_logarithm.is_finite() && coulomb_logarithm > 0.0
591}
592
593#[cfg(test)]
594mod tests {
595 use super::{
596 BOLTZMANN_CONSTANT, ELECTRON_MASS, ELEMENTARY_CHARGE, ElectronPlasma, PI, PROTON_MASS,
597 PlasmaSpecies, VACUUM_PERMEABILITY, alfven_speed, charge_density, debye_length,
598 debye_number, debye_sphere_volume, electron_gyrofrequency, electron_gyroradius,
599 electron_plasma_angular_frequency, electron_plasma_frequency, electron_thermal_speed,
600 gyro_angular_frequency, gyrofrequency, gyroradius, ion_plasma_angular_frequency,
601 is_quasi_neutral, is_valid_coulomb_logarithm, magnetic_pressure, particle_thermal_speed,
602 plasma_beta, plasma_pressure, total_plasma_pressure,
603 };
604
605 fn approx_eq(left: f64, right: f64) -> bool {
606 let scale = left.abs().max(right.abs()).max(1.0);
607 (left - right).abs() <= 1.0e-12 * scale
608 }
609
610 #[test]
611 fn plasma_frequency_helpers_cover_common_inputs() {
612 assert!(matches!(
613 electron_plasma_angular_frequency(1.0e18),
614 Some(value) if value.is_finite() && value > 0.0
615 ));
616 assert!(matches!(
617 electron_plasma_frequency(1.0e18),
618 Some(value) if value.is_finite() && value > 0.0
619 ));
620 assert_eq!(electron_plasma_angular_frequency(-1.0), None);
621
622 assert!(matches!(
623 ion_plasma_angular_frequency(1.0e18, 1.0, PROTON_MASS),
624 Some(value) if value.is_finite() && value > 0.0
625 ));
626 assert_eq!(
627 ion_plasma_angular_frequency(1.0e18, -1.0, PROTON_MASS),
628 None
629 );
630 assert_eq!(ion_plasma_angular_frequency(1.0e18, 1.0, 0.0), None);
631 }
632
633 #[test]
634 fn debye_helpers_cover_common_inputs() {
635 assert!(matches!(
636 debye_length(10_000.0, 1.0e18),
637 Some(value) if value.is_finite() && value > 0.0
638 ));
639 assert_eq!(debye_length(-1.0, 1.0e18), None);
640 assert_eq!(debye_length(10_000.0, 0.0), None);
641
642 let expected_volume = (4.0 / 3.0) * PI * 8.0;
643 assert!(matches!(
644 debye_sphere_volume(2.0),
645 Some(value) if approx_eq(value, expected_volume)
646 ));
647 assert_eq!(debye_sphere_volume(-1.0), None);
648
649 assert!(matches!(
650 debye_number(1.0e18, 1.0e-4),
651 Some(value) if value.is_finite() && value > 0.0
652 ));
653 assert_eq!(debye_number(-1.0, 1.0e-4), None);
654 }
655
656 #[test]
657 fn thermal_speed_helpers_cover_common_inputs() {
658 assert!(matches!(
659 electron_thermal_speed(10_000.0),
660 Some(value) if value.is_finite() && value > 0.0
661 ));
662 assert_eq!(electron_thermal_speed(-1.0), None);
663
664 assert!(matches!(
665 particle_thermal_speed(10_000.0, PROTON_MASS),
666 Some(value) if value.is_finite() && value > 0.0
667 ));
668 assert_eq!(particle_thermal_speed(10_000.0, 0.0), None);
669 }
670
671 #[test]
672 fn gyro_helpers_cover_common_inputs() {
673 assert!(matches!(
674 gyro_angular_frequency(ELEMENTARY_CHARGE, 1.0, ELECTRON_MASS),
675 Some(value) if value.is_finite() && value > 0.0
676 ));
677 assert_eq!(gyro_angular_frequency(0.0, 1.0, ELECTRON_MASS), None);
678 assert_eq!(
679 gyro_angular_frequency(ELEMENTARY_CHARGE, -1.0, ELECTRON_MASS),
680 None
681 );
682
683 assert!(matches!(
684 gyrofrequency(ELEMENTARY_CHARGE, 1.0, ELECTRON_MASS),
685 Some(value) if value.is_finite() && value > 0.0
686 ));
687
688 assert!(matches!(
689 gyroradius(ELECTRON_MASS, 1_000.0, ELEMENTARY_CHARGE, 1.0),
690 Some(value) if value.is_finite() && value > 0.0
691 ));
692 assert_eq!(
693 gyroradius(ELECTRON_MASS, -1_000.0, ELEMENTARY_CHARGE, 1.0),
694 None
695 );
696 assert_eq!(gyroradius(ELECTRON_MASS, 1_000.0, 0.0, 1.0), None);
697 assert_eq!(
698 gyroradius(ELECTRON_MASS, 1_000.0, ELEMENTARY_CHARGE, 0.0),
699 None
700 );
701
702 assert!(matches!(
703 electron_gyrofrequency(1.0),
704 Some(value) if value.is_finite() && value > 0.0
705 ));
706 assert!(matches!(
707 electron_gyroradius(1_000.0, 1.0),
708 Some(value) if value.is_finite() && value > 0.0
709 ));
710 }
711
712 #[test]
713 fn charge_density_and_quasi_neutrality_cover_common_inputs() {
714 assert!(matches!(
715 charge_density(1.0e18, 1.0, 1.0e18),
716 Some(value) if approx_eq(value, 0.0)
717 ));
718 assert!(matches!(
719 charge_density(1.0e18, 1.0, 0.5e18),
720 Some(value) if value.is_finite() && value > 0.0
721 ));
722 assert_eq!(charge_density(-1.0, 1.0, 1.0e18), None);
723
724 assert_eq!(is_quasi_neutral(1.0e18, 1.0, 1.0e18, 1.0e-9), Some(true));
725 assert_eq!(is_quasi_neutral(1.0e18, 1.0, 0.5e18, 1.0e-9), Some(false));
726 assert_eq!(is_quasi_neutral(0.0, 1.0, 0.0, 0.0), Some(true));
727 assert_eq!(is_quasi_neutral(1.0e18, 1.0, 1.0e18, -1.0), None);
728 }
729
730 #[test]
731 fn pressure_and_field_helpers_cover_common_inputs() {
732 assert!(matches!(
733 plasma_pressure(1.0e18, 10_000.0),
734 Some(value) if value.is_finite() && value > 0.0
735 ));
736 assert_eq!(plasma_pressure(-1.0, 10_000.0), None);
737 assert_eq!(plasma_pressure(1.0e18, -1.0), None);
738
739 assert!(matches!(
740 total_plasma_pressure(1.0e18, 10_000.0, 1.0e18, 10_000.0),
741 Some(value) if value.is_finite() && value > 0.0
742 ));
743
744 assert!(matches!(
745 magnetic_pressure(1.0),
746 Some(value) if value.is_finite() && value > 0.0
747 ));
748 assert_eq!(magnetic_pressure(-1.0), None);
749
750 assert!(matches!(
751 plasma_beta(1.0, 1.0),
752 Some(value) if value.is_finite() && value > 0.0
753 ));
754 assert_eq!(plasma_beta(-1.0, 1.0), None);
755 assert_eq!(plasma_beta(1.0, 0.0), None);
756
757 assert!(matches!(
758 alfven_speed(1.0, 1.0e-12),
759 Some(value) if value.is_finite() && value > 0.0
760 ));
761 assert_eq!(alfven_speed(-1.0, 1.0e-12), None);
762 assert_eq!(alfven_speed(1.0, 0.0), None);
763 }
764
765 #[test]
766 fn simple_validators_cover_common_inputs() {
767 assert!(is_valid_coulomb_logarithm(10.0));
768 assert!(!is_valid_coulomb_logarithm(0.0));
769 assert!(!is_valid_coulomb_logarithm(f64::NAN));
770 }
771
772 #[test]
773 fn simple_types_delegate_to_public_helpers() {
774 let proton_species = PlasmaSpecies::new(1.0e18, 10_000.0, 1.0, PROTON_MASS);
775 assert!(matches!(
776 proton_species.and_then(|species| species.pressure()),
777 Some(value) if value.is_finite() && value > 0.0
778 ));
779 assert!(matches!(
780 proton_species.and_then(|species| species.thermal_speed()),
781 Some(value) if value.is_finite() && value > 0.0
782 ));
783 assert_eq!(PlasmaSpecies::new(-1.0, 10_000.0, 1.0, PROTON_MASS), None);
784 assert_eq!(PlasmaSpecies::new(1.0e18, 10_000.0, 1.0, 0.0), None);
785
786 let electron_plasma = ElectronPlasma::new(1.0e18, 10_000.0);
787 assert!(matches!(
788 electron_plasma.and_then(|plasma| plasma.plasma_frequency()),
789 Some(value) if value.is_finite() && value > 0.0
790 ));
791 assert!(matches!(
792 electron_plasma.and_then(|plasma| plasma.debye_length()),
793 Some(value) if value.is_finite() && value > 0.0
794 ));
795 assert!(matches!(
796 electron_plasma.and_then(|plasma| plasma.debye_number()),
797 Some(value) if value.is_finite() && value > 0.0
798 ));
799 assert!(matches!(
800 electron_plasma.and_then(|plasma| plasma.thermal_speed()),
801 Some(value) if value.is_finite() && value > 0.0
802 ));
803 assert!(matches!(
804 electron_plasma.and_then(|plasma| plasma.pressure()),
805 Some(value) if value.is_finite() && value > 0.0
806 ));
807 assert_eq!(ElectronPlasma::new(-1.0, 10_000.0), None);
808 }
809
810 #[test]
811 fn formulas_match_expected_scalar_relations() {
812 let expected_pressure = 1.0e18 * BOLTZMANN_CONSTANT * 10_000.0;
813 assert!(matches!(
814 plasma_pressure(1.0e18, 10_000.0),
815 Some(value) if approx_eq(value, expected_pressure)
816 ));
817
818 let expected_magnetic_pressure = 1.0 / (2.0 * VACUUM_PERMEABILITY);
819 assert!(matches!(
820 magnetic_pressure(1.0),
821 Some(value) if approx_eq(value, expected_magnetic_pressure)
822 ));
823 }
824}