1use 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#[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_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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
209#[serde(tag = "type")]
210pub enum DirectivityConfig {
211 #[serde(rename = "omnidirectional")]
213 #[default]
214 Omnidirectional,
215 #[serde(rename = "custom")]
217 Custom {
218 horizontal_angles: Vec<f64>,
220 vertical_angles: Vec<f64>,
222 magnitude: Vec<Vec<f64>>,
224 },
225}
226
227impl DirectivityConfig {
228 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
280#[serde(tag = "type")]
281pub enum CrossoverConfig {
282 #[serde(rename = "fullrange")]
284 #[default]
285 FullRange,
286 #[serde(rename = "lowpass")]
288 Lowpass {
289 cutoff_freq: f64,
291 order: u32,
293 },
294 #[serde(rename = "highpass")]
296 Highpass {
297 cutoff_freq: f64,
299 order: u32,
301 },
302 #[serde(rename = "bandpass")]
304 Bandpass {
305 low_cutoff: f64,
307 high_cutoff: f64,
309 order: u32,
311 },
312}
313
314impl CrossoverConfig {
315 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#[derive(Debug, Clone, Serialize, Deserialize)]
342pub struct FrequencyConfig {
343 pub min_freq: f64,
345 pub max_freq: f64,
347 pub num_points: usize,
349 #[serde(default = "default_spacing")]
351 pub spacing: String,
352}
353
354fn default_spacing() -> String {
355 "logarithmic".to_string()
356}
357
358impl FrequencyConfig {
359 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#[derive(Debug, Clone, Serialize, Deserialize)]
371pub struct SolverConfig {
372 #[serde(default = "default_method")]
374 pub method: String,
375 #[serde(default = "default_mesh_resolution")]
377 pub mesh_resolution: usize,
378 #[serde(default)]
380 pub gmres: GmresConfig,
381 #[serde(default)]
383 pub ilu: IluConfig,
384 #[serde(default)]
386 pub fmm: FmmConfig,
387 #[serde(default = "default_adaptive_integration")]
389 pub adaptive_integration: bool,
390 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
423pub struct GmresConfig {
424 #[serde(default = "default_max_iter")]
426 pub max_iter: usize,
427 #[serde(default = "default_restart")]
429 pub restart: usize,
430 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
459pub struct IluConfig {
460 #[serde(default = "default_ilu_method")]
462 pub method: String,
463 #[serde(default = "default_scanning_degree")]
465 pub scanning_degree: String,
466 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
491pub struct FmmConfig {
492 #[serde(default = "default_fmm_type")]
494 pub fmm_type: String,
495 #[serde(default = "default_expansion_order")]
497 pub expansion_order: usize,
498 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
527pub struct VisualizationConfig {
528 #[serde(default = "default_generate_slices")]
530 pub generate_slices: bool,
531 #[serde(default = "default_slice_resolution")]
533 pub slice_resolution: usize,
534 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
559pub struct MetadataConfig {
560 #[serde(default)]
562 pub description: String,
563 #[serde(default)]
565 pub author: String,
566 #[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 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 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 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(), speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
625 })
626 }
627}
628
629#[derive(Debug, Clone, Serialize, Deserialize)]
631pub struct RoomSimulation {
632 pub room: RoomGeometry,
634 pub sources: Vec<Source>,
636 pub listening_positions: Vec<Point3D>,
638 pub frequencies: Vec<f64>,
640 #[serde(default)]
642 pub boundaries: BoundaryConfig,
643 pub speed_of_sound: f64,
645}
646
647impl RoomSimulation {
648 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 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 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}