1#![allow(clippy::manual_strip)]
2use std::fmt;
12#[cfg(test)]
13use std::io::BufReader;
14use std::io::{self, BufRead, Write};
15use std::path::Path;
16
17#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
23pub enum NodeIdMode {
24 #[default]
26 Off,
27 Given,
29 Assign,
31}
32
33impl fmt::Display for NodeIdMode {
34 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
35 match self {
36 NodeIdMode::Off => write!(f, "off"),
37 NodeIdMode::Given => write!(f, "given"),
38 NodeIdMode::Assign => write!(f, "assign"),
39 }
40 }
41}
42
43#[derive(Debug, Clone)]
52pub struct EnsightPart {
53 pub part_id: u32,
55 pub description: String,
57 pub element_type: String,
59 pub connectivity: Vec<u32>,
61}
62
63impl EnsightPart {
64 pub fn new(
66 part_id: u32,
67 description: impl Into<String>,
68 element_type: impl Into<String>,
69 ) -> Self {
70 Self {
71 part_id,
72 description: description.into(),
73 element_type: element_type.into(),
74 connectivity: Vec::new(),
75 }
76 }
77
78 pub fn nodes_per_element(&self) -> usize {
80 match self.element_type.as_str() {
81 "point" => 1,
82 "bar2" => 2,
83 "tria3" => 3,
84 "quad4" => 4,
85 "tetra4" => 4,
86 "pyramid5" => 5,
87 "penta6" => 6,
88 "hexa8" => 8,
89 _ => 0,
90 }
91 }
92
93 pub fn n_elements(&self) -> usize {
95 let npe = self.nodes_per_element();
96 if npe == 0 {
97 return 0;
98 }
99 self.connectivity.len() / npe
100 }
101}
102
103#[derive(Debug, Clone)]
109pub struct EnsightGeometry {
110 pub description1: String,
112 pub description2: String,
114 pub node_id_mode: NodeIdMode,
116 pub element_id_mode: NodeIdMode,
118 pub x: Vec<f32>,
120 pub y: Vec<f32>,
122 pub z: Vec<f32>,
124 pub parts: Vec<EnsightPart>,
126}
127
128impl EnsightGeometry {
129 pub fn new() -> Self {
131 Self {
132 description1: String::from("EnSight Gold Geometry"),
133 description2: String::from("Created by OxiPhysics"),
134 node_id_mode: NodeIdMode::Off,
135 element_id_mode: NodeIdMode::Off,
136 x: Vec::new(),
137 y: Vec::new(),
138 z: Vec::new(),
139 parts: Vec::new(),
140 }
141 }
142
143 pub fn n_nodes(&self) -> usize {
145 self.x.len()
146 }
147
148 pub fn add_node(&mut self, xi: f32, yi: f32, zi: f32) -> u32 {
150 let idx = self.x.len() as u32;
151 self.x.push(xi);
152 self.y.push(yi);
153 self.z.push(zi);
154 idx
155 }
156
157 pub fn add_part(&mut self, part: EnsightPart) {
159 self.parts.push(part);
160 }
161}
162
163impl Default for EnsightGeometry {
164 fn default() -> Self {
165 Self::new()
166 }
167}
168
169#[derive(Debug, Clone)]
175pub struct EnsightScalarVar {
176 pub name: String,
178 pub values: Vec<f32>,
180}
181
182impl EnsightScalarVar {
183 pub fn new(name: impl Into<String>, values: Vec<f32>) -> Self {
185 Self {
186 name: name.into(),
187 values,
188 }
189 }
190}
191
192#[derive(Debug, Clone)]
198pub struct EnsightVectorVar {
199 pub name: String,
201 pub vx: Vec<f32>,
203 pub vy: Vec<f32>,
205 pub vz: Vec<f32>,
207}
208
209impl EnsightVectorVar {
210 pub fn new(name: impl Into<String>, vx: Vec<f32>, vy: Vec<f32>, vz: Vec<f32>) -> Self {
212 Self {
213 name: name.into(),
214 vx,
215 vy,
216 vz,
217 }
218 }
219
220 pub fn n_nodes(&self) -> usize {
222 self.vx.len()
223 }
224}
225
226#[derive(Debug, Clone)]
232pub struct EnsightCase {
233 pub geometry_file: String,
235 pub scalar_files: Vec<(String, String)>,
237 pub vector_files: Vec<(String, String)>,
239}
240
241impl EnsightCase {
242 pub fn new(geometry_file: impl Into<String>) -> Self {
244 Self {
245 geometry_file: geometry_file.into(),
246 scalar_files: Vec::new(),
247 vector_files: Vec::new(),
248 }
249 }
250
251 pub fn add_scalar(&mut self, name: impl Into<String>, file: impl Into<String>) {
253 self.scalar_files.push((name.into(), file.into()));
254 }
255
256 pub fn add_vector(&mut self, name: impl Into<String>, file: impl Into<String>) {
258 self.vector_files.push((name.into(), file.into()));
259 }
260
261 pub fn write_to<W: Write>(&self, writer: &mut W) -> io::Result<()> {
263 writeln!(writer, "FORMAT")?;
264 writeln!(writer, "type: ensight gold")?;
265 writeln!(writer)?;
266 writeln!(writer, "GEOMETRY")?;
267 writeln!(writer, "model: {}", self.geometry_file)?;
268 if !self.scalar_files.is_empty() || !self.vector_files.is_empty() {
269 writeln!(writer)?;
270 writeln!(writer, "VARIABLE")?;
271 for (name, file) in &self.scalar_files {
272 writeln!(writer, "scalar per node: {name} {file}")?;
273 }
274 for (name, file) in &self.vector_files {
275 writeln!(writer, "vector per node: {name} {file}")?;
276 }
277 }
278 Ok(())
279 }
280}
281
282#[derive(Debug, Clone)]
288pub struct EnsightTimeSeries {
289 pub times: Vec<f64>,
291 pub geo_files: Vec<String>,
293}
294
295impl EnsightTimeSeries {
296 pub fn new() -> Self {
298 Self {
299 times: Vec::new(),
300 geo_files: Vec::new(),
301 }
302 }
303
304 pub fn push(&mut self, time: f64, geo_file: impl Into<String>) {
306 self.times.push(time);
307 self.geo_files.push(geo_file.into());
308 }
309
310 pub fn n_steps(&self) -> usize {
312 self.times.len()
313 }
314
315 pub fn write_transient_case<W: Write>(&self, writer: &mut W) -> io::Result<()> {
317 writeln!(writer, "FORMAT")?;
318 writeln!(writer, "type: ensight gold")?;
319 writeln!(writer)?;
320 writeln!(writer, "GEOMETRY")?;
321 writeln!(
322 writer,
323 "model: 1 {}",
324 if self.geo_files.is_empty() {
325 "geometry"
326 } else {
327 &self.geo_files[0]
328 }
329 )?;
330 writeln!(writer)?;
331 writeln!(writer, "TIME")?;
332 writeln!(writer, "time set: 1")?;
333 writeln!(writer, "number of steps: {}", self.times.len())?;
334 writeln!(writer, "time values:")?;
335 for &t in &self.times {
336 writeln!(writer, " {t:.6e}")?;
337 }
338 Ok(())
339 }
340}
341
342impl Default for EnsightTimeSeries {
343 fn default() -> Self {
344 Self::new()
345 }
346}
347
348pub struct EnsightWriter;
354
355impl EnsightWriter {
356 pub fn write_geo<W: Write>(writer: &mut W, geo: &EnsightGeometry) -> io::Result<()> {
358 writeln!(writer, "{}", geo.description1)?;
359 writeln!(writer, "{}", geo.description2)?;
360 writeln!(writer, "node id {}", geo.node_id_mode)?;
361 writeln!(writer, "element id {}", geo.element_id_mode)?;
362
363 for part in &geo.parts {
364 writeln!(writer, "part")?;
365 writeln!(writer, "{:10}", part.part_id)?;
366 writeln!(writer, "{}", part.description)?;
367 writeln!(writer, "coordinates")?;
368 writeln!(writer, "{:10}", geo.n_nodes())?;
369 for &v in &geo.x {
370 writeln!(writer, "{v:12.5e}")?;
371 }
372 for &v in &geo.y {
373 writeln!(writer, "{v:12.5e}")?;
374 }
375 for &v in &geo.z {
376 writeln!(writer, "{v:12.5e}")?;
377 }
378 writeln!(writer, "{}", part.element_type)?;
379 writeln!(writer, "{:10}", part.n_elements())?;
380 let npe = part.nodes_per_element();
381 for chunk in part.connectivity.chunks(npe) {
382 let s: Vec<String> = chunk.iter().map(|&c| format!("{:10}", c + 1)).collect();
383 writeln!(writer, "{}", s.join(""))?;
384 }
385 }
386 Ok(())
387 }
388
389 pub fn write_scalar<W: Write>(writer: &mut W, var: &EnsightScalarVar) -> io::Result<()> {
391 writeln!(writer, "{}", var.name)?;
392 for part_id in 1u32..=1 {
393 writeln!(writer, "part")?;
394 writeln!(writer, "{part_id:10}")?;
395 writeln!(writer, "coordinates")?;
396 for &v in &var.values {
397 writeln!(writer, "{v:12.5e}")?;
398 }
399 }
400 Ok(())
401 }
402
403 pub fn write_vector<W: Write>(writer: &mut W, var: &EnsightVectorVar) -> io::Result<()> {
405 writeln!(writer, "{}", var.name)?;
406 for part_id in 1u32..=1 {
407 writeln!(writer, "part")?;
408 writeln!(writer, "{part_id:10}")?;
409 writeln!(writer, "coordinates")?;
410 for i in 0..var.vx.len() {
411 writeln!(writer, "{:12.5e}", var.vx[i])?;
412 }
413 for i in 0..var.vy.len() {
414 writeln!(writer, "{:12.5e}", var.vy[i])?;
415 }
416 for i in 0..var.vz.len() {
417 writeln!(writer, "{:12.5e}", var.vz[i])?;
418 }
419 }
420 Ok(())
421 }
422}
423
424pub struct EnsightReader;
430
431impl EnsightReader {
432 pub fn read_case<R: BufRead>(reader: R) -> io::Result<EnsightCase> {
436 let mut geo_file = String::new();
437 let mut case = EnsightCase::new("");
438
439 for line in reader.lines() {
440 let line = line?;
441 let trimmed = line.trim();
442 if trimmed.starts_with("model:") {
443 let rest = trimmed["model:".len()..].trim();
444 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
446 geo_file = parts.last().unwrap_or(&"").to_string();
447 } else if trimmed.starts_with("scalar per node:") {
448 let rest = trimmed["scalar per node:".len()..].trim();
449 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
450 if parts.len() == 2 {
451 case.scalar_files
452 .push((parts[0].to_string(), parts[1].to_string()));
453 }
454 } else if trimmed.starts_with("vector per node:") {
455 let rest = trimmed["vector per node:".len()..].trim();
456 let parts: Vec<&str> = rest.splitn(2, ' ').collect();
457 if parts.len() == 2 {
458 case.vector_files
459 .push((parts[0].to_string(), parts[1].to_string()));
460 }
461 }
462 }
463 case.geometry_file = geo_file;
464 Ok(case)
465 }
466
467 pub fn read_geo<R: BufRead>(reader: R) -> io::Result<EnsightGeometry> {
471 let mut geo = EnsightGeometry::new();
472 let mut lines = reader.lines();
473
474 if let Some(l) = lines.next() {
476 geo.description1 = l?.trim().to_string();
477 }
478 if let Some(l) = lines.next() {
479 geo.description2 = l?.trim().to_string();
480 }
481 if let Some(l) = lines.next() {
482 let s = l?;
483 if s.contains("given") {
484 geo.node_id_mode = NodeIdMode::Given;
485 } else if s.contains("assign") {
486 geo.node_id_mode = NodeIdMode::Assign;
487 }
488 }
489 if let Some(l) = lines.next() {
490 let s = l?;
491 if s.contains("given") {
492 geo.element_id_mode = NodeIdMode::Given;
493 } else if s.contains("assign") {
494 geo.element_id_mode = NodeIdMode::Assign;
495 }
496 }
497
498 let mut line_buf: Vec<String> = lines.map(|l| l.unwrap_or_default()).collect();
499 let mut pos = 0;
500
501 while pos < line_buf.len() {
502 let line = line_buf[pos].trim().to_string();
503 pos += 1;
504
505 if line == "part" {
506 let part_id: u32 = line_buf
507 .get(pos)
508 .map(|l| l.trim().parse().unwrap_or(1))
509 .unwrap_or(1);
510 pos += 1;
511 let description = line_buf
512 .get(pos)
513 .map(|l| l.trim().to_string())
514 .unwrap_or_default();
515 pos += 1;
516 let keyword = line_buf
517 .get(pos)
518 .map(|l| l.trim().to_string())
519 .unwrap_or_default();
520 pos += 1;
521
522 if keyword == "coordinates" {
523 let n_nodes: usize = line_buf
524 .get(pos)
525 .map(|l| l.trim().parse().unwrap_or(0))
526 .unwrap_or(0);
527 pos += 1;
528 let mut xs = Vec::with_capacity(n_nodes);
529 let mut ys = Vec::with_capacity(n_nodes);
530 let mut zs = Vec::with_capacity(n_nodes);
531 for _ in 0..n_nodes {
532 let v: f32 = line_buf
533 .get(pos)
534 .map(|l| l.trim().parse().unwrap_or(0.0))
535 .unwrap_or(0.0);
536 xs.push(v);
537 pos += 1;
538 }
539 for _ in 0..n_nodes {
540 let v: f32 = line_buf
541 .get(pos)
542 .map(|l| l.trim().parse().unwrap_or(0.0))
543 .unwrap_or(0.0);
544 ys.push(v);
545 pos += 1;
546 }
547 for _ in 0..n_nodes {
548 let v: f32 = line_buf
549 .get(pos)
550 .map(|l| l.trim().parse().unwrap_or(0.0))
551 .unwrap_or(0.0);
552 zs.push(v);
553 pos += 1;
554 }
555 geo.x = xs;
556 geo.y = ys;
557 geo.z = zs;
558
559 let elem_type = line_buf
561 .get(pos)
562 .map(|l| l.trim().to_string())
563 .unwrap_or_default();
564 pos += 1;
565 let n_elements: usize = line_buf
566 .get(pos)
567 .map(|l| l.trim().parse().unwrap_or(0))
568 .unwrap_or(0);
569 pos += 1;
570
571 let mut part = EnsightPart::new(part_id, description, &elem_type);
572 let npe = part.nodes_per_element();
573 for _ in 0..n_elements {
574 let row = line_buf
575 .get(pos)
576 .map(|l| l.trim().to_string())
577 .unwrap_or_default();
578 pos += 1;
579 let tokens: Vec<u32> = row
581 .split_whitespace()
582 .filter_map(|t| t.parse::<u32>().ok())
583 .map(|v| v - 1) .collect();
585 for k in 0..npe {
586 part.connectivity.push(*tokens.get(k).unwrap_or(&0));
587 }
588 }
589 geo.parts.push(part);
590 }
591 }
592 }
593 line_buf.clear();
594 Ok(geo)
595 }
596}
597
598pub fn write_ensight_case(
608 path: &str,
609 geo: &EnsightGeometry,
610 scalars: &[EnsightScalarVar],
611 vectors: &[EnsightVectorVar],
612) -> io::Result<()> {
613 let stem = Path::new(path)
614 .file_stem()
615 .and_then(|s| s.to_str())
616 .unwrap_or("output");
617 let dir = Path::new(path).parent().unwrap_or(Path::new("."));
618
619 let geo_name = format!("{stem}.geo");
620 let case_name = format!("{stem}.case");
621
622 let geo_path = dir.join(&geo_name);
624 let mut geo_file = std::fs::File::create(&geo_path)?;
625 EnsightWriter::write_geo(&mut geo_file, geo)?;
626
627 let mut case = EnsightCase::new(&geo_name);
629 let mut scalar_bufs: Vec<Vec<u8>> = Vec::new();
630 let mut vector_bufs: Vec<Vec<u8>> = Vec::new();
631
632 for sv in scalars {
633 let fname = format!("{stem}_{}.escl", sv.name);
634 let fpath = dir.join(&fname);
635 let mut buf = Vec::new();
636 EnsightWriter::write_scalar(&mut buf, sv)?;
637 scalar_bufs.push(buf.clone());
638 std::fs::write(&fpath, &buf)?;
639 case.add_scalar(&sv.name, &fname);
640 }
641
642 for vv in vectors {
643 let fname = format!("{stem}_{}.evec", vv.name);
644 let fpath = dir.join(&fname);
645 let mut buf = Vec::new();
646 EnsightWriter::write_vector(&mut buf, vv)?;
647 vector_bufs.push(buf.clone());
648 std::fs::write(&fpath, &buf)?;
649 case.add_vector(&vv.name, &fname);
650 }
651
652 let case_path = dir.join(&case_name);
654 let mut case_file = std::fs::File::create(&case_path)?;
655 case.write_to(&mut case_file)?;
656
657 Ok(())
658}
659
660#[cfg(test)]
665mod tests {
666 use super::*;
667
668 #[test]
671 fn test_node_id_mode_display_off() {
672 assert_eq!(NodeIdMode::Off.to_string(), "off");
673 }
674
675 #[test]
676 fn test_node_id_mode_display_given() {
677 assert_eq!(NodeIdMode::Given.to_string(), "given");
678 }
679
680 #[test]
681 fn test_node_id_mode_display_assign() {
682 assert_eq!(NodeIdMode::Assign.to_string(), "assign");
683 }
684
685 #[test]
686 fn test_node_id_mode_default() {
687 assert_eq!(NodeIdMode::default(), NodeIdMode::Off);
688 }
689
690 #[test]
691 fn test_node_id_mode_equality() {
692 assert_eq!(NodeIdMode::Given, NodeIdMode::Given);
693 assert_ne!(NodeIdMode::Given, NodeIdMode::Off);
694 }
695
696 #[test]
699 fn test_part_nodes_per_element_point() {
700 let p = EnsightPart::new(1, "test", "point");
701 assert_eq!(p.nodes_per_element(), 1);
702 }
703
704 #[test]
705 fn test_part_nodes_per_element_tria3() {
706 let p = EnsightPart::new(1, "test", "tria3");
707 assert_eq!(p.nodes_per_element(), 3);
708 }
709
710 #[test]
711 fn test_part_nodes_per_element_tetra4() {
712 let p = EnsightPart::new(1, "test", "tetra4");
713 assert_eq!(p.nodes_per_element(), 4);
714 }
715
716 #[test]
717 fn test_part_nodes_per_element_hexa8() {
718 let p = EnsightPart::new(1, "test", "hexa8");
719 assert_eq!(p.nodes_per_element(), 8);
720 }
721
722 #[test]
723 fn test_part_n_elements_tria3() {
724 let mut p = EnsightPart::new(1, "mesh", "tria3");
725 p.connectivity = vec![0, 1, 2, 1, 2, 3];
726 assert_eq!(p.n_elements(), 2);
727 }
728
729 #[test]
730 fn test_part_n_elements_hexa8() {
731 let mut p = EnsightPart::new(1, "vol", "hexa8");
732 p.connectivity = vec![0; 8];
733 assert_eq!(p.n_elements(), 1);
734 }
735
736 #[test]
737 fn test_part_n_elements_empty() {
738 let p = EnsightPart::new(1, "empty", "tetra4");
739 assert_eq!(p.n_elements(), 0);
740 }
741
742 #[test]
745 fn test_geo_new_empty() {
746 let geo = EnsightGeometry::new();
747 assert_eq!(geo.n_nodes(), 0);
748 assert!(geo.parts.is_empty());
749 }
750
751 #[test]
752 fn test_geo_add_node() {
753 let mut geo = EnsightGeometry::new();
754 let idx = geo.add_node(1.0, 2.0, 3.0);
755 assert_eq!(idx, 0);
756 assert_eq!(geo.n_nodes(), 1);
757 assert!((geo.x[0] - 1.0).abs() < 1e-7);
758 assert!((geo.y[0] - 2.0).abs() < 1e-7);
759 assert!((geo.z[0] - 3.0).abs() < 1e-7);
760 }
761
762 #[test]
763 fn test_geo_add_multiple_nodes() {
764 let mut geo = EnsightGeometry::new();
765 geo.add_node(0.0, 0.0, 0.0);
766 geo.add_node(1.0, 0.0, 0.0);
767 geo.add_node(0.0, 1.0, 0.0);
768 assert_eq!(geo.n_nodes(), 3);
769 }
770
771 #[test]
772 fn test_geo_add_part() {
773 let mut geo = EnsightGeometry::new();
774 let part = EnsightPart::new(1, "surf", "tria3");
775 geo.add_part(part);
776 assert_eq!(geo.parts.len(), 1);
777 }
778
779 #[test]
780 fn test_geo_default() {
781 let geo = EnsightGeometry::default();
782 assert_eq!(geo.n_nodes(), 0);
783 }
784
785 #[test]
788 fn test_scalar_var_new() {
789 let sv = EnsightScalarVar::new("pressure", vec![1.0, 2.0, 3.0]);
790 assert_eq!(sv.name, "pressure");
791 assert_eq!(sv.values.len(), 3);
792 }
793
794 #[test]
795 fn test_scalar_var_values() {
796 let sv = EnsightScalarVar::new("temp", vec![300.0, 350.0]);
797 assert!((sv.values[0] - 300.0).abs() < 1e-6);
798 assert!((sv.values[1] - 350.0).abs() < 1e-6);
799 }
800
801 #[test]
804 fn test_vector_var_new() {
805 let vv = EnsightVectorVar::new("velocity", vec![1.0, 2.0], vec![0.0, 0.0], vec![0.0, 0.0]);
806 assert_eq!(vv.name, "velocity");
807 assert_eq!(vv.n_nodes(), 2);
808 }
809
810 #[test]
811 fn test_vector_var_n_nodes() {
812 let vv = EnsightVectorVar::new("disp", vec![0.1; 5], vec![0.2; 5], vec![0.3; 5]);
813 assert_eq!(vv.n_nodes(), 5);
814 }
815
816 #[test]
819 fn test_case_new() {
820 let c = EnsightCase::new("out.geo");
821 assert_eq!(c.geometry_file, "out.geo");
822 assert!(c.scalar_files.is_empty());
823 assert!(c.vector_files.is_empty());
824 }
825
826 #[test]
827 fn test_case_add_scalar() {
828 let mut c = EnsightCase::new("out.geo");
829 c.add_scalar("pressure", "out_pressure.escl");
830 assert_eq!(c.scalar_files.len(), 1);
831 assert_eq!(c.scalar_files[0].0, "pressure");
832 }
833
834 #[test]
835 fn test_case_add_vector() {
836 let mut c = EnsightCase::new("out.geo");
837 c.add_vector("velocity", "out_velocity.evec");
838 assert_eq!(c.vector_files.len(), 1);
839 }
840
841 #[test]
842 fn test_case_write_to_contains_format() {
843 let c = EnsightCase::new("geom.geo");
844 let mut buf = Vec::new();
845 c.write_to(&mut buf).unwrap();
846 let s = String::from_utf8(buf).unwrap();
847 assert!(s.contains("FORMAT"), "missing FORMAT section");
848 assert!(s.contains("type: ensight gold"));
849 assert!(s.contains("geom.geo"));
850 }
851
852 #[test]
853 fn test_case_write_to_with_scalar() {
854 let mut c = EnsightCase::new("g.geo");
855 c.add_scalar("p", "p.escl");
856 let mut buf = Vec::new();
857 c.write_to(&mut buf).unwrap();
858 let s = String::from_utf8(buf).unwrap();
859 assert!(s.contains("scalar per node: p p.escl"), "output: {s}");
860 }
861
862 #[test]
863 fn test_case_write_to_with_vector() {
864 let mut c = EnsightCase::new("g.geo");
865 c.add_vector("vel", "vel.evec");
866 let mut buf = Vec::new();
867 c.write_to(&mut buf).unwrap();
868 let s = String::from_utf8(buf).unwrap();
869 assert!(s.contains("vector per node: vel vel.evec"), "output: {s}");
870 }
871
872 #[test]
875 fn test_writer_geo_single_point_part() {
876 let mut geo = EnsightGeometry::new();
877 geo.add_node(0.0, 0.0, 0.0);
878 let mut part = EnsightPart::new(1, "pts", "point");
879 part.connectivity = vec![0];
880 geo.add_part(part);
881
882 let mut buf = Vec::new();
883 EnsightWriter::write_geo(&mut buf, &geo).unwrap();
884 let s = String::from_utf8(buf).unwrap();
885 assert!(s.contains("part"), "missing 'part' keyword");
886 assert!(s.contains("coordinates"));
887 assert!(s.contains("point"));
888 }
889
890 #[test]
891 fn test_writer_geo_tria3_part() {
892 let mut geo = EnsightGeometry::new();
893 geo.add_node(0.0, 0.0, 0.0);
894 geo.add_node(1.0, 0.0, 0.0);
895 geo.add_node(0.0, 1.0, 0.0);
896 let mut part = EnsightPart::new(1, "surf", "tria3");
897 part.connectivity = vec![0, 1, 2];
898 geo.add_part(part);
899
900 let mut buf = Vec::new();
901 EnsightWriter::write_geo(&mut buf, &geo).unwrap();
902 let s = String::from_utf8(buf).unwrap();
903 assert!(s.contains("tria3"));
904 }
905
906 #[test]
909 fn test_writer_scalar_output() {
910 let sv = EnsightScalarVar::new("pressure", vec![1.5, 2.5, 3.5]);
911 let mut buf = Vec::new();
912 EnsightWriter::write_scalar(&mut buf, &sv).unwrap();
913 let s = String::from_utf8(buf).unwrap();
914 assert!(s.contains("pressure"));
915 assert!(s.contains("coordinates"));
916 }
917
918 #[test]
921 fn test_writer_vector_output() {
922 let vv = EnsightVectorVar::new("vel", vec![1.0, 2.0], vec![0.0, 0.0], vec![0.0, 0.0]);
923 let mut buf = Vec::new();
924 EnsightWriter::write_vector(&mut buf, &vv).unwrap();
925 let s = String::from_utf8(buf).unwrap();
926 assert!(s.contains("vel"));
927 assert!(s.contains("coordinates"));
928 }
929
930 #[test]
933 fn test_reader_case_minimal() {
934 let input = "FORMAT\ntype: ensight gold\n\nGEOMETRY\nmodel: geom.geo\n";
935 let reader = BufReader::new(input.as_bytes());
936 let case = EnsightReader::read_case(reader).unwrap();
937 assert_eq!(case.geometry_file, "geom.geo");
938 }
939
940 #[test]
941 fn test_reader_case_with_scalar() {
942 let input = "FORMAT\ntype: ensight gold\n\nGEOMETRY\nmodel: g.geo\n\nVARIABLE\nscalar per node: pressure p.escl\n";
943 let reader = BufReader::new(input.as_bytes());
944 let case = EnsightReader::read_case(reader).unwrap();
945 assert_eq!(case.scalar_files.len(), 1);
946 assert_eq!(case.scalar_files[0].0, "pressure");
947 }
948
949 #[test]
950 fn test_reader_case_with_vector() {
951 let input = "FORMAT\ntype: ensight gold\n\nGEOMETRY\nmodel: g.geo\n\nVARIABLE\nvector per node: velocity v.evec\n";
952 let reader = BufReader::new(input.as_bytes());
953 let case = EnsightReader::read_case(reader).unwrap();
954 assert_eq!(case.vector_files.len(), 1);
955 assert_eq!(case.vector_files[0].0, "velocity");
956 }
957
958 #[test]
959 fn test_reader_case_roundtrip() {
960 let mut case_written = EnsightCase::new("out.geo");
961 case_written.add_scalar("p", "out_p.escl");
962 case_written.add_vector("v", "out_v.evec");
963 let mut buf = Vec::new();
964 case_written.write_to(&mut buf).unwrap();
965 let reader = BufReader::new(buf.as_slice());
966 let case_read = EnsightReader::read_case(reader).unwrap();
967 assert_eq!(case_read.geometry_file, "out.geo");
968 assert_eq!(case_read.scalar_files.len(), 1);
969 assert_eq!(case_read.vector_files.len(), 1);
970 }
971
972 #[test]
975 fn test_time_series_new_empty() {
976 let ts = EnsightTimeSeries::new();
977 assert_eq!(ts.n_steps(), 0);
978 }
979
980 #[test]
981 fn test_time_series_push() {
982 let mut ts = EnsightTimeSeries::new();
983 ts.push(0.0, "geo_0.geo");
984 ts.push(1.0, "geo_1.geo");
985 assert_eq!(ts.n_steps(), 2);
986 assert!((ts.times[1] - 1.0).abs() < 1e-12);
987 }
988
989 #[test]
990 fn test_time_series_write_transient() {
991 let mut ts = EnsightTimeSeries::new();
992 ts.push(0.0, "g0.geo");
993 ts.push(0.5, "g1.geo");
994 let mut buf = Vec::new();
995 ts.write_transient_case(&mut buf).unwrap();
996 let s = String::from_utf8(buf).unwrap();
997 assert!(s.contains("number of steps: 2"));
998 assert!(s.contains("TIME"));
999 }
1000
1001 #[test]
1002 fn test_time_series_default() {
1003 let ts = EnsightTimeSeries::default();
1004 assert_eq!(ts.n_steps(), 0);
1005 }
1006
1007 #[test]
1010 fn test_write_ensight_case_creates_files() {
1011 let tmp_dir = std::env::temp_dir();
1012 let base = tmp_dir.join("test_ensight_case_output");
1013 let path = base.to_str().unwrap();
1014
1015 let mut geo = EnsightGeometry::new();
1016 geo.add_node(0.0, 0.0, 0.0);
1017 geo.add_node(1.0, 0.0, 0.0);
1018 geo.add_node(0.0, 1.0, 0.0);
1019 let mut part = EnsightPart::new(1, "surf", "tria3");
1020 part.connectivity = vec![0, 1, 2];
1021 geo.add_part(part);
1022
1023 let scalars = vec![EnsightScalarVar::new("pressure", vec![1.0, 2.0, 3.0])];
1024 let vectors = vec![EnsightVectorVar::new(
1025 "velocity",
1026 vec![1.0, 0.0, 0.0],
1027 vec![0.0, 1.0, 0.0],
1028 vec![0.0, 0.0, 1.0],
1029 )];
1030
1031 write_ensight_case(path, &geo, &scalars, &vectors).unwrap();
1032
1033 assert!(tmp_dir.join("test_ensight_case_output.case").exists());
1034 assert!(tmp_dir.join("test_ensight_case_output.geo").exists());
1035 }
1036
1037 #[test]
1038 fn test_write_ensight_case_no_vars() {
1039 let tmp_dir = std::env::temp_dir();
1040 let base = tmp_dir.join("test_ensight_novar");
1041 let path = base.to_str().unwrap();
1042
1043 let mut geo = EnsightGeometry::new();
1044 geo.add_node(0.0, 0.0, 0.0);
1045 let mut part = EnsightPart::new(1, "pt", "point");
1046 part.connectivity = vec![0];
1047 geo.add_part(part);
1048
1049 write_ensight_case(path, &geo, &[], &[]).unwrap();
1050 assert!(tmp_dir.join("test_ensight_novar.case").exists());
1051 assert!(tmp_dir.join("test_ensight_novar.geo").exists());
1052 }
1053
1054 #[test]
1057 fn test_reader_geo_roundtrip_nodes() {
1058 let mut geo = EnsightGeometry::new();
1059 geo.add_node(1.0, 2.0, 3.0);
1060 geo.add_node(4.0, 5.0, 6.0);
1061 let mut part = EnsightPart::new(1, "pts", "point");
1062 part.connectivity = vec![0, 1];
1063 geo.add_part(part);
1064
1065 let mut buf = Vec::new();
1066 EnsightWriter::write_geo(&mut buf, &geo).unwrap();
1067
1068 let reader = BufReader::new(buf.as_slice());
1069 let geo2 = EnsightReader::read_geo(reader).unwrap();
1070 assert_eq!(geo2.n_nodes(), 2);
1071 assert!((geo2.x[0] - 1.0).abs() < 1e-4, "x0={}", geo2.x[0]);
1072 assert!((geo2.y[0] - 2.0).abs() < 1e-4, "y0={}", geo2.y[0]);
1073 assert!((geo2.z[0] - 3.0).abs() < 1e-4, "z0={}", geo2.z[0]);
1074 }
1075}