1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct RoomConfig {
13 pub room: RoomGeometryConfig,
15 pub sources: Vec<SourceConfig>,
17 pub listening_positions: Vec<Point3DConfig>,
19 pub frequencies: FrequencyConfig,
21 #[serde(default)]
23 pub boundaries: BoundaryConfig,
24 #[serde(default)]
26 pub solver: SolverConfig,
27 #[serde(default)]
29 pub visualization: VisualizationConfig,
30 #[serde(default)]
32 pub metadata: MetadataConfig,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37#[serde(tag = "type")]
38pub enum RoomGeometryConfig {
39 #[serde(rename = "rectangular")]
40 Rectangular {
42 width: f64,
44 depth: f64,
46 height: f64,
48 },
49 #[serde(rename = "lshaped")]
50 LShaped {
52 width1: f64,
54 depth1: f64,
56 width2: f64,
58 depth2: f64,
60 height: f64,
62 },
63}
64
65impl RoomGeometryConfig {
66 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#[derive(Debug, Clone, Serialize, Deserialize)]
91pub struct BoundaryConfig {
92 #[serde(default = "default_rigid")]
94 pub floor: SurfaceConfig,
95 #[serde(default = "default_rigid")]
97 pub ceiling: SurfaceConfig,
98 #[serde(default = "default_rigid")]
100 pub walls: SurfaceConfig,
101 pub front_wall: Option<SurfaceConfig>,
103 pub back_wall: Option<SurfaceConfig>,
105 pub left_wall: Option<SurfaceConfig>,
107 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#[derive(Debug, Clone, Serialize, Deserialize)]
131#[serde(tag = "type")]
132pub enum SurfaceConfig {
133 #[serde(rename = "rigid")]
135 Rigid,
136 #[serde(rename = "absorption")]
138 Absorption { coefficient: f64 },
139 #[serde(rename = "impedance")]
141 Impedance { real: f64, imag: f64 },
142}
143
144#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
146pub struct Point3DConfig {
147 pub x: f64,
149 pub y: f64,
151 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#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct SourceConfig {
174 pub name: String,
176 pub position: Point3DConfig,
178 #[serde(default = "default_amplitude")]
180 pub amplitude: f64,
181 #[serde(default)]
183 pub directivity: DirectivityConfig,
184 #[serde(default)]
186 pub crossover: CrossoverConfig,
187}
188
189fn default_amplitude() -> f64 {
190 1.0
191}
192
193impl SourceConfig {
194 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#[derive(Debug, Clone, Serialize, Deserialize)]
209#[serde(tag = "type")]
210#[derive(Default)]
211pub enum DirectivityConfig {
212 #[serde(rename = "omnidirectional")]
214 #[default]
215 Omnidirectional,
216 #[serde(rename = "classical")]
218 Classical {
219 horizontal_angle: f64,
221 vertical_angle: f64,
223 },
224 #[serde(rename = "custom")]
226 Custom {
227 horizontal_angles: Vec<f64>,
229 vertical_angles: Vec<f64>,
231 magnitude: Vec<Vec<f64>>,
233 },
234}
235
236
237impl DirectivityConfig {
238 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
299#[serde(tag = "type")]
300pub enum CrossoverConfig {
301 #[serde(rename = "fullrange")]
303 #[default]
304 FullRange,
305 #[serde(rename = "lowpass")]
307 Lowpass {
308 cutoff_freq: f64,
310 order: u32,
312 },
313 #[serde(rename = "highpass")]
315 Highpass {
316 cutoff_freq: f64,
318 order: u32,
320 },
321 #[serde(rename = "bandpass")]
323 Bandpass {
324 low_cutoff: f64,
326 high_cutoff: f64,
328 order: u32,
330 },
331}
332
333impl CrossoverConfig {
334 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#[derive(Debug, Clone, Serialize, Deserialize)]
361pub struct FrequencyConfig {
362 pub min_freq: f64,
364 pub max_freq: f64,
366 pub num_points: usize,
368 #[serde(default = "default_spacing")]
370 pub spacing: String,
371}
372
373fn default_spacing() -> String {
374 "logarithmic".to_string()
375}
376
377impl FrequencyConfig {
378 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#[derive(Debug, Clone, Serialize, Deserialize)]
390pub struct SolverConfig {
391 #[serde(default = "default_method")]
393 pub method: String,
394 #[serde(default = "default_mesh_resolution")]
396 pub mesh_resolution: usize,
397 #[serde(default)]
399 pub gmres: GmresConfig,
400 #[serde(default)]
402 pub ilu: IluConfig,
403 #[serde(default)]
405 pub fmm: FmmConfig,
406 #[serde(default = "default_adaptive_integration")]
408 pub adaptive_integration: bool,
409 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
442pub struct GmresConfig {
443 #[serde(default = "default_max_iter")]
445 pub max_iter: usize,
446 #[serde(default = "default_restart")]
448 pub restart: usize,
449 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
478pub struct IluConfig {
479 #[serde(default = "default_ilu_method")]
481 pub method: String,
482 #[serde(default = "default_scanning_degree")]
484 pub scanning_degree: String,
485 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
510pub struct FmmConfig {
511 #[serde(default = "default_fmm_type")]
513 pub fmm_type: String,
514 #[serde(default = "default_expansion_order")]
516 pub expansion_order: usize,
517 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
546pub struct VisualizationConfig {
547 #[serde(default = "default_generate_slices")]
549 pub generate_slices: bool,
550 #[serde(default = "default_slice_resolution")]
552 pub slice_resolution: usize,
553 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
578pub struct MetadataConfig {
579 #[serde(default)]
581 pub description: String,
582 #[serde(default)]
584 pub author: String,
585 #[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 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 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 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(), speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
644 })
645 }
646}
647
648#[derive(Debug, Clone, Serialize, Deserialize)]
650pub struct RoomSimulation {
651 pub room: RoomGeometry,
653 pub sources: Vec<Source>,
655 pub listening_positions: Vec<Point3D>,
657 pub frequencies: Vec<f64>,
659 #[serde(default)]
661 pub boundaries: BoundaryConfig,
662 pub speed_of_sound: f64,
664}
665
666impl RoomSimulation {
667 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 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 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}