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