Skip to main content

proof_engine/electromagnetic/
faraday.rs

1//! Faraday cage and electromagnetic shielding simulation.
2//! Computes shielded fields, induced surface charges, and skin depth.
3
4use glam::{Vec2, Vec3, Vec4};
5use std::f32::consts::PI;
6
7// ── Faraday Cage ──────────────────────────────────────────────────────────
8
9/// A Faraday cage defined by boundary points forming a closed polygon.
10#[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    /// Create a rectangular cage.
22    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        // Bottom edge
29        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        // Right edge
34        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        // Top edge
39        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        // Left edge
44        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    /// Create a circular cage.
53    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    /// Check if a point is inside the cage (ray casting algorithm).
65    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    /// Center of the cage (centroid of boundary points).
86    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
95/// Compute the shielded electric field inside and around a Faraday cage.
96/// The field inside a perfect conductor is zero; outside it is distorted.
97/// Returns field vectors on a grid.
98pub 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 each grid point, check if it's inside the cage
109    // If inside, field = 0 (perfect shielding)
110    // If on the boundary, field = 0 (conductor)
111    // If outside, apply a simple distortion model
112    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                // Inside cage: field is zero (perfect Faraday shielding)
119                field[i] = Vec3::ZERO;
120            } else {
121                // Outside: field is distorted near the cage
122                // Simple model: field lines bend around the cage
123                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                // Find closest boundary point
131                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                // Near the boundary, field is enhanced (charge accumulation)
140                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
151/// Shielding effectiveness in dB.
152/// SE = 20 * log10(E_incident / E_transmitted)
153/// For a good conductor: SE depends on frequency and thickness.
154pub fn shielding_effectiveness(cage: &FaradayCage, frequency: f32) -> f32 {
155    // Simplified model: SE = 20*log10(1 + sigma/(2*omega*eps0))
156    // In normalized units:
157    let omega = 2.0 * PI * frequency;
158    if omega < 1e-10 {
159        return f32::INFINITY; // DC: perfect shielding
160    }
161    let ratio = 1.0 + cage.conductivity / (2.0 * omega);
162    20.0 * ratio.log10()
163}
164
165/// Compute induced surface charge density on the cage boundary due to an external E field.
166/// The surface charge is proportional to the normal component of the external field.
167pub 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        // Outward normal at this boundary point
179        let tangent = cage.boundary_points[next] - cage.boundary_points[prev];
180        let normal = Vec2::new(-tangent.y, tangent.x).normalize();
181
182        // Surface charge: sigma = eps0 * E_n (normal component)
183        // In normalized units eps0 = 1
184        let e_normal = e_external.x * normal.x + e_external.y * normal.y;
185        charges.push(e_normal);
186    }
187
188    charges
189}
190
191// ── Skin Depth ────────────────────────────────────────────────────────────
192
193/// Skin depth: delta = sqrt(2 / (omega * mu * sigma))
194pub 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// ── Conducting Sphere ─────────────────────────────────────────────────────
203
204/// Analytical solution for EM shielding by a conducting sphere.
205#[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    /// Check if a point is inside the sphere.
218    pub fn contains(&self, point: Vec3) -> bool {
219        (point - self.center).length() < self.radius
220    }
221
222    /// Electric field around a conducting sphere in a uniform external field E0.
223    /// Outside: E = E0 + dipole correction.
224    /// Inside: E = 0 (perfect conductor).
225    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; // Inside: perfectly shielded
231        }
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        // Induced dipole: p = 4*pi*eps0*a^3 * E0
242        // Correction field: like a dipole field
243        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    /// Shielding effectiveness at the center.
250    pub fn shielding_at_center(&self) -> f32 {
251        // Perfect conductor: field is exactly zero inside
252        f32::INFINITY
253    }
254
255    /// Induced surface charge density at a point on the sphere surface.
256    /// sigma = 3 * eps0 * E0 * cos(theta), where theta is angle from E0 direction.
257    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
269// ── Cage Renderer ─────────────────────────────────────────────────────────
270
271/// Renderer for Faraday cage visualization.
272pub 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    /// Render the cage boundary as a series of glyphs.
290    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    /// Render the field on a grid, showing shielded interior.
303    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// ── Tests ─────────────────────────────────────────────────────────────────
344
345#[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        // Check that interior points have zero field
370        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        // delta = sqrt(2 / (2*pi*1e6 * 1 * 1e7)) = sqrt(2 / (2*pi*1e13))
383        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        // Far from sphere, field should approach external field
407        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        // Total induced charge should be approximately zero (charge conservation)
421        let total: f32 = charges.iter().sum();
422        // This won't be exactly zero due to discrete sampling, but should be small
423        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        // Higher conductivity should give better shielding
432        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        // At the "pole" facing the field: cos(0) = 1
442        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        // At the equator: cos(90°) = 0
445        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}