1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::TAU;
7
8pub mod prelude;
9
10const TRIG_EPSILON: f64 = 1.0e-12;
11
12pub 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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[must_use]
268pub fn magnetic_pressure(magnetic_flux_density: f64) -> Option<f64> {
269 magnetic_energy_density(magnetic_flux_density)
270}
271
272#[derive(Debug, Clone, Copy, PartialEq)]
274pub struct MagneticField {
275 pub flux_density: f64,
276}
277
278impl MagneticField {
279 #[must_use]
281 pub fn new(flux_density: f64) -> Option<Self> {
282 flux_density.is_finite().then_some(Self { flux_density })
283 }
284
285 #[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 #[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 #[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 #[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}