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