bem/core/io/
native.rs

1//! Native Rust JSON/TOML format for BEM solver configuration
2//!
3//! This module provides a clean, idiomatic Rust configuration format
4//! that supports both JSON and TOML serialization via serde.
5//!
6//! ## Example JSON Configuration
7//!
8//! ```json
9//! {
10//!     "physics": {
11//!         "frequency": 1000.0,
12//!         "speed_of_sound": 343.0,
13//!         "density": 1.21
14//!     },
15//!     "mesh": {
16//!         "nodes_file": "nodes.json",
17//!         "elements_file": "elements.json"
18//!     },
19//!     "solver": {
20//!         "method": "BiCGSTAB",
21//!         "tolerance": 1e-6,
22//!         "max_iterations": 1000
23//!     }
24//! }
25//! ```
26
27use 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/// Native BEM configuration
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BemConfig {
42    /// Problem description
43    #[serde(default)]
44    pub description: String,
45
46    /// Physics parameters
47    pub physics: PhysicsConfig,
48
49    /// Mesh configuration
50    pub mesh: MeshConfig,
51
52    /// Solver configuration
53    #[serde(default)]
54    pub solver: SolverConfig,
55
56    /// BEM method configuration
57    #[serde(default)]
58    pub bem: BemMethodConfig,
59
60    /// Boundary conditions
61    #[serde(default)]
62    pub boundary_conditions: Vec<BoundaryConditionConfig>,
63
64    /// Excitation sources
65    #[serde(default)]
66    pub sources: SourceConfig,
67
68    /// Output configuration
69    #[serde(default)]
70    pub output: OutputConfig,
71}
72
73/// Physics parameters
74#[derive(Debug, Clone, Serialize, Deserialize)]
75pub struct PhysicsConfig {
76    /// Frequency in Hz
77    pub frequency: f64,
78
79    /// Speed of sound in m/s
80    #[serde(default = "default_speed_of_sound")]
81    pub speed_of_sound: f64,
82
83    /// Medium density in kg/m³
84    #[serde(default = "default_density")]
85    pub density: f64,
86
87    /// Reference pressure in Pa
88    #[serde(default = "default_reference_pressure")]
89    pub reference_pressure: f64,
90
91    /// External problem (true) or internal (false)
92    #[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/// Mesh configuration
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct MeshConfig {
112    /// Path to nodes file (JSON or CSV)
113    #[serde(default)]
114    pub nodes_file: Option<PathBuf>,
115
116    /// Inline nodes data [[x, y, z], ...]
117    #[serde(default)]
118    pub nodes: Option<Vec<[f64; 3]>>,
119
120    /// Path to elements file (JSON or CSV)
121    #[serde(default)]
122    pub elements_file: Option<PathBuf>,
123
124    /// Inline elements data [[n1, n2, n3, ...], ...]
125    #[serde(default)]
126    pub elements: Option<Vec<Vec<usize>>>,
127
128    /// Symmetry planes (x, y, z)
129    #[serde(default)]
130    pub symmetry: Option<[bool; 3]>,
131
132    /// Symmetry origin
133    #[serde(default)]
134    pub symmetry_origin: Option<[f64; 3]>,
135}
136
137/// Solver configuration
138#[derive(Debug, Clone, Serialize, Deserialize, Default)]
139pub struct SolverConfig {
140    /// Iterative solver method
141    #[serde(default)]
142    pub method: SolverMethodConfig,
143
144    /// Convergence tolerance
145    #[serde(default = "default_tolerance")]
146    pub tolerance: f64,
147
148    /// Maximum iterations
149    #[serde(default = "default_max_iterations")]
150    pub max_iterations: usize,
151
152    /// Preconditioner type
153    #[serde(default)]
154    pub preconditioner: PreconditionerConfig,
155
156    /// Print progress interval (0 = no output)
157    #[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/// Iterative solver method
169#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
170#[serde(rename_all = "lowercase")]
171pub enum SolverMethodConfig {
172    /// Direct LU factorization
173    Direct,
174    /// Conjugate Gradient Squared
175    #[default]
176    Cgs,
177    /// Bi-Conjugate Gradient Stabilized
178    #[serde(alias = "bicgstab")]
179    BiCgstab,
180}
181
182/// Preconditioner type
183#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
184#[serde(rename_all = "lowercase")]
185pub enum PreconditionerConfig {
186    /// No preconditioning
187    #[default]
188    None,
189    /// Diagonal (Jacobi) preconditioner
190    Diagonal,
191    /// Row scaling preconditioner
192    RowScaling,
193    /// Block diagonal preconditioner
194    BlockDiagonal,
195}
196
197/// BEM method configuration
198#[derive(Debug, Clone, Serialize, Deserialize, Default)]
199pub struct BemMethodConfig {
200    /// BEM assembly method
201    #[serde(default)]
202    pub method: BemMethodType,
203
204    /// Use Burton-Miller formulation
205    #[serde(default)]
206    pub burton_miller: bool,
207
208    /// Burton-Miller coupling parameter
209    #[serde(default = "default_coupling")]
210    pub coupling_parameter: f64,
211
212    /// Cluster size for FMM
213    #[serde(default = "default_cluster_size")]
214    pub cluster_size: usize,
215
216    /// Expansion terms for FMM
217    #[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/// BEM assembly method type
232#[derive(Debug, Clone, Copy, Serialize, Deserialize, Default)]
233#[serde(rename_all = "lowercase")]
234pub enum BemMethodType {
235    /// Traditional BEM (dense matrices)
236    #[default]
237    Traditional,
238    /// Single-Level Fast Multipole Method
239    #[serde(alias = "slfmm")]
240    SingleLevelFmm,
241    /// Multi-Level Fast Multipole Method
242    #[serde(alias = "mlfmm")]
243    MultiLevelFmm,
244}
245
246/// Boundary condition configuration
247#[derive(Debug, Clone, Serialize, Deserialize)]
248pub struct BoundaryConditionConfig {
249    /// Element range (start, end) - inclusive
250    pub elements: (usize, usize),
251
252    /// Boundary condition type
253    #[serde(rename = "type")]
254    pub bc_type: BoundaryConditionType,
255
256    /// Value (real part)
257    pub value: f64,
258
259    /// Value (imaginary part)
260    #[serde(default)]
261    pub value_imag: f64,
262}
263
264/// Boundary condition type
265#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
266#[serde(rename_all = "lowercase")]
267pub enum BoundaryConditionType {
268    /// Velocity boundary condition
269    Velocity,
270    /// Pressure boundary condition
271    Pressure,
272    /// Admittance boundary condition
273    Admittance,
274    /// Impedance boundary condition
275    Impedance,
276    /// Transfer admittance
277    TransferAdmittance,
278}
279
280/// Source configuration
281#[derive(Debug, Clone, Serialize, Deserialize, Default)]
282pub struct SourceConfig {
283    /// Plane wave sources
284    #[serde(default)]
285    pub plane_waves: Vec<PlaneWaveConfig>,
286
287    /// Point sources
288    #[serde(default)]
289    pub point_sources: Vec<PointSourceConfig>,
290}
291
292/// Plane wave source configuration
293#[derive(Debug, Clone, Serialize, Deserialize)]
294pub struct PlaneWaveConfig {
295    /// Direction vector [x, y, z] (will be normalized)
296    pub direction: [f64; 3],
297
298    /// Amplitude (complex)
299    pub amplitude: f64,
300
301    /// Phase in radians
302    #[serde(default)]
303    pub phase: f64,
304}
305
306/// Point source configuration
307#[derive(Debug, Clone, Serialize, Deserialize)]
308pub struct PointSourceConfig {
309    /// Position [x, y, z]
310    pub position: [f64; 3],
311
312    /// Amplitude (complex)
313    pub amplitude: f64,
314
315    /// Phase in radians
316    #[serde(default)]
317    pub phase: f64,
318}
319
320/// Output configuration
321#[derive(Debug, Clone, Serialize, Deserialize, Default)]
322pub struct OutputConfig {
323    /// Output directory
324    #[serde(default)]
325    pub directory: Option<PathBuf>,
326
327    /// Output pressure at evaluation points
328    #[serde(default)]
329    pub pressure: bool,
330
331    /// Output velocity at evaluation points
332    #[serde(default)]
333    pub velocity: bool,
334
335    /// Evaluation points file
336    #[serde(default)]
337    pub evaluation_points_file: Option<PathBuf>,
338
339    /// Inline evaluation points
340    #[serde(default)]
341    pub evaluation_points: Option<Vec<[f64; 3]>>,
342}
343
344/// Configuration file format
345#[derive(Debug, Clone, Copy)]
346pub enum ConfigFormat {
347    /// JSON format
348    Json,
349    /// TOML format
350    Toml,
351}
352
353impl ConfigFormat {
354    /// Detect format from file extension
355    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
365/// Load BEM configuration from a file
366///
367/// Format is auto-detected from file extension (.json or .toml)
368pub 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
378/// Parse BEM configuration from a string
379pub 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
390/// Save BEM configuration to a file
391pub 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
401/// Serialize BEM configuration to a string
402pub 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/// Configuration error types
414#[derive(Debug, thiserror::Error)]
415pub enum ConfigError {
416    /// IO error
417    #[error("IO error: {0}")]
418    Io(#[from] std::io::Error),
419
420    /// Parse error
421    #[error("Parse error: {0}")]
422    ParseError(String),
423
424    /// Serialize error
425    #[error("Serialize error: {0}")]
426    SerializeError(String),
427
428    /// Unsupported format
429    #[error("Unsupported format: {0}")]
430    UnsupportedFormat(String),
431
432    /// Missing required field
433    #[error("Missing required field: {0}")]
434    MissingField(String),
435}
436
437impl BemConfig {
438    /// Create PhysicsParams from configuration
439    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    /// Get BEM method from configuration
449    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    /// Get solver method from configuration
458    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    /// Load nodes from configuration
467    pub fn load_nodes(&self, base_dir: &Path) -> Result<Array2<f64>, ConfigError> {
468        if let Some(ref nodes) = self.mesh.nodes {
469            // Inline nodes
470            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            // Try JSON first
485            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            // Try CSV
499            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    /// Load elements from configuration
507    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            // Try JSON first
517            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            // Try CSV
524            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
532/// Parse CSV nodes (x y z per line)
533fn 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
563/// Parse CSV elements (connectivity per line)
564fn 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
586/// Create elements from connectivity data
587fn 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        // Verify we can parse it back
750        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        // Verify we can parse it back
760        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}