1#![allow(clippy::manual_strip, clippy::should_implement_trait)]
2use std::collections::HashMap;
12use std::fs;
13use std::io::{self, Write as IoWrite};
14
15use crate::Error as IoError;
16
17#[derive(Debug, Clone)]
23pub struct CgnsNode {
24 pub id: u32,
26 pub name: String,
28 pub label: String,
30 pub data_type: String,
32 pub data: Vec<f64>,
34}
35
36impl CgnsNode {
37 pub fn new(
39 id: u32,
40 name: impl Into<String>,
41 label: impl Into<String>,
42 data_type: impl Into<String>,
43 ) -> Self {
44 Self {
45 id,
46 name: name.into(),
47 label: label.into(),
48 data_type: data_type.into(),
49 data: Vec::new(),
50 }
51 }
52}
53
54#[derive(Debug, Clone, PartialEq)]
60pub enum ZoneType {
61 Structured,
63 Unstructured,
65}
66
67impl ZoneType {
68 pub fn as_str(&self) -> &'static str {
70 match self {
71 ZoneType::Structured => "Structured",
72 ZoneType::Unstructured => "Unstructured",
73 }
74 }
75
76 pub fn from_str(s: &str) -> Self {
78 match s.trim() {
79 "Unstructured" => ZoneType::Unstructured,
80 _ => ZoneType::Structured,
81 }
82 }
83}
84
85#[derive(Debug, Clone)]
91pub struct CgnsZone {
92 pub zone_name: String,
94 pub zone_type: ZoneType,
96 pub x: Vec<f64>,
98 pub y: Vec<f64>,
100 pub z: Vec<f64>,
102 pub elements: Vec<usize>,
104}
105
106impl CgnsZone {
107 pub fn new(zone_name: impl Into<String>, zone_type: ZoneType) -> Self {
109 Self {
110 zone_name: zone_name.into(),
111 zone_type,
112 x: Vec::new(),
113 y: Vec::new(),
114 z: Vec::new(),
115 elements: Vec::new(),
116 }
117 }
118
119 pub fn set_coords(&mut self, x: Vec<f64>, y: Vec<f64>, z: Vec<f64>) {
121 self.x = x;
122 self.y = y;
123 self.z = z;
124 }
125
126 pub fn n_points(&self) -> usize {
128 self.x.len()
129 }
130}
131
132#[derive(Debug, Clone)]
138pub struct CgnsBase {
139 pub base_name: String,
141 pub cell_dim: u8,
143 pub phys_dim: u8,
145 pub zones: Vec<CgnsZone>,
147}
148
149impl CgnsBase {
150 pub fn new(base_name: impl Into<String>, cell_dim: u8, phys_dim: u8) -> Self {
152 Self {
153 base_name: base_name.into(),
154 cell_dim,
155 phys_dim,
156 zones: Vec::new(),
157 }
158 }
159
160 pub fn add_zone(&mut self, zone: CgnsZone) {
162 self.zones.push(zone);
163 }
164}
165
166#[derive(Debug, Clone, PartialEq)]
172pub enum SolutionLocation {
173 Vertex,
175 CellCenter,
177}
178
179impl SolutionLocation {
180 pub fn as_str(&self) -> &'static str {
182 match self {
183 SolutionLocation::Vertex => "Vertex",
184 SolutionLocation::CellCenter => "CellCenter",
185 }
186 }
187
188 pub fn from_str(s: &str) -> Self {
190 match s.trim() {
191 "CellCenter" => SolutionLocation::CellCenter,
192 _ => SolutionLocation::Vertex,
193 }
194 }
195}
196
197#[derive(Debug, Clone)]
203pub struct FlowSolution {
204 pub solution_name: String,
206 pub location: SolutionLocation,
208 pub fields: HashMap<String, Vec<f64>>,
210}
211
212impl FlowSolution {
213 pub fn new(solution_name: impl Into<String>, location: SolutionLocation) -> Self {
215 Self {
216 solution_name: solution_name.into(),
217 location,
218 fields: HashMap::new(),
219 }
220 }
221
222 pub fn add_field(&mut self, name: impl Into<String>, values: Vec<f64>) {
224 self.fields.insert(name.into(), values);
225 }
226}
227
228#[derive(Debug, Clone)]
234pub struct CgnsFile {
235 pub bases: Vec<CgnsBase>,
237}
238
239impl CgnsFile {
240 pub fn new() -> Self {
242 Self { bases: Vec::new() }
243 }
244
245 pub fn add_base(&mut self, base: CgnsBase) {
247 self.bases.push(base);
248 }
249
250 pub fn write_text(&self, path: &str) -> Result<(), IoError> {
252 let mut f = fs::File::create(path).map_err(IoError::Io)?;
253 writeln!(f, "CGNS_TEXT_V1").map_err(IoError::Io)?;
254 writeln!(f, "N_BASES {}", self.bases.len()).map_err(IoError::Io)?;
255 for base in &self.bases {
256 writeln!(
257 f,
258 "BASE {} {} {}",
259 base.base_name, base.cell_dim, base.phys_dim
260 )
261 .map_err(IoError::Io)?;
262 writeln!(f, "N_ZONES {}", base.zones.len()).map_err(IoError::Io)?;
263 for zone in &base.zones {
264 writeln!(f, "ZONE {} {}", zone.zone_name, zone.zone_type.as_str())
265 .map_err(IoError::Io)?;
266 writeln!(f, "N_POINTS {}", zone.x.len()).map_err(IoError::Io)?;
267 let x_strs: Vec<String> = zone.x.iter().map(|v| v.to_string()).collect();
269 writeln!(f, "X {}", x_strs.join(" ")).map_err(IoError::Io)?;
270 let y_strs: Vec<String> = zone.y.iter().map(|v| v.to_string()).collect();
271 writeln!(f, "Y {}", y_strs.join(" ")).map_err(IoError::Io)?;
272 let z_strs: Vec<String> = zone.z.iter().map(|v| v.to_string()).collect();
273 writeln!(f, "Z {}", z_strs.join(" ")).map_err(IoError::Io)?;
274 let elem_strs: Vec<String> = zone.elements.iter().map(|v| v.to_string()).collect();
276 writeln!(
277 f,
278 "ELEMENTS {} {}",
279 zone.elements.len(),
280 elem_strs.join(" ")
281 )
282 .map_err(IoError::Io)?;
283 writeln!(f, "END_ZONE").map_err(IoError::Io)?;
284 }
285 writeln!(f, "END_BASE").map_err(IoError::Io)?;
286 }
287 writeln!(f, "END_FILE").map_err(IoError::Io)?;
288 Ok(())
289 }
290
291 pub fn read_text(path: &str) -> Result<Self, IoError> {
293 let text = fs::read_to_string(path).map_err(IoError::Io)?;
294 CgnsReader::parse(&text)
295 }
296}
297
298impl Default for CgnsFile {
299 fn default() -> Self {
300 Self::new()
301 }
302}
303
304#[derive(Debug)]
311pub struct CgnsWriter {
312 pub file: CgnsFile,
314}
315
316impl CgnsWriter {
317 pub fn new() -> Self {
319 Self {
320 file: CgnsFile::new(),
321 }
322 }
323
324 #[allow(clippy::too_many_arguments)]
332 pub fn write_structured_grid(
333 &mut self,
334 base_name: &str,
335 zone_name: &str,
336 x: Vec<f64>,
337 y: Vec<f64>,
338 z: Vec<f64>,
339 solution: Option<FlowSolution>,
340 path: &str,
341 ) -> Result<(), IoError> {
342 let mut base = CgnsBase::new(base_name, 3, 3);
343 let mut zone = CgnsZone::new(zone_name, ZoneType::Structured);
344 zone.set_coords(x, y, z);
345 base.add_zone(zone);
346 self.file.add_base(base);
347
348 self.file.write_text(path)?;
350
351 if let Some(sol) = solution {
353 let sol_path = format!("{path}.sol");
354 let mut sf = fs::File::create(&sol_path).map_err(IoError::Io)?;
355 writeln!(
356 sf,
357 "SOLUTION {} {}",
358 sol.solution_name,
359 sol.location.as_str()
360 )
361 .map_err(IoError::Io)?;
362 for (name, vals) in &sol.fields {
363 let strs: Vec<String> = vals.iter().map(|v| v.to_string()).collect();
364 writeln!(sf, "FIELD {} {}", name, strs.join(" ")).map_err(IoError::Io)?;
365 }
366 writeln!(sf, "END_SOLUTION").map_err(IoError::Io)?;
367 }
368 Ok(())
369 }
370}
371
372impl Default for CgnsWriter {
373 fn default() -> Self {
374 Self::new()
375 }
376}
377
378#[derive(Debug)]
384pub struct CgnsReader;
385
386impl CgnsReader {
387 pub fn parse(text: &str) -> Result<CgnsFile, IoError> {
389 let mut file = CgnsFile::new();
390 let mut lines = text.lines().peekable();
391
392 match lines.next() {
394 Some(l) if l.trim() == "CGNS_TEXT_V1" => {}
395 _ => {
396 return Err(IoError::Io(io::Error::new(
397 io::ErrorKind::InvalidData,
398 "missing CGNS_TEXT_V1 header",
399 )));
400 }
401 }
402
403 while let Some(line) = lines.next() {
404 let line = line.trim();
405 if line.starts_with("N_BASES") {
406 continue;
408 }
409 if line.starts_with("BASE ") {
410 let parts: Vec<&str> = line.splitn(4, ' ').collect();
411 let base_name = parts.get(1).copied().unwrap_or("Base").to_string();
412 let cell_dim: u8 = parts.get(2).and_then(|s| s.parse().ok()).unwrap_or(3);
413 let phys_dim: u8 = parts.get(3).and_then(|s| s.parse().ok()).unwrap_or(3);
414 let mut base = CgnsBase::new(base_name, cell_dim, phys_dim);
415
416 'base_loop: loop {
418 match lines.next() {
419 None => break 'base_loop,
420 Some(l) => {
421 let l = l.trim();
422 if l == "END_BASE" {
423 break 'base_loop;
424 }
425 if l.starts_with("N_ZONES") {
426 continue;
427 }
428 if l.starts_with("ZONE ") {
429 let zparts: Vec<&str> = l.splitn(3, ' ').collect();
430 let zone_name =
431 zparts.get(1).copied().unwrap_or("Zone").to_string();
432 let zone_type = ZoneType::from_str(
433 zparts.get(2).copied().unwrap_or("Structured"),
434 );
435 let mut zone = CgnsZone::new(zone_name, zone_type);
436
437 'zone_loop: loop {
439 match lines.next() {
440 None => break 'zone_loop,
441 Some(zl) => {
442 let zl = zl.trim();
443 if zl == "END_ZONE" {
444 break 'zone_loop;
445 }
446 if zl.starts_with("N_POINTS") {
447 continue;
448 }
449 if zl.starts_with("X ") {
450 zone.x = Self::parse_floats(&zl[2..]);
451 } else if zl.starts_with("Y ") {
452 zone.y = Self::parse_floats(&zl[2..]);
453 } else if zl.starts_with("Z ") {
454 zone.z = Self::parse_floats(&zl[2..]);
455 } else if zl.starts_with("ELEMENTS ") {
456 let ep: Vec<&str> = zl.splitn(3, ' ').collect();
458 if let Some(data_str) = ep.get(2) {
459 zone.elements = data_str
460 .split_whitespace()
461 .filter_map(|s| s.parse().ok())
462 .collect();
463 }
464 }
465 }
466 }
467 }
468 base.add_zone(zone);
469 }
470 }
471 }
472 }
473 file.add_base(base);
474 }
475 }
476 Ok(file)
477 }
478
479 fn parse_floats(s: &str) -> Vec<f64> {
481 s.split_whitespace()
482 .filter_map(|tok| tok.parse().ok())
483 .collect()
484 }
485}
486
487#[cfg(test)]
492mod tests {
493 use super::*;
494
495 fn tmp_path(name: &str) -> String {
496 format!("/tmp/cgns_test_{name}")
497 }
498
499 #[test]
502 fn test_cgns_node_new() {
503 let node = CgnsNode::new(1, "GridCoords", "GridCoordinates_t", "R8");
504 assert_eq!(node.id, 1);
505 assert_eq!(node.name, "GridCoords");
506 assert_eq!(node.label, "GridCoordinates_t");
507 assert_eq!(node.data_type, "R8");
508 assert!(node.data.is_empty());
509 }
510
511 #[test]
512 fn test_cgns_node_data_storage() {
513 let mut node = CgnsNode::new(2, "Pressure", "DataArray_t", "R8");
514 node.data = vec![1.0, 2.0, 3.0];
515 assert_eq!(node.data.len(), 3);
516 assert!((node.data[1] - 2.0).abs() < 1e-12);
517 }
518
519 #[test]
522 fn test_zone_type_as_str() {
523 assert_eq!(ZoneType::Structured.as_str(), "Structured");
524 assert_eq!(ZoneType::Unstructured.as_str(), "Unstructured");
525 }
526
527 #[test]
528 fn test_zone_type_from_str_structured() {
529 assert_eq!(ZoneType::from_str("Structured"), ZoneType::Structured);
530 assert_eq!(ZoneType::from_str("anything"), ZoneType::Structured);
531 }
532
533 #[test]
534 fn test_zone_type_from_str_unstructured() {
535 assert_eq!(ZoneType::from_str("Unstructured"), ZoneType::Unstructured);
536 }
537
538 #[test]
541 fn test_zone_creation() {
542 let zone = CgnsZone::new("Zone1", ZoneType::Structured);
543 assert_eq!(zone.zone_name, "Zone1");
544 assert_eq!(zone.zone_type, ZoneType::Structured);
545 assert!(zone.x.is_empty());
546 }
547
548 #[test]
549 fn test_zone_set_coords() {
550 let mut zone = CgnsZone::new("Z", ZoneType::Structured);
551 zone.set_coords(vec![0.0, 1.0], vec![0.0, 0.0], vec![0.0, 0.0]);
552 assert_eq!(zone.n_points(), 2);
553 assert!((zone.x[1] - 1.0).abs() < 1e-12);
554 }
555
556 #[test]
557 fn test_zone_n_points() {
558 let mut zone = CgnsZone::new("Z", ZoneType::Unstructured);
559 zone.x = vec![1.0, 2.0, 3.0];
560 assert_eq!(zone.n_points(), 3);
561 }
562
563 #[test]
564 fn test_zone_elements() {
565 let mut zone = CgnsZone::new("Z", ZoneType::Unstructured);
566 zone.elements = vec![1, 2, 3, 4];
567 assert_eq!(zone.elements.len(), 4);
568 }
569
570 #[test]
573 fn test_base_creation() {
574 let base = CgnsBase::new("Base1", 3, 3);
575 assert_eq!(base.base_name, "Base1");
576 assert_eq!(base.cell_dim, 3);
577 assert_eq!(base.phys_dim, 3);
578 assert!(base.zones.is_empty());
579 }
580
581 #[test]
582 fn test_base_add_zone() {
583 let mut base = CgnsBase::new("Base1", 3, 3);
584 base.add_zone(CgnsZone::new("Z1", ZoneType::Structured));
585 base.add_zone(CgnsZone::new("Z2", ZoneType::Unstructured));
586 assert_eq!(base.zones.len(), 2);
587 }
588
589 #[test]
592 fn test_solution_location_as_str() {
593 assert_eq!(SolutionLocation::Vertex.as_str(), "Vertex");
594 assert_eq!(SolutionLocation::CellCenter.as_str(), "CellCenter");
595 }
596
597 #[test]
598 fn test_solution_location_from_str() {
599 assert_eq!(
600 SolutionLocation::from_str("Vertex"),
601 SolutionLocation::Vertex
602 );
603 assert_eq!(
604 SolutionLocation::from_str("CellCenter"),
605 SolutionLocation::CellCenter
606 );
607 assert_eq!(
608 SolutionLocation::from_str("other"),
609 SolutionLocation::Vertex
610 );
611 }
612
613 #[test]
616 fn test_flow_solution_new() {
617 let sol = FlowSolution::new("FlowSolution", SolutionLocation::Vertex);
618 assert_eq!(sol.solution_name, "FlowSolution");
619 assert_eq!(sol.location, SolutionLocation::Vertex);
620 assert!(sol.fields.is_empty());
621 }
622
623 #[test]
624 fn test_flow_solution_add_field() {
625 let mut sol = FlowSolution::new("Sol", SolutionLocation::CellCenter);
626 sol.add_field("Pressure", vec![1.0, 2.0, 3.0]);
627 assert!(sol.fields.contains_key("Pressure"));
628 assert_eq!(sol.fields["Pressure"].len(), 3);
629 }
630
631 #[test]
632 fn test_flow_solution_multiple_fields() {
633 let mut sol = FlowSolution::new("Sol", SolutionLocation::Vertex);
634 sol.add_field("P", vec![1.0]);
635 sol.add_field("T", vec![300.0]);
636 sol.add_field("U", vec![0.5]);
637 assert_eq!(sol.fields.len(), 3);
638 }
639
640 #[test]
643 fn test_file_write_read_empty() {
644 let file = CgnsFile::new();
645 let path = tmp_path("empty");
646 file.write_text(&path).unwrap();
647 let restored = CgnsFile::read_text(&path).unwrap();
648 assert_eq!(restored.bases.len(), 0);
649 let _ = fs::remove_file(&path);
650 }
651
652 #[test]
653 fn test_file_write_read_single_base() {
654 let mut file = CgnsFile::new();
655 let base = CgnsBase::new("MainBase", 3, 3);
656 file.add_base(base);
657 let path = tmp_path("single_base");
658 file.write_text(&path).unwrap();
659 let restored = CgnsFile::read_text(&path).unwrap();
660 assert_eq!(restored.bases.len(), 1);
661 assert_eq!(restored.bases[0].base_name, "MainBase");
662 let _ = fs::remove_file(&path);
663 }
664
665 #[test]
666 fn test_file_write_read_coords_roundtrip() {
667 let mut file = CgnsFile::new();
668 let mut base = CgnsBase::new("B", 3, 3);
669 let mut zone = CgnsZone::new("Z1", ZoneType::Structured);
670 zone.set_coords(vec![0.0, 1.0, 2.0], vec![0.0, 0.5, 1.0], vec![0.0; 3]);
671 base.add_zone(zone);
672 file.add_base(base);
673 let path = tmp_path("coords_rt");
674 file.write_text(&path).unwrap();
675 let restored = CgnsFile::read_text(&path).unwrap();
676 let rz = &restored.bases[0].zones[0];
677 assert_eq!(rz.x.len(), 3);
678 assert!((rz.x[2] - 2.0).abs() < 1e-10);
679 assert!((rz.y[1] - 0.5).abs() < 1e-10);
680 let _ = fs::remove_file(&path);
681 }
682
683 #[test]
684 fn test_file_write_read_zone_type() {
685 let mut file = CgnsFile::new();
686 let mut base = CgnsBase::new("B", 3, 3);
687 let mut zone = CgnsZone::new("Unstr", ZoneType::Unstructured);
688 zone.elements = vec![1, 2, 3];
689 base.add_zone(zone);
690 file.add_base(base);
691 let path = tmp_path("zone_type");
692 file.write_text(&path).unwrap();
693 let restored = CgnsFile::read_text(&path).unwrap();
694 assert_eq!(restored.bases[0].zones[0].zone_type, ZoneType::Unstructured);
695 let _ = fs::remove_file(&path);
696 }
697
698 #[test]
699 fn test_file_write_read_elements() {
700 let mut file = CgnsFile::new();
701 let mut base = CgnsBase::new("B", 3, 3);
702 let mut zone = CgnsZone::new("Z", ZoneType::Unstructured);
703 zone.elements = vec![1, 2, 3, 4, 5, 6];
704 base.add_zone(zone);
705 file.add_base(base);
706 let path = tmp_path("elements");
707 file.write_text(&path).unwrap();
708 let restored = CgnsFile::read_text(&path).unwrap();
709 assert_eq!(restored.bases[0].zones[0].elements, vec![1, 2, 3, 4, 5, 6]);
710 let _ = fs::remove_file(&path);
711 }
712
713 #[test]
714 fn test_file_write_read_multi_zone() {
715 let mut file = CgnsFile::new();
716 let mut base = CgnsBase::new("B", 3, 3);
717 base.add_zone(CgnsZone::new("Zone1", ZoneType::Structured));
718 base.add_zone(CgnsZone::new("Zone2", ZoneType::Unstructured));
719 file.add_base(base);
720 let path = tmp_path("multi_zone");
721 file.write_text(&path).unwrap();
722 let restored = CgnsFile::read_text(&path).unwrap();
723 assert_eq!(restored.bases[0].zones.len(), 2);
724 assert_eq!(restored.bases[0].zones[1].zone_name, "Zone2");
725 let _ = fs::remove_file(&path);
726 }
727
728 #[test]
729 fn test_file_write_read_multi_base() {
730 let mut file = CgnsFile::new();
731 file.add_base(CgnsBase::new("Base1", 3, 3));
732 file.add_base(CgnsBase::new("Base2", 2, 2));
733 let path = tmp_path("multi_base");
734 file.write_text(&path).unwrap();
735 let restored = CgnsFile::read_text(&path).unwrap();
736 assert_eq!(restored.bases.len(), 2);
737 assert_eq!(restored.bases[1].base_name, "Base2");
738 assert_eq!(restored.bases[1].cell_dim, 2);
739 let _ = fs::remove_file(&path);
740 }
741
742 #[test]
743 fn test_file_invalid_header() {
744 let result = CgnsReader::parse("NOT_CGNS\n");
745 assert!(result.is_err());
746 }
747
748 #[test]
749 fn test_file_default() {
750 let f = CgnsFile::default();
751 assert!(f.bases.is_empty());
752 }
753
754 #[test]
757 fn test_writer_structured_grid_no_solution() {
758 let mut writer = CgnsWriter::new();
759 let path = tmp_path("writer_noSol");
760 writer
761 .write_structured_grid(
762 "B",
763 "Z",
764 vec![0.0, 1.0],
765 vec![0.0, 0.0],
766 vec![0.0, 0.0],
767 None,
768 &path,
769 )
770 .unwrap();
771 let content = fs::read_to_string(&path).unwrap();
772 assert!(content.contains("CGNS_TEXT_V1"));
773 assert!(content.contains("Structured"));
774 let _ = fs::remove_file(&path);
775 }
776
777 #[test]
778 fn test_writer_structured_grid_with_solution() {
779 let mut writer = CgnsWriter::new();
780 let mut sol = FlowSolution::new("Sol", SolutionLocation::Vertex);
781 sol.add_field("Pressure", vec![101325.0, 101300.0]);
782 let path = tmp_path("writer_sol");
783 writer
784 .write_structured_grid(
785 "B",
786 "Z",
787 vec![0.0, 1.0],
788 vec![0.0, 0.0],
789 vec![0.0, 0.0],
790 Some(sol),
791 &path,
792 )
793 .unwrap();
794 let sol_path = format!("{path}.sol");
795 let sol_content = fs::read_to_string(&sol_path).unwrap();
796 assert!(sol_content.contains("Pressure"));
797 let _ = fs::remove_file(&path);
798 let _ = fs::remove_file(&sol_path);
799 }
800
801 #[test]
802 fn test_writer_default() {
803 let w = CgnsWriter::default();
804 assert!(w.file.bases.is_empty());
805 }
806
807 #[test]
810 fn test_reader_parse_minimal() {
811 let text = "CGNS_TEXT_V1\nN_BASES 0\nEND_FILE\n";
812 let file = CgnsReader::parse(text).unwrap();
813 assert_eq!(file.bases.len(), 0);
814 }
815
816 #[test]
817 fn test_reader_parse_with_zone_coords() {
818 let text = concat!(
819 "CGNS_TEXT_V1\n",
820 "N_BASES 1\n",
821 "BASE MyBase 3 3\n",
822 "N_ZONES 1\n",
823 "ZONE Z1 Structured\n",
824 "N_POINTS 2\n",
825 "X 0 1\n",
826 "Y 0 0\n",
827 "Z 0 0\n",
828 "ELEMENTS 0 \n",
829 "END_ZONE\n",
830 "END_BASE\n",
831 "END_FILE\n",
832 );
833 let file = CgnsReader::parse(text).unwrap();
834 assert_eq!(file.bases[0].zones[0].x.len(), 2);
835 }
836}