Skip to main content

math_audio_xem_common/
types.rs

1//! Basic types for room acoustics simulations
2
3use serde::{Deserialize, Serialize};
4use std::f64::consts::PI;
5
6/// 3D point in space
7#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)]
8pub struct Point3D {
9    /// X coordinate
10    pub x: f64,
11    /// Y coordinate
12    pub y: f64,
13    /// Z coordinate
14    pub z: f64,
15}
16
17impl Point3D {
18    /// Create a new 3D point
19    pub fn new(x: f64, y: f64, z: f64) -> Self {
20        Self { x, y, z }
21    }
22
23    /// Create a zero point (origin)
24    pub fn zero() -> Self {
25        Self::new(0.0, 0.0, 0.0)
26    }
27
28    /// Calculate Euclidean distance to another point
29    pub fn distance_to(&self, other: &Point3D) -> f64 {
30        let dx = self.x - other.x;
31        let dy = self.y - other.y;
32        let dz = self.z - other.z;
33        (dx * dx + dy * dy + dz * dz).sqrt()
34    }
35
36    /// Convert to spherical coordinates (r, theta, phi)
37    /// theta: polar angle (0 to PI)
38    /// phi: azimuthal angle (-PI to PI)
39    pub fn to_spherical(&self) -> (f64, f64, f64) {
40        let r = (self.x * self.x + self.y * self.y + self.z * self.z).sqrt();
41        let theta = if r > 1e-10 { (self.z / r).acos() } else { 0.0 };
42        let phi = self.y.atan2(self.x);
43        (r, theta, phi)
44    }
45
46    /// Create from spherical coordinates
47    pub fn from_spherical(r: f64, theta: f64, phi: f64) -> Self {
48        Self {
49            x: r * theta.sin() * phi.cos(),
50            y: r * theta.sin() * phi.sin(),
51            z: r * theta.cos(),
52        }
53    }
54
55    /// Compute dot product with another point (treating as vectors)
56    pub fn dot(&self, other: &Point3D) -> f64 {
57        self.x * other.x + self.y * other.y + self.z * other.z
58    }
59
60    /// Compute cross product with another point (treating as vectors)
61    pub fn cross(&self, other: &Point3D) -> Point3D {
62        Point3D {
63            x: self.y * other.z - self.z * other.y,
64            y: self.z * other.x - self.x * other.z,
65            z: self.x * other.y - self.y * other.x,
66        }
67    }
68
69    /// Compute the length (magnitude) of the vector
70    pub fn length(&self) -> f64 {
71        (self.x * self.x + self.y * self.y + self.z * self.z).sqrt()
72    }
73
74    /// Normalize the vector to unit length
75    pub fn normalize(&self) -> Option<Point3D> {
76        let len = self.length();
77        if len > 1e-10 {
78            Some(Point3D {
79                x: self.x / len,
80                y: self.y / len,
81                z: self.z / len,
82            })
83        } else {
84            None
85        }
86    }
87
88    /// Scale the vector by a scalar
89    pub fn scale(&self, s: f64) -> Point3D {
90        Point3D {
91            x: self.x * s,
92            y: self.y * s,
93            z: self.z * s,
94        }
95    }
96
97    /// Add another point/vector
98    pub fn add(&self, other: &Point3D) -> Point3D {
99        Point3D {
100            x: self.x + other.x,
101            y: self.y + other.y,
102            z: self.z + other.z,
103        }
104    }
105
106    /// Subtract another point/vector
107    pub fn sub(&self, other: &Point3D) -> Point3D {
108        Point3D {
109            x: self.x - other.x,
110            y: self.y - other.y,
111            z: self.z - other.z,
112        }
113    }
114}
115
116impl std::ops::Add for Point3D {
117    type Output = Point3D;
118    fn add(self, other: Point3D) -> Point3D {
119        Point3D {
120            x: self.x + other.x,
121            y: self.y + other.y,
122            z: self.z + other.z,
123        }
124    }
125}
126
127impl std::ops::Sub for Point3D {
128    type Output = Point3D;
129    fn sub(self, other: Point3D) -> Point3D {
130        Point3D {
131            x: self.x - other.x,
132            y: self.y - other.y,
133            z: self.z - other.z,
134        }
135    }
136}
137
138impl std::ops::Mul<f64> for Point3D {
139    type Output = Point3D;
140    fn mul(self, s: f64) -> Point3D {
141        Point3D {
142            x: self.x * s,
143            y: self.y * s,
144            z: self.z * s,
145        }
146    }
147}
148
149/// Listening position (alias for Point3D)
150pub type ListeningPosition = Point3D;
151
152/// Surface element (triangular or quadrilateral)
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct SurfaceElement {
155    /// Node indices that form this element
156    pub nodes: Vec<usize>,
157}
158
159impl SurfaceElement {
160    /// Create a triangular element
161    pub fn triangle(n0: usize, n1: usize, n2: usize) -> Self {
162        Self {
163            nodes: vec![n0, n1, n2],
164        }
165    }
166
167    /// Create a quadrilateral element
168    pub fn quad(n0: usize, n1: usize, n2: usize, n3: usize) -> Self {
169        Self {
170            nodes: vec![n0, n1, n2, n3],
171        }
172    }
173
174    /// Check if this is a triangular element
175    pub fn is_triangle(&self) -> bool {
176        self.nodes.len() == 3
177    }
178
179    /// Check if this is a quadrilateral element
180    pub fn is_quad(&self) -> bool {
181        self.nodes.len() == 4
182    }
183}
184
185/// Room mesh for BEM/FEM (surface mesh for BEM, volume mesh for FEM)
186#[derive(Debug, Clone, Serialize, Deserialize)]
187pub struct RoomMesh {
188    /// Node positions
189    pub nodes: Vec<Point3D>,
190    /// Surface elements
191    pub elements: Vec<SurfaceElement>,
192}
193
194impl RoomMesh {
195    /// Create an empty mesh
196    pub fn new() -> Self {
197        Self {
198            nodes: Vec::new(),
199            elements: Vec::new(),
200        }
201    }
202
203    /// Get the number of nodes
204    pub fn num_nodes(&self) -> usize {
205        self.nodes.len()
206    }
207
208    /// Get the number of elements
209    pub fn num_elements(&self) -> usize {
210        self.elements.len()
211    }
212
213    /// Compute the centroid of an element
214    pub fn element_centroid(&self, element_idx: usize) -> Point3D {
215        let element = &self.elements[element_idx];
216        let mut centroid = Point3D::zero();
217        for &node_idx in &element.nodes {
218            centroid = centroid + self.nodes[node_idx];
219        }
220        centroid * (1.0 / element.nodes.len() as f64)
221    }
222
223    /// Compute the normal of a triangular element
224    pub fn element_normal(&self, element_idx: usize) -> Option<Point3D> {
225        let element = &self.elements[element_idx];
226        if element.nodes.len() < 3 {
227            return None;
228        }
229
230        let p0 = self.nodes[element.nodes[0]];
231        let p1 = self.nodes[element.nodes[1]];
232        let p2 = self.nodes[element.nodes[2]];
233
234        let v1 = p1 - p0;
235        let v2 = p2 - p0;
236        v1.cross(&v2).normalize()
237    }
238
239    /// Compute the area of a triangular element
240    pub fn element_area(&self, element_idx: usize) -> f64 {
241        let element = &self.elements[element_idx];
242        if element.nodes.len() < 3 {
243            return 0.0;
244        }
245
246        let p0 = self.nodes[element.nodes[0]];
247        let p1 = self.nodes[element.nodes[1]];
248        let p2 = self.nodes[element.nodes[2]];
249
250        let v1 = p1 - p0;
251        let v2 = p2 - p0;
252        v1.cross(&v2).length() * 0.5
253    }
254}
255
256impl Default for RoomMesh {
257    fn default() -> Self {
258        Self::new()
259    }
260}
261
262/// Constants for room acoustics
263pub mod constants {
264    /// Speed of sound at 20°C in m/s
265    pub const SPEED_OF_SOUND_20C: f64 = 343.0;
266
267    /// Reference pressure for SPL calculation (20 μPa)
268    pub const REFERENCE_PRESSURE: f64 = 20e-6;
269
270    /// Air density at 20°C in kg/m³
271    pub const AIR_DENSITY_20C: f64 = 1.204;
272}
273
274/// Calculate wavenumber k = 2πf/c
275pub fn wavenumber(frequency: f64, speed_of_sound: f64) -> f64 {
276    2.0 * PI * frequency / speed_of_sound
277}
278
279/// Convert complex pressure to SPL in dB
280pub fn pressure_to_spl(pressure: num_complex::Complex64) -> f64 {
281    let magnitude = pressure.norm();
282    if magnitude > 1e-20 {
283        20.0 * (magnitude / constants::REFERENCE_PRESSURE).log10()
284    } else {
285        -120.0 // Very low SPL
286    }
287}
288
289/// Generate logarithmically spaced frequencies
290pub fn log_space(start: f64, end: f64, num: usize) -> Vec<f64> {
291    if num < 2 {
292        return vec![start];
293    }
294    let log_start = start.ln();
295    let log_end = end.ln();
296    (0..num)
297        .map(|i| {
298            let log_val = log_start + (log_end - log_start) * i as f64 / (num - 1) as f64;
299            log_val.exp()
300        })
301        .collect()
302}
303
304/// Generate linearly spaced frequencies
305pub fn lin_space(start: f64, end: f64, num: usize) -> Vec<f64> {
306    if num < 2 {
307        return vec![start];
308    }
309    (0..num)
310        .map(|i| start + (end - start) * i as f64 / (num - 1) as f64)
311        .collect()
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use approx::assert_relative_eq;
318
319    #[test]
320    fn test_point_distance() {
321        let p1 = Point3D::new(0.0, 0.0, 0.0);
322        let p2 = Point3D::new(3.0, 4.0, 0.0);
323        assert_relative_eq!(p1.distance_to(&p2), 5.0, epsilon = 1e-10);
324    }
325
326    #[test]
327    fn test_point_spherical() {
328        let p = Point3D::new(1.0, 0.0, 0.0);
329        let (r, theta, phi) = p.to_spherical();
330        assert_relative_eq!(r, 1.0, epsilon = 1e-10);
331        assert_relative_eq!(theta, std::f64::consts::FRAC_PI_2, epsilon = 1e-10);
332        assert_relative_eq!(phi, 0.0, epsilon = 1e-10);
333    }
334
335    #[test]
336    fn test_log_space() {
337        let freqs = log_space(20.0, 20000.0, 200);
338        assert_eq!(freqs.len(), 200);
339        assert_relative_eq!(freqs[0], 20.0, epsilon = 1e-6);
340        assert_relative_eq!(freqs[199], 20000.0, epsilon = 1e-6);
341        // Check logarithmic spacing
342        assert!(freqs[1] / freqs[0] > 1.0);
343    }
344
345    #[test]
346    fn test_wavenumber() {
347        let k = wavenumber(1000.0, 343.0);
348        assert_relative_eq!(
349            k,
350            2.0 * std::f64::consts::PI * 1000.0 / 343.0,
351            epsilon = 1e-10
352        );
353    }
354}