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")]
210pub enum DirectivityConfig {
211    /// Omnidirectional (spherical) radiation pattern
212    #[serde(rename = "omnidirectional")]
213    Omnidirectional,
214    /// Classical frequency-dependent directional source
215    #[serde(rename = "classical")]
216    Classical {
217        /// Horizontal beamwidth (degrees)
218        horizontal_angle: f64,
219        /// Vertical beamwidth (degrees)
220        vertical_angle: f64,
221    },
222    /// Custom directivity from measured data
223    #[serde(rename = "custom")]
224    Custom {
225        /// Horizontal angles (degrees)
226        horizontal_angles: Vec<f64>,
227        /// Vertical angles (degrees)
228        vertical_angles: Vec<f64>,
229        /// Magnitude data (matrix)
230        magnitude: Vec<Vec<f64>>,
231    },
232}
233
234impl Default for DirectivityConfig {
235    fn default() -> Self {
236        Self::Omnidirectional
237    }
238}
239
240impl DirectivityConfig {
241    /// Convert to Directivity
242    pub fn to_directivity(&self) -> Result<Directivity, String> {
243        match self {
244            DirectivityConfig::Omnidirectional => {
245                Ok(Directivity::Grid(DirectivityGrid::omnidirectional()))
246            }
247            DirectivityConfig::Classical {
248                horizontal_angle,
249                vertical_angle,
250            } => Ok(Directivity::Classical {
251                h_angle: *horizontal_angle,
252                v_angle: *vertical_angle,
253            }),
254            DirectivityConfig::Custom {
255                horizontal_angles,
256                vertical_angles,
257                magnitude,
258            } => {
259                use ndarray::Array2;
260
261                if magnitude.is_empty() {
262                    return Err("Empty magnitude array".to_string());
263                }
264
265                let n_vert = magnitude.len();
266                let n_horiz = magnitude[0].len();
267
268                if n_vert != vertical_angles.len() {
269                    return Err(format!(
270                        "Vertical angles mismatch: {} vs {}",
271                        n_vert,
272                        vertical_angles.len()
273                    ));
274                }
275                if n_horiz != horizontal_angles.len() {
276                    return Err(format!(
277                        "Horizontal angles mismatch: {} vs {}",
278                        n_horiz,
279                        horizontal_angles.len()
280                    ));
281                }
282
283                let flat: Vec<f64> = magnitude
284                    .iter()
285                    .flat_map(|row| row.iter().copied())
286                    .collect();
287                let mag_array = Array2::from_shape_vec((n_vert, n_horiz), flat)
288                    .map_err(|e| format!("Failed to create magnitude array: {}", e))?;
289
290                Ok(Directivity::Grid(DirectivityGrid {
291                    horizontal_angles: horizontal_angles.clone(),
292                    vertical_angles: vertical_angles.clone(),
293                    magnitude: mag_array,
294                }))
295            }
296        }
297    }
298}
299
300/// Crossover filter configuration
301#[derive(Debug, Clone, Default, Serialize, Deserialize)]
302#[serde(tag = "type")]
303pub enum CrossoverConfig {
304    /// Full range (no filter)
305    #[serde(rename = "fullrange")]
306    #[default]
307    FullRange,
308    /// Low-pass filter
309    #[serde(rename = "lowpass")]
310    Lowpass {
311        /// Cutoff frequency (Hz)
312        cutoff_freq: f64,
313        /// Filter order (e.g., 2, 4)
314        order: u32,
315    },
316    /// High-pass filter
317    #[serde(rename = "highpass")]
318    Highpass {
319        /// Cutoff frequency (Hz)
320        cutoff_freq: f64,
321        /// Filter order (e.g., 2, 4)
322        order: u32,
323    },
324    /// Band-pass filter
325    #[serde(rename = "bandpass")]
326    Bandpass {
327        /// Low cutoff frequency (Hz)
328        low_cutoff: f64,
329        /// High cutoff frequency (Hz)
330        high_cutoff: f64,
331        /// Filter order
332        order: u32,
333    },
334}
335
336impl CrossoverConfig {
337    /// Convert to CrossoverFilter
338    pub fn to_filter(&self) -> CrossoverFilter {
339        match self {
340            CrossoverConfig::FullRange => CrossoverFilter::FullRange,
341            CrossoverConfig::Lowpass { cutoff_freq, order } => CrossoverFilter::Lowpass {
342                cutoff_freq: *cutoff_freq,
343                order: *order,
344            },
345            CrossoverConfig::Highpass { cutoff_freq, order } => CrossoverFilter::Highpass {
346                cutoff_freq: *cutoff_freq,
347                order: *order,
348            },
349            CrossoverConfig::Bandpass {
350                low_cutoff,
351                high_cutoff,
352                order,
353            } => CrossoverFilter::Bandpass {
354                low_cutoff: *low_cutoff,
355                high_cutoff: *high_cutoff,
356                order: *order,
357            },
358        }
359    }
360}
361
362/// Frequency configuration
363#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct FrequencyConfig {
365    /// Minimum frequency (Hz)
366    pub min_freq: f64,
367    /// Maximum frequency (Hz)
368    pub max_freq: f64,
369    /// Number of frequency points
370    pub num_points: usize,
371    /// Spacing type ("logarithmic" or "linear")
372    #[serde(default = "default_spacing")]
373    pub spacing: String,
374}
375
376fn default_spacing() -> String {
377    "logarithmic".to_string()
378}
379
380impl FrequencyConfig {
381    /// Generate frequency array based on configuration
382    pub fn generate_frequencies(&self) -> Vec<f64> {
383        if self.spacing.to_lowercase() == "linear" {
384            crate::types::lin_space(self.min_freq, self.max_freq, self.num_points)
385        } else {
386            log_space(self.min_freq, self.max_freq, self.num_points)
387        }
388    }
389}
390
391/// Solver configuration
392#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct SolverConfig {
394    /// Solver method
395    #[serde(default = "default_method")]
396    pub method: String,
397    /// Mesh resolution (elements per meter)
398    #[serde(default = "default_mesh_resolution")]
399    pub mesh_resolution: usize,
400    /// GMRES parameters
401    #[serde(default)]
402    pub gmres: GmresConfig,
403    /// ILU preconditioner parameters
404    #[serde(default)]
405    pub ilu: IluConfig,
406    /// FMM parameters
407    #[serde(default)]
408    pub fmm: FmmConfig,
409    /// Adaptive integration
410    #[serde(default = "default_adaptive_integration")]
411    pub adaptive_integration: bool,
412    /// Adaptive mesh refinement
413    #[serde(default)]
414    pub adaptive_meshing: Option<bool>,
415}
416
417impl Default for SolverConfig {
418    fn default() -> Self {
419        Self {
420            method: default_method(),
421            mesh_resolution: default_mesh_resolution(),
422            gmres: GmresConfig::default(),
423            ilu: IluConfig::default(),
424            fmm: FmmConfig::default(),
425            adaptive_integration: default_adaptive_integration(),
426            adaptive_meshing: None,
427        }
428    }
429}
430
431fn default_method() -> String {
432    "direct".to_string()
433}
434
435fn default_mesh_resolution() -> usize {
436    2
437}
438
439fn default_adaptive_integration() -> bool {
440    false
441}
442
443/// GMRES solver configuration
444#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct GmresConfig {
446    /// Maximum number of iterations
447    #[serde(default = "default_max_iter")]
448    pub max_iter: usize,
449    /// Restart parameter
450    #[serde(default = "default_restart")]
451    pub restart: usize,
452    /// Convergence tolerance
453    #[serde(default = "default_tolerance")]
454    pub tolerance: f64,
455}
456
457impl Default for GmresConfig {
458    fn default() -> Self {
459        Self {
460            max_iter: default_max_iter(),
461            restart: default_restart(),
462            tolerance: default_tolerance(),
463        }
464    }
465}
466
467fn default_max_iter() -> usize {
468    100
469}
470
471fn default_restart() -> usize {
472    50
473}
474
475fn default_tolerance() -> f64 {
476    1e-6
477}
478
479/// ILU preconditioner configuration
480#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct IluConfig {
482    /// ILU method (e.g., "tbem", "slfmm")
483    #[serde(default = "default_ilu_method")]
484    pub method: String,
485    /// Scanning degree (e.g., "fine", "coarse")
486    #[serde(default = "default_scanning_degree")]
487    pub scanning_degree: String,
488    /// Whether to use hierarchical preconditioning
489    #[serde(default)]
490    pub use_hierarchical: bool,
491}
492
493impl Default for IluConfig {
494    fn default() -> Self {
495        Self {
496            method: default_ilu_method(),
497            scanning_degree: default_scanning_degree(),
498            use_hierarchical: false,
499        }
500    }
501}
502
503fn default_ilu_method() -> String {
504    "tbem".to_string()
505}
506
507fn default_scanning_degree() -> String {
508    "fine".to_string()
509}
510
511/// FMM configuration
512#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct FmmConfig {
514    /// FMM type (e.g., "slfmm", "mlfmm")
515    #[serde(default = "default_fmm_type")]
516    pub fmm_type: String,
517    /// Expansion order
518    #[serde(default = "default_expansion_order")]
519    pub expansion_order: usize,
520    /// Maximum particles per leaf in the octree
521    #[serde(default = "default_max_particles")]
522    pub max_particles_per_leaf: usize,
523}
524
525impl Default for FmmConfig {
526    fn default() -> Self {
527        Self {
528            fmm_type: default_fmm_type(),
529            expansion_order: default_expansion_order(),
530            max_particles_per_leaf: default_max_particles(),
531        }
532    }
533}
534
535fn default_fmm_type() -> String {
536    "slfmm".to_string()
537}
538
539fn default_expansion_order() -> usize {
540    6
541}
542
543fn default_max_particles() -> usize {
544    50
545}
546
547/// Visualization configuration
548#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct VisualizationConfig {
550    /// Whether to generate field slices
551    #[serde(default = "default_generate_slices")]
552    pub generate_slices: bool,
553    /// Resolution of the slices (points per side)
554    #[serde(default = "default_slice_resolution")]
555    pub slice_resolution: usize,
556    /// Indices of frequencies to visualize (empty for all)
557    #[serde(default)]
558    pub slice_frequency_indices: Vec<usize>,
559}
560
561impl Default for VisualizationConfig {
562    fn default() -> Self {
563        Self {
564            generate_slices: default_generate_slices(),
565            slice_resolution: default_slice_resolution(),
566            slice_frequency_indices: Vec::new(),
567        }
568    }
569}
570
571fn default_generate_slices() -> bool {
572    false
573}
574
575fn default_slice_resolution() -> usize {
576    50
577}
578
579/// Simulation metadata
580#[derive(Debug, Clone, Serialize, Deserialize)]
581pub struct MetadataConfig {
582    /// Simulation description
583    #[serde(default)]
584    pub description: String,
585    /// Author name
586    #[serde(default)]
587    pub author: String,
588    /// Simulation date
589    #[serde(default)]
590    pub date: String,
591}
592
593impl Default for MetadataConfig {
594    fn default() -> Self {
595        Self {
596            description: String::new(),
597            author: String::new(),
598            date: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
599        }
600    }
601}
602
603impl RoomConfig {
604    /// Load configuration from JSON file
605    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
606        let contents =
607            fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?;
608
609        let config: RoomConfig =
610            serde_json::from_str(&contents).map_err(|e| format!("Failed to parse JSON: {}", e))?;
611
612        Ok(config)
613    }
614
615    /// Save configuration to JSON file
616    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
617        let json = serde_json::to_string_pretty(self)
618            .map_err(|e| format!("Failed to serialize config: {}", e))?;
619
620        fs::write(path, json).map_err(|e| format!("Failed to write config file: {}", e))?;
621
622        Ok(())
623    }
624
625    /// Convert to RoomSimulation
626    pub fn to_simulation(&self) -> Result<RoomSimulation, String> {
627        let room = self.room.to_geometry()?;
628
629        let sources: Vec<Source> = self
630            .sources
631            .iter()
632            .map(|s| s.to_source())
633            .collect::<Result<Vec<_>, _>>()?;
634
635        let listening_positions: Vec<Point3D> =
636            self.listening_positions.iter().map(|&p| p.into()).collect();
637
638        let frequencies = self.frequencies.generate_frequencies();
639
640        Ok(RoomSimulation {
641            room,
642            sources,
643            listening_positions,
644            frequencies,
645            boundaries: self.boundaries.clone(), // Pass boundaries
646            speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
647        })
648    }
649}
650
651/// Room acoustics simulation configuration
652#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct RoomSimulation {
654    /// Room geometry
655    pub room: RoomGeometry,
656    /// Sound sources
657    pub sources: Vec<Source>,
658    /// Listening positions
659    pub listening_positions: Vec<Point3D>,
660    /// Frequencies to simulate
661    pub frequencies: Vec<f64>,
662    /// Boundary conditions
663    #[serde(default)]
664    pub boundaries: BoundaryConfig,
665    /// Speed of sound (m/s)
666    pub speed_of_sound: f64,
667}
668
669impl RoomSimulation {
670    /// Create a new simulation with default frequency range
671    pub fn new(
672        room: RoomGeometry,
673        sources: Vec<Source>,
674        listening_positions: Vec<Point3D>,
675    ) -> Self {
676        let frequencies = log_space(20.0, 20000.0, 200);
677
678        Self {
679            room,
680            sources,
681            listening_positions,
682            frequencies,
683            boundaries: BoundaryConfig::default(),
684            speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
685        }
686    }
687
688    /// Create simulation with custom frequency configuration
689    pub fn with_frequencies(
690        room: RoomGeometry,
691        sources: Vec<Source>,
692        listening_positions: Vec<Point3D>,
693        min_freq: f64,
694        max_freq: f64,
695        num_points: usize,
696    ) -> Self {
697        let frequencies = log_space(min_freq, max_freq, num_points);
698
699        Self {
700            room,
701            sources,
702            listening_positions,
703            frequencies,
704            boundaries: BoundaryConfig::default(),
705            speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
706        }
707    }
708
709    /// Calculate wavenumber k = 2πf/c
710    pub fn wavenumber(&self, frequency: f64) -> f64 {
711        crate::types::wavenumber(frequency, self.speed_of_sound)
712    }
713}
714
715#[cfg(test)]
716mod tests {
717    use super::*;
718
719    #[test]
720    fn test_frequency_config() {
721        let config = FrequencyConfig {
722            min_freq: 20.0,
723            max_freq: 20000.0,
724            num_points: 100,
725            spacing: "logarithmic".to_string(),
726        };
727        let freqs = config.generate_frequencies();
728        assert_eq!(freqs.len(), 100);
729    }
730
731    #[test]
732    fn test_room_geometry_config() {
733        let config = RoomGeometryConfig::Rectangular {
734            width: 5.0,
735            depth: 4.0,
736            height: 2.5,
737        };
738        let geometry = config.to_geometry().unwrap();
739        let (w, d, h) = geometry.dimensions();
740        assert_eq!(w, 5.0);
741        assert_eq!(d, 4.0);
742        assert_eq!(h, 2.5);
743    }
744}