1use glam::{Vec2, Vec3, Vec4};
5use std::f32::consts::PI;
6
7const C: f32 = 1.0;
9const ETA0: f32 = 1.0;
11
12#[derive(Clone, Debug)]
16pub struct HertzianDipole {
17 pub position: Vec3,
18 pub orientation: Vec3, pub current_moment: f32, 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 pub fn wavenumber(&self) -> f32 {
35 2.0 * PI * self.frequency / C
36 }
37
38 pub fn wavelength(&self) -> f32 {
40 C / self.frequency
41 }
42
43 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 let cos_theta = dir.dot(self.orientation);
55 let sin_theta = (1.0 - cos_theta * cos_theta).sqrt();
56
57 let e_mag = k * ETA0 * self.current_moment * sin_theta / (4.0 * PI * distance);
59
60 let e_dir = if sin_theta > 1e-10 {
62 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 let h = dir.cross(e) / ETA0;
72
73 (e, h)
74 }
75
76 pub fn power_pattern(&self, theta: f32) -> f32 {
78 let sin_theta = theta.sin();
79 sin_theta * sin_theta
80 }
81
82 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 pub fn radiation_resistance(&self) -> f32 {
99 let k = self.wavenumber();
100 let kdl = k * self.current_moment; ETA0 * 2.0 * PI / 3.0 * kdl * kdl / (4.0 * PI)
104 }
105}
106
107pub 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 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
147pub fn gain(pattern: &[(f32, f32, f32)], efficiency: f32) -> f32 {
149 directivity(pattern) * efficiency
150}
151
152#[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 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 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 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 pub fn theoretical_directivity(&self) -> f32 {
235 1.64
236 }
237}
238
239#[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 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 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 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 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 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 self.phase_shifts[i] = -k * elem.position.dot(dir);
317 }
318 }
319
320 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
335pub 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 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 let max_power = pattern.iter().map(|p| p.2).fold(0.0f32, f32::max).max(1e-10);
364
365 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 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 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#[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 let p_equator = dipole.power_pattern(PI / 2.0);
428 assert!((p_equator - 1.0).abs() < 1e-6);
429 let p_pole = dipole.power_pattern(0.0);
431 assert!(p_pole.abs() < 1e-6);
432 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 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 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 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 let p_eq = hw.power_pattern(PI / 2.0);
472 assert!(p_eq > 0.9, "Max at equator: {}", p_eq);
473 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 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 let af_broadside = array.array_factor(Vec3::Z);
488 let af_endfire = array.array_factor(Vec3::X);
490
491 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 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 array.beam_steering(Vec3::X);
517
518 let af_target = array.array_factor(Vec3::X);
519 let n = array.elements.len() as f32;
520 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}