1#![allow(clippy::should_implement_trait)]
2use std::collections::HashMap;
11use std::fs;
12use std::io::{BufRead, BufReader, Write as IoWrite};
13
14use crate::Error;
15use crate::Result;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub enum ExodusElementType {
22 Tri3,
24 Tri6,
26 Quad4,
28 Quad8,
30 Tet4,
32 Tet10,
34 Hex8,
36 Hex20,
38 Bar2,
40}
41
42impl ExodusElementType {
43 pub fn as_str(self) -> &'static str {
45 match self {
46 Self::Tri3 => "TRI3",
47 Self::Tri6 => "TRI6",
48 Self::Quad4 => "QUAD4",
49 Self::Quad8 => "QUAD8",
50 Self::Tet4 => "TET4",
51 Self::Tet10 => "TET10",
52 Self::Hex8 => "HEX8",
53 Self::Hex20 => "HEX20",
54 Self::Bar2 => "BAR2",
55 }
56 }
57
58 pub fn nodes_per_element(self) -> usize {
60 match self {
61 Self::Tri3 => 3,
62 Self::Tri6 => 6,
63 Self::Quad4 => 4,
64 Self::Quad8 => 8,
65 Self::Tet4 => 4,
66 Self::Tet10 => 10,
67 Self::Hex8 => 8,
68 Self::Hex20 => 20,
69 Self::Bar2 => 2,
70 }
71 }
72
73 pub fn from_str(s: &str) -> Option<Self> {
75 match s.to_ascii_uppercase().as_str() {
76 "TRI3" => Some(Self::Tri3),
77 "TRI6" => Some(Self::Tri6),
78 "QUAD4" => Some(Self::Quad4),
79 "QUAD8" => Some(Self::Quad8),
80 "TET4" => Some(Self::Tet4),
81 "TET10" => Some(Self::Tet10),
82 "HEX8" => Some(Self::Hex8),
83 "HEX20" => Some(Self::Hex20),
84 "BAR2" => Some(Self::Bar2),
85 _ => None,
86 }
87 }
88}
89
90#[derive(Debug, Clone)]
94pub struct ExodusNodeSet {
95 pub id: usize,
97 pub name: String,
99 pub node_ids: Vec<usize>,
101}
102
103impl ExodusNodeSet {
104 pub fn new(id: usize, name: impl Into<String>, node_ids: Vec<usize>) -> Self {
106 Self {
107 id,
108 name: name.into(),
109 node_ids,
110 }
111 }
112}
113
114#[derive(Debug, Clone)]
118pub struct ExodusSideSet {
119 pub id: usize,
121 pub name: String,
123 pub elem_ids: Vec<usize>,
125 pub side_ids: Vec<usize>,
127}
128
129impl ExodusSideSet {
130 pub fn new(
132 id: usize,
133 name: impl Into<String>,
134 elem_ids: Vec<usize>,
135 side_ids: Vec<usize>,
136 ) -> Self {
137 Self {
138 id,
139 name: name.into(),
140 elem_ids,
141 side_ids,
142 }
143 }
144}
145
146#[derive(Debug, Clone)]
150pub struct ExodusBlock {
151 pub id: usize,
153 pub element_type: ExodusElementType,
155 pub connectivity: Vec<Vec<usize>>,
157}
158
159impl ExodusBlock {
160 pub fn new(id: usize, element_type: ExodusElementType, connectivity: Vec<Vec<usize>>) -> Self {
162 Self {
163 id,
164 element_type,
165 connectivity,
166 }
167 }
168
169 pub fn num_elements(&self) -> usize {
171 self.connectivity.len()
172 }
173}
174
175#[derive(Debug, Clone)]
179pub struct ExodusMesh {
180 pub nodes: Vec<[f64; 3]>,
182 pub blocks: Vec<ExodusBlock>,
184 pub node_sets: Vec<ExodusNodeSet>,
186 pub side_sets: Vec<ExodusSideSet>,
188 pub variables: HashMap<String, Vec<f64>>,
190}
191
192impl ExodusMesh {
193 pub fn new() -> Self {
195 Self {
196 nodes: Vec::new(),
197 blocks: Vec::new(),
198 node_sets: Vec::new(),
199 side_sets: Vec::new(),
200 variables: HashMap::new(),
201 }
202 }
203
204 pub fn total_elements(&self) -> usize {
206 self.blocks.iter().map(|b| b.num_elements()).sum()
207 }
208}
209
210impl Default for ExodusMesh {
211 fn default() -> Self {
212 Self::new()
213 }
214}
215
216pub struct ExodusWriter;
220
221impl ExodusWriter {
222 pub fn new() -> Self {
224 Self
225 }
226
227 pub fn write_text(&self, mesh: &ExodusMesh, path: &str) -> Result<()> {
232 let mut f = fs::File::create(path).map_err(Error::Io)?;
233
234 writeln!(f, "# Exodus II ASCII (OxiPhysics)")?;
236 writeln!(f, "NUM_NODES {}", mesh.nodes.len())?;
237 writeln!(f, "NUM_BLOCKS {}", mesh.blocks.len())?;
238 writeln!(f, "NUM_NODE_SETS {}", mesh.node_sets.len())?;
239 writeln!(f, "NUM_SIDE_SETS {}", mesh.side_sets.len())?;
240 writeln!(f, "NUM_VARIABLES {}", mesh.variables.len())?;
241 writeln!(f)?;
242
243 writeln!(f, "NODES")?;
245 for (idx, n) in mesh.nodes.iter().enumerate() {
246 writeln!(f, "{} {} {} {}", idx, n[0], n[1], n[2])?;
247 }
248 writeln!(f)?;
249
250 for blk in &mesh.blocks {
252 writeln!(
253 f,
254 "BLOCK {} {} {}",
255 blk.id,
256 blk.element_type.as_str(),
257 blk.connectivity.len()
258 )?;
259 for row in &blk.connectivity {
260 let ids: Vec<String> = row.iter().map(|v| v.to_string()).collect();
261 writeln!(f, "{}", ids.join(" "))?;
262 }
263 writeln!(f)?;
264 }
265
266 for ns in &mesh.node_sets {
268 writeln!(f, "NODE_SET {} {} {}", ns.id, ns.name, ns.node_ids.len())?;
269 let ids: Vec<String> = ns.node_ids.iter().map(|v| v.to_string()).collect();
270 writeln!(f, "{}", ids.join(" "))?;
271 writeln!(f)?;
272 }
273
274 for ss in &mesh.side_sets {
276 writeln!(f, "SIDE_SET {} {} {}", ss.id, ss.name, ss.elem_ids.len())?;
277 for (e, s) in ss.elem_ids.iter().zip(ss.side_ids.iter()) {
278 writeln!(f, "{} {}", e, s)?;
279 }
280 writeln!(f)?;
281 }
282
283 let mut var_names: Vec<&String> = mesh.variables.keys().collect();
285 var_names.sort();
286 for name in var_names {
287 let vals = &mesh.variables[name];
288 writeln!(f, "VARIABLE {} {}", name, vals.len())?;
289 for v in vals {
290 writeln!(f, "{}", v)?;
291 }
292 writeln!(f)?;
293 }
294
295 Ok(())
296 }
297}
298
299impl Default for ExodusWriter {
300 fn default() -> Self {
301 Self::new()
302 }
303}
304
305pub struct ExodusReader;
309
310impl ExodusReader {
311 pub fn new() -> Self {
313 Self
314 }
315
316 pub fn parse(&self, path: &str) -> Result<ExodusMesh> {
318 let file = fs::File::open(path).map_err(Error::Io)?;
319 let reader = BufReader::new(file);
320 let mut lines: Vec<String> = Vec::new();
321 for line in reader.lines() {
322 let l = line.map_err(Error::Io)?;
323 let trimmed = l.trim().to_string();
324 if !trimmed.starts_with('#') && !trimmed.is_empty() {
325 lines.push(trimmed);
326 }
327 }
328
329 let mut mesh = ExodusMesh::new();
330 let mut pos = 0usize;
331
332 while pos < lines.len() {
334 let l = &lines[pos];
335 if l.starts_with("NUM_") {
336 pos += 1;
337 } else {
338 break;
339 }
340 }
341
342 while pos < lines.len() {
343 let l = lines[pos].clone();
344 let parts: Vec<&str> = l.split_whitespace().collect();
345 if parts.is_empty() {
346 pos += 1;
347 continue;
348 }
349 match parts[0] {
350 "NODES" => {
351 pos += 1;
352 while pos < lines.len() {
353 let row: Vec<&str> = lines[pos].split_whitespace().collect();
354 if row.len() < 4 {
355 break;
356 }
357 if row[0].parse::<usize>().is_err() {
360 break;
361 }
362 let x = row[1]
363 .parse::<f64>()
364 .map_err(|e| Error::Parse(e.to_string()))?;
365 let y = row[2]
366 .parse::<f64>()
367 .map_err(|e| Error::Parse(e.to_string()))?;
368 let z = row[3]
369 .parse::<f64>()
370 .map_err(|e| Error::Parse(e.to_string()))?;
371 mesh.nodes.push([x, y, z]);
372 pos += 1;
373 }
374 }
375 "BLOCK" => {
376 if parts.len() < 4 {
377 return Err(Error::Parse("malformed BLOCK header".into()));
378 }
379 let id: usize = parts[1]
380 .parse()
381 .map_err(|e: std::num::ParseIntError| Error::Parse(e.to_string()))?;
382 let etype = ExodusElementType::from_str(parts[2]).ok_or_else(|| {
383 Error::Parse(format!("unknown element type: {}", parts[2]))
384 })?;
385 let count: usize = parts[3]
386 .parse()
387 .map_err(|e: std::num::ParseIntError| Error::Parse(e.to_string()))?;
388 pos += 1;
389 let mut connectivity = Vec::with_capacity(count);
390 for _ in 0..count {
391 if pos >= lines.len() {
392 return Err(Error::Parse("unexpected end in BLOCK".into()));
393 }
394 let row: Vec<usize> = lines[pos]
395 .split_whitespace()
396 .map(|s| s.parse::<usize>().map_err(|e| Error::Parse(e.to_string())))
397 .collect::<Result<Vec<_>>>()?;
398 connectivity.push(row);
399 pos += 1;
400 }
401 mesh.blocks.push(ExodusBlock::new(id, etype, connectivity));
402 }
403 "NODE_SET" => {
404 if parts.len() < 4 {
405 return Err(Error::Parse("malformed NODE_SET header".into()));
406 }
407 let id: usize = parts[1]
408 .parse()
409 .map_err(|e: std::num::ParseIntError| Error::Parse(e.to_string()))?;
410 let name = parts[2].to_string();
411 let count: usize = parts[3]
412 .parse()
413 .map_err(|e: std::num::ParseIntError| Error::Parse(e.to_string()))?;
414 pos += 1;
415 let mut node_ids = Vec::with_capacity(count);
416 if pos < lines.len() && count > 0 {
417 let row: Vec<usize> = lines[pos]
418 .split_whitespace()
419 .map(|s| s.parse::<usize>().map_err(|e| Error::Parse(e.to_string())))
420 .collect::<Result<Vec<_>>>()?;
421 node_ids = row;
422 pos += 1;
423 } else if count == 0 {
424 }
426 mesh.node_sets.push(ExodusNodeSet::new(id, name, node_ids));
427 }
428 "SIDE_SET" => {
429 if parts.len() < 4 {
430 return Err(Error::Parse("malformed SIDE_SET header".into()));
431 }
432 let id: usize = parts[1]
433 .parse()
434 .map_err(|e: std::num::ParseIntError| Error::Parse(e.to_string()))?;
435 let name = parts[2].to_string();
436 let count: usize = parts[3]
437 .parse()
438 .map_err(|e: std::num::ParseIntError| Error::Parse(e.to_string()))?;
439 pos += 1;
440 let mut elem_ids = Vec::with_capacity(count);
441 let mut side_ids = Vec::with_capacity(count);
442 for _ in 0..count {
443 if pos >= lines.len() {
444 return Err(Error::Parse("unexpected end in SIDE_SET".into()));
445 }
446 let row: Vec<&str> = lines[pos].split_whitespace().collect();
447 if row.len() < 2 {
448 return Err(Error::Parse("malformed SIDE_SET entry".into()));
449 }
450 elem_ids.push(
451 row[0]
452 .parse::<usize>()
453 .map_err(|e| Error::Parse(e.to_string()))?,
454 );
455 side_ids.push(
456 row[1]
457 .parse::<usize>()
458 .map_err(|e| Error::Parse(e.to_string()))?,
459 );
460 pos += 1;
461 }
462 mesh.side_sets
463 .push(ExodusSideSet::new(id, name, elem_ids, side_ids));
464 }
465 "VARIABLE" => {
466 if parts.len() < 3 {
467 return Err(Error::Parse("malformed VARIABLE header".into()));
468 }
469 let name = parts[1].to_string();
470 let count: usize = parts[2]
471 .parse()
472 .map_err(|e: std::num::ParseIntError| Error::Parse(e.to_string()))?;
473 pos += 1;
474 let mut vals = Vec::with_capacity(count);
475 for _ in 0..count {
476 if pos >= lines.len() {
477 return Err(Error::Parse("unexpected end in VARIABLE".into()));
478 }
479 let v: f64 = lines[pos]
480 .trim()
481 .parse()
482 .map_err(|e: std::num::ParseFloatError| Error::Parse(e.to_string()))?;
483 vals.push(v);
484 pos += 1;
485 }
486 mesh.variables.insert(name, vals);
487 }
488 _ => {
489 pos += 1;
490 }
491 }
492 }
493
494 Ok(mesh)
495 }
496}
497
498impl Default for ExodusReader {
499 fn default() -> Self {
500 Self::new()
501 }
502}
503
504#[cfg(test)]
507mod tests {
508 use super::*;
509
510 fn simple_tri_mesh() -> ExodusMesh {
511 let mut m = ExodusMesh::new();
512 m.nodes = vec![
513 [0.0, 0.0, 0.0],
514 [1.0, 0.0, 0.0],
515 [0.5, 1.0, 0.0],
516 [1.5, 1.0, 0.0],
517 ];
518 m.blocks.push(ExodusBlock::new(
519 1,
520 ExodusElementType::Tri3,
521 vec![vec![0, 1, 2], vec![1, 3, 2]],
522 ));
523 m
524 }
525
526 fn tmp_path(name: &str) -> String {
527 format!("/tmp/oxiphysics_exodus_test_{name}.exo")
528 }
529
530 #[test]
533 fn element_type_as_str() {
534 assert_eq!(ExodusElementType::Tri3.as_str(), "TRI3");
535 assert_eq!(ExodusElementType::Hex20.as_str(), "HEX20");
536 assert_eq!(ExodusElementType::Bar2.as_str(), "BAR2");
537 }
538
539 #[test]
540 fn element_type_from_str_roundtrip() {
541 for et in [
542 ExodusElementType::Tri3,
543 ExodusElementType::Tri6,
544 ExodusElementType::Quad4,
545 ExodusElementType::Quad8,
546 ExodusElementType::Tet4,
547 ExodusElementType::Tet10,
548 ExodusElementType::Hex8,
549 ExodusElementType::Hex20,
550 ExodusElementType::Bar2,
551 ] {
552 let parsed = ExodusElementType::from_str(et.as_str());
553 assert_eq!(parsed, Some(et), "round-trip failed for {:?}", et);
554 }
555 }
556
557 #[test]
558 fn element_type_from_str_unknown() {
559 assert!(ExodusElementType::from_str("UNKNOWN").is_none());
560 assert!(ExodusElementType::from_str("").is_none());
561 }
562
563 #[test]
564 fn element_type_from_str_case_insensitive() {
565 assert_eq!(
566 ExodusElementType::from_str("tri3"),
567 Some(ExodusElementType::Tri3)
568 );
569 assert_eq!(
570 ExodusElementType::from_str("Hex8"),
571 Some(ExodusElementType::Hex8)
572 );
573 }
574
575 #[test]
576 fn element_type_nodes_per_element() {
577 assert_eq!(ExodusElementType::Tri3.nodes_per_element(), 3);
578 assert_eq!(ExodusElementType::Hex20.nodes_per_element(), 20);
579 assert_eq!(ExodusElementType::Bar2.nodes_per_element(), 2);
580 assert_eq!(ExodusElementType::Tet10.nodes_per_element(), 10);
581 }
582
583 #[test]
586 fn block_num_elements() {
587 let blk = ExodusBlock::new(
588 1,
589 ExodusElementType::Quad4,
590 vec![vec![0, 1, 2, 3], vec![4, 5, 6, 7]],
591 );
592 assert_eq!(blk.num_elements(), 2);
593 }
594
595 #[test]
596 fn block_empty_connectivity() {
597 let blk = ExodusBlock::new(2, ExodusElementType::Tet4, vec![]);
598 assert_eq!(blk.num_elements(), 0);
599 }
600
601 #[test]
602 fn block_stores_element_type() {
603 let blk = ExodusBlock::new(
604 3,
605 ExodusElementType::Hex8,
606 vec![vec![0, 1, 2, 3, 4, 5, 6, 7]],
607 );
608 assert_eq!(blk.element_type, ExodusElementType::Hex8);
609 }
610
611 #[test]
614 fn node_set_stores_data() {
615 let ns = ExodusNodeSet::new(10, "inlet", vec![0, 1, 2]);
616 assert_eq!(ns.id, 10);
617 assert_eq!(ns.name, "inlet");
618 assert_eq!(ns.node_ids, vec![0, 1, 2]);
619 }
620
621 #[test]
622 fn node_set_empty() {
623 let ns = ExodusNodeSet::new(1, "empty", vec![]);
624 assert!(ns.node_ids.is_empty());
625 }
626
627 #[test]
630 fn side_set_stores_data() {
631 let ss = ExodusSideSet::new(5, "wall", vec![0, 1], vec![2, 3]);
632 assert_eq!(ss.id, 5);
633 assert_eq!(ss.elem_ids, vec![0, 1]);
634 assert_eq!(ss.side_ids, vec![2, 3]);
635 }
636
637 #[test]
638 fn side_set_mismatched_lengths_allowed() {
639 let ss = ExodusSideSet::new(1, "ss", vec![0], vec![0, 1]);
641 assert_eq!(ss.elem_ids.len(), 1);
642 assert_eq!(ss.side_ids.len(), 2);
643 }
644
645 #[test]
648 fn mesh_default_is_empty() {
649 let m = ExodusMesh::default();
650 assert!(m.nodes.is_empty());
651 assert!(m.blocks.is_empty());
652 assert!(m.node_sets.is_empty());
653 assert!(m.side_sets.is_empty());
654 assert!(m.variables.is_empty());
655 }
656
657 #[test]
658 fn mesh_total_elements() {
659 let mut m = ExodusMesh::new();
660 m.blocks.push(ExodusBlock::new(
661 1,
662 ExodusElementType::Tri3,
663 vec![vec![0, 1, 2]; 3],
664 ));
665 m.blocks.push(ExodusBlock::new(
666 2,
667 ExodusElementType::Quad4,
668 vec![vec![0, 1, 2, 3]; 5],
669 ));
670 assert_eq!(m.total_elements(), 8);
671 }
672
673 #[test]
674 fn mesh_variable_storage() {
675 let mut m = ExodusMesh::new();
676 m.variables
677 .insert("temperature".into(), vec![1.0, 2.0, 3.0]);
678 assert_eq!(m.variables["temperature"], vec![1.0, 2.0, 3.0]);
679 }
680
681 #[test]
684 fn write_read_nodes_roundtrip() {
685 let m = simple_tri_mesh();
686 let path = tmp_path("nodes_rtrip");
687 ExodusWriter::new().write_text(&m, &path).unwrap();
688 let m2 = ExodusReader::new().parse(&path).unwrap();
689 assert_eq!(m2.nodes.len(), m.nodes.len());
690 for (a, b) in m.nodes.iter().zip(m2.nodes.iter()) {
691 for k in 0..3 {
692 assert!((a[k] - b[k]).abs() < 1e-12);
693 }
694 }
695 }
696
697 #[test]
698 fn write_read_blocks_roundtrip() {
699 let m = simple_tri_mesh();
700 let path = tmp_path("blocks_rtrip");
701 ExodusWriter::new().write_text(&m, &path).unwrap();
702 let m2 = ExodusReader::new().parse(&path).unwrap();
703 assert_eq!(m2.blocks.len(), 1);
704 assert_eq!(m2.blocks[0].element_type, ExodusElementType::Tri3);
705 assert_eq!(m2.blocks[0].connectivity.len(), 2);
706 assert_eq!(m2.blocks[0].connectivity[0], vec![0, 1, 2]);
707 }
708
709 #[test]
710 fn write_read_node_set_roundtrip() {
711 let mut m = simple_tri_mesh();
712 m.node_sets.push(ExodusNodeSet::new(1, "inlet", vec![0, 1]));
713 let path = tmp_path("nset_rtrip");
714 ExodusWriter::new().write_text(&m, &path).unwrap();
715 let m2 = ExodusReader::new().parse(&path).unwrap();
716 assert_eq!(m2.node_sets.len(), 1);
717 assert_eq!(m2.node_sets[0].name, "inlet");
718 assert_eq!(m2.node_sets[0].node_ids, vec![0, 1]);
719 }
720
721 #[test]
722 fn write_read_side_set_roundtrip() {
723 let mut m = simple_tri_mesh();
724 m.side_sets
725 .push(ExodusSideSet::new(1, "wall", vec![0, 1], vec![2, 3]));
726 let path = tmp_path("sset_rtrip");
727 ExodusWriter::new().write_text(&m, &path).unwrap();
728 let m2 = ExodusReader::new().parse(&path).unwrap();
729 assert_eq!(m2.side_sets.len(), 1);
730 assert_eq!(m2.side_sets[0].name, "wall");
731 assert_eq!(m2.side_sets[0].elem_ids, vec![0, 1]);
732 assert_eq!(m2.side_sets[0].side_ids, vec![2, 3]);
733 }
734
735 #[test]
736 fn write_read_variable_roundtrip() {
737 let mut m = simple_tri_mesh();
738 m.variables
739 .insert("pressure".into(), vec![1.0, 2.0, 3.0, 4.0]);
740 let path = tmp_path("var_rtrip");
741 ExodusWriter::new().write_text(&m, &path).unwrap();
742 let m2 = ExodusReader::new().parse(&path).unwrap();
743 assert!(m2.variables.contains_key("pressure"));
744 let v = &m2.variables["pressure"];
745 assert_eq!(v.len(), 4);
746 assert!((v[0] - 1.0).abs() < 1e-12);
747 assert!((v[3] - 4.0).abs() < 1e-12);
748 }
749
750 #[test]
751 fn write_read_multiple_blocks() {
752 let mut m = ExodusMesh::new();
753 m.nodes = vec![[0.0; 3]; 8];
754 m.blocks.push(ExodusBlock::new(
755 1,
756 ExodusElementType::Tri3,
757 vec![vec![0, 1, 2]],
758 ));
759 m.blocks.push(ExodusBlock::new(
760 2,
761 ExodusElementType::Quad4,
762 vec![vec![0, 1, 2, 3]],
763 ));
764 m.blocks.push(ExodusBlock::new(
765 3,
766 ExodusElementType::Bar2,
767 vec![vec![4, 5]],
768 ));
769 let path = tmp_path("multi_blk");
770 ExodusWriter::new().write_text(&m, &path).unwrap();
771 let m2 = ExodusReader::new().parse(&path).unwrap();
772 assert_eq!(m2.blocks.len(), 3);
773 assert_eq!(m2.blocks[1].element_type, ExodusElementType::Quad4);
774 assert_eq!(m2.blocks[2].element_type, ExodusElementType::Bar2);
775 }
776
777 #[test]
778 fn write_read_multiple_variables() {
779 let mut m = simple_tri_mesh();
780 let mut vars: HashMap<String, Vec<f64>> = HashMap::new();
781 vars.insert("vel_x".into(), vec![1.0, 2.0, 3.0, 4.0]);
782 vars.insert("vel_y".into(), vec![0.1, 0.2, 0.3, 0.4]);
783 m.variables = vars;
784 let path = tmp_path("multi_var");
785 ExodusWriter::new().write_text(&m, &path).unwrap();
786 let m2 = ExodusReader::new().parse(&path).unwrap();
787 assert_eq!(m2.variables.len(), 2);
788 assert!(m2.variables.contains_key("vel_x"));
789 assert!(m2.variables.contains_key("vel_y"));
790 }
791
792 #[test]
793 fn write_creates_file() {
794 let m = simple_tri_mesh();
795 let path = tmp_path("create_check");
796 ExodusWriter::new().write_text(&m, &path).unwrap();
797 assert!(std::path::Path::new(&path).exists());
798 }
799
800 #[test]
801 fn read_nonexistent_file_returns_error() {
802 let result = ExodusReader::new().parse("/tmp/nonexistent_oxiphysics_exodus.exo");
803 assert!(result.is_err());
804 }
805
806 #[test]
807 fn write_read_tet_mesh() {
808 let mut m = ExodusMesh::new();
809 m.nodes = vec![
810 [0.0, 0.0, 0.0],
811 [1.0, 0.0, 0.0],
812 [0.0, 1.0, 0.0],
813 [0.0, 0.0, 1.0],
814 ];
815 m.blocks.push(ExodusBlock::new(
816 1,
817 ExodusElementType::Tet4,
818 vec![vec![0, 1, 2, 3]],
819 ));
820 let path = tmp_path("tet_mesh");
821 ExodusWriter::new().write_text(&m, &path).unwrap();
822 let m2 = ExodusReader::new().parse(&path).unwrap();
823 assert_eq!(m2.blocks[0].element_type, ExodusElementType::Tet4);
824 assert_eq!(m2.blocks[0].connectivity[0], vec![0, 1, 2, 3]);
825 }
826
827 #[test]
828 fn write_read_hex_mesh() {
829 let mut m = ExodusMesh::new();
830 m.nodes = vec![[0.0; 3]; 8];
831 m.blocks.push(ExodusBlock::new(
832 1,
833 ExodusElementType::Hex8,
834 vec![vec![0, 1, 2, 3, 4, 5, 6, 7]],
835 ));
836 let path = tmp_path("hex_mesh");
837 ExodusWriter::new().write_text(&m, &path).unwrap();
838 let m2 = ExodusReader::new().parse(&path).unwrap();
839 assert_eq!(m2.blocks[0].element_type, ExodusElementType::Hex8);
840 assert_eq!(m2.blocks[0].connectivity[0].len(), 8);
841 }
842
843 #[test]
844 fn writer_default_works() {
845 let w = ExodusWriter;
846 let m = ExodusMesh::new();
847 let path = tmp_path("writer_default");
848 w.write_text(&m, &path).unwrap();
849 }
850
851 #[test]
852 fn reader_default_works() {
853 let _ = ExodusReader;
854 }
855
856 #[test]
857 fn mesh_clone() {
858 let m = simple_tri_mesh();
859 let m2 = m.clone();
860 assert_eq!(m2.nodes.len(), m.nodes.len());
861 assert_eq!(m2.blocks.len(), m.blocks.len());
862 }
863
864 #[test]
865 fn multiple_node_sets_roundtrip() {
866 let mut m = simple_tri_mesh();
867 m.node_sets.push(ExodusNodeSet::new(1, "inlet", vec![0]));
868 m.node_sets
869 .push(ExodusNodeSet::new(2, "outlet", vec![1, 2]));
870 let path = tmp_path("multi_nset");
871 ExodusWriter::new().write_text(&m, &path).unwrap();
872 let m2 = ExodusReader::new().parse(&path).unwrap();
873 assert_eq!(m2.node_sets.len(), 2);
874 }
875
876 #[test]
877 fn multiple_side_sets_roundtrip() {
878 let mut m = simple_tri_mesh();
879 m.side_sets
880 .push(ExodusSideSet::new(1, "top", vec![0], vec![1]));
881 m.side_sets
882 .push(ExodusSideSet::new(2, "bottom", vec![1], vec![0]));
883 let path = tmp_path("multi_sset");
884 ExodusWriter::new().write_text(&m, &path).unwrap();
885 let m2 = ExodusReader::new().parse(&path).unwrap();
886 assert_eq!(m2.side_sets.len(), 2);
887 }
888
889 #[test]
890 fn empty_mesh_roundtrip() {
891 let m = ExodusMesh::new();
892 let path = tmp_path("empty_mesh");
893 ExodusWriter::new().write_text(&m, &path).unwrap();
894 let m2 = ExodusReader::new().parse(&path).unwrap();
895 assert!(m2.nodes.is_empty());
896 assert!(m2.blocks.is_empty());
897 }
898
899 #[test]
900 fn block_id_preserved() {
901 let mut m = ExodusMesh::new();
902 m.nodes = vec![[0.0; 3]; 3];
903 m.blocks.push(ExodusBlock::new(
904 42,
905 ExodusElementType::Tri3,
906 vec![vec![0, 1, 2]],
907 ));
908 let path = tmp_path("block_id");
909 ExodusWriter::new().write_text(&m, &path).unwrap();
910 let m2 = ExodusReader::new().parse(&path).unwrap();
911 assert_eq!(m2.blocks[0].id, 42);
912 }
913}