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;
38 let length = dl.length();
39 if length < 1e-10 {
40 return Vec3::ZERO;
41 }
42 let u = dl / length;
43 let a = point - segment.start;
44 let along = a.dot(u);
45 let perp = a - u * along;
46 let d = perp.length();
47 if d < 1e-6 {
48 return Vec3::ZERO;
49 }
50 let s1 = -along;
51 let s2 = length - along;
52 let mag = MU0_OVER_4PI * segment.current / d
53 * (s2 / (s2 * s2 + d * d).sqrt() - s1 / (s1 * s1 + d * d).sqrt());
54 u.cross(perp / d) * mag
55}
56
57pub fn magnetic_field_at(segments: &[CurrentSegment], pos: Vec3) -> Vec3 {
59 let mut field = Vec3::ZERO;
60 for seg in segments {
61 field += biot_savart(seg, pos);
62 }
63 field
64}
65
66#[derive(Clone, Debug)]
70pub struct InfiniteWire {
71 pub position: Vec3, pub direction: Vec3, pub current: f32,
74}
75
76impl InfiniteWire {
77 pub fn new(position: Vec3, direction: Vec3, current: f32) -> Self {
78 Self {
79 position,
80 direction: direction.normalize(),
81 current,
82 }
83 }
84
85 pub fn field_at(&self, point: Vec3) -> Vec3 {
87 let to_point = point - self.position;
88 let parallel = to_point.dot(self.direction) * self.direction;
90 let perp = to_point - parallel;
91 let r = perp.length();
92 if r < 1e-10 {
93 return Vec3::ZERO;
94 }
95 let r_hat = perp / r;
97 let b_dir = self.direction.cross(r_hat);
98 let b_mag = 2.0 * MU0_OVER_4PI * self.current / r;
100 b_dir * b_mag
101 }
102}
103
104#[derive(Clone, Debug)]
108pub struct CircularLoop {
109 pub center: Vec3,
110 pub normal: Vec3, pub radius: f32,
112 pub current: f32,
113}
114
115impl CircularLoop {
116 pub fn new(center: Vec3, normal: Vec3, radius: f32, current: f32) -> Self {
117 Self {
118 center,
119 normal: normal.normalize(),
120 radius,
121 current,
122 }
123 }
124
125 pub fn on_axis_field(&self, distance_along_axis: f32) -> Vec3 {
128 let r2 = self.radius * self.radius;
129 let z2 = distance_along_axis * distance_along_axis;
130 let denom = (r2 + z2).powf(1.5);
131 if denom < 1e-10 {
132 return Vec3::ZERO;
133 }
134 let b_mag = 2.0 * PI * MU0_OVER_4PI * self.current * r2 / denom;
136 self.normal * b_mag
137 }
138
139 pub fn field_at(&self, point: Vec3, segments: usize) -> Vec3 {
141 let segs = self.to_segments(segments);
142 magnetic_field_at(&segs, point)
143 }
144
145 pub fn to_segments(&self, n: usize) -> Vec<CurrentSegment> {
147 let n = n.max(8);
148 let w = self.normal;
150 let u = if w.x.abs() < 0.9 {
151 Vec3::X.cross(w).normalize()
152 } else {
153 Vec3::Y.cross(w).normalize()
154 };
155 let v = w.cross(u);
156
157 let mut segments = Vec::with_capacity(n);
158 for i in 0..n {
159 let theta0 = 2.0 * PI * i as f32 / n as f32;
160 let theta1 = 2.0 * PI * (i + 1) as f32 / n as f32;
161 let p0 = self.center + self.radius * (u * theta0.cos() + v * theta0.sin());
162 let p1 = self.center + self.radius * (u * theta1.cos() + v * theta1.sin());
163 segments.push(CurrentSegment::new(p0, p1, self.current));
164 }
165 segments
166 }
167}
168
169#[derive(Clone, Debug)]
173pub struct Solenoid {
174 pub center: Vec3,
175 pub axis: Vec3,
176 pub radius: f32,
177 pub length: f32,
178 pub turns: u32,
179 pub current: f32,
180}
181
182impl Solenoid {
183 pub fn new(center: Vec3, axis: Vec3, radius: f32, length: f32, turns: u32, current: f32) -> Self {
184 Self {
185 center,
186 axis: axis.normalize(),
187 radius,
188 length,
189 turns,
190 current,
191 }
192 }
193
194 pub fn interior_field(&self) -> Vec3 {
196 let n = self.turns as f32 / self.length; let b_mag = 4.0 * PI * MU0_OVER_4PI * n * self.current;
199 self.axis * b_mag
200 }
201
202 pub fn to_loops(&self) -> Vec<CircularLoop> {
204 let mut loops = Vec::with_capacity(self.turns as usize);
205 let start = self.center - self.axis * self.length * 0.5;
206 for i in 0..self.turns {
207 let t = (i as f32 + 0.5) / self.turns as f32;
208 let pos = start + self.axis * self.length * t;
209 loops.push(CircularLoop::new(pos, self.axis, self.radius, self.current));
210 }
211 loops
212 }
213
214 pub fn field_at(&self, point: Vec3, segments_per_loop: usize) -> Vec3 {
216 let loops = self.to_loops();
217 let mut field = Vec3::ZERO;
218 for loop_ in &loops {
219 field += loop_.field_at(point, segments_per_loop);
220 }
221 field
222 }
223
224 pub fn is_inside(&self, point: Vec3) -> bool {
226 let to_point = point - self.center;
227 let along_axis = to_point.dot(self.axis);
228 if along_axis.abs() > self.length * 0.5 {
229 return false;
230 }
231 let perp = to_point - along_axis * self.axis;
232 perp.length() < self.radius
233 }
234}
235
236pub fn trace_magnetic_field_line(
241 segments: &[CurrentSegment],
242 start: Vec3,
243 steps: usize,
244) -> Vec<Vec3> {
245 let step_size = 0.1;
246 let mut points = Vec::with_capacity(steps + 1);
247 let mut pos = start;
248 points.push(pos);
249
250 for _ in 0..steps {
251 let b = magnetic_field_at(segments, pos);
252 if b.length_squared() < 1e-14 {
253 break;
254 }
255 let dir = b.normalize();
256
257 let k1 = dir * step_size;
259
260 let b2 = magnetic_field_at(segments, pos + k1 * 0.5);
261 if b2.length_squared() < 1e-14 { break; }
262 let k2 = b2.normalize() * step_size;
263
264 let b3 = magnetic_field_at(segments, pos + k2 * 0.5);
265 if b3.length_squared() < 1e-14 { break; }
266 let k3 = b3.normalize() * step_size;
267
268 let b4 = magnetic_field_at(segments, pos + k3);
269 if b4.length_squared() < 1e-14 { break; }
270 let k4 = b4.normalize() * step_size;
271
272 pos += (k1 + 2.0 * k2 + 2.0 * k3 + k4) / 6.0;
273 points.push(pos);
274
275 if points.len() > 10 && (pos - start).length() < step_size * 2.0 {
277 points.push(start); break;
279 }
280 }
281
282 points
283}
284
285pub fn magnetic_dipole_field(moment: Vec3, pos: Vec3) -> Vec3 {
290 let r = pos.length();
291 if r < 1e-10 {
292 return Vec3::ZERO;
293 }
294 let r_hat = pos / r;
295 let r3 = r * r * r;
296 let m_dot_r = moment.dot(r_hat);
297 MU0_OVER_4PI * (3.0 * m_dot_r * r_hat - moment) / r3
298}
299
300pub fn ampere_circulation(segments: &[CurrentSegment], path_points: &[Vec3]) -> f32 {
305 if path_points.len() < 2 {
306 return 0.0;
307 }
308 let mut circulation = 0.0f32;
309 for i in 0..path_points.len() {
310 let next = (i + 1) % path_points.len();
311 let dl = path_points[next] - path_points[i];
312 let midpoint = (path_points[i] + path_points[next]) * 0.5;
313 let b = magnetic_field_at(segments, midpoint);
314 circulation += b.dot(dl);
315 }
316 circulation
317}
318
319pub struct MagneticFieldRenderer {
323 pub field_line_color: Vec4,
324 pub arrow_color: Vec4,
325 pub flux_density_scale: f32,
326}
327
328impl MagneticFieldRenderer {
329 pub fn new() -> Self {
330 Self {
331 field_line_color: Vec4::new(0.2, 0.8, 0.3, 1.0),
332 arrow_color: Vec4::new(1.0, 1.0, 0.2, 1.0),
333 flux_density_scale: 1.0,
334 }
335 }
336
337 pub fn color_for_flux_density(&self, b_magnitude: f32) -> Vec4 {
339 let t = (b_magnitude * self.flux_density_scale).min(1.0);
340 let r = (2.0 * t - 1.0).max(0.0);
342 let g = 1.0 - (2.0 * t - 1.0).abs();
343 let b = (1.0 - 2.0 * t).max(0.0);
344 Vec4::new(r, g, b, 0.8)
345 }
346
347 pub fn direction_arrow(direction: Vec3) -> char {
349 let angle = direction.y.atan2(direction.x);
350 let octant = ((angle / (PI / 4.0)).round() as i32).rem_euclid(8);
351 match octant {
352 0 => '→',
353 1 => '↗',
354 2 => '↑',
355 3 => '↖',
356 4 => '←',
357 5 => '↙',
358 6 => '↓',
359 7 => '↘',
360 _ => '·',
361 }
362 }
363
364 pub fn render_field_line(&self, points: &[Vec3], segments: &[CurrentSegment]) -> Vec<(Vec3, char, Vec4)> {
366 let mut result = Vec::new();
367 for i in 0..points.len() {
368 let b = magnetic_field_at(segments, points[i]);
369 let mag = b.length();
370 let color = self.color_for_flux_density(mag);
371 let ch = if i + 1 < points.len() {
372 let dir = points[i + 1] - points[i];
373 Self::direction_arrow(dir)
374 } else {
375 '·'
376 };
377 result.push((points[i], ch, color));
378 }
379 result
380 }
381}
382
383impl Default for MagneticFieldRenderer {
384 fn default() -> Self {
385 Self::new()
386 }
387}
388
389#[cfg(test)]
392mod tests {
393 use super::*;
394
395 #[test]
396 fn test_biot_savart_direction() {
397 let seg = CurrentSegment::new(Vec3::new(0.0, 0.0, -5.0), Vec3::new(0.0, 0.0, 5.0), 1.0);
403 let b = biot_savart(&seg, Vec3::new(1.0, 0.0, 0.0));
404 assert!(b.y.abs() > b.x.abs() * 10.0, "B should be primarily in y: {:?}", b);
406 assert!(b.y > 0.0, "B_y should be positive for +z current at +x");
407 }
408
409 #[test]
410 fn test_infinite_wire_inverse_r() {
411 let wire = InfiniteWire::new(Vec3::ZERO, Vec3::Z, 1.0);
412 let b1 = wire.field_at(Vec3::new(1.0, 0.0, 0.0));
413 let b2 = wire.field_at(Vec3::new(2.0, 0.0, 0.0));
414 let ratio = b1.length() / b2.length();
416 assert!((ratio - 2.0).abs() < 0.01, "ratio={}", ratio);
417 }
418
419 #[test]
420 fn test_circular_loop_on_axis() {
421 let loop_ = CircularLoop::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
422 let b_center = loop_.on_axis_field(0.0);
424 let expected = 2.0 * PI * MU0_OVER_4PI * 1.0 / 1.0;
426 assert!((b_center.z - expected).abs() < 0.01, "b_center={:?}, expected={}", b_center, expected);
427
428 let b_far = loop_.on_axis_field(5.0);
430 assert!(b_far.length() < b_center.length(), "Field should decrease with distance");
431 }
432
433 #[test]
434 fn test_solenoid_interior_field() {
435 let sol = Solenoid::new(Vec3::ZERO, Vec3::Z, 0.5, 10.0, 100, 1.0);
436 let b_interior = sol.interior_field();
437 let n = 100.0 / 10.0;
439 let expected = 4.0 * PI * MU0_OVER_4PI * n * 1.0;
440 assert!((b_interior.z - expected).abs() < 0.01);
441 }
442
443 #[test]
444 fn test_solenoid_uniformity() {
445 let sol = Solenoid::new(Vec3::ZERO, Vec3::Z, 1.0, 20.0, 200, 1.0);
447 let b_center = sol.interior_field();
448 let b_mag = b_center.length();
451 assert!(b_mag > 0.0);
452 assert!(b_center.z.abs() > b_center.x.abs() * 100.0);
454 }
455
456 #[test]
457 fn test_ampere_law() {
458 let wire_segments: Vec<CurrentSegment> = {
460 vec![CurrentSegment::new(
462 Vec3::new(0.0, 0.0, -50.0),
463 Vec3::new(0.0, 0.0, 50.0),
464 1.0,
465 )]
466 };
467
468 let n = 200;
470 let r = 2.0;
471 let path: Vec<Vec3> = (0..n)
472 .map(|i| {
473 let theta = 2.0 * PI * i as f32 / n as f32;
474 Vec3::new(r * theta.cos(), r * theta.sin(), 0.0)
475 })
476 .collect();
477
478 let circulation = ampere_circulation(&wire_segments, &path);
479 let expected = 4.0 * PI * MU0_OVER_4PI * 1.0;
481 let relative_error = (circulation - expected).abs() / expected;
482 assert!(relative_error < 0.1, "Ampere's law: circ={}, expected={}, error={}", circulation, expected, relative_error);
483 }
484
485 #[test]
486 fn test_magnetic_dipole() {
487 let m = Vec3::new(0.0, 0.0, 1.0);
488 let b = magnetic_dipole_field(m, Vec3::new(0.0, 0.0, 5.0));
490 let expected = MU0_OVER_4PI * 2.0 / (5.0_f32.powi(3));
491 assert!((b.z - expected).abs() < 0.001, "dipole field: {}", b.z);
492 }
493
494 #[test]
495 fn test_renderer_colors() {
496 let renderer = MagneticFieldRenderer::new();
497 let weak = renderer.color_for_flux_density(0.0);
498 let strong = renderer.color_for_flux_density(1.0);
499 assert!(weak.z > weak.x, "Weak field should be blue");
501 assert!(strong.x > strong.z, "Strong field should be red");
502 }
503
504 #[test]
505 fn test_direction_arrow() {
506 assert_eq!(MagneticFieldRenderer::direction_arrow(Vec3::new(1.0, 0.0, 0.0)), '→');
507 assert_eq!(MagneticFieldRenderer::direction_arrow(Vec3::new(-1.0, 0.0, 0.0)), '←');
508 }
509}