Skip to main content

use_electromagnetism/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Small scalar helpers for combined electric and magnetic field relations.
5
6use core::f64::consts::TAU;
7
8pub mod prelude;
9
10/// Vacuum permittivity in farads per meter.
11///
12/// This crate keeps the value locally as a convenience for scalar electromagnetic helpers.
13/// Broader physical constants belong in the top-level `use-constants` set.
14pub const VACUUM_PERMITTIVITY: f64 = 8.854_187_812_8e-12;
15
16/// Vacuum permeability in henries per meter.
17///
18/// This crate keeps the value locally as a convenience for scalar electromagnetic helpers.
19/// Broader physical constants belong in the top-level `use-constants` set.
20pub const VACUUM_PERMEABILITY: f64 = 1.256_637_062_12e-6;
21
22/// Speed of light in vacuum, in meters per second.
23///
24/// This crate keeps the value locally as a convenience for scalar electromagnetic helpers.
25/// Broader physical constants belong in the top-level `use-constants` set.
26pub const SPEED_OF_LIGHT: f64 = 299_792_458.0;
27
28fn is_nonnegative_finite(value: f64) -> bool {
29    value.is_finite() && value >= 0.0
30}
31
32fn is_positive_finite(value: f64) -> bool {
33    value.is_finite() && value > 0.0
34}
35
36fn finite_result(value: f64) -> Option<f64> {
37    value.is_finite().then_some(value)
38}
39
40fn nonnegative_finite_result(value: f64) -> Option<f64> {
41    (value.is_finite() && value >= 0.0).then_some(value)
42}
43
44/// A scalar electric and magnetic field pair.
45#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct ElectromagneticField {
47    pub electric_field: f64,
48    pub magnetic_flux_density: f64,
49}
50
51impl ElectromagneticField {
52    /// Creates a field pair when both scalar components are finite.
53    #[must_use]
54    pub const fn new(electric_field: f64, magnetic_flux_density: f64) -> Option<Self> {
55        if !electric_field.is_finite() || !magnetic_flux_density.is_finite() {
56            return None;
57        }
58
59        Some(Self {
60            electric_field,
61            magnetic_flux_density,
62        })
63    }
64
65    /// Computes electric force using the field's electric component.
66    #[must_use]
67    pub fn electric_force_on_charge(&self, charge: f64) -> Option<f64> {
68        electric_force_on_charge(charge, self.electric_field)
69    }
70
71    /// Computes the scalar Lorentz-force convenience relation for this field pair.
72    #[must_use]
73    pub fn lorentz_force_scalar(
74        &self,
75        charge: f64,
76        velocity: f64,
77        angle_radians: f64,
78    ) -> Option<f64> {
79        lorentz_force_scalar(
80            charge,
81            self.electric_field,
82            velocity,
83            self.magnetic_flux_density,
84            angle_radians,
85        )
86    }
87
88    /// Computes combined electromagnetic energy density for this field pair.
89    ///
90    /// # Examples
91    ///
92    /// ```rust
93    /// use use_electromagnetism::ElectromagneticField;
94    ///
95    /// let field = ElectromagneticField::new(10.0, 2.0).unwrap();
96    ///
97    /// assert!(field.energy_density().unwrap() > 0.0);
98    /// ```
99    #[must_use]
100    pub fn energy_density(&self) -> Option<f64> {
101        electromagnetic_energy_density(self.electric_field, self.magnetic_flux_density)
102    }
103
104    /// Computes Poynting magnitude when the stored field values are used as magnitudes.
105    #[must_use]
106    pub fn poynting_magnitude(&self) -> Option<f64> {
107        poynting_magnitude(self.electric_field, self.magnetic_flux_density)
108    }
109}
110
111/// Computes electric force using `F = qE`.
112///
113/// Returns `None` when either input is not finite or when the computed result is not finite.
114///
115/// # Examples
116///
117/// ```rust
118/// use use_electromagnetism::electric_force_on_charge;
119///
120/// assert_eq!(electric_force_on_charge(2.0, 3.0), Some(6.0));
121/// assert_eq!(electric_force_on_charge(-2.0, 3.0), Some(-6.0));
122/// ```
123#[must_use]
124pub fn electric_force_on_charge(charge: f64, electric_field: f64) -> Option<f64> {
125    if !charge.is_finite() || !electric_field.is_finite() {
126        return None;
127    }
128
129    finite_result(charge * electric_field)
130}
131
132/// Computes magnetic force using `F = qvB sin(theta)`.
133///
134/// Returns `None` when any input is not finite or when the computed result is not finite.
135#[must_use]
136pub fn magnetic_force_on_moving_charge(
137    charge: f64,
138    velocity: f64,
139    magnetic_flux_density: f64,
140    angle_radians: f64,
141) -> Option<f64> {
142    if !charge.is_finite()
143        || !velocity.is_finite()
144        || !magnetic_flux_density.is_finite()
145        || !angle_radians.is_finite()
146    {
147        return None;
148    }
149
150    finite_result(charge * velocity * magnetic_flux_density * angle_radians.sin())
151}
152
153/// Computes magnetic force using `F = qvB sin(theta)` with the angle in degrees.
154#[must_use]
155pub fn magnetic_force_on_moving_charge_degrees(
156    charge: f64,
157    velocity: f64,
158    magnetic_flux_density: f64,
159    angle_degrees: f64,
160) -> Option<f64> {
161    magnetic_force_on_moving_charge(
162        charge,
163        velocity,
164        magnetic_flux_density,
165        angle_degrees.to_radians(),
166    )
167}
168
169/// Computes the scalar Lorentz-force convenience relation `F = q(E + vB sin(theta))`.
170///
171/// This helper is scalar-only and does not model the full vector Lorentz force.
172///
173/// Returns `None` when any input is not finite or when the computed result is not finite.
174///
175/// # Examples
176///
177/// ```rust
178/// use use_electromagnetism::lorentz_force_scalar;
179///
180/// assert_eq!(
181///     lorentz_force_scalar(1.0, 10.0, 2.0, 3.0, core::f64::consts::FRAC_PI_2),
182///     Some(16.0)
183/// );
184/// ```
185#[must_use]
186pub fn lorentz_force_scalar(
187    charge: f64,
188    electric_field: f64,
189    velocity: f64,
190    magnetic_flux_density: f64,
191    angle_radians: f64,
192) -> Option<f64> {
193    if !charge.is_finite()
194        || !electric_field.is_finite()
195        || !velocity.is_finite()
196        || !magnetic_flux_density.is_finite()
197        || !angle_radians.is_finite()
198    {
199        return None;
200    }
201
202    let magnetic_term = velocity * magnetic_flux_density * angle_radians.sin();
203    finite_result(charge * (electric_field + magnetic_term))
204}
205
206/// Computes the scalar Lorentz-force convenience relation with the angle in degrees.
207#[must_use]
208pub fn lorentz_force_scalar_degrees(
209    charge: f64,
210    electric_field: f64,
211    velocity: f64,
212    magnetic_flux_density: f64,
213    angle_degrees: f64,
214) -> Option<f64> {
215    lorentz_force_scalar(
216        charge,
217        electric_field,
218        velocity,
219        magnetic_flux_density,
220        angle_degrees.to_radians(),
221    )
222}
223
224/// Computes `|F| = |q| * |E + vB|` for perpendicular fields along the same scalar direction.
225///
226/// Returns `None` when `speed` is negative, when either field magnitude is negative, when any
227/// input is not finite, or when the computed result is not finite.
228#[must_use]
229pub fn lorentz_force_magnitude_perpendicular(
230    charge: f64,
231    electric_field_magnitude: f64,
232    speed: f64,
233    magnetic_flux_density_magnitude: f64,
234) -> Option<f64> {
235    if !charge.is_finite()
236        || !is_nonnegative_finite(electric_field_magnitude)
237        || !is_nonnegative_finite(speed)
238        || !is_nonnegative_finite(magnetic_flux_density_magnitude)
239    {
240        return None;
241    }
242
243    let combined_term = speed.mul_add(magnetic_flux_density_magnitude, electric_field_magnitude);
244    nonnegative_finite_result(charge.abs() * combined_term.abs())
245}
246
247/// Computes selector speed using `v = E / B`.
248///
249/// Inputs are treated as magnitudes.
250///
251/// # Examples
252///
253/// ```rust
254/// use use_electromagnetism::velocity_selector_speed;
255///
256/// assert_eq!(velocity_selector_speed(20.0, 4.0), Some(5.0));
257/// assert_eq!(velocity_selector_speed(20.0, 0.0), None);
258/// ```
259#[must_use]
260pub fn velocity_selector_speed(electric_field: f64, magnetic_flux_density: f64) -> Option<f64> {
261    if !is_nonnegative_finite(electric_field) || !is_positive_finite(magnetic_flux_density) {
262        return None;
263    }
264
265    nonnegative_finite_result(electric_field / magnetic_flux_density)
266}
267
268/// Computes electric field magnitude for a selector using `E = vB`.
269#[must_use]
270pub fn electric_field_for_velocity_selector(speed: f64, magnetic_flux_density: f64) -> Option<f64> {
271    if !is_nonnegative_finite(speed) || !is_nonnegative_finite(magnetic_flux_density) {
272        return None;
273    }
274
275    nonnegative_finite_result(speed * magnetic_flux_density)
276}
277
278/// Computes magnetic flux density magnitude for a selector using `B = E / v`.
279#[must_use]
280pub fn magnetic_flux_density_for_velocity_selector(electric_field: f64, speed: f64) -> Option<f64> {
281    if !is_nonnegative_finite(electric_field) || !is_positive_finite(speed) {
282        return None;
283    }
284
285    nonnegative_finite_result(electric_field / speed)
286}
287
288/// Computes cyclotron radius using `r = mv / (|q|B)`.
289///
290/// # Examples
291///
292/// ```rust
293/// use use_electromagnetism::cyclotron_radius;
294///
295/// assert_eq!(cyclotron_radius(2.0, 10.0, 1.0, 5.0), Some(4.0));
296/// ```
297#[must_use]
298pub fn cyclotron_radius(
299    mass: f64,
300    speed: f64,
301    charge: f64,
302    magnetic_flux_density: f64,
303) -> Option<f64> {
304    if !is_nonnegative_finite(mass)
305        || !is_nonnegative_finite(speed)
306        || !charge.is_finite()
307        || charge == 0.0
308        || !is_positive_finite(magnetic_flux_density)
309    {
310        return None;
311    }
312
313    nonnegative_finite_result(mass * speed / (charge.abs() * magnetic_flux_density))
314}
315
316/// Computes cyclotron angular frequency using `ω = |q|B / m`.
317#[must_use]
318pub fn cyclotron_angular_frequency(
319    charge: f64,
320    magnetic_flux_density: f64,
321    mass: f64,
322) -> Option<f64> {
323    if !charge.is_finite()
324        || charge == 0.0
325        || !is_nonnegative_finite(magnetic_flux_density)
326        || !is_positive_finite(mass)
327    {
328        return None;
329    }
330
331    nonnegative_finite_result(charge.abs() * magnetic_flux_density / mass)
332}
333
334/// Computes cyclotron frequency in cycles per second using `f = |q|B / (2πm)`.
335#[must_use]
336pub fn cyclotron_frequency(charge: f64, magnetic_flux_density: f64, mass: f64) -> Option<f64> {
337    nonnegative_finite_result(
338        cyclotron_angular_frequency(charge, magnetic_flux_density, mass)? / TAU,
339    )
340}
341
342/// Computes electric field energy density using `u_E = 0.5 * ε0 * E²`.
343#[must_use]
344pub fn electric_field_energy_density(electric_field: f64) -> Option<f64> {
345    if !electric_field.is_finite() {
346        return None;
347    }
348
349    nonnegative_finite_result(0.5 * VACUUM_PERMITTIVITY * electric_field * electric_field)
350}
351
352/// Computes magnetic field energy density using `u_B = B² / (2μ0)`.
353#[must_use]
354pub fn magnetic_field_energy_density(magnetic_flux_density: f64) -> Option<f64> {
355    if !magnetic_flux_density.is_finite() {
356        return None;
357    }
358
359    nonnegative_finite_result(
360        magnetic_flux_density * magnetic_flux_density / (2.0 * VACUUM_PERMEABILITY),
361    )
362}
363
364/// Computes combined electromagnetic energy density.
365///
366/// # Examples
367///
368/// ```rust
369/// use use_electromagnetism::electromagnetic_energy_density;
370///
371/// assert!(electromagnetic_energy_density(10.0, 2.0).unwrap() > 0.0);
372/// ```
373#[must_use]
374pub fn electromagnetic_energy_density(
375    electric_field: f64,
376    magnetic_flux_density: f64,
377) -> Option<f64> {
378    let electric_density = electric_field_energy_density(electric_field)?;
379    let magnetic_density = magnetic_field_energy_density(magnetic_flux_density)?;
380
381    nonnegative_finite_result(electric_density + magnetic_density)
382}
383
384/// Computes Poynting magnitude in vacuum using `S = EB / μ0`.
385///
386/// Inputs are treated as magnitudes.
387///
388/// # Examples
389///
390/// ```rust
391/// use use_electromagnetism::poynting_magnitude;
392///
393/// assert!(poynting_magnitude(10.0, 2.0).unwrap() > 0.0);
394/// assert_eq!(poynting_magnitude(-10.0, 2.0), None);
395/// ```
396#[must_use]
397pub fn poynting_magnitude(electric_field: f64, magnetic_flux_density: f64) -> Option<f64> {
398    if !is_nonnegative_finite(electric_field) || !is_nonnegative_finite(magnetic_flux_density) {
399        return None;
400    }
401
402    nonnegative_finite_result(electric_field * magnetic_flux_density / VACUUM_PERMEABILITY)
403}
404
405/// Computes magnetic flux density magnitude in vacuum using `B = E / c`.
406///
407/// # Examples
408///
409/// ```rust
410/// use use_electromagnetism::{SPEED_OF_LIGHT, magnetic_flux_density_from_electric_field_in_vacuum};
411///
412/// assert_eq!(
413///     magnetic_flux_density_from_electric_field_in_vacuum(SPEED_OF_LIGHT),
414///     Some(1.0)
415/// );
416/// ```
417#[must_use]
418pub fn magnetic_flux_density_from_electric_field_in_vacuum(electric_field: f64) -> Option<f64> {
419    if !is_nonnegative_finite(electric_field) {
420        return None;
421    }
422
423    nonnegative_finite_result(electric_field / SPEED_OF_LIGHT)
424}
425
426/// Computes electric field magnitude in vacuum using `E = cB`.
427#[must_use]
428pub fn electric_field_from_magnetic_flux_density_in_vacuum(
429    magnetic_flux_density: f64,
430) -> Option<f64> {
431    if !is_nonnegative_finite(magnetic_flux_density) {
432        return None;
433    }
434
435    nonnegative_finite_result(SPEED_OF_LIGHT * magnetic_flux_density)
436}
437
438/// Computes propagation speed from permittivity and permeability using `v = 1 / sqrt(εμ)`.
439#[must_use]
440pub fn speed_from_permittivity_permeability(permittivity: f64, permeability: f64) -> Option<f64> {
441    if !is_positive_finite(permittivity) || !is_positive_finite(permeability) {
442        return None;
443    }
444
445    let product = permittivity * permeability;
446    if !is_positive_finite(product) {
447        return None;
448    }
449
450    nonnegative_finite_result(product.sqrt().recip())
451}
452
453#[cfg(test)]
454#[allow(clippy::float_cmp)]
455mod tests {
456    use super::*;
457
458    fn approx_eq(left: f64, right: f64) -> bool {
459        let scale = left.abs().max(right.abs()).max(1.0);
460        (left - right).abs() <= 1.0e-9 * scale
461    }
462
463    #[test]
464    fn electric_force_helpers_cover_sign() {
465        assert_eq!(electric_force_on_charge(2.0, 3.0), Some(6.0));
466        assert_eq!(electric_force_on_charge(-2.0, 3.0), Some(-6.0));
467    }
468
469    #[test]
470    fn magnetic_force_helpers_cover_radians_and_degrees() {
471        let radians_force =
472            magnetic_force_on_moving_charge(1.0, 2.0, 3.0, core::f64::consts::FRAC_PI_2).unwrap();
473        let degrees_force = magnetic_force_on_moving_charge_degrees(1.0, 2.0, 3.0, 90.0).unwrap();
474
475        assert!(approx_eq(radians_force, 6.0));
476        assert!(approx_eq(degrees_force, 6.0));
477    }
478
479    #[test]
480    fn lorentz_force_helpers_cover_sign_and_magnitude() {
481        let positive_force =
482            lorentz_force_scalar(1.0, 10.0, 2.0, 3.0, core::f64::consts::FRAC_PI_2).unwrap();
483        let degrees_force = lorentz_force_scalar_degrees(1.0, 10.0, 2.0, 3.0, 90.0).unwrap();
484        let negative_force =
485            lorentz_force_scalar(-1.0, 10.0, 2.0, 3.0, core::f64::consts::FRAC_PI_2).unwrap();
486
487        assert!(approx_eq(positive_force, 16.0));
488        assert!(approx_eq(degrees_force, 16.0));
489        assert!(approx_eq(negative_force, -16.0));
490        assert_eq!(
491            lorentz_force_magnitude_perpendicular(1.0, 10.0, 2.0, 3.0),
492            Some(16.0)
493        );
494        assert_eq!(
495            lorentz_force_magnitude_perpendicular(1.0, -10.0, 2.0, 3.0),
496            None
497        );
498        assert_eq!(
499            lorentz_force_magnitude_perpendicular(1.0, 10.0, -2.0, 3.0),
500            None
501        );
502    }
503
504    #[test]
505    fn velocity_selector_helpers_cover_common_relations() {
506        assert_eq!(velocity_selector_speed(20.0, 4.0), Some(5.0));
507        assert_eq!(velocity_selector_speed(20.0, 0.0), None);
508        assert_eq!(electric_field_for_velocity_selector(5.0, 4.0), Some(20.0));
509        assert_eq!(electric_field_for_velocity_selector(-5.0, 4.0), None);
510        assert_eq!(
511            magnetic_flux_density_for_velocity_selector(20.0, 5.0),
512            Some(4.0)
513        );
514        assert_eq!(magnetic_flux_density_for_velocity_selector(20.0, 0.0), None);
515    }
516
517    #[test]
518    fn cyclotron_helpers_cover_radius_and_frequency() {
519        assert_eq!(cyclotron_radius(2.0, 10.0, 1.0, 5.0), Some(4.0));
520        assert_eq!(cyclotron_radius(2.0, 10.0, 0.0, 5.0), None);
521        assert_eq!(cyclotron_radius(2.0, 10.0, 1.0, 0.0), None);
522        assert_eq!(cyclotron_angular_frequency(2.0, 5.0, 10.0), Some(1.0));
523
524        let frequency = cyclotron_frequency(2.0, 5.0, 10.0).unwrap();
525        assert!(approx_eq(frequency, 1.0 / (2.0 * core::f64::consts::PI)));
526    }
527
528    #[test]
529    fn energy_density_helpers_return_positive_results() {
530        let electric_density = electric_field_energy_density(10.0).unwrap();
531        let magnetic_density = magnetic_field_energy_density(2.0).unwrap();
532        let combined_density = electromagnetic_energy_density(10.0, 2.0).unwrap();
533
534        assert!(electric_density.is_finite() && electric_density > 0.0);
535        assert!(magnetic_density.is_finite() && magnetic_density > 0.0);
536        assert!(combined_density.is_finite() && combined_density > 0.0);
537    }
538
539    #[test]
540    fn poynting_magnitude_requires_nonnegative_inputs() {
541        let poynting = poynting_magnitude(10.0, 2.0).unwrap();
542
543        assert!(poynting.is_finite() && poynting > 0.0);
544        assert_eq!(poynting_magnitude(-10.0, 2.0), None);
545    }
546
547    #[test]
548    fn plane_wave_and_speed_relations_cover_vacuum_helpers() {
549        let magnetic_flux_density =
550            magnetic_flux_density_from_electric_field_in_vacuum(SPEED_OF_LIGHT).unwrap();
551        let electric_field = electric_field_from_magnetic_flux_density_in_vacuum(1.0).unwrap();
552        let speed =
553            speed_from_permittivity_permeability(VACUUM_PERMITTIVITY, VACUUM_PERMEABILITY).unwrap();
554
555        assert!(approx_eq(magnetic_flux_density, 1.0));
556        assert!(approx_eq(electric_field, SPEED_OF_LIGHT));
557        assert!(approx_eq(speed, SPEED_OF_LIGHT));
558        assert_eq!(
559            speed_from_permittivity_permeability(0.0, VACUUM_PERMEABILITY),
560            None
561        );
562    }
563
564    #[test]
565    fn electromagnetic_field_methods_delegate_to_free_functions() {
566        let field = ElectromagneticField::new(10.0, 2.0).unwrap();
567        let lorentz_force = field
568            .lorentz_force_scalar(1.0, 2.0, core::f64::consts::FRAC_PI_2)
569            .unwrap();
570        let energy_density = field.energy_density().unwrap();
571
572        assert_eq!(field.electric_force_on_charge(3.0), Some(30.0));
573        assert!(approx_eq(lorentz_force, 14.0));
574        assert!(energy_density.is_finite() && energy_density > 0.0);
575        assert_eq!(ElectromagneticField::new(f64::NAN, 2.0), None);
576    }
577}