1use std::fs;
13use std::path::{Path, PathBuf};
14
15use ndarray::{Array1, Array2};
16use num_complex::Complex64;
17
18use crate::core::types::{BoundaryCondition, Element, ElementProperty, ElementType, PhysicsParams};
19
20#[derive(Debug, Clone)]
22pub struct NcInputConfig {
23 pub version: String,
25 pub description: String,
27 pub control_params_i: Vec<i32>,
29 pub control_params_ii: Vec<f64>,
31 pub frequency_curve: Vec<(f64, f64, f64)>,
33 pub main_params_i: MainParamsI,
35 pub main_params_ii: MainParamsII,
37 pub main_params_iii: Vec<i32>,
39 pub main_params_iv: MainParamsIV,
41 pub node_files: Vec<PathBuf>,
43 pub element_files: Vec<PathBuf>,
45 pub symmetry: Option<SymmetryConfig>,
47 pub boundary_conditions: Vec<BoundarySpec>,
49 pub plane_waves: Vec<PlaneWaveSource>,
51 pub point_sources: Vec<PointSource>,
53 pub base_dir: PathBuf,
55}
56
57#[derive(Debug, Clone, Default)]
59pub struct MainParamsI {
60 pub element_type: i32,
62 pub num_nodes: usize,
64 pub num_elements: usize,
66 pub num_object_files: i32,
68 pub num_eval_files: i32,
70 pub bc_type: i32,
72 pub solver_method: i32,
74 pub fmm_method: i32,
76 pub parallel: i32,
78}
79
80#[derive(Debug, Clone, Default)]
82pub struct MainParamsII {
83 pub preconditioner: i32,
85 pub iterative_solver: i32,
87 pub reserved1: i32,
89 pub reserved2: f64,
91 pub output_level: i32,
93 pub reserved3: i32,
95 pub reserved4: i32,
97}
98
99#[derive(Debug, Clone)]
101pub struct MainParamsIV {
102 pub speed_of_sound: f64,
104 pub density: f64,
106 pub reference_pressure: f64,
108 pub reserved: Vec<f64>,
110}
111
112impl Default for MainParamsIV {
113 fn default() -> Self {
114 Self {
115 speed_of_sound: 343.0,
116 density: 1.21,
117 reference_pressure: 1.0,
118 reserved: vec![0.0; 4],
119 }
120 }
121}
122
123#[derive(Debug, Clone)]
125pub struct SymmetryConfig {
126 pub flags: [bool; 3],
128 pub origin: [f64; 3],
130}
131
132#[derive(Debug, Clone)]
134pub struct BoundarySpec {
135 pub elem_start: usize,
137 pub elem_end: usize,
139 pub bc_type: String,
141 pub value_re: f64,
143 pub curve_re: i32,
145 pub value_im: f64,
147 pub curve_im: i32,
149}
150
151#[derive(Debug, Clone)]
153pub struct PlaneWaveSource {
154 pub direction: [f64; 3],
156 pub amplitude_re: f64,
158 pub curve_re: i32,
160 pub amplitude_im: f64,
162 pub curve_im: i32,
164}
165
166#[derive(Debug, Clone)]
168pub struct PointSource {
169 pub position: [f64; 3],
171 pub amplitude_re: f64,
173 pub curve_re: i32,
175 pub amplitude_im: f64,
177 pub curve_im: i32,
179}
180
181#[derive(Debug, thiserror::Error)]
183pub enum NcParseError {
184 #[error("IO error: {0}")]
186 Io(#[from] std::io::Error),
187 #[error("Parse error at line {line}: {message}")]
189 Parse {
190 line: usize,
192 message: String,
194 },
195 #[error("Missing required section: {0}")]
197 MissingSection(String),
198 #[error("Invalid format: {0}")]
200 InvalidFormat(String),
201}
202
203pub fn parse_nc_input<P: AsRef<Path>>(path: P) -> Result<NcInputConfig, NcParseError> {
205 let path = path.as_ref();
206 let base_dir = path.parent().unwrap_or(Path::new(".")).to_path_buf();
207 let content = fs::read_to_string(path)?;
208
209 parse_nc_input_string(&content, base_dir)
210}
211
212pub fn parse_nc_input_string(
214 content: &str,
215 base_dir: PathBuf,
216) -> Result<NcInputConfig, NcParseError> {
217 let mut config = NcInputConfig {
218 version: String::new(),
219 description: String::new(),
220 control_params_i: Vec::new(),
221 control_params_ii: Vec::new(),
222 frequency_curve: Vec::new(),
223 main_params_i: MainParamsI::default(),
224 main_params_ii: MainParamsII::default(),
225 main_params_iii: Vec::new(),
226 main_params_iv: MainParamsIV::default(),
227 node_files: Vec::new(),
228 element_files: Vec::new(),
229 symmetry: None,
230 boundary_conditions: Vec::new(),
231 plane_waves: Vec::new(),
232 point_sources: Vec::new(),
233 base_dir,
234 };
235
236 let lines: Vec<&str> = content.lines().collect();
237 let mut i = 0;
238
239 while i < lines.len() {
240 let line = lines[i].trim();
241
242 if line.is_empty() || line.starts_with("##") {
244 i += 1;
245 continue;
246 }
247
248 if line.starts_with('#') && !line.starts_with("##") {
250 i += 1;
252 continue;
253 }
254
255 if line.starts_with("Mesh2HRTF") {
257 config.version = line.to_string();
258 i += 1;
259 continue;
260 }
261
262 if config.version.is_empty() {
264 i += 1;
265 continue;
266 }
267
268 if i > 0
270 && lines.get(i.saturating_sub(1)).is_some_and(|l| {
271 l.contains("Controlparameter I") && !l.contains("Controlparameter II")
272 })
273 {
274 config.control_params_i = parse_int_line(line);
275 i += 1;
276 continue;
277 }
278
279 if i > 0
281 && lines
282 .get(i.saturating_sub(1))
283 .is_some_and(|l| l.contains("Controlparameter II"))
284 {
285 config.control_params_ii = parse_float_line(line);
286 i += 1;
287 continue;
288 }
289
290 if i > 0
292 && lines
293 .get(i.saturating_sub(1))
294 .is_some_and(|l| l.contains("Frequency Curve"))
295 {
296 let header = parse_int_line(line);
297 let num_points = header.get(1).copied().unwrap_or(0) as usize;
298 i += 1;
299
300 for _ in 0..num_points {
301 if i < lines.len() {
302 let values = parse_float_line(lines[i]);
303 if values.len() >= 3 {
304 config
305 .frequency_curve
306 .push((values[0], values[1], values[2]));
307 }
308 i += 1;
309 }
310 }
311 continue;
312 }
313
314 if i > 0
316 && lines.get(i.saturating_sub(1)).is_some_and(|l| {
317 l.contains("Main Parameters I")
318 && !l.contains("Main Parameters II")
319 && !l.contains("Main Parameters III")
320 && !l.contains("Main Parameters IV")
321 })
322 {
323 let values = parse_int_line(line);
324 config.main_params_i = MainParamsI {
325 element_type: values.first().copied().unwrap_or(0),
326 num_nodes: values.get(1).copied().unwrap_or(0) as usize,
327 num_elements: values.get(2).copied().unwrap_or(0) as usize,
328 num_object_files: values.get(3).copied().unwrap_or(0),
329 num_eval_files: values.get(4).copied().unwrap_or(0),
330 bc_type: values.get(5).copied().unwrap_or(0),
331 solver_method: values.get(6).copied().unwrap_or(0),
332 fmm_method: values.get(7).copied().unwrap_or(0),
333 parallel: values.get(8).copied().unwrap_or(0),
334 };
335 i += 1;
336 continue;
337 }
338
339 if i > 0
341 && lines.get(i.saturating_sub(1)).is_some_and(|l| {
342 l.contains("Main Parameters II")
343 && !l.contains("Main Parameters III")
344 && !l.contains("Main Parameters IV")
345 })
346 {
347 let values = parse_mixed_line(line);
348 config.main_params_ii = MainParamsII {
349 preconditioner: values.first().map(|v| *v as i32).unwrap_or(0),
350 iterative_solver: values.get(1).map(|v| *v as i32).unwrap_or(0),
351 reserved1: values.get(2).map(|v| *v as i32).unwrap_or(0),
352 reserved2: values.get(3).copied().unwrap_or(0.0),
353 output_level: values.get(4).map(|v| *v as i32).unwrap_or(0),
354 reserved3: values.get(5).map(|v| *v as i32).unwrap_or(0),
355 reserved4: values.get(6).map(|v| *v as i32).unwrap_or(0),
356 };
357 i += 1;
358 continue;
359 }
360
361 if i > 0
363 && lines.get(i.saturating_sub(1)).is_some_and(|l| {
364 l.contains("Main Parameters III") && !l.contains("Main Parameters IV")
365 })
366 {
367 config.main_params_iii = parse_int_line(line);
368 i += 1;
369 continue;
370 }
371
372 if i > 0
374 && lines
375 .get(i.saturating_sub(1))
376 .is_some_and(|l| l.contains("Main Parameters IV"))
377 {
378 let values = parse_float_line(line);
379 config.main_params_iv = MainParamsIV {
380 speed_of_sound: values.first().copied().unwrap_or(343.0),
381 density: values.get(1).copied().unwrap_or(1.21),
382 reference_pressure: values.get(2).copied().unwrap_or(1.0),
383 reserved: values.get(3..).map(|s| s.to_vec()).unwrap_or_default(),
384 };
385 i += 1;
386 continue;
387 }
388
389 if line == "NODES" {
391 i += 1;
392 while i < lines.len() {
393 let node_line = lines[i].trim();
394 if node_line.starts_with("##") || node_line.is_empty() {
395 break;
396 }
397 if !node_line.starts_with('#') {
398 let path = config.base_dir.join(node_line);
399 config.node_files.push(path);
400 }
401 i += 1;
402 }
403 continue;
404 }
405
406 if line == "ELEMENTS" {
408 i += 1;
409 while i < lines.len() {
410 let elem_line = lines[i].trim();
411 if elem_line.starts_with("##") || elem_line.is_empty() {
412 break;
413 }
414 if !elem_line.starts_with('#') {
415 let path = config.base_dir.join(elem_line);
416 config.element_files.push(path);
417 }
418 i += 1;
419 }
420 continue;
421 }
422
423 if line == "SYMMETRY" {
425 i += 1;
426 if i < lines.len() {
427 let flags_line = lines[i].trim();
428 if !flags_line.starts_with('#') {
429 let flags = parse_int_line(flags_line);
430 i += 1;
431 if i < lines.len() {
432 let origin = parse_float_line(lines[i].trim());
433 config.symmetry = Some(SymmetryConfig {
434 flags: [
435 flags.first().copied().unwrap_or(0) != 0,
436 flags.get(1).copied().unwrap_or(0) != 0,
437 flags.get(2).copied().unwrap_or(0) != 0,
438 ],
439 origin: [
440 origin.first().copied().unwrap_or(0.0),
441 origin.get(1).copied().unwrap_or(0.0),
442 origin.get(2).copied().unwrap_or(0.0),
443 ],
444 });
445 i += 1;
446 }
447 }
448 }
449 continue;
450 }
451
452 if line == "BOUNDARY" {
454 i += 1;
455 while i < lines.len() {
456 let bc_line = lines[i].trim();
457 if bc_line.starts_with("##") || bc_line == "RETU" {
458 i += 1;
459 break;
460 }
461 if bc_line.starts_with('#') || bc_line.is_empty() {
462 i += 1;
463 continue;
464 }
465
466 if let Some(bc) = parse_boundary_line(bc_line) {
467 config.boundary_conditions.push(bc);
468 }
469 i += 1;
470 }
471 continue;
472 }
473
474 if line == "PLANE WAVES" {
476 i += 1;
477 while i < lines.len() {
478 let pw_line = lines[i].trim();
479 if pw_line.starts_with("##") || pw_line.is_empty() {
480 break;
481 }
482 if !pw_line.starts_with('#')
483 && let Some(pw) = parse_plane_wave_line(pw_line)
484 {
485 config.plane_waves.push(pw);
486 }
487 i += 1;
488 }
489 continue;
490 }
491
492 if line == "POINT SOURCES" {
494 i += 1;
495 while i < lines.len() {
496 let ps_line = lines[i].trim();
497 if ps_line.starts_with("##") || ps_line.is_empty() {
498 break;
499 }
500 if !ps_line.starts_with('#')
501 && let Some(ps) = parse_point_source_line(ps_line)
502 {
503 config.point_sources.push(ps);
504 }
505 i += 1;
506 }
507 continue;
508 }
509
510 if line == "END" {
512 break;
513 }
514
515 i += 1;
516 }
517
518 Ok(config)
519}
520
521fn parse_int_line(line: &str) -> Vec<i32> {
523 line.split_whitespace()
524 .filter_map(|s| s.parse::<i32>().ok())
525 .collect()
526}
527
528fn parse_float_line(line: &str) -> Vec<f64> {
530 line.split_whitespace()
531 .filter_map(|s| {
532 let s = s.replace(" ", "");
534 s.parse::<f64>().ok()
535 })
536 .collect()
537}
538
539fn parse_mixed_line(line: &str) -> Vec<f64> {
541 parse_float_line(line)
542}
543
544fn parse_boundary_line(line: &str) -> Option<BoundarySpec> {
546 let parts: Vec<&str> = line.split_whitespace().collect();
547
548 if parts.len() >= 9 && parts[0] == "ELEM" && parts[2] == "TO" {
550 let elem_start = parts[1].parse().ok()?;
551 let elem_end = parts[3].parse().ok()?;
552 let bc_type = parts[4].to_string();
553 let value_re = parts[5].parse().ok()?;
554 let curve_re = parts[6].parse().ok()?;
555 let value_im = parts[7].parse().ok()?;
556 let curve_im = parts[8].parse().ok()?;
557
558 return Some(BoundarySpec {
559 elem_start,
560 elem_end,
561 bc_type,
562 value_re,
563 curve_re,
564 value_im,
565 curve_im,
566 });
567 }
568
569 None
570}
571
572fn parse_plane_wave_line(line: &str) -> Option<PlaneWaveSource> {
574 let values = parse_float_line(line);
575 if values.len() >= 8 {
576 Some(PlaneWaveSource {
577 direction: [values[1], values[2], values[3]],
578 amplitude_re: values[4],
579 curve_re: values[5] as i32,
580 amplitude_im: values[6],
581 curve_im: values[7] as i32,
582 })
583 } else {
584 None
585 }
586}
587
588fn parse_point_source_line(line: &str) -> Option<PointSource> {
590 let values = parse_float_line(line);
591 if values.len() >= 8 {
592 Some(PointSource {
593 position: [values[1], values[2], values[3]],
594 amplitude_re: values[4],
595 curve_re: values[5] as i32,
596 amplitude_im: values[6],
597 curve_im: values[7] as i32,
598 })
599 } else {
600 None
601 }
602}
603
604pub fn load_nc_nodes<P: AsRef<Path>>(path: P) -> Result<Array2<f64>, NcParseError> {
606 let content = fs::read_to_string(path)?;
607 let lines: Vec<&str> = content.lines().collect();
608
609 if lines.is_empty() {
610 return Ok(Array2::zeros((0, 3)));
611 }
612
613 let start_line = if lines[0].trim().parse::<usize>().is_ok() {
615 1
616 } else {
617 0
618 };
619
620 let mut nodes = Vec::new();
621 for line in &lines[start_line..] {
622 let values = parse_float_line(line);
623 if values.len() >= 4 {
624 nodes.push([values[1], values[2], values[3]]);
626 } else if values.len() >= 3 {
627 nodes.push([values[0], values[1], values[2]]);
629 }
630 }
631
632 let n = nodes.len();
633 let flat: Vec<f64> = nodes.into_iter().flatten().collect();
634 Array2::from_shape_vec((n, 3), flat).map_err(|e| NcParseError::InvalidFormat(e.to_string()))
635}
636
637pub fn load_nc_elements<P: AsRef<Path>>(
639 path: P,
640 property: ElementProperty,
641) -> Result<Vec<Element>, NcParseError> {
642 let content = fs::read_to_string(path)?;
643 let lines: Vec<&str> = content.lines().collect();
644
645 if lines.is_empty() {
646 return Ok(Vec::new());
647 }
648
649 let start_line = if lines[0].trim().parse::<usize>().is_ok() {
650 1
651 } else {
652 0
653 };
654
655 let mut elements = Vec::new();
656 for (idx, line) in lines[start_line..].iter().enumerate() {
657 let values: Vec<i32> = line
658 .split_whitespace()
659 .filter_map(|s| s.parse().ok())
660 .collect();
661
662 if values.len() >= 4 {
663 let connectivity: Vec<usize> = values[1..]
665 .iter()
666 .take_while(|&&v| v >= 0)
667 .map(|&v| v as usize)
668 .collect();
669
670 let element_type = if connectivity.len() == 3 {
671 ElementType::Tri3
672 } else {
673 ElementType::Quad4
674 };
675
676 let elem = Element {
677 connectivity,
678 element_type,
679 property,
680 normal: Array1::zeros(3),
681 node_normals: Array2::zeros((element_type.num_nodes(), 3)),
682 center: Array1::zeros(3),
683 area: 0.0,
684 boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)]),
685 group: 0,
686 dof_addresses: vec![idx],
687 };
688 elements.push(elem);
689 }
690 }
691
692 Ok(elements)
693}
694
695impl NcInputConfig {
697 pub fn to_physics_params(&self, frequency: f64) -> PhysicsParams {
699 PhysicsParams::new(
700 frequency,
701 self.main_params_iv.speed_of_sound,
702 self.main_params_iv.density,
703 false, )
705 }
706
707 pub fn bem_method(&self) -> crate::core::types::BemMethod {
709 match self.main_params_i.solver_method {
710 0 => crate::core::types::BemMethod::Traditional,
711 1 => crate::core::types::BemMethod::SingleLevelFmm,
712 2 | 3 => crate::core::types::BemMethod::MultiLevelFmm,
713 _ => crate::core::types::BemMethod::Traditional,
714 }
715 }
716
717 pub fn solver_method(&self) -> crate::core::types::SolverMethod {
719 match self.main_params_ii.iterative_solver {
720 0 => crate::core::types::SolverMethod::Cgs,
721 1 => crate::core::types::SolverMethod::BiCgstab,
722 _ => crate::core::types::SolverMethod::Cgs,
723 }
724 }
725}
726
727#[cfg(test)]
728mod tests {
729 use super::*;
730
731 const SAMPLE_NC_INP: &str = r#"##-------------------------------------------
732## This file was created by mesh2input
733##-------------------------------------------
734Mesh2HRTF 1.0.0
735##
736Test Description
737##
738## Controlparameter I
7390 0 0 0 7 0
740##
741## Controlparameter II
7421 1 0.000001 0.00e+00 1 0 0
743##
744## Load Frequency Curve
7450 2
7460.000000 0.000000e+00 0.0
7470.000001 0.400000e+04 0.0
748##
749## 1. Main Parameters I
7502 100 50 0 0 2 1 0 0
751##
752## 2. Main Parameters II
7531 0 0 0.0000e+00 0 0 0
754##
755## 3. Main Parameters III
7560 0 0 0
757##
758## 4. Main Parameters IV
759343 1.21 1.0 0.0 0.0 0.0 0.0
760##
761NODES
762nodes.txt
763##
764ELEMENTS
765elements.txt
766##
767BOUNDARY
768ELEM 0 TO 49 VELO 1.0 -1 0.0 -1
769RETU
770##
771PLANE WAVES
7721 0.0 -1.0 0.0 1.0 -1 0.0 -1
773##
774END
775"#;
776
777 #[test]
778 fn test_parse_nc_input() {
779 let config = parse_nc_input_string(SAMPLE_NC_INP, PathBuf::from(".")).unwrap();
780
781 assert!(config.version.contains("Mesh2HRTF"));
782 assert_eq!(config.main_params_i.num_nodes, 100);
783 assert_eq!(config.main_params_i.num_elements, 50);
784 assert_eq!(config.main_params_i.solver_method, 1);
785 assert!((config.main_params_iv.speed_of_sound - 343.0).abs() < 0.01);
786 assert!((config.main_params_iv.density - 1.21).abs() < 0.01);
787 assert_eq!(config.node_files.len(), 1);
788 assert_eq!(config.element_files.len(), 1);
789 assert_eq!(config.boundary_conditions.len(), 1);
790 assert_eq!(config.plane_waves.len(), 1);
791 }
792
793 #[test]
794 fn test_parse_boundary_line() {
795 let bc = parse_boundary_line("ELEM 0 TO 100 VELO 1.0 -1 0.0 -1").unwrap();
796 assert_eq!(bc.elem_start, 0);
797 assert_eq!(bc.elem_end, 100);
798 assert_eq!(bc.bc_type, "VELO");
799 assert!((bc.value_re - 1.0).abs() < 0.001);
800 }
801
802 #[test]
803 fn test_parse_plane_wave() {
804 let pw = parse_plane_wave_line("1 0.0 -1.0 0.0 1.0 -1 0.0 -1").unwrap();
805 assert!((pw.direction[1] - (-1.0)).abs() < 0.001);
806 assert!((pw.amplitude_re - 1.0).abs() < 0.001);
807 }
808
809 #[test]
810 fn test_config_to_physics() {
811 let config = parse_nc_input_string(SAMPLE_NC_INP, PathBuf::from(".")).unwrap();
812 let physics = config.to_physics_params(1000.0);
813
814 assert!((physics.speed_of_sound - 343.0).abs() < 0.01);
815 assert!((physics.density - 1.21).abs() < 0.01);
816 assert!((physics.frequency - 1000.0).abs() < 0.01);
817 }
818}