Skip to main content

oxiphysics_io/hdf5_simple/
functions.rs

1// Auto-generated module
2//
3// πŸ€– Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::types::{DataType, Dataset, ShdfGroup, XdmfParams};
6
7pub(super) const MAGIC: &[u8; 4] = b"SHDF";
8pub(super) const VERSION: u32 = 1;
9/// Encode a UTF-8 string as a length-prefixed byte sequence.
10///
11/// Format: `[length: u32 LE][UTF-8 bytes]`
12#[allow(dead_code)]
13pub fn encode_string(s: &str) -> Vec<u8> {
14    let bytes = s.as_bytes();
15    let mut out = Vec::with_capacity(4 + bytes.len());
16    out.extend_from_slice(&(bytes.len() as u32).to_le_bytes());
17    out.extend_from_slice(bytes);
18    out
19}
20/// Decode a length-prefixed UTF-8 string from `data` starting at `*offset`.
21///
22/// Advances `*offset` past the consumed bytes.
23#[allow(dead_code)]
24pub fn decode_string(data: &[u8], offset: &mut usize) -> Result<String, String> {
25    let len = read_u32(data, offset)? as usize;
26    if *offset + len > data.len() {
27        return Err(format!(
28            "string data out of bounds: need {} bytes at offset {}",
29            len, offset
30        ));
31    }
32    let s = std::str::from_utf8(&data[*offset..*offset + len])
33        .map_err(|e| format!("invalid UTF-8 in string: {e}"))?
34        .to_string();
35    *offset += len;
36    Ok(s)
37}
38pub(super) fn read_u8(data: &[u8], pos: &mut usize) -> Result<u8, String> {
39    if *pos >= data.len() {
40        return Err(format!("unexpected EOF reading u8 at offset {pos}"));
41    }
42    let v = data[*pos];
43    *pos += 1;
44    Ok(v)
45}
46pub(super) fn read_u32(data: &[u8], pos: &mut usize) -> Result<u32, String> {
47    require_bytes(data, *pos, 4)?;
48    let v = u32::from_le_bytes(
49        data[*pos..*pos + 4]
50            .try_into()
51            .expect("slice length must match"),
52    );
53    *pos += 4;
54    Ok(v)
55}
56pub(super) fn read_u64(data: &[u8], pos: &mut usize) -> Result<u64, String> {
57    require_bytes(data, *pos, 8)?;
58    let v = u64::from_le_bytes(
59        data[*pos..*pos + 8]
60            .try_into()
61            .expect("slice length must match"),
62    );
63    *pos += 8;
64    Ok(v)
65}
66pub(super) fn read_f32(data: &[u8], pos: &mut usize) -> Result<f32, String> {
67    require_bytes(data, *pos, 4)?;
68    let v = f32::from_le_bytes(
69        data[*pos..*pos + 4]
70            .try_into()
71            .expect("slice length must match"),
72    );
73    *pos += 4;
74    Ok(v)
75}
76pub(super) fn read_f64(data: &[u8], pos: &mut usize) -> Result<f64, String> {
77    require_bytes(data, *pos, 8)?;
78    let v = f64::from_le_bytes(
79        data[*pos..*pos + 8]
80            .try_into()
81            .expect("slice length must match"),
82    );
83    *pos += 8;
84    Ok(v)
85}
86pub(super) fn read_i32(data: &[u8], pos: &mut usize) -> Result<i32, String> {
87    require_bytes(data, *pos, 4)?;
88    let v = i32::from_le_bytes(
89        data[*pos..*pos + 4]
90            .try_into()
91            .expect("slice length must match"),
92    );
93    *pos += 4;
94    Ok(v)
95}
96pub(super) fn read_i64(data: &[u8], pos: &mut usize) -> Result<i64, String> {
97    require_bytes(data, *pos, 8)?;
98    let v = i64::from_le_bytes(
99        data[*pos..*pos + 8]
100            .try_into()
101            .expect("slice length must match"),
102    );
103    *pos += 8;
104    Ok(v)
105}
106pub(super) fn require_bytes(data: &[u8], pos: usize, n: usize) -> Result<(), String> {
107    if pos + n > data.len() {
108        Err(format!(
109            "unexpected EOF: need {n} bytes at offset {pos}, have {}",
110            data.len()
111        ))
112    } else {
113        Ok(())
114    }
115}
116#[cfg(test)]
117mod tests {
118    use super::*;
119    use crate::hdf5_simple::AttributeHelper;
120
121    use crate::hdf5_simple::ChunkingConfig;
122
123    use crate::hdf5_simple::CompressionAlgorithm;
124
125    use crate::hdf5_simple::CompressionSettings;
126
127    use crate::hdf5_simple::ShdfFile;
128    use crate::hdf5_simple::ShdfSchema;
129
130    use crate::hdf5_simple::types::*;
131    #[test]
132    fn test_roundtrip_f64() {
133        let mut file = ShdfFile::new();
134        let original_data: Vec<f64> = vec![1.0, 2.5, -3.15625, 0.0, 1e100];
135        file.add_dataset_f64("temperatures", vec![5], original_data.clone());
136        let bytes = file.to_bytes();
137        let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
138        let got = recovered
139            .get_f64("temperatures")
140            .expect("dataset not found");
141        assert_eq!(got.len(), original_data.len());
142        for (a, b) in original_data.iter().zip(got.iter()) {
143            assert_eq!(a.to_bits(), b.to_bits(), "f64 value mismatch: {a} vs {b}");
144        }
145    }
146    #[test]
147    fn test_roundtrip_i32() {
148        let mut file = ShdfFile::new();
149        let original_data: Vec<i32> = vec![0, 1, -1, i32::MAX, i32::MIN];
150        file.add_dataset_i32("indices", vec![5], original_data.clone());
151        let bytes = file.to_bytes();
152        let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
153        let got = recovered.get_i32("indices").expect("dataset not found");
154        assert_eq!(got, original_data.as_slice());
155    }
156    #[test]
157    fn test_multiple_datasets() {
158        let mut file = ShdfFile::new();
159        file.add_dataset_f64("velocities", vec![3, 3], vec![0.1; 9]);
160        file.add_dataset_i32("labels", vec![3], vec![10, 20, 30]);
161        file.add_dataset_f64("pressure", vec![1], vec![101325.0]);
162        let bytes = file.to_bytes();
163        let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
164        let vel = recovered.get_f64("velocities").expect("velocities missing");
165        assert_eq!(vel.len(), 9);
166        let lbl = recovered.get_i32("labels").expect("labels missing");
167        assert_eq!(lbl, &[10, 20, 30]);
168        let pres = recovered.get_f64("pressure").expect("pressure missing");
169        assert!((pres[0] - 101325.0).abs() < 1e-6);
170        assert_eq!(recovered.datasets[0].shape, vec![3, 3]);
171        assert_eq!(recovered.datasets[1].shape, vec![3]);
172        assert_eq!(recovered.datasets[2].shape, vec![1]);
173    }
174    #[test]
175    fn test_empty_file_roundtrip() {
176        let file = ShdfFile::new();
177        let bytes = file.to_bytes();
178        let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
179        assert!(recovered.datasets.is_empty());
180        assert!(recovered.global_attributes.is_empty());
181    }
182    #[test]
183    fn test_attribute_roundtrip() {
184        let mut file = ShdfFile::new();
185        file.add_global_attr("author", "Team KitaSan");
186        file.add_global_attr("date", "2026-03-14");
187        file.add_dataset_f64("energy", vec![2], vec![1.0, 2.0]);
188        file.datasets[0]
189            .attributes
190            .push(("units".to_string(), "Joules".to_string()));
191        let bytes = file.to_bytes();
192        let recovered = ShdfFile::from_bytes(&bytes).expect("from_bytes failed");
193        assert_eq!(recovered.global_attributes.len(), 2);
194        assert_eq!(
195            recovered.global_attributes[0],
196            ("author".to_string(), "Team KitaSan".to_string())
197        );
198        assert_eq!(
199            recovered.global_attributes[1],
200            ("date".to_string(), "2026-03-14".to_string())
201        );
202        assert_eq!(recovered.datasets[0].attributes.len(), 1);
203        assert_eq!(recovered.datasets[0].attributes[0].0, "units");
204        assert_eq!(recovered.datasets[0].attributes[0].1, "Joules");
205    }
206    #[test]
207    fn test_write_to_text() {
208        let mut file = ShdfFile::new();
209        file.add_global_attr("title", "Demo");
210        file.add_dataset_f64("pos", vec![2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
211        let text = file.write_to_text();
212        assert!(text.contains("SHDF"));
213        assert!(text.contains("title = Demo"));
214        assert!(text.contains("[pos]"));
215        assert!(text.contains("2Γ—3"));
216        assert!(text.contains("Float64"));
217    }
218    #[test]
219    fn test_encode_decode_string() {
220        let original = "hello, δΈ–η•Œ!";
221        let encoded = encode_string(original);
222        let mut offset = 0usize;
223        let decoded = decode_string(&encoded, &mut offset).expect("decode failed");
224        assert_eq!(decoded, original);
225        assert_eq!(offset, encoded.len());
226    }
227    #[test]
228    fn test_group_empty() {
229        let group = ShdfGroup::new("root");
230        assert_eq!(group.name, "root");
231        assert!(group.datasets.is_empty());
232        assert!(group.children.is_empty());
233        assert_eq!(group.total_datasets(), 0);
234    }
235    #[test]
236    fn test_group_add_dataset() {
237        let mut group = ShdfGroup::new("root");
238        group.add_dataset_f64("temperatures", vec![10], vec![0.0; 10]);
239        assert_eq!(group.datasets.len(), 1);
240        assert_eq!(group.total_datasets(), 1);
241        assert!(group.get_dataset("temperatures").is_some());
242    }
243    #[test]
244    fn test_group_nested() {
245        let mut root = ShdfGroup::new("root");
246        let mut child = ShdfGroup::new("particles");
247        child.add_dataset_f64("positions", vec![100, 3], vec![0.0; 300]);
248        child.add_dataset_i32("types", vec![100], vec![0; 100]);
249        root.add_child(child);
250        assert_eq!(root.total_datasets(), 2);
251        let particles = root.get_child("particles").unwrap();
252        assert_eq!(particles.datasets.len(), 2);
253    }
254    #[test]
255    fn test_group_attributes() {
256        let mut group = ShdfGroup::new("simulation");
257        group.add_attribute("timestep", "0.001");
258        group.add_attribute("units", "SI");
259        assert_eq!(group.attributes.len(), 2);
260    }
261    #[test]
262    fn test_group_summary() {
263        let mut root = ShdfGroup::new("root");
264        root.add_dataset_f64("energy", vec![1], vec![42.0]);
265        let summary = root.summary(0);
266        assert!(summary.contains("root"));
267        assert!(summary.contains("energy"));
268    }
269    #[test]
270    fn test_group_get_missing_dataset() {
271        let group = ShdfGroup::new("root");
272        assert!(group.get_dataset("nonexistent").is_none());
273    }
274    #[test]
275    fn test_group_get_missing_child() {
276        let group = ShdfGroup::new("root");
277        assert!(group.get_child("nonexistent").is_none());
278    }
279    #[test]
280    fn test_chunking_n_chunks_1d() {
281        let config = ChunkingConfig::new(vec![10]);
282        let n = config.n_chunks(&[100]);
283        assert_eq!(n, 10);
284    }
285    #[test]
286    fn test_chunking_n_chunks_2d() {
287        let config = ChunkingConfig::new(vec![10, 5]);
288        let n = config.n_chunks(&[20, 15]);
289        assert_eq!(n, 2 * 3);
290    }
291    #[test]
292    fn test_chunking_n_chunks_remainder() {
293        let config = ChunkingConfig::new(vec![10]);
294        let n = config.n_chunks(&[15]);
295        assert_eq!(n, 2);
296    }
297    #[test]
298    fn test_chunking_chunk_index() {
299        let config = ChunkingConfig::new(vec![10]);
300        assert_eq!(config.chunk_index(&[5], &[100]), 0);
301        assert_eq!(config.chunk_index(&[15], &[100]), 1);
302    }
303    #[test]
304    fn test_chunking_default() {
305        let config = ChunkingConfig::default_for_shape(&[1000, 3]);
306        assert_eq!(config.chunk_dims, vec![64, 3]);
307    }
308    #[test]
309    fn test_chunking_mismatched_dims() {
310        let config = ChunkingConfig::new(vec![10, 10]);
311        assert_eq!(config.n_chunks(&[100]), 0);
312    }
313    #[test]
314    fn test_delta_encode_decode_f64() {
315        let data = vec![1.0, 3.0, 6.0, 10.0, 15.0];
316        let encoded = CompressionSettings::delta_encode_f64(&data);
317        let decoded = CompressionSettings::delta_decode_f64(&encoded);
318        for (a, b) in data.iter().zip(decoded.iter()) {
319            assert!((a - b).abs() < 1e-14, "Mismatch: {a} vs {b}");
320        }
321    }
322    #[test]
323    fn test_delta_encode_decode_i32() {
324        let data = vec![10, 20, 30, 25, 35];
325        let encoded = CompressionSettings::delta_encode_i32(&data);
326        let decoded = CompressionSettings::delta_decode_i32(&encoded);
327        assert_eq!(data, decoded);
328    }
329    #[test]
330    fn test_delta_encode_empty() {
331        assert!(CompressionSettings::delta_encode_f64(&[]).is_empty());
332        assert!(CompressionSettings::delta_decode_f64(&[]).is_empty());
333        assert!(CompressionSettings::delta_encode_i32(&[]).is_empty());
334        assert!(CompressionSettings::delta_decode_i32(&[]).is_empty());
335    }
336    #[test]
337    fn test_delta_encode_single() {
338        let data = vec![42.0];
339        let encoded = CompressionSettings::delta_encode_f64(&data);
340        let decoded = CompressionSettings::delta_decode_f64(&encoded);
341        assert_eq!(decoded, data);
342    }
343    #[test]
344    fn test_compression_none() {
345        let settings = CompressionSettings::none();
346        assert_eq!(settings.algorithm, CompressionAlgorithm::None);
347        assert_eq!(settings.level, 0);
348    }
349    #[test]
350    fn test_compression_delta() {
351        let settings = CompressionSettings::delta();
352        assert_eq!(settings.algorithm, CompressionAlgorithm::Delta);
353    }
354    #[test]
355    fn test_attribute_value_string_roundtrip() {
356        let val = AttributeValue::String("hello".to_string());
357        let s = AttributeHelper::to_string(&val);
358        let recovered = AttributeHelper::from_string(&s);
359        assert_eq!(recovered, val);
360    }
361    #[test]
362    fn test_attribute_value_float_roundtrip() {
363        let val = AttributeValue::Float64(3.125);
364        let s = AttributeHelper::to_string(&val);
365        let recovered = AttributeHelper::from_string(&s);
366        if let AttributeValue::Float64(f) = recovered {
367            assert!((f - 3.125).abs() < 1e-10);
368        } else {
369            panic!("Expected Float64");
370        }
371    }
372    #[test]
373    fn test_attribute_value_int_roundtrip() {
374        let val = AttributeValue::Int32(42);
375        let s = AttributeHelper::to_string(&val);
376        let recovered = AttributeHelper::from_string(&s);
377        assert_eq!(recovered, val);
378    }
379    #[test]
380    fn test_attribute_value_bool_roundtrip() {
381        let val = AttributeValue::Bool(true);
382        let s = AttributeHelper::to_string(&val);
383        let recovered = AttributeHelper::from_string(&s);
384        assert_eq!(recovered, val);
385        let val2 = AttributeValue::Bool(false);
386        let s2 = AttributeHelper::to_string(&val2);
387        let recovered2 = AttributeHelper::from_string(&s2);
388        assert_eq!(recovered2, val2);
389    }
390    #[test]
391    fn test_attribute_value_unknown_prefix() {
392        let recovered = AttributeHelper::from_string("unknown_value");
393        assert_eq!(
394            recovered,
395            AttributeValue::String("unknown_value".to_string())
396        );
397    }
398    #[test]
399    fn test_schema_valid_file() {
400        let mut schema = ShdfSchema::new();
401        schema.expect_dataset("positions", DataType::Float64);
402        schema.require_attribute("author");
403        let mut file = ShdfFile::new();
404        file.add_dataset_f64("positions", vec![10, 3], vec![0.0; 30]);
405        file.add_global_attr("author", "Test");
406        let errors = schema.validate(&file);
407        assert!(errors.is_empty(), "Expected no errors, got: {:?}", errors);
408    }
409    #[test]
410    fn test_schema_missing_dataset() {
411        let mut schema = ShdfSchema::new();
412        schema.expect_dataset("positions", DataType::Float64);
413        let file = ShdfFile::new();
414        let errors = schema.validate(&file);
415        assert_eq!(errors.len(), 1);
416        assert!(errors[0].contains("Missing dataset: positions"));
417    }
418    #[test]
419    fn test_schema_wrong_dtype() {
420        let mut schema = ShdfSchema::new();
421        schema.expect_dataset("indices", DataType::Float64);
422        let mut file = ShdfFile::new();
423        file.add_dataset_i32("indices", vec![10], vec![0; 10]);
424        let errors = schema.validate(&file);
425        assert_eq!(errors.len(), 1);
426        assert!(errors[0].contains("expected"));
427    }
428    #[test]
429    fn test_schema_missing_attribute() {
430        let mut schema = ShdfSchema::new();
431        schema.require_attribute("version");
432        let file = ShdfFile::new();
433        let errors = schema.validate(&file);
434        assert_eq!(errors.len(), 1);
435        assert!(errors[0].contains("Missing global attribute: version"));
436    }
437    #[test]
438    fn test_schema_empty_valid() {
439        let schema = ShdfSchema::new();
440        let file = ShdfFile::new();
441        let errors = schema.validate(&file);
442        assert!(errors.is_empty());
443    }
444    #[test]
445    fn test_datatype_equality() {
446        assert_eq!(DataType::Float64, DataType::Float64);
447        assert_ne!(DataType::Float64, DataType::Float32);
448        assert_ne!(DataType::Int32, DataType::Int64);
449    }
450    #[test]
451    fn test_from_bytes_bad_magic() {
452        let data = b"BAAD\x01\x00\x00\x00";
453        let result = ShdfFile::from_bytes(data);
454        assert!(result.is_err());
455    }
456    #[test]
457    fn test_from_bytes_too_short() {
458        let result = ShdfFile::from_bytes(&[0u8; 3]);
459        assert!(result.is_err());
460    }
461    #[test]
462    fn test_from_bytes_bad_version() {
463        let mut data = Vec::new();
464        data.extend_from_slice(b"SHDF");
465        data.extend_from_slice(&99u32.to_le_bytes());
466        let result = ShdfFile::from_bytes(&data);
467        assert!(result.is_err());
468    }
469}
470/// Write an XDMF XML file that points to data in an HDF5/SHDF file.
471#[allow(dead_code)]
472pub fn write_xdmf(path: &str, params: &XdmfParams) -> std::io::Result<()> {
473    use std::io::Write;
474    let file = std::fs::File::create(path)?;
475    let mut w = std::io::BufWriter::new(file);
476    writeln!(w, r#"<?xml version="1.0" ?>"#)?;
477    writeln!(w, r#"<!DOCTYPE Xdmf SYSTEM "Xdmf.dtd" []>"#)?;
478    writeln!(w, r#"<Xdmf Version="2.0">"#)?;
479    writeln!(w, r#"  <Domain>"#)?;
480    writeln!(w, r#"    <Grid Name="Mesh" GridType="Uniform">"#)?;
481    writeln!(
482        w,
483        r#"      <Topology TopologyType="{}" NumberOfElements="{}">"#,
484        params.topology.as_str(),
485        params.n_elements
486    )?;
487    writeln!(
488        w,
489        r#"        <DataItem Dimensions="{} {}" NumberType="Int" Format="HDF">"#,
490        params.n_elements, params.nodes_per_element
491    )?;
492    writeln!(
493        w,
494        r#"          {}:{}"#,
495        params.hdf5_path, params.connectivity_dataset
496    )?;
497    writeln!(w, r#"        </DataItem>"#)?;
498    writeln!(w, r#"      </Topology>"#)?;
499    writeln!(w, r#"      <Geometry GeometryType="XYZ">"#)?;
500    writeln!(
501        w,
502        r#"        <DataItem Dimensions="{} 3" NumberType="Float" Precision="8" Format="HDF">"#,
503        params.n_nodes
504    )?;
505    writeln!(
506        w,
507        r#"          {}:{}"#,
508        params.hdf5_path, params.coords_dataset
509    )?;
510    writeln!(w, r#"        </DataItem>"#)?;
511    writeln!(w, r#"      </Geometry>"#)?;
512    for (attr_name, ds_path) in &params.attributes {
513        writeln!(
514            w,
515            r#"      <Attribute Name="{attr_name}" AttributeType="Scalar" Center="Node">"#
516        )?;
517        writeln!(
518            w,
519            r#"        <DataItem Dimensions="{}" NumberType="Float" Precision="8" Format="HDF">"#,
520            params.n_nodes
521        )?;
522        writeln!(w, r#"          {}:{}"#, params.hdf5_path, ds_path)?;
523        writeln!(w, r#"        </DataItem>"#)?;
524        writeln!(w, r#"      </Attribute>"#)?;
525    }
526    writeln!(w, r#"    </Grid>"#)?;
527    writeln!(w, r#"  </Domain>"#)?;
528    writeln!(w, r#"</Xdmf>"#)?;
529    w.flush()?;
530    Ok(())
531}
532#[cfg(test)]
533mod tests_hdf5_extended {
534    use super::*;
535
536    use crate::hdf5_simple::ChunkedDataset;
537
538    use crate::hdf5_simple::CompoundDataset;
539    use crate::hdf5_simple::CompoundField;
540
541    use crate::hdf5_simple::CompressionLevel;
542
543    use crate::hdf5_simple::DeflateMetadata;
544
545    use crate::hdf5_simple::XdmfTopologyType;
546    use crate::hdf5_simple::types::*;
547    #[test]
548    fn test_chunked_dataset_n_elements() {
549        let ds = ChunkedDataset::new("data", vec![100, 50], vec![10, 10]);
550        assert_eq!(ds.n_elements(), 5000);
551    }
552    #[test]
553    fn test_chunked_dataset_n_chunks_1d() {
554        let ds = ChunkedDataset::new("v", vec![100], vec![32]);
555        let nchunks = ds.n_chunks_per_dim();
556        assert_eq!(nchunks[0], 4);
557    }
558    #[test]
559    fn test_chunked_dataset_total_chunks() {
560        let ds = ChunkedDataset::new("v", vec![100, 50], vec![10, 10]);
561        assert_eq!(ds.total_chunks(), 50);
562    }
563    #[test]
564    fn test_chunked_dataset_write_read_chunk() {
565        let mut ds = ChunkedDataset::new("v", vec![10], vec![5]);
566        ds.write_chunk_1d(0, &[1.0, 2.0, 3.0, 4.0, 5.0]);
567        ds.write_chunk_1d(1, &[6.0, 7.0, 8.0, 9.0, 10.0]);
568        assert!((ds.data[0] - 1.0).abs() < 1e-12);
569        assert!((ds.data[9] - 10.0).abs() < 1e-12);
570    }
571    #[test]
572    fn test_chunked_dataset_to_bytes_nonempty() {
573        let mut ds = ChunkedDataset::new("pressure", vec![4], vec![2]);
574        ds.data = vec![1.0, 2.0, 3.0, 4.0];
575        let bytes = ds.to_bytes();
576        assert!(!bytes.is_empty());
577    }
578    #[test]
579    fn test_chunked_dataset_attrs() {
580        let mut ds = ChunkedDataset::new("temp", vec![5], vec![5]);
581        ds.add_attr("units", "K");
582        ds.add_attr("long_name", "Temperature");
583        assert_eq!(ds.attrs.len(), 2);
584    }
585    #[test]
586    fn test_shdf_group_new() {
587        let g = ShdfGroup::new("root");
588        assert_eq!(g.name, "root");
589        assert!(g.datasets.is_empty());
590        assert!(g.children.is_empty());
591    }
592    #[test]
593    fn test_shdf_group_add_dataset() {
594        let mut g = ShdfGroup::new("data");
595        g.add_dataset_f64("velocity", vec![3], vec![1.0, 2.0, 3.0]);
596        assert_eq!(g.datasets.len(), 1);
597        assert!(g.get_dataset("velocity").is_some());
598    }
599    #[test]
600    fn test_shdf_group_hierarchy() {
601        let mut root = ShdfGroup::new("root");
602        let child1 = ShdfGroup::new("geometry");
603        let child2 = ShdfGroup::new("fields");
604        root.add_child(child1);
605        root.add_child(child2);
606        assert_eq!(root.children.len(), 2);
607        assert!(root.get_child("geometry").is_some());
608        assert!(root.get_child("missing").is_none());
609    }
610    #[test]
611    fn test_shdf_group_total_datasets_recursive() {
612        let mut root = ShdfGroup::new("root");
613        let mut child = ShdfGroup::new("child");
614        child.add_dataset_f64("x", vec![1], vec![0.0]);
615        root.add_child(child);
616        assert_eq!(root.total_datasets(), 1);
617    }
618    #[test]
619    fn test_shdf_group_name() {
620        let g = ShdfGroup::new("geometry");
621        assert_eq!(g.name, "geometry");
622    }
623    #[test]
624    fn test_write_xdmf_creates_file() {
625        let params = XdmfParams {
626            hdf5_path: "sim.shdf".to_string(),
627            coords_dataset: "/coordinates".to_string(),
628            connectivity_dataset: "/connectivity".to_string(),
629            n_nodes: 100,
630            n_elements: 50,
631            nodes_per_element: 4,
632            topology: XdmfTopologyType::Tetrahedron,
633            attributes: vec![("pressure".to_string(), "/fields/pressure".to_string())],
634        };
635        let path = "/tmp/test_oxiphysics_xdmf.xdmf";
636        write_xdmf(path, &params).unwrap();
637        let content = std::fs::read_to_string(path).unwrap();
638        assert!(content.contains("Xdmf"));
639        assert!(content.contains("Tetrahedron"));
640        assert!(content.contains("pressure"));
641        assert!(content.contains("sim.shdf"));
642        std::fs::remove_file(path).ok();
643    }
644    #[test]
645    fn test_xdmf_topology_type_str() {
646        assert_eq!(XdmfTopologyType::Triangle.as_str(), "Triangle");
647        assert_eq!(XdmfTopologyType::Hexahedron.as_str(), "Hexahedron");
648    }
649    #[test]
650    fn test_compression_level_values() {
651        assert_eq!(CompressionLevel::None.level(), 0);
652        assert_eq!(CompressionLevel::Fast.level(), 1);
653        assert_eq!(CompressionLevel::Balanced.level(), 5);
654        assert_eq!(CompressionLevel::Maximum.level(), 9);
655    }
656    #[test]
657    fn test_compression_level_is_compressed() {
658        assert!(!CompressionLevel::None.is_compressed());
659        assert!(CompressionLevel::Fast.is_compressed());
660        assert!(CompressionLevel::Maximum.is_compressed());
661    }
662    #[test]
663    fn test_deflate_metadata_uncompressed() {
664        let meta = DeflateMetadata::uncompressed(1000);
665        assert_eq!(meta.compression_ratio(), 1.0);
666        assert_eq!(meta.space_savings(), 0.0);
667    }
668    #[test]
669    fn test_deflate_metadata_compression_ratio() {
670        let meta = DeflateMetadata {
671            level: CompressionLevel::Balanced,
672            shuffle: true,
673            chunk_shape: vec![100],
674            compressed_size: 400,
675            uncompressed_size: 1000,
676        };
677        assert!(
678            (meta.compression_ratio() - 2.5).abs() < 1e-10,
679            "ratio={}",
680            meta.compression_ratio()
681        );
682        assert!(
683            (meta.space_savings() - 0.6).abs() < 1e-10,
684            "savings={}",
685            meta.space_savings()
686        );
687    }
688    #[test]
689    fn test_deflate_metadata_zero_compressed() {
690        let meta = DeflateMetadata {
691            level: CompressionLevel::Balanced,
692            shuffle: false,
693            chunk_shape: vec![],
694            compressed_size: 0,
695            uncompressed_size: 1000,
696        };
697        assert_eq!(meta.compression_ratio(), 1.0);
698    }
699    #[test]
700    fn test_compound_dataset_new() {
701        let ds = CompoundDataset::new("particles", 10);
702        assert_eq!(ds.n_records, 10);
703        assert_eq!(ds.n_fields(), 0);
704    }
705    #[test]
706    fn test_compound_dataset_add_field() {
707        let mut ds = CompoundDataset::new("atoms", 3);
708        ds.add_field(CompoundField::new(
709            "x",
710            DataType::Float64,
711            vec![1.0, 2.0, 3.0],
712        ));
713        ds.add_field(CompoundField::new(
714            "y",
715            DataType::Float64,
716            vec![4.0, 5.0, 6.0],
717        ));
718        assert_eq!(ds.n_fields(), 2);
719        let xs = ds.get_field("x").unwrap();
720        assert_eq!(xs.len(), 3);
721        assert!((xs[0] - 1.0).abs() < 1e-12);
722    }
723    #[test]
724    fn test_compound_dataset_get_missing_field() {
725        let ds = CompoundDataset::new("particles", 5);
726        assert!(ds.get_field("missing").is_none());
727    }
728    #[test]
729    fn test_compound_dataset_to_csv_bytes() {
730        let mut ds = CompoundDataset::new("data", 2);
731        ds.add_field(CompoundField::new("a", DataType::Float64, vec![1.0, 2.0]));
732        ds.add_field(CompoundField::new("b", DataType::Float32, vec![3.0, 4.0]));
733        let csv = ds.to_csv_bytes();
734        let s = String::from_utf8(csv).unwrap();
735        assert!(s.contains("a,b"));
736        assert!(s.contains("1.000000"));
737    }
738    #[test]
739    fn test_compound_dataset_attrs() {
740        let mut ds = CompoundDataset::new("traj", 0);
741        ds.add_attr("source", "MD simulation");
742        ds.add_attr("units", "nm");
743        assert_eq!(ds.attrs.len(), 2);
744    }
745    #[test]
746    #[should_panic]
747    fn test_compound_dataset_wrong_field_length() {
748        let mut ds = CompoundDataset::new("atoms", 5);
749        ds.add_field(CompoundField::new(
750            "x",
751            DataType::Float64,
752            vec![1.0, 2.0, 3.0],
753        ));
754    }
755}
756/// Generate a CDL (Common Data form Language) text representation of an
757/// [`ShdfGroup`] hierarchy for debugging purposes.
758pub fn cdl_dump(root: &ShdfGroup) -> String {
759    let mut out = String::new();
760    cdl_dump_group(root, 0, &mut out);
761    out
762}
763pub(super) fn cdl_dump_group(group: &ShdfGroup, depth: usize, out: &mut String) {
764    let indent = "  ".repeat(depth);
765    out.push_str(&format!("{}group: {} {{\n", indent, group.name));
766    for ds in &group.datasets {
767        cdl_dump_dataset(ds, depth + 1, out);
768    }
769    for child in &group.children {
770        cdl_dump_group(child, depth + 1, out);
771    }
772    out.push_str(&format!("{}}} // group: {}\n", indent, group.name));
773}
774pub(super) fn cdl_dump_dataset(ds: &Dataset, depth: usize, out: &mut String) {
775    let indent = "  ".repeat(depth);
776    let type_str = match ds.dtype {
777        DataType::Float64 => "double",
778        DataType::Float32 => "float",
779        DataType::Int32 => "int",
780        DataType::Int64 => "int64",
781    };
782    let shape_str: Vec<String> = ds.shape.iter().map(|d| d.to_string()).collect();
783    out.push_str(&format!(
784        "{}{}  {}({}) ;\n",
785        indent,
786        type_str,
787        ds.name,
788        shape_str.join(", ")
789    ));
790    for (k, v) in &ds.attributes {
791        out.push_str(&format!("{}    {}:{} = \"{}\" ;\n", indent, ds.name, k, v));
792    }
793    let preview_len = ds.data_f64.len().min(8);
794    if preview_len > 0 {
795        let vals: Vec<String> = ds.data_f64[..preview_len]
796            .iter()
797            .map(|v| format!("{:.6}", v))
798            .collect();
799        let ellipsis = if ds.data_f64.len() > 8 { ", ..." } else { "" };
800        out.push_str(&format!(
801            "{}    // data = {}{} ;\n",
802            indent,
803            vals.join(", "),
804            ellipsis
805        ));
806    }
807}
808#[cfg(test)]
809mod tests_hdf5_additions {
810    use super::*;
811
812    use crate::hdf5_simple::DatasetStats;
813
814    use crate::hdf5_simple::GroupNavigator;
815
816    use crate::hdf5_simple::TimeSeriesAppender;
817    use crate::hdf5_simple::VirtualLink;
818
819    use crate::hdf5_simple::types::*;
820    #[test]
821    fn test_navigator_get_dataset_simple() {
822        let mut root = ShdfGroup::new("root");
823        root.add_dataset_f64("pos", vec![3], vec![1.0, 2.0, 3.0]);
824        let nav = GroupNavigator::new(root);
825        assert!(nav.get_dataset("/root/pos").is_some());
826        assert!(nav.get_dataset("/root/missing").is_none());
827    }
828    #[test]
829    fn test_navigator_nested_path() {
830        let mut root = ShdfGroup::new("root");
831        let mut sim = ShdfGroup::new("simulation");
832        let mut atoms = ShdfGroup::new("atoms");
833        atoms.add_dataset_f64("positions", vec![10, 3], vec![0.0; 30]);
834        sim.add_child(atoms);
835        root.add_child(sim);
836        let nav = GroupNavigator::new(root);
837        assert!(
838            nav.get_dataset("/root/simulation/atoms/positions")
839                .is_some()
840        );
841        assert_eq!(nav.total_datasets(), 1);
842    }
843    #[test]
844    fn test_navigator_all_paths() {
845        let mut root = ShdfGroup::new("root");
846        root.add_dataset_f64("a", vec![1], vec![1.0]);
847        root.add_dataset_f64("b", vec![1], vec![2.0]);
848        let nav = GroupNavigator::new(root);
849        let paths = nav.all_paths();
850        assert_eq!(paths.len(), 2);
851        assert!(paths.iter().any(|p| p.ends_with("/a")));
852        assert!(paths.iter().any(|p| p.ends_with("/b")));
853    }
854    #[test]
855    fn test_navigator_empty_group() {
856        let root = ShdfGroup::new("empty");
857        let nav = GroupNavigator::new(root);
858        assert_eq!(nav.all_paths().len(), 0);
859        assert_eq!(nav.total_datasets(), 0);
860    }
861    #[test]
862    fn test_navigator_leading_slash_optional() {
863        let mut root = ShdfGroup::new("root");
864        root.add_dataset_f64("v", vec![2], vec![1.0, 2.0]);
865        let nav = GroupNavigator::new(root);
866        assert!(nav.get_dataset("/root/v").is_some());
867    }
868    #[test]
869    fn test_timeseries_appender_basic() {
870        let mut app = TimeSeriesAppender::new("temperature", 3);
871        app.append(&[300.0, 301.0, 302.0]);
872        app.append(&[303.0, 304.0, 305.0]);
873        assert_eq!(app.n_frames, 2);
874        assert_eq!(app.total_samples(), 6);
875    }
876    #[test]
877    fn test_timeseries_get_frame() {
878        let mut app = TimeSeriesAppender::new("vel", 3);
879        app.append(&[1.0, 2.0, 3.0]);
880        app.append(&[4.0, 5.0, 6.0]);
881        let frame = app.get_frame(1).unwrap();
882        assert_eq!(frame, &[4.0, 5.0, 6.0]);
883    }
884    #[test]
885    fn test_timeseries_get_frame_out_of_bounds() {
886        let app = TimeSeriesAppender::new("x", 2);
887        assert!(app.get_frame(0).is_none());
888    }
889    #[test]
890    fn test_timeseries_to_dataset_shape() {
891        let mut app = TimeSeriesAppender::new("pressure", 1);
892        app.append(&[1.0]);
893        app.append(&[2.0]);
894        app.append(&[3.0]);
895        let ds = app.to_dataset();
896        assert_eq!(ds.shape, vec![3, 1]);
897        assert!(ds.attributes.iter().any(|(k, _)| k == "n_frames"));
898    }
899    #[test]
900    #[should_panic]
901    fn test_timeseries_wrong_frame_width() {
902        let mut app = TimeSeriesAppender::new("x", 3);
903        app.append(&[1.0, 2.0]);
904    }
905    #[test]
906    fn test_cdl_dump_contains_group_name() {
907        let mut root = ShdfGroup::new("simulation");
908        root.add_dataset_f64("energy", vec![5], vec![1.0; 5]);
909        let cdl = cdl_dump(&root);
910        assert!(cdl.contains("simulation"), "CDL should contain group name");
911        assert!(cdl.contains("energy"), "CDL should contain dataset name");
912        assert!(cdl.contains("double"), "CDL should contain type");
913    }
914    #[test]
915    fn test_cdl_dump_nested() {
916        let mut root = ShdfGroup::new("root");
917        let mut child = ShdfGroup::new("atoms");
918        child.add_dataset_f64("x", vec![4], vec![1.0, 2.0, 3.0, 4.0]);
919        root.add_child(child);
920        let cdl = cdl_dump(&root);
921        assert!(cdl.contains("atoms"));
922        assert!(cdl.contains("x"));
923    }
924    #[test]
925    fn test_cdl_dump_with_attrs() {
926        let mut root = ShdfGroup::new("meta");
927        let mut ds = Dataset {
928            name: "temp".to_string(),
929            dtype: DataType::Float32,
930            shape: vec![2],
931            data_f64: vec![300.0, 310.0],
932            data_i32: Vec::new(),
933            attributes: vec![("units".to_string(), "K".to_string())],
934        };
935        ds.attributes
936            .push(("source".to_string(), "sensor".to_string()));
937        root.datasets.push(ds);
938        let cdl = cdl_dump(&root);
939        assert!(cdl.contains("units"));
940        assert!(cdl.contains("source"));
941    }
942    #[test]
943    fn test_cdl_dump_int32_type_label() {
944        let mut root = ShdfGroup::new("ids");
945        root.datasets.push(Dataset {
946            name: "atom_id".to_string(),
947            dtype: DataType::Int32,
948            shape: vec![3],
949            data_f64: vec![1.0, 2.0, 3.0],
950            data_i32: Vec::new(),
951            attributes: vec![],
952        });
953        let cdl = cdl_dump(&root);
954        assert!(cdl.contains("int"), "CDL should label Int32 as 'int'");
955    }
956    #[test]
957    fn test_virtual_link_no_slice() {
958        let link = VirtualLink::new("/virtual/pos", "sim.shdf", "/atoms/positions");
959        let cdl = link.to_cdl();
960        assert!(cdl.contains("/virtual/pos"));
961        assert!(cdl.contains("sim.shdf"));
962        assert!(cdl.contains("(:)"), "no-slice should show '(:)'");
963    }
964    #[test]
965    fn test_virtual_link_with_slice() {
966        let link = VirtualLink::new("/v/vel", "traj.shdf", "/vel")
967            .with_slice(0, 100, 1)
968            .with_slice(0, 3, 1);
969        let cdl = link.to_cdl();
970        assert!(
971            cdl.contains("0:100:1"),
972            "should contain first dimension slice"
973        );
974        assert!(
975            cdl.contains("0:3:1"),
976            "should contain second dimension slice"
977        );
978    }
979    #[test]
980    fn test_virtual_link_clone() {
981        let link = VirtualLink::new("/a", "f.shdf", "/b");
982        let cloned = link.clone();
983        assert_eq!(cloned.virtual_path, link.virtual_path);
984    }
985    #[test]
986    fn test_dataset_stats_basic() {
987        let data = vec![1.0, 2.0, 3.0, 4.0, 5.0];
988        let stats = DatasetStats::from_slice(&data).unwrap();
989        assert!((stats.min - 1.0).abs() < 1e-12);
990        assert!((stats.max - 5.0).abs() < 1e-12);
991        assert!((stats.mean - 3.0).abs() < 1e-12);
992        assert!((stats.range() - 4.0).abs() < 1e-12);
993        assert_eq!(stats.count, 5);
994    }
995    #[test]
996    fn test_dataset_stats_single_element() {
997        let stats = DatasetStats::from_slice(&[42.0]).unwrap();
998        assert!((stats.min - 42.0).abs() < 1e-12);
999        assert!((stats.max - 42.0).abs() < 1e-12);
1000        assert!((stats.mean - 42.0).abs() < 1e-12);
1001        assert!((stats.variance).abs() < 1e-12);
1002        assert!((stats.std_dev()).abs() < 1e-12);
1003    }
1004    #[test]
1005    fn test_dataset_stats_empty() {
1006        assert!(DatasetStats::from_slice(&[]).is_none());
1007    }
1008    #[test]
1009    fn test_dataset_stats_std_dev_constant() {
1010        let data = vec![5.0; 10];
1011        let stats = DatasetStats::from_slice(&data).unwrap();
1012        assert!(
1013            stats.std_dev().abs() < 1e-12,
1014            "constant data has zero std dev"
1015        );
1016    }
1017    #[test]
1018    fn test_dataset_stats_variance_known() {
1019        let data = vec![0.0, 2.0, 4.0];
1020        let stats = DatasetStats::from_slice(&data).unwrap();
1021        let expected_var = 8.0 / 3.0;
1022        assert!(
1023            (stats.variance - expected_var).abs() < 1e-10,
1024            "variance={}",
1025            stats.variance
1026        );
1027    }
1028}