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 solver: SolverConfig,
24 #[serde(default)]
26 pub visualization: VisualizationConfig,
27 #[serde(default)]
29 pub metadata: MetadataConfig,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34#[serde(tag = "type")]
35pub enum RoomGeometryConfig {
36 #[serde(rename = "rectangular")]
37 Rectangular {
39 width: f64,
41 depth: f64,
43 height: f64,
45 },
46 #[serde(rename = "lshaped")]
47 LShaped {
49 width1: f64,
51 depth1: f64,
53 width2: f64,
55 depth2: f64,
57 height: f64,
59 },
60}
61
62impl RoomGeometryConfig {
63 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#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
88pub struct Point3DConfig {
89 pub x: f64,
91 pub y: f64,
93 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#[derive(Debug, Clone, Serialize, Deserialize)]
115pub struct SourceConfig {
116 pub name: String,
118 pub position: Point3DConfig,
120 #[serde(default = "default_amplitude")]
122 pub amplitude: f64,
123 #[serde(default)]
125 pub directivity: DirectivityConfig,
126 #[serde(default)]
128 pub crossover: CrossoverConfig,
129}
130
131fn default_amplitude() -> f64 {
132 1.0
133}
134
135impl SourceConfig {
136 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
151#[serde(tag = "type")]
152pub enum DirectivityConfig {
153 #[serde(rename = "omnidirectional")]
155 #[default]
156 Omnidirectional,
157 #[serde(rename = "custom")]
159 Custom {
160 horizontal_angles: Vec<f64>,
162 vertical_angles: Vec<f64>,
164 magnitude: Vec<Vec<f64>>,
166 },
167}
168
169impl DirectivityConfig {
170 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#[derive(Debug, Clone, Default, Serialize, Deserialize)]
222#[serde(tag = "type")]
223pub enum CrossoverConfig {
224 #[serde(rename = "fullrange")]
226 #[default]
227 FullRange,
228 #[serde(rename = "lowpass")]
230 Lowpass {
231 cutoff_freq: f64,
233 order: u32,
235 },
236 #[serde(rename = "highpass")]
238 Highpass {
239 cutoff_freq: f64,
241 order: u32,
243 },
244 #[serde(rename = "bandpass")]
246 Bandpass {
247 low_cutoff: f64,
249 high_cutoff: f64,
251 order: u32,
253 },
254}
255
256impl CrossoverConfig {
257 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#[derive(Debug, Clone, Serialize, Deserialize)]
284pub struct FrequencyConfig {
285 pub min_freq: f64,
287 pub max_freq: f64,
289 pub num_points: usize,
291 #[serde(default = "default_spacing")]
293 pub spacing: String,
294}
295
296fn default_spacing() -> String {
297 "logarithmic".to_string()
298}
299
300impl FrequencyConfig {
301 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#[derive(Debug, Clone, Serialize, Deserialize)]
313pub struct SolverConfig {
314 #[serde(default = "default_method")]
316 pub method: String,
317 #[serde(default = "default_mesh_resolution")]
319 pub mesh_resolution: usize,
320 #[serde(default)]
322 pub gmres: GmresConfig,
323 #[serde(default)]
325 pub ilu: IluConfig,
326 #[serde(default)]
328 pub fmm: FmmConfig,
329 #[serde(default = "default_adaptive_integration")]
331 pub adaptive_integration: bool,
332 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
365pub struct GmresConfig {
366 #[serde(default = "default_max_iter")]
368 pub max_iter: usize,
369 #[serde(default = "default_restart")]
371 pub restart: usize,
372 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
401pub struct IluConfig {
402 #[serde(default = "default_ilu_method")]
404 pub method: String,
405 #[serde(default = "default_scanning_degree")]
407 pub scanning_degree: String,
408 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
433pub struct FmmConfig {
434 #[serde(default = "default_fmm_type")]
436 pub fmm_type: String,
437 #[serde(default = "default_expansion_order")]
439 pub expansion_order: usize,
440 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
469pub struct VisualizationConfig {
470 #[serde(default = "default_generate_slices")]
472 pub generate_slices: bool,
473 #[serde(default = "default_slice_resolution")]
475 pub slice_resolution: usize,
476 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
501pub struct MetadataConfig {
502 #[serde(default)]
504 pub description: String,
505 #[serde(default)]
507 pub author: String,
508 #[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 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 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 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#[derive(Debug, Clone, Serialize, Deserialize)]
572pub struct RoomSimulation {
573 pub room: RoomGeometry,
575 pub sources: Vec<Source>,
577 pub listening_positions: Vec<Point3D>,
579 pub frequencies: Vec<f64>,
581 pub speed_of_sound: f64,
583}
584
585impl RoomSimulation {
586 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 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 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}