1#![forbid(unsafe_code)]
2#![doc = include_str!("../README.md")]
3
4use core::f64::consts::TAU;
7
8pub mod prelude;
9
10pub const VACUUM_PERMITTIVITY: f64 = 8.854_187_812_8e-12;
15
16pub const VACUUM_PERMEABILITY: f64 = 1.256_637_062_12e-6;
21
22pub 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#[derive(Debug, Clone, Copy, PartialEq)]
46pub struct ElectromagneticField {
47 pub electric_field: f64,
48 pub magnetic_flux_density: f64,
49}
50
51impl ElectromagneticField {
52 #[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 #[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 #[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 #[must_use]
100 pub fn energy_density(&self) -> Option<f64> {
101 electromagnetic_energy_density(self.electric_field, self.magnetic_flux_density)
102 }
103
104 #[must_use]
106 pub fn poynting_magnitude(&self) -> Option<f64> {
107 poynting_magnitude(self.electric_field, self.magnetic_flux_density)
108 }
109}
110
111#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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#[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}