Skip to main content

oxiphysics_io/xdmf/
functions.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5#![allow(clippy::manual_div_ceil)]
6use oxiphysics_core::math::Vec3;
7use std::io::Write;
8
9use super::types::{
10    XdmfAttribute, XdmfFieldDescriptor, XdmfMeshPatch, XdmfMeshTimeSeries, XdmfTopologyType,
11    XdmfUniformGrid,
12};
13
14/// Write an XDMF file for a particle dataset (points + optional scalar fields).
15///
16/// Produces XML like:
17/// ```xml
18/// <?xml version="1.0"?>
19/// <Xdmf Version="3.0">
20///   `Domain`
21///     <Grid Name="particles" GridType="Uniform">
22///       <Topology TopologyType="Polyvertex" NumberOfElements="N"/>
23///       <Geometry GeometryType="XYZ">
24///         <DataItem Format="XML" Dimensions="N 3">...</DataItem>
25///       </Geometry>
26///       <Attribute Name="density" AttributeType="Scalar" Center="Node">
27///         <DataItem Format="XML" Dimensions="N">...</DataItem>
28///       </Attribute>
29///     </Grid>
30///   </Domain>
31/// </Xdmf>
32/// ```
33pub fn write_xdmf_particles<W: Write>(
34    writer: &mut W,
35    positions: &[Vec3],
36    scalar_fields: &[(&str, &[f64])],
37) -> std::io::Result<()> {
38    let n = positions.len();
39    writeln!(writer, "<?xml version=\"1.0\"?>")?;
40    writeln!(writer, "<Xdmf Version=\"3.0\">")?;
41    writeln!(writer, "  <Domain>")?;
42    writeln!(writer, "    <Grid Name=\"particles\" GridType=\"Uniform\">")?;
43    writeln!(
44        writer,
45        "      <Topology TopologyType=\"Polyvertex\" NumberOfElements=\"{}\"/>",
46        n
47    )?;
48    writeln!(writer, "      <Geometry GeometryType=\"XYZ\">")?;
49    writeln!(
50        writer,
51        "        <DataItem Format=\"XML\" Dimensions=\"{} 3\">",
52        n
53    )?;
54    for p in positions {
55        writeln!(writer, "          {} {} {}", p.x, p.y, p.z)?;
56    }
57    writeln!(writer, "        </DataItem>")?;
58    writeln!(writer, "      </Geometry>")?;
59    for (name, values) in scalar_fields {
60        writeln!(
61            writer,
62            "      <Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"Node\">",
63            name
64        )?;
65        writeln!(
66            writer,
67            "        <DataItem Format=\"XML\" Dimensions=\"{}\">",
68            n
69        )?;
70        write!(writer, "          ")?;
71        for (i, v) in values.iter().enumerate() {
72            if i > 0 {
73                write!(writer, " ")?;
74            }
75            write!(writer, "{}", v)?;
76        }
77        writeln!(writer)?;
78        writeln!(writer, "        </DataItem>")?;
79        writeln!(writer, "      </Attribute>")?;
80    }
81    writeln!(writer, "    </Grid>")?;
82    writeln!(writer, "  </Domain>")?;
83    writeln!(writer, "</Xdmf>")?;
84    Ok(())
85}
86/// Write a multi-timestep XDMF temporal collection.
87///
88/// Each entry in `timesteps` is `(time, positions)`. The result is a
89/// `<Grid GridType="Collection" CollectionType="Temporal">` containing one
90/// child grid per timestep.
91pub fn write_xdmf_temporal<W: Write>(
92    writer: &mut W,
93    timesteps: &[(f64, &[Vec3])],
94) -> std::io::Result<()> {
95    writeln!(writer, "<?xml version=\"1.0\"?>")?;
96    writeln!(writer, "<Xdmf Version=\"3.0\">")?;
97    writeln!(writer, "  <Domain>")?;
98    writeln!(
99        writer,
100        "    <Grid Name=\"TimeSeries\" GridType=\"Collection\" CollectionType=\"Temporal\">"
101    )?;
102    for (time, positions) in timesteps {
103        let n = positions.len();
104        writeln!(
105            writer,
106            "      <Grid Name=\"particles\" GridType=\"Uniform\">"
107        )?;
108        writeln!(writer, "        <Time Value=\"{}\"/>", time)?;
109        writeln!(
110            writer,
111            "        <Topology TopologyType=\"Polyvertex\" NumberOfElements=\"{}\"/>",
112            n
113        )?;
114        writeln!(writer, "        <Geometry GeometryType=\"XYZ\">")?;
115        writeln!(
116            writer,
117            "          <DataItem Format=\"XML\" Dimensions=\"{} 3\">",
118            n
119        )?;
120        for p in *positions {
121            writeln!(writer, "            {} {} {}", p.x, p.y, p.z)?;
122        }
123        writeln!(writer, "          </DataItem>")?;
124        writeln!(writer, "        </Geometry>")?;
125        writeln!(writer, "      </Grid>")?;
126    }
127    writeln!(writer, "    </Grid>")?;
128    writeln!(writer, "  </Domain>")?;
129    writeln!(writer, "</Xdmf>")?;
130    Ok(())
131}
132/// Write an XDMF file referencing HDF5 geometry and attributes.
133#[allow(dead_code)]
134pub fn write_xdmf_with_attributes(
135    path: &str,
136    hdf5_path: &str,
137    n_nodes: usize,
138    n_elements: usize,
139    topology: &str,
140    attributes: &[XdmfAttribute],
141) -> std::io::Result<()> {
142    let mut f = std::fs::File::create(path)?;
143    writeln!(f, "<?xml version=\"1.0\"?>")?;
144    writeln!(f, "<Xdmf Version=\"3.0\">")?;
145    writeln!(f, "  <Domain>")?;
146    writeln!(f, "    <Grid Name=\"mesh\" GridType=\"Uniform\">")?;
147    writeln!(
148        f,
149        "      <Topology TopologyType=\"{}\" NumberOfElements=\"{}\"/>",
150        topology, n_elements
151    )?;
152    writeln!(f, "      <Geometry GeometryType=\"XYZ\">")?;
153    writeln!(
154        f,
155        "        <DataItem Format=\"HDF\" Dimensions=\"{} 3\">{}:/coordinates</DataItem>",
156        n_nodes, hdf5_path
157    )?;
158    writeln!(f, "      </Geometry>")?;
159    for attr in attributes {
160        let attr_type = if attr.n_components == 1 {
161            "Scalar"
162        } else {
163            "Vector"
164        };
165        let dims = if attr.n_components == 1 {
166            format!("{}", n_nodes)
167        } else {
168            format!("{} {}", n_nodes, attr.n_components)
169        };
170        writeln!(
171            f,
172            "      <Attribute Name=\"{}\" AttributeType=\"{}\" Center=\"{}\">",
173            attr.name, attr_type, attr.center
174        )?;
175        writeln!(
176            f,
177            "        <DataItem Format=\"HDF\" Dimensions=\"{}\">{}</DataItem>",
178            dims, attr.hdf5_path
179        )?;
180        writeln!(f, "      </Attribute>")?;
181    }
182    writeln!(f, "    </Grid>")?;
183    writeln!(f, "  </Domain>")?;
184    writeln!(f, "</Xdmf>")?;
185    Ok(())
186}
187/// Parse topology information from XDMF XML content.
188///
189/// Returns a list of `(topology_type, n_elements)` pairs found in the document.
190#[allow(dead_code)]
191pub fn parse_xdmf_topology(content: &str) -> Vec<(String, usize)> {
192    let mut result = Vec::new();
193    for line in content.lines() {
194        let trimmed = line.trim();
195        if !trimmed.contains("Topology") {
196            continue;
197        }
198        let topo_type = if let Some(start) = trimmed.find("TopologyType=\"") {
199            let rest = &trimmed[start + 14..];
200            rest.find('"').map(|end| rest[..end].to_string())
201        } else {
202            None
203        };
204        let n_elements = if let Some(start) = trimmed.find("NumberOfElements=\"") {
205            let rest = &trimmed[start + 18..];
206            rest.find('"')
207                .and_then(|end| rest[..end].parse::<usize>().ok())
208        } else {
209            None
210        };
211        if let (Some(t), Some(n)) = (topo_type, n_elements) {
212            result.push((t, n));
213        }
214    }
215    result
216}
217/// Write a structured uniform-grid XDMF file.
218#[allow(dead_code)]
219pub fn write_xdmf_uniform_grid(path: &str, params: &XdmfUniformGrid) -> std::io::Result<()> {
220    let [nx, ny, nz] = params.dimensions;
221    let [ox, oy, oz] = params.origin;
222    let [dx, dy, dz] = params.spacing;
223    let mut f = std::fs::File::create(path)?;
224    writeln!(f, "<?xml version=\"1.0\"?>")?;
225    writeln!(f, "<Xdmf Version=\"3.0\">")?;
226    writeln!(f, "  <Domain>")?;
227    writeln!(
228        f,
229        "    <Grid Name=\"{}\" GridType=\"Uniform\">",
230        params.name
231    )?;
232    writeln!(
233        f,
234        "      <Topology TopologyType=\"3DCoRectMesh\" Dimensions=\"{} {} {}\"/>",
235        nz, ny, nx
236    )?;
237    writeln!(f, "      <Geometry GeometryType=\"ORIGIN_DXDYDZ\">")?;
238    writeln!(
239        f,
240        "        <DataItem Format=\"XML\" Dimensions=\"3\">{} {} {}</DataItem>",
241        oz, oy, ox
242    )?;
243    writeln!(
244        f,
245        "        <DataItem Format=\"XML\" Dimensions=\"3\">{} {} {}</DataItem>",
246        dz, dy, dx
247    )?;
248    writeln!(f, "      </Geometry>")?;
249    writeln!(f, "    </Grid>")?;
250    writeln!(f, "  </Domain>")?;
251    writeln!(f, "</Xdmf>")?;
252    Ok(())
253}
254/// Generate an XDMF ``DataItem` element referencing an HDF5 file.
255#[allow(dead_code)]
256pub fn write_xdmf_hdf5_reference(filename: &str, dataset_path: &str) -> String {
257    format!(
258        "<DataItem Format=\"HDF\" Dimensions=\"1\">\n  {}:{}\n</DataItem>",
259        filename, dataset_path
260    )
261}
262/// Write a vector attribute block (e.g., velocity) into an XDMF XML string.
263///
264/// Produces:
265/// ```xml
266/// <Attribute Name="velocity" AttributeType="Vector" Center="Node">
267///   <DataItem Format="XML" Dimensions="N 3">
268///     vx vy vz
269///     ...
270///   </DataItem>
271/// </Attribute>
272/// ```
273#[allow(dead_code)]
274pub fn xdmf_vector_attribute(name: &str, vectors: &[[f64; 3]]) -> String {
275    let n = vectors.len();
276    let mut s = String::new();
277    s.push_str(&format!(
278        "      <Attribute Name=\"{}\" AttributeType=\"Vector\" Center=\"Node\">\n",
279        name
280    ));
281    s.push_str(&format!(
282        "        <DataItem Format=\"XML\" Dimensions=\"{} 3\">\n",
283        n
284    ));
285    for v in vectors {
286        s.push_str(&format!("          {} {} {}\n", v[0], v[1], v[2]));
287    }
288    s.push_str("        </DataItem>\n");
289    s.push_str("      </Attribute>\n");
290    s
291}
292/// Write a symmetric 3×3 tensor attribute block (e.g., stress tensor).
293///
294/// Each tensor is stored as 6 unique components: `\[xx, yy, zz, xy, xz, yz\]`.
295/// XDMF AttributeType is `"Tensor6"`.
296#[allow(dead_code)]
297pub fn xdmf_tensor6_attribute(name: &str, tensors: &[[f64; 6]]) -> String {
298    let n = tensors.len();
299    let mut s = String::new();
300    s.push_str(&format!(
301        "      <Attribute Name=\"{}\" AttributeType=\"Tensor6\" Center=\"Node\">\n",
302        name
303    ));
304    s.push_str(&format!(
305        "        <DataItem Format=\"XML\" Dimensions=\"{} 6\">\n",
306        n
307    ));
308    for t in tensors {
309        s.push_str(&format!(
310            "          {} {} {} {} {} {}\n",
311            t[0], t[1], t[2], t[3], t[4], t[5]
312        ));
313    }
314    s.push_str("        </DataItem>\n");
315    s.push_str("      </Attribute>\n");
316    s
317}
318/// Write an unstructured mesh XDMF grid to a `Write` target.
319///
320/// `nodes` are 3-D point coordinates; `connectivity` is a flat list of
321/// node indices, grouped by element (length must be a multiple of
322/// `topo.nodes_per_element()`).
323#[allow(dead_code)]
324pub fn write_xdmf_unstructured<W: Write>(
325    writer: &mut W,
326    topo: XdmfTopologyType,
327    nodes: &[[f64; 3]],
328    connectivity: &[usize],
329    scalar_fields: &[(&str, &[f64])],
330) -> std::io::Result<()> {
331    let n_nodes = nodes.len();
332    let npe = topo.nodes_per_element();
333    let n_elements = connectivity.len().checked_div(npe).unwrap_or(0);
334    writeln!(writer, "<?xml version=\"1.0\"?>")?;
335    writeln!(writer, "<Xdmf Version=\"3.0\">")?;
336    writeln!(writer, "  <Domain>")?;
337    writeln!(writer, "    <Grid Name=\"mesh\" GridType=\"Uniform\">")?;
338    writeln!(
339        writer,
340        "      <Topology TopologyType=\"{}\" NumberOfElements=\"{}\">",
341        topo.xdmf_name(),
342        n_elements
343    )?;
344    writeln!(
345        writer,
346        "        <DataItem Format=\"XML\" Dimensions=\"{} {}\">",
347        n_elements, npe
348    )?;
349    for chunk in connectivity.chunks(npe) {
350        let row: Vec<String> = chunk.iter().map(|&i| i.to_string()).collect();
351        writeln!(writer, "          {}", row.join(" "))?;
352    }
353    writeln!(writer, "        </DataItem>")?;
354    writeln!(writer, "      </Topology>")?;
355    writeln!(writer, "      <Geometry GeometryType=\"XYZ\">")?;
356    writeln!(
357        writer,
358        "        <DataItem Format=\"XML\" Dimensions=\"{} 3\">",
359        n_nodes
360    )?;
361    for p in nodes {
362        writeln!(writer, "          {} {} {}", p[0], p[1], p[2])?;
363    }
364    writeln!(writer, "        </DataItem>")?;
365    writeln!(writer, "      </Geometry>")?;
366    for (name, values) in scalar_fields {
367        writeln!(
368            writer,
369            "      <Attribute Name=\"{}\" AttributeType=\"Scalar\" Center=\"Node\">",
370            name
371        )?;
372        writeln!(
373            writer,
374            "        <DataItem Format=\"XML\" Dimensions=\"{}\">",
375            values.len()
376        )?;
377        write!(writer, "          ")?;
378        for (i, v) in values.iter().enumerate() {
379            if i > 0 {
380                write!(writer, " ")?;
381            }
382            write!(writer, "{}", v)?;
383        }
384        writeln!(writer)?;
385        writeln!(writer, "        </DataItem>")?;
386        writeln!(writer, "      </Attribute>")?;
387    }
388    writeln!(writer, "    </Grid>")?;
389    writeln!(writer, "  </Domain>")?;
390    writeln!(writer, "</Xdmf>")?;
391    Ok(())
392}
393/// Produce a scalar XDMF ``DataItem` XML string (inline, Format="XML").
394///
395/// Useful for embedding small fields directly in the XDMF document.
396#[allow(dead_code)]
397pub fn xdmf_scalar_data_item(values: &[f64]) -> String {
398    let mut s = format!(
399        "<DataItem Format=\"XML\" Dimensions=\"{}\">\n  ",
400        values.len()
401    );
402    for (i, v) in values.iter().enumerate() {
403        if i > 0 {
404            s.push(' ');
405        }
406        s.push_str(&format!("{}", v));
407    }
408    s.push_str("\n</DataItem>");
409    s
410}
411/// Produce a 3-component vector XDMF ``DataItem` XML string.
412#[allow(dead_code)]
413pub fn xdmf_vector_data_item(vectors: &[[f64; 3]]) -> String {
414    let n = vectors.len();
415    let mut s = format!("<DataItem Format=\"XML\" Dimensions=\"{} 3\">\n", n);
416    for v in vectors {
417        s.push_str(&format!("  {} {} {}\n", v[0], v[1], v[2]));
418    }
419    s.push_str("</DataItem>");
420    s
421}
422/// Generate a ``Time` element for a temporal grid.
423#[allow(dead_code)]
424pub fn xdmf_time_element(t: f64) -> String {
425    format!("<Time Value=\"{}\"/>", t)
426}
427/// Compute the total node count across all steps in a mesh time series.
428#[allow(dead_code)]
429pub fn total_node_count(series: &XdmfMeshTimeSeries) -> usize {
430    series.steps.iter().map(|s| s.nodes.len()).sum()
431}
432/// Return the step index with the most elements (peak mesh density).
433#[allow(dead_code)]
434pub fn peak_element_step(series: &XdmfMeshTimeSeries) -> Option<usize> {
435    series
436        .steps
437        .iter()
438        .enumerate()
439        .max_by_key(|(_, s)| s.n_elements())
440        .map(|(i, _)| i)
441}
442#[cfg(test)]
443mod tests {
444    use super::*;
445    use crate::xdmf::types::*;
446    fn make_positions(n: usize) -> Vec<Vec3> {
447        (0..n)
448            .map(|i| Vec3::new(i as f64, i as f64 * 0.5, 0.0))
449            .collect()
450    }
451    #[test]
452    fn test_xdmf_valid_xml() {
453        let positions = make_positions(3);
454        let mut buf = Vec::new();
455        write_xdmf_particles(&mut buf, &positions, &[]).unwrap();
456        let s = String::from_utf8(buf).unwrap();
457        assert!(s.contains("<?xml"), "missing XML declaration");
458        assert!(s.contains("<Xdmf"), "missing Xdmf root element");
459        assert!(s.contains("<Grid"), "missing Grid element");
460    }
461    #[test]
462    fn test_xdmf_correct_particle_count() {
463        let positions = make_positions(5);
464        let mut buf = Vec::new();
465        write_xdmf_particles(&mut buf, &positions, &[]).unwrap();
466        let s = String::from_utf8(buf).unwrap();
467        assert!(
468            s.contains("NumberOfElements=\"5\""),
469            "expected NumberOfElements=\"5\" in output, got:\n{}",
470            s
471        );
472    }
473    #[test]
474    fn test_xdmf_scalar_field_included() {
475        let positions = make_positions(4);
476        let density = vec![1.0, 2.0, 3.0, 4.0];
477        let fields: &[(&str, &[f64])] = &[("density", &density)];
478        let mut buf = Vec::new();
479        write_xdmf_particles(&mut buf, &positions, fields).unwrap();
480        let s = String::from_utf8(buf).unwrap();
481        assert!(
482            s.contains("density"),
483            "scalar field name 'density' not found in output"
484        );
485        assert!(s.contains("Scalar"), "AttributeType Scalar not found");
486    }
487    #[test]
488    fn test_xdmf_temporal_multiple_steps() {
489        let pos0 = make_positions(3);
490        let pos1 = make_positions(3);
491        let steps: &[(f64, &[Vec3])] = &[(0.0, &pos0), (1.0, &pos1)];
492        let mut buf = Vec::new();
493        write_xdmf_temporal(&mut buf, steps).unwrap();
494        let s = String::from_utf8(buf).unwrap();
495        assert!(s.contains("Temporal"), "expected CollectionType Temporal");
496        assert!(
497            s.contains("Time Value=\"0\"") || s.contains("Time Value=\"0."),
498            "time step 0 not found"
499        );
500        assert!(
501            s.contains("Time Value=\"1\"") || s.contains("Time Value=\"1."),
502            "time step 1 not found"
503        );
504    }
505    #[test]
506    fn test_xdmf_empty_positions() {
507        let positions: Vec<Vec3> = vec![];
508        let mut buf = Vec::new();
509        write_xdmf_particles(&mut buf, &positions, &[]).unwrap();
510        let s = String::from_utf8(buf).unwrap();
511        assert!(s.contains("NumberOfElements=\"0\""));
512    }
513    #[test]
514    fn test_time_series_add_step() {
515        let mut ts = XdmfTimeSeries::new();
516        ts.add_step(0.0, vec![[0.0, 0.0, 0.0]], vec![]);
517        ts.add_step(1.0, vec![[1.0, 0.0, 0.0]], vec![]);
518        assert_eq!(ts.steps.len(), 2);
519        assert!((ts.steps[0].time - 0.0).abs() < 1e-10);
520        assert!((ts.steps[1].time - 1.0).abs() < 1e-10);
521    }
522    #[test]
523    fn test_time_series_to_xml() {
524        let mut ts = XdmfTimeSeries::new();
525        ts.add_step(
526            0.5,
527            vec![[1.0, 2.0, 3.0]],
528            vec![("density".to_string(), vec![1.5])],
529        );
530        let xml = ts.to_xml();
531        assert!(xml.contains("Temporal"));
532        assert!(xml.contains("Time Value=\"0.5\""));
533        assert!(xml.contains("density"));
534        assert!(xml.contains("1 2 3"));
535    }
536    #[test]
537    fn test_time_series_multiple_scalars() {
538        let mut ts = XdmfTimeSeries::new();
539        ts.add_step(
540            0.0,
541            vec![[0.0; 3]],
542            vec![
543                ("temperature".to_string(), vec![300.0]),
544                ("pressure".to_string(), vec![101325.0]),
545            ],
546        );
547        let xml = ts.to_xml();
548        assert!(xml.contains("temperature"));
549        assert!(xml.contains("pressure"));
550    }
551    #[test]
552    fn test_xdmf_reader_from_xml() {
553        let mut ts = XdmfTimeSeries::new();
554        ts.add_step(0.0, vec![[0.0; 3]; 3], vec![]);
555        ts.add_step(1.0, vec![[1.0; 3]; 5], vec![]);
556        let xml = ts.to_xml();
557        let steps = XdmfReader::from_xml(&xml).unwrap();
558        assert_eq!(steps.len(), 2);
559        assert!((steps[0].time - 0.0).abs() < 1e-10);
560        assert_eq!(steps[0].n_points, 3);
561        assert!((steps[1].time - 1.0).abs() < 1e-10);
562        assert_eq!(steps[1].n_points, 5);
563    }
564    #[test]
565    fn test_xdmf_reader_empty_xml() {
566        let steps = XdmfReader::from_xml("").unwrap();
567        assert!(steps.is_empty());
568    }
569    #[test]
570    fn test_xdmf_hdf5_reference() {
571        let ref_str = write_xdmf_hdf5_reference("data.h5", "/positions");
572        assert!(ref_str.contains("data.h5:/positions"));
573        assert!(ref_str.contains("HDF"));
574    }
575    #[test]
576    fn test_time_series_empty() {
577        let ts = XdmfTimeSeries::new();
578        let xml = ts.to_xml();
579        assert!(xml.contains("Temporal"));
580        assert!(xml.contains("<Xdmf"));
581        assert!(xml.contains("</Xdmf>"));
582    }
583    #[test]
584    fn test_xdmf_step_n_points() {
585        let mut ts = XdmfTimeSeries::new();
586        let positions: Vec<[f64; 3]> = (0..10).map(|i| [i as f64, 0.0, 0.0]).collect();
587        ts.add_step(0.0, positions, vec![]);
588        assert_eq!(ts.steps[0].n_points, 10);
589    }
590    #[test]
591    fn test_vector_attribute_basic() {
592        let vecs = vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
593        let s = xdmf_vector_attribute("velocity", &vecs);
594        assert!(s.contains("velocity"));
595        assert!(s.contains("Vector"));
596        assert!(s.contains("2 3"));
597        assert!(s.contains("1 0 0"));
598    }
599    #[test]
600    fn test_vector_attribute_empty() {
601        let s = xdmf_vector_attribute("v", &[]);
602        assert!(s.contains("0 3"));
603    }
604    #[test]
605    fn test_tensor6_attribute_basic() {
606        let tensors = vec![[1.0, 2.0, 3.0, 0.5, 0.1, 0.2]];
607        let s = xdmf_tensor6_attribute("stress", &tensors);
608        assert!(s.contains("stress"));
609        assert!(s.contains("Tensor6"));
610        assert!(s.contains("1 6"));
611        assert!(s.contains("0.5"));
612    }
613    #[test]
614    fn test_unstructured_triangle_mesh() {
615        let nodes = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]];
616        let connectivity = vec![0usize, 1, 2];
617        let mut buf = Vec::new();
618        write_xdmf_unstructured(
619            &mut buf,
620            XdmfTopologyType::Triangle,
621            &nodes,
622            &connectivity,
623            &[],
624        )
625        .unwrap();
626        let s = String::from_utf8(buf).unwrap();
627        assert!(s.contains("Triangle"), "topology type not found");
628        assert!(s.contains("NumberOfElements=\"1\""));
629        assert!(s.contains("0 1 2"));
630    }
631    #[test]
632    fn test_unstructured_tet_mesh() {
633        let nodes: Vec<[f64; 3]> = (0..4).map(|i| [i as f64, 0.0, 0.0]).collect();
634        let connectivity = vec![0usize, 1, 2, 3];
635        let mut buf = Vec::new();
636        write_xdmf_unstructured(
637            &mut buf,
638            XdmfTopologyType::Tetrahedron,
639            &nodes,
640            &connectivity,
641            &[],
642        )
643        .unwrap();
644        let s = String::from_utf8(buf).unwrap();
645        assert!(s.contains("Tetrahedron"));
646        assert!(s.contains("NumberOfElements=\"1\""));
647    }
648    #[test]
649    fn test_unstructured_with_scalar_field() {
650        let nodes = vec![[0.0; 3]; 3];
651        let connectivity = vec![0usize, 1, 2];
652        let pressure = vec![1.0, 2.0, 3.0];
653        let mut buf = Vec::new();
654        write_xdmf_unstructured(
655            &mut buf,
656            XdmfTopologyType::Triangle,
657            &nodes,
658            &connectivity,
659            &[("pressure", &pressure)],
660        )
661        .unwrap();
662        let s = String::from_utf8(buf).unwrap();
663        assert!(s.contains("pressure"));
664    }
665    #[test]
666    fn topology_type_nodes_per_element() {
667        assert_eq!(XdmfTopologyType::Triangle.nodes_per_element(), 3);
668        assert_eq!(XdmfTopologyType::Tetrahedron.nodes_per_element(), 4);
669        assert_eq!(XdmfTopologyType::Hexahedron.nodes_per_element(), 8);
670        assert_eq!(XdmfTopologyType::Quadrilateral.nodes_per_element(), 4);
671    }
672    #[test]
673    fn schema_validate_ok() {
674        let schema = XdmfSchema::new("Polyvertex", vec!["density".to_string()]);
675        let positions = make_positions(2);
676        let density = vec![1.0, 2.0];
677        let fields: &[(&str, &[f64])] = &[("density", &density)];
678        let mut buf = Vec::new();
679        write_xdmf_particles(&mut buf, &positions, fields).unwrap();
680        let xml = String::from_utf8(buf).unwrap();
681        let errors = schema.validate(&xml);
682        assert!(errors.is_empty(), "unexpected errors: {errors:?}");
683    }
684    #[test]
685    fn schema_validate_missing_topology() {
686        let schema = XdmfSchema::new("Triangle", vec![]);
687        let positions = make_positions(3);
688        let mut buf = Vec::new();
689        write_xdmf_particles(&mut buf, &positions, &[]).unwrap();
690        let xml = String::from_utf8(buf).unwrap();
691        let errors = schema.validate(&xml);
692        assert!(
693            !errors.is_empty(),
694            "should report missing topology Triangle"
695        );
696    }
697    #[test]
698    fn schema_validate_missing_attribute() {
699        let schema = XdmfSchema::new("Polyvertex", vec!["velocity".to_string()]);
700        let positions = make_positions(2);
701        let mut buf = Vec::new();
702        write_xdmf_particles(&mut buf, &positions, &[]).unwrap();
703        let xml = String::from_utf8(buf).unwrap();
704        let errors = schema.validate(&xml);
705        assert!(!errors.is_empty());
706    }
707    #[test]
708    fn time_series_vector_step_encodes_xyz() {
709        let mut ts = XdmfTimeSeries::new();
710        ts.add_step_with_vectors(
711            0.0,
712            vec![[0.0; 3]],
713            vec![],
714            vec![("velocity".to_string(), vec![[1.0, 2.0, 3.0]])],
715        );
716        let xml = ts.to_xml();
717        assert!(xml.contains("velocity_x"), "should have velocity_x field");
718        assert!(xml.contains("velocity_y"));
719        assert!(xml.contains("velocity_z"));
720    }
721    #[test]
722    fn time_series_times() {
723        let mut ts = XdmfTimeSeries::new();
724        ts.add_step(0.0, vec![], vec![]);
725        ts.add_step(0.5, vec![], vec![]);
726        ts.add_step(1.0, vec![], vec![]);
727        assert_eq!(ts.times(), vec![0.0, 0.5, 1.0]);
728    }
729    #[test]
730    fn time_series_total_particle_count() {
731        let mut ts = XdmfTimeSeries::new();
732        ts.add_step(0.0, vec![[0.0; 3]; 5], vec![]);
733        ts.add_step(1.0, vec![[0.0; 3]; 3], vec![]);
734        assert_eq!(ts.total_particle_count(), 8);
735    }
736    #[test]
737    fn time_series_default() {
738        let ts = XdmfTimeSeries::default();
739        assert!(ts.steps.is_empty());
740    }
741    fn make_tri_step(time: f64) -> XdmfMeshStep {
742        XdmfMeshStep {
743            time,
744            nodes: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
745            connectivity: vec![0, 1, 2],
746            topology: XdmfTopologyType::Triangle,
747            node_scalars: vec![],
748            node_vectors: vec![],
749        }
750    }
751    #[test]
752    fn mesh_series_add_and_len() {
753        let mut ms = XdmfMeshTimeSeries::new();
754        ms.add_step(make_tri_step(0.0));
755        ms.add_step(make_tri_step(1.0));
756        assert_eq!(ms.len(), 2);
757        assert!(!ms.is_empty());
758    }
759    #[test]
760    fn mesh_series_to_xml_contains_temporal() {
761        let mut ms = XdmfMeshTimeSeries::new();
762        ms.add_step(make_tri_step(0.0));
763        let xml = ms.to_xml();
764        assert!(xml.contains("Temporal"));
765        assert!(xml.contains("Triangle"));
766        assert!(xml.contains("0 1 2"));
767    }
768    #[test]
769    fn mesh_series_scalar_field_in_xml() {
770        let mut step = make_tri_step(0.0);
771        step.node_scalars
772            .push(("temperature".into(), vec![300.0, 310.0, 305.0]));
773        let mut ms = XdmfMeshTimeSeries::new();
774        ms.add_step(step);
775        let xml = ms.to_xml();
776        assert!(xml.contains("temperature"));
777        assert!(xml.contains("310"));
778    }
779    #[test]
780    fn mesh_series_vector_field_in_xml() {
781        let mut step = make_tri_step(0.0);
782        step.node_vectors.push((
783            "velocity".into(),
784            vec![[1.0, 0.0, 0.0], [0.0, 1.0, 0.0], [0.0, 0.0, 1.0]],
785        ));
786        let mut ms = XdmfMeshTimeSeries::new();
787        ms.add_step(step);
788        let xml = ms.to_xml();
789        assert!(xml.contains("velocity"));
790        assert!(xml.contains("Vector"));
791    }
792    #[test]
793    fn mesh_series_times() {
794        let mut ms = XdmfMeshTimeSeries::new();
795        ms.add_step(make_tri_step(0.0));
796        ms.add_step(make_tri_step(0.5));
797        ms.add_step(make_tri_step(1.0));
798        assert_eq!(ms.times(), vec![0.0, 0.5, 1.0]);
799    }
800    #[test]
801    fn mesh_step_n_elements() {
802        let step = make_tri_step(0.0);
803        assert_eq!(step.n_elements(), 1);
804    }
805    #[test]
806    fn hdf5_builder_basic() {
807        let b = Hdf5DataItemBuilder::new("data.h5");
808        let xml = b.build("positions", "100 3");
809        assert!(xml.contains("data.h5:/positions"));
810        assert!(xml.contains("100 3"));
811        assert!(xml.contains("HDF"));
812    }
813    #[test]
814    fn hdf5_builder_with_group() {
815        let b = Hdf5DataItemBuilder::new("sim.h5").group("/step0");
816        let xml = b.build("velocity", "50 3");
817        assert!(xml.contains("sim.h5:/step0/velocity"));
818    }
819    #[test]
820    fn scalar_data_item_format() {
821        let item = xdmf_scalar_data_item(&[1.0, 2.0, 3.0]);
822        assert!(item.contains("Dimensions=\"3\""));
823        assert!(item.contains("1 2 3"));
824    }
825    #[test]
826    fn vector_data_item_format() {
827        let item = xdmf_vector_data_item(&[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
828        assert!(item.contains("2 3"));
829        assert!(item.contains("1 0 0"));
830    }
831    #[test]
832    fn time_element_format() {
833        let t = xdmf_time_element(3.125);
834        assert!(t.contains("3.125"));
835        assert!(t.contains("<Time"));
836    }
837    #[test]
838    fn total_node_count_basic() {
839        let mut ms = XdmfMeshTimeSeries::new();
840        ms.add_step(make_tri_step(0.0));
841        ms.add_step(make_tri_step(1.0));
842        assert_eq!(total_node_count(&ms), 6);
843    }
844    #[test]
845    fn peak_element_step_basic() {
846        let mut ms = XdmfMeshTimeSeries::new();
847        ms.add_step(make_tri_step(0.0));
848        ms.add_step(XdmfMeshStep {
849            time: 1.0,
850            nodes: vec![[0.0; 3]; 4],
851            connectivity: vec![0, 1, 2, 1, 2, 3],
852            topology: XdmfTopologyType::Triangle,
853            node_scalars: vec![],
854            node_vectors: vec![],
855        });
856        assert_eq!(peak_element_step(&ms), Some(1));
857    }
858    #[test]
859    fn peak_element_step_empty_series() {
860        let ms = XdmfMeshTimeSeries::new();
861        assert_eq!(peak_element_step(&ms), None);
862    }
863    #[test]
864    fn test_write_collection_creates_file_with_timestep_count() {
865        let mut ts = XdmfTimeSeriesHdf5::new();
866        ts.timesteps = vec![0.0, 1.0, 2.0];
867        ts.hdf5_paths = vec!["a.h5".into(), "b.h5".into(), "c.h5".into()];
868        ts.attribute_names = vec!["temperature".into()];
869        let path = "/tmp/test_write_collection.xmf";
870        ts.write_collection(path, 10, 5, "Triangle").unwrap();
871        let content = std::fs::read_to_string(path).unwrap();
872        let count = content.matches("<Time Value=").count();
873        assert_eq!(count, 3, "expected 3 timestep entries, got {}", count);
874        assert!(content.contains("CollectionType=\"Temporal\""));
875        assert!(content.contains("temperature"));
876    }
877    #[test]
878    fn test_parse_xdmf_topology_finds_topology() {
879        let xml = r#"<?xml version="1.0"?>
880<Xdmf Version="3.0">
881  <Domain>
882    <Grid Name="mesh" GridType="Uniform">
883      <Topology TopologyType="Triangle" NumberOfElements="42"/>
884    </Grid>
885  </Domain>
886</Xdmf>"#;
887        let topologies = parse_xdmf_topology(xml);
888        assert_eq!(topologies.len(), 1);
889        assert_eq!(topologies[0].0, "Triangle");
890        assert_eq!(topologies[0].1, 42);
891    }
892    #[test]
893    fn test_write_xdmf_uniform_grid_contains_grid_tag() {
894        let params = XdmfUniformGrid {
895            name: "volume".to_string(),
896            dimensions: [4, 5, 6],
897            origin: [0.0, 0.0, 0.0],
898            spacing: [1.0, 1.0, 1.0],
899        };
900        let path = "/tmp/test_uniform_grid.xmf";
901        write_xdmf_uniform_grid(path, &params).unwrap();
902        let content = std::fs::read_to_string(path).unwrap();
903        assert!(content.contains("<Grid"), "missing Grid tag");
904        assert!(content.contains("volume"), "missing grid name");
905        assert!(content.contains("3DCoRectMesh"), "missing topology type");
906        assert!(content.contains("ORIGIN_DXDYDZ"), "missing geometry type");
907    }
908    #[test]
909    fn test_write_xdmf_with_attributes_creates_valid_file() {
910        let attrs = vec![XdmfAttribute {
911            name: "pressure".to_string(),
912            center: "Node".to_string(),
913            n_components: 1,
914            hdf5_path: "data.h5:/pressure".to_string(),
915        }];
916        let path = "/tmp/test_xdmf_with_attrs.xmf";
917        write_xdmf_with_attributes(path, "data.h5", 20, 10, "Tetrahedron", &attrs).unwrap();
918        let content = std::fs::read_to_string(path).unwrap();
919        assert!(content.contains("pressure"));
920        assert!(content.contains("Tetrahedron"));
921        assert!(content.contains("data.h5:/coordinates"));
922    }
923}
924#[cfg(test)]
925mod tests_xdmf_ext {
926
927    use crate::xdmf::types::*;
928    #[test]
929    fn add_frame_increments_step_count() {
930        let mut ts = XdmfTimeSeries::new();
931        ts.add_frame(0.0, vec![[0.0; 3]; 5]);
932        ts.add_frame(1.0, vec![[1.0; 3]; 5]);
933        assert_eq!(ts.steps.len(), 2);
934    }
935    #[test]
936    fn add_frame_sets_n_points() {
937        let mut ts = XdmfTimeSeries::new();
938        ts.add_frame(0.0, vec![[0.0; 3]; 7]);
939        assert_eq!(ts.steps[0].n_points, 7);
940    }
941    #[test]
942    fn add_frame_with_scalars_persists_data() {
943        let mut ts = XdmfTimeSeries::new();
944        ts.add_frame_with_scalars(
945            0.5,
946            vec![[0.0; 3]; 3],
947            vec![("pressure".to_string(), vec![1.0, 2.0, 3.0])],
948        );
949        assert_eq!(ts.steps[0].scalar_fields.len(), 1);
950        assert_eq!(ts.steps[0].scalar_fields[0].0, "pressure");
951    }
952    #[test]
953    fn write_xml_produces_valid_xml_string() {
954        let mut ts = XdmfTimeSeries::new();
955        ts.add_frame(0.0, vec![[1.0, 2.0, 3.0]]);
956        let mut buf: Vec<u8> = Vec::new();
957        ts.write_xml(&mut buf).unwrap();
958        let s = String::from_utf8(buf).unwrap();
959        assert!(s.contains("<?xml"));
960        assert!(s.contains("Temporal"));
961        assert!(s.contains("Time Value=\"0\"") || s.contains("Time Value=\"0."));
962    }
963    #[test]
964    fn write_xml_matches_to_xml() {
965        let mut ts = XdmfTimeSeries::new();
966        ts.add_frame(1.5, vec![[0.0; 3]; 4]);
967        let expected = ts.to_xml();
968        let mut buf: Vec<u8> = Vec::new();
969        ts.write_xml(&mut buf).unwrap();
970        let actual = String::from_utf8(buf).unwrap();
971        assert_eq!(actual, expected);
972    }
973    #[test]
974    fn write_xml_to_file_creates_file() {
975        let mut ts = XdmfTimeSeries::new();
976        ts.add_frame(0.0, vec![[0.0; 3]; 2]);
977        let path = "/tmp/test_write_xml_ext.xmf";
978        ts.write_xml_to_file(path).unwrap();
979        let content = std::fs::read_to_string(path).unwrap();
980        assert!(content.contains("<Xdmf"));
981    }
982    #[test]
983    fn mixed_topology_xdmf_name() {
984        assert_eq!(XdmfTopologyType::Mixed.xdmf_name(), "Mixed");
985    }
986    #[test]
987    fn mixed_topology_nodes_per_element_zero() {
988        assert_eq!(XdmfTopologyType::Mixed.nodes_per_element(), 0);
989    }
990    #[test]
991    fn topology_type_names_cover_all_variants() {
992        let types = [
993            XdmfTopologyType::Triangle,
994            XdmfTopologyType::Tetrahedron,
995            XdmfTopologyType::Hexahedron,
996            XdmfTopologyType::Quadrilateral,
997            XdmfTopologyType::Mixed,
998        ];
999        let names: Vec<&str> = types.iter().map(|t| t.xdmf_name()).collect();
1000        assert!(names.contains(&"Triangle"));
1001        assert!(names.contains(&"Tetrahedron"));
1002        assert!(names.contains(&"Hexahedron"));
1003        assert!(names.contains(&"Quadrilateral"));
1004        assert!(names.contains(&"Mixed"));
1005    }
1006    #[test]
1007    fn add_frame_empty_positions() {
1008        let mut ts = XdmfTimeSeries::new();
1009        ts.add_frame(0.0, vec![]);
1010        assert_eq!(ts.steps[0].n_points, 0);
1011        let xml = ts.to_xml();
1012        assert!(xml.contains("<Xdmf"));
1013    }
1014    #[test]
1015    fn write_xml_multiple_frames_all_present() {
1016        let mut ts = XdmfTimeSeries::new();
1017        for i in 0..4 {
1018            ts.add_frame(i as f64, vec![[0.0; 3]; 2]);
1019        }
1020        let mut buf: Vec<u8> = Vec::new();
1021        ts.write_xml(&mut buf).unwrap();
1022        let s = String::from_utf8(buf).unwrap();
1023        let count = s.matches("<Time Value=").count();
1024        assert_eq!(count, 4);
1025    }
1026}
1027/// Write a full 3×3 tensor attribute (9 components per node).
1028///
1029/// XDMF represents this as `AttributeType="Tensor"` with `Dimensions="N 9"`.
1030#[allow(dead_code)]
1031pub fn xdmf_tensor9_attribute(name: &str, tensors: &[[f64; 9]]) -> String {
1032    let n = tensors.len();
1033    let mut s = String::new();
1034    s.push_str(&format!(
1035        "      <Attribute Name=\"{}\" AttributeType=\"Tensor\" Center=\"Node\">\n",
1036        name
1037    ));
1038    s.push_str(&format!(
1039        "        <DataItem Format=\"XML\" Dimensions=\"{} 9\">\n",
1040        n
1041    ));
1042    for t in tensors {
1043        s.push_str("          ");
1044        for (i, c) in t.iter().enumerate() {
1045            if i > 0 {
1046                s.push(' ');
1047            }
1048            s.push_str(&format!("{}", c));
1049        }
1050        s.push('\n');
1051    }
1052    s.push_str("        </DataItem>\n");
1053    s.push_str("      </Attribute>\n");
1054    s
1055}
1056/// Validate that an XDMF string contains required version and domain tags.
1057#[allow(dead_code)]
1058pub fn xdmf_is_well_formed(xml: &str) -> bool {
1059    xml.contains("<Xdmf")
1060        && xml.contains("Version=")
1061        && xml.contains("<Domain>")
1062        && xml.contains("</Domain>")
1063        && xml.contains("</Xdmf>")
1064}
1065/// Count the number of `<Grid` elements in an XDMF document.
1066#[allow(dead_code)]
1067pub fn xdmf_count_grids(xml: &str) -> usize {
1068    xml.matches("<Grid").count()
1069}
1070/// Count the number of `<Attribute` elements in an XDMF document.
1071#[allow(dead_code)]
1072pub fn xdmf_count_attributes(xml: &str) -> usize {
1073    xml.matches("<Attribute").count()
1074}
1075#[cfg(test)]
1076mod tests_xdmf_new {
1077    use super::*;
1078    use crate::xdmf::Hdf5DataItemBuilder;
1079
1080    use crate::xdmf::XdmfMeshStep;
1081    use crate::xdmf::XdmfMeshTimeSeries;
1082    use crate::xdmf::XdmfMultiBlock;
1083
1084    use crate::xdmf::XdmfStructuredGrid;
1085
1086    use crate::xdmf::XdmfTimeSeriesHdf5;
1087    use crate::xdmf::XdmfWriter;
1088
1089    use crate::xdmf::total_node_count;
1090    use crate::xdmf::types::*;
1091
1092    use crate::xdmf::xdmf_is_well_formed;
1093    use crate::xdmf::xdmf_scalar_data_item;
1094    use crate::xdmf::xdmf_time_element;
1095    use crate::xdmf::xdmf_vector_data_item;
1096    #[test]
1097    fn structured_grid_node_count() {
1098        let g = XdmfStructuredGrid::new("test", 4, 5, 6, [0.0; 3], 1.0, 1.0, 1.0);
1099        assert_eq!(g.n_nodes(), 4 * 5 * 6);
1100    }
1101    #[test]
1102    fn structured_grid_cell_count() {
1103        let g = XdmfStructuredGrid::new("test", 4, 5, 6, [0.0; 3], 1.0, 1.0, 1.0);
1104        assert_eq!(g.n_cells(), 3 * 4 * 5);
1105    }
1106    #[test]
1107    fn structured_grid_to_xml_contains_topology() {
1108        let g = XdmfStructuredGrid::new("flow", 3, 3, 3, [0.0; 3], 0.5, 0.5, 0.5);
1109        let xml = g.to_xml();
1110        assert!(xml.contains("3DCoRectMesh"));
1111        assert!(xml.contains("ORIGIN_DXDYDZ"));
1112        assert!(xml.contains("flow"));
1113    }
1114    #[test]
1115    fn structured_grid_scalar_in_xml() {
1116        let mut g = XdmfStructuredGrid::new("g", 2, 2, 2, [0.0; 3], 1.0, 1.0, 1.0);
1117        let n = g.n_nodes();
1118        g.add_node_scalar("pressure", vec![1.0; n]);
1119        let xml = g.to_xml();
1120        assert!(xml.contains("pressure"));
1121        assert!(xml.contains("Center=\"Node\""));
1122    }
1123    #[test]
1124    fn structured_grid_cell_scalar_in_xml() {
1125        let mut g = XdmfStructuredGrid::new("g", 3, 3, 3, [0.0; 3], 1.0, 1.0, 1.0);
1126        let nc = g.n_cells();
1127        g.add_cell_scalar("vorticity", vec![0.5; nc]);
1128        let xml = g.to_xml();
1129        assert!(xml.contains("vorticity"));
1130        assert!(xml.contains("Center=\"Cell\""));
1131    }
1132    #[test]
1133    fn structured_grid_vector_in_xml() {
1134        let mut g = XdmfStructuredGrid::new("g", 2, 2, 2, [0.0; 3], 1.0, 1.0, 1.0);
1135        let n = g.n_nodes();
1136        g.add_node_vector("velocity", vec![[1.0, 0.0, 0.0]; n]);
1137        let xml = g.to_xml();
1138        assert!(xml.contains("velocity"));
1139        assert!(xml.contains("AttributeType=\"Vector\""));
1140    }
1141    #[test]
1142    fn structured_grid_origin_appears_in_geometry() {
1143        let g = XdmfStructuredGrid::new("g", 2, 2, 2, [1.0, 2.0, 3.0], 0.1, 0.2, 0.3);
1144        let xml = g.to_xml();
1145        assert!(xml.contains("3") && xml.contains("ORIGIN_DXDYDZ"));
1146    }
1147    #[test]
1148    fn xdmf_writer_basic_document() {
1149        let mut w = XdmfWriter::new();
1150        w.open_domain();
1151        w.open_grid("particles", "Uniform");
1152        w.write_polyvertex_topology(5);
1153        w.write_xyz_geometry(&[[0.0, 0.0, 0.0]; 5]);
1154        w.close_grid();
1155        w.close_domain();
1156        let xml = w.finish();
1157        assert!(xml.contains("<?xml"));
1158        assert!(xml.contains("<Xdmf"));
1159        assert!(xml.contains("Polyvertex"));
1160        assert!(xml.contains("particles"));
1161        assert!(xml.contains("</Xdmf>"));
1162    }
1163    #[test]
1164    fn xdmf_writer_temporal_collection() {
1165        let mut w = XdmfWriter::new();
1166        w.open_domain();
1167        w.open_temporal_collection("ts");
1168        for i in 0..3_usize {
1169            w.open_grid("step", "Uniform");
1170            w.write_time(i as f64 * 0.1);
1171            w.write_polyvertex_topology(2);
1172            w.write_xyz_geometry(&[[0.0; 3]; 2]);
1173            w.close_grid();
1174        }
1175        w.close_grid();
1176        w.close_domain();
1177        let xml = w.finish();
1178        let count = xml.matches("<Time Value=").count();
1179        assert_eq!(count, 3);
1180        assert!(xml.contains("CollectionType=\"Temporal\""));
1181    }
1182    #[test]
1183    fn xdmf_writer_scalar_attribute() {
1184        let mut w = XdmfWriter::new();
1185        w.open_domain();
1186        w.open_grid("g", "Uniform");
1187        w.write_polyvertex_topology(3);
1188        w.write_xyz_geometry(&[[0.0; 3]; 3]);
1189        w.write_scalar_attribute("density", "Node", &[1.0, 2.0, 3.0]);
1190        w.close_grid();
1191        w.close_domain();
1192        let xml = w.finish();
1193        assert!(xml.contains("density"));
1194        assert!(xml.contains("Scalar"));
1195    }
1196    #[test]
1197    fn xdmf_writer_vector_attribute() {
1198        let mut w = XdmfWriter::new();
1199        w.open_domain();
1200        w.open_grid("g", "Uniform");
1201        w.write_polyvertex_topology(2);
1202        w.write_xyz_geometry(&[[0.0; 3]; 2]);
1203        w.write_vector_attribute("velocity", "Node", &[[1.0, 0.0, 0.0], [0.0, 1.0, 0.0]]);
1204        w.close_grid();
1205        w.close_domain();
1206        let xml = w.finish();
1207        assert!(xml.contains("velocity"));
1208        assert!(xml.contains("Vector"));
1209    }
1210    #[test]
1211    fn xdmf_writer_hdf5_attribute() {
1212        let mut w = XdmfWriter::new();
1213        w.open_domain();
1214        w.open_grid("g", "Uniform");
1215        w.write_polyvertex_topology(10);
1216        w.write_hdf5_attribute("temp", "Node", "Scalar", "10", "data.h5:/temperature");
1217        w.close_grid();
1218        w.close_domain();
1219        let xml = w.finish();
1220        assert!(xml.contains("HDF"));
1221        assert!(xml.contains("temperature"));
1222    }
1223    #[test]
1224    fn xdmf_writer_peek() {
1225        let mut w = XdmfWriter::new();
1226        w.open_domain();
1227        let preview = w.peek().to_string();
1228        assert!(preview.contains("<Domain>"));
1229    }
1230    #[test]
1231    fn tensor9_attribute_has_correct_dimensions() {
1232        let tensors = vec![[1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0f64]; 3];
1233        let xml = xdmf_tensor9_attribute("stress", &tensors);
1234        assert!(xml.contains("Tensor"));
1235        assert!(xml.contains("3 9"));
1236        assert!(xml.contains("stress"));
1237    }
1238    #[test]
1239    fn tensor9_attribute_empty() {
1240        let xml = xdmf_tensor9_attribute("empty_tensor", &[]);
1241        assert!(xml.contains("0 9"));
1242    }
1243    #[test]
1244    fn well_formed_valid_document() {
1245        let xml =
1246            "<?xml version=\"1.0\"?>\n<Xdmf Version=\"3.0\">\n  <Domain>\n  </Domain>\n</Xdmf>\n";
1247        assert!(xdmf_is_well_formed(xml));
1248    }
1249    #[test]
1250    fn well_formed_rejects_truncated() {
1251        let xml = "<Xdmf Version=\"3.0\">\n  <Domain>\n";
1252        assert!(!xdmf_is_well_formed(xml));
1253    }
1254    #[test]
1255    fn count_grids_correct() {
1256        let mut w = XdmfWriter::new();
1257        w.open_domain();
1258        w.open_temporal_collection("ts");
1259        for _ in 0..3_usize {
1260            w.open_grid("g", "Uniform");
1261            w.close_grid();
1262        }
1263        w.close_grid();
1264        w.close_domain();
1265        let xml = w.finish();
1266        assert_eq!(xdmf_count_grids(&xml), 4);
1267    }
1268    #[test]
1269    fn count_attributes_correct() {
1270        let mut w = XdmfWriter::new();
1271        w.open_domain();
1272        w.open_grid("g", "Uniform");
1273        w.write_polyvertex_topology(2);
1274        w.write_xyz_geometry(&[[0.0; 3]; 2]);
1275        w.write_scalar_attribute("a", "Node", &[1.0, 2.0]);
1276        w.write_scalar_attribute("b", "Node", &[3.0, 4.0]);
1277        w.write_vector_attribute("v", "Node", &[[0.0; 3]; 2]);
1278        w.close_grid();
1279        w.close_domain();
1280        let xml = w.finish();
1281        assert_eq!(xdmf_count_attributes(&xml), 3);
1282    }
1283    #[test]
1284    fn multiblock_empty() {
1285        let mb = XdmfMultiBlock::new();
1286        assert!(mb.is_empty());
1287        assert_eq!(mb.len(), 0);
1288    }
1289    #[test]
1290    fn multiblock_spatial_collection() {
1291        let mut mb = XdmfMultiBlock::new();
1292        mb.add_block("block0", "<Grid Name=\"b0\" GridType=\"Uniform\"></Grid>");
1293        mb.add_block("block1", "<Grid Name=\"b1\" GridType=\"Uniform\"></Grid>");
1294        assert_eq!(mb.len(), 2);
1295        let xml = mb.to_xml();
1296        assert!(xml.contains("Spatial"));
1297        assert!(xml.contains("b0"));
1298        assert!(xml.contains("b1"));
1299    }
1300    #[test]
1301    fn multiblock_to_xml_well_formed() {
1302        let mut mb = XdmfMultiBlock::new();
1303        mb.add_block("g", "<Grid Name=\"g\" GridType=\"Uniform\"></Grid>");
1304        let xml = mb.to_xml();
1305        assert!(xdmf_is_well_formed(&xml));
1306    }
1307    #[test]
1308    fn hdf5_time_series_attribute_names() {
1309        let mut ts = XdmfTimeSeriesHdf5::new();
1310        ts.timesteps = vec![0.0, 1.0];
1311        ts.hdf5_paths = vec!["step0.h5".into(), "step1.h5".into()];
1312        ts.attribute_names = vec!["density".into(), "pressure".into()];
1313        let path = "/tmp/test_hdf5_series_attrs.xmf";
1314        ts.write_collection(path, 100, 50, "Tetrahedron").unwrap();
1315        let content = std::fs::read_to_string(path).unwrap();
1316        assert!(content.contains("density"));
1317        assert!(content.contains("pressure"));
1318        let time_count = content.matches("<Time Value=").count();
1319        assert_eq!(time_count, 2);
1320    }
1321    #[test]
1322    fn hdf5_data_item_builder_group() {
1323        let b = Hdf5DataItemBuilder::new("sim.h5").group("/results");
1324        let item = b.build("velocity", "100 3");
1325        assert!(item.contains("sim.h5:/results/velocity"));
1326        assert!(item.contains("100 3"));
1327    }
1328    #[test]
1329    fn hdf5_data_item_builder_root_group() {
1330        let b = Hdf5DataItemBuilder::new("data.h5");
1331        let item = b.build("coordinates", "50 3");
1332        assert!(item.contains("data.h5:/coordinates"));
1333    }
1334    #[test]
1335    fn write_xdmf_unstructured_tetra() {
1336        let nodes: Vec<[f64; 3]> = vec![
1337            [0.0, 0.0, 0.0],
1338            [1.0, 0.0, 0.0],
1339            [0.0, 1.0, 0.0],
1340            [0.0, 0.0, 1.0],
1341        ];
1342        let connectivity = vec![0, 1, 2, 3];
1343        let mut buf = Vec::new();
1344        write_xdmf_unstructured(
1345            &mut buf,
1346            XdmfTopologyType::Tetrahedron,
1347            &nodes,
1348            &connectivity,
1349            &[],
1350        )
1351        .unwrap();
1352        let s = String::from_utf8(buf).unwrap();
1353        assert!(s.contains("Tetrahedron"));
1354        assert!(s.contains("NumberOfElements=\"1\""));
1355    }
1356    #[test]
1357    fn write_xdmf_unstructured_hex() {
1358        let nodes = vec![[0.0f64; 3]; 8];
1359        let connectivity: Vec<usize> = (0..8).collect();
1360        let mut buf = Vec::new();
1361        write_xdmf_unstructured(
1362            &mut buf,
1363            XdmfTopologyType::Hexahedron,
1364            &nodes,
1365            &connectivity,
1366            &[],
1367        )
1368        .unwrap();
1369        let s = String::from_utf8(buf).unwrap();
1370        assert!(s.contains("Hexahedron"));
1371        assert!(s.contains("NumberOfElements=\"1\""));
1372    }
1373    #[test]
1374    fn xdmf_scalar_data_item_values_present() {
1375        let item = xdmf_scalar_data_item(&[1.0, 2.0, 3.0]);
1376        assert!(item.contains("Dimensions=\"3\""));
1377        assert!(item.contains("1"));
1378        assert!(item.contains("2"));
1379        assert!(item.contains("3"));
1380    }
1381    #[test]
1382    fn xdmf_vector_data_item_dimensions() {
1383        let item = xdmf_vector_data_item(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
1384        assert!(item.contains("2 3"));
1385        assert!(item.contains("1 2 3"));
1386        assert!(item.contains("4 5 6"));
1387    }
1388    #[test]
1389    fn xdmf_time_element_format() {
1390        let t = xdmf_time_element(1.5);
1391        assert_eq!(t, "<Time Value=\"1.5\"/>");
1392    }
1393    #[test]
1394    fn total_node_count_multi_step() {
1395        let mut ms = XdmfMeshTimeSeries::new();
1396        ms.add_step(XdmfMeshStep {
1397            time: 0.0,
1398            nodes: vec![[0.0; 3]; 4],
1399            connectivity: vec![0, 1, 2],
1400            topology: XdmfTopologyType::Triangle,
1401            node_scalars: vec![],
1402            node_vectors: vec![],
1403        });
1404        ms.add_step(XdmfMeshStep {
1405            time: 1.0,
1406            nodes: vec![[0.0; 3]; 6],
1407            connectivity: vec![0, 1, 2, 3, 4, 5],
1408            topology: XdmfTopologyType::Triangle,
1409            node_scalars: vec![],
1410            node_vectors: vec![],
1411        });
1412        assert_eq!(total_node_count(&ms), 10);
1413    }
1414    #[test]
1415    fn mesh_time_series_to_xml_well_formed() {
1416        let mut ms = XdmfMeshTimeSeries::new();
1417        ms.add_step(XdmfMeshStep {
1418            time: 0.5,
1419            nodes: vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
1420            connectivity: vec![0, 1, 2],
1421            topology: XdmfTopologyType::Triangle,
1422            node_scalars: vec![("T".to_string(), vec![300.0, 310.0, 320.0])],
1423            node_vectors: vec![("v".to_string(), vec![[1.0, 0.0, 0.0]; 3])],
1424        });
1425        let xml = ms.to_xml();
1426        assert!(xdmf_is_well_formed(&xml));
1427        assert!(xml.contains("MeshTimeSeries"));
1428        assert!(xml.contains("Time Value=\"0.5\""));
1429        assert!(xml.contains("T"));
1430        assert!(xml.contains("v"));
1431    }
1432}
1433#[cfg(test)]
1434#[allow(dead_code)]
1435mod tests_xdmf_extra {
1436    use super::*;
1437    use crate::xdmf::types::*;
1438    #[test]
1439    fn time_series_default_is_empty() {
1440        let ts: XdmfTimeSeries = Default::default();
1441        assert!(ts.steps.is_empty());
1442    }
1443    #[test]
1444    fn time_series_times_single_step() {
1445        let mut ts = XdmfTimeSeries::new();
1446        ts.add_step(2.5, vec![[0.0; 3]; 3], vec![]);
1447        let t = ts.times();
1448        assert_eq!(t.len(), 1);
1449        assert!((t[0] - 2.5).abs() < 1e-12);
1450    }
1451    #[test]
1452    fn time_series_times_multiple_steps() {
1453        let mut ts = XdmfTimeSeries::new();
1454        ts.add_step(0.0, vec![[0.0; 3]; 2], vec![]);
1455        ts.add_step(1.0, vec![[0.0; 3]; 2], vec![]);
1456        ts.add_step(2.0, vec![[0.0; 3]; 2], vec![]);
1457        let t = ts.times();
1458        assert_eq!(t, vec![0.0, 1.0, 2.0]);
1459    }
1460    #[test]
1461    fn time_series_total_particle_count_empty() {
1462        let ts = XdmfTimeSeries::new();
1463        assert_eq!(ts.total_particle_count(), 0);
1464    }
1465    #[test]
1466    fn time_series_total_particle_count_multi() {
1467        let mut ts = XdmfTimeSeries::new();
1468        ts.add_step(0.0, vec![[0.0; 3]; 4], vec![]);
1469        ts.add_step(1.0, vec![[0.0; 3]; 6], vec![]);
1470        assert_eq!(ts.total_particle_count(), 10);
1471    }
1472    #[test]
1473    fn time_series_add_step_with_vectors() {
1474        let mut ts = XdmfTimeSeries::new();
1475        ts.add_step_with_vectors(
1476            0.1,
1477            vec![[0.0; 3]; 2],
1478            vec![],
1479            vec![("vel".to_string(), vec![[1.0, 0.0, 0.0]; 2])],
1480        );
1481        assert_eq!(ts.steps.len(), 1);
1482    }
1483    #[test]
1484    fn time_series_to_xml_contains_positions() {
1485        let mut ts = XdmfTimeSeries::new();
1486        ts.add_step(0.0, vec![[1.0, 2.0, 3.0]], vec![]);
1487        let xml = ts.to_xml();
1488        assert!(xml.contains("1 2 3") || xml.contains("1.0 2.0 3.0") || xml.contains("1"));
1489        assert!(xml.contains("GeometryType=\"XYZ\""));
1490    }
1491    #[test]
1492    fn time_series_to_xml_contains_scalar_field() {
1493        let mut ts = XdmfTimeSeries::new();
1494        ts.add_step(
1495            0.0,
1496            vec![[0.0; 3]; 2],
1497            vec![("temperature".to_string(), vec![300.0, 301.0])],
1498        );
1499        let xml = ts.to_xml();
1500        assert!(xml.contains("temperature"));
1501    }
1502    #[test]
1503    fn topology_type_triangle_xdmf_name() {
1504        assert_eq!(XdmfTopologyType::Triangle.xdmf_name(), "Triangle");
1505    }
1506    #[test]
1507    fn topology_type_tet_nodes_per_element() {
1508        assert_eq!(XdmfTopologyType::Tetrahedron.nodes_per_element(), 4);
1509    }
1510    #[test]
1511    fn topology_type_quad_nodes_per_element() {
1512        assert_eq!(XdmfTopologyType::Quadrilateral.nodes_per_element(), 4);
1513    }
1514    #[test]
1515    fn topology_type_hex_nodes_per_element() {
1516        assert_eq!(XdmfTopologyType::Hexahedron.nodes_per_element(), 8);
1517    }
1518    #[test]
1519    fn topology_type_mixed_name() {
1520        assert_eq!(XdmfTopologyType::Mixed.xdmf_name(), "Mixed");
1521    }
1522    #[test]
1523    fn mesh_time_series_empty_is_empty() {
1524        let ms = XdmfMeshTimeSeries::new();
1525        assert!(ms.is_empty());
1526        assert_eq!(ms.len(), 0);
1527    }
1528    #[test]
1529    fn mesh_time_series_len_after_add() {
1530        let mut ms = XdmfMeshTimeSeries::new();
1531        ms.add_step(XdmfMeshStep {
1532            time: 0.0,
1533            nodes: vec![[0.0; 3]; 3],
1534            connectivity: vec![0, 1, 2],
1535            topology: XdmfTopologyType::Triangle,
1536            node_scalars: vec![],
1537            node_vectors: vec![],
1538        });
1539        assert_eq!(ms.len(), 1);
1540        assert!(!ms.is_empty());
1541    }
1542    #[test]
1543    fn mesh_time_series_times_list() {
1544        let mut ms = XdmfMeshTimeSeries::new();
1545        for i in 0..3 {
1546            ms.add_step(XdmfMeshStep {
1547                time: i as f64 * 0.5,
1548                nodes: vec![[0.0; 3]; 3],
1549                connectivity: vec![0, 1, 2],
1550                topology: XdmfTopologyType::Triangle,
1551                node_scalars: vec![],
1552                node_vectors: vec![],
1553            });
1554        }
1555        assert_eq!(ms.times(), vec![0.0, 0.5, 1.0]);
1556    }
1557    #[test]
1558    fn mesh_step_n_elements_triangle() {
1559        let step = XdmfMeshStep {
1560            time: 0.0,
1561            nodes: vec![[0.0; 3]; 3],
1562            connectivity: vec![0, 1, 2],
1563            topology: XdmfTopologyType::Triangle,
1564            node_scalars: vec![],
1565            node_vectors: vec![],
1566        };
1567        assert_eq!(step.n_elements(), 1);
1568    }
1569    #[test]
1570    fn mesh_step_n_elements_hex() {
1571        let step = XdmfMeshStep {
1572            time: 0.0,
1573            nodes: vec![[0.0; 3]; 8],
1574            connectivity: vec![0, 1, 2, 3, 4, 5, 6, 7],
1575            topology: XdmfTopologyType::Hexahedron,
1576            node_scalars: vec![],
1577            node_vectors: vec![],
1578        };
1579        assert_eq!(step.n_elements(), 1);
1580    }
1581    #[test]
1582    fn hdf5_builder_no_group() {
1583        let b = Hdf5DataItemBuilder::new("data.h5");
1584        let item = b.build("coordinates", "100 3");
1585        assert!(item.contains("data.h5:/coordinates"));
1586        assert!(item.contains("100 3"));
1587    }
1588    #[test]
1589    fn hdf5_builder_with_group() {
1590        let b = Hdf5DataItemBuilder::new("out.h5").group("step_0");
1591        let item = b.build("positions", "50 3");
1592        assert!(item.contains("step_0") && item.contains("positions"));
1593        assert!(item.contains("out.h5"));
1594    }
1595    #[test]
1596    fn hdf5_builder_dimensions_in_output() {
1597        let b = Hdf5DataItemBuilder::new("f.h5");
1598        let item = b.build("vel", "200 3");
1599        assert!(item.contains("200 3"));
1600        assert!(item.contains("HDF"));
1601    }
1602    #[test]
1603    fn scalar_data_item_empty() {
1604        let item = xdmf_scalar_data_item(&[]);
1605        assert!(item.contains("0"));
1606    }
1607    #[test]
1608    fn scalar_data_item_single_value() {
1609        let item = xdmf_scalar_data_item(&[42.0]);
1610        assert!(item.contains("42"));
1611        assert!(item.contains("Dimensions=\"1\""));
1612    }
1613    #[test]
1614    fn vector_data_item_two_rows() {
1615        let item = xdmf_vector_data_item(&[[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]]);
1616        assert!(item.contains("2 3"));
1617        assert!(item.contains("Vector") || item.contains("XML"));
1618    }
1619    #[test]
1620    fn xdmf_schema_validate_ok() {
1621        let schema = XdmfSchema::new("Triangle", vec!["pressure".to_string()]);
1622        let xml = "<Topology TopologyType=\"Triangle\"/><Attribute Name=\"pressure\"/>";
1623        let errs = schema.validate(xml);
1624        assert!(errs.is_empty(), "unexpected errors: {:?}", errs);
1625    }
1626    #[test]
1627    fn xdmf_schema_validate_missing_attr() {
1628        let schema = XdmfSchema::new("Triangle", vec!["velocity".to_string()]);
1629        let xml = "<Topology TopologyType=\"Triangle\"/>";
1630        let errs = schema.validate(xml);
1631        assert!(!errs.is_empty());
1632    }
1633    #[test]
1634    fn xdmf_schema_validate_wrong_topology() {
1635        let schema = XdmfSchema::new("Hexahedron", vec![]);
1636        let xml = "<Topology TopologyType=\"Triangle\"/>";
1637        let errs = schema.validate(xml);
1638        assert!(!errs.is_empty());
1639    }
1640    #[test]
1641    fn xdmf_writer_new_starts_with_xml_declaration() {
1642        let w = XdmfWriter::new();
1643        let out = w.finish();
1644        assert!(out.starts_with("<?xml"));
1645        assert!(out.contains("<Xdmf Version=\"3.0\">"));
1646    }
1647    #[test]
1648    fn xdmf_writer_domain_round_trip() {
1649        let mut w = XdmfWriter::new();
1650        w.open_domain();
1651        w.close_domain();
1652        let out = w.finish();
1653        assert!(out.contains("<Domain>"));
1654        assert!(out.contains("</Domain>"));
1655    }
1656    #[test]
1657    fn xdmf_writer_grid_elements() {
1658        let mut w = XdmfWriter::new();
1659        w.open_domain();
1660        w.open_grid("particles", "Uniform");
1661        w.write_time(3.125);
1662        w.write_polyvertex_topology(100);
1663        w.close_grid();
1664        w.close_domain();
1665        let out = w.finish();
1666        assert!(out.contains("particles"));
1667        assert!(out.contains("3.125"));
1668        assert!(out.contains("100"));
1669    }
1670    #[test]
1671    fn xdmf_writer_temporal_collection() {
1672        let mut w = XdmfWriter::new();
1673        w.open_domain();
1674        w.open_temporal_collection("ts");
1675        w.close_grid();
1676        w.close_domain();
1677        let out = w.finish();
1678        assert!(out.contains("CollectionType=\"Temporal\""));
1679    }
1680    #[test]
1681    fn structured_grid_single_cell_size() {
1682        let g = XdmfStructuredGrid::new("g", 2, 2, 2, [0.0; 3], 1.0, 1.0, 1.0);
1683        assert_eq!(g.n_cells(), 1);
1684    }
1685    #[test]
1686    fn structured_grid_1d_degenerate_cells() {
1687        let g = XdmfStructuredGrid::new("g", 1, 5, 5, [0.0; 3], 1.0, 1.0, 1.0);
1688        assert_eq!(g.n_cells(), 0);
1689    }
1690    #[test]
1691    fn structured_grid_xml_well_formed() {
1692        let g = XdmfStructuredGrid::new("box", 3, 3, 3, [0.0; 3], 0.5, 0.5, 0.5);
1693        let xml = g.to_xml();
1694        assert!(xdmf_is_well_formed(&xml));
1695    }
1696    #[test]
1697    fn total_node_count_empty_series() {
1698        let ms = XdmfMeshTimeSeries::new();
1699        assert_eq!(total_node_count(&ms), 0);
1700    }
1701    #[test]
1702    fn peak_element_step_empty_returns_none() {
1703        let ms = XdmfMeshTimeSeries::new();
1704        assert!(peak_element_step(&ms).is_none());
1705    }
1706    #[test]
1707    fn peak_element_step_single_step() {
1708        let mut ms = XdmfMeshTimeSeries::new();
1709        ms.add_step(XdmfMeshStep {
1710            time: 0.0,
1711            nodes: vec![[0.0; 3]; 4],
1712            connectivity: vec![0, 1, 2, 3],
1713            topology: XdmfTopologyType::Quadrilateral,
1714            node_scalars: vec![],
1715            node_vectors: vec![],
1716        });
1717        assert_eq!(peak_element_step(&ms), Some(0));
1718    }
1719    #[test]
1720    fn hdf5_series_default_empty() {
1721        let s: XdmfTimeSeriesHdf5 = Default::default();
1722        assert!(s.timesteps.is_empty());
1723        assert!(s.hdf5_paths.is_empty());
1724    }
1725    #[test]
1726    fn hdf5_series_stores_attributes() {
1727        let mut s = XdmfTimeSeriesHdf5::new();
1728        s.timesteps.push(0.0);
1729        s.hdf5_paths.push("step0.h5".to_string());
1730        s.attribute_names.push("density".to_string());
1731        assert_eq!(s.attribute_names[0], "density");
1732    }
1733    #[test]
1734    fn write_xdmf_with_attributes_basic() {
1735        let attrs = vec![XdmfAttribute {
1736            name: "pressure".to_string(),
1737            center: "Node".to_string(),
1738            n_components: 1,
1739            hdf5_path: "data.h5:/pressure".to_string(),
1740        }];
1741        let path = "/tmp/test_xdmf_attrs_extra.xmf";
1742        write_xdmf_with_attributes(path, "data.h5", 5, 2, "Triangle", &attrs).unwrap();
1743        let s = std::fs::read_to_string(path).unwrap();
1744        assert!(s.contains("pressure"));
1745        assert!(s.contains("Scalar"));
1746    }
1747    #[test]
1748    fn parse_xdmf_topology_triangle() {
1749        let xml = "<Topology TopologyType=\"Triangle\" NumberOfElements=\"10\"/>";
1750        let result = parse_xdmf_topology(xml);
1751        assert_eq!(result.len(), 1);
1752        assert_eq!(result[0].0, "Triangle");
1753        assert_eq!(result[0].1, 10);
1754    }
1755    #[test]
1756    fn parse_xdmf_topology_multiple() {
1757        let xml = "<Topology TopologyType=\"Triangle\" NumberOfElements=\"4\"/>\n\
1758                   <Topology TopologyType=\"Hexahedron\" NumberOfElements=\"2\"/>";
1759        let result = parse_xdmf_topology(xml);
1760        assert_eq!(result.len(), 2);
1761    }
1762    #[test]
1763    fn parse_xdmf_topology_empty_input() {
1764        let result = parse_xdmf_topology("");
1765        assert!(result.is_empty());
1766    }
1767    #[test]
1768    fn xdmf_time_element_zero() {
1769        let t = xdmf_time_element(0.0);
1770        assert_eq!(t, "<Time Value=\"0\"/>");
1771    }
1772    #[test]
1773    fn xdmf_time_element_negative() {
1774        let t = xdmf_time_element(-1.0);
1775        assert!(t.contains("-1"));
1776    }
1777}
1778/// Write an XDMF XML block for one time step with multiple fields.
1779///
1780/// All fields are embedded inline (`Format="XML"`).
1781#[allow(dead_code)]
1782pub fn write_xdmf_timestep_fields<W: std::io::Write>(
1783    writer: &mut W,
1784    time: f64,
1785    n_nodes: usize,
1786    topology_type: &str,
1787    n_elements: usize,
1788    nodes: &[[f64; 3]],
1789    fields: &[XdmfFieldDescriptor],
1790) -> std::io::Result<()> {
1791    writeln!(writer, "  <Grid Name=\"timestep\" GridType=\"Uniform\">")?;
1792    writeln!(writer, "    <Time Value=\"{time}\"/>")?;
1793    writeln!(
1794        writer,
1795        "    <Topology TopologyType=\"{topology_type}\" NumberOfElements=\"{n_elements}\"/>"
1796    )?;
1797    writeln!(writer, "    <Geometry GeometryType=\"XYZ\">")?;
1798    writeln!(
1799        writer,
1800        "      <DataItem Format=\"XML\" Dimensions=\"{n_nodes} 3\">"
1801    )?;
1802    for n in nodes.iter().take(n_nodes) {
1803        writeln!(writer, "        {} {} {}", n[0], n[1], n[2])?;
1804    }
1805    writeln!(writer, "      </DataItem>")?;
1806    writeln!(writer, "    </Geometry>")?;
1807    for field in fields {
1808        let n_entries = field.entry_count();
1809        let dim_str = if field.n_components == 1 {
1810            format!("{n_entries}")
1811        } else {
1812            format!("{n_entries} {}", field.n_components)
1813        };
1814        writeln!(
1815            writer,
1816            "    <Attribute Name=\"{}\" AttributeType=\"{}\" Center=\"{}\">",
1817            field.name, field.attribute_type, field.center
1818        )?;
1819        writeln!(
1820            writer,
1821            "      <DataItem Format=\"XML\" Dimensions=\"{dim_str}\">"
1822        )?;
1823        write!(writer, "        ")?;
1824        for (i, v) in field.data.iter().enumerate() {
1825            if i > 0 {
1826                write!(writer, " ")?;
1827            }
1828            write!(writer, "{v}")?;
1829        }
1830        writeln!(writer)?;
1831        writeln!(writer, "      </DataItem>")?;
1832        writeln!(writer, "    </Attribute>")?;
1833    }
1834    writeln!(writer, "  </Grid>")?;
1835    Ok(())
1836}
1837/// Collect all patches into a mapping element_id → patch_name (first match).
1838#[allow(dead_code)]
1839pub fn patch_element_map(patches: &[XdmfMeshPatch]) -> std::collections::HashMap<usize, String> {
1840    let mut map = std::collections::HashMap::new();
1841    for patch in patches {
1842        for &eid in &patch.element_ids {
1843            map.entry(eid).or_insert_with(|| patch.name.clone());
1844        }
1845    }
1846    map
1847}
1848/// Format a flat array of `[f64; 3]` positions as a space-separated XDMF data string.
1849#[allow(dead_code)]
1850pub fn format_xdmf_geometry_inline(nodes: &[[f64; 3]]) -> String {
1851    nodes
1852        .iter()
1853        .map(|n| format!("{} {} {}", n[0], n[1], n[2]))
1854        .collect::<Vec<_>>()
1855        .join("\n")
1856}
1857/// Format a flat `Vec`f64` as a single space-separated line suitable for XDMF DataItem.
1858#[allow(dead_code)]
1859pub fn format_xdmf_data_inline(data: &[f64]) -> String {
1860    data.iter()
1861        .map(|v| v.to_string())
1862        .collect::<Vec<_>>()
1863        .join(" ")
1864}
1865/// Build an XDMF `<DataItem Format="HDF" ...>` reference string.
1866///
1867/// `hdf5_file` is the HDF5 file path; `dataset` is the dataset path inside
1868/// the file; `dims` is a space-separated dimension string like `"100 3"`.
1869#[allow(dead_code)]
1870pub fn xdmf_hdf5_dataitem(hdf5_file: &str, dataset: &str, dims: &str, number_type: &str) -> String {
1871    format!(
1872        "<DataItem Format=\"HDF\" Dimensions=\"{dims}\" NumberType=\"{number_type}\" Precision=\"8\">{hdf5_file}:{dataset}</DataItem>"
1873    )
1874}
1875/// Validate that an XDMF XML string contains the minimum required tags.
1876///
1877/// Returns `Ok(())` if the string looks like valid XDMF, or `Err(msg)` describing
1878/// the first missing element.
1879#[allow(dead_code)]
1880pub fn validate_xdmf_structure(xml: &str) -> Result<(), String> {
1881    let required = ["<?xml", "<Xdmf", "<Domain>", "</Domain>", "</Xdmf>"];
1882    for tag in &required {
1883        if !xml.contains(tag) {
1884            return Err(format!("XDMF validation: missing `{tag}`"));
1885        }
1886    }
1887    Ok(())
1888}
1889/// Indent all lines in an XDMF string by `level * 2` spaces.
1890#[allow(dead_code)]
1891pub fn indent_xdmf(xml: &str, level: usize) -> String {
1892    let prefix = " ".repeat(level * 2);
1893    xml.lines()
1894        .map(|line| format!("{prefix}{line}"))
1895        .collect::<Vec<_>>()
1896        .join("\n")
1897}