1#![allow(clippy::should_implement_trait)]
2use std::fmt::Write as FmtWrite;
19use std::fs;
20use std::io::{self, BufRead};
21
22use crate::Error as IoError;
23
24#[derive(Debug, Clone, PartialEq, Eq)]
28pub enum FluentZoneType {
29 Fluid,
31 Solid,
33 Wall,
35 Inlet,
37 Outlet,
39 Symmetry,
41 Interior,
43}
44
45impl FluentZoneType {
46 pub fn as_str(&self) -> &str {
48 match self {
49 FluentZoneType::Fluid => "fluid",
50 FluentZoneType::Solid => "solid",
51 FluentZoneType::Wall => "wall",
52 FluentZoneType::Inlet => "velocity-inlet",
53 FluentZoneType::Outlet => "pressure-outlet",
54 FluentZoneType::Symmetry => "symmetry",
55 FluentZoneType::Interior => "interior",
56 }
57 }
58
59 pub fn from_str(s: &str) -> Self {
61 match s.trim() {
62 "fluid" => FluentZoneType::Fluid,
63 "solid" => FluentZoneType::Solid,
64 "wall" => FluentZoneType::Wall,
65 "velocity-inlet" | "inlet" => FluentZoneType::Inlet,
66 "pressure-outlet" | "outlet" => FluentZoneType::Outlet,
67 "symmetry" => FluentZoneType::Symmetry,
68 "interior" => FluentZoneType::Interior,
69 _ => FluentZoneType::Interior,
70 }
71 }
72}
73
74#[derive(Debug, Clone, PartialEq)]
78pub struct FluentNode {
79 pub id: usize,
81 pub coordinates: [f64; 3],
83}
84
85impl FluentNode {
86 pub fn new(id: usize, coordinates: [f64; 3]) -> Self {
88 Self { id, coordinates }
89 }
90}
91
92#[derive(Debug, Clone, PartialEq, Eq)]
96pub enum FluentFaceType {
97 Line,
99 Triangle,
101 Quad,
103}
104
105impl FluentFaceType {
106 pub fn code(&self) -> u32 {
108 match self {
109 FluentFaceType::Line => 2,
110 FluentFaceType::Triangle => 3,
111 FluentFaceType::Quad => 4,
112 }
113 }
114
115 pub fn from_code(code: u32) -> Self {
117 match code {
118 2 => FluentFaceType::Line,
119 3 => FluentFaceType::Triangle,
120 4 => FluentFaceType::Quad,
121 _ => FluentFaceType::Triangle,
122 }
123 }
124
125 pub fn node_count(&self) -> usize {
127 match self {
128 FluentFaceType::Line => 2,
129 FluentFaceType::Triangle => 3,
130 FluentFaceType::Quad => 4,
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq)]
139pub struct FluentFace {
140 pub id: usize,
142 pub face_type: FluentFaceType,
144 pub node_ids: Vec<usize>,
146 pub left_cell: usize,
148 pub right_cell: usize,
150}
151
152impl FluentFace {
153 pub fn new(
155 id: usize,
156 face_type: FluentFaceType,
157 node_ids: Vec<usize>,
158 left_cell: usize,
159 right_cell: usize,
160 ) -> Self {
161 Self {
162 id,
163 face_type,
164 node_ids,
165 left_cell,
166 right_cell,
167 }
168 }
169
170 pub fn is_boundary(&self) -> bool {
172 self.left_cell == 0 || self.right_cell == 0
173 }
174}
175
176#[derive(Debug, Clone, PartialEq, Eq)]
180pub enum FluentCellType {
181 Tri,
183 Tet,
185 Quad,
187 Hex,
189}
190
191impl FluentCellType {
192 pub fn code(&self) -> u32 {
194 match self {
195 FluentCellType::Tri => 1,
196 FluentCellType::Tet => 2,
197 FluentCellType::Quad => 3,
198 FluentCellType::Hex => 4,
199 }
200 }
201
202 pub fn from_code(code: u32) -> Self {
204 match code {
205 1 => FluentCellType::Tri,
206 2 => FluentCellType::Tet,
207 3 => FluentCellType::Quad,
208 4 => FluentCellType::Hex,
209 _ => FluentCellType::Tet,
210 }
211 }
212}
213
214#[derive(Debug, Clone, PartialEq)]
218pub struct FluentCell {
219 pub id: usize,
221 pub cell_type: FluentCellType,
223 pub zone_id: usize,
225}
226
227impl FluentCell {
228 pub fn new(id: usize, cell_type: FluentCellType, zone_id: usize) -> Self {
230 Self {
231 id,
232 cell_type,
233 zone_id,
234 }
235 }
236}
237
238#[derive(Debug, Clone, Default)]
242pub struct FluentMesh {
243 pub nodes: Vec<FluentNode>,
245 pub faces: Vec<FluentFace>,
247 pub cells: Vec<FluentCell>,
249 pub zones: Vec<(usize, FluentZoneType)>,
251}
252
253impl FluentMesh {
254 pub fn new() -> Self {
256 Self::default()
257 }
258
259 pub fn add_zone(&mut self, zone_id: usize, zone_type: FluentZoneType) {
261 self.zones.push((zone_id, zone_type));
262 }
263
264 pub fn write(&self, path: &str) -> crate::Result<()> {
266 let writer = FluentWriter::new(self);
267 writer.write_to_path(path)
268 }
269
270 pub fn read(path: &str) -> crate::Result<Self> {
272 FluentReader::read_from_path(path)
273 }
274
275 pub fn node_count(&self) -> usize {
277 self.nodes.len()
278 }
279
280 pub fn face_count(&self) -> usize {
282 self.faces.len()
283 }
284
285 pub fn cell_count(&self) -> usize {
287 self.cells.len()
288 }
289
290 pub fn zone_type(&self, zone_id: usize) -> Option<&FluentZoneType> {
292 self.zones
293 .iter()
294 .find(|(id, _)| *id == zone_id)
295 .map(|(_, t)| t)
296 }
297}
298
299pub struct FluentWriter<'a> {
303 mesh: &'a FluentMesh,
304}
305
306impl<'a> FluentWriter<'a> {
307 pub fn new(mesh: &'a FluentMesh) -> Self {
309 Self { mesh }
310 }
311
312 pub fn to_string(&self) -> crate::Result<String> {
314 let mut buf = String::new();
315 self.write_comment(&mut buf)?;
316 self.write_dimension(&mut buf)?;
317 self.write_nodes(&mut buf)?;
318 self.write_cells(&mut buf)?;
319 self.write_faces(&mut buf)?;
320 self.write_zones(&mut buf)?;
321 Ok(buf)
322 }
323
324 pub fn write_to_path(&self, path: &str) -> crate::Result<()> {
326 let content = self.to_string()?;
327 fs::write(path, content)?;
328 Ok(())
329 }
330
331 fn write_comment(&self, buf: &mut String) -> crate::Result<()> {
332 writeln!(buf, "(0 \"OxiPhysics Fluent mesh export\")")?;
333 Ok(())
334 }
335
336 fn write_dimension(&self, buf: &mut String) -> crate::Result<()> {
337 let dim = if self
339 .mesh
340 .nodes
341 .iter()
342 .any(|n| n.coordinates[2].abs() > 1e-15)
343 {
344 3
345 } else {
346 2
347 };
348 writeln!(buf, "(2 {dim})")?;
349 Ok(())
350 }
351
352 fn write_nodes(&self, buf: &mut String) -> crate::Result<()> {
353 let n = self.mesh.nodes.len();
354 if n == 0 {
355 return Ok(());
356 }
357 writeln!(buf, "(10 (0 1 {n:x} 0))")?;
359 writeln!(buf, "(10 (1 1 {n:x} 1 3)")?;
360 writeln!(buf, "(")?;
361 for node in &self.mesh.nodes {
362 let [x, y, z] = node.coordinates;
363 writeln!(buf, "{x:.10e} {y:.10e} {z:.10e}")?;
364 }
365 writeln!(buf, "))")?;
366 Ok(())
367 }
368
369 fn write_cells(&self, buf: &mut String) -> crate::Result<()> {
370 let n = self.mesh.cells.len();
371 if n == 0 {
372 return Ok(());
373 }
374 writeln!(buf, "(12 (0 1 {n:x} 0))")?;
375 let mut zones: Vec<usize> = self.mesh.cells.iter().map(|c| c.zone_id).collect();
377 zones.sort_unstable();
378 zones.dedup();
379 for zone_id in zones {
380 let zone_cells: Vec<&FluentCell> = self
381 .mesh
382 .cells
383 .iter()
384 .filter(|c| c.zone_id == zone_id)
385 .collect();
386 let first = zone_cells.first().map(|c| c.id).unwrap_or(1);
387 let last = zone_cells.last().map(|c| c.id).unwrap_or(1);
388 let cell_type = zone_cells.first().map(|c| c.cell_type.code()).unwrap_or(1);
389 writeln!(buf, "(12 ({zone_id:x} {first:x} {last:x} 1 {cell_type}))")?;
390 }
391 Ok(())
392 }
393
394 fn write_faces(&self, buf: &mut String) -> crate::Result<()> {
395 let n = self.mesh.faces.len();
396 if n == 0 {
397 return Ok(());
398 }
399 writeln!(buf, "(13 (0 1 {n:x} 0))")?;
400 writeln!(buf, "(13 (1 1 {n:x} 2 0)")?;
401 writeln!(buf, "(")?;
402 for face in &self.mesh.faces {
403 write!(buf, "{:x}", face.face_type.code())?;
405 for &nid in &face.node_ids {
406 write!(buf, " {nid:x}")?;
407 }
408 writeln!(buf, " {:x} {:x}", face.left_cell, face.right_cell)?;
409 }
410 writeln!(buf, "))")?;
411 Ok(())
412 }
413
414 fn write_zones(&self, buf: &mut String) -> crate::Result<()> {
415 for (zone_id, zone_type) in &self.mesh.zones {
416 writeln!(
417 buf,
418 "(45 ({zone_id} {} zone-{zone_id} ()))",
419 zone_type.as_str()
420 )?;
421 }
422 Ok(())
423 }
424}
425
426pub struct FluentReader;
430
431impl FluentReader {
432 pub fn read_from_path(path: &str) -> crate::Result<FluentMesh> {
434 let file = fs::File::open(path)?;
435 let reader = io::BufReader::new(file);
436 Self::parse(reader)
437 }
438
439 pub fn parse<R: BufRead>(reader: R) -> crate::Result<FluentMesh> {
441 let mut mesh = FluentMesh::new();
442 let lines: Vec<String> = reader
443 .lines()
444 .collect::<Result<_, _>>()
445 .map_err(IoError::Io)?;
446
447 let mut i = 0;
448 while i < lines.len() {
449 let line = lines[i].trim();
450
451 if line.starts_with("(10") {
452 i = Self::parse_nodes(&lines, i, &mut mesh)?;
454 } else if line.starts_with("(12") {
455 i = Self::parse_cells(&lines, i, &mut mesh)?;
457 } else if line.starts_with("(13") {
458 i = Self::parse_faces(&lines, i, &mut mesh)?;
460 } else if line.starts_with("(45") {
461 Self::parse_zone(line, &mut mesh);
463 i += 1;
464 } else {
465 i += 1;
466 }
467 }
468 Ok(mesh)
469 }
470
471 fn parse_nodes(lines: &[String], start: usize, mesh: &mut FluentMesh) -> crate::Result<usize> {
472 let header = lines[start].trim();
473 if header.contains("(0 ") || !header.contains('(') {
475 return Ok(start + 1);
476 }
477 let mut i = start + 1;
480 while i < lines.len() && !lines[i].trim().starts_with('(') {
482 i += 1;
483 }
484 if i >= lines.len() {
485 return Ok(i);
486 }
487 i += 1; let mut node_id = 1usize;
489 while i < lines.len() {
490 let line = lines[i].trim();
491 if line.starts_with(')') {
492 i += 1;
493 break;
494 }
495 let parts: Vec<&str> = line.split_whitespace().collect();
496 if parts.len() >= 3 {
497 let x = parts[0].parse::<f64>().unwrap_or(0.0);
498 let y = parts[1].parse::<f64>().unwrap_or(0.0);
499 let z = parts[2].parse::<f64>().unwrap_or(0.0);
500 mesh.nodes.push(FluentNode::new(node_id, [x, y, z]));
501 node_id += 1;
502 }
503 i += 1;
504 }
505 Ok(i)
506 }
507
508 fn parse_cells(lines: &[String], start: usize, mesh: &mut FluentMesh) -> crate::Result<usize> {
509 let header = lines[start].trim();
510 if !header.contains("(0 ") {
513 if let Some(inner) = Self::extract_inner(header, "12") {
515 let parts: Vec<&str> = inner.split_whitespace().collect();
516 if parts.len() >= 5
517 && let (Ok(zone_id), Ok(first), Ok(last), Ok(cell_type)) = (
518 usize::from_str_radix(parts[0], 16),
519 usize::from_str_radix(parts[1], 16),
520 usize::from_str_radix(parts[2], 16),
521 u32::from_str_radix(parts[4], 16),
522 )
523 {
524 for id in first..=last {
525 mesh.cells.push(FluentCell::new(
526 id,
527 FluentCellType::from_code(cell_type),
528 zone_id,
529 ));
530 }
531 }
532 }
533 }
534 Ok(start + 1)
535 }
536
537 fn parse_faces(lines: &[String], start: usize, mesh: &mut FluentMesh) -> crate::Result<usize> {
538 let header = lines[start].trim();
539 if header.contains("(0 ") {
540 return Ok(start + 1);
541 }
542 let mut i = start + 1;
544 while i < lines.len() && !lines[i].trim().starts_with('(') {
545 i += 1;
546 }
547 if i >= lines.len() {
548 return Ok(i);
549 }
550 i += 1; let mut face_id = mesh.faces.len() + 1;
552 while i < lines.len() {
553 let line = lines[i].trim();
554 if line.starts_with(')') {
555 i += 1;
556 break;
557 }
558 let parts: Vec<&str> = line.split_whitespace().collect();
559 if parts.len() >= 4
560 && let Ok(ft_code) = u32::from_str_radix(parts[0], 16)
561 {
562 let face_type = FluentFaceType::from_code(ft_code);
563 let nn = face_type.node_count();
564 if parts.len() >= 1 + nn + 2 {
565 let node_ids: Vec<usize> = (1..=nn)
566 .filter_map(|k| usize::from_str_radix(parts[k], 16).ok())
567 .collect();
568 let lc = usize::from_str_radix(parts[1 + nn], 16).unwrap_or(0);
569 let rc = usize::from_str_radix(parts[2 + nn], 16).unwrap_or(0);
570 mesh.faces
571 .push(FluentFace::new(face_id, face_type, node_ids, lc, rc));
572 face_id += 1;
573 }
574 }
575 i += 1;
576 }
577 Ok(i)
578 }
579
580 fn parse_zone(line: &str, mesh: &mut FluentMesh) {
581 if let Some(inner) = Self::extract_inner(line, "45") {
583 let parts: Vec<&str> = inner.splitn(3, ' ').collect();
584 if parts.len() >= 2
585 && let Ok(zone_id) = parts[0].parse::<usize>()
586 {
587 let zone_type = FluentZoneType::from_str(parts[1]);
588 mesh.add_zone(zone_id, zone_type);
589 }
590 }
591 }
592
593 fn extract_inner<'a>(line: &'a str, section: &str) -> Option<&'a str> {
595 let prefix = format!("({section} (");
596 if let Some(pos) = line.find(&prefix) {
597 let rest = &line[pos + prefix.len()..];
598 let end = rest.rfind(')')?;
600 let end2 = rest[..end].rfind(')')?;
601 Some(rest[..end2].trim())
602 } else {
603 None
604 }
605 }
606}
607
608impl From<std::fmt::Error> for IoError {
611 fn from(e: std::fmt::Error) -> Self {
612 IoError::General(e.to_string())
613 }
614}
615
616#[cfg(test)]
619mod tests {
620 use super::*;
621
622 #[test]
625 fn test_zone_type_as_str_fluid() {
626 assert_eq!(FluentZoneType::Fluid.as_str(), "fluid");
627 }
628
629 #[test]
630 fn test_zone_type_as_str_wall() {
631 assert_eq!(FluentZoneType::Wall.as_str(), "wall");
632 }
633
634 #[test]
635 fn test_zone_type_roundtrip_fluid() {
636 let zt = FluentZoneType::Fluid;
637 assert_eq!(FluentZoneType::from_str(zt.as_str()), zt);
638 }
639
640 #[test]
641 fn test_zone_type_roundtrip_all() {
642 let types = [
643 FluentZoneType::Fluid,
644 FluentZoneType::Solid,
645 FluentZoneType::Wall,
646 FluentZoneType::Symmetry,
647 FluentZoneType::Interior,
648 ];
649 for zt in &types {
650 assert_eq!(&FluentZoneType::from_str(zt.as_str()), zt);
651 }
652 }
653
654 #[test]
655 fn test_zone_type_unknown_defaults_to_interior() {
656 assert_eq!(
657 FluentZoneType::from_str("unknown-zone"),
658 FluentZoneType::Interior
659 );
660 }
661
662 #[test]
665 fn test_fluent_node_creation() {
666 let node = FluentNode::new(1, [1.0, 2.0, 3.0]);
667 assert_eq!(node.id, 1);
668 assert_eq!(node.coordinates, [1.0, 2.0, 3.0]);
669 }
670
671 #[test]
672 fn test_fluent_node_zero_coords() {
673 let node = FluentNode::new(5, [0.0; 3]);
674 assert_eq!(node.coordinates, [0.0; 3]);
675 }
676
677 #[test]
680 fn test_face_type_code_roundtrip() {
681 let types = [
682 FluentFaceType::Line,
683 FluentFaceType::Triangle,
684 FluentFaceType::Quad,
685 ];
686 for ft in &types {
687 assert_eq!(&FluentFaceType::from_code(ft.code()), ft);
688 }
689 }
690
691 #[test]
692 fn test_face_type_node_count_line() {
693 assert_eq!(FluentFaceType::Line.node_count(), 2);
694 }
695
696 #[test]
697 fn test_face_type_node_count_tri() {
698 assert_eq!(FluentFaceType::Triangle.node_count(), 3);
699 }
700
701 #[test]
702 fn test_face_type_node_count_quad() {
703 assert_eq!(FluentFaceType::Quad.node_count(), 4);
704 }
705
706 #[test]
709 fn test_fluent_face_boundary_detection() {
710 let face = FluentFace::new(1, FluentFaceType::Triangle, vec![1, 2, 3], 0, 5);
711 assert!(face.is_boundary());
712 }
713
714 #[test]
715 fn test_fluent_face_interior() {
716 let face = FluentFace::new(2, FluentFaceType::Triangle, vec![1, 2, 3], 4, 5);
717 assert!(!face.is_boundary());
718 }
719
720 #[test]
721 fn test_fluent_face_node_ids_preserved() {
722 let ids = vec![10, 20, 30];
723 let face = FluentFace::new(1, FluentFaceType::Triangle, ids.clone(), 1, 2);
724 assert_eq!(face.node_ids, ids);
725 }
726
727 #[test]
730 fn test_cell_type_code_roundtrip() {
731 let types = [
732 FluentCellType::Tri,
733 FluentCellType::Tet,
734 FluentCellType::Quad,
735 FluentCellType::Hex,
736 ];
737 for ct in &types {
738 assert_eq!(&FluentCellType::from_code(ct.code()), ct);
739 }
740 }
741
742 #[test]
745 fn test_fluent_cell_creation() {
746 let cell = FluentCell::new(1, FluentCellType::Tet, 2);
747 assert_eq!(cell.id, 1);
748 assert_eq!(cell.cell_type, FluentCellType::Tet);
749 assert_eq!(cell.zone_id, 2);
750 }
751
752 #[test]
755 fn test_fluent_mesh_empty() {
756 let mesh = FluentMesh::new();
757 assert_eq!(mesh.node_count(), 0);
758 assert_eq!(mesh.face_count(), 0);
759 assert_eq!(mesh.cell_count(), 0);
760 }
761
762 #[test]
763 fn test_fluent_mesh_add_zone() {
764 let mut mesh = FluentMesh::new();
765 mesh.add_zone(1, FluentZoneType::Fluid);
766 assert_eq!(mesh.zones.len(), 1);
767 assert_eq!(mesh.zone_type(1), Some(&FluentZoneType::Fluid));
768 }
769
770 #[test]
771 fn test_fluent_mesh_zone_type_not_found() {
772 let mesh = FluentMesh::new();
773 assert_eq!(mesh.zone_type(99), None);
774 }
775
776 #[test]
777 fn test_fluent_mesh_counts() {
778 let mut mesh = FluentMesh::new();
779 mesh.nodes.push(FluentNode::new(1, [0.0; 3]));
780 mesh.cells.push(FluentCell::new(1, FluentCellType::Tet, 1));
781 mesh.faces.push(FluentFace::new(
782 1,
783 FluentFaceType::Triangle,
784 vec![1, 2, 3],
785 0,
786 1,
787 ));
788 assert_eq!(mesh.node_count(), 1);
789 assert_eq!(mesh.cell_count(), 1);
790 assert_eq!(mesh.face_count(), 1);
791 }
792
793 fn make_simple_mesh() -> FluentMesh {
796 let mut mesh = FluentMesh::new();
797 mesh.nodes.push(FluentNode::new(1, [0.0, 0.0, 0.0]));
798 mesh.nodes.push(FluentNode::new(2, [1.0, 0.0, 0.0]));
799 mesh.nodes.push(FluentNode::new(3, [0.5, 1.0, 0.0]));
800 mesh.cells.push(FluentCell::new(1, FluentCellType::Tri, 1));
801 mesh.faces
802 .push(FluentFace::new(1, FluentFaceType::Line, vec![1, 2], 0, 1));
803 mesh.faces
804 .push(FluentFace::new(2, FluentFaceType::Line, vec![2, 3], 0, 1));
805 mesh.faces
806 .push(FluentFace::new(3, FluentFaceType::Line, vec![3, 1], 0, 1));
807 mesh.add_zone(1, FluentZoneType::Fluid);
808 mesh
809 }
810
811 #[test]
812 fn test_writer_produces_comment_section() {
813 let mesh = make_simple_mesh();
814 let writer = FluentWriter::new(&mesh);
815 let out = writer.to_string().unwrap();
816 assert!(out.contains("(0 "));
817 }
818
819 #[test]
820 fn test_writer_produces_node_section() {
821 let mesh = make_simple_mesh();
822 let writer = FluentWriter::new(&mesh);
823 let out = writer.to_string().unwrap();
824 assert!(out.contains("(10 "));
825 }
826
827 #[test]
828 fn test_writer_produces_cell_section() {
829 let mesh = make_simple_mesh();
830 let writer = FluentWriter::new(&mesh);
831 let out = writer.to_string().unwrap();
832 assert!(out.contains("(12 "));
833 }
834
835 #[test]
836 fn test_writer_produces_face_section() {
837 let mesh = make_simple_mesh();
838 let writer = FluentWriter::new(&mesh);
839 let out = writer.to_string().unwrap();
840 assert!(out.contains("(13 "));
841 }
842
843 #[test]
844 fn test_writer_produces_zone_section() {
845 let mesh = make_simple_mesh();
846 let writer = FluentWriter::new(&mesh);
847 let out = writer.to_string().unwrap();
848 assert!(out.contains("(45 "));
849 }
850
851 #[test]
852 fn test_writer_node_count_hex() {
853 let mesh = make_simple_mesh();
855 let writer = FluentWriter::new(&mesh);
856 let out = writer.to_string().unwrap();
857 assert!(out.contains("(10 (0 1 3 0))"));
859 }
860
861 #[test]
864 fn test_write_read_roundtrip_node_count() {
865 let mesh = make_simple_mesh();
866 let path = "/tmp/oxiphysics_fluent_test_roundtrip.msh";
867 mesh.write(path).expect("write failed");
868 let loaded = FluentMesh::read(path).expect("read failed");
869 assert_eq!(loaded.node_count(), mesh.node_count());
870 }
871
872 #[test]
873 fn test_write_read_roundtrip_node_coords() {
874 let mesh = make_simple_mesh();
875 let path = "/tmp/oxiphysics_fluent_test_coords.msh";
876 mesh.write(path).unwrap();
877 let loaded = FluentMesh::read(path).unwrap();
878 for (orig, loaded_node) in mesh.nodes.iter().zip(loaded.nodes.iter()) {
879 for k in 0..3 {
880 let diff = (orig.coordinates[k] - loaded_node.coordinates[k]).abs();
881 assert!(diff < 1e-6, "coord diff={diff}");
882 }
883 }
884 }
885
886 #[test]
887 fn test_write_read_roundtrip_cell_count() {
888 let mesh = make_simple_mesh();
889 let path = "/tmp/oxiphysics_fluent_test_cells.msh";
890 mesh.write(path).unwrap();
891 let loaded = FluentMesh::read(path).unwrap();
892 assert_eq!(loaded.cell_count(), mesh.cell_count());
893 }
894
895 #[test]
896 fn test_write_read_roundtrip_zone() {
897 let mesh = make_simple_mesh();
898 let path = "/tmp/oxiphysics_fluent_test_zones.msh";
899 mesh.write(path).unwrap();
900 let loaded = FluentMesh::read(path).unwrap();
901 assert!(!loaded.zones.is_empty());
902 assert_eq!(loaded.zones[0].1, FluentZoneType::Fluid);
903 }
904
905 #[test]
906 fn test_write_creates_file() {
907 let mesh = make_simple_mesh();
908 let path = "/tmp/oxiphysics_fluent_write_check.msh";
909 mesh.write(path).unwrap();
910 assert!(std::path::Path::new(path).exists());
911 }
912
913 #[test]
914 fn test_read_nonexistent_file_errors() {
915 let result = FluentMesh::read("/tmp/oxiphysics_does_not_exist.msh");
916 assert!(result.is_err());
917 }
918
919 #[test]
922 fn test_3d_mesh_dimension() {
923 let mut mesh = FluentMesh::new();
924 mesh.nodes.push(FluentNode::new(1, [0.0, 0.0, 1.0]));
925 let writer = FluentWriter::new(&mesh);
926 let out = writer.to_string().unwrap();
927 assert!(out.contains("(2 3)"));
928 }
929
930 #[test]
931 fn test_2d_mesh_dimension() {
932 let mut mesh = FluentMesh::new();
933 mesh.nodes.push(FluentNode::new(1, [1.0, 2.0, 0.0]));
934 let writer = FluentWriter::new(&mesh);
935 let out = writer.to_string().unwrap();
936 assert!(out.contains("(2 2)"));
937 }
938
939 #[test]
940 fn test_multiple_zones() {
941 let mut mesh = FluentMesh::new();
942 mesh.add_zone(1, FluentZoneType::Fluid);
943 mesh.add_zone(2, FluentZoneType::Wall);
944 assert_eq!(mesh.zones.len(), 2);
945 assert_eq!(mesh.zone_type(2), Some(&FluentZoneType::Wall));
946 }
947
948 #[test]
949 fn test_hex_cell_type_preserved() {
950 let cell = FluentCell::new(1, FluentCellType::Hex, 1);
951 assert_eq!(cell.cell_type, FluentCellType::Hex);
952 assert_eq!(cell.cell_type.code(), 4);
953 }
954
955 #[test]
956 fn test_quad_face_node_ids() {
957 let face = FluentFace::new(1, FluentFaceType::Quad, vec![1, 2, 3, 4], 1, 2);
958 assert_eq!(face.node_ids.len(), 4);
959 assert!(!face.is_boundary());
960 }
961}