Skip to main content

use_plasma/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small scalar plasma physics helpers.
5
6use core::f64::consts::{PI, TAU};
7
8pub mod prelude;
9
10/// Elementary charge in coulombs.
11///
12/// Broader physical constants belong in the top-level `use-constants` set.
13pub const ELEMENTARY_CHARGE: f64 = 1.602_176_634e-19;
14
15/// Electron mass in kilograms.
16///
17/// Broader physical constants belong in the top-level `use-constants` set.
18pub const ELECTRON_MASS: f64 = 9.109_383_701_5e-31;
19
20/// Proton mass in kilograms.
21///
22/// Broader physical constants belong in the top-level `use-constants` set.
23pub const PROTON_MASS: f64 = 1.672_621_923_69e-27;
24
25/// Vacuum permittivity in farads per meter.
26///
27/// Broader physical constants belong in the top-level `use-constants` set.
28pub const VACUUM_PERMITTIVITY: f64 = 8.854_187_812_8e-12;
29
30/// Vacuum permeability in henries per meter.
31///
32/// Broader physical constants belong in the top-level `use-constants` set.
33pub const VACUUM_PERMEABILITY: f64 = 1.256_637_062_12e-6;
34
35/// Boltzmann constant in joules per kelvin.
36///
37/// Broader physical constants belong in the top-level `use-constants` set.
38pub 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/// A simple scalar plasma species description.
49#[derive(Debug, Clone, Copy, PartialEq)]
50pub struct PlasmaSpecies {
51    /// Number density in particles per cubic meter.
52    pub number_density: f64,
53    /// Temperature in kelvin.
54    pub temperature_kelvin: f64,
55    /// Signed charge state in elementary-charge units.
56    pub charge_state: f64,
57    /// Particle mass in kilograms.
58    pub mass: f64,
59}
60
61impl PlasmaSpecies {
62    /// Creates a plasma species when the inputs are finite and physically valid.
63    #[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    /// Computes scalar species pressure using `p = n k_B T`.
87    ///
88    /// # Examples
89    ///
90    /// ```rust
91    /// use use_plasma::{PROTON_MASS, PlasmaSpecies};
92    ///
93    /// let species = PlasmaSpecies::new(1.0e18, 10_000.0, 1.0, PROTON_MASS);
94    ///
95    /// assert!(species.and_then(|value| value.pressure()).is_some_and(|value| value > 0.0));
96    /// ```
97    #[must_use]
98    pub fn pressure(&self) -> Option<f64> {
99        plasma_pressure(self.number_density, self.temperature_kelvin)
100    }
101
102    /// Computes the species thermal speed using `v_th = sqrt(k_B T / m)`.
103    #[must_use]
104    pub fn thermal_speed(&self) -> Option<f64> {
105        particle_thermal_speed(self.temperature_kelvin, self.mass)
106    }
107}
108
109/// A simple electron-plasma state.
110#[derive(Debug, Clone, Copy, PartialEq)]
111pub struct ElectronPlasma {
112    /// Electron number density in particles per cubic meter.
113    pub electron_number_density: f64,
114    /// Electron temperature in kelvin.
115    pub electron_temperature_kelvin: f64,
116}
117
118impl ElectronPlasma {
119    /// Creates an electron-plasma state when both values are finite and non-negative.
120    #[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    /// Computes the electron plasma angular frequency.
136    #[must_use]
137    pub fn plasma_angular_frequency(&self) -> Option<f64> {
138        electron_plasma_angular_frequency(self.electron_number_density)
139    }
140
141    /// Computes the electron plasma frequency.
142    #[must_use]
143    pub fn plasma_frequency(&self) -> Option<f64> {
144        electron_plasma_frequency(self.electron_number_density)
145    }
146
147    /// Computes the Debye length for this plasma state.
148    ///
149    /// # Examples
150    ///
151    /// ```rust
152    /// use use_plasma::ElectronPlasma;
153    ///
154    /// let plasma = ElectronPlasma::new(1.0e18, 10_000.0);
155    ///
156    /// assert!(plasma.and_then(|value| value.debye_length()).is_some_and(|value| value > 0.0));
157    /// ```
158    #[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    /// Computes the Debye number for this plasma state.
167    #[must_use]
168    pub fn debye_number(&self) -> Option<f64> {
169        debye_number(self.electron_number_density, self.debye_length()?)
170    }
171
172    /// Computes the electron thermal speed.
173    #[must_use]
174    pub fn thermal_speed(&self) -> Option<f64> {
175        electron_thermal_speed(self.electron_temperature_kelvin)
176    }
177
178    /// Computes the scalar electron pressure using `p = n k_B T`.
179    #[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/// Computes the electron plasma angular frequency.
189///
190/// Formula: `ω_pe = sqrt(n_e e^2 / (ε0 m_e))`
191///
192/// Returns `None` when `electron_number_density` is negative, when the input is not finite, or
193/// when the computed result is not finite.
194///
195/// # Examples
196///
197/// ```rust
198/// use use_plasma::electron_plasma_frequency;
199///
200/// assert!(electron_plasma_frequency(1.0e18).is_some_and(|value| value > 0.0));
201/// ```
202#[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/// Computes the electron plasma frequency in hertz.
215///
216/// Formula: `f_pe = ω_pe / (2π)`
217///
218/// # Examples
219///
220/// ```rust
221/// use use_plasma::electron_plasma_frequency;
222///
223/// assert!(electron_plasma_frequency(1.0e18).is_some_and(|value| value > 0.0));
224/// ```
225#[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/// Computes the ion plasma angular frequency.
231///
232/// Formula: `ω_pi = sqrt(n_i (Z e)^2 / (ε0 m_i))`
233#[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/// Computes the Debye length.
255///
256/// Formula: `λ_D = sqrt(ε0 k_B T_e / (n_e e^2))`
257///
258/// # Examples
259///
260/// ```rust
261/// use use_plasma::debye_length;
262///
263/// assert!(debye_length(10_000.0, 1.0e18).is_some_and(|value| value > 0.0));
264/// ```
265#[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/// Computes the Debye sphere volume.
281///
282/// Formula: `V_D = (4/3)πλ_D^3`
283#[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/// Computes the Debye number.
293///
294/// Formula: `N_D = n_e (4/3)πλ_D^3`
295#[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/// Computes the electron thermal speed.
308///
309/// Formula: `v_th,e = sqrt(k_B T_e / m_e)`
310///
311/// # Examples
312///
313/// ```rust
314/// use use_plasma::electron_thermal_speed;
315///
316/// assert!(electron_thermal_speed(10_000.0).is_some_and(|value| value > 0.0));
317/// ```
318#[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/// Computes a particle thermal speed.
324///
325/// Formula: `v_th = sqrt(k_B T / m)`
326#[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/// Computes the gyro angular frequency.
339///
340/// Formula: `ω_c = |q| B / m`
341#[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/// Computes the gyrofrequency in hertz.
355///
356/// Formula: `f_c = ω_c / (2π)`
357///
358/// # Examples
359///
360/// ```rust
361/// use use_plasma::{ELECTRON_MASS, ELEMENTARY_CHARGE, gyrofrequency};
362///
363/// assert!(gyrofrequency(ELEMENTARY_CHARGE, 1.0, ELECTRON_MASS).is_some_and(|value| value > 0.0));
364/// ```
365#[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/// Computes the gyroradius.
371///
372/// Formula: `r_L = m v_perp / (|q| B)`
373///
374/// # Examples
375///
376/// ```rust
377/// use use_plasma::{ELECTRON_MASS, ELEMENTARY_CHARGE, gyroradius};
378///
379/// assert!(gyroradius(ELECTRON_MASS, 1_000.0, ELEMENTARY_CHARGE, 1.0)
380///     .is_some_and(|value| value > 0.0));
381/// ```
382#[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/// Computes the electron gyrofrequency using electron mass and charge magnitude.
402#[must_use]
403pub fn electron_gyrofrequency(magnetic_flux_density: f64) -> Option<f64> {
404    gyrofrequency(ELEMENTARY_CHARGE, magnetic_flux_density, ELECTRON_MASS)
405}
406
407/// Computes the electron gyroradius using electron mass and charge magnitude.
408#[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/// Computes charge density.
419///
420/// Formula: `ρ_q = e (Z n_i - n_e)`
421#[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/// Checks whether a plasma is quasi-neutral within a relative tolerance.
447///
448/// # Examples
449///
450/// ```rust
451/// use use_plasma::is_quasi_neutral;
452///
453/// assert_eq!(is_quasi_neutral(1.0e18, 1.0, 1.0e18, 1.0e-9), Some(true));
454/// ```
455#[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/// Computes scalar plasma pressure.
496///
497/// Formula: `p = n k_B T`
498#[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/// Computes total scalar plasma pressure.
511///
512/// Formula: `p_total = n_e k_B T_e + n_i k_B T_i`
513#[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/// Computes magnetic pressure.
527///
528/// Formula: `p_B = B^2 / (2μ0)`
529#[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/// Computes plasma beta.
539///
540/// Formula: `β = p / p_B`
541///
542/// # Examples
543///
544/// ```rust
545/// use use_plasma::plasma_beta;
546///
547/// assert!(plasma_beta(1.0, 1.0).is_some_and(|value| value > 0.0));
548/// ```
549#[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/// Computes the Alfven speed.
562///
563/// Formula: `v_A = B / sqrt(μ0 ρ)`
564///
565/// # Examples
566///
567/// ```rust
568/// use use_plasma::alfven_speed;
569///
570/// assert!(alfven_speed(1.0, 1.0e-12).is_some_and(|value| value > 0.0));
571/// ```
572#[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/// Returns whether a Coulomb logarithm value is finite and positive.
585///
586/// Detailed collisional plasma models and collision-frequency formulas are intentionally out of
587/// scope for this crate.
588#[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}