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")]
210pub enum DirectivityConfig {
211 #[serde(rename = "omnidirectional")]
213 Omnidirectional,
214 #[serde(rename = "classical")]
216 Classical {
217 horizontal_angle: f64,
219 vertical_angle: f64,
221 },
222 #[serde(rename = "custom")]
224 Custom {
225 horizontal_angles: Vec<f64>,
227 vertical_angles: Vec<f64>,
229 magnitude: Vec<Vec<f64>>,
231 },
232}
233
234impl Default for DirectivityConfig {
235 fn default() -> Self {
236 Self::Omnidirectional
237 }
238}
239
240impl DirectivityConfig {
241 pub fn to_directivity(&self) -> Result<Directivity, String> {
243 match self {
244 DirectivityConfig::Omnidirectional => {
245 Ok(Directivity::Grid(DirectivityGrid::omnidirectional()))
246 }
247 DirectivityConfig::Classical {
248 horizontal_angle,
249 vertical_angle,
250 } => Ok(Directivity::Classical {
251 h_angle: *horizontal_angle,
252 v_angle: *vertical_angle,
253 }),
254 DirectivityConfig::Custom {
255 horizontal_angles,
256 vertical_angles,
257 magnitude,
258 } => {
259 use ndarray::Array2;
260
261 if magnitude.is_empty() {
262 return Err("Empty magnitude array".to_string());
263 }
264
265 let n_vert = magnitude.len();
266 let n_horiz = magnitude[0].len();
267
268 if n_vert != vertical_angles.len() {
269 return Err(format!(
270 "Vertical angles mismatch: {} vs {}",
271 n_vert,
272 vertical_angles.len()
273 ));
274 }
275 if n_horiz != horizontal_angles.len() {
276 return Err(format!(
277 "Horizontal angles mismatch: {} vs {}",
278 n_horiz,
279 horizontal_angles.len()
280 ));
281 }
282
283 let flat: Vec<f64> = magnitude
284 .iter()
285 .flat_map(|row| row.iter().copied())
286 .collect();
287 let mag_array = Array2::from_shape_vec((n_vert, n_horiz), flat)
288 .map_err(|e| format!("Failed to create magnitude array: {}", e))?;
289
290 Ok(Directivity::Grid(DirectivityGrid {
291 horizontal_angles: horizontal_angles.clone(),
292 vertical_angles: vertical_angles.clone(),
293 magnitude: mag_array,
294 }))
295 }
296 }
297 }
298}
299
300#[derive(Debug, Clone, Default, Serialize, Deserialize)]
302#[serde(tag = "type")]
303pub enum CrossoverConfig {
304 #[serde(rename = "fullrange")]
306 #[default]
307 FullRange,
308 #[serde(rename = "lowpass")]
310 Lowpass {
311 cutoff_freq: f64,
313 order: u32,
315 },
316 #[serde(rename = "highpass")]
318 Highpass {
319 cutoff_freq: f64,
321 order: u32,
323 },
324 #[serde(rename = "bandpass")]
326 Bandpass {
327 low_cutoff: f64,
329 high_cutoff: f64,
331 order: u32,
333 },
334}
335
336impl CrossoverConfig {
337 pub fn to_filter(&self) -> CrossoverFilter {
339 match self {
340 CrossoverConfig::FullRange => CrossoverFilter::FullRange,
341 CrossoverConfig::Lowpass { cutoff_freq, order } => CrossoverFilter::Lowpass {
342 cutoff_freq: *cutoff_freq,
343 order: *order,
344 },
345 CrossoverConfig::Highpass { cutoff_freq, order } => CrossoverFilter::Highpass {
346 cutoff_freq: *cutoff_freq,
347 order: *order,
348 },
349 CrossoverConfig::Bandpass {
350 low_cutoff,
351 high_cutoff,
352 order,
353 } => CrossoverFilter::Bandpass {
354 low_cutoff: *low_cutoff,
355 high_cutoff: *high_cutoff,
356 order: *order,
357 },
358 }
359 }
360}
361
362#[derive(Debug, Clone, Serialize, Deserialize)]
364pub struct FrequencyConfig {
365 pub min_freq: f64,
367 pub max_freq: f64,
369 pub num_points: usize,
371 #[serde(default = "default_spacing")]
373 pub spacing: String,
374}
375
376fn default_spacing() -> String {
377 "logarithmic".to_string()
378}
379
380impl FrequencyConfig {
381 pub fn generate_frequencies(&self) -> Vec<f64> {
383 if self.spacing.to_lowercase() == "linear" {
384 crate::types::lin_space(self.min_freq, self.max_freq, self.num_points)
385 } else {
386 log_space(self.min_freq, self.max_freq, self.num_points)
387 }
388 }
389}
390
391#[derive(Debug, Clone, Serialize, Deserialize)]
393pub struct SolverConfig {
394 #[serde(default = "default_method")]
396 pub method: String,
397 #[serde(default = "default_mesh_resolution")]
399 pub mesh_resolution: usize,
400 #[serde(default)]
402 pub gmres: GmresConfig,
403 #[serde(default)]
405 pub ilu: IluConfig,
406 #[serde(default)]
408 pub fmm: FmmConfig,
409 #[serde(default = "default_adaptive_integration")]
411 pub adaptive_integration: bool,
412 #[serde(default)]
414 pub adaptive_meshing: Option<bool>,
415}
416
417impl Default for SolverConfig {
418 fn default() -> Self {
419 Self {
420 method: default_method(),
421 mesh_resolution: default_mesh_resolution(),
422 gmres: GmresConfig::default(),
423 ilu: IluConfig::default(),
424 fmm: FmmConfig::default(),
425 adaptive_integration: default_adaptive_integration(),
426 adaptive_meshing: None,
427 }
428 }
429}
430
431fn default_method() -> String {
432 "direct".to_string()
433}
434
435fn default_mesh_resolution() -> usize {
436 2
437}
438
439fn default_adaptive_integration() -> bool {
440 false
441}
442
443#[derive(Debug, Clone, Serialize, Deserialize)]
445pub struct GmresConfig {
446 #[serde(default = "default_max_iter")]
448 pub max_iter: usize,
449 #[serde(default = "default_restart")]
451 pub restart: usize,
452 #[serde(default = "default_tolerance")]
454 pub tolerance: f64,
455}
456
457impl Default for GmresConfig {
458 fn default() -> Self {
459 Self {
460 max_iter: default_max_iter(),
461 restart: default_restart(),
462 tolerance: default_tolerance(),
463 }
464 }
465}
466
467fn default_max_iter() -> usize {
468 100
469}
470
471fn default_restart() -> usize {
472 50
473}
474
475fn default_tolerance() -> f64 {
476 1e-6
477}
478
479#[derive(Debug, Clone, Serialize, Deserialize)]
481pub struct IluConfig {
482 #[serde(default = "default_ilu_method")]
484 pub method: String,
485 #[serde(default = "default_scanning_degree")]
487 pub scanning_degree: String,
488 #[serde(default)]
490 pub use_hierarchical: bool,
491}
492
493impl Default for IluConfig {
494 fn default() -> Self {
495 Self {
496 method: default_ilu_method(),
497 scanning_degree: default_scanning_degree(),
498 use_hierarchical: false,
499 }
500 }
501}
502
503fn default_ilu_method() -> String {
504 "tbem".to_string()
505}
506
507fn default_scanning_degree() -> String {
508 "fine".to_string()
509}
510
511#[derive(Debug, Clone, Serialize, Deserialize)]
513pub struct FmmConfig {
514 #[serde(default = "default_fmm_type")]
516 pub fmm_type: String,
517 #[serde(default = "default_expansion_order")]
519 pub expansion_order: usize,
520 #[serde(default = "default_max_particles")]
522 pub max_particles_per_leaf: usize,
523}
524
525impl Default for FmmConfig {
526 fn default() -> Self {
527 Self {
528 fmm_type: default_fmm_type(),
529 expansion_order: default_expansion_order(),
530 max_particles_per_leaf: default_max_particles(),
531 }
532 }
533}
534
535fn default_fmm_type() -> String {
536 "slfmm".to_string()
537}
538
539fn default_expansion_order() -> usize {
540 6
541}
542
543fn default_max_particles() -> usize {
544 50
545}
546
547#[derive(Debug, Clone, Serialize, Deserialize)]
549pub struct VisualizationConfig {
550 #[serde(default = "default_generate_slices")]
552 pub generate_slices: bool,
553 #[serde(default = "default_slice_resolution")]
555 pub slice_resolution: usize,
556 #[serde(default)]
558 pub slice_frequency_indices: Vec<usize>,
559}
560
561impl Default for VisualizationConfig {
562 fn default() -> Self {
563 Self {
564 generate_slices: default_generate_slices(),
565 slice_resolution: default_slice_resolution(),
566 slice_frequency_indices: Vec::new(),
567 }
568 }
569}
570
571fn default_generate_slices() -> bool {
572 false
573}
574
575fn default_slice_resolution() -> usize {
576 50
577}
578
579#[derive(Debug, Clone, Serialize, Deserialize)]
581pub struct MetadataConfig {
582 #[serde(default)]
584 pub description: String,
585 #[serde(default)]
587 pub author: String,
588 #[serde(default)]
590 pub date: String,
591}
592
593impl Default for MetadataConfig {
594 fn default() -> Self {
595 Self {
596 description: String::new(),
597 author: String::new(),
598 date: chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
599 }
600 }
601}
602
603impl RoomConfig {
604 pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, String> {
606 let contents =
607 fs::read_to_string(path).map_err(|e| format!("Failed to read config file: {}", e))?;
608
609 let config: RoomConfig =
610 serde_json::from_str(&contents).map_err(|e| format!("Failed to parse JSON: {}", e))?;
611
612 Ok(config)
613 }
614
615 pub fn to_file<P: AsRef<Path>>(&self, path: P) -> Result<(), String> {
617 let json = serde_json::to_string_pretty(self)
618 .map_err(|e| format!("Failed to serialize config: {}", e))?;
619
620 fs::write(path, json).map_err(|e| format!("Failed to write config file: {}", e))?;
621
622 Ok(())
623 }
624
625 pub fn to_simulation(&self) -> Result<RoomSimulation, String> {
627 let room = self.room.to_geometry()?;
628
629 let sources: Vec<Source> = self
630 .sources
631 .iter()
632 .map(|s| s.to_source())
633 .collect::<Result<Vec<_>, _>>()?;
634
635 let listening_positions: Vec<Point3D> =
636 self.listening_positions.iter().map(|&p| p.into()).collect();
637
638 let frequencies = self.frequencies.generate_frequencies();
639
640 Ok(RoomSimulation {
641 room,
642 sources,
643 listening_positions,
644 frequencies,
645 boundaries: self.boundaries.clone(), speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
647 })
648 }
649}
650
651#[derive(Debug, Clone, Serialize, Deserialize)]
653pub struct RoomSimulation {
654 pub room: RoomGeometry,
656 pub sources: Vec<Source>,
658 pub listening_positions: Vec<Point3D>,
660 pub frequencies: Vec<f64>,
662 #[serde(default)]
664 pub boundaries: BoundaryConfig,
665 pub speed_of_sound: f64,
667}
668
669impl RoomSimulation {
670 pub fn new(
672 room: RoomGeometry,
673 sources: Vec<Source>,
674 listening_positions: Vec<Point3D>,
675 ) -> Self {
676 let frequencies = log_space(20.0, 20000.0, 200);
677
678 Self {
679 room,
680 sources,
681 listening_positions,
682 frequencies,
683 boundaries: BoundaryConfig::default(),
684 speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
685 }
686 }
687
688 pub fn with_frequencies(
690 room: RoomGeometry,
691 sources: Vec<Source>,
692 listening_positions: Vec<Point3D>,
693 min_freq: f64,
694 max_freq: f64,
695 num_points: usize,
696 ) -> Self {
697 let frequencies = log_space(min_freq, max_freq, num_points);
698
699 Self {
700 room,
701 sources,
702 listening_positions,
703 frequencies,
704 boundaries: BoundaryConfig::default(),
705 speed_of_sound: crate::types::constants::SPEED_OF_SOUND_20C,
706 }
707 }
708
709 pub fn wavenumber(&self, frequency: f64) -> f64 {
711 crate::types::wavenumber(frequency, self.speed_of_sound)
712 }
713}
714
715#[cfg(test)]
716mod tests {
717 use super::*;
718
719 #[test]
720 fn test_frequency_config() {
721 let config = FrequencyConfig {
722 min_freq: 20.0,
723 max_freq: 20000.0,
724 num_points: 100,
725 spacing: "logarithmic".to_string(),
726 };
727 let freqs = config.generate_frequencies();
728 assert_eq!(freqs.len(), 100);
729 }
730
731 #[test]
732 fn test_room_geometry_config() {
733 let config = RoomGeometryConfig::Rectangular {
734 width: 5.0,
735 depth: 4.0,
736 height: 2.5,
737 };
738 let geometry = config.to_geometry().unwrap();
739 let (w, d, h) = geometry.dimensions();
740 assert_eq!(w, 5.0);
741 assert_eq!(d, 4.0);
742 assert_eq!(h, 2.5);
743 }
744}