Skip to main content

oxiphysics_io/vtk_writer/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#[allow(unused_imports)]
6use super::functions::*;
7use std::io::Write;
8/// Writer for the VTK XML Unstructured Grid format (`.vtu`).
9pub struct XmlVtuWriter;
10impl XmlVtuWriter {
11    /// Serialise a complete unstructured grid to a VTU XML string.
12    pub fn write(grid: &VtkUnstructuredGrid) -> String {
13        let mut s = String::new();
14        s.push_str(&Self::write_xml_header());
15        s.push_str(&format!(
16            "  <UnstructuredGrid>\n    <Piece NumberOfPoints=\"{}\" NumberOfCells=\"{}\">\n",
17            grid.n_points(),
18            grid.n_cells()
19        ));
20        s.push_str("      <Points>\n");
21        s.push_str(&Self::write_points_xml(&grid.points));
22        s.push_str("      </Points>\n");
23        s.push_str("      <Cells>\n");
24        s.push_str(&Self::write_cells_xml(&grid.cells, &grid.cell_types));
25        s.push_str("      </Cells>\n");
26        if !grid.point_data.is_empty() {
27            s.push_str("      <PointData>\n");
28            for arr in &grid.point_data {
29                match arr {
30                    VtkDataArrayW::Scalars { name, values } => {
31                        let flat: Vec<f64> = values.clone();
32                        s.push_str(&Self::write_data_array_xml(name, 1, &flat));
33                    }
34                    VtkDataArrayW::Vectors { name, values } => {
35                        let flat: Vec<f64> =
36                            values.iter().flat_map(|v| v.iter().copied()).collect();
37                        s.push_str(&Self::write_data_array_xml(name, 3, &flat));
38                    }
39                    VtkDataArrayW::Tensors { name, values } => {
40                        let flat: Vec<f64> = values
41                            .iter()
42                            .flat_map(|t| t.iter().flat_map(|r| r.iter().copied()))
43                            .collect();
44                        s.push_str(&Self::write_data_array_xml(name, 9, &flat));
45                    }
46                }
47            }
48            s.push_str("      </PointData>\n");
49        }
50        if !grid.cell_data.is_empty() {
51            s.push_str("      <CellData>\n");
52            for arr in &grid.cell_data {
53                match arr {
54                    VtkDataArrayW::Scalars { name, values } => {
55                        let flat: Vec<f64> = values.clone();
56                        s.push_str(&Self::write_data_array_xml(name, 1, &flat));
57                    }
58                    VtkDataArrayW::Vectors { name, values } => {
59                        let flat: Vec<f64> =
60                            values.iter().flat_map(|v| v.iter().copied()).collect();
61                        s.push_str(&Self::write_data_array_xml(name, 3, &flat));
62                    }
63                    VtkDataArrayW::Tensors { name, values } => {
64                        let flat: Vec<f64> = values
65                            .iter()
66                            .flat_map(|t| t.iter().flat_map(|r| r.iter().copied()))
67                            .collect();
68                        s.push_str(&Self::write_data_array_xml(name, 9, &flat));
69                    }
70                }
71            }
72            s.push_str("      </CellData>\n");
73        }
74        s.push_str("    </Piece>\n  </UnstructuredGrid>\n</VTKFile>\n");
75        s
76    }
77    /// Generate the VTU XML file header.
78    pub fn write_xml_header() -> String {
79        "<?xml version=\"1.0\"?>\n<VTKFile type=\"UnstructuredGrid\" version=\"0.1\" byte_order=\"LittleEndian\">\n"
80            .to_owned()
81    }
82    /// Serialise the Points DataArray element.
83    pub fn write_points_xml(points: &[[f64; 3]]) -> String {
84        let mut s = String::from(
85            "        <DataArray type=\"Float64\" NumberOfComponents=\"3\" format=\"ascii\">\n",
86        );
87        for p in points {
88            s.push_str(&format!("          {} {} {}\n", p[0], p[1], p[2]));
89        }
90        s.push_str("        </DataArray>\n");
91        s
92    }
93    /// Serialise the Cells section (connectivity, offsets, types).
94    pub fn write_cells_xml(cells: &[Vec<usize>], cell_types: &[VtkCellTypeW]) -> String {
95        let mut s = String::new();
96        s.push_str(
97            "        <DataArray type=\"Int64\" Name=\"connectivity\" format=\"ascii\">\n          ",
98        );
99        let mut first = true;
100        for conn in cells {
101            for &idx in conn {
102                if !first {
103                    s.push(' ');
104                }
105                s.push_str(&idx.to_string());
106                first = false;
107            }
108        }
109        s.push_str("\n        </DataArray>\n");
110        s.push_str(
111            "        <DataArray type=\"Int64\" Name=\"offsets\" format=\"ascii\">\n          ",
112        );
113        let mut offset: usize = 0;
114        for (i, conn) in cells.iter().enumerate() {
115            if i > 0 {
116                s.push(' ');
117            }
118            offset += conn.len();
119            s.push_str(&offset.to_string());
120        }
121        s.push_str("\n        </DataArray>\n");
122        s.push_str(
123            "        <DataArray type=\"UInt8\" Name=\"types\" format=\"ascii\">\n          ",
124        );
125        for (i, ct) in cell_types.iter().enumerate() {
126            if i > 0 {
127                s.push(' ');
128            }
129            s.push_str(&(*ct as u8).to_string());
130        }
131        s.push_str("\n        </DataArray>\n");
132        s
133    }
134    /// Serialise a generic DataArray XML element.
135    pub fn write_data_array_xml(name: &str, n_components: usize, values: &[f64]) -> String {
136        let mut s = format!(
137            "        <DataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"{}\" format=\"ascii\">\n          ",
138            name, n_components
139        );
140        for (i, v) in values.iter().enumerate() {
141            if i > 0 {
142                s.push(' ');
143            }
144            s.push_str(&v.to_string());
145        }
146        s.push_str("\n        </DataArray>\n");
147        s
148    }
149    /// Encode a byte slice as a Base64 string (no line wrapping).
150    pub fn encode_base64(data: &[u8]) -> String {
151        const TABLE: &[u8; 64] =
152            b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
153        let mut out = String::with_capacity(data.len().div_ceil(3) * 4);
154        for chunk in data.chunks(3) {
155            let b0 = chunk[0] as u32;
156            let b1 = if chunk.len() > 1 { chunk[1] as u32 } else { 0 };
157            let b2 = if chunk.len() > 2 { chunk[2] as u32 } else { 0 };
158            let triple = (b0 << 16) | (b1 << 8) | b2;
159            out.push(TABLE[((triple >> 18) & 0x3F) as usize] as char);
160            out.push(TABLE[((triple >> 12) & 0x3F) as usize] as char);
161            if chunk.len() > 1 {
162                out.push(TABLE[((triple >> 6) & 0x3F) as usize] as char);
163            } else {
164                out.push('=');
165            }
166            if chunk.len() > 2 {
167                out.push(TABLE[(triple & 0x3F) as usize] as char);
168            } else {
169                out.push('=');
170            }
171        }
172        out
173    }
174}
175/// Writer for the VTK legacy ASCII format (`.vtk`).
176pub struct LegacyVtkWriter;
177impl LegacyVtkWriter {
178    /// Serialise a complete unstructured grid to a legacy VTK ASCII string.
179    pub fn write(grid: &VtkUnstructuredGrid) -> String {
180        let mut s = String::new();
181        s.push_str(&Self::write_header());
182        s.push_str(&Self::write_points(&grid.points));
183        s.push_str(&Self::write_cells(&grid.cells, &grid.cell_types));
184        if !grid.point_data.is_empty() {
185            s.push_str(&Self::write_point_data(&grid.point_data));
186        }
187        if !grid.cell_data.is_empty() {
188            s.push_str(&Self::write_cell_data(&grid.cell_data));
189        }
190        s
191    }
192    /// Generate the standard VTK legacy file header.
193    pub fn write_header() -> String {
194        "# vtk DataFile Version 3.0\nOxiPhysics unstructured grid\nASCII\nDATASET UNSTRUCTURED_GRID\n"
195            .to_owned()
196    }
197    /// Serialise the POINTS section.
198    pub fn write_points(points: &[[f64; 3]]) -> String {
199        let mut s = format!("POINTS {} double\n", points.len());
200        for p in points {
201            s.push_str(&format!("{} {} {}\n", p[0], p[1], p[2]));
202        }
203        s
204    }
205    /// Serialise the CELLS and CELL_TYPES sections.
206    pub fn write_cells(cells: &[Vec<usize>], cell_types: &[VtkCellTypeW]) -> String {
207        let total_entries: usize = cells.iter().map(|c| c.len() + 1).sum();
208        let mut s = format!("CELLS {} {}\n", cells.len(), total_entries);
209        for conn in cells {
210            s.push_str(&conn.len().to_string());
211            for &idx in conn {
212                s.push(' ');
213                s.push_str(&idx.to_string());
214            }
215            s.push('\n');
216        }
217        s.push_str(&format!("CELL_TYPES {}\n", cell_types.len()));
218        for ct in cell_types {
219            s.push_str(&format!("{}\n", *ct as u8));
220        }
221        s
222    }
223    /// Serialise the POINT_DATA section.
224    pub fn write_point_data(arrays: &[VtkDataArrayW]) -> String {
225        if arrays.is_empty() {
226            return String::new();
227        }
228        let n = arrays[0].len();
229        let mut s = format!("POINT_DATA {}\n", n);
230        for arr in arrays {
231            s.push_str(&Self::write_data_array(arr));
232        }
233        s
234    }
235    /// Serialise the CELL_DATA section.
236    pub fn write_cell_data(arrays: &[VtkDataArrayW]) -> String {
237        if arrays.is_empty() {
238            return String::new();
239        }
240        let n = arrays[0].len();
241        let mut s = format!("CELL_DATA {}\n", n);
242        for arr in arrays {
243            s.push_str(&Self::write_data_array(arr));
244        }
245        s
246    }
247    /// Serialise a single data array in legacy VTK format.
248    pub fn write_data_array(arr: &VtkDataArrayW) -> String {
249        let mut s = String::new();
250        match arr {
251            VtkDataArrayW::Scalars { name, values } => {
252                s.push_str(&format!(
253                    "SCALARS {} double 1\nLOOKUP_TABLE default\n",
254                    name
255                ));
256                for v in values {
257                    s.push_str(&format!("{}\n", v));
258                }
259            }
260            VtkDataArrayW::Vectors { name, values } => {
261                s.push_str(&format!("VECTORS {} double\n", name));
262                for v in values {
263                    s.push_str(&format!("{} {} {}\n", v[0], v[1], v[2]));
264                }
265            }
266            VtkDataArrayW::Tensors { name, values } => {
267                s.push_str(&format!("TENSORS {} double\n", name));
268                for t in values {
269                    for row in t {
270                        s.push_str(&format!("{} {} {}\n", row[0], row[1], row[2]));
271                    }
272                    s.push('\n');
273                }
274            }
275        }
276        s
277    }
278}
279/// Time-step writer: manages sequential VTU output with a PVD index.
280pub struct TimeStepWriter {
281    /// Base directory for output files.
282    pub output_dir: String,
283    /// Base name for each VTU file (without extension).
284    pub base_name: String,
285    /// Accumulated PVD entries.
286    pub entries: Vec<(f64, String)>,
287}
288impl TimeStepWriter {
289    /// Create a new time-step writer.
290    pub fn new(output_dir: impl Into<String>, base_name: impl Into<String>) -> Self {
291        Self {
292            output_dir: output_dir.into(),
293            base_name: base_name.into(),
294            entries: Vec::new(),
295        }
296    }
297    /// Get the VTU filename for step `i`.
298    pub fn vtu_filename(&self, step: usize) -> String {
299        format!("{}_{:06}.vtu", self.base_name, step)
300    }
301    /// Full path to the VTU file for step `i`.
302    pub fn vtu_path(&self, step: usize) -> String {
303        format!("{}/{}", self.output_dir, self.vtu_filename(step))
304    }
305    /// Register a time step (without writing).
306    pub fn register_step(&mut self, time: f64, step: usize) {
307        self.entries.push((time, self.vtu_filename(step)));
308    }
309    /// Write the PVD collection file.
310    pub fn write_pvd(&self, pvd_name: &str) -> std::io::Result<()> {
311        use std::io::Write;
312        let path = format!("{}/{}", self.output_dir, pvd_name);
313        let file = std::fs::File::create(&path)?;
314        let mut w = std::io::BufWriter::new(file);
315        writeln!(w, r#"<?xml version="1.0"?>"#)?;
316        writeln!(
317            w,
318            r#"<VTKFile type="Collection" version="0.1" byte_order="LittleEndian">"#
319        )?;
320        writeln!(w, r#"  <Collection>"#)?;
321        for (t, fname) in &self.entries {
322            writeln!(
323                w,
324                r#"    <DataSet timestep="{t:.6e}" part="0" file="{fname}"/>"#
325            )?;
326        }
327        writeln!(w, r#"  </Collection>"#)?;
328        writeln!(w, r#"</VTKFile>"#)?;
329        w.flush()?;
330        Ok(())
331    }
332    /// Number of registered steps.
333    pub fn n_steps(&self) -> usize {
334        self.entries.len()
335    }
336}
337/// A VTK POLYDATA dataset for surface meshes (lines and polygons).
338pub struct VtkPolyData {
339    /// 3-D coordinates of every point.
340    pub points: Vec<[f64; 3]>,
341    /// Line connectivity lists.
342    pub lines: Vec<Vec<usize>>,
343    /// Polygon connectivity lists.
344    pub polygons: Vec<Vec<usize>>,
345    /// Per-point data arrays.
346    pub point_data: Vec<VtkDataArrayW>,
347}
348impl VtkPolyData {
349    /// Create an empty poly data set.
350    pub fn new() -> Self {
351        Self {
352            points: Vec::new(),
353            lines: Vec::new(),
354            polygons: Vec::new(),
355            point_data: Vec::new(),
356        }
357    }
358    /// Serialise to VTK legacy ASCII POLYDATA format.
359    pub fn write_legacy(&self) -> String {
360        let mut s = String::new();
361        s.push_str("# vtk DataFile Version 3.0\nOxiPhysics polydata\nASCII\nDATASET POLYDATA\n");
362        s.push_str(&format!("POINTS {} double\n", self.points.len()));
363        for p in &self.points {
364            s.push_str(&format!("{} {} {}\n", p[0], p[1], p[2]));
365        }
366        if !self.lines.is_empty() {
367            let total: usize = self.lines.iter().map(|l| l.len() + 1).sum();
368            s.push_str(&format!("LINES {} {}\n", self.lines.len(), total));
369            for line in &self.lines {
370                s.push_str(&line.len().to_string());
371                for &idx in line {
372                    s.push(' ');
373                    s.push_str(&idx.to_string());
374                }
375                s.push('\n');
376            }
377        }
378        if !self.polygons.is_empty() {
379            let total: usize = self.polygons.iter().map(|p| p.len() + 1).sum();
380            s.push_str(&format!("POLYGONS {} {}\n", self.polygons.len(), total));
381            for poly in &self.polygons {
382                s.push_str(&poly.len().to_string());
383                for &idx in poly {
384                    s.push(' ');
385                    s.push_str(&idx.to_string());
386                }
387                s.push('\n');
388            }
389        }
390        if !self.point_data.is_empty() {
391            s.push_str(&format!("POINT_DATA {}\n", self.points.len()));
392            for arr in &self.point_data {
393                s.push_str(&LegacyVtkWriter::write_data_array(arr));
394            }
395        }
396        s
397    }
398}
399/// An unstructured grid for VTK output.
400///
401/// Build up the grid with [`add_point`](VtkUnstructuredGrid::add_point) and
402/// [`add_cell`](VtkUnstructuredGrid::add_cell), attach field data with the
403/// `add_point_*` / `add_cell_*` helpers, then serialise with
404/// [`LegacyVtkWriter::write`] or [`XmlVtuWriter::write`].
405pub struct VtkUnstructuredGrid {
406    /// 3-D coordinates of every point.
407    pub points: Vec<[f64; 3]>,
408    /// Cell connectivity (variable-length point-index lists).
409    pub cells: Vec<Vec<usize>>,
410    /// VTK cell type for each cell (parallel to `cells`).
411    pub cell_types: Vec<VtkCellTypeW>,
412    /// Per-vertex data arrays.
413    pub point_data: Vec<VtkDataArrayW>,
414    /// Per-cell data arrays.
415    pub cell_data: Vec<VtkDataArrayW>,
416}
417impl VtkUnstructuredGrid {
418    /// Create an empty unstructured grid.
419    pub fn new() -> Self {
420        Self {
421            points: Vec::new(),
422            cells: Vec::new(),
423            cell_types: Vec::new(),
424            point_data: Vec::new(),
425            cell_data: Vec::new(),
426        }
427    }
428    /// Append a point and return its zero-based index.
429    pub fn add_point(&mut self, p: [f64; 3]) -> usize {
430        let idx = self.points.len();
431        self.points.push(p);
432        idx
433    }
434    /// Append a cell defined by `connectivity` (point indices) and a `cell_type`.
435    pub fn add_cell(&mut self, connectivity: Vec<usize>, cell_type: VtkCellTypeW) {
436        self.cells.push(connectivity);
437        self.cell_types.push(cell_type);
438    }
439    /// Attach a per-point scalar field.
440    pub fn add_point_scalars(&mut self, name: &str, values: Vec<f64>) {
441        self.point_data.push(VtkDataArrayW::Scalars {
442            name: name.to_owned(),
443            values,
444        });
445    }
446    /// Attach a per-point vector field.
447    pub fn add_point_vectors(&mut self, name: &str, values: Vec<[f64; 3]>) {
448        self.point_data.push(VtkDataArrayW::Vectors {
449            name: name.to_owned(),
450            values,
451        });
452    }
453    /// Attach a per-cell scalar field.
454    pub fn add_cell_scalars(&mut self, name: &str, values: Vec<f64>) {
455        self.cell_data.push(VtkDataArrayW::Scalars {
456            name: name.to_owned(),
457            values,
458        });
459    }
460    /// Returns the number of points in the grid.
461    pub fn n_points(&self) -> usize {
462        self.points.len()
463    }
464    /// Returns the number of cells in the grid.
465    pub fn n_cells(&self) -> usize {
466        self.cells.len()
467    }
468}
469/// Partition-information for one MPI rank's data in a parallel VTK output.
470#[allow(dead_code)]
471#[derive(Debug, Clone)]
472pub struct VtkPartition {
473    /// Rank index.
474    pub rank: usize,
475    /// File name for this piece.
476    pub filename: String,
477    /// Global point offset.
478    pub point_offset: usize,
479    /// Number of points in this piece.
480    pub n_points: usize,
481    /// Number of cells in this piece.
482    pub n_cells: usize,
483}
484/// VTK cell type identifiers used by the vtk_writer module.
485#[derive(Debug, Clone, Copy, PartialEq, Eq)]
486pub enum VtkCellTypeW {
487    /// VTK_VERTEX (1)
488    Vertex = 1,
489    /// VTK_LINE (3)
490    Line = 3,
491    /// VTK_TRIANGLE (5)
492    Triangle = 5,
493    /// VTK_QUAD (9)
494    Quad = 9,
495    /// VTK_TETRA (10)
496    Tetrahedron = 10,
497    /// VTK_HEXAHEDRON (12)
498    Hexahedron = 12,
499    /// VTK_WEDGE (13)
500    Wedge = 13,
501    /// VTK_PYRAMID (14)
502    Pyramid = 14,
503}
504impl VtkCellTypeW {
505    /// Returns the number of points per cell for this cell type.
506    pub fn n_points(&self) -> usize {
507        match self {
508            Self::Vertex => 1,
509            Self::Line => 2,
510            Self::Triangle => 3,
511            Self::Quad => 4,
512            Self::Tetrahedron => 4,
513            Self::Hexahedron => 8,
514            Self::Wedge => 6,
515            Self::Pyramid => 5,
516        }
517    }
518}
519/// Improved ASCII writer with configurable precision.
520pub struct FormattedAsciiWriter {
521    /// Number of decimal places for floating-point output.
522    pub precision: usize,
523}
524impl FormattedAsciiWriter {
525    /// Create with default precision (6 decimal places).
526    pub fn new() -> Self {
527        Self { precision: 6 }
528    }
529    /// Create with specified precision.
530    pub fn with_precision(precision: usize) -> Self {
531        Self { precision }
532    }
533    /// Format a single f64 value.
534    pub fn format_f64(&self, v: f64) -> String {
535        format!("{:.prec$}", v, prec = self.precision)
536    }
537    /// Format a 3D point.
538    pub fn format_point(&self, p: [f64; 3]) -> String {
539        format!(
540            "{:.prec$} {:.prec$} {:.prec$}",
541            p[0],
542            p[1],
543            p[2],
544            prec = self.precision
545        )
546    }
547    /// Write POINTS section with specified precision.
548    pub fn write_points(&self, points: &[[f64; 3]]) -> String {
549        let mut s = format!("POINTS {} double\n", points.len());
550        for p in points {
551            s.push_str(&self.format_point(*p));
552            s.push('\n');
553        }
554        s
555    }
556    /// Write a scalar data section with specified precision.
557    pub fn write_scalars(&self, name: &str, values: &[f64]) -> String {
558        let mut s = format!("SCALARS {} double 1\nLOOKUP_TABLE default\n", name);
559        for v in values {
560            s.push_str(&self.format_f64(*v));
561            s.push('\n');
562        }
563        s
564    }
565    /// Write a full grid using the legacy ASCII format with configurable precision.
566    pub fn write_grid(&self, grid: &VtkUnstructuredGrid) -> String {
567        let mut s = LegacyVtkWriter::write_header();
568        s.push_str(&self.write_points(&grid.points));
569        s.push_str(&LegacyVtkWriter::write_cells(&grid.cells, &grid.cell_types));
570        if !grid.point_data.is_empty() {
571            let n = grid.point_data[0].len();
572            s.push_str(&format!("POINT_DATA {}\n", n));
573            for arr in &grid.point_data {
574                if let VtkDataArrayW::Scalars { name, values } = arr {
575                    s.push_str(&self.write_scalars(name, values));
576                } else {
577                    s.push_str(&LegacyVtkWriter::write_data_array(arr));
578                }
579            }
580        }
581        s
582    }
583}
584/// Validation result for a VTU grid.
585#[allow(dead_code)]
586#[derive(Debug, Clone)]
587pub struct VtuValidationResult {
588    /// Whether the file structure is valid.
589    pub is_valid: bool,
590    /// List of issues found.
591    pub issues: Vec<String>,
592}
593impl VtuValidationResult {
594    pub(super) fn ok() -> Self {
595        Self {
596            is_valid: true,
597            issues: Vec::new(),
598        }
599    }
600    pub(super) fn add_issue(&mut self, msg: impl Into<String>) {
601        self.is_valid = false;
602        self.issues.push(msg.into());
603    }
604}
605/// Provides simple run-length encoding for f64 data (for illustration).
606pub struct VtkCompression;
607impl VtkCompression {
608    /// Run-length encode a slice of `f64` values.
609    ///
610    /// Consecutive equal values are stored as `(value, count)` pairs.
611    pub fn rle_encode_f64(data: &[f64]) -> Vec<(f64, usize)> {
612        if data.is_empty() {
613            return vec![];
614        }
615        let mut runs: Vec<(f64, usize)> = Vec::new();
616        let mut cur = data[0];
617        let mut count = 1usize;
618        for &v in &data[1..] {
619            if (v - cur).abs() < 1e-15 {
620                count += 1;
621            } else {
622                runs.push((cur, count));
623                cur = v;
624                count = 1;
625            }
626        }
627        runs.push((cur, count));
628        runs
629    }
630    /// Decode RLE-encoded f64 data back to a flat vector.
631    pub fn rle_decode_f64(runs: &[(f64, usize)]) -> Vec<f64> {
632        let total: usize = runs.iter().map(|(_, c)| c).sum();
633        let mut out = Vec::with_capacity(total);
634        for &(v, c) in runs {
635            for _ in 0..c {
636                out.push(v);
637            }
638        }
639        out
640    }
641    /// Compute compression ratio: `original_len / encoded_pairs`.
642    pub fn compression_ratio(original_len: usize, n_runs: usize) -> f64 {
643        if n_runs == 0 {
644            return 0.0;
645        }
646        original_len as f64 / n_runs as f64
647    }
648    /// Losslessly encode a u8 slice using a simple byte-pair delta encoding.
649    ///
650    /// Returns `(delta_bytes, first_value)`. Decode with `delta_decode_u8`.
651    pub fn delta_encode_u8(data: &[u8]) -> (Vec<i8>, u8) {
652        if data.is_empty() {
653            return (vec![], 0);
654        }
655        let first = data[0];
656        let mut deltas: Vec<i8> = Vec::with_capacity(data.len() - 1);
657        for w in data.windows(2) {
658            deltas.push(w[1].wrapping_sub(w[0]) as i8);
659        }
660        (deltas, first)
661    }
662    /// Decode delta-encoded bytes.
663    pub fn delta_decode_u8(deltas: &[i8], first: u8) -> Vec<u8> {
664        let mut out = Vec::with_capacity(deltas.len() + 1);
665        out.push(first);
666        let mut cur = first;
667        for &d in deltas {
668            cur = cur.wrapping_add(d as u8);
669            out.push(cur);
670        }
671        out
672    }
673}
674/// Exports particle data as VTK VERTEX cells.
675pub struct ParticleVtkExporter;
676impl ParticleVtkExporter {
677    /// Export a particle set (each particle as a VTK VERTEX cell).
678    ///
679    /// Optional velocity vectors and a scalar field can be attached.
680    pub fn export_particles(
681        positions: &[[f64; 3]],
682        velocities: Option<&[[f64; 3]]>,
683        scalars: Option<(&str, &[f64])>,
684    ) -> String {
685        let mut grid = VtkUnstructuredGrid::new();
686        for &p in positions {
687            let idx = grid.add_point(p);
688            grid.add_cell(vec![idx], VtkCellTypeW::Vertex);
689        }
690        if let Some(vels) = velocities {
691            grid.add_point_vectors("velocity", vels.to_vec());
692        }
693        if let Some((name, vals)) = scalars {
694            grid.add_point_scalars(name, vals.to_vec());
695        }
696        LegacyVtkWriter::write(&grid)
697    }
698    /// Export SPH particles with density and pressure fields.
699    pub fn export_sph_particles(
700        positions: &[[f64; 3]],
701        densities: &[f64],
702        pressures: &[f64],
703    ) -> String {
704        let mut grid = VtkUnstructuredGrid::new();
705        for &p in positions {
706            let idx = grid.add_point(p);
707            grid.add_cell(vec![idx], VtkCellTypeW::Vertex);
708        }
709        grid.add_point_scalars("density", densities.to_vec());
710        grid.add_point_scalars("pressure", pressures.to_vec());
711        LegacyVtkWriter::write(&grid)
712    }
713}
714/// A named data array for VTK point or cell data.
715#[derive(Debug, Clone)]
716pub enum VtkDataArrayW {
717    /// Scalar (1-component) field.
718    Scalars {
719        /// Field name.
720        name: String,
721        /// One value per point or cell.
722        values: Vec<f64>,
723    },
724    /// 3-component vector field.
725    Vectors {
726        /// Field name.
727        name: String,
728        /// One `[x, y, z]` tuple per point or cell.
729        values: Vec<[f64; 3]>,
730    },
731    /// Symmetric 3x3 tensor field.
732    Tensors {
733        /// Field name.
734        name: String,
735        /// One 3x3 tensor per point or cell.
736        values: Vec<[[f64; 3]; 3]>,
737    },
738}
739impl VtkDataArrayW {
740    /// Returns the name of this data array.
741    pub fn name(&self) -> &str {
742        match self {
743            Self::Scalars { name, .. } => name,
744            Self::Vectors { name, .. } => name,
745            Self::Tensors { name, .. } => name,
746        }
747    }
748    /// Returns the number of tuples in the array.
749    pub fn len(&self) -> usize {
750        match self {
751            Self::Scalars { values, .. } => values.len(),
752            Self::Vectors { values, .. } => values.len(),
753            Self::Tensors { values, .. } => values.len(),
754        }
755    }
756    /// Returns `true` if the array contains no tuples.
757    pub fn is_empty(&self) -> bool {
758        self.len() == 0
759    }
760}
761/// Write a VTK XML UnstructuredGrid (.vtu) file (ASCII or raw binary inline).
762///
763/// Produces valid VTU XML that ParaView and VisIt can read.
764pub struct VtuXmlWriter;
765impl VtuXmlWriter {
766    /// Write points and cells to a VTU XML file.
767    ///
768    /// `point_arrays`: list of (name, values) where values.len() == n_points.
769    /// `cell_arrays`: list of (name, values) where values.len() == n_cells.
770    #[allow(clippy::too_many_arguments)]
771    pub fn write(
772        path: &str,
773        points: &[[f64; 3]],
774        cells: &[Vec<usize>],
775        cell_types: &[VtkCellTypeW],
776        point_arrays: &[(&str, &[f64])],
777        cell_arrays: &[(&str, &[f64])],
778    ) -> std::io::Result<()> {
779        let file = std::fs::File::create(path)?;
780        let mut w = std::io::BufWriter::new(file);
781        let n_pts = points.len();
782        let n_cells = cells.len();
783        let offsets: Vec<usize> = {
784            let mut off = Vec::with_capacity(n_cells);
785            let mut running = 0_usize;
786            for c in cells {
787                running += c.len();
788                off.push(running);
789            }
790            off
791        };
792        writeln!(w, r#"<?xml version="1.0"?>"#)?;
793        writeln!(
794            w,
795            r#"<VTKFile type="UnstructuredGrid" version="0.1" byte_order="LittleEndian">"#
796        )?;
797        writeln!(w, r#"  <UnstructuredGrid>"#)?;
798        writeln!(
799            w,
800            r#"    <Piece NumberOfPoints="{n_pts}" NumberOfCells="{n_cells}">"#
801        )?;
802        if !point_arrays.is_empty() {
803            writeln!(w, r#"      <PointData>"#)?;
804            for &(name, vals) in point_arrays {
805                writeln!(
806                    w,
807                    r#"        <DataArray type="Float64" Name="{name}" format="ascii">"#
808                )?;
809                let row: Vec<String> = vals.iter().map(|v| format!("{v:.10e}")).collect();
810                writeln!(w, "          {}", row.join(" "))?;
811                writeln!(w, r#"        </DataArray>"#)?;
812            }
813            writeln!(w, r#"      </PointData>"#)?;
814        }
815        if !cell_arrays.is_empty() {
816            writeln!(w, r#"      <CellData>"#)?;
817            for &(name, vals) in cell_arrays {
818                writeln!(
819                    w,
820                    r#"        <DataArray type="Float64" Name="{name}" format="ascii">"#
821                )?;
822                let row: Vec<String> = vals.iter().map(|v| format!("{v:.10e}")).collect();
823                writeln!(w, "          {}", row.join(" "))?;
824                writeln!(w, r#"        </DataArray>"#)?;
825            }
826            writeln!(w, r#"      </CellData>"#)?;
827        }
828        writeln!(w, r#"      <Points>"#)?;
829        writeln!(
830            w,
831            r#"        <DataArray type="Float64" NumberOfComponents="3" format="ascii">"#
832        )?;
833        for p in points {
834            writeln!(w, "          {:.10e} {:.10e} {:.10e}", p[0], p[1], p[2])?;
835        }
836        writeln!(w, r#"        </DataArray>"#)?;
837        writeln!(w, r#"      </Points>"#)?;
838        writeln!(w, r#"      <Cells>"#)?;
839        writeln!(
840            w,
841            r#"        <DataArray type="Int64" Name="connectivity" format="ascii">"#
842        )?;
843        for c in cells {
844            let row: Vec<String> = c.iter().map(|i| i.to_string()).collect();
845            writeln!(w, "          {}", row.join(" "))?;
846        }
847        writeln!(w, r#"        </DataArray>"#)?;
848        writeln!(
849            w,
850            r#"        <DataArray type="Int64" Name="offsets" format="ascii">"#
851        )?;
852        let off_row: Vec<String> = offsets.iter().map(|o| o.to_string()).collect();
853        writeln!(w, "          {}", off_row.join(" "))?;
854        writeln!(w, r#"        </DataArray>"#)?;
855        writeln!(
856            w,
857            r#"        <DataArray type="UInt8" Name="types" format="ascii">"#
858        )?;
859        let types_row: Vec<String> = cell_types.iter().map(|t| (*t as u8).to_string()).collect();
860        writeln!(w, "          {}", types_row.join(" "))?;
861        writeln!(w, r#"        </DataArray>"#)?;
862        writeln!(w, r#"      </Cells>"#)?;
863        writeln!(w, r#"    </Piece>"#)?;
864        writeln!(w, r#"  </UnstructuredGrid>"#)?;
865        writeln!(w, r#"</VTKFile>"#)?;
866        w.flush()?;
867        Ok(())
868    }
869}
870/// Utilities for writing numeric data in little-endian binary format.
871pub struct VtkBinaryWriter;
872impl VtkBinaryWriter {
873    /// Encode a slice of `f64` values as little-endian bytes.
874    pub fn encode_f64_le(values: &[f64]) -> Vec<u8> {
875        let mut out = Vec::with_capacity(values.len() * 8);
876        for &v in values {
877            out.extend_from_slice(&v.to_le_bytes());
878        }
879        out
880    }
881    /// Encode a slice of `i64` values as little-endian bytes.
882    pub fn encode_i64_le(values: &[i64]) -> Vec<u8> {
883        let mut out = Vec::with_capacity(values.len() * 8);
884        for &v in values {
885            out.extend_from_slice(&v.to_le_bytes());
886        }
887        out
888    }
889    /// Encode a slice of `u8` values (pass-through).
890    pub fn encode_u8(values: &[u8]) -> Vec<u8> {
891        values.to_vec()
892    }
893    /// Base64-encode a byte slice (same algorithm as `XmlVtuWriter::encode_base64`).
894    pub fn base64_encode(data: &[u8]) -> String {
895        XmlVtuWriter::encode_base64(data)
896    }
897    /// Build a binary-format DataArray XML element for float64 data.
898    ///
899    /// The data is base64-encoded with a 4-byte little-endian length prefix.
900    pub fn binary_data_array_xml(name: &str, n_components: usize, values: &[f64]) -> String {
901        let raw = Self::encode_f64_le(values);
902        let len_bytes = (raw.len() as u32).to_le_bytes();
903        let mut payload = len_bytes.to_vec();
904        payload.extend_from_slice(&raw);
905        let encoded = Self::base64_encode(&payload);
906        format!(
907            "        <DataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"{}\" format=\"binary\">\n          {}\n        </DataArray>\n",
908            name, n_components, encoded
909        )
910    }
911}
912/// Stateless collection of higher-level VTK writing helpers.
913#[allow(dead_code)]
914pub struct VtkWriter;
915#[allow(dead_code)]
916impl VtkWriter {
917    /// Write an unstructured mesh with an attached vector field to a VTU file.
918    ///
919    /// # Arguments
920    /// * `path`       – output file path (`.vtu`)
921    /// * `points`     – node coordinates
922    /// * `cells`      – connectivity (each element is a list of 0-based point indices)
923    /// * `cell_types` – VTK cell type for each cell (parallel to `cells`)
924    /// * `field_name` – name of the vector field (e.g. `"velocity"`)
925    /// * `vectors`    – one `[vx, vy, vz]` per *node* (point data)
926    pub fn write_unstructured_vector_field(
927        path: &str,
928        points: &[[f64; 3]],
929        cells: &[Vec<usize>],
930        cell_types: &[VtkCellTypeW],
931        field_name: &str,
932        vectors: &[[f64; 3]],
933    ) -> std::io::Result<()> {
934        let file = std::fs::File::create(path)?;
935        let mut w = std::io::BufWriter::new(file);
936        let n_pts = points.len();
937        let n_cells = cells.len();
938        let offsets: Vec<usize> = {
939            let mut running = 0_usize;
940            cells
941                .iter()
942                .map(|c| {
943                    running += c.len();
944                    running
945                })
946                .collect()
947        };
948        writeln!(w, r#"<?xml version="1.0"?>"#)?;
949        writeln!(
950            w,
951            r#"<VTKFile type="UnstructuredGrid" version="0.1" byte_order="LittleEndian">"#
952        )?;
953        writeln!(w, r#"  <UnstructuredGrid>"#)?;
954        writeln!(
955            w,
956            r#"    <Piece NumberOfPoints="{n_pts}" NumberOfCells="{n_cells}">"#
957        )?;
958        if !vectors.is_empty() {
959            writeln!(w, r#"      <PointData>"#)?;
960            writeln!(
961                w,
962                r#"        <DataArray type="Float64" Name="{field_name}" NumberOfComponents="3" format="ascii">"#
963            )?;
964            for v in vectors {
965                writeln!(w, "          {:.10e} {:.10e} {:.10e}", v[0], v[1], v[2])?;
966            }
967            writeln!(w, r#"        </DataArray>"#)?;
968            writeln!(w, r#"      </PointData>"#)?;
969        }
970        writeln!(w, r#"      <Points>"#)?;
971        writeln!(
972            w,
973            r#"        <DataArray type="Float64" NumberOfComponents="3" format="ascii">"#
974        )?;
975        for p in points {
976            writeln!(w, "          {:.10e} {:.10e} {:.10e}", p[0], p[1], p[2])?;
977        }
978        writeln!(w, r#"        </DataArray>"#)?;
979        writeln!(w, r#"      </Points>"#)?;
980        writeln!(w, r#"      <Cells>"#)?;
981        writeln!(
982            w,
983            r#"        <DataArray type="Int64" Name="connectivity" format="ascii">"#
984        )?;
985        for c in cells {
986            let row: Vec<String> = c.iter().map(|i| i.to_string()).collect();
987            writeln!(w, "          {}", row.join(" "))?;
988        }
989        writeln!(w, r#"        </DataArray>"#)?;
990        writeln!(
991            w,
992            r#"        <DataArray type="Int64" Name="offsets" format="ascii">"#
993        )?;
994        let off_row: Vec<String> = offsets.iter().map(|o| o.to_string()).collect();
995        writeln!(w, "          {}", off_row.join(" "))?;
996        writeln!(w, r#"        </DataArray>"#)?;
997        writeln!(
998            w,
999            r#"        <DataArray type="UInt8" Name="types" format="ascii">"#
1000        )?;
1001        let types_row: Vec<String> = cell_types.iter().map(|t| (*t as u8).to_string()).collect();
1002        writeln!(w, "          {}", types_row.join(" "))?;
1003        writeln!(w, r#"        </DataArray>"#)?;
1004        writeln!(w, r#"      </Cells>"#)?;
1005        writeln!(w, r#"    </Piece>"#)?;
1006        writeln!(w, r#"  </UnstructuredGrid>"#)?;
1007        writeln!(w, r#"</VTKFile>"#)?;
1008        Ok(())
1009    }
1010    /// Write a set of streamlines as VTK PolyData in legacy ASCII format.
1011    ///
1012    /// Each streamline is a polyline: a sequence of 3-D points.  The output
1013    /// is a valid legacy VTK file containing one `POLYDATA` dataset.
1014    ///
1015    /// # Arguments
1016    /// * `path`        – output file path (`.vtk`)
1017    /// * `streamlines` – each inner `Vec` is one polyline (≥ 2 points each)
1018    pub fn write_streamlines(path: &str, streamlines: &[Vec<[f64; 3]>]) -> std::io::Result<()> {
1019        let mut f = std::fs::File::create(path)?;
1020        let n_points: usize = streamlines.iter().map(|s| s.len()).sum();
1021        let n_lines: usize = streamlines.len();
1022        let total_entries: usize = streamlines.iter().map(|s| s.len() + 1).sum();
1023        writeln!(f, "# vtk DataFile Version 3.0")?;
1024        writeln!(f, "Streamlines")?;
1025        writeln!(f, "ASCII")?;
1026        writeln!(f, "DATASET POLYDATA")?;
1027        writeln!(f, "POINTS {} double", n_points)?;
1028        for sl in streamlines {
1029            for p in sl {
1030                writeln!(f, "{} {} {}", p[0], p[1], p[2])?;
1031            }
1032        }
1033        writeln!(f, "LINES {} {}", n_lines, total_entries)?;
1034        let mut offset = 0usize;
1035        for sl in streamlines {
1036            let n = sl.len();
1037            let indices: Vec<String> = (offset..offset + n).map(|i| i.to_string()).collect();
1038            writeln!(f, "{} {}", n, indices.join(" "))?;
1039            offset += n;
1040        }
1041        Ok(())
1042    }
1043    /// Write a rectilinear grid to a legacy VTK ASCII file.
1044    ///
1045    /// A rectilinear grid is defined by three 1-D coordinate arrays
1046    /// `x_coords`, `y_coords`, `z_coords`.  The grid dimensions are
1047    /// `nx × ny × nz` where `nx = x_coords.len()` etc.
1048    ///
1049    /// Optionally attach a scalar point-data field `scalars` of length
1050    /// `nx * ny * nz` (pass an empty slice to omit).
1051    #[allow(clippy::too_many_arguments)]
1052    pub fn write_rectilinear_grid(
1053        path: &str,
1054        x_coords: &[f64],
1055        y_coords: &[f64],
1056        z_coords: &[f64],
1057        field_name: Option<&str>,
1058        scalars: &[f64],
1059    ) -> std::io::Result<()> {
1060        let mut f = std::fs::File::create(path)?;
1061        let nx = x_coords.len();
1062        let ny = y_coords.len();
1063        let nz = z_coords.len();
1064        let n_pts = nx * ny * nz;
1065        writeln!(f, "# vtk DataFile Version 3.0")?;
1066        writeln!(f, "Rectilinear Grid")?;
1067        writeln!(f, "ASCII")?;
1068        writeln!(f, "DATASET RECTILINEAR_GRID")?;
1069        writeln!(f, "DIMENSIONS {} {} {}", nx, ny, nz)?;
1070        writeln!(f, "X_COORDINATES {} double", nx)?;
1071        for &x in x_coords {
1072            write!(f, "{} ", x)?;
1073        }
1074        writeln!(f)?;
1075        writeln!(f, "Y_COORDINATES {} double", ny)?;
1076        for &y in y_coords {
1077            write!(f, "{} ", y)?;
1078        }
1079        writeln!(f)?;
1080        writeln!(f, "Z_COORDINATES {} double", nz)?;
1081        for &z in z_coords {
1082            write!(f, "{} ", z)?;
1083        }
1084        writeln!(f)?;
1085        if !scalars.is_empty() && scalars.len() == n_pts {
1086            let name = field_name.unwrap_or("scalar_field");
1087            writeln!(f, "POINT_DATA {}", n_pts)?;
1088            writeln!(f, "SCALARS {} double 1", name)?;
1089            writeln!(f, "LOOKUP_TABLE default")?;
1090            for &v in scalars {
1091                writeln!(f, "{}", v)?;
1092            }
1093        }
1094        Ok(())
1095    }
1096}
1097/// A piece descriptor for a parallel VTU file.
1098#[allow(dead_code)]
1099#[derive(Debug, Clone)]
1100pub struct PvtuPiece {
1101    /// Relative path to the piece VTU file.
1102    pub filename: String,
1103}
1104impl PvtuPiece {
1105    /// Create a new piece entry.
1106    pub fn new(filename: impl Into<String>) -> Self {
1107        Self {
1108            filename: filename.into(),
1109        }
1110    }
1111}
1112/// Writer for parallel VTK datasets (PVTU format).
1113pub struct VtkParallelWriter;
1114impl VtkParallelWriter {
1115    /// Generate a PVTU XML file that references individual piece files.
1116    ///
1117    /// # Arguments
1118    /// * `partitions` – information about each piece.
1119    /// * `point_arrays` – list of `(name, n_components)` for point data arrays.
1120    /// * `cell_arrays` – list of `(name, n_components)` for cell data arrays.
1121    pub fn write_pvtu(
1122        partitions: &[VtkPartition],
1123        point_arrays: &[(&str, usize)],
1124        cell_arrays: &[(&str, usize)],
1125    ) -> String {
1126        let mut s = String::new();
1127        s.push_str("<?xml version=\"1.0\"?>\n");
1128        s.push_str(
1129            "<VTKFile type=\"PUnstructuredGrid\" version=\"0.1\" byte_order=\"LittleEndian\">\n",
1130        );
1131        s.push_str("  <PUnstructuredGrid GhostLevel=\"0\">\n");
1132        s.push_str("    <PPoints>\n");
1133        s.push_str("      <PDataArray type=\"Float64\" NumberOfComponents=\"3\"/>\n");
1134        s.push_str("    </PPoints>\n");
1135        if !point_arrays.is_empty() {
1136            s.push_str("    <PPointData>\n");
1137            for (name, ncomp) in point_arrays {
1138                s.push_str(&format!(
1139                    "      <PDataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"{}\"/>\n",
1140                    name, ncomp
1141                ));
1142            }
1143            s.push_str("    </PPointData>\n");
1144        }
1145        if !cell_arrays.is_empty() {
1146            s.push_str("    <PCellData>\n");
1147            for (name, ncomp) in cell_arrays {
1148                s.push_str(&format!(
1149                    "      <PDataArray type=\"Float64\" Name=\"{}\" NumberOfComponents=\"{}\"/>\n",
1150                    name, ncomp
1151                ));
1152            }
1153            s.push_str("    </PCellData>\n");
1154        }
1155        for part in partitions {
1156            s.push_str(&format!("    <Piece Source=\"{}\"/>\n", part.filename));
1157        }
1158        s.push_str("  </PUnstructuredGrid>\n</VTKFile>\n");
1159        s
1160    }
1161    /// Compute a balanced partition of `n_points` across `n_ranks` ranks.
1162    ///
1163    /// Returns a list of `(offset, count)` pairs.
1164    pub fn partition_points(n_points: usize, n_ranks: usize) -> Vec<(usize, usize)> {
1165        if n_ranks == 0 || n_points == 0 {
1166            return vec![];
1167        }
1168        let base = n_points / n_ranks;
1169        let extra = n_points % n_ranks;
1170        let mut partitions = Vec::with_capacity(n_ranks);
1171        let mut offset = 0;
1172        for r in 0..n_ranks {
1173            let count = base + if r < extra { 1 } else { 0 };
1174            partitions.push((offset, count));
1175            offset += count;
1176        }
1177        partitions
1178    }
1179}