bem/room_acoustics/
config.rs

1//! JSON configuration for room acoustics simulations
2
3use super::*;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::Path;
7
8/// Complete room configuration loaded from JSON
9#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RoomConfig {
11    /// Room geometry specification
12    pub room: RoomGeometryConfig,
13    /// Sound sources
14    pub sources: Vec<SourceConfig>,
15    /// Listening positions
16    pub listening_positions: Vec<Point3DConfig>,
17    /// Frequency configuration
18    pub frequencies: FrequencyConfig,
19    /// Solver configuration
20    #[serde(default)]
21    pub solver: SolverConfig,
22    /// Visualization configuration
23    #[serde(default)]
24    pub visualization: VisualizationConfig,
25    /// Simulation metadata
26    #[serde(default)]
27    pub metadata: MetadataConfig,
28}
29
30/// Room geometry configuration
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(tag = "type")]
33pub enum RoomGeometryConfig {
34    #[serde(rename = "rectangular")]
35    Rectangular {
36        width: f64,
37        depth: f64,
38        height: f64,
39    },
40    #[serde(rename = "lshaped")]
41    LShaped {
42        /// Main section width
43        width1: f64,
44        /// Main section depth
45        depth1: f64,
46        /// Extension width
47        width2: f64,
48        /// Extension depth
49        depth2: f64,
50        /// Common height
51        height: f64,
52    },
53}
54
55/// 3D point configuration
56#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
57pub struct Point3DConfig {
58    pub x: f64,
59    pub y: f64,
60    pub z: f64,
61}
62
63impl From<Point3DConfig> for Point3D {
64    fn from(p: Point3DConfig) -> Self {
65        Point3D::new(p.x, p.y, p.z)
66    }
67}
68
69impl From<Point3D> for Point3DConfig {
70    fn from(p: Point3D) -> Self {
71        Point3DConfig { x: p.x, y: p.y, z: p.z }
72    }
73}
74
75/// Source configuration
76#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SourceConfig {
78    /// Source name
79    pub name: String,
80    /// Source position
81    pub position: Point3DConfig,
82    /// Source amplitude
83    #[serde(default = "default_amplitude")]
84    pub amplitude: f64,
85    /// Directivity pattern
86    #[serde(default)]
87    pub directivity: DirectivityConfig,
88    /// Crossover filter
89    #[serde(default)]
90    pub crossover: CrossoverConfig,
91}
92
93fn default_amplitude() -> f64 {
94    1.0
95}
96
97/// Directivity pattern configuration
98#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99#[serde(tag = "type")]
100pub enum DirectivityConfig {
101    /// Omnidirectional (spherical) radiation pattern
102    #[serde(rename = "omnidirectional")]
103    #[default]
104    Omnidirectional,
105    /// Custom directivity from measured data
106    #[serde(rename = "custom")]
107    Custom {
108        /// Horizontal angles in degrees [0, 360)
109        horizontal_angles: Vec<f64>,
110        /// Vertical angles in degrees [0, 180]
111        vertical_angles: Vec<f64>,
112        /// Magnitude values (row-major: [n_vertical][n_horizontal])
113        magnitude: Vec<Vec<f64>>,
114    },
115}
116
117/// Crossover filter configuration
118#[derive(Debug, Clone, Default, Serialize, Deserialize)]
119#[serde(tag = "type")]
120pub enum CrossoverConfig {
121    /// Full range speaker (no crossover)
122    #[serde(rename = "fullrange")]
123    #[default]
124    FullRange,
125    /// Lowpass filter for subwoofers
126    #[serde(rename = "lowpass")]
127    Lowpass {
128        /// Cutoff frequency in Hz
129        cutoff_freq: f64,
130        /// Filter order
131        order: u32,
132    },
133    /// Highpass filter for tweeters/satellites
134    #[serde(rename = "highpass")]
135    Highpass {
136        /// Cutoff frequency in Hz
137        cutoff_freq: f64,
138        /// Filter order
139        order: u32,
140    },
141    /// Bandpass filter for midrange drivers
142    #[serde(rename = "bandpass")]
143    Bandpass {
144        /// Low cutoff frequency in Hz
145        low_cutoff: f64,
146        /// High cutoff frequency in Hz
147        high_cutoff: f64,
148        /// Filter order
149        order: u32,
150    },
151}
152
153/// Frequency configuration
154#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct FrequencyConfig {
156    /// Minimum frequency (Hz)
157    pub min_freq: f64,
158    /// Maximum frequency (Hz)
159    pub max_freq: f64,
160    /// Number of frequency points
161    pub num_points: usize,
162    /// Spacing type
163    #[serde(default = "default_spacing")]
164    pub spacing: String, // "logarithmic" or "linear"
165}
166
167fn default_spacing() -> String {
168    "logarithmic".to_string()
169}
170
171/// Solver configuration
172#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct SolverConfig {
174    /// Solver method
175    #[serde(default = "default_method")]
176    pub method: String, // "direct", "gmres", "gmres+ilu", "fmm+gmres", "fmm+gmres+ilu"
177
178    /// Mesh resolution (elements per meter)
179    #[serde(default = "default_mesh_resolution")]
180    pub mesh_resolution: usize,
181
182    /// GMRES parameters
183    #[serde(default)]
184    pub gmres: GmresConfig,
185
186    /// ILU preconditioner parameters
187    #[serde(default)]
188    pub ilu: IluConfig,
189
190    /// FMM parameters
191    #[serde(default)]
192    pub fmm: FmmConfig,
193
194    /// Adaptive integration
195    #[serde(default = "default_adaptive_integration")]
196    pub adaptive_integration: bool,
197
198    /// Adaptive mesh refinement (frequency-dependent, source-proximity based)
199    #[serde(default)]
200    pub adaptive_meshing: Option<bool>,
201}
202
203impl Default for SolverConfig {
204    fn default() -> Self {
205        Self {
206            method: default_method(),
207            mesh_resolution: default_mesh_resolution(),
208            gmres: GmresConfig::default(),
209            ilu: IluConfig::default(),
210            fmm: FmmConfig::default(),
211            adaptive_integration: default_adaptive_integration(),
212            adaptive_meshing: None,
213        }
214    }
215}
216
217fn default_method() -> String {
218    "direct".to_string()
219}
220
221fn default_mesh_resolution() -> usize {
222    2
223}
224
225fn default_adaptive_integration() -> bool {
226    false
227}
228
229/// GMRES solver configuration
230#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct GmresConfig {
232    /// Maximum iterations
233    #[serde(default = "default_max_iter")]
234    pub max_iter: usize,
235    /// Restart interval
236    #[serde(default = "default_restart")]
237    pub restart: usize,
238    /// Convergence tolerance
239    #[serde(default = "default_tolerance")]
240    pub tolerance: f64,
241}
242
243impl Default for GmresConfig {
244    fn default() -> Self {
245        Self {
246            max_iter: default_max_iter(),
247            restart: default_restart(),
248            tolerance: default_tolerance(),
249        }
250    }
251}
252
253fn default_max_iter() -> usize {
254    100
255}
256
257fn default_restart() -> usize {
258    50
259}
260
261fn default_tolerance() -> f64 {
262    1e-6
263}
264
265/// ILU preconditioner configuration
266#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct IluConfig {
268    /// ILU method (TBEM, SLFMM, MLFMM)
269    #[serde(default = "default_ilu_method")]
270    pub method: String,
271    /// Scanning degree (coarse, medium, fine, finest)
272    #[serde(default = "default_scanning_degree")]
273    pub scanning_degree: String,
274    /// Use hierarchical FMM preconditioner instead of ILU
275    /// This is faster for very large problems but may converge slower
276    #[serde(default)]
277    pub use_hierarchical: bool,
278}
279
280impl Default for IluConfig {
281    fn default() -> Self {
282        Self {
283            method: default_ilu_method(),
284            scanning_degree: default_scanning_degree(),
285            use_hierarchical: false,
286        }
287    }
288}
289
290fn default_ilu_method() -> String {
291    "tbem".to_string()
292}
293
294fn default_scanning_degree() -> String {
295    "fine".to_string()
296}
297
298/// FMM configuration
299#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct FmmConfig {
301    /// FMM type (SLFMM or MLFMM)
302    #[serde(default = "default_fmm_type")]
303    pub fmm_type: String,
304    /// Expansion order (p)
305    #[serde(default = "default_expansion_order")]
306    pub expansion_order: usize,
307    /// Max particles per leaf
308    #[serde(default = "default_max_particles")]
309    pub max_particles_per_leaf: usize,
310}
311
312impl Default for FmmConfig {
313    fn default() -> Self {
314        Self {
315            fmm_type: default_fmm_type(),
316            expansion_order: default_expansion_order(),
317            max_particles_per_leaf: default_max_particles(),
318        }
319    }
320}
321
322fn default_fmm_type() -> String {
323    "slfmm".to_string()
324}
325
326fn default_expansion_order() -> usize {
327    6
328}
329
330fn default_max_particles() -> usize {
331    50
332}
333
334/// Visualization configuration
335#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct VisualizationConfig {
337    /// Generate horizontal and vertical spatial slices
338    #[serde(default = "default_generate_slices")]
339    pub generate_slices: bool,
340    /// Resolution for spatial grids (points per dimension)
341    #[serde(default = "default_slice_resolution")]
342    pub slice_resolution: usize,
343    /// Frequencies to compute slices at (indices into frequency array, or empty for all)
344    #[serde(default)]
345    pub slice_frequency_indices: Vec<usize>,
346}
347
348impl Default for VisualizationConfig {
349    fn default() -> Self {
350        Self {
351            generate_slices: default_generate_slices(),
352            slice_resolution: default_slice_resolution(),
353            slice_frequency_indices: Vec::new(),
354        }
355    }
356}
357
358fn default_generate_slices() -> bool {
359    false
360}
361
362fn default_slice_resolution() -> usize {
363    50
364}
365
366/// Simulation metadata
367#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct MetadataConfig {
369    /// Simulation description
370    #[serde(default)]
371    pub description: String,
372    /// Author
373    #[serde(default)]
374    pub author: String,
375    /// Creation date
376    #[serde(default)]
377    pub date: String,
378}
379
380impl Default for MetadataConfig {
381    fn default() -> Self {
382        Self {
383            description: String::new(),
384            author: String::new(),
385            date: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
386        }
387    }
388}
389
390impl RoomConfig {
391    /// Load configuration from JSON file
392    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
393        let contents = fs::read_to_string(path)
394            .map_err(|e| format!("Failed to read config file: {}", e))?;
395
396        let config: RoomConfig = serde_json::from_str(&contents)
397            .map_err(|e| format!("Failed to parse JSON: {}", e))?;
398
399        Ok(config)
400    }
401
402    /// Save configuration to JSON file
403    pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
404        let json = serde_json::to_string_pretty(self)
405            .map_err(|e| format!("Failed to serialize config: {}", e))?;
406
407        fs::write(path, json)
408            .map_err(|e| format!("Failed to write config file: {}", e))?;
409
410        Ok(())
411    }
412
413    /// Convert to RoomSimulation
414    pub fn to_simulation(&self) -> Result<RoomSimulation, String> {
415        // Convert room geometry
416        let room = self.room.to_geometry()?;
417
418        // Convert sources
419        let sources: Vec<Source> = self.sources.iter()
420            .map(|s| s.to_source())
421            .collect::<Result<Vec<_>, _>>()?;
422
423        // Convert listening positions
424        let lps: Vec<ListeningPosition> = self.listening_positions.iter()
425            .map(|&p| p.into())
426            .collect();
427
428        // Create simulation
429        let simulation = RoomSimulation::with_frequencies(
430            room,
431            sources,
432            lps,
433            self.frequencies.min_freq,
434            self.frequencies.max_freq,
435            self.frequencies.num_points,
436        );
437
438        Ok(simulation)
439    }
440}
441
442impl RoomGeometryConfig {
443    fn to_geometry(&self) -> Result<RoomGeometry, String> {
444        match self {
445            RoomGeometryConfig::Rectangular { width, depth, height } => {
446                Ok(RoomGeometry::Rectangular(RectangularRoom::new(*width, *depth, *height)))
447            }
448            RoomGeometryConfig::LShaped { width1, depth1, width2, depth2, height } => {
449                Ok(RoomGeometry::LShaped(LShapedRoom::new(*width1, *depth1, *width2, *depth2, *height)))
450            }
451        }
452    }
453}
454
455impl SourceConfig {
456    fn to_source(&self) -> Result<Source, String> {
457        let directivity = self.directivity.to_pattern()?;
458        let crossover = self.crossover.to_filter();
459
460        let source = Source::new(
461            self.position.into(),
462            directivity,
463            self.amplitude,
464        )
465        .with_name(self.name.clone())
466        .with_crossover(crossover);
467
468        Ok(source)
469    }
470}
471
472impl DirectivityConfig {
473    fn to_pattern(&self) -> Result<DirectivityPattern, String> {
474        match self {
475            DirectivityConfig::Omnidirectional => {
476                Ok(DirectivityPattern::omnidirectional())
477            }
478            DirectivityConfig::Custom { horizontal_angles, vertical_angles, magnitude } => {
479                use ndarray::Array2;
480
481                if magnitude.is_empty() {
482                    return Err("Empty magnitude array".to_string());
483                }
484
485                let n_vert = magnitude.len();
486                let n_horiz = magnitude[0].len();
487
488                if n_vert != vertical_angles.len() {
489                    return Err(format!("Vertical angles mismatch: {} vs {}", n_vert, vertical_angles.len()));
490                }
491                if n_horiz != horizontal_angles.len() {
492                    return Err(format!("Horizontal angles mismatch: {} vs {}", n_horiz, horizontal_angles.len()));
493                }
494
495                // Convert Vec<Vec<f64>> to Array2
496                let flat: Vec<f64> = magnitude.iter().flat_map(|row| row.iter().copied()).collect();
497                let mag_array = Array2::from_shape_vec((n_vert, n_horiz), flat)
498                    .map_err(|e| format!("Failed to create magnitude array: {}", e))?;
499
500                Ok(DirectivityPattern {
501                    horizontal_angles: horizontal_angles.clone(),
502                    vertical_angles: vertical_angles.clone(),
503                    magnitude: mag_array,
504                })
505            }
506        }
507    }
508}
509
510impl CrossoverConfig {
511    fn to_filter(&self) -> CrossoverFilter {
512        match self {
513            CrossoverConfig::FullRange => CrossoverFilter::FullRange,
514            CrossoverConfig::Lowpass { cutoff_freq, order } => {
515                CrossoverFilter::Lowpass {
516                    cutoff_freq: *cutoff_freq,
517                    order: *order,
518                }
519            }
520            CrossoverConfig::Highpass { cutoff_freq, order } => {
521                CrossoverFilter::Highpass {
522                    cutoff_freq: *cutoff_freq,
523                    order: *order,
524                }
525            }
526            CrossoverConfig::Bandpass { low_cutoff, high_cutoff, order } => {
527                CrossoverFilter::Bandpass {
528                    low_cutoff: *low_cutoff,
529                    high_cutoff: *high_cutoff,
530                    order: *order,
531                }
532            }
533        }
534    }
535}