Skip to main content

math_audio_xem_common/
config.rs

1//! JSON configuration for room acoustics simulations
2
3use crate::geometry::{LShapedRoom, RectangularRoom, RoomGeometry};
4use crate::source::{CrossoverFilter, Directivity, DirectivityGrid, Source};
5use crate::types::{Point3D, log_space};
6use serde::{Deserialize, Serialize};
7use std::fs;
8use std::path::Path;
9
10/// Complete room configuration loaded from JSON
11#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RoomConfig {
13    /// Room geometry specification
14    pub room: RoomGeometryConfig,
15    /// Sound sources
16    pub sources: Vec<SourceConfig>,
17    /// Listening positions
18    pub listening_positions: Vec<Point3DConfig>,
19    /// Frequency configuration
20    pub frequencies: FrequencyConfig,
21    /// Boundary conditions
22    #[serde(default)]
23    pub boundaries: BoundaryConfig,
24    /// Solver configuration
25    #[serde(default)]
26    pub solver: SolverConfig,
27    /// Visualization configuration
28    #[serde(default)]
29    pub visualization: VisualizationConfig,
30    /// Simulation metadata
31    #[serde(default)]
32    pub metadata: MetadataConfig,
33}
34
35/// Room geometry configuration
36#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(tag = "type")]
38pub enum RoomGeometryConfig {
39    #[serde(rename = "rectangular")]
40    /// Rectangular room geometry
41    Rectangular {
42        /// Width (x-dimension)
43        width: f64,
44        /// Depth (y-dimension)
45        depth: f64,
46        /// Height (z-dimension)
47        height: f64,
48    },
49    #[serde(rename = "lshaped")]
50    /// L-shaped room geometry
51    LShaped {
52        /// Width of the main section
53        width1: f64,
54        /// Depth of the main section
55        depth1: f64,
56        /// Width of the extension
57        width2: f64,
58        /// Depth of the extension
59        depth2: f64,
60        /// Height of the room
61        height: f64,
62    },
63}
64
65impl RoomGeometryConfig {
66    /// Convert to RoomGeometry
67    pub fn to_geometry(&self) -> Result<RoomGeometry, String> {
68        match self {
69            RoomGeometryConfig::Rectangular {
70                width,
71                depth,
72                height,
73            } => Ok(RoomGeometry::Rectangular(RectangularRoom::new(
74                *width, *depth, *height,
75            ))),
76            RoomGeometryConfig::LShaped {
77                width1,
78                depth1,
79                width2,
80                depth2,
81                height,
82            } => Ok(RoomGeometry::LShaped(LShapedRoom::new(
83                *width1, *depth1, *width2, *depth2, *height,
84            ))),
85        }
86    }
87}
88
89/// Boundary conditions configuration
90#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct BoundaryConfig {
92    /// Floor boundary condition
93    #[serde(default = "default_rigid")]
94    pub floor: SurfaceConfig,
95    /// Ceiling boundary condition
96    #[serde(default = "default_rigid")]
97    pub ceiling: SurfaceConfig,
98    /// Default condition for all vertical walls
99    #[serde(default = "default_rigid")]
100    pub walls: SurfaceConfig,
101    /// Override for front wall (y=0)
102    pub front_wall: Option<SurfaceConfig>,
103    /// Override for back wall (y=depth)
104    pub back_wall: Option<SurfaceConfig>,
105    /// Override for left wall (x=0)
106    pub left_wall: Option<SurfaceConfig>,
107    /// Override for right wall (x=width)
108    pub right_wall: Option<SurfaceConfig>,
109}
110
111impl Default for BoundaryConfig {
112    fn default() -> Self {
113        Self {
114            floor: SurfaceConfig::Rigid,
115            ceiling: SurfaceConfig::Rigid,
116            walls: SurfaceConfig::Rigid,
117            front_wall: None,
118            back_wall: None,
119            left_wall: None,
120            right_wall: None,
121        }
122    }
123}
124
125fn default_rigid() -> SurfaceConfig {
126    SurfaceConfig::Rigid
127}
128
129/// Surface boundary condition type
130#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(tag = "type")]
132pub enum SurfaceConfig {
133    /// Perfectly rigid (Neumann BC, velocity = 0)
134    #[serde(rename = "rigid")]
135    Rigid,
136    /// Absorption coefficient (0.0 to 1.0)
137    #[serde(rename = "absorption")]
138    Absorption { coefficient: f64 },
139    /// Specific acoustic impedance (complex)
140    #[serde(rename = "impedance")]
141    Impedance { real: f64, imag: f64 },
142}
143
144/// 3D point configuration
145#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
146pub struct Point3DConfig {
147    /// X coordinate
148    pub x: f64,
149    /// Y coordinate
150    pub y: f64,
151    /// Z coordinate
152    pub z: f64,
153}
154
155impl From<Point3DConfig> for Point3D {
156    fn from(p: Point3DConfig) -> Self {
157        Point3D::new(p.x, p.y, p.z)
158    }
159}
160
161impl From<Point3D> for Point3DConfig {
162    fn from(p: Point3D) -> Self {
163        Point3DConfig {
164            x: p.x,
165            y: p.y,
166            z: p.z,
167        }
168    }
169}
170
171/// Source configuration
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct SourceConfig {
174    /// Source name
175    pub name: String,
176    /// Source position
177    pub position: Point3DConfig,
178    /// Source amplitude
179    #[serde(default = "default_amplitude")]
180    pub amplitude: f64,
181    /// Directivity pattern
182    #[serde(default)]
183    pub directivity: DirectivityConfig,
184    /// Crossover filter
185    #[serde(default)]
186    pub crossover: CrossoverConfig,
187}
188
189fn default_amplitude() -> f64 {
190    1.0
191}
192
193impl SourceConfig {
194    /// Convert to Source
195    pub fn to_source(&self) -> Result<Source, String> {
196        let directivity = self.directivity.to_directivity()?;
197        let crossover = self.crossover.to_filter();
198
199        let source = Source::new(self.position.into(), directivity, self.amplitude)
200            .with_name(self.name.clone())
201            .with_crossover(crossover);
202
203        Ok(source)
204    }
205}
206
207/// Directivity pattern configuration
208#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(tag = "type")]
210#[derive(Default)]
211pub enum DirectivityConfig {
212    /// Omnidirectional (spherical) radiation pattern
213    #[serde(rename = "omnidirectional")]
214    #[default]
215    Omnidirectional,
216    /// Classical frequency-dependent directional source
217    #[serde(rename = "classical")]
218    Classical {
219        /// Horizontal beamwidth (degrees)
220        horizontal_angle: f64,
221        /// Vertical beamwidth (degrees)
222        vertical_angle: f64,
223    },
224    /// Custom directivity from measured data
225    #[serde(rename = "custom")]
226    Custom {
227        /// Horizontal angles (degrees)
228        horizontal_angles: Vec<f64>,
229        /// Vertical angles (degrees)
230        vertical_angles: Vec<f64>,
231        /// Magnitude data (matrix)
232        magnitude: Vec<Vec<f64>>,
233    },
234}
235
236
237impl DirectivityConfig {
238    /// Convert to Directivity
239    pub fn to_directivity(&self) -> Result<Directivity, String> {
240        match self {
241            DirectivityConfig::Omnidirectional => {
242                Ok(Directivity::Grid(DirectivityGrid::omnidirectional()))
243            }
244            DirectivityConfig::Classical {
245                horizontal_angle,
246                vertical_angle,
247            } => Ok(Directivity::Classical {
248                h_angle: *horizontal_angle,
249                v_angle: *vertical_angle,
250            }),
251            DirectivityConfig::Custom {
252                horizontal_angles,
253                vertical_angles,
254                magnitude,
255            } => {
256                use ndarray::Array2;
257
258                if magnitude.is_empty() {
259                    return Err("Empty magnitude array".to_string());
260                }
261
262                let n_vert = magnitude.len();
263                let n_horiz = magnitude[0].len();
264
265                if n_vert != vertical_angles.len() {
266                    return Err(format!(
267                        "Vertical angles mismatch: {} vs {}",
268                        n_vert,
269                        vertical_angles.len()
270                    ));
271                }
272                if n_horiz != horizontal_angles.len() {
273                    return Err(format!(
274                        "Horizontal angles mismatch: {} vs {}",
275                        n_horiz,
276                        horizontal_angles.len()
277                    ));
278                }
279
280                let flat: Vec<f64> = magnitude
281                    .iter()
282                    .flat_map(|row| row.iter().copied())
283                    .collect();
284                let mag_array = Array2::from_shape_vec((n_vert, n_horiz), flat)
285                    .map_err(|e| format!("Failed to create magnitude array: {}", e))?;
286
287                Ok(Directivity::Grid(DirectivityGrid {
288                    horizontal_angles: horizontal_angles.clone(),
289                    vertical_angles: vertical_angles.clone(),
290                    magnitude: mag_array,
291                }))
292            }
293        }
294    }
295}
296
297/// Crossover filter configuration
298#[derive(Debug, Clone, Default, Serialize, Deserialize)]
299#[serde(tag = "type")]
300pub enum CrossoverConfig {
301    /// Full range (no filter)
302    #[serde(rename = "fullrange")]
303    #[default]
304    FullRange,
305    /// Low-pass filter
306    #[serde(rename = "lowpass")]
307    Lowpass {
308        /// Cutoff frequency (Hz)
309        cutoff_freq: f64,
310        /// Filter order (e.g., 2, 4)
311        order: u32,
312    },
313    /// High-pass filter
314    #[serde(rename = "highpass")]
315    Highpass {
316        /// Cutoff frequency (Hz)
317        cutoff_freq: f64,
318        /// Filter order (e.g., 2, 4)
319        order: u32,
320    },
321    /// Band-pass filter
322    #[serde(rename = "bandpass")]
323    Bandpass {
324        /// Low cutoff frequency (Hz)
325        low_cutoff: f64,
326        /// High cutoff frequency (Hz)
327        high_cutoff: f64,
328        /// Filter order
329        order: u32,
330    },
331}
332
333impl CrossoverConfig {
334    /// Convert to CrossoverFilter
335    pub fn to_filter(&self) -> CrossoverFilter {
336        match self {
337            CrossoverConfig::FullRange => CrossoverFilter::FullRange,
338            CrossoverConfig::Lowpass { cutoff_freq, order } => CrossoverFilter::Lowpass {
339                cutoff_freq: *cutoff_freq,
340                order: *order,
341            },
342            CrossoverConfig::Highpass { cutoff_freq, order } => CrossoverFilter::Highpass {
343                cutoff_freq: *cutoff_freq,
344                order: *order,
345            },
346            CrossoverConfig::Bandpass {
347                low_cutoff,
348                high_cutoff,
349                order,
350            } => CrossoverFilter::Bandpass {
351                low_cutoff: *low_cutoff,
352                high_cutoff: *high_cutoff,
353                order: *order,
354            },
355        }
356    }
357}
358
359/// Frequency configuration
360#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct FrequencyConfig {
362    /// Minimum frequency (Hz)
363    pub min_freq: f64,
364    /// Maximum frequency (Hz)
365    pub max_freq: f64,
366    /// Number of frequency points
367    pub num_points: usize,
368    /// Spacing type ("logarithmic" or "linear")
369    #[serde(default = "default_spacing")]
370    pub spacing: String,
371}
372
373fn default_spacing() -> String {
374    "logarithmic".to_string()
375}
376
377impl FrequencyConfig {
378    /// Generate frequency array based on configuration
379    pub fn generate_frequencies(&self) -> Vec<f64> {
380        if self.spacing.to_lowercase() == "linear" {
381            crate::types::lin_space(self.min_freq, self.max_freq, self.num_points)
382        } else {
383            log_space(self.min_freq, self.max_freq, self.num_points)
384        }
385    }
386}
387
388/// Solver configuration
389#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct SolverConfig {
391    /// Solver method
392    #[serde(default = "default_method")]
393    pub method: String,
394    /// Mesh resolution (elements per meter)
395    #[serde(default = "default_mesh_resolution")]
396    pub mesh_resolution: usize,
397    /// GMRES parameters
398    #[serde(default)]
399    pub gmres: GmresConfig,
400    /// ILU preconditioner parameters
401    #[serde(default)]
402    pub ilu: IluConfig,
403    /// FMM parameters
404    #[serde(default)]
405    pub fmm: FmmConfig,
406    /// Adaptive integration
407    #[serde(default = "default_adaptive_integration")]
408    pub adaptive_integration: bool,
409    /// Adaptive mesh refinement
410    #[serde(default)]
411    pub adaptive_meshing: Option<bool>,
412}
413
414impl Default for SolverConfig {
415    fn default() -> Self {
416        Self {
417            method: default_method(),
418            mesh_resolution: default_mesh_resolution(),
419            gmres: GmresConfig::default(),
420            ilu: IluConfig::default(),
421            fmm: FmmConfig::default(),
422            adaptive_integration: default_adaptive_integration(),
423            adaptive_meshing: None,
424        }
425    }
426}
427
428fn default_method() -> String {
429    "direct".to_string()
430}
431
432fn default_mesh_resolution() -> usize {
433    2
434}
435
436fn default_adaptive_integration() -> bool {
437    false
438}
439
440/// GMRES solver configuration
441#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct GmresConfig {
443    /// Maximum number of iterations
444    #[serde(default = "default_max_iter")]
445    pub max_iter: usize,
446    /// Restart parameter
447    #[serde(default = "default_restart")]
448    pub restart: usize,
449    /// Convergence tolerance
450    #[serde(default = "default_tolerance")]
451    pub tolerance: f64,
452}
453
454impl Default for GmresConfig {
455    fn default() -> Self {
456        Self {
457            max_iter: default_max_iter(),
458            restart: default_restart(),
459            tolerance: default_tolerance(),
460        }
461    }
462}
463
464fn default_max_iter() -> usize {
465    100
466}
467
468fn default_restart() -> usize {
469    50
470}
471
472fn default_tolerance() -> f64 {
473    1e-6
474}
475
476/// ILU preconditioner configuration
477#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct IluConfig {
479    /// ILU method (e.g., "tbem", "slfmm")
480    #[serde(default = "default_ilu_method")]
481    pub method: String,
482    /// Scanning degree (e.g., "fine", "coarse")
483    #[serde(default = "default_scanning_degree")]
484    pub scanning_degree: String,
485    /// Whether to use hierarchical preconditioning
486    #[serde(default)]
487    pub use_hierarchical: bool,
488}
489
490impl Default for IluConfig {
491    fn default() -> Self {
492        Self {
493            method: default_ilu_method(),
494            scanning_degree: default_scanning_degree(),
495            use_hierarchical: false,
496        }
497    }
498}
499
500fn default_ilu_method() -> String {
501    "tbem".to_string()
502}
503
504fn default_scanning_degree() -> String {
505    "fine".to_string()
506}
507
508/// FMM configuration
509#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct FmmConfig {
511    /// FMM type (e.g., "slfmm", "mlfmm")
512    #[serde(default = "default_fmm_type")]
513    pub fmm_type: String,
514    /// Expansion order
515    #[serde(default = "default_expansion_order")]
516    pub expansion_order: usize,
517    /// Maximum particles per leaf in the octree
518    #[serde(default = "default_max_particles")]
519    pub max_particles_per_leaf: usize,
520}
521
522impl Default for FmmConfig {
523    fn default() -> Self {
524        Self {
525            fmm_type: default_fmm_type(),
526            expansion_order: default_expansion_order(),
527            max_particles_per_leaf: default_max_particles(),
528        }
529    }
530}
531
532fn default_fmm_type() -> String {
533    "slfmm".to_string()
534}
535
536fn default_expansion_order() -> usize {
537    6
538}
539
540fn default_max_particles() -> usize {
541    50
542}
543
544/// Visualization configuration
545#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct VisualizationConfig {
547    /// Whether to generate field slices
548    #[serde(default = "default_generate_slices")]
549    pub generate_slices: bool,
550    /// Resolution of the slices (points per side)
551    #[serde(default = "default_slice_resolution")]
552    pub slice_resolution: usize,
553    /// Indices of frequencies to visualize (empty for all)
554    #[serde(default)]
555    pub slice_frequency_indices: Vec<usize>,
556}
557
558impl Default for VisualizationConfig {
559    fn default() -> Self {
560        Self {
561            generate_slices: default_generate_slices(),
562            slice_resolution: default_slice_resolution(),
563            slice_frequency_indices: Vec::new(),
564        }
565    }
566}
567
568fn default_generate_slices() -> bool {
569    false
570}
571
572fn default_slice_resolution() -> usize {
573    50
574}
575
576/// Simulation metadata
577#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct MetadataConfig {
579    /// Simulation description
580    #[serde(default)]
581    pub description: String,
582    /// Author name
583    #[serde(default)]
584    pub author: String,
585    /// Simulation date
586    #[serde(default)]
587    pub date: String,
588}
589
590impl Default for MetadataConfig {
591    fn default() -> Self {
592        Self {
593            description: String::new(),
594            author: String::new(),
595            date: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
596        }
597    }
598}
599
600impl RoomConfig {
601    /// Load configuration from JSON file
602    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
603        let contents =
604            fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?;
605
606        let config: RoomConfig =
607            serde_json::from_str(&contents).map_err(|e| format!("Failed to parse JSON: {}", e))?;
608
609        Ok(config)
610    }
611
612    /// Save configuration to JSON file
613    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
614        let json = serde_json::to_string_pretty(self)
615            .map_err(|e| format!("Failed to serialize config: {}", e))?;
616
617        fs::write(path, json).map_err(|e| format!("Failed to write config file: {}", e))?;
618
619        Ok(())
620    }
621
622    /// Convert to RoomSimulation
623    pub fn to_simulation(&self) -> Result<RoomSimulation, String> {
624        let room = self.room.to_geometry()?;
625
626        let sources: Vec<Source> = self
627            .sources
628            .iter()
629            .map(|s| s.to_source())
630            .collect::<Result<Vec<_>, _>>()?;
631
632        let listening_positions: Vec<Point3D> =
633            self.listening_positions.iter().map(|&p| p.into()).collect();
634
635        let frequencies = self.frequencies.generate_frequencies();
636
637        Ok(RoomSimulation {
638            room,
639            sources,
640            listening_positions,
641            frequencies,
642            boundaries: self.boundaries.clone(), // Pass boundaries
643            speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
644        })
645    }
646}
647
648/// Room acoustics simulation configuration
649#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct RoomSimulation {
651    /// Room geometry
652    pub room: RoomGeometry,
653    /// Sound sources
654    pub sources: Vec<Source>,
655    /// Listening positions
656    pub listening_positions: Vec<Point3D>,
657    /// Frequencies to simulate
658    pub frequencies: Vec<f64>,
659    /// Boundary conditions
660    #[serde(default)]
661    pub boundaries: BoundaryConfig,
662    /// Speed of sound (m/s)
663    pub speed_of_sound: f64,
664}
665
666impl RoomSimulation {
667    /// Create a new simulation with default frequency range
668    pub fn new(
669        room: RoomGeometry,
670        sources: Vec<Source>,
671        listening_positions: Vec<Point3D>,
672    ) -> Self {
673        let frequencies = log_space(20.0, 20000.0, 200);
674
675        Self {
676            room,
677            sources,
678            listening_positions,
679            frequencies,
680            boundaries: BoundaryConfig::default(),
681            speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
682        }
683    }
684
685    /// Create simulation with custom frequency configuration
686    pub fn with_frequencies(
687        room: RoomGeometry,
688        sources: Vec<Source>,
689        listening_positions: Vec<Point3D>,
690        min_freq: f64,
691        max_freq: f64,
692        num_points: usize,
693    ) -> Self {
694        let frequencies = log_space(min_freq, max_freq, num_points);
695
696        Self {
697            room,
698            sources,
699            listening_positions,
700            frequencies,
701            boundaries: BoundaryConfig::default(),
702            speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
703        }
704    }
705
706    /// Calculate wavenumber k = 2πf/c
707    pub fn wavenumber(&self, frequency: f64) -> f64 {
708        crate::types::wavenumber(frequency, self.speed_of_sound)
709    }
710}
711
712#[cfg(test)]
713mod tests {
714    use super::*;
715
716    #[test]
717    fn test_frequency_config() {
718        let config = FrequencyConfig {
719            min_freq: 20.0,
720            max_freq: 20000.0,
721            num_points: 100,
722            spacing: "logarithmic".to_string(),
723        };
724        let freqs = config.generate_frequencies();
725        assert_eq!(freqs.len(), 100);
726    }
727
728    #[test]
729    fn test_room_geometry_config() {
730        let config = RoomGeometryConfig::Rectangular {
731            width: 5.0,
732            depth: 4.0,
733            height: 2.5,
734        };
735        let geometry = config.to_geometry().unwrap();
736        let (w, d, h) = geometry.dimensions();
737        assert_eq!(w, 5.0);
738        assert_eq!(d, 4.0);
739        assert_eq!(h, 2.5);
740    }
741}