Skip to main content

oxiphysics_io/xdmf/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::manual_div_ceil, clippy::too_many_arguments)]
6#[allow(unused_imports)]
7use super::functions::*;
8#[allow(unused_imports)]
9use super::functions_2::*;
10use std::io::Write;
11
12/// Topology types supported for unstructured mesh output.
13#[allow(dead_code)]
14#[derive(Debug, Clone, Copy, PartialEq)]
15pub enum XdmfTopologyType {
16    /// Linear triangle (3 nodes).
17    Triangle,
18    /// Linear tetrahedron (4 nodes).
19    Tetrahedron,
20    /// Linear hexahedron (8 nodes).
21    Hexahedron,
22    /// Linear quadrilateral (4 nodes).
23    Quadrilateral,
24    /// Mixed element types (XDMF Mixed topology).
25    Mixed,
26}
27impl XdmfTopologyType {
28    /// Return the XDMF topology type string.
29    pub fn xdmf_name(self) -> &'static str {
30        match self {
31            XdmfTopologyType::Triangle => "Triangle",
32            XdmfTopologyType::Tetrahedron => "Tetrahedron",
33            XdmfTopologyType::Hexahedron => "Hexahedron",
34            XdmfTopologyType::Quadrilateral => "Quadrilateral",
35            XdmfTopologyType::Mixed => "Mixed",
36        }
37    }
38    /// Nodes per element (0 for Mixed, since element size varies).
39    pub fn nodes_per_element(self) -> usize {
40        match self {
41            XdmfTopologyType::Triangle => 3,
42            XdmfTopologyType::Tetrahedron => 4,
43            XdmfTopologyType::Hexahedron => 8,
44            XdmfTopologyType::Quadrilateral => 4,
45            XdmfTopologyType::Mixed => 0,
46        }
47    }
48}
49/// A single time step in an XDMF time series.
50#[derive(Debug, Clone)]
51#[allow(dead_code)]
52pub struct XdmfStep {
53    /// Simulation time.
54    pub time: f64,
55    /// Number of points.
56    pub n_points: usize,
57    /// Position data.
58    pub position_data: Vec<[f64; 3]>,
59    /// Named scalar fields.
60    pub scalar_fields: Vec<(String, Vec<f64>)>,
61}
62/// Parameters for a structured uniform 3-D grid.
63#[derive(Debug, Clone)]
64#[allow(dead_code)]
65pub struct XdmfUniformGrid {
66    /// Grid name.
67    pub name: String,
68    /// Number of nodes along each axis `[nx, ny, nz]`.
69    pub dimensions: [usize; 3],
70    /// Origin of the grid `[ox, oy, oz]`.
71    pub origin: [f64; 3],
72    /// Grid spacing `[dx, dy, dz]`.
73    pub spacing: [f64; 3],
74}
75/// Build an XDMF multi-block (spatial collection) document containing
76/// multiple uniform grids sharing the same domain.
77#[derive(Debug, Clone, Default)]
78#[allow(dead_code)]
79pub struct XdmfMultiBlock {
80    /// Named blocks (each block is a complete XDMF Uniform grid XML fragment,
81    /// without the outer `<?xml …>`, `<Xdmf …>`, or ``Domain` wrappers).
82    pub(super) blocks: Vec<(String, String)>,
83}
84#[allow(dead_code)]
85impl XdmfMultiBlock {
86    /// Create an empty multi-block container.
87    pub fn new() -> Self {
88        Self::default()
89    }
90    /// Add a named block given its inner grid XML (starting with `<Grid …>`).
91    pub fn add_block(&mut self, name: &str, grid_xml: &str) {
92        self.blocks.push((name.to_string(), grid_xml.to_string()));
93    }
94    /// Number of blocks.
95    pub fn len(&self) -> usize {
96        self.blocks.len()
97    }
98    /// Returns `true` if no blocks have been added.
99    pub fn is_empty(&self) -> bool {
100        self.blocks.is_empty()
101    }
102    /// Serialize to a complete XDMF document.
103    pub fn to_xml(&self) -> String {
104        let mut s = String::new();
105        s.push_str("<?xml version=\"1.0\"?>\n");
106        s.push_str("<Xdmf Version=\"3.0\">\n");
107        s.push_str("  <Domain>\n");
108        s.push_str(
109            "    <Grid Name=\"MultiBlock\" GridType=\"Collection\" CollectionType=\"Spatial\">\n",
110        );
111        for (_name, grid) in &self.blocks {
112            for line in grid.lines() {
113                s.push_str("      ");
114                s.push_str(line);
115                s.push('\n');
116            }
117        }
118        s.push_str("    </Grid>\n");
119        s.push_str("  </Domain>\n");
120        s.push_str("</Xdmf>\n");
121        s
122    }
123}
124/// A time series referencing HDF5 datasets by path.
125#[derive(Debug, Clone)]
126#[allow(dead_code)]
127pub struct XdmfTimeSeriesHdf5 {
128    /// Time values for each step.
129    pub timesteps: Vec<f64>,
130    /// HDF5 file paths for each step's geometry data.
131    pub hdf5_paths: Vec<String>,
132    /// Attribute names to reference in HDF5.
133    pub attribute_names: Vec<String>,
134}
135#[allow(dead_code)]
136impl XdmfTimeSeriesHdf5 {
137    /// Create a new empty HDF5-backed time series.
138    pub fn new() -> Self {
139        Self {
140            timesteps: Vec::new(),
141            hdf5_paths: Vec::new(),
142            attribute_names: Vec::new(),
143        }
144    }
145    /// Write a multi-timestep XDMF collection to `path`.
146    ///
147    /// `n_nodes` and `n_elements` describe the mesh size for each step;
148    /// `topology` is the XDMF topology type string (e.g., `"Triangle"`).
149    pub fn write_collection(
150        &self,
151        path: &str,
152        n_nodes: usize,
153        n_elements: usize,
154        topology: &str,
155    ) -> std::io::Result<()> {
156        let mut f = std::fs::File::create(path)?;
157        writeln!(f, "<?xml version=\"1.0\"?>")?;
158        writeln!(f, "<Xdmf Version=\"3.0\">")?;
159        writeln!(f, "  <Domain>")?;
160        writeln!(
161            f,
162            "    <Grid Name=\"TimeSeries\" GridType=\"Collection\" CollectionType=\"Temporal\">"
163        )?;
164        for (step_idx, &t) in self.timesteps.iter().enumerate() {
165            let h5path = self
166                .hdf5_paths
167                .get(step_idx)
168                .map(|s| s.as_str())
169                .unwrap_or("data.h5");
170            writeln!(
171                f,
172                "      <Grid Name=\"step{}\" GridType=\"Uniform\">",
173                step_idx
174            )?;
175            writeln!(f, "        <Time Value=\"{}\"/>", t)?;
176            writeln!(
177                f,
178                "        <Topology TopologyType=\"{}\" NumberOfElements=\"{}\"/>",
179                topology, n_elements
180            )?;
181            writeln!(f, "        <Geometry GeometryType=\"XYZ\">")?;
182            writeln!(
183                f,
184                "          <DataItem Format=\"HDF\" Dimensions=\"{} 3\">{}:/coordinates</DataItem>",
185                n_nodes, h5path
186            )?;
187            writeln!(f, "        </Geometry>")?;
188            for attr in &self.attribute_names {
189                writeln!(
190                    f,
191                    "        <Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"Node\">",
192                    attr
193                )?;
194                writeln!(
195                    f,
196                    "          <DataItem Format=\"HDF\" Dimensions=\"{}\">{}/{}</DataItem>",
197                    n_nodes, h5path, attr
198                )?;
199                writeln!(f, "        </Attribute>")?;
200            }
201            writeln!(f, "      </Grid>")?;
202        }
203        writeln!(f, "    </Grid>")?;
204        writeln!(f, "  </Domain>")?;
205        writeln!(f, "</Xdmf>")?;
206        Ok(())
207    }
208}
209/// A structured (regular) Cartesian grid with optional scalar/vector fields.
210///
211/// This is the primary building block for CFD output on structured meshes.
212#[derive(Debug, Clone)]
213#[allow(dead_code)]
214pub struct XdmfStructuredGrid {
215    /// Grid name shown in post-processors.
216    pub name: String,
217    /// Number of nodes along X (nodes = cells + 1 per axis).
218    pub ni: usize,
219    /// Number of nodes along Y.
220    pub nj: usize,
221    /// Number of nodes along Z.
222    pub nk: usize,
223    /// Physical origin of the grid.
224    pub origin: [f64; 3],
225    /// Grid spacing along X.
226    pub dx: f64,
227    /// Grid spacing along Y.
228    pub dy: f64,
229    /// Grid spacing along Z.
230    pub dz: f64,
231    /// Node-centered scalar fields `(name, flat values, len = ni*nj*nk)`.
232    pub node_scalars: Vec<(String, Vec<f64>)>,
233    /// Node-centered vector fields `(name, flat values, len = ni*nj*nk)`.
234    pub node_vectors: Vec<(String, Vec<[f64; 3]>)>,
235    /// Cell-centered scalar fields `(name, flat values, len = (ni-1)*(nj-1)*(nk-1))`.
236    pub cell_scalars: Vec<(String, Vec<f64>)>,
237}
238#[allow(dead_code)]
239impl XdmfStructuredGrid {
240    /// Create a new structured grid with no fields.
241    pub fn new(
242        name: &str,
243        ni: usize,
244        nj: usize,
245        nk: usize,
246        origin: [f64; 3],
247        dx: f64,
248        dy: f64,
249        dz: f64,
250    ) -> Self {
251        Self {
252            name: name.to_string(),
253            ni,
254            nj,
255            nk,
256            origin,
257            dx,
258            dy,
259            dz,
260            node_scalars: Vec::new(),
261            node_vectors: Vec::new(),
262            cell_scalars: Vec::new(),
263        }
264    }
265    /// Total node count.
266    pub fn n_nodes(&self) -> usize {
267        self.ni * self.nj * self.nk
268    }
269    /// Total cell count.
270    pub fn n_cells(&self) -> usize {
271        (self.ni.saturating_sub(1)) * (self.nj.saturating_sub(1)) * (self.nk.saturating_sub(1))
272    }
273    /// Add a node-centered scalar field.
274    pub fn add_node_scalar(&mut self, name: &str, data: Vec<f64>) {
275        self.node_scalars.push((name.to_string(), data));
276    }
277    /// Add a node-centered vector field.
278    pub fn add_node_vector(&mut self, name: &str, data: Vec<[f64; 3]>) {
279        self.node_vectors.push((name.to_string(), data));
280    }
281    /// Add a cell-centered scalar field.
282    pub fn add_cell_scalar(&mut self, name: &str, data: Vec<f64>) {
283        self.cell_scalars.push((name.to_string(), data));
284    }
285    /// Serialize the structured grid to XDMF XML using ORIGIN_DXDYDZ geometry.
286    pub fn to_xml(&self) -> String {
287        let mut s = String::new();
288        s.push_str("<?xml version=\"1.0\"?>\n");
289        s.push_str("<Xdmf Version=\"3.0\">\n");
290        s.push_str("  <Domain>\n");
291        s.push_str(&format!(
292            "    <Grid Name=\"{}\" GridType=\"Uniform\">\n",
293            self.name
294        ));
295        s.push_str(&format!(
296            "      <Topology TopologyType=\"3DCoRectMesh\" Dimensions=\"{} {} {}\"/>\n",
297            self.nk, self.nj, self.ni
298        ));
299        s.push_str("      <Geometry GeometryType=\"ORIGIN_DXDYDZ\">\n");
300        s.push_str(&format!(
301            "        <DataItem Format=\"XML\" Dimensions=\"3\">{} {} {}</DataItem>\n",
302            self.origin[2], self.origin[1], self.origin[0]
303        ));
304        s.push_str(&format!(
305            "        <DataItem Format=\"XML\" Dimensions=\"3\">{} {} {}</DataItem>\n",
306            self.dz, self.dy, self.dx
307        ));
308        s.push_str("      </Geometry>\n");
309        for (name, data) in &self.node_scalars {
310            s.push_str(&format!(
311                "      <Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"Node\">\n",
312                name
313            ));
314            s.push_str(&format!(
315                "        <DataItem Format=\"XML\" Dimensions=\"{}\">\n          ",
316                data.len()
317            ));
318            for (i, v) in data.iter().enumerate() {
319                if i > 0 {
320                    s.push(' ');
321                }
322                s.push_str(&format!("{}", v));
323            }
324            s.push_str("\n        </DataItem>\n      </Attribute>\n");
325        }
326        for (name, data) in &self.node_vectors {
327            s.push_str(&format!(
328                "      <Attribute Name=\"{}\" AttributeType=\"Vector\" Center=\"Node\">\n",
329                name
330            ));
331            s.push_str(&format!(
332                "        <DataItem Format=\"XML\" Dimensions=\"{} 3\">\n",
333                data.len()
334            ));
335            for v in data {
336                s.push_str(&format!("          {} {} {}\n", v[0], v[1], v[2]));
337            }
338            s.push_str("        </DataItem>\n      </Attribute>\n");
339        }
340        for (name, data) in &self.cell_scalars {
341            s.push_str(&format!(
342                "      <Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"Cell\">\n",
343                name
344            ));
345            s.push_str(&format!(
346                "        <DataItem Format=\"XML\" Dimensions=\"{}\">\n          ",
347                data.len()
348            ));
349            for (i, v) in data.iter().enumerate() {
350                if i > 0 {
351                    s.push(' ');
352                }
353                s.push_str(&format!("{}", v));
354            }
355            s.push_str("\n        </DataItem>\n      </Attribute>\n");
356        }
357        s.push_str("    </Grid>\n");
358        s.push_str("  </Domain>\n");
359        s.push_str("</Xdmf>\n");
360        s
361    }
362}
363/// A stateful writer that accumulates XDMF XML into a `String`.
364///
365/// Supports opening/closing domains, grids, topology, geometry, and attributes
366/// incrementally, allowing complex documents to be built programmatically.
367#[allow(dead_code)]
368pub struct XdmfWriter {
369    pub(super) buf: String,
370    pub(super) indent: usize,
371}
372#[allow(dead_code)]
373impl XdmfWriter {
374    /// Create a new writer and emit the XML declaration and root ``Xdmf` element.
375    pub fn new() -> Self {
376        let mut w = XdmfWriter {
377            buf: String::new(),
378            indent: 0,
379        };
380        w.buf.push_str("<?xml version=\"1.0\"?>\n");
381        w.buf.push_str("<Xdmf Version=\"3.0\">\n");
382        w.indent = 1;
383        w
384    }
385    fn push_line(&mut self, s: &str) {
386        let pad = "  ".repeat(self.indent);
387        self.buf.push_str(&pad);
388        self.buf.push_str(s);
389        self.buf.push('\n');
390    }
391    /// Open ``Domain`.
392    pub fn open_domain(&mut self) {
393        self.push_line("<Domain>");
394        self.indent += 1;
395    }
396    /// Close `</Domain>`.
397    pub fn close_domain(&mut self) {
398        self.indent = self.indent.saturating_sub(1);
399        self.push_line("</Domain>");
400    }
401    /// Open a ``Grid` element with `name` and optional `GridType`.
402    pub fn open_grid(&mut self, name: &str, grid_type: &str) {
403        self.push_line(&format!(
404            "<Grid Name=\"{}\" GridType=\"{}\">",
405            name, grid_type
406        ));
407        self.indent += 1;
408    }
409    /// Open a temporal collection grid.
410    pub fn open_temporal_collection(&mut self, name: &str) {
411        self.push_line(&format!(
412            "<Grid Name=\"{}\" GridType=\"Collection\" CollectionType=\"Temporal\">",
413            name
414        ));
415        self.indent += 1;
416    }
417    /// Emit a `<Time Value="t"/>` element.
418    pub fn write_time(&mut self, t: f64) {
419        self.push_line(&format!("<Time Value=\"{}\"/>", t));
420    }
421    /// Close `</Grid>`.
422    pub fn close_grid(&mut self) {
423        self.indent = self.indent.saturating_sub(1);
424        self.push_line("</Grid>");
425    }
426    /// Emit a ``Topology` element for a polyvertex (point cloud).
427    pub fn write_polyvertex_topology(&mut self, n: usize) {
428        self.push_line(&format!(
429            "<Topology TopologyType=\"Polyvertex\" NumberOfElements=\"{}\"/>",
430            n
431        ));
432    }
433    /// Emit a ``Topology` element for an unstructured mesh.
434    pub fn write_unstructured_topology(
435        &mut self,
436        topo_type: &str,
437        n_elements: usize,
438        connectivity: &[usize],
439        npe: usize,
440    ) {
441        self.push_line(&format!(
442            "<Topology TopologyType=\"{}\" NumberOfElements=\"{}\">",
443            topo_type, n_elements
444        ));
445        self.indent += 1;
446        self.push_line(&format!(
447            "<DataItem Format=\"XML\" Dimensions=\"{} {}\">\n{}  ",
448            n_elements,
449            npe,
450            "  ".repeat(self.indent)
451        ));
452        let row_strings: Vec<String> = connectivity
453            .chunks(npe)
454            .map(|chunk| {
455                chunk
456                    .iter()
457                    .map(|i| i.to_string())
458                    .collect::<Vec<_>>()
459                    .join(" ")
460            })
461            .collect();
462        let pad = "  ".repeat(self.indent);
463        for r in &row_strings {
464            self.buf.push_str(&pad);
465            self.buf.push_str(r);
466            self.buf.push('\n');
467        }
468        self.push_line("</DataItem>");
469        self.indent = self.indent.saturating_sub(1);
470        self.push_line("</Topology>");
471    }
472    /// Emit a `<Geometry GeometryType="XYZ">` with inline node coordinates.
473    pub fn write_xyz_geometry(&mut self, nodes: &[[f64; 3]]) {
474        self.push_line("<Geometry GeometryType=\"XYZ\">");
475        self.indent += 1;
476        self.push_line(&format!(
477            "<DataItem Format=\"XML\" Dimensions=\"{} 3\">",
478            nodes.len()
479        ));
480        self.indent += 1;
481        let pad = "  ".repeat(self.indent);
482        for p in nodes {
483            self.buf.push_str(&pad);
484            self.buf.push_str(&format!("{} {} {}\n", p[0], p[1], p[2]));
485        }
486        self.indent = self.indent.saturating_sub(1);
487        self.push_line("</DataItem>");
488        self.indent = self.indent.saturating_sub(1);
489        self.push_line("</Geometry>");
490    }
491    /// Emit a scalar attribute with inline data.
492    pub fn write_scalar_attribute(&mut self, name: &str, center: &str, values: &[f64]) {
493        self.push_line(&format!(
494            "<Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"{}\">",
495            name, center
496        ));
497        self.indent += 1;
498        self.push_line(&format!(
499            "<DataItem Format=\"XML\" Dimensions=\"{}\">",
500            values.len()
501        ));
502        self.indent += 1;
503        let pad = "  ".repeat(self.indent);
504        self.buf.push_str(&pad);
505        for (i, v) in values.iter().enumerate() {
506            if i > 0 {
507                self.buf.push(' ');
508            }
509            self.buf.push_str(&format!("{}", v));
510        }
511        self.buf.push('\n');
512        self.indent = self.indent.saturating_sub(1);
513        self.push_line("</DataItem>");
514        self.indent = self.indent.saturating_sub(1);
515        self.push_line("</Attribute>");
516    }
517    /// Emit a vector attribute with inline data.
518    pub fn write_vector_attribute(&mut self, name: &str, center: &str, vectors: &[[f64; 3]]) {
519        self.push_line(&format!(
520            "<Attribute Name=\"{}\" AttributeType=\"Vector\" Center=\"{}\">",
521            name, center
522        ));
523        self.indent += 1;
524        self.push_line(&format!(
525            "<DataItem Format=\"XML\" Dimensions=\"{} 3\">",
526            vectors.len()
527        ));
528        self.indent += 1;
529        let pad = "  ".repeat(self.indent);
530        for v in vectors {
531            self.buf.push_str(&pad);
532            self.buf.push_str(&format!("{} {} {}\n", v[0], v[1], v[2]));
533        }
534        self.indent = self.indent.saturating_sub(1);
535        self.push_line("</DataItem>");
536        self.indent = self.indent.saturating_sub(1);
537        self.push_line("</Attribute>");
538    }
539    /// Emit a HDF5-reference attribute.
540    pub fn write_hdf5_attribute(
541        &mut self,
542        name: &str,
543        center: &str,
544        attr_type: &str,
545        dims: &str,
546        hdf5_ref: &str,
547    ) {
548        self.push_line(&format!(
549            "<Attribute Name=\"{}\" AttributeType=\"{}\" Center=\"{}\">",
550            name, attr_type, center
551        ));
552        self.indent += 1;
553        self.push_line(&format!(
554            "<DataItem Format=\"HDF\" Dimensions=\"{}\">{}</DataItem>",
555            dims, hdf5_ref
556        ));
557        self.indent = self.indent.saturating_sub(1);
558        self.push_line("</Attribute>");
559    }
560    /// Emit the closing `</Xdmf>` element and return the accumulated XML string.
561    pub fn finish(mut self) -> String {
562        self.buf.push_str("</Xdmf>\n");
563        self.buf
564    }
565    /// Return the current buffer without closing (useful for inspection).
566    pub fn peek(&self) -> &str {
567        &self.buf
568    }
569}
570/// Builds a canonical HDF5 dataset path for use in XDMF ``DataItem` elements.
571///
572/// A path looks like `file.h5:/group/dataset`.
573#[allow(dead_code)]
574pub struct Hdf5DataItemBuilder {
575    pub(super) filename: String,
576    pub(super) group: String,
577}
578#[allow(dead_code)]
579impl Hdf5DataItemBuilder {
580    /// Create a builder targeting `filename`.
581    pub fn new(filename: &str) -> Self {
582        Self {
583            filename: filename.to_owned(),
584            group: "/".to_owned(),
585        }
586    }
587    /// Set the HDF5 group prefix (must start with `/`).
588    pub fn group(mut self, group: &str) -> Self {
589        self.group = group.to_owned();
590        self
591    }
592    /// Build a ``DataItem` XML fragment referencing `dataset_name` under
593    /// the configured group, with the given `dimensions` string (e.g., `"100 3"`).
594    pub fn build(&self, dataset_name: &str, dimensions: &str) -> String {
595        let path = if self.group == "/" {
596            format!("/{}", dataset_name)
597        } else {
598            format!("{}/{}", self.group, dataset_name)
599        };
600        format!(
601            "<DataItem Format=\"HDF\" Dimensions=\"{}\">\n  {}:{}\n</DataItem>",
602            dimensions, self.filename, path
603        )
604    }
605}
606/// A named grid inside a domain collection.
607#[derive(Debug, Clone)]
608#[allow(dead_code)]
609pub struct XdmfGridEntry {
610    /// Grid name.
611    pub name: String,
612    /// Time value for this grid.
613    pub time: f64,
614    /// Node positions.
615    pub nodes: Vec<[f64; 3]>,
616    /// Flat connectivity array.
617    pub connectivity: Vec<usize>,
618    /// Topology type string.
619    pub topology_type: String,
620}
621impl XdmfGridEntry {
622    /// Number of nodes in this grid.
623    #[allow(dead_code)]
624    pub fn node_count(&self) -> usize {
625        self.nodes.len()
626    }
627    /// Compute the axis-aligned bounding box of all nodes.
628    /// Returns `([min_x, min_y, min_z], [max_x, max_y, max_z])`.
629    #[allow(dead_code)]
630    pub fn bounding_box(&self) -> ([f64; 3], [f64; 3]) {
631        let mut lo = [f64::INFINITY; 3];
632        let mut hi = [f64::NEG_INFINITY; 3];
633        for n in &self.nodes {
634            for k in 0..3 {
635                if n[k] < lo[k] {
636                    lo[k] = n[k];
637                }
638                if n[k] > hi[k] {
639                    hi[k] = n[k];
640                }
641            }
642        }
643        (lo, hi)
644    }
645    /// Compute the centroid of all nodes.
646    #[allow(dead_code)]
647    pub fn centroid(&self) -> [f64; 3] {
648        if self.nodes.is_empty() {
649            return [0.0; 3];
650        }
651        let mut s = [0.0_f64; 3];
652        for n in &self.nodes {
653            s[0] += n[0];
654            s[1] += n[1];
655            s[2] += n[2];
656        }
657        let inv = 1.0 / self.nodes.len() as f64;
658        [s[0] * inv, s[1] * inv, s[2] * inv]
659    }
660}
661/// Describes an XDMF attribute (field) referencing HDF5 data.
662#[derive(Debug, Clone)]
663#[allow(dead_code)]
664pub struct XdmfAttribute {
665    /// Attribute name.
666    pub name: String,
667    /// Centering: `"Node"`, `"Cell"`, etc.
668    pub center: String,
669    /// Number of components (1 = scalar, 3 = vector, etc.).
670    pub n_components: usize,
671    /// HDF5 dataset path (e.g., `"data.h5:/temperature"`).
672    pub hdf5_path: String,
673}
674/// A single time step for an unstructured mesh time series.
675#[allow(dead_code)]
676#[derive(Debug, Clone)]
677pub struct XdmfMeshStep {
678    /// Simulation time.
679    pub time: f64,
680    /// Node positions (3-D).
681    pub nodes: Vec<[f64; 3]>,
682    /// Flat connectivity array (length = n_elements * nodes_per_element).
683    pub connectivity: Vec<usize>,
684    /// Element topology.
685    pub topology: XdmfTopologyType,
686    /// Named node-centered scalar fields.
687    pub node_scalars: Vec<(String, Vec<f64>)>,
688    /// Named node-centered vector fields (each vec is a `[f64;3]` per node).
689    pub node_vectors: Vec<(String, Vec<[f64; 3]>)>,
690}
691impl XdmfMeshStep {
692    /// Number of elements in this step.
693    pub fn n_elements(&self) -> usize {
694        let npe = self.topology.nodes_per_element();
695        self.connectivity.len().checked_div(npe).unwrap_or(0)
696    }
697}
698/// A collection of grids forming an XDMF domain.
699#[derive(Debug, Clone, Default)]
700#[allow(dead_code)]
701pub struct XdmfDomainCollection {
702    /// All grids in this collection.
703    pub grids: Vec<XdmfGridEntry>,
704}
705impl XdmfDomainCollection {
706    /// Create an empty domain collection.
707    #[allow(dead_code)]
708    pub fn new() -> Self {
709        XdmfDomainCollection { grids: Vec::new() }
710    }
711    /// Add a grid entry to this collection.
712    #[allow(dead_code)]
713    pub fn add_grid(&mut self, grid: XdmfGridEntry) {
714        self.grids.push(grid);
715    }
716    /// Total number of nodes across all grids.
717    #[allow(dead_code)]
718    pub fn total_node_count(&self) -> usize {
719        self.grids.iter().map(|g| g.node_count()).sum()
720    }
721    /// Find a grid by name.
722    #[allow(dead_code)]
723    pub fn find_grid(&self, name: &str) -> Option<&XdmfGridEntry> {
724        self.grids.iter().find(|g| g.name == name)
725    }
726    /// Write the full XDMF XML for this domain collection.
727    #[allow(dead_code)]
728    pub fn write_xml<W: std::io::Write>(&self, writer: &mut W) -> std::io::Result<()> {
729        writeln!(writer, "<?xml version=\"1.0\"?>")?;
730        writeln!(writer, "<Xdmf Version=\"3.0\">")?;
731        writeln!(writer, "  <Domain>")?;
732        writeln!(
733            writer,
734            "    <Grid Name=\"collection\" GridType=\"Collection\" CollectionType=\"Temporal\">"
735        )?;
736        for grid in &self.grids {
737            let nn = grid.node_count();
738            writeln!(
739                writer,
740                "      <Grid Name=\"{}\" GridType=\"Uniform\">",
741                grid.name
742            )?;
743            writeln!(writer, "        <Time Value=\"{}\"/>", grid.time)?;
744            writeln!(
745                writer,
746                "        <Topology TopologyType=\"{}\" NumberOfElements=\"{}\"/>",
747                grid.topology_type,
748                grid.connectivity.len()
749            )?;
750            writeln!(writer, "        <Geometry GeometryType=\"XYZ\">")?;
751            writeln!(
752                writer,
753                "          <DataItem Format=\"XML\" Dimensions=\"{nn} 3\">"
754            )?;
755            for n in &grid.nodes {
756                writeln!(writer, "            {} {} {}", n[0], n[1], n[2])?;
757            }
758            writeln!(writer, "          </DataItem>")?;
759            writeln!(writer, "        </Geometry>")?;
760            writeln!(writer, "      </Grid>")?;
761        }
762        writeln!(writer, "    </Grid>")?;
763        writeln!(writer, "  </Domain>")?;
764        writeln!(writer, "</Xdmf>")?;
765        Ok(())
766    }
767    /// Time range: (min_time, max_time).  Returns (0.0, 0.0) if no grids.
768    #[allow(dead_code)]
769    pub fn time_range(&self) -> (f64, f64) {
770        if self.grids.is_empty() {
771            return (0.0, 0.0);
772        }
773        let t_min = self
774            .grids
775            .iter()
776            .map(|g| g.time)
777            .fold(f64::INFINITY, f64::min);
778        let t_max = self
779            .grids
780            .iter()
781            .map(|g| g.time)
782            .fold(f64::NEG_INFINITY, f64::max);
783        (t_min, t_max)
784    }
785}
786/// Describes a single data field to embed inline in XDMF XML.
787#[derive(Debug, Clone)]
788#[allow(dead_code)]
789pub struct XdmfFieldDescriptor {
790    /// Name of the field (e.g. `"pressure"`, `"velocity"`).
791    pub name: String,
792    /// Attribute type: `"Scalar"`, `"Vector"`, or `"Tensor"`.
793    pub attribute_type: String,
794    /// Center: `"Node"`, `"Cell"`, or `"Grid"`.
795    pub center: String,
796    /// Flat data values (row-major).
797    pub data: Vec<f64>,
798    /// Number of components per entry (1 for scalar, 3 for vector, 9 for tensor).
799    pub n_components: usize,
800}
801impl XdmfFieldDescriptor {
802    /// Create a new scalar field descriptor.
803    #[allow(dead_code)]
804    pub fn scalar(name: &str, data: Vec<f64>) -> Self {
805        XdmfFieldDescriptor {
806            name: name.to_string(),
807            attribute_type: "Scalar".to_string(),
808            center: "Node".to_string(),
809            data,
810            n_components: 1,
811        }
812    }
813    /// Create a new vector field descriptor (3 components per node).
814    #[allow(dead_code)]
815    pub fn vector(name: &str, data: Vec<f64>) -> Self {
816        XdmfFieldDescriptor {
817            name: name.to_string(),
818            attribute_type: "Vector".to_string(),
819            center: "Node".to_string(),
820            data,
821            n_components: 3,
822        }
823    }
824    /// Number of logical entries (nodes/cells) this field covers.
825    #[allow(dead_code)]
826    pub fn entry_count(&self) -> usize {
827        self.data.len().checked_div(self.n_components).unwrap_or(0)
828    }
829    /// Compute the Lp-norm of all data values.
830    #[allow(dead_code)]
831    pub fn data_lp_norm(&self, p: f64) -> f64 {
832        if p <= 0.0 || self.data.is_empty() {
833            return 0.0;
834        }
835        let sum: f64 = self.data.iter().map(|v| v.abs().powf(p)).sum();
836        sum.powf(1.0 / p)
837    }
838    /// Return the maximum absolute value in the field data.
839    #[allow(dead_code)]
840    pub fn max_abs(&self) -> f64 {
841        self.data
842            .iter()
843            .cloned()
844            .fold(0.0_f64, |acc, v| acc.max(v.abs()))
845    }
846    /// Return the minimum value in the field data (or 0 if empty).
847    #[allow(dead_code)]
848    pub fn min_value(&self) -> f64 {
849        self.data.iter().cloned().fold(f64::INFINITY, f64::min)
850    }
851    /// Return the maximum value in the field data (or 0 if empty).
852    #[allow(dead_code)]
853    pub fn max_value(&self) -> f64 {
854        self.data.iter().cloned().fold(f64::NEG_INFINITY, f64::max)
855    }
856}
857/// A time-varying unstructured mesh collection (multi-topology supported across
858/// steps, though a single topology per step is assumed).
859#[allow(dead_code)]
860#[derive(Debug, Clone, Default)]
861pub struct XdmfMeshTimeSeries {
862    /// Ordered list of mesh steps.
863    pub steps: Vec<XdmfMeshStep>,
864}
865#[allow(dead_code)]
866impl XdmfMeshTimeSeries {
867    /// Create an empty mesh time series.
868    pub fn new() -> Self {
869        Self::default()
870    }
871    /// Append one mesh time step.
872    pub fn add_step(&mut self, step: XdmfMeshStep) {
873        self.steps.push(step);
874    }
875    /// Number of steps.
876    pub fn len(&self) -> usize {
877        self.steps.len()
878    }
879    /// Returns `true` if no steps have been added.
880    pub fn is_empty(&self) -> bool {
881        self.steps.is_empty()
882    }
883    /// Return the simulation time values of all steps.
884    pub fn times(&self) -> Vec<f64> {
885        self.steps.iter().map(|s| s.time).collect()
886    }
887    /// Serialize the mesh time series to XDMF XML.
888    pub fn to_xml(&self) -> String {
889        let mut s = String::new();
890        s.push_str("<?xml version=\"1.0\"?>\n");
891        s.push_str("<Xdmf Version=\"3.0\">\n");
892        s.push_str("  <Domain>\n");
893        s.push_str(
894            "    <Grid Name=\"MeshTimeSeries\" GridType=\"Collection\" CollectionType=\"Temporal\">\n",
895        );
896        for step in &self.steps {
897            let n_nodes = step.nodes.len();
898            let n_elements = step.n_elements();
899            let npe = step.topology.nodes_per_element();
900            s.push_str("      <Grid Name=\"mesh\" GridType=\"Uniform\">\n");
901            s.push_str(&format!("        <Time Value=\"{}\"/>\n", step.time));
902            s.push_str(&format!(
903                "        <Topology TopologyType=\"{}\" NumberOfElements=\"{}\">\n",
904                step.topology.xdmf_name(),
905                n_elements
906            ));
907            s.push_str(&format!(
908                "          <DataItem Format=\"XML\" Dimensions=\"{} {}\">\n",
909                n_elements, npe
910            ));
911            for chunk in step.connectivity.chunks(npe) {
912                let row: Vec<String> = chunk.iter().map(|&i| i.to_string()).collect();
913                s.push_str(&format!("            {}\n", row.join(" ")));
914            }
915            s.push_str("          </DataItem>\n");
916            s.push_str("        </Topology>\n");
917            s.push_str("        <Geometry GeometryType=\"XYZ\">\n");
918            s.push_str(&format!(
919                "          <DataItem Format=\"XML\" Dimensions=\"{} 3\">\n",
920                n_nodes
921            ));
922            for p in &step.nodes {
923                s.push_str(&format!("            {} {} {}\n", p[0], p[1], p[2]));
924            }
925            s.push_str("          </DataItem>\n");
926            s.push_str("        </Geometry>\n");
927            for (name, values) in &step.node_scalars {
928                s.push_str(&format!(
929                    "        <Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"Node\">\n",
930                    name
931                ));
932                s.push_str(&format!(
933                    "          <DataItem Format=\"XML\" Dimensions=\"{}\">\n",
934                    values.len()
935                ));
936                s.push_str("            ");
937                for (i, v) in values.iter().enumerate() {
938                    if i > 0 {
939                        s.push(' ');
940                    }
941                    s.push_str(&format!("{}", v));
942                }
943                s.push('\n');
944                s.push_str("          </DataItem>\n");
945                s.push_str("        </Attribute>\n");
946            }
947            for (name, vdata) in &step.node_vectors {
948                s.push_str(&format!(
949                    "        <Attribute Name=\"{}\" AttributeType=\"Vector\" Center=\"Node\">\n",
950                    name
951                ));
952                s.push_str(&format!(
953                    "          <DataItem Format=\"XML\" Dimensions=\"{} 3\">\n",
954                    vdata.len()
955                ));
956                for v in vdata {
957                    s.push_str(&format!("            {} {} {}\n", v[0], v[1], v[2]));
958                }
959                s.push_str("          </DataItem>\n");
960                s.push_str("        </Attribute>\n");
961            }
962            s.push_str("      </Grid>\n");
963        }
964        s.push_str("    </Grid>\n");
965        s.push_str("  </Domain>\n");
966        s.push_str("</Xdmf>\n");
967        s
968    }
969}
970/// An XDMF time series collection.
971#[derive(Debug, Clone)]
972#[allow(dead_code)]
973pub struct XdmfTimeSeries {
974    /// Time steps.
975    pub steps: Vec<XdmfStep>,
976}
977#[allow(dead_code)]
978impl XdmfTimeSeries {
979    /// Create a new empty time series.
980    pub fn new() -> Self {
981        Self { steps: Vec::new() }
982    }
983    /// Add a time step with positions and optional scalar fields.
984    pub fn add_step(
985        &mut self,
986        time: f64,
987        positions: Vec<[f64; 3]>,
988        scalars: Vec<(String, Vec<f64>)>,
989    ) {
990        let n_points = positions.len();
991        self.steps.push(XdmfStep {
992            time,
993            n_points,
994            position_data: positions,
995            scalar_fields: scalars,
996        });
997    }
998    /// Serialize the time series to XDMF XML.
999    pub fn to_xml(&self) -> String {
1000        let mut s = String::new();
1001        s.push_str("<?xml version=\"1.0\"?>\n");
1002        s.push_str("<Xdmf Version=\"3.0\">\n");
1003        s.push_str("  <Domain>\n");
1004        s.push_str(
1005            "    <Grid Name=\"TimeSeries\" GridType=\"Collection\" CollectionType=\"Temporal\">\n",
1006        );
1007        for step in &self.steps {
1008            s.push_str("      <Grid Name=\"particles\" GridType=\"Uniform\">\n");
1009            s.push_str(&format!("        <Time Value=\"{}\"/>\n", step.time));
1010            s.push_str(&format!(
1011                "        <Topology TopologyType=\"Polyvertex\" NumberOfElements=\"{}\"/>\n",
1012                step.n_points
1013            ));
1014            s.push_str("        <Geometry GeometryType=\"XYZ\">\n");
1015            s.push_str(&format!(
1016                "          <DataItem Format=\"XML\" Dimensions=\"{} 3\">\n",
1017                step.n_points
1018            ));
1019            for p in &step.position_data {
1020                s.push_str(&format!("            {} {} {}\n", p[0], p[1], p[2]));
1021            }
1022            s.push_str("          </DataItem>\n");
1023            s.push_str("        </Geometry>\n");
1024            for (name, values) in &step.scalar_fields {
1025                s.push_str(&format!(
1026                    "        <Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"Node\">\n",
1027                    name
1028                ));
1029                s.push_str(&format!(
1030                    "          <DataItem Format=\"XML\" Dimensions=\"{}\">\n",
1031                    values.len()
1032                ));
1033                s.push_str("            ");
1034                for (i, v) in values.iter().enumerate() {
1035                    if i > 0 {
1036                        s.push(' ');
1037                    }
1038                    s.push_str(&format!("{}", v));
1039                }
1040                s.push('\n');
1041                s.push_str("          </DataItem>\n");
1042                s.push_str("        </Attribute>\n");
1043            }
1044            s.push_str("      </Grid>\n");
1045        }
1046        s.push_str("    </Grid>\n");
1047        s.push_str("  </Domain>\n");
1048        s.push_str("</Xdmf>\n");
1049        s
1050    }
1051}
1052impl XdmfTimeSeries {
1053    /// Add a step that includes both scalar and vector fields.
1054    ///
1055    /// `vectors` is a list of `(name, data)` pairs where each data entry is
1056    /// a `[f64; 3]` per node.
1057    #[allow(dead_code)]
1058    pub fn add_step_with_vectors(
1059        &mut self,
1060        time: f64,
1061        positions: Vec<[f64; 3]>,
1062        scalars: Vec<(String, Vec<f64>)>,
1063        vectors: Vec<(String, Vec<[f64; 3]>)>,
1064    ) {
1065        let n_points = positions.len();
1066        let mut all_scalars = scalars;
1067        for (vname, vdata) in vectors {
1068            let mut xs = Vec::with_capacity(n_points);
1069            let mut ys = Vec::with_capacity(n_points);
1070            let mut zs = Vec::with_capacity(n_points);
1071            for v in &vdata {
1072                xs.push(v[0]);
1073                ys.push(v[1]);
1074                zs.push(v[2]);
1075            }
1076            all_scalars.push((format!("{}_x", vname), xs));
1077            all_scalars.push((format!("{}_y", vname), ys));
1078            all_scalars.push((format!("{}_z", vname), zs));
1079        }
1080        self.steps.push(XdmfStep {
1081            time,
1082            n_points,
1083            position_data: positions,
1084            scalar_fields: all_scalars,
1085        });
1086    }
1087    /// Return the time values of all steps.
1088    #[allow(dead_code)]
1089    pub fn times(&self) -> Vec<f64> {
1090        self.steps.iter().map(|s| s.time).collect()
1091    }
1092    /// Return the total number of particles across all steps (sum).
1093    #[allow(dead_code)]
1094    pub fn total_particle_count(&self) -> usize {
1095        self.steps.iter().map(|s| s.n_points).sum()
1096    }
1097}
1098impl XdmfTimeSeries {
1099    /// Add a frame with positions and no scalar fields.
1100    ///
1101    /// This is a convenience alias for [`add_step`](XdmfTimeSeries::add_step)
1102    /// that uses the common `add_frame` naming convention.
1103    #[allow(dead_code)]
1104    pub fn add_frame(&mut self, time: f64, positions: Vec<[f64; 3]>) {
1105        self.add_step(time, positions, vec![]);
1106    }
1107    /// Add a frame with positions and named scalar attributes.
1108    #[allow(dead_code)]
1109    pub fn add_frame_with_scalars(
1110        &mut self,
1111        time: f64,
1112        positions: Vec<[f64; 3]>,
1113        scalars: Vec<(String, Vec<f64>)>,
1114    ) {
1115        self.add_step(time, positions, scalars);
1116    }
1117    /// Write the time series as XDMF XML to any [`Write`] sink.
1118    ///
1119    /// This is the `write_xml` variant of [`to_xml`](XdmfTimeSeries::to_xml)
1120    /// that streams output rather than building a full string first.
1121    #[allow(dead_code)]
1122    pub fn write_xml<W: Write>(&self, writer: &mut W) -> std::io::Result<()> {
1123        let xml = self.to_xml();
1124        writer.write_all(xml.as_bytes())
1125    }
1126    /// Write the time series to a file at `path`.
1127    #[allow(dead_code)]
1128    pub fn write_xml_to_file(&self, path: &str) -> std::io::Result<()> {
1129        let mut f = std::fs::File::create(path)?;
1130        self.write_xml(&mut f)
1131    }
1132}
1133/// A named patch/region inside a larger mesh, identified by element indices.
1134#[derive(Debug, Clone)]
1135#[allow(dead_code)]
1136pub struct XdmfMeshPatch {
1137    /// Patch name (e.g. `"inlet"`, `"wall"`).
1138    pub name: String,
1139    /// Zero-based element indices belonging to this patch.
1140    pub element_ids: Vec<usize>,
1141    /// Optional scalar tag (e.g. boundary condition id).
1142    pub tag: Option<i32>,
1143}
1144impl XdmfMeshPatch {
1145    /// Create a new patch with a name and element list.
1146    #[allow(dead_code)]
1147    pub fn new(name: &str, element_ids: Vec<usize>) -> Self {
1148        XdmfMeshPatch {
1149            name: name.to_string(),
1150            element_ids,
1151            tag: None,
1152        }
1153    }
1154    /// Number of elements in this patch.
1155    #[allow(dead_code)]
1156    pub fn element_count(&self) -> usize {
1157        self.element_ids.len()
1158    }
1159    /// Check if a given element index belongs to this patch.
1160    #[allow(dead_code)]
1161    pub fn contains_element(&self, idx: usize) -> bool {
1162        self.element_ids.contains(&idx)
1163    }
1164    /// Merge another patch's elements into this one (duplicates removed).
1165    #[allow(dead_code)]
1166    pub fn merge(&mut self, other: &XdmfMeshPatch) {
1167        for &id in &other.element_ids {
1168            if !self.element_ids.contains(&id) {
1169                self.element_ids.push(id);
1170            }
1171        }
1172    }
1173    /// Generate a CDL-like string representation for debugging.
1174    #[allow(dead_code)]
1175    pub fn to_debug_string(&self) -> String {
1176        format!(
1177            "patch \"{}\" [{} elements] tag={:?}",
1178            self.name,
1179            self.element_ids.len(),
1180            self.tag
1181        )
1182    }
1183}
1184/// Basic XDMF XML reader.
1185#[allow(dead_code)]
1186pub struct XdmfReader;
1187#[allow(dead_code)]
1188impl XdmfReader {
1189    /// Parse XDMF XML data into a list of time steps.
1190    ///
1191    /// This is a simplified parser that looks for `<Time Value="..."/>` and
1192    /// `NumberOfElements="..."` tags. It does not handle all XDMF features.
1193    pub fn from_xml(data: &str) -> Result<Vec<XdmfStep>, std::io::Error> {
1194        let mut steps = Vec::new();
1195        let mut current_time: Option<f64> = None;
1196        let mut current_n: Option<usize> = None;
1197        for line in data.lines() {
1198            let trimmed = line.trim();
1199            if trimmed.starts_with("<Time")
1200                && let Some(start) = trimmed.find("Value=\"")
1201            {
1202                let rest = &trimmed[start + 7..];
1203                if let Some(end) = rest.find('"')
1204                    && let Ok(t) = rest[..end].parse::<f64>()
1205                {
1206                    current_time = Some(t);
1207                }
1208            }
1209            if trimmed.contains("NumberOfElements=\"")
1210                && let Some(start) = trimmed.find("NumberOfElements=\"")
1211            {
1212                let rest = &trimmed[start + 18..];
1213                if let Some(end) = rest.find('"')
1214                    && let Ok(n) = rest[..end].parse::<usize>()
1215                {
1216                    current_n = Some(n);
1217                }
1218            }
1219            if trimmed == "</Grid>"
1220                && let (Some(t), Some(n)) = (current_time.take(), current_n.take())
1221            {
1222                steps.push(XdmfStep {
1223                    time: t,
1224                    n_points: n,
1225                    position_data: Vec::new(),
1226                    scalar_fields: Vec::new(),
1227                });
1228            }
1229        }
1230        Ok(steps)
1231    }
1232}
1233/// A simple XDMF schema descriptor: expected topology type and attribute names.
1234#[allow(dead_code)]
1235#[derive(Debug, Clone)]
1236pub struct XdmfSchema {
1237    /// Expected topology type string (e.g., `"Polyvertex"`, `"Triangle"`).
1238    pub topology_type: String,
1239    /// Attribute names that must be present.
1240    pub required_attributes: Vec<String>,
1241}
1242impl XdmfSchema {
1243    /// Create a schema.
1244    pub fn new(topology_type: &str, required_attributes: Vec<String>) -> Self {
1245        Self {
1246            topology_type: topology_type.to_owned(),
1247            required_attributes,
1248        }
1249    }
1250    /// Validate an XDMF XML string against this schema.
1251    ///
1252    /// Returns a list of human-readable errors; empty if valid.
1253    pub fn validate(&self, xml: &str) -> Vec<String> {
1254        let mut errors = Vec::new();
1255        if !xml.contains(&format!("TopologyType=\"{}\"", self.topology_type)) {
1256            errors.push(format!("missing TopologyType=\"{}\"", self.topology_type));
1257        }
1258        for attr in &self.required_attributes {
1259            if !xml.contains(&format!("Name=\"{}\"", attr)) {
1260                errors.push(format!("missing Attribute Name=\"{}\"", attr));
1261            }
1262        }
1263        errors
1264    }
1265}