Skip to main content

math_audio_xem_common/
output.rs

1//! Output JSON formatting for room acoustics simulations
2
3use crate::config::{
4    BoundaryConfig, MetadataConfig, RoomConfig, RoomSimulation, VisualizationConfig,
5};
6use crate::geometry::RoomGeometry;
7use crate::types::{Point3D, RoomMesh, pressure_to_spl};
8use ndarray::{Array1, Array2};
9use num_complex::Complex64;
10use serde::{Deserialize, Serialize};
11
12/// Result of room simulation at one frequency
13#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FrequencyResult {
15    /// Frequency (Hz)
16    pub frequency: f64,
17    /// SPL values at each listening position
18    pub spl_at_lp: Vec<f64>,
19}
20
21/// Complete simulation results
22#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SimulationResults {
24    /// List of frequencies simulated
25    pub frequencies: Vec<f64>,
26    /// Frequency response at each listening position
27    pub lp_frequency_responses: Vec<Vec<f64>>,
28    /// Horizontal slice data (if generated)
29    pub horizontal_slice: Option<SliceData>,
30    /// Vertical slice data (if generated)
31    pub vertical_slice: Option<SliceData>,
32}
33
34/// Pressure field data on a 2D slice
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SliceData {
37    /// X coordinates
38    pub x: Vec<f64>,
39    /// Y or Z coordinates
40    pub y: Vec<f64>,
41    /// SPL grid data
42    pub spl: Array2<f64>,
43    /// Frequency of the slice
44    pub frequency: f64,
45}
46
47/// Create output JSON without slices
48pub fn create_output_json(
49    simulation: &RoomSimulation,
50    config: &RoomConfig,
51    lp_spl_values: Vec<f64>,
52    solver_name: &str,
53) -> serde_json::Value {
54    let lp = simulation.listening_positions[0];
55    let (room_width, room_depth, room_height) = simulation.room.dimensions();
56    let edges = simulation.room.get_edges();
57
58    serde_json::json!({
59        "room": {
60            "type": match simulation.room {
61                RoomGeometry::Rectangular(_) => "rectangular",
62                RoomGeometry::LShaped(_) => "lshaped",
63            },
64            "width": room_width,
65            "depth": room_depth,
66            "height": room_height,
67            "edges": edges.iter().map(|(p1, p2)| {
68                vec![
69                    vec![p1.x, p1.y, p1.z],
70                    vec![p2.x, p2.y, p2.z],
71                ]
72            }).collect::<Vec<_>>(),
73        },
74        "sources": simulation.sources.iter().map(|s| {
75            serde_json::json!({
76                "name": s.name,
77                "position": [s.position.x, s.position.y, s.position.z],
78            })
79        }).collect::<Vec<_>>(),
80        "listening_position": [lp.x, lp.y, lp.z],
81        "frequencies": simulation.frequencies,
82        "frequency_response": lp_spl_values,
83        "solver": solver_name,
84        "metadata": {
85            "description": config.metadata.description,
86            "author": config.metadata.author,
87            "date": chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
88        },
89    })
90}
91
92/// Create output JSON with per-source responses
93pub fn create_output_json_with_sources(
94    simulation: &RoomSimulation,
95    config: &RoomConfig,
96    lp_spl_values: Vec<f64>,
97    source_spl_values: &[Vec<f64>],
98    solver_name: &str,
99) -> serde_json::Value {
100    let lp = simulation.listening_positions[0];
101    let (room_width, room_depth, room_height) = simulation.room.dimensions();
102    let edges = simulation.room.get_edges();
103
104    serde_json::json!({
105        "room": {
106            "type": match simulation.room {
107                RoomGeometry::Rectangular(_) => "rectangular",
108                RoomGeometry::LShaped(_) => "lshaped",
109            },
110            "width": room_width,
111            "depth": room_depth,
112            "height": room_height,
113            "edges": edges.iter().map(|(p1, p2)| {
114                vec![
115                    vec![p1.x, p1.y, p1.z],
116                    vec![p2.x, p2.y, p2.z],
117                ]
118            }).collect::<Vec<_>>(),
119        },
120        "sources": simulation.sources.iter().map(|s| {
121            serde_json::json!({
122                "name": s.name,
123                "position": [s.position.x, s.position.y, s.position.z],
124            })
125        }).collect::<Vec<_>>(),
126        "listening_position": [lp.x, lp.y, lp.z],
127        "frequencies": simulation.frequencies,
128        "frequency_response": lp_spl_values,
129        "source_responses": source_spl_values.iter().enumerate().map(|(idx, spl_vals)| {
130            serde_json::json!({
131                "source_name": simulation.sources[idx].name,
132                "source_index": idx,
133                "spl": spl_vals,
134            })
135        }).collect::<Vec<_>>(),
136        "solver": solver_name,
137        "metadata": {
138            "description": config.metadata.description,
139            "author": config.metadata.author,
140            "date": chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(),
141        },
142    })
143}
144
145/// Trait for computing field pressure at points (implemented by BEM/FEM solvers)
146pub trait FieldPressureCalculator {
147    /// Compute pressure at field points given the surface solution
148    fn calculate_field_pressure(
149        &self,
150        mesh: &RoomMesh,
151        surface_solution: &Array1<Complex64>,
152        field_points: &[Point3D],
153        wavenumber: f64,
154        frequency: f64,
155    ) -> Vec<Complex64>;
156}
157
158/// Generate spatial slices for visualization
159pub fn generate_spatial_slices<F>(
160    simulation: &RoomSimulation,
161    config: &VisualizationConfig,
162    mesh: &RoomMesh,
163    solutions: &[(f64, Array1<Complex64>)],
164    calculate_pressure: F,
165) -> (Vec<serde_json::Value>, Vec<serde_json::Value>)
166where
167    F: Fn(&RoomMesh, &Array1<Complex64>, &[Point3D], f64, f64) -> Vec<Complex64>,
168{
169    let lp = simulation.listening_positions[0];
170    let (room_width, room_depth, room_height) = simulation.room.dimensions();
171
172    let res = config.slice_resolution;
173    let x_points: Vec<f64> = (0..res)
174        .map(|i| i as f64 * room_width / (res - 1) as f64)
175        .collect();
176    let y_points: Vec<f64> = (0..res)
177        .map(|i| i as f64 * room_depth / (res - 1) as f64)
178        .collect();
179    let z_points: Vec<f64> = (0..res)
180        .map(|i| i as f64 * room_height / (res - 1) as f64)
181        .collect();
182
183    let mut horizontal_slices = Vec::new();
184    let mut vertical_slices = Vec::new();
185
186    for (freq, surface_pressure) in solutions.iter() {
187        let k = simulation.wavenumber(*freq);
188
189        // Horizontal slice (XY at LP.z)
190        let mut h_field_points = Vec::new();
191        for &y in &y_points {
192            for &x in &x_points {
193                h_field_points.push(Point3D::new(x, y, lp.z));
194            }
195        }
196
197        let h_pressures = calculate_pressure(mesh, surface_pressure, &h_field_points, k, *freq);
198
199        let mut h_spl_grid = Array2::zeros((res, res));
200        for (idx, p) in h_pressures.iter().enumerate() {
201            let i = idx % res;
202            let j = idx / res;
203            h_spl_grid[[j, i]] = pressure_to_spl(*p);
204        }
205
206        horizontal_slices.push(serde_json::json!({
207            "x": x_points,
208            "y": y_points,
209            "spl": h_spl_grid.iter().cloned().collect::<Vec<f64>>(),
210            "shape": [res, res],
211            "frequency": freq,
212        }));
213
214        // Vertical slice (XZ at LP.y)
215        let mut v_field_points = Vec::new();
216        for &z in &z_points {
217            for &x in &x_points {
218                v_field_points.push(Point3D::new(x, lp.y, z));
219            }
220        }
221
222        let v_pressures = calculate_pressure(mesh, surface_pressure, &v_field_points, k, *freq);
223
224        let mut v_spl_grid = Array2::zeros((res, res));
225        for (idx, p) in v_pressures.iter().enumerate() {
226            let i = idx % res;
227            let j = idx / res;
228            v_spl_grid[[j, i]] = pressure_to_spl(*p);
229        }
230
231        vertical_slices.push(serde_json::json!({
232            "x": x_points,
233            "z": z_points,
234            "spl": v_spl_grid.iter().cloned().collect::<Vec<f64>>(),
235            "shape": [res, res],
236            "frequency": freq,
237        }));
238    }
239
240    (horizontal_slices, vertical_slices)
241}
242
243/// Print configuration summary to stdout
244pub fn print_config_summary(config: &RoomConfig) {
245    println!("\n=== Configuration Summary ===");
246    match &config.room {
247        crate::config::RoomGeometryConfig::Rectangular {
248            width,
249            depth,
250            height,
251        } => {
252            println!(
253                "Room: Rectangular {:.1}m × {:.1}m × {:.1}m",
254                width, depth, height
255            );
256        }
257        crate::config::RoomGeometryConfig::LShaped {
258            width1,
259            depth1,
260            width2,
261            depth2,
262            height,
263        } => {
264            println!("Room: L-shaped");
265            println!("  Main: {:.1}m × {:.1}m", width1, depth1);
266            println!("  Extension: {:.1}m × {:.1}m", width2, depth2);
267            println!("  Height: {:.1}m", height);
268        }
269    }
270
271    println!("\nSources: {}", config.sources.len());
272    for source in &config.sources {
273        println!(
274            "  - {}: ({:.2}, {:.2}, {:.2})",
275            source.name, source.position.x, source.position.y, source.position.z
276        );
277        match &source.crossover {
278            crate::config::CrossoverConfig::Lowpass { cutoff_freq, order } => {
279                println!("    Lowpass: {:.0}Hz, order {}", cutoff_freq, order)
280            }
281            crate::config::CrossoverConfig::Highpass { cutoff_freq, order } => {
282                println!("    Highpass: {:.0}Hz, order {}", cutoff_freq, order)
283            }
284            crate::config::CrossoverConfig::Bandpass {
285                low_cutoff,
286                high_cutoff,
287                order,
288            } => println!(
289                "    Bandpass: {:.0}-{:.0}Hz, order {}",
290                low_cutoff, high_cutoff, order
291            ),
292            _ => {} // Ignore other crossover types
293        }
294    }
295
296    println!(
297        "\nFrequencies: {:.0} Hz to {:.0} Hz ({} points)",
298        config.frequencies.min_freq, config.frequencies.max_freq, config.frequencies.num_points
299    );
300
301    println!("\nSolver Configuration:");
302    println!("  Method: {}", config.solver.method);
303    println!(
304        "  Mesh resolution: {} elements/meter",
305        config.solver.mesh_resolution
306    );
307    println!(
308        "  Adaptive integration: {}",
309        config.solver.adaptive_integration
310    );
311
312    // Boundary summary
313    println!("\nBoundaries:");
314    let b = &config.boundaries;
315    let format_bc = |s: &crate::config::SurfaceConfig| match s {
316        crate::config::SurfaceConfig::Rigid => "Rigid".to_string(),
317        crate::config::SurfaceConfig::Absorption { coefficient } => {
318            format!("Abs α={:.2}", coefficient)
319        }
320        crate::config::SurfaceConfig::Impedance { real, imag } => {
321            format!("Z={:.1}+{:.1}i", real, imag)
322        }
323    };
324
325    println!("  Default walls: {}", format_bc(&b.walls));
326    println!("  Floor:         {}", format_bc(&b.floor));
327    println!("  Ceiling:       {}", format_bc(&b.ceiling));
328}
329
330/// Create a default room configuration for testing
331pub fn create_default_config() -> RoomConfig {
332    RoomConfig {
333        room: crate::config::RoomGeometryConfig::Rectangular {
334            width: 5.0,
335            depth: 4.0,
336            height: 2.5,
337        },
338        sources: vec![crate::config::SourceConfig {
339            name: "Main Speaker".to_string(),
340            position: crate::config::Point3DConfig {
341                x: 2.5,
342                y: 0.5,
343                z: 1.2,
344            },
345            amplitude: 1.0,
346            directivity: crate::config::DirectivityConfig::Omnidirectional,
347            crossover: crate::config::CrossoverConfig::FullRange,
348        }],
349        listening_positions: vec![crate::config::Point3DConfig {
350            x: 2.5,
351            y: 2.0,
352            z: 1.2,
353        }],
354        frequencies: crate::config::FrequencyConfig {
355            min_freq: 50.0,
356            max_freq: 500.0,
357            num_points: 20,
358            spacing: "logarithmic".to_string(),
359        },
360        solver: crate::config::SolverConfig::default(),
361        boundaries: BoundaryConfig::default(),
362        visualization: VisualizationConfig::default(),
363        metadata: MetadataConfig::default(),
364    }
365}