1use glam::{Vec2, Vec3, Vec4};
5use std::f32::consts::PI;
6
7#[derive(Clone, Debug)]
11pub struct FaradayCage {
12 pub boundary_points: Vec<Vec2>,
13 pub conductivity: f32,
14}
15
16impl FaradayCage {
17 pub fn new(boundary_points: Vec<Vec2>, conductivity: f32) -> Self {
18 Self { boundary_points, conductivity }
19 }
20
21 pub fn rectangular(center: Vec2, width: f32, height: f32, conductivity: f32, segments: usize) -> Self {
23 let half_w = width * 0.5;
24 let half_h = height * 0.5;
25 let mut points = Vec::with_capacity(segments * 4);
26 let n = segments.max(2);
27
28 for i in 0..n {
30 let t = i as f32 / n as f32;
31 points.push(center + Vec2::new(-half_w + t * width, -half_h));
32 }
33 for i in 0..n {
35 let t = i as f32 / n as f32;
36 points.push(center + Vec2::new(half_w, -half_h + t * height));
37 }
38 for i in 0..n {
40 let t = i as f32 / n as f32;
41 points.push(center + Vec2::new(half_w - t * width, half_h));
42 }
43 for i in 0..n {
45 let t = i as f32 / n as f32;
46 points.push(center + Vec2::new(-half_w, half_h - t * height));
47 }
48
49 Self { boundary_points: points, conductivity }
50 }
51
52 pub fn circular(center: Vec2, radius: f32, conductivity: f32, segments: usize) -> Self {
54 let n = segments.max(8);
55 let points: Vec<Vec2> = (0..n)
56 .map(|i| {
57 let theta = 2.0 * PI * i as f32 / n as f32;
58 center + Vec2::new(radius * theta.cos(), radius * theta.sin())
59 })
60 .collect();
61 Self { boundary_points: points, conductivity }
62 }
63
64 pub fn contains(&self, point: Vec2) -> bool {
66 let n = self.boundary_points.len();
67 if n < 3 {
68 return false;
69 }
70 let mut inside = false;
71 let mut j = n - 1;
72 for i in 0..n {
73 let pi = self.boundary_points[i];
74 let pj = self.boundary_points[j];
75 if ((pi.y > point.y) != (pj.y > point.y))
76 && (point.x < (pj.x - pi.x) * (point.y - pi.y) / (pj.y - pi.y) + pi.x)
77 {
78 inside = !inside;
79 }
80 j = i;
81 }
82 inside
83 }
84
85 pub fn center(&self) -> Vec2 {
87 if self.boundary_points.is_empty() {
88 return Vec2::ZERO;
89 }
90 let sum: Vec2 = self.boundary_points.iter().copied().sum();
91 sum / self.boundary_points.len() as f32
92 }
93}
94
95pub fn compute_shielded_field(
99 cage: &FaradayCage,
100 external_field: Vec3,
101 grid_nx: usize,
102 grid_ny: usize,
103 grid_dx: f32,
104) -> Vec<Vec3> {
105 let size = grid_nx * grid_ny;
106 let mut field = vec![external_field; size];
107
108 for y in 0..grid_ny {
113 for x in 0..grid_nx {
114 let pos = Vec2::new(x as f32 * grid_dx, y as f32 * grid_dx);
115 let i = x + y * grid_nx;
116
117 if cage.contains(pos) {
118 field[i] = Vec3::ZERO;
120 } else {
121 let to_center = Vec2::new(cage.center().x - pos.x, cage.center().y - pos.y);
124 let dist = to_center.length();
125 if dist < 1e-10 {
126 field[i] = Vec3::ZERO;
127 continue;
128 }
129
130 let mut min_dist = f32::MAX;
132 for bp in &cage.boundary_points {
133 let d = (*bp - pos).length();
134 if d < min_dist {
135 min_dist = d;
136 }
137 }
138
139 if min_dist < grid_dx * 3.0 {
141 let enhancement = 1.0 + 1.0 / (min_dist / grid_dx + 0.5);
142 field[i] = external_field * enhancement;
143 }
144 }
145 }
146 }
147
148 field
149}
150
151pub fn shielding_effectiveness(cage: &FaradayCage, frequency: f32) -> f32 {
155 let omega = 2.0 * PI * frequency;
158 if omega < 1e-10 {
159 return f32::INFINITY; }
161 let ratio = 1.0 + cage.conductivity / (2.0 * omega);
162 20.0 * ratio.log10()
163}
164
165pub fn induced_surface_charge(cage: &FaradayCage, e_external: Vec3) -> Vec<f32> {
168 let n = cage.boundary_points.len();
169 if n < 2 {
170 return vec![0.0; n];
171 }
172 let mut charges = Vec::with_capacity(n);
173
174 for i in 0..n {
175 let next = (i + 1) % n;
176 let prev = if i == 0 { n - 1 } else { i - 1 };
177
178 let tangent = cage.boundary_points[next] - cage.boundary_points[prev];
180 let normal = Vec2::new(-tangent.y, tangent.x).normalize();
181
182 let e_normal = e_external.x * normal.x + e_external.y * normal.y;
185 charges.push(e_normal);
186 }
187
188 charges
189}
190
191pub fn skin_depth(conductivity: f32, permeability: f32, frequency: f32) -> f32 {
195 let omega = 2.0 * PI * frequency;
196 if omega < 1e-10 || conductivity < 1e-10 || permeability < 1e-10 {
197 return f32::INFINITY;
198 }
199 (2.0 / (omega * permeability * conductivity)).sqrt()
200}
201
202#[derive(Clone, Debug)]
206pub struct ConductingSphere {
207 pub center: Vec3,
208 pub radius: f32,
209 pub conductivity: f32,
210}
211
212impl ConductingSphere {
213 pub fn new(center: Vec3, radius: f32, conductivity: f32) -> Self {
214 Self { center, radius, conductivity }
215 }
216
217 pub fn contains(&self, point: Vec3) -> bool {
219 (point - self.center).length() < self.radius
220 }
221
222 pub fn field_at(&self, point: Vec3, e_external: Vec3) -> Vec3 {
226 let r_vec = point - self.center;
227 let r = r_vec.length();
228
229 if r < self.radius {
230 return Vec3::ZERO; }
232
233 if r < 1e-10 {
234 return Vec3::ZERO;
235 }
236
237 let r_hat = r_vec / r;
238 let a = self.radius;
239 let ratio = (a / r).powi(3);
240
241 let e0_dot_r = e_external.dot(r_hat);
244 let dipole_correction = ratio * (3.0 * e0_dot_r * r_hat - e_external);
245
246 e_external + dipole_correction
247 }
248
249 pub fn shielding_at_center(&self) -> f32 {
251 f32::INFINITY
253 }
254
255 pub fn surface_charge_at(&self, surface_point: Vec3, e_external: Vec3) -> f32 {
258 let r_vec = (surface_point - self.center).normalize();
259 let e_hat = if e_external.length() > 1e-10 {
260 e_external.normalize()
261 } else {
262 return 0.0;
263 };
264 let cos_theta = r_vec.dot(e_hat);
265 3.0 * e_external.length() * cos_theta
266 }
267}
268
269pub struct CageRenderer {
273 pub cage_color: Vec4,
274 pub interior_color: Vec4,
275 pub field_color: Vec4,
276 pub field_scale: f32,
277}
278
279impl CageRenderer {
280 pub fn new() -> Self {
281 Self {
282 cage_color: Vec4::new(0.8, 0.8, 0.2, 1.0),
283 interior_color: Vec4::new(0.0, 0.1, 0.0, 0.3),
284 field_color: Vec4::new(0.5, 0.5, 1.0, 0.6),
285 field_scale: 1.0,
286 }
287 }
288
289 pub fn render_boundary(&self, cage: &FaradayCage) -> Vec<(Vec2, char, Vec4)> {
291 let mut result = Vec::new();
292 let n = cage.boundary_points.len();
293 for i in 0..n {
294 let next = (i + 1) % n;
295 let dir = cage.boundary_points[next] - cage.boundary_points[i];
296 let ch = if dir.x.abs() > dir.y.abs() { '─' } else { '│' };
297 result.push((cage.boundary_points[i], ch, self.cage_color));
298 }
299 result
300 }
301
302 pub fn render_field_grid(
304 &self,
305 cage: &FaradayCage,
306 field: &[Vec3],
307 grid_nx: usize,
308 grid_ny: usize,
309 grid_dx: f32,
310 ) -> Vec<(Vec2, Vec4)> {
311 let mut result = Vec::with_capacity(grid_nx * grid_ny);
312 for y in 0..grid_ny {
313 for x in 0..grid_nx {
314 let pos = Vec2::new(x as f32 * grid_dx, y as f32 * grid_dx);
315 let i = x + y * grid_nx;
316 let f = field[i];
317 let mag = f.length();
318
319 let color = if cage.contains(pos) {
320 self.interior_color
321 } else {
322 let brightness = (mag * self.field_scale).min(1.0);
323 Vec4::new(
324 self.field_color.x * brightness,
325 self.field_color.y * brightness,
326 self.field_color.z * brightness,
327 brightness * 0.8,
328 )
329 };
330 result.push((pos, color));
331 }
332 }
333 result
334 }
335}
336
337impl Default for CageRenderer {
338 fn default() -> Self {
339 Self::new()
340 }
341}
342
343#[cfg(test)]
346mod tests {
347 use super::*;
348
349 #[test]
350 fn test_cage_contains() {
351 let cage = FaradayCage::rectangular(Vec2::new(5.0, 5.0), 4.0, 4.0, 1e6, 10);
352 assert!(cage.contains(Vec2::new(5.0, 5.0)), "Center should be inside");
353 assert!(!cage.contains(Vec2::new(0.0, 0.0)), "Origin should be outside");
354 }
355
356 #[test]
357 fn test_circular_cage() {
358 let cage = FaradayCage::circular(Vec2::new(5.0, 5.0), 3.0, 1e6, 32);
359 assert!(cage.contains(Vec2::new(5.0, 5.0)));
360 assert!(!cage.contains(Vec2::new(5.0, 9.0)));
361 }
362
363 #[test]
364 fn test_shielded_field_interior_zero() {
365 let cage = FaradayCage::rectangular(Vec2::new(5.0, 5.0), 4.0, 4.0, 1e6, 10);
366 let external = Vec3::new(1.0, 0.0, 0.0);
367 let field = compute_shielded_field(&cage, external, 10, 10, 1.0);
368
369 let center_idx = 5 + 5 * 10;
371 let interior_field = field[center_idx];
372 assert!(
373 interior_field.length() < 0.01,
374 "Interior field should be ~0: {:?}",
375 interior_field
376 );
377 }
378
379 #[test]
380 fn test_skin_depth_formula() {
381 let sd = skin_depth(1e7, 1.0, 1e6);
382 let expected = (2.0 / (2.0 * PI * 1e6 * 1.0 * 1e7)).sqrt();
384 assert!((sd - expected).abs() < 1e-10, "sd={}, expected={}", sd, expected);
385 }
386
387 #[test]
388 fn test_skin_depth_decreases_with_frequency() {
389 let sd_low = skin_depth(1e6, 1.0, 1e3);
390 let sd_high = skin_depth(1e6, 1.0, 1e6);
391 assert!(sd_high < sd_low, "Skin depth should decrease with frequency");
392 }
393
394 #[test]
395 fn test_conducting_sphere_interior() {
396 let sphere = ConductingSphere::new(Vec3::ZERO, 1.0, 1e7);
397 let e_ext = Vec3::new(1.0, 0.0, 0.0);
398 let e_inside = sphere.field_at(Vec3::new(0.0, 0.0, 0.0), e_ext);
399 assert!(e_inside.length() < 1e-6, "Field inside sphere should be zero");
400 }
401
402 #[test]
403 fn test_conducting_sphere_far_field() {
404 let sphere = ConductingSphere::new(Vec3::ZERO, 1.0, 1e7);
405 let e_ext = Vec3::new(1.0, 0.0, 0.0);
406 let e_far = sphere.field_at(Vec3::new(100.0, 0.0, 0.0), e_ext);
408 assert!(
409 (e_far - e_ext).length() < 0.01,
410 "Far field should approach E_external: {:?}",
411 e_far
412 );
413 }
414
415 #[test]
416 fn test_induced_surface_charge() {
417 let cage = FaradayCage::circular(Vec2::new(0.0, 0.0), 1.0, 1e6, 16);
418 let charges = induced_surface_charge(&cage, Vec3::new(1.0, 0.0, 0.0));
419 assert_eq!(charges.len(), 16);
420 let total: f32 = charges.iter().sum();
422 assert!(total.abs() < 1.0, "Total induced charge should be small: {}", total);
424 }
425
426 #[test]
427 fn test_shielding_effectiveness() {
428 let cage = FaradayCage::new(vec![], 1e6);
429 let se = shielding_effectiveness(&cage, 1e3);
430 assert!(se > 0.0, "Shielding effectiveness should be positive");
431 let cage2 = FaradayCage::new(vec![], 1e8);
433 let se2 = shielding_effectiveness(&cage2, 1e3);
434 assert!(se2 > se, "Higher conductivity = better shielding");
435 }
436
437 #[test]
438 fn test_sphere_surface_charge() {
439 let sphere = ConductingSphere::new(Vec3::ZERO, 1.0, 1e7);
440 let e_ext = Vec3::new(1.0, 0.0, 0.0);
441 let sigma_pole = sphere.surface_charge_at(Vec3::new(1.0, 0.0, 0.0), e_ext);
443 assert!(sigma_pole > 0.0, "Charge at pole facing field should be positive");
444 let sigma_eq = sphere.surface_charge_at(Vec3::new(0.0, 1.0, 0.0), e_ext);
446 assert!(sigma_eq.abs() < 1e-6, "Charge at equator should be zero");
447 }
448}