Skip to main content

use_magnetism/
lib.rs

1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4//! Magnetism-specific scalar helpers.
5
6use core::f64::consts::TAU;
7
8pub mod prelude;
9
10const TRIG_EPSILON: f64 = 1.0e-12;
11
12/// Vacuum permeability in newtons per ampere squared.
13///
14/// This crate keeps the value locally as a convenience for magnetism-specific
15/// helpers. Broader physical constants belong in the top-level `use-constants`
16/// set.
17pub const VACUUM_PERMEABILITY: f64 = 1.256_637_062_12e-6;
18
19fn all_finite(values: &[f64]) -> bool {
20    values.iter().all(|value| value.is_finite())
21}
22
23fn finite_result(value: f64) -> Option<f64> {
24    value.is_finite().then_some(value)
25}
26
27/// Computes magnetic flux through an area.
28///
29/// Formula: `Φ = B * A * cos(theta)`
30///
31/// Returns `None` when `area` is negative, when any input is not finite, or
32/// when the computed result is not finite.
33///
34/// # Examples
35///
36/// ```
37/// use use_magnetism::magnetic_flux;
38///
39/// assert_eq!(magnetic_flux(2.0, 3.0, 0.0), Some(6.0));
40/// ```
41#[must_use]
42pub fn magnetic_flux(magnetic_flux_density: f64, area: f64, angle_radians: f64) -> Option<f64> {
43    if !all_finite(&[magnetic_flux_density, area, angle_radians]) || area < 0.0 {
44        return None;
45    }
46
47    finite_result(magnetic_flux_density * area * angle_radians.cos())
48}
49
50/// Computes magnetic flux through an area using an angle in degrees.
51#[must_use]
52pub fn magnetic_flux_degrees(
53    magnetic_flux_density: f64,
54    area: f64,
55    angle_degrees: f64,
56) -> Option<f64> {
57    magnetic_flux(magnetic_flux_density, area, angle_degrees.to_radians())
58}
59
60/// Computes magnetic flux density from magnetic flux, area, and orientation.
61///
62/// Formula: `B = Φ / (A * cos(theta))`
63///
64/// Returns `None` when `area` is less than or equal to zero, when
65/// `cos(theta)` is zero or effectively zero, when any input is not finite, or
66/// when the computed result is not finite.
67#[must_use]
68pub fn magnetic_flux_density_from_flux(flux: f64, area: f64, angle_radians: f64) -> Option<f64> {
69    if !all_finite(&[flux, area, angle_radians]) || area <= 0.0 {
70        return None;
71    }
72
73    let angle_factor = angle_radians.cos();
74    if !angle_factor.is_finite() || angle_factor.abs() <= TRIG_EPSILON {
75        return None;
76    }
77
78    finite_result(flux / (area * angle_factor))
79}
80
81/// Computes magnetic force on a moving charge.
82///
83/// Formula: `F = q * v * B * sin(theta)`
84///
85/// The sign is preserved from the scalar inputs and angle convention.
86///
87/// # Examples
88///
89/// ```
90/// use std::f64::consts::FRAC_PI_2;
91///
92/// use use_magnetism::magnetic_force_on_charge;
93///
94/// assert_eq!(magnetic_force_on_charge(1.0, 2.0, 3.0, FRAC_PI_2), Some(6.0));
95/// ```
96#[must_use]
97pub fn magnetic_force_on_charge(
98    charge: f64,
99    velocity: f64,
100    magnetic_flux_density: f64,
101    angle_radians: f64,
102) -> Option<f64> {
103    if !all_finite(&[charge, velocity, magnetic_flux_density, angle_radians]) {
104        return None;
105    }
106
107    finite_result(charge * velocity * magnetic_flux_density * angle_radians.sin())
108}
109
110/// Computes magnetic force on a moving charge using an angle in degrees.
111#[must_use]
112pub fn magnetic_force_on_charge_degrees(
113    charge: f64,
114    velocity: f64,
115    magnetic_flux_density: f64,
116    angle_degrees: f64,
117) -> Option<f64> {
118    magnetic_force_on_charge(
119        charge,
120        velocity,
121        magnetic_flux_density,
122        angle_degrees.to_radians(),
123    )
124}
125
126/// Computes the magnitude of magnetic force on a moving charge.
127///
128/// Formula: `|F| = |q| * speed * |B| * sin(theta)`
129#[must_use]
130pub fn magnetic_force_magnitude_on_charge(
131    charge: f64,
132    speed: f64,
133    magnetic_flux_density: f64,
134    angle_radians: f64,
135) -> Option<f64> {
136    if !all_finite(&[charge, speed, magnetic_flux_density, angle_radians]) || speed < 0.0 {
137        return None;
138    }
139
140    finite_result(charge.abs() * speed * magnetic_flux_density.abs() * angle_radians.sin().abs())
141}
142
143/// Computes magnetic force on a current-carrying wire.
144///
145/// Formula: `F = I * L * B * sin(theta)`
146///
147/// The sign is preserved from the scalar inputs and angle convention.
148///
149/// # Examples
150///
151/// ```
152/// use std::f64::consts::FRAC_PI_2;
153///
154/// use use_magnetism::magnetic_force_on_wire;
155///
156/// assert_eq!(magnetic_force_on_wire(2.0, 3.0, 4.0, FRAC_PI_2), Some(24.0));
157/// ```
158#[must_use]
159pub fn magnetic_force_on_wire(
160    current: f64,
161    length: f64,
162    magnetic_flux_density: f64,
163    angle_radians: f64,
164) -> Option<f64> {
165    if !all_finite(&[current, length, magnetic_flux_density, angle_radians]) || length < 0.0 {
166        return None;
167    }
168
169    finite_result(current * length * magnetic_flux_density * angle_radians.sin())
170}
171
172/// Computes magnetic force on a current-carrying wire using an angle in degrees.
173#[must_use]
174pub fn magnetic_force_on_wire_degrees(
175    current: f64,
176    length: f64,
177    magnetic_flux_density: f64,
178    angle_degrees: f64,
179) -> Option<f64> {
180    magnetic_force_on_wire(
181        current,
182        length,
183        magnetic_flux_density,
184        angle_degrees.to_radians(),
185    )
186}
187
188/// Computes magnetic flux density around a long straight wire.
189///
190/// Formula: `B = μ0 * I / (2πr)`
191///
192/// # Examples
193///
194/// ```
195/// use use_magnetism::magnetic_field_around_long_straight_wire;
196///
197/// let field = magnetic_field_around_long_straight_wire(10.0, 0.5).unwrap();
198/// assert!(field.is_sign_positive());
199/// ```
200#[must_use]
201pub fn magnetic_field_around_long_straight_wire(current: f64, distance: f64) -> Option<f64> {
202    if !all_finite(&[current, distance]) || distance <= 0.0 {
203        return None;
204    }
205
206    finite_result(VACUUM_PERMEABILITY * current / (TAU * distance))
207}
208
209/// Computes magnetic flux density inside an ideal long solenoid.
210///
211/// Formula: `B = μ0 * (N / L) * I`
212///
213/// # Examples
214///
215/// ```
216/// use use_magnetism::magnetic_field_inside_solenoid;
217///
218/// let field = magnetic_field_inside_solenoid(1_000.0, 2.0, 0.5).unwrap();
219/// assert!(field.is_sign_positive());
220/// ```
221#[must_use]
222pub fn magnetic_field_inside_solenoid(turns: f64, current: f64, length: f64) -> Option<f64> {
223    if !all_finite(&[turns, current, length]) || turns < 0.0 || length <= 0.0 {
224        return None;
225    }
226
227    finite_result(VACUUM_PERMEABILITY * (turns / length) * current)
228}
229
230/// Computes magnetic flux density at the center of a circular current loop.
231///
232/// Formula: `B = μ0 * I / (2r)`
233#[must_use]
234pub fn magnetic_field_at_center_of_loop(current: f64, radius: f64) -> Option<f64> {
235    if !all_finite(&[current, radius]) || radius <= 0.0 {
236        return None;
237    }
238
239    finite_result(VACUUM_PERMEABILITY * current / (2.0 * radius))
240}
241
242/// Computes magnetic energy density.
243///
244/// Formula: `u = B² / (2μ0)`
245///
246/// # Examples
247///
248/// ```
249/// use use_magnetism::magnetic_energy_density;
250///
251/// let energy_density = magnetic_energy_density(2.0).unwrap();
252/// assert!(energy_density.is_sign_positive());
253/// ```
254#[must_use]
255pub fn magnetic_energy_density(magnetic_flux_density: f64) -> Option<f64> {
256    if !magnetic_flux_density.is_finite() {
257        return None;
258    }
259
260    finite_result((magnetic_flux_density * magnetic_flux_density) / (2.0 * VACUUM_PERMEABILITY))
261}
262
263/// Computes magnetic pressure.
264///
265/// Magnetic pressure and magnetic energy density share the same numeric
266/// expression in SI units.
267#[must_use]
268pub fn magnetic_pressure(magnetic_flux_density: f64) -> Option<f64> {
269    magnetic_energy_density(magnetic_flux_density)
270}
271
272/// A simple magnetic field described by flux density.
273#[derive(Debug, Clone, Copy, PartialEq)]
274pub struct MagneticField {
275    pub flux_density: f64,
276}
277
278impl MagneticField {
279    /// Creates a new magnetic field from flux density.
280    #[must_use]
281    pub fn new(flux_density: f64) -> Option<Self> {
282        flux_density.is_finite().then_some(Self { flux_density })
283    }
284
285    /// Computes the magnetic flux through an area in this field.
286    #[must_use]
287    pub fn flux_through_area(&self, area: f64, angle_radians: f64) -> Option<f64> {
288        magnetic_flux(self.flux_density, area, angle_radians)
289    }
290
291    /// Computes the magnetic force on a moving charge in this field.
292    ///
293    /// # Examples
294    ///
295    /// ```
296    /// use std::f64::consts::FRAC_PI_2;
297    ///
298    /// use use_magnetism::MagneticField;
299    ///
300    /// let field = MagneticField::new(3.0).unwrap();
301    /// assert_eq!(field.force_on_charge(1.0, 2.0, FRAC_PI_2), Some(6.0));
302    /// ```
303    #[must_use]
304    pub fn force_on_charge(&self, charge: f64, velocity: f64, angle_radians: f64) -> Option<f64> {
305        magnetic_force_on_charge(charge, velocity, self.flux_density, angle_radians)
306    }
307
308    /// Computes the magnetic force on a current-carrying wire in this field.
309    #[must_use]
310    pub fn force_on_wire(&self, current: f64, length: f64, angle_radians: f64) -> Option<f64> {
311        magnetic_force_on_wire(current, length, self.flux_density, angle_radians)
312    }
313
314    /// Computes the magnetic energy density for this field.
315    #[must_use]
316    pub fn energy_density(&self) -> Option<f64> {
317        magnetic_energy_density(self.flux_density)
318    }
319}
320
321#[cfg(test)]
322#[allow(clippy::float_cmp)]
323mod tests {
324    use core::f64::consts::FRAC_PI_2;
325
326    use super::{
327        MagneticField, magnetic_energy_density, magnetic_field_around_long_straight_wire,
328        magnetic_field_at_center_of_loop, magnetic_field_inside_solenoid, magnetic_flux,
329        magnetic_flux_degrees, magnetic_flux_density_from_flux, magnetic_force_magnitude_on_charge,
330        magnetic_force_on_charge, magnetic_force_on_charge_degrees, magnetic_force_on_wire,
331        magnetic_force_on_wire_degrees, magnetic_pressure,
332    };
333
334    const EPSILON: f64 = 1.0e-12;
335
336    fn assert_close(actual: f64, expected: f64) {
337        let scale = actual.abs().max(expected.abs()).max(1.0);
338        let delta = (actual - expected).abs();
339
340        assert!(
341            delta <= EPSILON * scale,
342            "actual={actual} expected={expected} delta={delta} tolerance={}",
343            EPSILON * scale
344        );
345    }
346
347    fn assert_some_close(actual: Option<f64>, expected: f64) {
348        assert_close(actual.expect("expected Some value"), expected);
349    }
350
351    #[test]
352    fn magnetic_flux_handles_requested_cases() {
353        assert_eq!(magnetic_flux(2.0, 3.0, 0.0), Some(6.0));
354        assert_some_close(magnetic_flux(2.0, 3.0, FRAC_PI_2), 0.0);
355        assert_eq!(magnetic_flux(2.0, -3.0, 0.0), None);
356        assert_some_close(magnetic_flux_degrees(2.0, 3.0, 60.0), 3.0);
357    }
358
359    #[test]
360    fn magnetic_flux_density_requires_valid_geometry() {
361        assert_eq!(magnetic_flux_density_from_flux(6.0, 3.0, 0.0), Some(2.0));
362        assert_eq!(magnetic_flux_density_from_flux(6.0, 0.0, 0.0), None);
363        assert_eq!(magnetic_flux_density_from_flux(6.0, 3.0, FRAC_PI_2), None);
364    }
365
366    #[test]
367    fn magnetic_force_on_charge_handles_sign_and_units() {
368        assert_some_close(magnetic_force_on_charge(1.0, 2.0, 3.0, FRAC_PI_2), 6.0);
369        assert_some_close(magnetic_force_on_charge(-1.0, 2.0, 3.0, FRAC_PI_2), -6.0);
370        assert_some_close(magnetic_force_on_charge_degrees(1.0, 2.0, 3.0, 90.0), 6.0);
371    }
372
373    #[test]
374    fn magnetic_force_magnitude_requires_non_negative_speed() {
375        assert_some_close(
376            magnetic_force_magnitude_on_charge(-1.0, 2.0, -3.0, FRAC_PI_2),
377            6.0,
378        );
379        assert_eq!(
380            magnetic_force_magnitude_on_charge(1.0, -2.0, 3.0, FRAC_PI_2),
381            None
382        );
383    }
384
385    #[test]
386    fn magnetic_force_on_wire_handles_requested_cases() {
387        assert_some_close(magnetic_force_on_wire(2.0, 3.0, 4.0, FRAC_PI_2), 24.0);
388        assert_some_close(magnetic_force_on_wire_degrees(2.0, 3.0, 4.0, 90.0), 24.0);
389        assert_eq!(magnetic_force_on_wire(2.0, -3.0, 4.0, FRAC_PI_2), None);
390    }
391
392    #[test]
393    fn magnetic_field_helpers_require_positive_lengths() {
394        let wire_field = magnetic_field_around_long_straight_wire(10.0, 0.5)
395            .expect("expected finite wire field");
396        assert!(wire_field.is_finite());
397        assert!(wire_field > 0.0);
398        assert_eq!(magnetic_field_around_long_straight_wire(10.0, 0.0), None);
399
400        let solenoid_field =
401            magnetic_field_inside_solenoid(1_000.0, 2.0, 0.5).expect("expected finite solenoid");
402        assert!(solenoid_field.is_finite());
403        assert!(solenoid_field > 0.0);
404        assert_eq!(magnetic_field_inside_solenoid(-1_000.0, 2.0, 0.5), None);
405        assert_eq!(magnetic_field_inside_solenoid(1_000.0, 2.0, 0.0), None);
406
407        let loop_field =
408            magnetic_field_at_center_of_loop(10.0, 0.5).expect("expected finite loop field");
409        assert!(loop_field.is_finite());
410        assert!(loop_field > 0.0);
411        assert_eq!(magnetic_field_at_center_of_loop(10.0, 0.0), None);
412    }
413
414    #[test]
415    fn magnetic_pressure_matches_energy_density() {
416        let energy_density = magnetic_energy_density(2.0).expect("expected finite energy density");
417        assert!(energy_density.is_finite());
418        assert!(energy_density > 0.0);
419        assert_eq!(magnetic_pressure(2.0), magnetic_energy_density(2.0));
420    }
421
422    #[test]
423    fn magnetic_field_struct_delegates_to_free_functions() {
424        let field = MagneticField::new(3.0).expect("valid field");
425
426        assert_eq!(field.flux_through_area(2.0, 0.0), Some(6.0));
427        assert_some_close(field.force_on_charge(1.0, 2.0, FRAC_PI_2), 6.0);
428        assert_eq!(MagneticField::new(f64::NAN), None);
429    }
430}