1use glam::{Vec3, Vec4};
5use std::f32::consts::PI;
6
7const MU0_OVER_4PI: f32 = 1.0;
9
10#[derive(Clone, Debug)]
14pub struct CurrentSegment {
15 pub start: Vec3,
16 pub end: Vec3,
17 pub current: f32,
18}
19
20impl CurrentSegment {
21 pub fn new(start: Vec3, end: Vec3, current: f32) -> Self {
22 Self { start, end, current }
23 }
24}
25
26pub fn biot_savart(segment: &CurrentSegment, point: Vec3) -> Vec3 {
30 let dl = segment.end - segment.start;
31 let length = dl.length();
32 if length < 1e-10 {
33 return Vec3::ZERO;
34 }
35
36 let n = 20;
38 let mut b = Vec3::ZERO;
39 let dl_step = dl / n as f32;
40 let dl_mag = dl_step.length();
41
42 for i in 0..n {
43 let t = (i as f32 + 0.5) / n as f32;
44 let src = segment.start + dl * t;
45 let r_vec = point - src;
46 let r2 = r_vec.length_squared();
47 if r2 < 1e-10 {
48 continue;
49 }
50 let r = r2.sqrt();
51 let r_hat = r_vec / r;
52
53 let cross = dl_step.cross(r_hat);
55 b += MU0_OVER_4PI * segment.current * cross / r2;
56 }
57
58 b
59}
60
61pub fn magnetic_field_at(segments: &[CurrentSegment], pos: Vec3) -> Vec3 {
63 let mut field = Vec3::ZERO;
64 for seg in segments {
65 field += biot_savart(seg, pos);
66 }
67 field
68}
69
70#[derive(Clone, Debug)]
74pub struct InfiniteWire {
75 pub position: Vec3, pub direction: Vec3, pub current: f32,
78}
79
80impl InfiniteWire {
81 pub fn new(position: Vec3, direction: Vec3, current: f32) -> Self {
82 Self {
83 position,
84 direction: direction.normalize(),
85 current,
86 }
87 }
88
89 pub fn field_at(&self, point: Vec3) -> Vec3 {
91 let to_point = point - self.position;
92 let parallel = to_point.dot(self.direction) * self.direction;
94 let perp = to_point - parallel;
95 let r = perp.length();
96 if r < 1e-10 {
97 return Vec3::ZERO;
98 }
99 let r_hat = perp / r;
101 let b_dir = self.direction.cross(r_hat);
102 let b_mag = 2.0 * MU0_OVER_4PI * self.current / r;
104 b_dir * b_mag
105 }
106}
107
108#[derive(Clone, Debug)]
112pub struct CircularLoop {
113 pub center: Vec3,
114 pub normal: Vec3, pub radius: f32,
116 pub current: f32,
117}
118
119impl CircularLoop {
120 pub fn new(center: Vec3, normal: Vec3, radius: f32, current: f32) -> Self {
121 Self {
122 center,
123 normal: normal.normalize(),
124 radius,
125 current,
126 }
127 }
128
129 pub fn on_axis_field(&self, distance_along_axis: f32) -> Vec3 {
132 let r2 = self.radius * self.radius;
133 let z2 = distance_along_axis * distance_along_axis;
134 let denom = (r2 + z2).powf(1.5);
135 if denom < 1e-10 {
136 return Vec3::ZERO;
137 }
138 let b_mag = 2.0 * PI * MU0_OVER_4PI * self.current * r2 / denom;
140 self.normal * b_mag
141 }
142
143 pub fn field_at(&self, point: Vec3, segments: usize) -> Vec3 {
145 let segs = self.to_segments(segments);
146 magnetic_field_at(&segs, point)
147 }
148
149 pub fn to_segments(&self, n: usize) -> Vec<CurrentSegment> {
151 let n = n.max(8);
152 let w = self.normal;
154 let u = if w.x.abs() < 0.9 {
155 Vec3::X.cross(w).normalize()
156 } else {
157 Vec3::Y.cross(w).normalize()
158 };
159 let v = w.cross(u);
160
161 let mut segments = Vec::with_capacity(n);
162 for i in 0..n {
163 let theta0 = 2.0 * PI * i as f32 / n as f32;
164 let theta1 = 2.0 * PI * (i + 1) as f32 / n as f32;
165 let p0 = self.center + self.radius * (u * theta0.cos() + v * theta0.sin());
166 let p1 = self.center + self.radius * (u * theta1.cos() + v * theta1.sin());
167 segments.push(CurrentSegment::new(p0, p1, self.current));
168 }
169 segments
170 }
171}
172
173#[derive(Clone, Debug)]
177pub struct Solenoid {
178 pub center: Vec3,
179 pub axis: Vec3,
180 pub radius: f32,
181 pub length: f32,
182 pub turns: u32,
183 pub current: f32,
184}
185
186impl Solenoid {
187 pub fn new(center: Vec3, axis: Vec3, radius: f32, length: f32, turns: u32, current: f32) -> Self {
188 Self {
189 center,
190 axis: axis.normalize(),
191 radius,
192 length,
193 turns,
194 current,
195 }
196 }
197
198 pub fn interior_field(&self) -> Vec3 {
200 let n = self.turns as f32 / self.length; let b_mag = 4.0 * PI * MU0_OVER_4PI * n * self.current;
203 self.axis * b_mag
204 }
205
206 pub fn to_loops(&self) -> Vec<CircularLoop> {
208 let mut loops = Vec::with_capacity(self.turns as usize);
209 let start = self.center - self.axis * self.length * 0.5;
210 for i in 0..self.turns {
211 let t = (i as f32 + 0.5) / self.turns as f32;
212 let pos = start + self.axis * self.length * t;
213 loops.push(CircularLoop::new(pos, self.axis, self.radius, self.current));
214 }
215 loops
216 }
217
218 pub fn field_at(&self, point: Vec3, segments_per_loop: usize) -> Vec3 {
220 let loops = self.to_loops();
221 let mut field = Vec3::ZERO;
222 for loop_ in &loops {
223 field += loop_.field_at(point, segments_per_loop);
224 }
225 field
226 }
227
228 pub fn is_inside(&self, point: Vec3) -> bool {
230 let to_point = point - self.center;
231 let along_axis = to_point.dot(self.axis);
232 if along_axis.abs() > self.length * 0.5 {
233 return false;
234 }
235 let perp = to_point - along_axis * self.axis;
236 perp.length() < self.radius
237 }
238}
239
240pub fn trace_magnetic_field_line(
245 segments: &[CurrentSegment],
246 start: Vec3,
247 steps: usize,
248) -> Vec<Vec3> {
249 let step_size = 0.1;
250 let mut points = Vec::with_capacity(steps + 1);
251 let mut pos = start;
252 points.push(pos);
253
254 for _ in 0..steps {
255 let b = magnetic_field_at(segments, pos);
256 if b.length_squared() < 1e-14 {
257 break;
258 }
259 let dir = b.normalize();
260
261 let k1 = dir * step_size;
263
264 let b2 = magnetic_field_at(segments, pos + k1 * 0.5);
265 if b2.length_squared() < 1e-14 { break; }
266 let k2 = b2.normalize() * step_size;
267
268 let b3 = magnetic_field_at(segments, pos + k2 * 0.5);
269 if b3.length_squared() < 1e-14 { break; }
270 let k3 = b3.normalize() * step_size;
271
272 let b4 = magnetic_field_at(segments, pos + k3);
273 if b4.length_squared() < 1e-14 { break; }
274 let k4 = b4.normalize() * step_size;
275
276 pos += (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0;
277 points.push(pos);
278
279 if points.len() > 10 && (pos - start).length() < step_size * 2.0 {
281 points.push(start); break;
283 }
284 }
285
286 points
287}
288
289pub fn magnetic_dipole_field(moment: Vec3, pos: Vec3) -> Vec3 {
294 let r = pos.length();
295 if r < 1e-10 {
296 return Vec3::ZERO;
297 }
298 let r_hat = pos / r;
299 let r3 = r * r * r;
300 let m_dot_r = moment.dot(r_hat);
301 MU0_OVER_4PI * (3.0 * m_dot_r * r_hat - moment) / r3
302}
303
304pub fn ampere_circulation(segments: &[CurrentSegment], path_points: &[Vec3]) -> f32 {
309 if path_points.len() < 2 {
310 return 0.0;
311 }
312 let mut circulation = 0.0f32;
313 for i in 0..path_points.len() {
314 let next = (i + 1) % path_points.len();
315 let dl = path_points[next] - path_points[i];
316 let midpoint = (path_points[i] + path_points[next]) * 0.5;
317 let b = magnetic_field_at(segments, midpoint);
318 circulation += b.dot(dl);
319 }
320 circulation
321}
322
323pub struct MagneticFieldRenderer {
327 pub field_line_color: Vec4,
328 pub arrow_color: Vec4,
329 pub flux_density_scale: f32,
330}
331
332impl MagneticFieldRenderer {
333 pub fn new() -> Self {
334 Self {
335 field_line_color: Vec4::new(0.2, 0.8, 0.3, 1.0),
336 arrow_color: Vec4::new(1.0, 1.0, 0.2, 1.0),
337 flux_density_scale: 1.0,
338 }
339 }
340
341 pub fn color_for_flux_density(&self, b_magnitude: f32) -> Vec4 {
343 let t = (b_magnitude * self.flux_density_scale).min(1.0);
344 let r = (2.0 * t - 1.0).max(0.0);
346 let g = 1.0 - (2.0 * t - 1.0).abs();
347 let b = (1.0 - 2.0 * t).max(0.0);
348 Vec4::new(r, g, b, 0.8)
349 }
350
351 pub fn direction_arrow(direction: Vec3) -> char {
353 let angle = direction.y.atan2(direction.x);
354 let octant = ((angle / (PI / 4.0)).round() as i32).rem_euclid(8);
355 match octant {
356 0 => '→',
357 1 => '↗',
358 2 => '↑',
359 3 => '↖',
360 4 => '←',
361 5 => '↙',
362 6 => '↓',
363 7 => '↘',
364 _ => '·',
365 }
366 }
367
368 pub fn render_field_line(&self, points: &[Vec3], segments: &[CurrentSegment]) -> Vec<(Vec3, char, Vec4)> {
370 let mut result = Vec::new();
371 for i in 0..points.len() {
372 let b = magnetic_field_at(segments, points[i]);
373 let mag = b.length();
374 let color = self.color_for_flux_density(mag);
375 let ch = if i + 1 < points.len() {
376 let dir = points[i + 1] - points[i];
377 Self::direction_arrow(dir)
378 } else {
379 '·'
380 };
381 result.push((points[i], ch, color));
382 }
383 result
384 }
385}
386
387impl Default for MagneticFieldRenderer {
388 fn default() -> Self {
389 Self::new()
390 }
391}
392
393#[cfg(test)]
396mod tests {
397 use super::*;
398
399 #[test]
400 fn test_biot_savart_direction() {
401 let seg = CurrentSegment::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 5.0), 1.0);
407 let b = biot_savart(&seg, Vec3::new(1.0, 0.0, 0.0));
408 assert!(b.y.abs() > b.x.abs() * 10.0, "B should be primarily in y: {:?}", b);
410 assert!(b.y > 0.0, "B_y should be positive for +z current at +x");
411 }
412
413 #[test]
414 fn test_infinite_wire_inverse_r() {
415 let wire = InfiniteWire::new(Vec3::ZERO, Vec3::Z, 1.0);
416 let b1 = wire.field_at(Vec3::new(1.0, 0.0, 0.0));
417 let b2 = wire.field_at(Vec3::new(2.0, 0.0, 0.0));
418 let ratio = b1.length() / b2.length();
420 assert!((ratio - 2.0).abs() < 0.01, "ratio={}", ratio);
421 }
422
423 #[test]
424 fn test_circular_loop_on_axis() {
425 let loop_ = CircularLoop::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
426 let b_center = loop_.on_axis_field(0.0);
428 let expected = 2.0 * PI * MU0_OVER_4PI * 1.0 / 1.0;
430 assert!((b_center.z - expected).abs() < 0.01, "b_center={:?}, expected={}", b_center, expected);
431
432 let b_far = loop_.on_axis_field(5.0);
434 assert!(b_far.length() < b_center.length(), "Field should decrease with distance");
435 }
436
437 #[test]
438 fn test_solenoid_interior_field() {
439 let sol = Solenoid::new(Vec3::ZERO, Vec3::Z, 0.5, 10.0, 100, 1.0);
440 let b_interior = sol.interior_field();
441 let n = 100.0 / 10.0;
443 let expected = 4.0 * PI * MU0_OVER_4PI * n * 1.0;
444 assert!((b_interior.z - expected).abs() < 0.01);
445 }
446
447 #[test]
448 fn test_solenoid_uniformity() {
449 let sol = Solenoid::new(Vec3::ZERO, Vec3::Z, 1.0, 20.0, 200, 1.0);
451 let b_center = sol.interior_field();
452 let b_mag = b_center.length();
455 assert!(b_mag > 0.0);
456 assert!(b_center.z.abs() > b_center.x.abs() * 100.0);
458 }
459
460 #[test]
461 fn test_ampere_law() {
462 let wire_segments: Vec<CurrentSegment> = {
464 vec![CurrentSegment::new(
466 Vec3::new(0.0, 0.0, -50.0),
467 Vec3::new(0.0, 0.0, 50.0),
468 1.0,
469 )]
470 };
471
472 let n = 200;
474 let r = 2.0;
475 let path: Vec<Vec3> = (0..n)
476 .map(|i| {
477 let theta = 2.0 * PI * i as f32 / n as f32;
478 Vec3::new(r * theta.cos(), r * theta.sin(), 0.0)
479 })
480 .collect();
481
482 let circulation = ampere_circulation(&wire_segments, &path);
483 let expected = 4.0 * PI * MU0_OVER_4PI * 1.0;
485 let relative_error = (circulation - expected).abs() / expected;
486 assert!(relative_error < 0.1, "Ampere's law: circ={}, expected={}, error={}", circulation, expected, relative_error);
487 }
488
489 #[test]
490 fn test_magnetic_dipole() {
491 let m = Vec3::new(0.0, 0.0, 1.0);
492 let b = magnetic_dipole_field(m, Vec3::new(0.0, 0.0, 5.0));
494 let expected = MU0_OVER_4PI * 2.0 / (5.0_f32.powi(3));
495 assert!((b.z - expected).abs() < 0.001, "dipole field: {}", b.z);
496 }
497
498 #[test]
499 fn test_renderer_colors() {
500 let renderer = MagneticFieldRenderer::new();
501 let weak = renderer.color_for_flux_density(0.0);
502 let strong = renderer.color_for_flux_density(1.0);
503 assert!(weak.z > weak.x, "Weak field should be blue");
505 assert!(strong.x > strong.z, "Strong field should be red");
506 }
507
508 #[test]
509 fn test_direction_arrow() {
510 assert_eq!(MagneticFieldRenderer::direction_arrow(Vec3::new(1.0, 0.0, 0.0)), '→');
511 assert_eq!(MagneticFieldRenderer::direction_arrow(Vec3::new(-1.0, 0.0, 0.0)), '←');
512 }
513}