xem_common/
output.rs

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