1use crate::types::Point3D;
4use ndarray::Array2;
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize, Deserialize)]
9pub struct DirectivityGrid {
10 pub horizontal_angles: Vec<f64>,
12 pub vertical_angles: Vec<f64>,
14 pub magnitude: Array2<f64>,
17}
18
19impl DirectivityGrid {
20 pub fn omnidirectional() -> Self {
22 let horizontal_angles: Vec<f64> = (0..36).map(|i| i as f64 * 10.0).collect();
23 let vertical_angles: Vec<f64> = (0..19).map(|i| i as f64 * 10.0).collect();
24
25 let magnitude = Array2::ones((vertical_angles.len(), horizontal_angles.len()));
26
27 Self {
28 horizontal_angles,
29 vertical_angles,
30 magnitude,
31 }
32 }
33
34 pub fn cardioid() -> Self {
36 let horizontal_angles: Vec<f64> = (0..36).map(|i| i as f64 * 10.0).collect();
37 let vertical_angles: Vec<f64> = (0..19).map(|i| i as f64 * 10.0).collect();
38
39 let mut magnitude = Array2::zeros((vertical_angles.len(), horizontal_angles.len()));
40
41 for (v_idx, &v_angle) in vertical_angles.iter().enumerate() {
42 for (h_idx, &h_angle) in horizontal_angles.iter().enumerate() {
43 let theta_rad = v_angle.to_radians();
45 let phi_rad = h_angle.to_radians();
46 let forward_dot = theta_rad.sin() * phi_rad.sin();
48 magnitude[[v_idx, h_idx]] = 0.5 * (1.0 + forward_dot).max(0.0);
49 }
50 }
51
52 Self {
53 horizontal_angles,
54 vertical_angles,
55 magnitude,
56 }
57 }
58
59 pub fn interpolate(&self, theta: f64, phi: f64) -> f64 {
61 let theta_deg = theta.to_degrees();
63 let mut phi_deg = phi.to_degrees();
64
65 while phi_deg < 0.0 {
67 phi_deg += 360.0;
68 }
69 while phi_deg >= 360.0 {
70 phi_deg -= 360.0;
71 }
72
73 let h_idx = (phi_deg / 10.0).floor() as usize;
75 let v_idx = (theta_deg / 10.0).floor() as usize;
76
77 let h_idx = h_idx.min(self.horizontal_angles.len() - 1);
78 let v_idx = v_idx.min(self.vertical_angles.len() - 1);
79
80 let h_next = (h_idx + 1) % self.horizontal_angles.len();
81 let v_next = (v_idx + 1).min(self.vertical_angles.len() - 1);
82
83 let h_frac = (phi_deg / 10.0) - h_idx as f64;
85 let v_frac = (theta_deg / 10.0) - v_idx as f64;
86
87 let m00 = self.magnitude[[v_idx, h_idx]];
88 let m01 = self.magnitude[[v_idx, h_next]];
89 let m10 = self.magnitude[[v_next, h_idx]];
90 let m11 = self.magnitude[[v_next, h_next]];
91
92 let m0 = m00 * (1.0 - h_frac) + m01 * h_frac;
93 let m1 = m10 * (1.0 - h_frac) + m11 * h_frac;
94
95 m0 * (1.0 - v_frac) + m1 * v_frac
96 }
97}
98
99#[derive(Debug, Clone, Serialize, Deserialize)]
101pub enum Directivity {
102 Grid(DirectivityGrid),
104 Classical {
106 h_angle: f64,
108 v_angle: f64,
110 },
111}
112
113impl Directivity {
114 pub fn amplitude(&self, theta: f64, phi: f64, frequency: f64) -> f64 {
116 match self {
117 Directivity::Grid(grid) => grid.interpolate(theta, phi),
118 Directivity::Classical { h_angle, v_angle } => {
119 let min_f = 80.0;
123 let max_f = 800.0; let t = if frequency <= min_f {
126 0.0
127 } else if frequency >= max_f {
128 1.0
129 } else {
130 (frequency.ln() - min_f.ln()) / (max_f.ln() - min_f.ln())
131 };
132
133 let h_width = 360.0 * (1.0 - t) + h_angle * t;
134 let v_width = 360.0 * (1.0 - t) + v_angle * t;
135
136 let mut phi_deg = phi.to_degrees();
148 while phi_deg < 0.0 {
149 phi_deg += 360.0;
150 }
151 let delta_phi = (phi_deg - 90.0).abs().min((360.0 - (phi_deg - 90.0)).abs()); let theta_deg = theta.to_degrees();
154 let delta_theta = (theta_deg - 90.0).abs();
155
156 let x_h = delta_phi / (h_width / 2.0);
159 let x_v = delta_theta / (v_width / 2.0);
160
161 let r_sq = x_h * x_h + x_v * x_v;
163
164 0.5_f64.powf(r_sq)
166 }
167 }
168 }
169}
170
171#[derive(Debug, Clone, Default, Serialize, Deserialize)]
173pub enum CrossoverFilter {
174 #[default]
176 FullRange,
177 Lowpass {
179 cutoff_freq: f64,
181 order: u32,
183 },
184 Highpass {
186 cutoff_freq: f64,
188 order: u32,
190 },
191 Bandpass {
193 low_cutoff: f64,
195 high_cutoff: f64,
197 order: u32,
199 },
200}
201
202impl CrossoverFilter {
203 pub fn amplitude_at_frequency(&self, frequency: f64) -> f64 {
205 match self {
206 CrossoverFilter::FullRange => 1.0,
207 CrossoverFilter::Lowpass { cutoff_freq, order } => {
208 let ratio = frequency / cutoff_freq;
209 1.0 / (1.0 + ratio.powi(*order as i32 * 2)).sqrt()
210 }
211 CrossoverFilter::Highpass { cutoff_freq, order } => {
212 let ratio = cutoff_freq / frequency;
213 1.0 / (1.0 + ratio.powi(*order as i32 * 2)).sqrt()
214 }
215 CrossoverFilter::Bandpass {
216 low_cutoff,
217 high_cutoff,
218 order,
219 } => {
220 let high_ratio = low_cutoff / frequency;
221 let low_ratio = frequency / high_cutoff;
222 let hp_response = 1.0 / (1.0 + high_ratio.powi(*order as i32 * 2)).sqrt();
223 let lp_response = 1.0 / (1.0 + low_ratio.powi(*order as i32 * 2)).sqrt();
224 hp_response * lp_response
225 }
226 }
227 }
228}
229
230#[derive(Debug, Clone, Serialize, Deserialize)]
232pub struct Source {
233 pub position: Point3D,
235 pub directivity: Directivity,
237 pub amplitude: f64,
239 pub crossover: CrossoverFilter,
241 pub name: String,
243}
244
245impl Source {
246 pub fn new(position: Point3D, directivity: Directivity, amplitude: f64) -> Self {
248 Self {
249 position,
250 directivity,
251 amplitude,
252 crossover: CrossoverFilter::FullRange,
253 name: String::from("Source"),
254 }
255 }
256
257 pub fn omnidirectional(position: Point3D, amplitude: f64) -> Self {
259 Self::new(
260 position,
261 Directivity::Grid(DirectivityGrid::omnidirectional()),
262 amplitude,
263 )
264 }
265
266 pub fn classical(position: Point3D, h_angle: f64, v_angle: f64, amplitude: f64) -> Self {
268 Self::new(
269 position,
270 Directivity::Classical { h_angle, v_angle },
271 amplitude,
272 )
273 }
274
275 pub fn with_crossover(mut self, crossover: CrossoverFilter) -> Self {
277 self.crossover = crossover;
278 self
279 }
280
281 pub fn with_name(mut self, name: String) -> Self {
283 self.name = name;
284 self
285 }
286
287 pub fn amplitude_towards(&self, point: &Point3D, frequency: f64) -> f64 {
289 let dx = point.x - self.position.x;
290 let dy = point.y - self.position.y;
291 let dz = point.z - self.position.z;
292
293 let r = (dx * dx + dy * dy + dz * dz).sqrt();
294 if r < 1e-10 {
295 return self.amplitude * self.crossover.amplitude_at_frequency(frequency);
296 }
297
298 let theta = (dz / r).acos();
299 let phi = dy.atan2(dx);
300
301 let directivity_factor = self.directivity.amplitude(theta, phi, frequency);
302 let crossover_factor = self.crossover.amplitude_at_frequency(frequency);
303 self.amplitude * directivity_factor * crossover_factor
304 }
305}
306
307#[cfg(test)]
308mod tests {
309 use super::*;
310 use std::f64::consts::PI;
311
312 #[test]
313 fn test_omnidirectional_pattern() {
314 let grid = DirectivityGrid::omnidirectional();
315 assert!((grid.interpolate(0.0, 0.0) - 1.0).abs() < 1e-6);
317 assert!((grid.interpolate(PI / 2.0, PI) - 1.0).abs() < 1e-6);
318 assert!((grid.interpolate(PI, 0.0) - 1.0).abs() < 1e-6);
319 }
320
321 #[test]
322 fn test_crossover_lowpass() {
323 let crossover = CrossoverFilter::Lowpass {
324 cutoff_freq: 100.0,
325 order: 2,
326 };
327 assert!((crossover.amplitude_at_frequency(10.0) - 1.0).abs() < 0.1);
329 let at_cutoff = crossover.amplitude_at_frequency(100.0);
331 assert!(at_cutoff > 0.6 && at_cutoff < 0.8);
332 assert!(crossover.amplitude_at_frequency(1000.0) < 0.1);
334 }
335
336 #[test]
337 fn test_source_amplitude() {
338 let source = Source::omnidirectional(Point3D::new(0.0, 0.0, 0.0), 1.0);
339 let amp = source.amplitude_towards(&Point3D::new(1.0, 0.0, 0.0), 1000.0);
340 assert!((amp - 1.0).abs() < 1e-6);
341 }
342
343 #[test]
344 fn test_classical_source_beaming() {
345 let source = Source::classical(Point3D::new(0.0, 0.0, 0.0), 60.0, 40.0, 1.0);
346
347 let amp_low = source.amplitude_towards(&Point3D::new(0.0, -1.0, 0.0), 50.0); assert!(amp_low > 0.4);
365
366 let amp_high_side = source.amplitude_towards(&Point3D::new(1.0, 0.0, 0.0), 2000.0); assert!(amp_high_side < 0.1);
371
372 let amp_high_on = source.amplitude_towards(&Point3D::new(0.0, 1.0, 0.0), 2000.0); assert!((amp_high_on - 1.0).abs() < 1e-6);
375 }
376}