1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
14pub struct FrequencyResult {
15 pub frequency: f64,
17 pub spl_at_lp: Vec<f64>,
19}
20
21#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct SimulationResults {
24 pub frequencies: Vec<f64>,
26 pub lp_frequency_responses: Vec<Vec<f64>>,
28 pub horizontal_slice: Option<SliceData>,
30 pub vertical_slice: Option<SliceData>,
32}
33
34#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct SliceData {
37 pub x: Vec<f64>,
39 pub y: Vec<f64>,
41 pub spl: Array2<f64>,
43 pub frequency: f64,
45}
46
47pub 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
92pub 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
145pub trait FieldPressureCalculator {
147 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
158pub 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 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 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
243pub 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 _ => {} }
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 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
330pub 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}