Skip to main content

proof_engine/electromagnetic/
waves.rs

1//! Electromagnetic wave propagation — plane waves, spherical waves,
2//! Gaussian beams, wave packets, interference, diffraction, and Snell's law.
3
4use glam::{Vec3, Vec4};
5use std::f32::consts::PI;
6
7/// Speed of light in normalized units.
8const C: f32 = 1.0;
9
10// ── Plane Wave ────────────────────────────────────────────────────────────
11
12/// Plane electromagnetic wave: E and B perpendicular to propagation direction.
13#[derive(Clone, Debug)]
14pub struct PlaneWave {
15    pub direction: Vec3,     // propagation direction (unit)
16    pub polarization: Vec3,  // E-field polarization direction (unit, ⊥ direction)
17    pub frequency: f32,
18    pub amplitude: f32,
19    pub phase: f32,
20}
21
22impl PlaneWave {
23    pub fn new(direction: Vec3, polarization: Vec3, frequency: f32, amplitude: f32) -> Self {
24        Self {
25            direction: direction.normalize(),
26            polarization: polarization.normalize(),
27            frequency,
28            amplitude,
29            phase: 0.0,
30        }
31    }
32
33    /// Evaluate E and B fields at a position and time.
34    /// E = E0 * sin(k·r - ωt + φ) * polarization
35    /// B = (1/c) * direction × E
36    pub fn evaluate(&self, pos: Vec3, time: f32) -> (Vec3, Vec3) {
37        let omega = 2.0 * PI * self.frequency;
38        let k = omega / C;
39        let k_vec = self.direction * k;
40        let phase = k_vec.dot(pos) - omega * time + self.phase;
41        let e_mag = self.amplitude * phase.sin();
42        let e = self.polarization * e_mag;
43        let b = self.direction.cross(e) / C;
44        (e, b)
45    }
46
47    /// Wavelength = c / frequency.
48    pub fn wavelength(&self) -> f32 {
49        C / self.frequency
50    }
51
52    /// Wave vector magnitude.
53    pub fn wavenumber(&self) -> f32 {
54        2.0 * PI * self.frequency / C
55    }
56
57    /// Angular frequency.
58    pub fn angular_frequency(&self) -> f32 {
59        2.0 * PI * self.frequency
60    }
61
62    /// Intensity (time-averaged): I = 0.5 * E0^2 / (mu0*c), normalized to 0.5*E0^2.
63    pub fn intensity(&self) -> f32 {
64        0.5 * self.amplitude * self.amplitude
65    }
66}
67
68// ── Spherical Wave ────────────────────────────────────────────────────────
69
70/// Spherical electromagnetic wave emanating from a point source with 1/r decay.
71#[derive(Clone, Debug)]
72pub struct SphericalWave {
73    pub origin: Vec3,
74    pub frequency: f32,
75    pub amplitude: f32,
76}
77
78impl SphericalWave {
79    pub fn new(origin: Vec3, frequency: f32, amplitude: f32) -> Self {
80        Self { origin, frequency, amplitude }
81    }
82
83    /// Evaluate the scalar field amplitude at a point and time.
84    /// A(r,t) = (A0/r) * sin(kr - ωt)
85    pub fn evaluate_scalar(&self, pos: Vec3, time: f32) -> f32 {
86        let r_vec = pos - self.origin;
87        let r = r_vec.length();
88        if r < 1e-10 {
89            return 0.0;
90        }
91        let omega = 2.0 * PI * self.frequency;
92        let k = omega / C;
93        (self.amplitude / r) * (k * r - omega * time).sin()
94    }
95
96    /// Evaluate E and B vectors (polarized radially outward for simplicity).
97    pub fn evaluate(&self, pos: Vec3, time: f32) -> (Vec3, Vec3) {
98        let r_vec = pos - self.origin;
99        let r = r_vec.length();
100        if r < 1e-10 {
101            return (Vec3::ZERO, Vec3::ZERO);
102        }
103        let r_hat = r_vec / r;
104        let omega = 2.0 * PI * self.frequency;
105        let k = omega / C;
106        let scalar = (self.amplitude / r) * (k * r - omega * time).sin();
107
108        // E perpendicular to r_hat; pick an arbitrary perpendicular direction
109        let e_dir = if r_hat.x.abs() < 0.9 {
110            Vec3::X.cross(r_hat).normalize()
111        } else {
112            Vec3::Y.cross(r_hat).normalize()
113        };
114        let e = e_dir * scalar;
115        let b = r_hat.cross(e) / C;
116        (e, b)
117    }
118}
119
120// ── Gaussian Beam ─────────────────────────────────────────────────────────
121
122/// Gaussian beam with a finite waist (focused beam).
123#[derive(Clone, Debug)]
124pub struct GaussianBeam {
125    pub origin: Vec3,
126    pub direction: Vec3,
127    pub waist: f32,      // beam waist (minimum radius) w0
128    pub wavelength: f32,
129}
130
131impl GaussianBeam {
132    pub fn new(origin: Vec3, direction: Vec3, waist: f32, wavelength: f32) -> Self {
133        Self {
134            origin,
135            direction: direction.normalize(),
136            waist,
137            wavelength,
138        }
139    }
140
141    /// Rayleigh range: z_R = pi * w0^2 / lambda
142    pub fn rayleigh_range(&self) -> f32 {
143        PI * self.waist * self.waist / self.wavelength
144    }
145
146    /// Beam radius at distance z from waist: w(z) = w0 * sqrt(1 + (z/z_R)^2)
147    pub fn beam_radius(&self, z: f32) -> f32 {
148        let zr = self.rayleigh_range();
149        self.waist * (1.0 + (z / zr).powi(2)).sqrt()
150    }
151
152    /// Evaluate the beam intensity profile at a point.
153    pub fn intensity_at(&self, pos: Vec3) -> f32 {
154        let to_point = pos - self.origin;
155        let z = to_point.dot(self.direction);
156        let perp = to_point - z * self.direction;
157        let rho = perp.length();
158
159        let w = self.beam_radius(z);
160        let w0 = self.waist;
161        let intensity = (w0 / w) * (w0 / w) * (-2.0 * rho * rho / (w * w)).exp();
162        intensity
163    }
164
165    /// Evaluate the complex amplitude (scalar, real part) at a point and time.
166    pub fn evaluate(&self, pos: Vec3, time: f32) -> f32 {
167        let to_point = pos - self.origin;
168        let z = to_point.dot(self.direction);
169        let perp = to_point - z * self.direction;
170        let rho = perp.length();
171
172        let zr = self.rayleigh_range();
173        let w = self.beam_radius(z);
174        let k = 2.0 * PI / self.wavelength;
175        let omega = 2.0 * PI * C / self.wavelength;
176
177        let amplitude = (self.waist / w) * (-rho * rho / (w * w)).exp();
178        let gouy_phase = (z / zr).atan();
179        let phase = k * z - omega * time + k * rho * rho * z / (2.0 * (z * z + zr * zr)) - gouy_phase;
180
181        amplitude * phase.cos()
182    }
183}
184
185// ── Wave Packet ───────────────────────────────────────────────────────────
186
187/// Wave packet with a finite bandwidth (group velocity envelope).
188#[derive(Clone, Debug)]
189pub struct WavePacket {
190    pub center_freq: f32,
191    pub bandwidth: f32,
192    pub group_velocity: f32,
193}
194
195impl WavePacket {
196    pub fn new(center_freq: f32, bandwidth: f32, group_velocity: f32) -> Self {
197        Self { center_freq, bandwidth, group_velocity }
198    }
199
200    /// Evaluate the wave packet amplitude at position x and time t.
201    /// Gaussian envelope modulating a carrier wave.
202    pub fn evaluate(&self, x: f32, t: f32) -> f32 {
203        let omega0 = 2.0 * PI * self.center_freq;
204        let k0 = omega0 / C;
205        let sigma = 1.0 / (2.0 * PI * self.bandwidth);
206
207        // Envelope moves at group velocity
208        let xi = x - self.group_velocity * t;
209        let envelope = (-xi * xi / (2.0 * sigma * sigma)).exp();
210
211        // Carrier wave
212        let carrier = (k0 * x - omega0 * t).cos();
213
214        envelope * carrier
215    }
216
217    /// Phase velocity.
218    pub fn phase_velocity(&self) -> f32 {
219        C // In vacuum, phase velocity = c
220    }
221
222    /// Spatial width (1/e amplitude) of the packet.
223    pub fn spatial_width(&self) -> f32 {
224        1.0 / (2.0 * PI * self.bandwidth)
225    }
226}
227
228// ── Standing Wave ─────────────────────────────────────────────────────────
229
230/// Standing wave: superposition of two counter-propagating waves.
231pub fn standing_wave(pos: f32, time: f32, wavelength: f32, amplitude: f32) -> f32 {
232    let k = 2.0 * PI / wavelength;
233    let omega = 2.0 * PI * C / wavelength;
234    // 2*A*cos(kx)*cos(wt) — the product of spatial and temporal oscillations
235    2.0 * amplitude * (k * pos).cos() * (omega * time).cos()
236}
237
238// ── Interference ──────────────────────────────────────────────────────────
239
240/// Compute the interference pattern of two plane waves at a set of points.
241pub fn interference_pattern(wave1: &PlaneWave, wave2: &PlaneWave, points: &[Vec3], time: f32) -> Vec<f32> {
242    points.iter().map(|&pos| {
243        let (e1, _) = wave1.evaluate(pos, time);
244        let (e2, _) = wave2.evaluate(pos, time);
245        let total = e1 + e2;
246        total.length_squared() // intensity ∝ |E|^2
247    }).collect()
248}
249
250// ── Diffraction ───────────────────────────────────────────────────────────
251
252/// Single-slit diffraction intensity pattern (Fraunhofer).
253/// Returns normalized intensity at angle `angle` for given slit width and wavelength.
254pub fn diffraction_single_slit(slit_width: f32, wavelength: f32, angle: f32) -> f32 {
255    let beta = PI * slit_width * angle.sin() / wavelength;
256    if beta.abs() < 1e-8 {
257        return 1.0; // Central maximum
258    }
259    let sinc = beta.sin() / beta;
260    sinc * sinc
261}
262
263// ── Snell's Law ───────────────────────────────────────────────────────────
264
265/// Compute the refracted angle using Snell's law: n1*sin(θ_i) = n2*sin(θ_t).
266/// Returns None for total internal reflection.
267pub fn snells_law(n1: f32, n2: f32, theta_i: f32) -> Option<f32> {
268    let sin_t = n1 * theta_i.sin() / n2;
269    if sin_t.abs() > 1.0 {
270        None // Total internal reflection
271    } else {
272        Some(sin_t.asin())
273    }
274}
275
276/// Fresnel coefficients for reflection and transmission (s-polarization, intensity).
277/// Returns (reflectance, transmittance).
278pub fn fresnel_coefficients(n1: f32, n2: f32, theta_i: f32) -> (f32, f32) {
279    let cos_i = theta_i.cos();
280    let sin_t = n1 * theta_i.sin() / n2;
281    if sin_t.abs() > 1.0 {
282        return (1.0, 0.0); // Total internal reflection
283    }
284    let cos_t = (1.0 - sin_t * sin_t).sqrt();
285
286    // s-polarization (TE)
287    let rs = (n1 * cos_i - n2 * cos_t) / (n1 * cos_i + n2 * cos_t);
288    // p-polarization (TM)
289    let rp = (n2 * cos_i - n1 * cos_t) / (n2 * cos_i + n1 * cos_t);
290
291    // Average reflectance for unpolarized light
292    let reflectance = 0.5 * (rs * rs + rp * rp);
293    let transmittance = 1.0 - reflectance;
294    (reflectance, transmittance)
295}
296
297// ── Wave Renderer ─────────────────────────────────────────────────────────
298
299/// Renderer for EM waves as animated color/brightness patterns.
300pub struct WaveRenderer {
301    pub color_positive: Vec4,
302    pub color_negative: Vec4,
303    pub brightness_scale: f32,
304}
305
306impl WaveRenderer {
307    pub fn new() -> Self {
308        Self {
309            color_positive: Vec4::new(1.0, 0.8, 0.2, 1.0),
310            color_negative: Vec4::new(0.2, 0.4, 1.0, 1.0),
311            brightness_scale: 1.0,
312        }
313    }
314
315    /// Color for a wave amplitude value.
316    pub fn color_for_amplitude(&self, amplitude: f32) -> Vec4 {
317        let normalized = (amplitude * self.brightness_scale).clamp(-1.0, 1.0);
318        let t = (normalized + 1.0) * 0.5; // map [-1,1] to [0,1]
319        let color = self.color_negative * (1.0 - t) + self.color_positive * t;
320        let alpha = normalized.abs().max(0.05);
321        Vec4::new(color.x, color.y, color.z, alpha)
322    }
323
324    /// Render a 1D wave along an axis.
325    pub fn render_1d_wave(
326        &self,
327        wave: &PlaneWave,
328        x_range: (f32, f32),
329        num_points: usize,
330        time: f32,
331    ) -> Vec<(f32, Vec4)> {
332        let mut result = Vec::with_capacity(num_points);
333        for i in 0..num_points {
334            let t = i as f32 / (num_points - 1).max(1) as f32;
335            let x = x_range.0 + t * (x_range.1 - x_range.0);
336            let pos = wave.direction * x;
337            let (e, _) = wave.evaluate(pos, time);
338            let amp = e.dot(wave.polarization);
339            result.push((x, self.color_for_amplitude(amp)));
340        }
341        result
342    }
343
344    /// Glyph for wave amplitude.
345    pub fn glyph_for_amplitude(amplitude: f32) -> char {
346        let a = amplitude.abs();
347        if a > 0.8 {
348            '█'
349        } else if a > 0.6 {
350            '▓'
351        } else if a > 0.4 {
352            '▒'
353        } else if a > 0.2 {
354            '░'
355        } else if a > 0.05 {
356            '·'
357        } else {
358            ' '
359        }
360    }
361}
362
363impl Default for WaveRenderer {
364    fn default() -> Self {
365        Self::new()
366    }
367}
368
369// ── Tests ─────────────────────────────────────────────────────────────────
370
371#[cfg(test)]
372mod tests {
373    use super::*;
374
375    #[test]
376    fn test_dispersion_relation() {
377        // omega = c * k
378        let wave = PlaneWave::new(Vec3::X, Vec3::Y, 2.0, 1.0);
379        let omega = wave.angular_frequency();
380        let k = wave.wavenumber();
381        assert!((omega - C * k).abs() < 1e-6, "Dispersion relation: omega={}, ck={}", omega, C * k);
382    }
383
384    #[test]
385    fn test_plane_wave_orthogonality() {
386        let wave = PlaneWave::new(Vec3::X, Vec3::Y, 1.0, 1.0);
387        let (e, b) = wave.evaluate(Vec3::new(0.25, 0.0, 0.0), 0.0);
388        // E ⊥ B
389        let dot = e.dot(b);
390        assert!(dot.abs() < 1e-6, "E·B should be 0, got {}", dot);
391        // E ⊥ k
392        let dot_ek = e.dot(wave.direction);
393        assert!(dot_ek.abs() < 1e-6, "E·k should be 0, got {}", dot_ek);
394        // B ⊥ k
395        let dot_bk = b.dot(wave.direction);
396        assert!(dot_bk.abs() < 1e-6, "B·k should be 0, got {}", dot_bk);
397    }
398
399    #[test]
400    fn test_plane_wave_ratio() {
401        // |E| / |B| = c
402        let wave = PlaneWave::new(Vec3::X, Vec3::Y, 1.0, 3.0);
403        let (e, b) = wave.evaluate(Vec3::new(0.3, 0.0, 0.0), 0.1);
404        if b.length() > 1e-10 {
405            let ratio = e.length() / b.length();
406            assert!((ratio - C).abs() < 0.01, "|E|/|B| should be c, got {}", ratio);
407        }
408    }
409
410    #[test]
411    fn test_standing_wave_nodes() {
412        let wavelength = 2.0;
413        // Nodes of cos(kx) at x = lambda/4, 3*lambda/4, etc.
414        let node_x = wavelength / 4.0;
415        let val = standing_wave(node_x, 0.0, wavelength, 1.0);
416        assert!(val.abs() < 1e-5, "Standing wave should have node at lambda/4: {}", val);
417    }
418
419    #[test]
420    fn test_standing_wave_antinode() {
421        let wavelength = 2.0;
422        // Antinode at x=0 (cos(0)=1), t=0 (cos(0)=1)
423        let val = standing_wave(0.0, 0.0, wavelength, 1.0);
424        assert!((val - 2.0).abs() < 1e-5, "Antinode amplitude should be 2A: {}", val);
425    }
426
427    #[test]
428    fn test_snells_law_normal_incidence() {
429        let theta_t = snells_law(1.0, 1.5, 0.0).unwrap();
430        assert!(theta_t.abs() < 1e-6, "Normal incidence should give 0 refraction angle");
431    }
432
433    #[test]
434    fn test_snells_law_tir() {
435        // Going from glass (n=1.5) to air (n=1.0) at steep angle
436        let critical = (1.0 / 1.5_f32).asin();
437        // Angle greater than critical should give TIR
438        let result = snells_law(1.5, 1.0, critical + 0.1);
439        assert!(result.is_none(), "Should have total internal reflection");
440    }
441
442    #[test]
443    fn test_snells_law_symmetry() {
444        let theta_i = 0.5;
445        let theta_t = snells_law(1.0, 1.5, theta_i).unwrap();
446        let theta_back = snells_law(1.5, 1.0, theta_t).unwrap();
447        assert!((theta_back - theta_i).abs() < 1e-5, "Snell's law should be reversible");
448    }
449
450    #[test]
451    fn test_fresnel_normal_incidence() {
452        let (r, t) = fresnel_coefficients(1.0, 1.5, 0.0);
453        // At normal incidence: R = ((n1-n2)/(n1+n2))^2
454        let expected_r = ((1.0 - 1.5) / (1.0 + 1.5)).powi(2);
455        assert!((r - expected_r).abs() < 0.01, "R={}, expected={}", r, expected_r);
456        assert!((r + t - 1.0).abs() < 0.01, "R+T should be 1");
457    }
458
459    #[test]
460    fn test_diffraction_central_maximum() {
461        let intensity = diffraction_single_slit(1.0, 0.5, 0.0);
462        assert!((intensity - 1.0).abs() < 1e-6, "Central max should be 1.0");
463    }
464
465    #[test]
466    fn test_diffraction_first_minimum() {
467        // First minimum at sin(θ) = λ/a
468        let a = 2.0;
469        let lambda = 0.5;
470        let angle = (lambda / a).asin();
471        let intensity = diffraction_single_slit(a, lambda, angle);
472        assert!(intensity < 0.001, "First minimum should be ~0, got {}", intensity);
473    }
474
475    #[test]
476    fn test_spherical_wave_decay() {
477        let wave = SphericalWave::new(Vec3::ZERO, 1.0, 1.0);
478        let a1 = wave.evaluate_scalar(Vec3::new(1.0, 0.0, 0.0), 0.0).abs();
479        let a2 = wave.evaluate_scalar(Vec3::new(2.0, 0.0, 0.0), 0.0).abs();
480        // 1/r decay: ratio should be ~2
481        if a2 > 1e-10 {
482            let ratio = a1 / a2;
483            assert!((ratio - 2.0).abs() < 0.1, "1/r decay ratio: {}", ratio);
484        }
485    }
486
487    #[test]
488    fn test_gaussian_beam_waist() {
489        let beam = GaussianBeam::new(Vec3::ZERO, Vec3::X, 1.0, 0.5);
490        // At z=0, beam radius = w0
491        assert!((beam.beam_radius(0.0) - 1.0).abs() < 1e-6);
492        // At z=z_R, beam radius = w0*sqrt(2)
493        let zr = beam.rayleigh_range();
494        let w_zr = beam.beam_radius(zr);
495        assert!((w_zr - 1.0 * 2.0_f32.sqrt()).abs() < 0.01);
496    }
497
498    #[test]
499    fn test_wave_packet_moves() {
500        let wp = WavePacket::new(1.0, 0.1, 0.5);
501        // Envelope peak should be at x = v_g * t
502        let t = 10.0;
503        let peak_x = wp.group_velocity * t;
504        // Sample around the peak
505        let at_peak = wp.evaluate(peak_x, t).abs();
506        let away = wp.evaluate(peak_x + 100.0, t).abs();
507        assert!(at_peak > away, "Packet should be centered at group velocity position");
508    }
509
510    #[test]
511    fn test_interference() {
512        let w1 = PlaneWave::new(Vec3::X, Vec3::Y, 1.0, 1.0);
513        let w2 = PlaneWave { phase: PI, ..w1.clone() }; // opposite phase
514        let points = vec![Vec3::ZERO];
515        let pattern = interference_pattern(&w1, &w2, &points, 0.0);
516        // Two waves with π phase difference should destructively interfere at origin
517        // (both have sin(0+phase) at origin, t=0)
518        // w1: sin(0) = 0, w2: sin(pi) = 0. So both are 0 at origin, t=0
519        // Let's test at a specific point where it matters
520        let points2 = vec![Vec3::new(0.1, 0.0, 0.0)];
521        let p = interference_pattern(&w1, &w2, &points2, 0.0);
522        // With phase diff of PI, should have reduced intensity
523        assert!(p[0] < 1.0);
524    }
525}