1use super::*;
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::Path;
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct RoomConfig {
11 pub room: RoomGeometryConfig,
13 pub sources: Vec<SourceConfig>,
15 pub listening_positions: Vec<Point3DConfig>,
17 pub frequencies: FrequencyConfig,
19 #[serde(default)]
21 pub solver: SolverConfig,
22 #[serde(default)]
24 pub visualization: VisualizationConfig,
25 #[serde(default)]
27 pub metadata: MetadataConfig,
28}
29
30#[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 width1: f64,
44 depth1: f64,
46 width2: f64,
48 depth2: f64,
50 height: f64,
52 },
53}
54
55#[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#[derive(Debug, Clone, Serialize, Deserialize)]
77pub struct SourceConfig {
78 pub name: String,
80 pub position: Point3DConfig,
82 #[serde(default = "default_amplitude")]
84 pub amplitude: f64,
85 #[serde(default)]
87 pub directivity: DirectivityConfig,
88 #[serde(default)]
90 pub crossover: CrossoverConfig,
91}
92
93fn default_amplitude() -> f64 {
94 1.0
95}
96
97#[derive(Debug, Clone, Default, Serialize, Deserialize)]
99#[serde(tag = "type")]
100pub enum DirectivityConfig {
101 #[serde(rename = "omnidirectional")]
103 #[default]
104 Omnidirectional,
105 #[serde(rename = "custom")]
107 Custom {
108 horizontal_angles: Vec<f64>,
110 vertical_angles: Vec<f64>,
112 magnitude: Vec<Vec<f64>>,
114 },
115}
116
117#[derive(Debug, Clone, Default, Serialize, Deserialize)]
119#[serde(tag = "type")]
120pub enum CrossoverConfig {
121 #[serde(rename = "fullrange")]
123 #[default]
124 FullRange,
125 #[serde(rename = "lowpass")]
127 Lowpass {
128 cutoff_freq: f64,
130 order: u32,
132 },
133 #[serde(rename = "highpass")]
135 Highpass {
136 cutoff_freq: f64,
138 order: u32,
140 },
141 #[serde(rename = "bandpass")]
143 Bandpass {
144 low_cutoff: f64,
146 high_cutoff: f64,
148 order: u32,
150 },
151}
152
153#[derive(Debug, Clone, Serialize, Deserialize)]
155pub struct FrequencyConfig {
156 pub min_freq: f64,
158 pub max_freq: f64,
160 pub num_points: usize,
162 #[serde(default = "default_spacing")]
164 pub spacing: String, }
166
167fn default_spacing() -> String {
168 "logarithmic".to_string()
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
173pub struct SolverConfig {
174 #[serde(default = "default_method")]
176 pub method: String, #[serde(default = "default_mesh_resolution")]
180 pub mesh_resolution: usize,
181
182 #[serde(default)]
184 pub gmres: GmresConfig,
185
186 #[serde(default)]
188 pub ilu: IluConfig,
189
190 #[serde(default)]
192 pub fmm: FmmConfig,
193
194 #[serde(default = "default_adaptive_integration")]
196 pub adaptive_integration: bool,
197
198 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
231pub struct GmresConfig {
232 #[serde(default = "default_max_iter")]
234 pub max_iter: usize,
235 #[serde(default = "default_restart")]
237 pub restart: usize,
238 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
267pub struct IluConfig {
268 #[serde(default = "default_ilu_method")]
270 pub method: String,
271 #[serde(default = "default_scanning_degree")]
273 pub scanning_degree: String,
274 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
300pub struct FmmConfig {
301 #[serde(default = "default_fmm_type")]
303 pub fmm_type: String,
304 #[serde(default = "default_expansion_order")]
306 pub expansion_order: usize,
307 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
336pub struct VisualizationConfig {
337 #[serde(default = "default_generate_slices")]
339 pub generate_slices: bool,
340 #[serde(default = "default_slice_resolution")]
342 pub slice_resolution: usize,
343 #[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#[derive(Debug, Clone, Serialize, Deserialize)]
368pub struct MetadataConfig {
369 #[serde(default)]
371 pub description: String,
372 #[serde(default)]
374 pub author: String,
375 #[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 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 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 pub fn to_simulation(&self) -> Result<RoomSimulation, String> {
415 let room = self.room.to_geometry()?;
417
418 let sources: Vec<Source> = self.sources.iter()
420 .map(|s| s.to_source())
421 .collect::<Result<Vec<_>, _>>()?;
422
423 let lps: Vec<ListeningPosition> = self.listening_positions.iter()
425 .map(|&p| p.into())
426 .collect();
427
428 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 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}