bem/
room_acoustics.rs

1//! Room Acoustics Simulator using BEM
2//!
3//! This module implements a 3D room acoustics simulator for calculating sound pressure
4//! levels (SPL) at listening positions from directional sources with frequency-dependent
5//! radiation patterns.
6
7mod solver;
8mod config;
9
10pub use solver::*;
11pub use config::*;
12
13use ndarray::Array2;
14use serde::{Deserialize, Serialize};
15use std::f64::consts::PI;
16
17/// 3D point in space
18#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
19pub struct Point3D {
20    /// X coordinate
21    pub x: f64,
22    /// Y coordinate
23    pub y: f64,
24    /// Z coordinate
25    pub z: f64,
26}
27
28impl Point3D {
29    /// Create a new 3D point
30    pub fn new(x: f64, y: f64, z: f64) -> Self {
31        Self { x, y, z }
32    }
33
34    /// Calculate Euclidean distance to another point
35    pub fn distance_to(&self, other: &Point3D) -> f64 {
36        let dx = self.x - other.x;
37        let dy = self.y - other.y;
38        let dz = self.z - other.z;
39        (dx * dx + dy * dy + dz * dz).sqrt()
40    }
41
42    /// Convert to spherical coordinates (r, theta, phi)
43    pub fn to_spherical(&self) -> (f64, f64, f64) {
44        let r = (self.x * self.x + self.y * self.y + self.z * self.z).sqrt();
45        let theta = (self.z / r).acos(); // polar angle (0 to π)
46        let phi = self.y.atan2(self.x); // azimuthal angle (-π to π)
47        (r, theta, phi)
48    }
49}
50
51/// Room geometry types
52#[derive(Debug, Clone, Serialize, Deserialize)]
53pub enum RoomGeometry {
54    Rectangular(RectangularRoom),
55    LShaped(LShapedRoom),
56}
57
58impl RoomGeometry {
59    pub fn generate_mesh(&self, elements_per_meter: usize) -> RoomMesh {
60        match self {
61            RoomGeometry::Rectangular(room) => room.generate_mesh(elements_per_meter),
62            RoomGeometry::LShaped(room) => room.generate_mesh(elements_per_meter),
63        }
64    }
65
66    /// Generate frequency-adaptive mesh with selective refinement
67    ///
68    /// Refines mesh based on:
69    /// - Wavelength criterion (λ/6 to λ/10 per element)
70    /// - Distance to sources (finer near sources)
71    /// - Geometric features (corners, edges get extra refinement)
72    pub fn generate_adaptive_mesh(
73        &self,
74        base_elements_per_meter: usize,
75        frequency: f64,
76        sources: &[Source],
77        speed_of_sound: f64,
78    ) -> RoomMesh {
79        match self {
80            RoomGeometry::Rectangular(room) => {
81                room.generate_adaptive_mesh(base_elements_per_meter, frequency, sources, speed_of_sound)
82            }
83            RoomGeometry::LShaped(room) => {
84                room.generate_adaptive_mesh(base_elements_per_meter, frequency, sources, speed_of_sound)
85            }
86        }
87    }
88
89    pub fn get_edges(&self) -> Vec<(Point3D, Point3D)> {
90        match self {
91            RoomGeometry::Rectangular(room) => room.get_edges(),
92            RoomGeometry::LShaped(room) => room.get_edges(),
93        }
94    }
95}
96
97/// Surface information for adaptive mesh generation
98struct SurfaceInfo {
99    origin: Point3D,
100    u_dir: Point3D,
101    v_dir: Point3D,
102    u_length: f64,
103    v_length: f64,
104}
105
106/// Rectangular room defined by dimensions
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct RectangularRoom {
109    pub width: f64,  // x dimension
110    pub depth: f64,  // y dimension
111    pub height: f64, // z dimension
112}
113
114impl RectangularRoom {
115    pub fn new(width: f64, depth: f64, height: f64) -> Self {
116        Self {
117            width,
118            depth,
119            height,
120        }
121    }
122
123    /// Generate surface mesh for BEM
124    pub fn generate_mesh(&self, elements_per_meter: usize) -> RoomMesh {
125        let nx = (self.width * elements_per_meter as f64).ceil() as usize;
126        let ny = (self.depth * elements_per_meter as f64).ceil() as usize;
127        let nz = (self.height * elements_per_meter as f64).ceil() as usize;
128
129        let mut nodes = Vec::new();
130        let mut elements = Vec::new();
131
132        // Floor (z=0)
133        self.add_surface_mesh(
134            &mut nodes,
135            &mut elements,
136            Point3D::new(0.0, 0.0, 0.0),
137            Point3D::new(self.width, 0.0, 0.0),
138            Point3D::new(0.0, self.depth, 0.0),
139            nx,
140            ny,
141        );
142
143        // Ceiling (z=height)
144        self.add_surface_mesh(
145            &mut nodes,
146            &mut elements,
147            Point3D::new(0.0, 0.0, self.height),
148            Point3D::new(self.width, 0.0, self.height),
149            Point3D::new(0.0, self.depth, self.height),
150            nx,
151            ny,
152        );
153
154        // Front wall (y=0)
155        self.add_surface_mesh(
156            &mut nodes,
157            &mut elements,
158            Point3D::new(0.0, 0.0, 0.0),
159            Point3D::new(self.width, 0.0, 0.0),
160            Point3D::new(0.0, 0.0, self.height),
161            nx,
162            nz,
163        );
164
165        // Back wall (y=depth)
166        self.add_surface_mesh(
167            &mut nodes,
168            &mut elements,
169            Point3D::new(0.0, self.depth, 0.0),
170            Point3D::new(self.width, self.depth, 0.0),
171            Point3D::new(0.0, self.depth, self.height),
172            nx,
173            nz,
174        );
175
176        // Left wall (x=0)
177        self.add_surface_mesh(
178            &mut nodes,
179            &mut elements,
180            Point3D::new(0.0, 0.0, 0.0),
181            Point3D::new(0.0, self.depth, 0.0),
182            Point3D::new(0.0, 0.0, self.height),
183            ny,
184            nz,
185        );
186
187        // Right wall (x=width)
188        self.add_surface_mesh(
189            &mut nodes,
190            &mut elements,
191            Point3D::new(self.width, 0.0, 0.0),
192            Point3D::new(self.width, self.depth, 0.0),
193            Point3D::new(self.width, 0.0, self.height),
194            ny,
195            nz,
196        );
197
198        RoomMesh { nodes, elements }
199    }
200
201    /// Generate frequency-adaptive mesh with selective refinement
202    pub fn generate_adaptive_mesh(
203        &self,
204        base_elements_per_meter: usize,
205        frequency: f64,
206        sources: &[Source],
207        speed_of_sound: f64,
208    ) -> RoomMesh {
209        let wavelength = speed_of_sound / frequency;
210
211        // Target: λ/8 per element as a good compromise
212        let target_element_size = wavelength / 8.0;
213
214        let mut nodes = Vec::new();
215        let mut elements = Vec::new();
216
217        // Define all 6 surfaces with their refinement strategies
218        let surfaces = vec![
219            // Floor (z=0)
220            SurfaceInfo {
221                origin: Point3D::new(0.0, 0.0, 0.0),
222                u_dir: Point3D::new(self.width, 0.0, 0.0),
223                v_dir: Point3D::new(0.0, self.depth, 0.0),
224                u_length: self.width,
225                v_length: self.depth,
226            },
227            // Ceiling (z=height)
228            SurfaceInfo {
229                origin: Point3D::new(0.0, 0.0, self.height),
230                u_dir: Point3D::new(self.width, 0.0, self.height),
231                v_dir: Point3D::new(0.0, self.depth, self.height),
232                u_length: self.width,
233                v_length: self.depth,
234            },
235            // Front wall (y=0)
236            SurfaceInfo {
237                origin: Point3D::new(0.0, 0.0, 0.0),
238                u_dir: Point3D::new(self.width, 0.0, 0.0),
239                v_dir: Point3D::new(0.0, 0.0, self.height),
240                u_length: self.width,
241                v_length: self.height,
242            },
243            // Back wall (y=depth)
244            SurfaceInfo {
245                origin: Point3D::new(0.0, self.depth, 0.0),
246                u_dir: Point3D::new(self.width, self.depth, 0.0),
247                v_dir: Point3D::new(0.0, self.depth, self.height),
248                u_length: self.width,
249                v_length: self.height,
250            },
251            // Left wall (x=0)
252            SurfaceInfo {
253                origin: Point3D::new(0.0, 0.0, 0.0),
254                u_dir: Point3D::new(0.0, self.depth, 0.0),
255                v_dir: Point3D::new(0.0, 0.0, self.height),
256                u_length: self.depth,
257                v_length: self.height,
258            },
259            // Right wall (x=width)
260            SurfaceInfo {
261                origin: Point3D::new(self.width, 0.0, 0.0),
262                u_dir: Point3D::new(self.width, self.depth, 0.0),
263                v_dir: Point3D::new(self.width, 0.0, self.height),
264                u_length: self.depth,
265                v_length: self.height,
266            },
267        ];
268
269        for surface in surfaces {
270            self.add_adaptive_surface_mesh(
271                &mut nodes,
272                &mut elements,
273                &surface,
274                target_element_size,
275                base_elements_per_meter,
276                sources,
277            );
278        }
279
280        RoomMesh { nodes, elements }
281    }
282
283    fn add_adaptive_surface_mesh(
284        &self,
285        nodes: &mut Vec<Point3D>,
286        elements: &mut Vec<SurfaceElement>,
287        surface: &SurfaceInfo,
288        target_element_size: f64,
289        base_elements_per_meter: usize,
290        sources: &[Source],
291    ) {
292        // Compute adaptive resolution based on:
293        // 1. Target element size from wavelength
294        // 2. Distance to sources (refine near sources)
295        // 3. Base resolution
296
297        // Base number of elements from wavelength criterion
298        let nu_base = (surface.u_length / target_element_size).ceil() as usize;
299        let nv_base = (surface.v_length / target_element_size).ceil() as usize;
300
301        // Ensure minimum resolution from base setting
302        let nu_min = (surface.u_length * base_elements_per_meter as f64).ceil() as usize;
303        let nv_min = (surface.v_length * base_elements_per_meter as f64).ceil() as usize;
304
305        // Use the finer of the two criteria
306        let nu = nu_base.max(nu_min);
307        let nv = nv_base.max(nv_min);
308
309        // Check if this surface is near any source
310        let near_source = sources.iter().any(|source| {
311            let dist_to_surface = self.distance_point_to_surface(&source.position, surface);
312            dist_to_surface < target_element_size * 2.0
313        });
314
315        // Refine 2x near sources
316        let (nu_final, nv_final) = if near_source {
317            (nu * 2, nv * 2)
318        } else {
319            (nu, nv)
320        };
321
322        // Generate the mesh for this surface
323        let base_idx = nodes.len();
324
325        // Generate nodes with optional grading near edges/corners
326        for j in 0..=nv_final {
327            for i in 0..=nu_final {
328                // Use graded spacing near edges (0 and 1) for better corner resolution
329                let u = self.graded_parameter(i as f64 / nu_final as f64);
330                let v = self.graded_parameter(j as f64 / nv_final as f64);
331
332                let u_vec = Point3D::new(
333                    surface.u_dir.x - surface.origin.x,
334                    surface.u_dir.y - surface.origin.y,
335                    surface.u_dir.z - surface.origin.z,
336                );
337                let v_vec = Point3D::new(
338                    surface.v_dir.x - surface.origin.x,
339                    surface.v_dir.y - surface.origin.y,
340                    surface.v_dir.z - surface.origin.z,
341                );
342
343                let node = Point3D::new(
344                    surface.origin.x + u * u_vec.x + v * v_vec.x,
345                    surface.origin.y + u * u_vec.y + v * v_vec.y,
346                    surface.origin.z + u * u_vec.z + v * v_vec.z,
347                );
348
349                nodes.push(node);
350            }
351        }
352
353        // Generate triangular elements
354        for j in 0..nv_final {
355            for i in 0..nu_final {
356                let i0 = base_idx + j * (nu_final + 1) + i;
357                let i1 = i0 + 1;
358                let i2 = i0 + (nu_final + 1);
359                let i3 = i2 + 1;
360
361                // First triangle
362                elements.push(SurfaceElement {
363                    nodes: vec![i0, i1, i2],
364                });
365
366                // Second triangle
367                elements.push(SurfaceElement {
368                    nodes: vec![i1, i3, i2],
369                });
370            }
371        }
372    }
373
374    /// Graded parameter for finer spacing near edges (0 and 1)
375    /// Uses smooth transition with quadratic grading
376    fn graded_parameter(&self, t: f64) -> f64 {
377        // Linear near center, finer near edges
378        // Grading strength: 0.1 means subtle grading, 0.3 means stronger
379        let grading = 0.15;
380
381        if t < grading {
382            // Near t=0: quadratic densification
383            0.5 * (t / grading).powi(2) * grading
384        } else if t > 1.0 - grading {
385            // Near t=1: quadratic densification
386            let t_rel = (t - (1.0 - grading)) / grading;
387            1.0 - 0.5 * grading * (1.0 - t_rel).powi(2)
388        } else {
389            // Linear in the middle
390            let t_mid = (t - grading) / (1.0 - 2.0 * grading);
391            grading + t_mid * (1.0 - 2.0 * grading)
392        }
393    }
394
395    /// Compute distance from a point to a surface
396    fn distance_point_to_surface(&self, point: &Point3D, surface: &SurfaceInfo) -> f64 {
397        // Compute plane normal
398        let u_vec = Point3D::new(
399            surface.u_dir.x - surface.origin.x,
400            surface.u_dir.y - surface.origin.y,
401            surface.u_dir.z - surface.origin.z,
402        );
403        let v_vec = Point3D::new(
404            surface.v_dir.x - surface.origin.x,
405            surface.v_dir.y - surface.origin.y,
406            surface.v_dir.z - surface.origin.z,
407        );
408
409        // Cross product for normal
410        let normal = Point3D::new(
411            u_vec.y * v_vec.z - u_vec.z * v_vec.y,
412            u_vec.z * v_vec.x - u_vec.x * v_vec.z,
413            u_vec.x * v_vec.y - u_vec.y * v_vec.x,
414        );
415
416        let normal_length = (normal.x * normal.x + normal.y * normal.y + normal.z * normal.z).sqrt();
417
418        if normal_length < 1e-10 {
419            return point.distance_to(&surface.origin);
420        }
421
422        // Normalized normal
423        let nx = normal.x / normal_length;
424        let ny = normal.y / normal_length;
425        let nz = normal.z / normal_length;
426
427        // Distance to plane
428        let dx = point.x - surface.origin.x;
429        let dy = point.y - surface.origin.y;
430        let dz = point.z - surface.origin.z;
431
432        (dx * nx + dy * ny + dz * nz).abs()
433    }
434
435    /// Get room edges for visualization
436    pub fn get_edges(&self) -> Vec<(Point3D, Point3D)> {
437        vec![
438            // Floor edges
439            (Point3D::new(0.0, 0.0, 0.0), Point3D::new(self.width, 0.0, 0.0)),
440            (Point3D::new(self.width, 0.0, 0.0), Point3D::new(self.width, self.depth, 0.0)),
441            (Point3D::new(self.width, self.depth, 0.0), Point3D::new(0.0, self.depth, 0.0)),
442            (Point3D::new(0.0, self.depth, 0.0), Point3D::new(0.0, 0.0, 0.0)),
443            // Ceiling edges
444            (Point3D::new(0.0, 0.0, self.height), Point3D::new(self.width, 0.0, self.height)),
445            (Point3D::new(self.width, 0.0, self.height), Point3D::new(self.width, self.depth, self.height)),
446            (Point3D::new(self.width, self.depth, self.height), Point3D::new(0.0, self.depth, self.height)),
447            (Point3D::new(0.0, self.depth, self.height), Point3D::new(0.0, 0.0, self.height)),
448            // Vertical edges
449            (Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 0.0, self.height)),
450            (Point3D::new(self.width, 0.0, 0.0), Point3D::new(self.width, 0.0, self.height)),
451            (Point3D::new(self.width, self.depth, 0.0), Point3D::new(self.width, self.depth, self.height)),
452            (Point3D::new(0.0, self.depth, 0.0), Point3D::new(0.0, self.depth, self.height)),
453        ]
454    }
455
456    fn add_surface_mesh(
457        &self,
458        nodes: &mut Vec<Point3D>,
459        elements: &mut Vec<SurfaceElement>,
460        origin: Point3D,
461        u_dir: Point3D,
462        v_dir: Point3D,
463        nu: usize,
464        nv: usize,
465    ) {
466        let base_idx = nodes.len();
467
468        // Generate nodes
469        for j in 0..=nv {
470            for i in 0..=nu {
471                let u = i as f64 / nu as f64;
472                let v = j as f64 / nv as f64;
473
474                let node = Point3D::new(
475                    origin.x + u * (u_dir.x - origin.x) + v * (v_dir.x - origin.x),
476                    origin.y + u * (u_dir.y - origin.y) + v * (v_dir.y - origin.y),
477                    origin.z + u * (u_dir.z - origin.z) + v * (v_dir.z - origin.z),
478                );
479                nodes.push(node);
480            }
481        }
482
483        // Generate quadrilateral elements
484        for j in 0..nv {
485            for i in 0..nu {
486                let n0 = base_idx + j * (nu + 1) + i;
487                let n1 = base_idx + j * (nu + 1) + i + 1;
488                let n2 = base_idx + (j + 1) * (nu + 1) + i + 1;
489                let n3 = base_idx + (j + 1) * (nu + 1) + i;
490
491                elements.push(SurfaceElement {
492                    nodes: vec![n0, n1, n2, n3],
493                });
494            }
495        }
496    }
497}
498
499/// L-shaped room defined by two rectangular sections
500/// Section 1: main room (0, 0) to (width1, depth1)
501/// Section 2: extension from (0, depth1) to (width2, depth1 + depth2)
502#[derive(Debug, Clone, Serialize, Deserialize)]
503pub struct LShapedRoom {
504    pub width1: f64,   // Main section width (x)
505    pub depth1: f64,   // Main section depth (y)
506    pub width2: f64,   // Extension width (x), typically < width1
507    pub depth2: f64,   // Extension depth (y)
508    pub height: f64,   // Common height for both sections
509}
510
511impl LShapedRoom {
512    pub fn new(width1: f64, depth1: f64, width2: f64, depth2: f64, height: f64) -> Self {
513        Self {
514            width1,
515            depth1,
516            width2,
517            depth2,
518            height,
519        }
520    }
521
522    /// Generate surface mesh for L-shaped room
523    pub fn generate_mesh(&self, elements_per_meter: usize) -> RoomMesh {
524        let mut nodes = Vec::new();
525        let mut elements = Vec::new();
526
527        // Main section dimensions
528        let nx1 = (self.width1 * elements_per_meter as f64).ceil() as usize;
529        let ny1 = (self.depth1 * elements_per_meter as f64).ceil() as usize;
530
531        // Extension dimensions
532        let nx2 = (self.width2 * elements_per_meter as f64).ceil() as usize;
533        let ny2 = (self.depth2 * elements_per_meter as f64).ceil() as usize;
534
535        let nz = (self.height * elements_per_meter as f64).ceil() as usize;
536
537        // Floor - Main section
538        self.add_surface_mesh_lshaped(
539            &mut nodes,
540            &mut elements,
541            Point3D::new(0.0, 0.0, 0.0),
542            Point3D::new(self.width1, 0.0, 0.0),
543            Point3D::new(0.0, self.depth1, 0.0),
544            nx1,
545            ny1,
546        );
547
548        // Floor - Extension section
549        self.add_surface_mesh_lshaped(
550            &mut nodes,
551            &mut elements,
552            Point3D::new(0.0, self.depth1, 0.0),
553            Point3D::new(self.width2, self.depth1, 0.0),
554            Point3D::new(0.0, self.depth1 + self.depth2, 0.0),
555            nx2,
556            ny2,
557        );
558
559        // Ceiling - Main section
560        self.add_surface_mesh_lshaped(
561            &mut nodes,
562            &mut elements,
563            Point3D::new(0.0, 0.0, self.height),
564            Point3D::new(self.width1, 0.0, self.height),
565            Point3D::new(0.0, self.depth1, self.height),
566            nx1,
567            ny1,
568        );
569
570        // Ceiling - Extension section
571        self.add_surface_mesh_lshaped(
572            &mut nodes,
573            &mut elements,
574            Point3D::new(0.0, self.depth1, self.height),
575            Point3D::new(self.width2, self.depth1, self.height),
576            Point3D::new(0.0, self.depth1 + self.depth2, self.height),
577            nx2,
578            ny2,
579        );
580
581        // Walls - Main section
582        // Front wall (y=0)
583        self.add_surface_mesh_lshaped(
584            &mut nodes,
585            &mut elements,
586            Point3D::new(0.0, 0.0, 0.0),
587            Point3D::new(self.width1, 0.0, 0.0),
588            Point3D::new(0.0, 0.0, self.height),
589            nx1,
590            nz,
591        );
592
593        // Right wall of main section (x=width1)
594        self.add_surface_mesh_lshaped(
595            &mut nodes,
596            &mut elements,
597            Point3D::new(self.width1, 0.0, 0.0),
598            Point3D::new(self.width1, self.depth1, 0.0),
599            Point3D::new(self.width1, 0.0, self.height),
600            ny1,
601            nz,
602        );
603
604        // Left wall (x=0) - full height
605        let total_depth = self.depth1 + self.depth2;
606        let ny_total = ((total_depth) * elements_per_meter as f64).ceil() as usize;
607        self.add_surface_mesh_lshaped(
608            &mut nodes,
609            &mut elements,
610            Point3D::new(0.0, 0.0, 0.0),
611            Point3D::new(0.0, total_depth, 0.0),
612            Point3D::new(0.0, 0.0, self.height),
613            ny_total,
614            nz,
615        );
616
617        // Back wall of extension (y=depth1+depth2)
618        self.add_surface_mesh_lshaped(
619            &mut nodes,
620            &mut elements,
621            Point3D::new(0.0, total_depth, 0.0),
622            Point3D::new(self.width2, total_depth, 0.0),
623            Point3D::new(0.0, total_depth, self.height),
624            nx2,
625            nz,
626        );
627
628        // Right wall of extension (x=width2)
629        self.add_surface_mesh_lshaped(
630            &mut nodes,
631            &mut elements,
632            Point3D::new(self.width2, self.depth1, 0.0),
633            Point3D::new(self.width2, total_depth, 0.0),
634            Point3D::new(self.width2, self.depth1, self.height),
635            ny2,
636            nz,
637        );
638
639        // Internal walls at the L junction
640        // Vertical wall from (width2, depth1) to (width1, depth1)
641        let internal_width = self.width1 - self.width2;
642        let nx_internal = (internal_width * elements_per_meter as f64).ceil() as usize;
643        self.add_surface_mesh_lshaped(
644            &mut nodes,
645            &mut elements,
646            Point3D::new(self.width2, self.depth1, 0.0),
647            Point3D::new(self.width1, self.depth1, 0.0),
648            Point3D::new(self.width2, self.depth1, self.height),
649            nx_internal,
650            nz,
651        );
652
653        RoomMesh { nodes, elements }
654    }
655
656    /// Get room edges for visualization
657    pub fn get_edges(&self) -> Vec<(Point3D, Point3D)> {
658        let total_depth = self.depth1 + self.depth2;
659        vec![
660            // Floor edges - Main section
661            (Point3D::new(0.0, 0.0, 0.0), Point3D::new(self.width1, 0.0, 0.0)),
662            (Point3D::new(self.width1, 0.0, 0.0), Point3D::new(self.width1, self.depth1, 0.0)),
663            (Point3D::new(self.width1, self.depth1, 0.0), Point3D::new(self.width2, self.depth1, 0.0)),
664            (Point3D::new(self.width2, self.depth1, 0.0), Point3D::new(self.width2, total_depth, 0.0)),
665            (Point3D::new(self.width2, total_depth, 0.0), Point3D::new(0.0, total_depth, 0.0)),
666            (Point3D::new(0.0, total_depth, 0.0), Point3D::new(0.0, 0.0, 0.0)),
667
668            // Ceiling edges
669            (Point3D::new(0.0, 0.0, self.height), Point3D::new(self.width1, 0.0, self.height)),
670            (Point3D::new(self.width1, 0.0, self.height), Point3D::new(self.width1, self.depth1, self.height)),
671            (Point3D::new(self.width1, self.depth1, self.height), Point3D::new(self.width2, self.depth1, self.height)),
672            (Point3D::new(self.width2, self.depth1, self.height), Point3D::new(self.width2, total_depth, self.height)),
673            (Point3D::new(self.width2, total_depth, self.height), Point3D::new(0.0, total_depth, self.height)),
674            (Point3D::new(0.0, total_depth, self.height), Point3D::new(0.0, 0.0, self.height)),
675
676            // Vertical edges
677            (Point3D::new(0.0, 0.0, 0.0), Point3D::new(0.0, 0.0, self.height)),
678            (Point3D::new(self.width1, 0.0, 0.0), Point3D::new(self.width1, 0.0, self.height)),
679            (Point3D::new(self.width1, self.depth1, 0.0), Point3D::new(self.width1, self.depth1, self.height)),
680            (Point3D::new(self.width2, self.depth1, 0.0), Point3D::new(self.width2, self.depth1, self.height)),
681            (Point3D::new(self.width2, total_depth, 0.0), Point3D::new(self.width2, total_depth, self.height)),
682            (Point3D::new(0.0, total_depth, 0.0), Point3D::new(0.0, total_depth, self.height)),
683        ]
684    }
685
686    fn add_surface_mesh_lshaped(
687        &self,
688        nodes: &mut Vec<Point3D>,
689        elements: &mut Vec<SurfaceElement>,
690        origin: Point3D,
691        u_dir: Point3D,
692        v_dir: Point3D,
693        nu: usize,
694        nv: usize,
695    ) {
696        let base_idx = nodes.len();
697
698        // Generate nodes
699        for j in 0..=nv {
700            for i in 0..=nu {
701                let u = i as f64 / nu as f64;
702                let v = j as f64 / nv as f64;
703
704                let node = Point3D::new(
705                    origin.x + u * (u_dir.x - origin.x) + v * (v_dir.x - origin.x),
706                    origin.y + u * (u_dir.y - origin.y) + v * (v_dir.y - origin.y),
707                    origin.z + u * (u_dir.z - origin.z) + v * (v_dir.z - origin.z),
708                );
709                nodes.push(node);
710            }
711        }
712
713        // Generate quadrilateral elements
714        for j in 0..nv {
715            for i in 0..nu {
716                let n0 = base_idx + j * (nu + 1) + i;
717                let n1 = base_idx + j * (nu + 1) + i + 1;
718                let n2 = base_idx + (j + 1) * (nu + 1) + i + 1;
719                let n3 = base_idx + (j + 1) * (nu + 1) + i;
720
721                elements.push(SurfaceElement {
722                    nodes: vec![n0, n1, n2, n3],
723                });
724            }
725        }
726    }
727
728    /// Generate frequency-adaptive mesh (stub - uses regular mesh for now)
729    pub fn generate_adaptive_mesh(
730        &self,
731        base_elements_per_meter: usize,
732        _frequency: f64,
733        _sources: &[Source],
734        _speed_of_sound: f64,
735    ) -> RoomMesh {
736        // TODO: Implement adaptive meshing for L-shaped rooms
737        // For now, just use the regular mesh
738        self.generate_mesh(base_elements_per_meter)
739    }
740}
741
742/// Surface element (quadrilateral)
743#[derive(Debug, Clone, Serialize, Deserialize)]
744pub struct SurfaceElement {
745    pub nodes: Vec<usize>,
746}
747
748/// Room mesh for BEM
749#[derive(Debug, Clone, Serialize, Deserialize)]
750pub struct RoomMesh {
751    pub nodes: Vec<Point3D>,
752    pub elements: Vec<SurfaceElement>,
753}
754
755/// Directivity pattern sampled on a grid
756#[derive(Debug, Clone, Serialize, Deserialize)]
757pub struct DirectivityPattern {
758    /// Horizontal angles (azimuth) in degrees [0, 360) with step 10°
759    pub horizontal_angles: Vec<f64>,
760    /// Vertical angles (elevation) in degrees [0, 180] with step 10°
761    pub vertical_angles: Vec<f64>,
762    /// Magnitude at each (horizontal, vertical) angle pair
763    /// Shape: [n_vertical, n_horizontal]
764    pub magnitude: Array2<f64>,
765}
766
767impl DirectivityPattern {
768    /// Create omnidirectional pattern (uniform radiation)
769    pub fn omnidirectional() -> Self {
770        let horizontal_angles: Vec<f64> = (0..36).map(|i| i as f64 * 10.0).collect();
771        let vertical_angles: Vec<f64> = (0..19).map(|i| i as f64 * 10.0).collect();
772
773        let magnitude = Array2::ones((vertical_angles.len(), horizontal_angles.len()));
774
775        Self {
776            horizontal_angles,
777            vertical_angles,
778            magnitude,
779        }
780    }
781
782    /// Interpolate directivity at arbitrary direction
783    pub fn interpolate(&self, theta: f64, phi: f64) -> f64 {
784        // Convert spherical to degrees
785        let theta_deg = theta.to_degrees();
786        let mut phi_deg = phi.to_degrees();
787
788        // Normalize phi to [0, 360)
789        while phi_deg < 0.0 {
790            phi_deg += 360.0;
791        }
792        while phi_deg >= 360.0 {
793            phi_deg -= 360.0;
794        }
795
796        // Find surrounding angles
797        let h_idx = (phi_deg / 10.0).floor() as usize;
798        let v_idx = (theta_deg / 10.0).floor() as usize;
799
800        let h_idx = h_idx.min(self.horizontal_angles.len() - 1);
801        let v_idx = v_idx.min(self.vertical_angles.len() - 1);
802
803        let h_next = (h_idx + 1) % self.horizontal_angles.len();
804        let v_next = (v_idx + 1).min(self.vertical_angles.len() - 1);
805
806        // Bilinear interpolation
807        let h_frac = (phi_deg / 10.0) - h_idx as f64;
808        let v_frac = (theta_deg / 10.0) - v_idx as f64;
809
810        let m00 = self.magnitude[[v_idx, h_idx]];
811        let m01 = self.magnitude[[v_idx, h_next]];
812        let m10 = self.magnitude[[v_next, h_idx]];
813        let m11 = self.magnitude[[v_next, h_next]];
814
815        let m0 = m00 * (1.0 - h_frac) + m01 * h_frac;
816        let m1 = m10 * (1.0 - h_frac) + m11 * h_frac;
817
818        m0 * (1.0 - v_frac) + m1 * v_frac
819    }
820}
821
822/// Crossover filter for frequency-limited sources
823#[derive(Debug, Clone, Serialize, Deserialize)]
824pub enum CrossoverFilter {
825    /// Full range (no filtering)
826    FullRange,
827    /// Lowpass filter (for subwoofers)
828    Lowpass {
829        cutoff_freq: f64,
830        order: u32, // Filter order (2, 4, 6, 8)
831    },
832    /// Highpass filter (for tweeters/small speakers)
833    Highpass {
834        cutoff_freq: f64,
835        order: u32,
836    },
837    /// Bandpass filter
838    Bandpass {
839        low_cutoff: f64,
840        high_cutoff: f64,
841        order: u32,
842    },
843}
844
845impl CrossoverFilter {
846    /// Get amplitude multiplier at a given frequency
847    pub fn amplitude_at_frequency(&self, frequency: f64) -> f64 {
848        match self {
849            CrossoverFilter::FullRange => 1.0,
850            CrossoverFilter::Lowpass { cutoff_freq, order } => {
851                let ratio = frequency / cutoff_freq;
852                let response = 1.0 / (1.0 + ratio.powi(*order as i32 * 2)).sqrt();
853                response
854            }
855            CrossoverFilter::Highpass { cutoff_freq, order } => {
856                let ratio = cutoff_freq / frequency;
857                let response = 1.0 / (1.0 + ratio.powi(*order as i32 * 2)).sqrt();
858                response
859            }
860            CrossoverFilter::Bandpass {
861                low_cutoff,
862                high_cutoff,
863                order,
864            } => {
865                // Cascade of highpass and lowpass
866                let high_ratio = low_cutoff / frequency;
867                let low_ratio = frequency / high_cutoff;
868                let hp_response = 1.0 / (1.0 + high_ratio.powi(*order as i32 * 2)).sqrt();
869                let lp_response = 1.0 / (1.0 + low_ratio.powi(*order as i32 * 2)).sqrt();
870                hp_response * lp_response
871            }
872        }
873    }
874}
875
876/// Sound source with position and directivity
877#[derive(Debug, Clone, Serialize, Deserialize)]
878pub struct Source {
879    pub position: Point3D,
880    pub directivity: DirectivityPattern,
881    pub amplitude: f64, // Source strength
882    pub crossover: CrossoverFilter,
883    pub name: String, // Optional name for the source (e.g., "Left Main", "Subwoofer")
884}
885
886impl Source {
887    pub fn new(position: Point3D, directivity: DirectivityPattern, amplitude: f64) -> Self {
888        Self {
889            position,
890            directivity,
891            amplitude,
892            crossover: CrossoverFilter::FullRange,
893            name: String::from("Source"),
894        }
895    }
896
897    pub fn with_crossover(mut self, crossover: CrossoverFilter) -> Self {
898        self.crossover = crossover;
899        self
900    }
901
902    pub fn with_name(mut self, name: String) -> Self {
903        self.name = name;
904        self
905    }
906
907    /// Get directional amplitude towards a point at a specific frequency
908    pub fn amplitude_towards(&self, point: &Point3D, frequency: f64) -> f64 {
909        let dx = point.x - self.position.x;
910        let dy = point.y - self.position.y;
911        let dz = point.z - self.position.z;
912
913        let r = (dx * dx + dy * dy + dz * dz).sqrt();
914        if r < 1e-10 {
915            return self.amplitude * self.crossover.amplitude_at_frequency(frequency);
916        }
917
918        let theta = (dz / r).acos();
919        let phi = dy.atan2(dx);
920
921        let directivity_factor = self.directivity.interpolate(theta, phi);
922        let crossover_factor = self.crossover.amplitude_at_frequency(frequency);
923        self.amplitude * directivity_factor * crossover_factor
924    }
925}
926
927/// Listening position
928pub type ListeningPosition = Point3D;
929
930/// Room acoustics simulation configuration
931#[derive(Debug, Clone, Serialize, Deserialize)]
932pub struct RoomSimulation {
933    pub room: RoomGeometry,
934    pub sources: Vec<Source>,
935    pub listening_positions: Vec<ListeningPosition>,
936    pub frequencies: Vec<f64>,
937    pub speed_of_sound: f64,
938}
939
940impl RoomSimulation {
941    pub fn new(
942        room: RoomGeometry,
943        sources: Vec<Source>,
944        listening_positions: Vec<ListeningPosition>,
945    ) -> Self {
946        // Generate logarithmically spaced frequencies 20Hz to 20kHz
947        let frequencies = Self::log_space(20.0, 20000.0, 200);
948
949        Self {
950            room,
951            sources,
952            listening_positions,
953            frequencies,
954            speed_of_sound: 343.0, // m/s at 20°C
955        }
956    }
957
958    /// Create simulation with custom frequency configuration
959    pub fn with_frequencies(
960        room: RoomGeometry,
961        sources: Vec<Source>,
962        listening_positions: Vec<ListeningPosition>,
963        min_freq: f64,
964        max_freq: f64,
965        num_points: usize,
966    ) -> Self {
967        let frequencies = Self::log_space(min_freq, max_freq, num_points);
968
969        Self {
970            room,
971            sources,
972            listening_positions,
973            frequencies,
974            speed_of_sound: 343.0,
975        }
976    }
977
978    fn log_space(start: f64, end: f64, num: usize) -> Vec<f64> {
979        let log_start = start.ln();
980        let log_end = end.ln();
981        (0..num)
982            .map(|i| {
983                let log_val = log_start + (log_end - log_start) * i as f64 / (num - 1) as f64;
984                log_val.exp()
985            })
986            .collect()
987    }
988
989    /// Calculate wavenumber k = 2π f / c
990    pub fn wavenumber(&self, frequency: f64) -> f64 {
991        2.0 * PI * frequency / self.speed_of_sound
992    }
993}
994
995/// Result of room simulation at one frequency
996#[derive(Debug, Clone, Serialize, Deserialize)]
997pub struct FrequencyResult {
998    pub frequency: f64,
999    pub spl_at_lp: Vec<f64>, // SPL (dB) at each listening position
1000}
1001
1002/// Complete simulation results
1003#[derive(Debug, Clone, Serialize, Deserialize)]
1004pub struct SimulationResults {
1005    pub frequencies: Vec<f64>,
1006    pub lp_frequency_responses: Vec<Vec<f64>>, // [n_lp][n_freq]
1007    pub horizontal_slice: Option<SliceData>,
1008    pub vertical_slice: Option<SliceData>,
1009}
1010
1011/// Pressure field data on a 2D slice
1012#[derive(Debug, Clone, Serialize, Deserialize)]
1013pub struct SliceData {
1014    pub x: Vec<f64>,
1015    pub y: Vec<f64>,
1016    pub spl: Array2<f64>, // [y, x]
1017    pub frequency: f64,
1018}
1019
1020#[cfg(test)]
1021mod tests {
1022    use super::*;
1023
1024    #[test]
1025    fn test_rectangular_room() {
1026        let room = RectangularRoom::new(5.0, 4.0, 3.0);
1027        assert_eq!(room.width, 5.0);
1028        assert_eq!(room.depth, 4.0);
1029        assert_eq!(room.height, 3.0);
1030    }
1031
1032    #[test]
1033    fn test_omnidirectional_pattern() {
1034        let pattern = DirectivityPattern::omnidirectional();
1035        // Should be 1.0 in all directions
1036        assert!((pattern.interpolate(0.0, 0.0) - 1.0).abs() < 1e-6);
1037        assert!((pattern.interpolate(PI / 2.0, PI) - 1.0).abs() < 1e-6);
1038        assert!((pattern.interpolate(PI, 0.0) - 1.0).abs() < 1e-6);
1039    }
1040
1041    #[test]
1042    fn test_log_space() {
1043        let freqs = RoomSimulation::log_space(20.0, 20000.0, 200);
1044        assert_eq!(freqs.len(), 200);
1045        assert!((freqs[0] - 20.0).abs() < 1e-6);
1046        assert!((freqs[199] - 20000.0).abs() < 1e-6);
1047        // Check logarithmic spacing
1048        assert!(freqs[1] / freqs[0] > 1.0);
1049    }
1050
1051    #[test]
1052    fn test_point_distance() {
1053        let p1 = Point3D::new(0.0, 0.0, 0.0);
1054        let p2 = Point3D::new(3.0, 4.0, 0.0);
1055        assert!((p1.distance_to(&p2) - 5.0).abs() < 1e-6);
1056    }
1057}