1use std::fs;
28use std::path::{Path, PathBuf};
29
30use ndarray::{Array1, Array2};
31use num_complex::Complex64;
32use serde::{Deserialize, Serialize};
33
34use crate::core::types::{
35 BemMethod, BoundaryCondition, Element, ElementProperty, ElementType, PhysicsParams,
36 SolverMethod,
37};
38
39#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BemConfig {
42 #[serde(default)]
44 pub description: String,
45
46 pub physics: PhysicsConfig,
48
49 pub mesh: MeshConfig,
51
52 #[serde(default)]
54 pub solver: SolverConfig,
55
56 #[serde(default)]
58 pub bem: BemMethodConfig,
59
60 #[serde(default)]
62 pub boundary_conditions: Vec<BoundaryConditionConfig>,
63
64 #[serde(default)]
66 pub sources: SourceConfig,
67
68 #[serde(default)]
70 pub output: OutputConfig,
71}
72
73#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct PhysicsConfig {
76 pub frequency: f64,
78
79 #[serde(default = "default_speed_of_sound")]
81 pub speed_of_sound: f64,
82
83 #[serde(default = "default_density")]
85 pub density: f64,
86
87 #[serde(default = "default_reference_pressure")]
89 pub reference_pressure: f64,
90
91 #[serde(default = "default_true")]
93 pub external_problem: bool,
94}
95
96fn default_speed_of_sound() -> f64 {
97 343.0
98}
99fn default_density() -> f64 {
100 1.21
101}
102fn default_reference_pressure() -> f64 {
103 1.0
104}
105fn default_true() -> bool {
106 true
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct MeshConfig {
112 #[serde(default)]
114 pub nodes_file: Option<PathBuf>,
115
116 #[serde(default)]
118 pub nodes: Option<Vec<[f64; 3]>>,
119
120 #[serde(default)]
122 pub elements_file: Option<PathBuf>,
123
124 #[serde(default)]
126 pub elements: Option<Vec<Vec<usize>>>,
127
128 #[serde(default)]
130 pub symmetry: Option<[bool; 3]>,
131
132 #[serde(default)]
134 pub symmetry_origin: Option<[f64; 3]>,
135}
136
137#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139pub struct SolverConfig {
140 #[serde(default)]
142 pub method: SolverMethodConfig,
143
144 #[serde(default = "default_tolerance")]
146 pub tolerance: f64,
147
148 #[serde(default = "default_max_iterations")]
150 pub max_iterations: usize,
151
152 #[serde(default)]
154 pub preconditioner: PreconditionerConfig,
155
156 #[serde(default)]
158 pub print_interval: usize,
159}
160
161fn default_tolerance() -> f64 {
162 1e-6
163}
164fn default_max_iterations() -> usize {
165 1000
166}
167
168#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
170#[serde(rename_all = "lowercase")]
171pub enum SolverMethodConfig {
172 Direct,
174 #[default]
176 Cgs,
177 #[serde(alias = "bicgstab")]
179 BiCgstab,
180}
181
182#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
184#[serde(rename_all = "lowercase")]
185pub enum PreconditionerConfig {
186 #[default]
188 None,
189 Diagonal,
191 RowScaling,
193 BlockDiagonal,
195}
196
197#[derive(Debug, Clone, Serialize, Deserialize, Default)]
199pub struct BemMethodConfig {
200 #[serde(default)]
202 pub method: BemMethodType,
203
204 #[serde(default)]
206 pub burton_miller: bool,
207
208 #[serde(default = "default_coupling")]
210 pub coupling_parameter: f64,
211
212 #[serde(default = "default_cluster_size")]
214 pub cluster_size: usize,
215
216 #[serde(default = "default_expansion_terms")]
218 pub expansion_terms: usize,
219}
220
221fn default_coupling() -> f64 {
222 1.0
223}
224fn default_cluster_size() -> usize {
225 20
226}
227fn default_expansion_terms() -> usize {
228 4
229}
230
231#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
233#[serde(rename_all = "lowercase")]
234pub enum BemMethodType {
235 #[default]
237 Traditional,
238 #[serde(alias = "slfmm")]
240 SingleLevelFmm,
241 #[serde(alias = "mlfmm")]
243 MultiLevelFmm,
244}
245
246#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct BoundaryConditionConfig {
249 pub elements: (usize, usize),
251
252 #[serde(rename = "type")]
254 pub bc_type: BoundaryConditionType,
255
256 pub value: f64,
258
259 #[serde(default)]
261 pub value_imag: f64,
262}
263
264#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
266#[serde(rename_all = "lowercase")]
267pub enum BoundaryConditionType {
268 Velocity,
270 Pressure,
272 Admittance,
274 Impedance,
276 TransferAdmittance,
278}
279
280#[derive(Debug, Clone, Serialize, Deserialize, Default)]
282pub struct SourceConfig {
283 #[serde(default)]
285 pub plane_waves: Vec<PlaneWaveConfig>,
286
287 #[serde(default)]
289 pub point_sources: Vec<PointSourceConfig>,
290}
291
292#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct PlaneWaveConfig {
295 pub direction: [f64; 3],
297
298 pub amplitude: f64,
300
301 #[serde(default)]
303 pub phase: f64,
304}
305
306#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct PointSourceConfig {
309 pub position: [f64; 3],
311
312 pub amplitude: f64,
314
315 #[serde(default)]
317 pub phase: f64,
318}
319
320#[derive(Debug, Clone, Serialize, Deserialize, Default)]
322pub struct OutputConfig {
323 #[serde(default)]
325 pub directory: Option<PathBuf>,
326
327 #[serde(default)]
329 pub pressure: bool,
330
331 #[serde(default)]
333 pub velocity: bool,
334
335 #[serde(default)]
337 pub evaluation_points_file: Option<PathBuf>,
338
339 #[serde(default)]
341 pub evaluation_points: Option<Vec<[f64; 3]>>,
342}
343
344#[derive(Debug, Clone, Copy)]
346pub enum ConfigFormat {
347 Json,
349 Toml,
351}
352
353impl ConfigFormat {
354 pub fn from_path<P: AsRef<Path>>(path: P) -> Option<Self> {
356 let ext = path.as_ref().extension()?.to_str()?;
357 match ext.to_lowercase().as_str() {
358 "json" => Some(ConfigFormat::Json),
359 "toml" => Some(ConfigFormat::Toml),
360 _ => None,
361 }
362 }
363}
364
365pub fn load_config<P: AsRef<Path>>(path: P) -> Result<BemConfig, ConfigError> {
369 let path = path.as_ref();
370 let content = fs::read_to_string(path)?;
371
372 let format = ConfigFormat::from_path(path)
373 .ok_or_else(|| ConfigError::UnsupportedFormat(path.display().to_string()))?;
374
375 parse_config(&content, format)
376}
377
378pub fn parse_config(content: &str, format: ConfigFormat) -> Result<BemConfig, ConfigError> {
380 match format {
381 ConfigFormat::Json => {
382 serde_json::from_str(content).map_err(|e| ConfigError::ParseError(e.to_string()))
383 }
384 ConfigFormat::Toml => {
385 toml::from_str(content).map_err(|e| ConfigError::ParseError(e.to_string()))
386 }
387 }
388}
389
390pub fn save_config<P: AsRef<Path>>(config: &BemConfig, path: P) -> Result<(), ConfigError> {
392 let path = path.as_ref();
393 let format = ConfigFormat::from_path(path)
394 .ok_or_else(|| ConfigError::UnsupportedFormat(path.display().to_string()))?;
395
396 let content = serialize_config(config, format)?;
397 fs::write(path, content)?;
398 Ok(())
399}
400
401pub fn serialize_config(config: &BemConfig, format: ConfigFormat) -> Result<String, ConfigError> {
403 match format {
404 ConfigFormat::Json => {
405 serde_json::to_string_pretty(config).map_err(|e| ConfigError::SerializeError(e.to_string()))
406 }
407 ConfigFormat::Toml => {
408 toml::to_string_pretty(config).map_err(|e| ConfigError::SerializeError(e.to_string()))
409 }
410 }
411}
412
413#[derive(Debug, thiserror::Error)]
415pub enum ConfigError {
416 #[error("IO error: {0}")]
418 Io(#[from] std::io::Error),
419
420 #[error("Parse error: {0}")]
422 ParseError(String),
423
424 #[error("Serialize error: {0}")]
426 SerializeError(String),
427
428 #[error("Unsupported format: {0}")]
430 UnsupportedFormat(String),
431
432 #[error("Missing required field: {0}")]
434 MissingField(String),
435}
436
437impl BemConfig {
438 pub fn to_physics_params(&self) -> PhysicsParams {
440 PhysicsParams::new(
441 self.physics.frequency,
442 self.physics.speed_of_sound,
443 self.physics.density,
444 self.physics.external_problem,
445 )
446 }
447
448 pub fn bem_method(&self) -> BemMethod {
450 match self.bem.method {
451 BemMethodType::Traditional => BemMethod::Traditional,
452 BemMethodType::SingleLevelFmm => BemMethod::SingleLevelFmm,
453 BemMethodType::MultiLevelFmm => BemMethod::MultiLevelFmm,
454 }
455 }
456
457 pub fn solver_method(&self) -> SolverMethod {
459 match self.solver.method {
460 SolverMethodConfig::Direct => SolverMethod::Direct,
461 SolverMethodConfig::Cgs => SolverMethod::Cgs,
462 SolverMethodConfig::BiCgstab => SolverMethod::BiCgstab,
463 }
464 }
465
466 pub fn load_nodes(&self, base_dir: &Path) -> Result<Array2<f64>, ConfigError> {
468 if let Some(ref nodes) = self.mesh.nodes {
469 let n = nodes.len();
471 let mut arr = Array2::zeros((n, 3));
472 for (i, node) in nodes.iter().enumerate() {
473 arr[[i, 0]] = node[0];
474 arr[[i, 1]] = node[1];
475 arr[[i, 2]] = node[2];
476 }
477 return Ok(arr);
478 }
479
480 if let Some(ref file) = self.mesh.nodes_file {
481 let path = base_dir.join(file);
482 let content = fs::read_to_string(&path)?;
483
484 if path.extension().map_or(false, |e| e == "json") {
486 let nodes: Vec<[f64; 3]> = serde_json::from_str(&content)
487 .map_err(|e| ConfigError::ParseError(e.to_string()))?;
488 let n = nodes.len();
489 let mut arr = Array2::zeros((n, 3));
490 for (i, node) in nodes.iter().enumerate() {
491 arr[[i, 0]] = node[0];
492 arr[[i, 1]] = node[1];
493 arr[[i, 2]] = node[2];
494 }
495 return Ok(arr);
496 }
497
498 let nodes = parse_csv_nodes(&content)?;
500 return Ok(nodes);
501 }
502
503 Err(ConfigError::MissingField("mesh.nodes or mesh.nodes_file".to_string()))
504 }
505
506 pub fn load_elements(&self, base_dir: &Path) -> Result<Vec<Element>, ConfigError> {
508 if let Some(ref elements) = self.mesh.elements {
509 return Ok(elements_from_connectivity(elements));
510 }
511
512 if let Some(ref file) = self.mesh.elements_file {
513 let path = base_dir.join(file);
514 let content = fs::read_to_string(&path)?;
515
516 if path.extension().map_or(false, |e| e == "json") {
518 let connectivity: Vec<Vec<usize>> = serde_json::from_str(&content)
519 .map_err(|e| ConfigError::ParseError(e.to_string()))?;
520 return Ok(elements_from_connectivity(&connectivity));
521 }
522
523 let elements = parse_csv_elements(&content)?;
525 return Ok(elements);
526 }
527
528 Err(ConfigError::MissingField("mesh.elements or mesh.elements_file".to_string()))
529 }
530}
531
532fn parse_csv_nodes(content: &str) -> Result<Array2<f64>, ConfigError> {
534 let mut nodes = Vec::new();
535
536 for line in content.lines() {
537 let line = line.trim();
538 if line.is_empty() || line.starts_with('#') {
539 continue;
540 }
541
542 let values: Vec<f64> = line
543 .split(|c| c == ',' || c == ' ' || c == '\t')
544 .filter_map(|s| s.trim().parse().ok())
545 .collect();
546
547 if values.len() >= 3 {
548 nodes.push([values[0], values[1], values[2]]);
549 }
550 }
551
552 let n = nodes.len();
553 let mut arr = Array2::zeros((n, 3));
554 for (i, node) in nodes.iter().enumerate() {
555 arr[[i, 0]] = node[0];
556 arr[[i, 1]] = node[1];
557 arr[[i, 2]] = node[2];
558 }
559
560 Ok(arr)
561}
562
563fn parse_csv_elements(content: &str) -> Result<Vec<Element>, ConfigError> {
565 let mut connectivity = Vec::new();
566
567 for line in content.lines() {
568 let line = line.trim();
569 if line.is_empty() || line.starts_with('#') {
570 continue;
571 }
572
573 let values: Vec<usize> = line
574 .split(|c| c == ',' || c == ' ' || c == '\t')
575 .filter_map(|s| s.trim().parse().ok())
576 .collect();
577
578 if values.len() >= 3 {
579 connectivity.push(values);
580 }
581 }
582
583 Ok(elements_from_connectivity(&connectivity))
584}
585
586fn elements_from_connectivity(connectivity: &[Vec<usize>]) -> Vec<Element> {
588 connectivity
589 .iter()
590 .enumerate()
591 .map(|(idx, conn)| {
592 let element_type = if conn.len() == 3 {
593 ElementType::Tri3
594 } else {
595 ElementType::Quad4
596 };
597
598 Element {
599 connectivity: conn.clone(),
600 element_type,
601 property: ElementProperty::Surface,
602 normal: Array1::zeros(3),
603 node_normals: Array2::zeros((element_type.num_nodes(), 3)),
604 center: Array1::zeros(3),
605 area: 0.0,
606 boundary_condition: BoundaryCondition::Velocity(vec![Complex64::new(0.0, 0.0)]),
607 group: 0,
608 dof_addresses: vec![idx],
609 }
610 })
611 .collect()
612}
613
614#[cfg(test)]
615mod tests {
616 use super::*;
617
618 const SAMPLE_JSON: &str = r#"{
619 "description": "Test BEM problem",
620 "physics": {
621 "frequency": 1000.0,
622 "speed_of_sound": 343.0,
623 "density": 1.21
624 },
625 "mesh": {
626 "nodes": [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
627 "elements": [[0, 1, 2]]
628 },
629 "solver": {
630 "method": "bicgstab",
631 "tolerance": 1e-8,
632 "max_iterations": 500
633 },
634 "bem": {
635 "method": "traditional",
636 "burton_miller": true
637 },
638 "boundary_conditions": [
639 {
640 "elements": [0, 0],
641 "type": "velocity",
642 "value": 1.0
643 }
644 ],
645 "sources": {
646 "plane_waves": [
647 {
648 "direction": [0.0, 0.0, -1.0],
649 "amplitude": 1.0,
650 "phase": 0.0
651 }
652 ]
653 }
654 }"#;
655
656 const SAMPLE_TOML: &str = r#"
657description = "Test BEM problem"
658
659[physics]
660frequency = 1000.0
661speed_of_sound = 343.0
662density = 1.21
663
664[mesh]
665nodes = [[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]
666elements = [[0, 1, 2]]
667
668[solver]
669method = "bicgstab"
670tolerance = 1e-8
671max_iterations = 500
672
673[bem]
674method = "traditional"
675burton_miller = true
676
677[[boundary_conditions]]
678elements = [0, 0]
679type = "velocity"
680value = 1.0
681
682[sources]
683[[sources.plane_waves]]
684direction = [0.0, 0.0, -1.0]
685amplitude = 1.0
686phase = 0.0
687"#;
688
689 #[test]
690 fn test_parse_json() {
691 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
692
693 assert_eq!(config.description, "Test BEM problem");
694 assert!((config.physics.frequency - 1000.0).abs() < 0.01);
695 assert!((config.physics.speed_of_sound - 343.0).abs() < 0.01);
696 assert_eq!(config.mesh.nodes.as_ref().unwrap().len(), 3);
697 assert_eq!(config.mesh.elements.as_ref().unwrap().len(), 1);
698 assert!(matches!(config.solver.method, SolverMethodConfig::BiCgstab));
699 assert!(config.bem.burton_miller);
700 assert_eq!(config.boundary_conditions.len(), 1);
701 assert_eq!(config.sources.plane_waves.len(), 1);
702 }
703
704 #[test]
705 fn test_parse_toml() {
706 let config = parse_config(SAMPLE_TOML, ConfigFormat::Toml).unwrap();
707
708 assert_eq!(config.description, "Test BEM problem");
709 assert!((config.physics.frequency - 1000.0).abs() < 0.01);
710 assert!((config.physics.speed_of_sound - 343.0).abs() < 0.01);
711 assert_eq!(config.mesh.nodes.as_ref().unwrap().len(), 3);
712 }
713
714 #[test]
715 fn test_to_physics_params() {
716 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
717 let physics = config.to_physics_params();
718
719 assert!((physics.frequency - 1000.0).abs() < 0.01);
720 assert!((physics.speed_of_sound - 343.0).abs() < 0.01);
721 assert!((physics.density - 1.21).abs() < 0.01);
722 }
723
724 #[test]
725 fn test_load_inline_nodes() {
726 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
727 let nodes = config.load_nodes(Path::new(".")).unwrap();
728
729 assert_eq!(nodes.nrows(), 3);
730 assert_eq!(nodes.ncols(), 3);
731 assert!((nodes[[0, 0]] - 0.0).abs() < 1e-10);
732 assert!((nodes[[1, 0]] - 1.0).abs() < 1e-10);
733 }
734
735 #[test]
736 fn test_load_inline_elements() {
737 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
738 let elements = config.load_elements(Path::new(".")).unwrap();
739
740 assert_eq!(elements.len(), 1);
741 assert!(matches!(elements[0].element_type, ElementType::Tri3));
742 }
743
744 #[test]
745 fn test_serialize_json() {
746 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
747 let serialized = serialize_config(&config, ConfigFormat::Json).unwrap();
748
749 let reparsed = parse_config(&serialized, ConfigFormat::Json).unwrap();
751 assert_eq!(reparsed.description, config.description);
752 }
753
754 #[test]
755 fn test_serialize_toml() {
756 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
757 let serialized = serialize_config(&config, ConfigFormat::Toml).unwrap();
758
759 let reparsed = parse_config(&serialized, ConfigFormat::Toml).unwrap();
761 assert_eq!(reparsed.description, config.description);
762 }
763
764 #[test]
765 fn test_bem_method_conversion() {
766 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
767 assert!(matches!(config.bem_method(), BemMethod::Traditional));
768
769 let json_fmm = r#"{
770 "physics": {"frequency": 1000.0},
771 "mesh": {"nodes": [], "elements": []},
772 "bem": {"method": "slfmm"}
773 }"#;
774 let config_fmm = parse_config(json_fmm, ConfigFormat::Json).unwrap();
775 assert!(matches!(config_fmm.bem_method(), BemMethod::SingleLevelFmm));
776 }
777
778 #[test]
779 fn test_solver_method_conversion() {
780 let config = parse_config(SAMPLE_JSON, ConfigFormat::Json).unwrap();
781 assert!(matches!(config.solver_method(), SolverMethod::BiCgstab));
782 }
783
784 #[test]
785 fn test_parse_csv_nodes() {
786 let csv = "0.0 0.0 0.0\n1.0 0.0 0.0\n0.5 1.0 0.0";
787 let nodes = parse_csv_nodes(csv).unwrap();
788
789 assert_eq!(nodes.nrows(), 3);
790 assert!((nodes[[1, 0]] - 1.0).abs() < 1e-10);
791 }
792
793 #[test]
794 fn test_parse_csv_elements() {
795 let csv = "0, 1, 2\n1, 2, 3";
796 let elements = parse_csv_elements(csv).unwrap();
797
798 assert_eq!(elements.len(), 2);
799 assert_eq!(elements[0].connectivity, vec![0, 1, 2]);
800 }
801}