Skip to main content

proof_engine/electromagnetic/
antenna.rs

1//! Antenna radiation patterns — Hertzian dipole, half-wave dipole,
2//! antenna arrays with beam steering, directivity, and gain.
3
4use glam::{Vec2, Vec3, Vec4};
5use std::f32::consts::PI;
6
7/// Speed of light in normalized units.
8const C: f32 = 1.0;
9/// Impedance of free space (normalized).
10const ETA0: f32 = 1.0;
11
12// ── Hertzian Dipole ───────────────────────────────────────────────────────
13
14/// An infinitesimal (Hertzian) dipole antenna.
15#[derive(Clone, Debug)]
16pub struct HertzianDipole {
17    pub position: Vec3,
18    pub orientation: Vec3, // direction of the dipole moment (unit)
19    pub current_moment: f32, // I * dl (current times length)
20    pub frequency: f32,
21}
22
23impl HertzianDipole {
24    pub fn new(position: Vec3, orientation: Vec3, current_moment: f32, frequency: f32) -> Self {
25        Self {
26            position,
27            orientation: orientation.normalize(),
28            frequency,
29            current_moment,
30        }
31    }
32
33    /// Wavenumber k = 2*pi*f/c.
34    pub fn wavenumber(&self) -> f32 {
35        2.0 * PI * self.frequency / C
36    }
37
38    /// Wavelength.
39    pub fn wavelength(&self) -> f32 {
40        C / self.frequency
41    }
42
43    /// Far-field E and H at a given direction and distance.
44    /// E_theta = j * k * eta * I*dl * sin(theta) * exp(-jkr) / (4*pi*r)
45    /// We return the magnitude (real envelope).
46    pub fn far_field(&self, direction: Vec3, distance: f32) -> (Vec3, Vec3) {
47        if distance < 1e-10 {
48            return (Vec3::ZERO, Vec3::ZERO);
49        }
50        let dir = direction.normalize();
51        let k = self.wavenumber();
52
53        // sin(theta) where theta is angle between direction and dipole orientation
54        let cos_theta = dir.dot(self.orientation);
55        let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
56
57        // E-field magnitude in far field
58        let e_mag = k * ETA0 * self.current_moment * sin_theta / (4.0 * PI * distance);
59
60        // E-field direction: theta-hat (perpendicular to r in the r-orientation plane)
61        let e_dir = if sin_theta > 1e-10 {
62            // theta_hat = cos(theta)*r_hat - orientation (projected perpendicular to r)
63            let theta_hat = (cos_theta * dir - self.orientation).normalize();
64            theta_hat
65        } else {
66            Vec3::ZERO
67        };
68
69        let e = e_dir * e_mag;
70        // H = (1/eta) * r_hat × E
71        let h = dir.cross(e) / ETA0;
72
73        (e, h)
74    }
75
76    /// Radiation power pattern: P(theta) proportional to sin^2(theta).
77    pub fn power_pattern(&self, theta: f32) -> f32 {
78        let sin_theta = theta.sin();
79        sin_theta * sin_theta
80    }
81
82    /// Compute the full radiation pattern.
83    /// Returns (theta, phi, power) for each angular sample.
84    pub fn radiation_pattern(&self, theta_steps: usize, phi_steps: usize) -> Vec<(f32, f32, f32)> {
85        let mut pattern = Vec::with_capacity(theta_steps * phi_steps);
86        for i in 0..theta_steps {
87            let theta = PI * (i as f32 + 0.5) / theta_steps as f32;
88            for j in 0..phi_steps {
89                let phi = 2.0 * PI * j as f32 / phi_steps as f32;
90                let power = self.power_pattern(theta);
91                pattern.push((theta, phi, power));
92            }
93        }
94        pattern
95    }
96
97    /// Radiation resistance: R_rad = (2*pi/3) * eta * (k*dl)^2 / (4*pi) (normalized).
98    pub fn radiation_resistance(&self) -> f32 {
99        let k = self.wavenumber();
100        let kdl = k * self.current_moment; // k * I*dl, but dl is embedded in current_moment
101        // For a Hertzian dipole: R_rad = (2*pi/3) * eta * (dl/lambda)^2
102        // Simplified: proportional to (k*dl)^2
103        ETA0 * 2.0 * PI / 3.0 * kdl * kdl / (4.0 * PI)
104    }
105}
106
107// ── Radiation Pattern Analysis ────────────────────────────────────────────
108
109/// Compute directivity from a radiation pattern.
110/// D = 4*pi * max(P) / integral(P * sin(theta) dtheta dphi)
111pub fn directivity(pattern: &[(f32, f32, f32)]) -> f32 {
112    if pattern.is_empty() {
113        return 0.0;
114    }
115
116    let max_power = pattern.iter().map(|p| p.2).fold(0.0f32, f32::max);
117    if max_power < 1e-10 {
118        return 0.0;
119    }
120
121    // Approximate the integral using the pattern data
122    // Determine step sizes from the pattern
123    let theta_values: Vec<f32> = pattern.iter().map(|p| p.0).collect();
124    let mut unique_thetas: Vec<f32> = Vec::new();
125    for &t in &theta_values {
126        if unique_thetas.last().map_or(true, |&last| (last - t).abs() > 1e-6) {
127            unique_thetas.push(t);
128        }
129    }
130    let n_theta = unique_thetas.len().max(1);
131    let n_phi = pattern.len() / n_theta;
132    let d_theta = PI / n_theta as f32;
133    let d_phi = 2.0 * PI / n_phi.max(1) as f32;
134
135    let mut total = 0.0f32;
136    for &(theta, _phi, power) in pattern {
137        total += power * theta.sin() * d_theta * d_phi;
138    }
139
140    if total < 1e-10 {
141        return 0.0;
142    }
143
144    4.0 * PI * max_power / total
145}
146
147/// Gain = directivity * efficiency.
148pub fn gain(pattern: &[(f32, f32, f32)], efficiency: f32) -> f32 {
149    directivity(pattern) * efficiency
150}
151
152// ── Half-Wave Dipole ──────────────────────────────────────────────────────
153
154/// A half-wave dipole antenna.
155#[derive(Clone, Debug)]
156pub struct HalfWaveDipole {
157    pub position: Vec3,
158    pub orientation: Vec3,
159    pub frequency: f32,
160}
161
162impl HalfWaveDipole {
163    pub fn new(position: Vec3, orientation: Vec3, frequency: f32) -> Self {
164        Self {
165            position,
166            orientation: orientation.normalize(),
167            frequency,
168        }
169    }
170
171    pub fn wavelength(&self) -> f32 {
172        C / self.frequency
173    }
174
175    pub fn wavenumber(&self) -> f32 {
176        2.0 * PI * self.frequency / C
177    }
178
179    /// Half-wave dipole pattern: P(theta) = [cos(pi/2 * cos(theta)) / sin(theta)]^2
180    pub fn power_pattern(&self, theta: f32) -> f32 {
181        let sin_theta = theta.sin();
182        if sin_theta.abs() < 1e-10 {
183            return 0.0;
184        }
185        let numerator = ((PI / 2.0) * theta.cos()).cos();
186        let val = numerator / sin_theta;
187        val * val
188    }
189
190    /// Far field of the half-wave dipole.
191    pub fn far_field(&self, direction: Vec3, distance: f32) -> (Vec3, Vec3) {
192        if distance < 1e-10 {
193            return (Vec3::ZERO, Vec3::ZERO);
194        }
195        let dir = direction.normalize();
196        let cos_theta = dir.dot(self.orientation);
197        let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
198
199        let pattern_val = if sin_theta > 1e-10 {
200            ((PI / 2.0) * cos_theta).cos() / sin_theta
201        } else {
202            0.0
203        };
204
205        let k = self.wavenumber();
206        let e_mag = ETA0 * pattern_val / (2.0 * PI * distance);
207
208        let e_dir = if sin_theta > 1e-10 {
209            (cos_theta * dir - self.orientation).normalize()
210        } else {
211            Vec3::ZERO
212        };
213
214        let e = e_dir * e_mag;
215        let h = dir.cross(e) / ETA0;
216        (e, h)
217    }
218
219    /// Radiation pattern.
220    pub fn radiation_pattern(&self, theta_steps: usize, phi_steps: usize) -> Vec<(f32, f32, f32)> {
221        let mut pattern = Vec::with_capacity(theta_steps * phi_steps);
222        for i in 0..theta_steps {
223            let theta = PI * (i as f32 + 0.5) / theta_steps as f32;
224            for j in 0..phi_steps {
225                let phi = 2.0 * PI * j as f32 / phi_steps as f32;
226                let power = self.power_pattern(theta);
227                pattern.push((theta, phi, power));
228            }
229        }
230        pattern
231    }
232
233    /// Directivity of a half-wave dipole ≈ 1.64 (2.15 dBi).
234    pub fn theoretical_directivity(&self) -> f32 {
235        1.64
236    }
237}
238
239// ── Antenna Array ─────────────────────────────────────────────────────────
240
241/// An array of Hertzian dipole antennas with programmable phase shifts for beam forming.
242#[derive(Clone, Debug)]
243pub struct AntennaArray {
244    pub elements: Vec<HertzianDipole>,
245    pub phase_shifts: Vec<f32>,
246}
247
248impl AntennaArray {
249    pub fn new(elements: Vec<HertzianDipole>, phase_shifts: Vec<f32>) -> Self {
250        assert_eq!(elements.len(), phase_shifts.len());
251        Self { elements, phase_shifts }
252    }
253
254    /// Create a uniform linear array along a given axis.
255    pub fn uniform_linear(
256        n: usize,
257        spacing: f32,
258        axis: Vec3,
259        orientation: Vec3,
260        frequency: f32,
261        current_moment: f32,
262    ) -> Self {
263        let axis_norm = axis.normalize();
264        let start = -axis_norm * spacing * (n - 1) as f32 * 0.5;
265        let elements: Vec<HertzianDipole> = (0..n)
266            .map(|i| {
267                let pos = start + axis_norm * spacing * i as f32;
268                HertzianDipole::new(pos, orientation, current_moment, frequency)
269            })
270            .collect();
271        let phase_shifts = vec![0.0; n];
272        Self { elements, phase_shifts }
273    }
274
275    /// Array factor: AF(direction) = sum_i exp(j*(k*d_i·direction + phase_i))
276    /// Returns the magnitude |AF|^2.
277    pub fn array_factor(&self, direction: Vec3) -> f32 {
278        let dir = direction.normalize();
279        let mut real_sum = 0.0f32;
280        let mut imag_sum = 0.0f32;
281
282        for (i, elem) in self.elements.iter().enumerate() {
283            let k = elem.wavenumber();
284            let phase = k * elem.position.dot(dir) + self.phase_shifts[i];
285            real_sum += phase.cos();
286            imag_sum += phase.sin();
287        }
288
289        real_sum * real_sum + imag_sum * imag_sum
290    }
291
292    /// Total radiation pattern: element pattern × array factor.
293    pub fn total_pattern(&self, theta: f32, phi: f32) -> f32 {
294        let direction = Vec3::new(
295            theta.sin() * phi.cos(),
296            theta.sin() * phi.sin(),
297            theta.cos(),
298        );
299
300        // Element pattern (assuming all elements are identical)
301        let element_power = if !self.elements.is_empty() {
302            self.elements[0].power_pattern(theta)
303        } else {
304            0.0
305        };
306
307        element_power * self.array_factor(direction)
308    }
309
310    /// Compute phase shifts to steer the beam toward a target direction.
311    pub fn beam_steering(&mut self, target_direction: Vec3) {
312        let dir = target_direction.normalize();
313        for (i, elem) in self.elements.iter().enumerate() {
314            let k = elem.wavenumber();
315            // Phase shift = -k * d_i · target_direction
316            self.phase_shifts[i] = -k * elem.position.dot(dir);
317        }
318    }
319
320    /// Full radiation pattern of the array.
321    pub fn radiation_pattern(&self, theta_steps: usize, phi_steps: usize) -> Vec<(f32, f32, f32)> {
322        let mut pattern = Vec::with_capacity(theta_steps * phi_steps);
323        for i in 0..theta_steps {
324            let theta = PI * (i as f32 + 0.5) / theta_steps as f32;
325            for j in 0..phi_steps {
326                let phi = 2.0 * PI * j as f32 / phi_steps as f32;
327                let power = self.total_pattern(theta, phi);
328                pattern.push((theta, phi, power));
329            }
330        }
331        pattern
332    }
333}
334
335// ── Antenna Renderer ──────────────────────────────────────────────────────
336
337/// Renderer for antenna radiation patterns.
338pub struct AntennaRenderer {
339    pub pattern_color: Vec4,
340    pub element_color: Vec4,
341    pub scale: f32,
342}
343
344impl AntennaRenderer {
345    pub fn new() -> Self {
346        Self {
347            pattern_color: Vec4::new(0.2, 0.8, 0.4, 0.7),
348            element_color: Vec4::new(1.0, 0.5, 0.1, 1.0),
349            scale: 1.0,
350        }
351    }
352
353    /// Render a 2D polar plot of the radiation pattern (theta slice at phi=0).
354    pub fn render_polar_plot(
355        &self,
356        pattern: &[(f32, f32, f32)],
357        phi_slice: f32,
358        num_points: usize,
359    ) -> Vec<(Vec2, Vec4)> {
360        let mut result = Vec::new();
361
362        // Find max power for normalization
363        let max_power = pattern.iter().map(|p| p.2).fold(0.0f32, f32::max).max(1e-10);
364
365        // Extract the phi slice
366        let tolerance = PI / num_points as f32;
367        for &(theta, phi, power) in pattern {
368            if (phi - phi_slice).abs() < tolerance {
369                let r = (power / max_power).sqrt() * self.scale;
370                let x = r * theta.sin();
371                let y = r * theta.cos();
372                let brightness = (power / max_power).sqrt();
373                let color = self.pattern_color * brightness;
374                result.push((Vec2::new(x, y), color));
375            }
376        }
377
378        result
379    }
380
381    /// Render a 3D pattern as a set of (position, brightness) for glyph rendering.
382    pub fn render_3d_pattern(
383        &self,
384        pattern: &[(f32, f32, f32)],
385    ) -> Vec<(Vec3, f32)> {
386        let max_power = pattern.iter().map(|p| p.2).fold(0.0f32, f32::max).max(1e-10);
387
388        pattern.iter().map(|&(theta, phi, power)| {
389            let r = (power / max_power).sqrt() * self.scale;
390            let pos = Vec3::new(
391                r * theta.sin() * phi.cos(),
392                r * theta.sin() * phi.sin(),
393                r * theta.cos(),
394            );
395            let brightness = (power / max_power).sqrt();
396            (pos, brightness)
397        }).collect()
398    }
399
400    /// Glyph based on pattern intensity.
401    pub fn intensity_glyph(intensity: f32) -> char {
402        if intensity > 0.8 { '█' }
403        else if intensity > 0.6 { '▓' }
404        else if intensity > 0.4 { '▒' }
405        else if intensity > 0.2 { '░' }
406        else if intensity > 0.05 { '·' }
407        else { ' ' }
408    }
409}
410
411impl Default for AntennaRenderer {
412    fn default() -> Self {
413        Self::new()
414    }
415}
416
417// ── Tests ─────────────────────────────────────────────────────────────────
418
419#[cfg(test)]
420mod tests {
421    use super::*;
422
423    #[test]
424    fn test_hertzian_dipole_sin2_pattern() {
425        let dipole = HertzianDipole::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
426        // At theta=pi/2 (equator), pattern should be maximum (sin^2(90°) = 1)
427        let p_equator = dipole.power_pattern(PI / 2.0);
428        assert!((p_equator - 1.0).abs() < 1e-6);
429        // At theta=0 (along axis), pattern should be zero (sin^2(0) = 0)
430        let p_pole = dipole.power_pattern(0.0);
431        assert!(p_pole.abs() < 1e-6);
432        // At theta=pi/4, should be sin^2(45°) = 0.5
433        let p_45 = dipole.power_pattern(PI / 4.0);
434        assert!((p_45 - 0.5).abs() < 1e-5);
435    }
436
437    #[test]
438    fn test_hertzian_dipole_far_field() {
439        let dipole = HertzianDipole::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
440        // Far field at equator
441        let (e, h) = dipole.far_field(Vec3::X, 10.0);
442        assert!(e.length() > 0.0, "E should be nonzero at equator");
443        assert!(h.length() > 0.0, "H should be nonzero at equator");
444        // E and H should be perpendicular
445        let dot = e.dot(h);
446        assert!(dot.abs() < 1e-6, "E·H should be 0");
447    }
448
449    #[test]
450    fn test_hertzian_dipole_1_over_r() {
451        let dipole = HertzianDipole::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
452        let (e1, _) = dipole.far_field(Vec3::X, 10.0);
453        let (e2, _) = dipole.far_field(Vec3::X, 20.0);
454        let ratio = e1.length() / e2.length();
455        assert!((ratio - 2.0).abs() < 0.01, "Far field should decay as 1/r: ratio={}", ratio);
456    }
457
458    #[test]
459    fn test_directivity_hertzian() {
460        let dipole = HertzianDipole::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
461        let pattern = dipole.radiation_pattern(90, 36);
462        let d = directivity(&pattern);
463        // Hertzian dipole directivity = 1.5
464        assert!((d - 1.5).abs() < 0.2, "Directivity should be ~1.5: {}", d);
465    }
466
467    #[test]
468    fn test_half_wave_dipole_pattern() {
469        let hw = HalfWaveDipole::new(Vec3::ZERO, Vec3::Z, 1.0);
470        // Maximum at equator
471        let p_eq = hw.power_pattern(PI / 2.0);
472        assert!(p_eq > 0.9, "Max at equator: {}", p_eq);
473        // Zero at poles
474        let p_pole = hw.power_pattern(0.01);
475        assert!(p_pole < 0.1, "Should be near zero at pole: {}", p_pole);
476    }
477
478    #[test]
479    fn test_array_factor_broadside() {
480        // 4-element array, half-wavelength spacing, no phase shift → broadside radiation
481        let freq = 1.0;
482        let lambda = C / freq;
483        let spacing = lambda / 2.0;
484        let array = AntennaArray::uniform_linear(4, spacing, Vec3::X, Vec3::Z, freq, 1.0);
485
486        // Broadside (perpendicular to array axis) should have max AF
487        let af_broadside = array.array_factor(Vec3::Z);
488        // End-fire (along array axis)
489        let af_endfire = array.array_factor(Vec3::X);
490
491        // For uniform array with zero phase shift, broadside should be N^2 = 16
492        assert!((af_broadside - 16.0).abs() < 1.0, "Broadside AF should be ~16: {}", af_broadside);
493    }
494
495    #[test]
496    fn test_array_factor_periodicity() {
497        let freq = 1.0;
498        let lambda = C / freq;
499        let spacing = lambda / 2.0;
500        let array = AntennaArray::uniform_linear(4, spacing, Vec3::X, Vec3::Z, freq, 1.0);
501
502        // AF should have same value for opposite directions at broadside
503        let af1 = array.array_factor(Vec3::Z);
504        let af2 = array.array_factor(-Vec3::Z);
505        assert!((af1 - af2).abs() < 0.1, "Pattern should be symmetric: {} vs {}", af1, af2);
506    }
507
508    #[test]
509    fn test_beam_steering() {
510        let freq = 1.0;
511        let lambda = C / freq;
512        let spacing = lambda / 2.0;
513        let mut array = AntennaArray::uniform_linear(8, spacing, Vec3::X, Vec3::Z, freq, 1.0);
514
515        // Steer toward +X direction
516        array.beam_steering(Vec3::X);
517
518        let af_target = array.array_factor(Vec3::X);
519        let n = array.elements.len() as f32;
520        // Steered array should have AF ≈ N^2 in the target direction
521        assert!((af_target - n * n).abs() < 1.0, "Steered AF should be ~N^2: {}", af_target);
522    }
523
524    #[test]
525    fn test_gain() {
526        let dipole = HertzianDipole::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
527        let pattern = dipole.radiation_pattern(60, 24);
528        let g = gain(&pattern, 0.9);
529        let d = directivity(&pattern);
530        assert!((g - d * 0.9).abs() < 0.01);
531    }
532
533    #[test]
534    fn test_renderer() {
535        let renderer = AntennaRenderer::new();
536        assert_eq!(AntennaRenderer::intensity_glyph(0.9), '█');
537        assert_eq!(AntennaRenderer::intensity_glyph(0.01), ' ');
538    }
539
540    #[test]
541    fn test_radiation_pattern_size() {
542        let dipole = HertzianDipole::new(Vec3::ZERO, Vec3::Z, 1.0, 1.0);
543        let pattern = dipole.radiation_pattern(10, 20);
544        assert_eq!(pattern.len(), 200);
545    }
546}