1use 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#[derive(Debug, Clone, Serialize, Deserialize)]
12pub struct FrequencyResult {
13 pub frequency: f64,
15 pub spl_at_lp: Vec<f64>,
17}
18
19#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct SimulationResults {
22 pub frequencies: Vec<f64>,
24 pub lp_frequency_responses: Vec<Vec<f64>>,
26 pub horizontal_slice: Option<SliceData>,
28 pub vertical_slice: Option<SliceData>,
30}
31
32#[derive(Debug, Clone, Serialize, Deserialize)]
34pub struct SliceData {
35 pub x: Vec<f64>,
37 pub y: Vec<f64>,
39 pub spl: Array2<f64>,
41 pub frequency: f64,
43}
44
45pub 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
90pub 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
143pub trait FieldPressureCalculator {
145 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
156pub 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 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 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
241pub 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
311pub 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}