Skip to main content

alice_physics/
electromagnetic.rs

1//! Lorentz-force electromagnetics for charged rigid bodies.
2//!
3//! Complements [`crate::force::ForceField::Vortex`] and gravity-well
4//! fields with the canonical electromagnetic force
5//!
6//! ```text
7//! F = q · (E + v × B)
8//! ```
9//!
10//! and provides uniform-field / dipole-field / single-charge Coulomb
11//! sources sufficient for gameplay-scale magnetics, mag-lev
12//! demonstrations, and induction-motor toy models. Full Maxwell
13//! equations, magnetic-hysteresis, and Faraday-induced EMF closures
14//! are out of scope.
15
16use crate::math::{Fix128, Vec3Fix};
17use crate::solver::RigidBody;
18
19/// Charged rigid body decorator.
20#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub struct ChargedBody {
22    /// Underlying rigid-body index in `PhysicsWorld`.
23    pub body: usize,
24    /// Charge in coulombs.
25    pub charge_c: Fix128,
26}
27
28impl ChargedBody {
29    /// Construct with a positive or negative charge.
30    #[must_use]
31    pub const fn new(body: usize, charge_c: Fix128) -> Self {
32        Self { body, charge_c }
33    }
34}
35
36/// A source of electric / magnetic field.
37#[derive(Debug, Clone, Copy)]
38pub enum EmSource {
39    /// Uniform vector electric or magnetic field.
40    Uniform {
41        /// Uniform electric field `E` (V/m).
42        electric: Vec3Fix,
43        /// Uniform magnetic field `B` (T).
44        magnetic: Vec3Fix,
45    },
46    /// Point charge at `position` with strength `charge_c`, producing
47    /// an inverse-square Coulomb field `E = k · q / r² · r̂` with
48    /// `k = 8.99e9 V·m/C`.
49    PointCharge {
50        /// World-space position of the source.
51        position: Vec3Fix,
52        /// Charge in coulombs.
53        charge_c: Fix128,
54    },
55    /// Ideal magnetic dipole with moment `m` at `position`. Produces
56    /// `B = μ₀ / (4π) · (3(m·r̂)r̂ − m) / r³` with
57    /// `μ₀ / (4π) ≈ 1e-7 T·m/A`.
58    MagneticDipole {
59        /// World-space position of the dipole.
60        position: Vec3Fix,
61        /// Dipole moment vector `m` (A·m²).
62        moment: Vec3Fix,
63    },
64}
65
66impl EmSource {
67    /// Sample `(E, B)` at a world-space point.
68    #[must_use]
69    pub fn sample(&self, point: Vec3Fix) -> (Vec3Fix, Vec3Fix) {
70        match *self {
71            Self::Uniform { electric, magnetic } => (electric, magnetic),
72            Self::PointCharge { position, charge_c } => {
73                let r = point - position;
74                let r_sq = r.length_squared();
75                if r_sq <= Fix128::ZERO {
76                    return (Vec3Fix::ZERO, Vec3Fix::ZERO);
77                }
78                // Coulomb constant k ≈ 8.99e9 V·m/C.
79                let k = Fix128::from_int(8_990_000_000);
80                let r_mag = r_sq.sqrt();
81                let scale = k * charge_c / (r_sq * r_mag);
82                (r * scale, Vec3Fix::ZERO)
83            }
84            Self::MagneticDipole { position, moment } => {
85                let r = point - position;
86                let r_sq = r.length_squared();
87                if r_sq <= Fix128::ZERO {
88                    return (Vec3Fix::ZERO, Vec3Fix::ZERO);
89                }
90                let r_mag = r_sq.sqrt();
91                let r_hat = r * (Fix128::ONE / r_mag);
92                let m_dot_r = moment.dot(r_hat);
93                let inv_r3 = Fix128::ONE / (r_sq * r_mag);
94                // μ₀ / (4π) = 1e-7 T·m/A.
95                let mu0_over_4pi = Fix128::from_ratio(1, 10_000_000);
96                let three = Fix128::from_int(3);
97                let b = (r_hat * (three * m_dot_r) - moment) * (mu0_over_4pi * inv_r3);
98                (Vec3Fix::ZERO, b)
99            }
100        }
101    }
102}
103
104/// Lorentz force `q(E + v × B)` on a charged body from a single source.
105#[must_use]
106pub fn lorentz_force(charged: ChargedBody, body: &RigidBody, source: &EmSource) -> Vec3Fix {
107    let (e_field, b_field) = source.sample(body.position);
108    let v_cross_b = body.velocity.cross(b_field);
109    (e_field + v_cross_b) * charged.charge_c
110}
111
112/// Aggregate Lorentz force from a slice of sources.
113#[must_use]
114pub fn lorentz_force_sum(charged: ChargedBody, body: &RigidBody, sources: &[EmSource]) -> Vec3Fix {
115    sources.iter().fold(Vec3Fix::ZERO, |acc, s| {
116        acc + lorentz_force(charged, body, s)
117    })
118}
119
120#[cfg(test)]
121mod tests {
122    use super::*;
123    use crate::math::QuatFix;
124    use crate::solver::{BodyType, RigidBody};
125
126    fn body_at(x: i64, y: i64, z: i64, vx: i64, vy: i64, vz: i64) -> RigidBody {
127        RigidBody {
128            position: Vec3Fix::new(
129                Fix128::from_int(x),
130                Fix128::from_int(y),
131                Fix128::from_int(z),
132            ),
133            velocity: Vec3Fix::new(
134                Fix128::from_int(vx),
135                Fix128::from_int(vy),
136                Fix128::from_int(vz),
137            ),
138            inv_mass: Fix128::ONE,
139            inv_inertia: Vec3Fix::ZERO,
140            prev_position: Vec3Fix::ZERO,
141            rotation: QuatFix::IDENTITY,
142            angular_velocity: Vec3Fix::ZERO,
143            prev_rotation: QuatFix::IDENTITY,
144            restitution: Fix128::ZERO,
145            friction: Fix128::ZERO,
146            gravity_scale: Fix128::ONE,
147            linear_damping: Fix128::ONE,
148            angular_damping: Fix128::ONE,
149            is_sensor: false,
150            body_type: BodyType::Dynamic,
151            kinematic_target: None,
152        }
153    }
154
155    #[test]
156    fn uniform_electric_field_pushes_positive_charge() {
157        let source = EmSource::Uniform {
158            electric: Vec3Fix::new(Fix128::from_int(10), Fix128::ZERO, Fix128::ZERO),
159            magnetic: Vec3Fix::ZERO,
160        };
161        let charged = ChargedBody::new(0, Fix128::ONE);
162        let body = body_at(0, 0, 0, 0, 0, 0);
163        let f = lorentz_force(charged, &body, &source);
164        assert!(f.x > Fix128::ZERO);
165        assert_eq!(f.y, Fix128::ZERO);
166        assert_eq!(f.z, Fix128::ZERO);
167    }
168
169    #[test]
170    fn uniform_electric_field_pulls_negative_charge() {
171        let source = EmSource::Uniform {
172            electric: Vec3Fix::new(Fix128::from_int(10), Fix128::ZERO, Fix128::ZERO),
173            magnetic: Vec3Fix::ZERO,
174        };
175        let charged = ChargedBody::new(0, -Fix128::ONE);
176        let body = body_at(0, 0, 0, 0, 0, 0);
177        let f = lorentz_force(charged, &body, &source);
178        assert!(f.x < Fix128::ZERO);
179    }
180
181    #[test]
182    fn magnetic_field_produces_perpendicular_force() {
183        // B = +z, v = +x → v×B = +x × +z = -y.
184        let source = EmSource::Uniform {
185            electric: Vec3Fix::ZERO,
186            magnetic: Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE),
187        };
188        let charged = ChargedBody::new(0, Fix128::ONE);
189        let body = body_at(0, 0, 0, 1, 0, 0);
190        let f = lorentz_force(charged, &body, &source);
191        assert!(f.y < Fix128::ZERO);
192        assert_eq!(f.x, Fix128::ZERO);
193        assert_eq!(f.z, Fix128::ZERO);
194    }
195
196    #[test]
197    fn stationary_body_feels_no_magnetic_force() {
198        let source = EmSource::Uniform {
199            electric: Vec3Fix::ZERO,
200            magnetic: Vec3Fix::new(Fix128::ONE, Fix128::ZERO, Fix128::ZERO),
201        };
202        let charged = ChargedBody::new(0, Fix128::ONE);
203        let body = body_at(0, 0, 0, 0, 0, 0);
204        let f = lorentz_force(charged, &body, &source);
205        assert_eq!(f, Vec3Fix::ZERO);
206    }
207
208    #[test]
209    fn point_charge_produces_radial_field() {
210        let source = EmSource::PointCharge {
211            position: Vec3Fix::ZERO,
212            charge_c: Fix128::from_ratio(1, 1000),
213        };
214        let (e, b) = source.sample(Vec3Fix::new(
215            Fix128::from_int(1),
216            Fix128::ZERO,
217            Fix128::ZERO,
218        ));
219        assert!(e.x > Fix128::ZERO);
220        assert_eq!(e.y, Fix128::ZERO);
221        assert_eq!(b, Vec3Fix::ZERO);
222    }
223
224    #[test]
225    fn magnetic_dipole_axis_alignment() {
226        let source = EmSource::MagneticDipole {
227            position: Vec3Fix::ZERO,
228            moment: Vec3Fix::new(Fix128::ZERO, Fix128::ZERO, Fix128::ONE),
229        };
230        // Along the dipole axis (+z), B is aligned with the moment.
231        let (_e, b) = source.sample(Vec3Fix::new(
232            Fix128::ZERO,
233            Fix128::ZERO,
234            Fix128::from_int(1),
235        ));
236        assert!(b.z > Fix128::ZERO);
237    }
238
239    #[test]
240    fn lorentz_force_sum_accumulates_multiple_sources() {
241        let s1 = EmSource::Uniform {
242            electric: Vec3Fix::new(Fix128::from_int(1), Fix128::ZERO, Fix128::ZERO),
243            magnetic: Vec3Fix::ZERO,
244        };
245        let s2 = EmSource::Uniform {
246            electric: Vec3Fix::new(Fix128::from_int(2), Fix128::ZERO, Fix128::ZERO),
247            magnetic: Vec3Fix::ZERO,
248        };
249        let charged = ChargedBody::new(0, Fix128::ONE);
250        let body = body_at(0, 0, 0, 0, 0, 0);
251        let f = lorentz_force_sum(charged, &body, &[s1, s2]);
252        assert_eq!(f.x, Fix128::from_int(3));
253    }
254}