Skip to main content

oxiphysics_io/hdf5_simple/
types.rs

1// Auto-generated module
2//
3// 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6
7/// XDMF topology type.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy, PartialEq, Eq)]
10pub enum XdmfTopologyType {
11    /// Triangle elements.
12    Triangle,
13    /// Tetrahedral elements.
14    Tetrahedron,
15    /// Hexahedral elements.
16    Hexahedron,
17    /// Unstructured elements (mixed).
18    Mixed,
19}
20impl XdmfTopologyType {
21    pub(super) fn as_str(self) -> &'static str {
22        match self {
23            Self::Triangle => "Triangle",
24            Self::Tetrahedron => "Tetrahedron",
25            Self::Hexahedron => "Hexahedron",
26            Self::Mixed => "Mixed",
27        }
28    }
29}
30/// Metadata describing compression settings for a dataset.
31#[allow(dead_code)]
32#[derive(Debug, Clone)]
33pub struct DeflateMetadata {
34    /// Compression level.
35    pub level: CompressionLevel,
36    /// Whether shuffle filter is applied before compression.
37    pub shuffle: bool,
38    /// Chunk shape used during compression.
39    pub chunk_shape: Vec<u64>,
40    /// Compressed size in bytes (0 if not yet written).
41    pub compressed_size: u64,
42    /// Uncompressed size in bytes.
43    pub uncompressed_size: u64,
44}
45impl DeflateMetadata {
46    /// Create metadata for an uncompressed dataset.
47    pub fn uncompressed(uncompressed_size: u64) -> Self {
48        Self {
49            level: CompressionLevel::None,
50            shuffle: false,
51            chunk_shape: Vec::new(),
52            compressed_size: uncompressed_size,
53            uncompressed_size,
54        }
55    }
56    /// Estimated compression ratio (uncompressed / compressed).
57    pub fn compression_ratio(&self) -> f64 {
58        if self.compressed_size == 0 {
59            return 1.0;
60        }
61        self.uncompressed_size as f64 / self.compressed_size as f64
62    }
63    /// Space savings fraction \[0, 1).
64    pub fn space_savings(&self) -> f64 {
65        if self.uncompressed_size == 0 {
66            return 0.0;
67        }
68        1.0 - self.compressed_size as f64 / self.uncompressed_size as f64
69    }
70}
71/// Compression level for SHDF datasets (analogous to HDF5 deflate filter).
72#[allow(dead_code)]
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum CompressionLevel {
75    /// No compression.
76    None,
77    /// Fast compression (level 1).
78    Fast,
79    /// Balanced compression (level 5).
80    Balanced,
81    /// Maximum compression (level 9).
82    Maximum,
83}
84impl CompressionLevel {
85    /// Return the numeric compression level.
86    pub fn level(self) -> u8 {
87        match self {
88            Self::None => 0,
89            Self::Fast => 1,
90            Self::Balanced => 5,
91            Self::Maximum => 9,
92        }
93    }
94    /// Whether compression is active.
95    pub fn is_compressed(self) -> bool {
96        !matches!(self, Self::None)
97    }
98}
99/// A single named, shaped dataset inside a [`ShdfFile`].
100#[derive(Debug, Clone)]
101#[allow(dead_code)]
102pub struct Dataset {
103    /// Dataset name (must be unique within a file).
104    pub name: String,
105    /// Shape (dimension sizes), e.g. `[rows, cols]`.
106    pub shape: Vec<usize>,
107    /// Declared element type.
108    pub dtype: DataType,
109    /// Floating-point payload (used when `dtype` is Float64 or Float32).
110    pub data_f64: Vec<f64>,
111    /// Integer payload (used when `dtype` is Int32 or Int64).
112    pub data_i32: Vec<i32>,
113    /// Per-dataset key/value string attributes.
114    pub attributes: Vec<(String, String)>,
115}
116/// Compression settings for a dataset.
117#[derive(Debug, Clone)]
118#[allow(dead_code)]
119pub struct CompressionSettings {
120    /// Which algorithm to use.
121    pub algorithm: CompressionAlgorithm,
122    /// Compression level (0-9, higher = more compression).
123    pub level: u32,
124}
125#[allow(dead_code)]
126impl CompressionSettings {
127    /// No compression.
128    pub fn none() -> Self {
129        Self {
130            algorithm: CompressionAlgorithm::None,
131            level: 0,
132        }
133    }
134    /// Delta encoding.
135    pub fn delta() -> Self {
136        Self {
137            algorithm: CompressionAlgorithm::Delta,
138            level: 1,
139        }
140    }
141    /// Apply delta encoding to f64 data.
142    pub fn delta_encode_f64(data: &[f64]) -> Vec<f64> {
143        if data.is_empty() {
144            return Vec::new();
145        }
146        let mut encoded = Vec::with_capacity(data.len());
147        encoded.push(data[0]);
148        for i in 1..data.len() {
149            encoded.push(data[i] - data[i - 1]);
150        }
151        encoded
152    }
153    /// Decode delta-encoded f64 data.
154    pub fn delta_decode_f64(encoded: &[f64]) -> Vec<f64> {
155        if encoded.is_empty() {
156            return Vec::new();
157        }
158        let mut decoded = Vec::with_capacity(encoded.len());
159        decoded.push(encoded[0]);
160        for i in 1..encoded.len() {
161            decoded.push(decoded[i - 1] + encoded[i]);
162        }
163        decoded
164    }
165    /// Apply delta encoding to i32 data.
166    pub fn delta_encode_i32(data: &[i32]) -> Vec<i32> {
167        if data.is_empty() {
168            return Vec::new();
169        }
170        let mut encoded = Vec::with_capacity(data.len());
171        encoded.push(data[0]);
172        for i in 1..data.len() {
173            encoded.push(data[i] - data[i - 1]);
174        }
175        encoded
176    }
177    /// Decode delta-encoded i32 data.
178    pub fn delta_decode_i32(encoded: &[i32]) -> Vec<i32> {
179        if encoded.is_empty() {
180            return Vec::new();
181        }
182        let mut decoded = Vec::with_capacity(encoded.len());
183        decoded.push(encoded[0]);
184        for i in 1..encoded.len() {
185            decoded.push(decoded[i - 1] + encoded[i]);
186        }
187        decoded
188    }
189}
190/// A field in a compound dataset (analogous to an HDF5 compound type member).
191#[allow(dead_code)]
192#[derive(Debug, Clone)]
193pub struct CompoundField {
194    /// Field name.
195    pub name: String,
196    /// Data type.
197    pub dtype: DataType,
198    /// Field values (all records, serialized as f64 regardless of dtype for simplicity).
199    pub values: Vec<f64>,
200}
201impl CompoundField {
202    /// Create a new compound field.
203    pub fn new(name: impl Into<String>, dtype: DataType, values: Vec<f64>) -> Self {
204        Self {
205            name: name.into(),
206            dtype,
207            values,
208        }
209    }
210}
211/// Compression algorithm selection.
212#[derive(Debug, Clone, Copy, PartialEq)]
213#[allow(dead_code)]
214pub enum CompressionAlgorithm {
215    /// No compression.
216    None,
217    /// Run-length encoding (simple).
218    RunLength,
219    /// Delta encoding (store differences).
220    Delta,
221}
222/// A compound dataset (analogous to HDF5 compound dataset with multiple fields per record).
223#[allow(dead_code)]
224#[derive(Debug, Clone)]
225pub struct CompoundDataset {
226    /// Dataset name.
227    pub name: String,
228    /// Number of records.
229    pub n_records: usize,
230    /// Fields (each has `n_records` values).
231    pub fields: Vec<CompoundField>,
232    /// Attributes.
233    pub attrs: Vec<(String, String)>,
234}
235impl CompoundDataset {
236    /// Create a new compound dataset.
237    pub fn new(name: impl Into<String>, n_records: usize) -> Self {
238        Self {
239            name: name.into(),
240            n_records,
241            fields: Vec::new(),
242            attrs: Vec::new(),
243        }
244    }
245    /// Add a field.
246    pub fn add_field(&mut self, field: CompoundField) {
247        assert_eq!(
248            field.values.len(),
249            self.n_records,
250            "Field {} has {} values, expected {}",
251            field.name,
252            field.values.len(),
253            self.n_records
254        );
255        self.fields.push(field);
256    }
257    /// Add an attribute.
258    pub fn add_attr(&mut self, key: impl Into<String>, value: impl Into<String>) {
259        self.attrs.push((key.into(), value.into()));
260    }
261    /// Get values for a named field.
262    pub fn get_field(&self, name: &str) -> Option<&[f64]> {
263        self.fields
264            .iter()
265            .find(|f| f.name == name)
266            .map(|f| f.values.as_slice())
267    }
268    /// Number of fields.
269    pub fn n_fields(&self) -> usize {
270        self.fields.len()
271    }
272    /// Serialize the compound dataset to a flat CSV-like byte buffer (for debugging).
273    pub fn to_csv_bytes(&self) -> Vec<u8> {
274        let mut out = Vec::new();
275        let header: Vec<&str> = self.fields.iter().map(|f| f.name.as_str()).collect();
276        out.extend_from_slice(header.join(",").as_bytes());
277        out.push(b'\n');
278        for rec in 0..self.n_records {
279            let vals: Vec<String> = self
280                .fields
281                .iter()
282                .map(|f| format!("{:.6}", f.values[rec]))
283                .collect();
284            out.extend_from_slice(vals.join(",").as_bytes());
285            out.push(b'\n');
286        }
287        out
288    }
289}
290/// Basic statistics computed over a [`Dataset`]'s raw f64 data.
291#[derive(Debug, Clone)]
292pub struct DatasetStats {
293    /// Number of elements.
294    pub count: usize,
295    /// Minimum value.
296    pub min: f64,
297    /// Maximum value.
298    pub max: f64,
299    /// Arithmetic mean.
300    pub mean: f64,
301    /// Variance.
302    pub variance: f64,
303}
304impl DatasetStats {
305    /// Compute statistics from a slice of f64 values.
306    pub fn from_slice(data: &[f64]) -> Option<Self> {
307        if data.is_empty() {
308            return None;
309        }
310        let count = data.len();
311        let mut min = data[0];
312        let mut max = data[0];
313        let mut sum = 0.0_f64;
314        for &v in data {
315            if v < min {
316                min = v;
317            }
318            if v > max {
319                max = v;
320            }
321            sum += v;
322        }
323        let mean = sum / count as f64;
324        let variance = data.iter().map(|&v| (v - mean) * (v - mean)).sum::<f64>() / count as f64;
325        Some(Self {
326            count,
327            min,
328            max,
329            mean,
330            variance,
331        })
332    }
333    /// Standard deviation (sqrt of variance).
334    pub fn std_dev(&self) -> f64 {
335        self.variance.sqrt()
336    }
337    /// Range: max - min.
338    pub fn range(&self) -> f64 {
339        self.max - self.min
340    }
341}
342/// Expected schema for an SHDF file.
343#[derive(Debug, Clone)]
344#[allow(dead_code)]
345pub struct ShdfSchema {
346    /// Expected dataset names and their types.
347    pub expected_datasets: Vec<(String, DataType)>,
348    /// Required global attributes.
349    pub required_attributes: Vec<String>,
350}
351#[allow(dead_code)]
352impl ShdfSchema {
353    /// Create a new schema.
354    pub fn new() -> Self {
355        Self {
356            expected_datasets: Vec::new(),
357            required_attributes: Vec::new(),
358        }
359    }
360    /// Add an expected dataset.
361    pub fn expect_dataset(&mut self, name: &str, dtype: DataType) {
362        self.expected_datasets.push((name.to_string(), dtype));
363    }
364    /// Add a required global attribute.
365    pub fn require_attribute(&mut self, key: &str) {
366        self.required_attributes.push(key.to_string());
367    }
368    /// Validate an SHDF file against this schema.
369    ///
370    /// Returns a list of validation errors (empty = valid).
371    pub fn validate(&self, file: &ShdfFile) -> Vec<String> {
372        let mut errors = Vec::new();
373        for (name, dtype) in &self.expected_datasets {
374            match file.datasets.iter().find(|d| &d.name == name) {
375                None => errors.push(format!("Missing dataset: {name}")),
376                Some(ds) => {
377                    if ds.dtype != *dtype {
378                        errors.push(format!(
379                            "Dataset '{name}': expected {:?}, got {:?}",
380                            dtype, ds.dtype
381                        ));
382                    }
383                }
384            }
385        }
386        for key in &self.required_attributes {
387            if !file.global_attributes.iter().any(|(k, _)| k == key) {
388                errors.push(format!("Missing global attribute: {key}"));
389            }
390        }
391        errors
392    }
393}
394impl Default for ShdfSchema {
395    fn default() -> Self {
396        Self::new()
397    }
398}
399/// Chunk descriptor: defines chunk shape and offset within a dataset.
400#[allow(dead_code)]
401#[derive(Debug, Clone)]
402pub struct ChunkDescriptor {
403    /// Chunk dimensions (same rank as dataset).
404    pub shape: Vec<u64>,
405    /// Offset of this chunk in the dataset (per dimension).
406    pub offset: Vec<u64>,
407    /// Flat index of this chunk.
408    pub index: u64,
409}
410/// Data type tag stored in each [`Dataset`].
411#[derive(Debug, Clone, PartialEq)]
412#[allow(dead_code)]
413pub enum DataType {
414    /// 64-bit IEEE 754 floating point.
415    Float64,
416    /// 32-bit IEEE 754 floating point.
417    Float32,
418    /// 32-bit signed integer.
419    Int32,
420    /// 64-bit signed integer.
421    Int64,
422}
423/// A chunked dataset: stores data split into uniform-sized chunks.
424#[allow(dead_code)]
425#[derive(Debug, Clone)]
426pub struct ChunkedDataset {
427    /// Dataset name.
428    pub name: String,
429    /// Full dataset dimensions.
430    pub dims: Vec<u64>,
431    /// Chunk shape.
432    pub chunk_shape: Vec<u64>,
433    /// Data (f64), stored flat across all chunks.
434    pub data: Vec<f64>,
435    /// Attributes.
436    pub attrs: Vec<(String, String)>,
437}
438impl ChunkedDataset {
439    /// Create a new chunked dataset.
440    pub fn new(name: impl Into<String>, dims: Vec<u64>, chunk_shape: Vec<u64>) -> Self {
441        let total: u64 = dims.iter().product();
442        Self {
443            name: name.into(),
444            dims,
445            chunk_shape,
446            data: vec![0.0; total as usize],
447            attrs: Vec::new(),
448        }
449    }
450    /// Total number of elements.
451    pub fn n_elements(&self) -> usize {
452        self.dims.iter().product::<u64>() as usize
453    }
454    /// Compute number of chunks per dimension.
455    pub fn n_chunks_per_dim(&self) -> Vec<u64> {
456        self.dims
457            .iter()
458            .zip(self.chunk_shape.iter())
459            .map(|(&d, &c)| d.div_ceil(c))
460            .collect()
461    }
462    /// Total number of chunks.
463    pub fn total_chunks(&self) -> u64 {
464        self.n_chunks_per_dim().iter().product()
465    }
466    /// Write data for a specific 1-D chunk (row-major slice of `data`).
467    pub fn write_chunk_1d(&mut self, chunk_idx: u64, chunk_data: &[f64]) {
468        let chunk_size = self.chunk_shape[0] as usize;
469        let start = (chunk_idx as usize) * chunk_size;
470        let end = (start + chunk_data.len()).min(self.data.len());
471        let src_end = end - start;
472        self.data[start..end].copy_from_slice(&chunk_data[..src_end]);
473    }
474    /// Add an attribute.
475    pub fn add_attr(&mut self, key: impl Into<String>, value: impl Into<String>) {
476        self.attrs.push((key.into(), value.into()));
477    }
478    /// Serialize the chunked dataset to bytes (SHDF-compatible format).
479    pub fn to_bytes(&self) -> Vec<u8> {
480        let mut buf = Vec::new();
481        let name_bytes = self.name.as_bytes();
482        buf.extend_from_slice(&(name_bytes.len() as u32).to_le_bytes());
483        buf.extend_from_slice(name_bytes);
484        buf.push(0u8);
485        buf.extend_from_slice(&(self.dims.len() as u32).to_le_bytes());
486        for &d in &self.dims {
487            buf.extend_from_slice(&d.to_le_bytes());
488        }
489        let n = self.n_elements() as u64;
490        buf.extend_from_slice(&n.to_le_bytes());
491        for &v in &self.data {
492            buf.extend_from_slice(&v.to_le_bytes());
493        }
494        buf.extend_from_slice(&(self.attrs.len() as u32).to_le_bytes());
495        for (k, v) in &self.attrs {
496            let kb = k.as_bytes();
497            buf.extend_from_slice(&(kb.len() as u32).to_le_bytes());
498            buf.extend_from_slice(kb);
499            let vb = v.as_bytes();
500            buf.extend_from_slice(&(vb.len() as u32).to_le_bytes());
501            buf.extend_from_slice(vb);
502        }
503        buf
504    }
505}
506/// Parameters for writing an XDMF file that references HDF5/SHDF data.
507#[allow(dead_code)]
508#[derive(Debug, Clone)]
509pub struct XdmfParams {
510    /// Path to the HDF5/SHDF file.
511    pub hdf5_path: String,
512    /// Dataset name for coordinates (within the HDF5 file).
513    pub coords_dataset: String,
514    /// Dataset name for connectivity (within the HDF5 file).
515    pub connectivity_dataset: String,
516    /// Number of nodes.
517    pub n_nodes: usize,
518    /// Number of elements.
519    pub n_elements: usize,
520    /// Nodes per element.
521    pub nodes_per_element: usize,
522    /// Topology type.
523    pub topology: XdmfTopologyType,
524    /// Named attribute datasets (attribute_name → dataset_path).
525    pub attributes: Vec<(String, String)>,
526}
527/// Navigate an [`ShdfGroup`] hierarchy using HDF5-style slash-separated paths.
528///
529/// # Example
530/// ```no_run
531/// # use oxiphysics_io::hdf5_simple::*;
532/// let mut root = ShdfGroup::new("root");
533/// let mut sim  = ShdfGroup::new("simulation");
534/// sim.add_dataset_f64("time", vec![3], vec![0.0, 0.5, 1.0]);
535/// root.add_child(sim);
536/// let nav = GroupNavigator::new(root);
537/// assert!(nav.get_dataset("/simulation/time").is_some());
538/// ```
539pub struct GroupNavigator {
540    /// Root group of the hierarchy.
541    pub root: ShdfGroup,
542}
543impl GroupNavigator {
544    /// Create a navigator wrapping a root group.
545    pub fn new(root: ShdfGroup) -> Self {
546        Self { root }
547    }
548    /// Resolve a slash-separated path such as `"/root/simulation/atoms/positions"`
549    /// and return the terminal dataset if it exists.
550    ///
551    /// The first path component must match the root group name.
552    pub fn get_dataset(&self, path: &str) -> Option<&Dataset> {
553        let parts: Vec<&str> = path.trim_start_matches('/').splitn(64, '/').collect();
554        if parts.is_empty() {
555            return None;
556        }
557        let (ds_name, group_parts) = parts.split_last()?;
558        let mut group = &self.root;
559        let effective_parts = if group_parts.first().copied() == Some(self.root.name.as_str()) {
560            &group_parts[1..]
561        } else {
562            group_parts
563        };
564        for &part in effective_parts {
565            group = group.get_child(part)?;
566        }
567        group.get_dataset(ds_name)
568    }
569    /// Return all dataset paths reachable from the root, in DFS order.
570    pub fn all_paths(&self) -> Vec<String> {
571        let mut result = Vec::new();
572        Self::collect_paths(&self.root, "", &mut result);
573        result
574    }
575    fn collect_paths(group: &ShdfGroup, prefix: &str, out: &mut Vec<String>) {
576        let base = if prefix.is_empty() {
577            format!("/{}", group.name)
578        } else {
579            format!("{}/{}", prefix, group.name)
580        };
581        for ds in &group.datasets {
582            out.push(format!("{}/{}", base, ds.name));
583        }
584        for child in &group.children {
585            Self::collect_paths(child, &base, out);
586        }
587    }
588    /// Count total datasets reachable from the root.
589    pub fn total_datasets(&self) -> usize {
590        self.root.total_datasets()
591    }
592}
593/// Chunking configuration for a dataset.
594#[derive(Debug, Clone)]
595#[allow(dead_code)]
596pub struct ChunkingConfig {
597    /// Chunk dimensions. Must have the same number of dimensions as the dataset.
598    pub chunk_dims: Vec<usize>,
599}
600#[allow(dead_code)]
601impl ChunkingConfig {
602    /// Create a chunking config with the given chunk dimensions.
603    pub fn new(chunk_dims: Vec<usize>) -> Self {
604        Self { chunk_dims }
605    }
606    /// Compute the number of chunks needed for a dataset with the given shape.
607    pub fn n_chunks(&self, shape: &[usize]) -> usize {
608        if shape.len() != self.chunk_dims.len() {
609            return 0;
610        }
611        let mut total = 1_usize;
612        for (s, c) in shape.iter().zip(self.chunk_dims.iter()) {
613            if *c == 0 {
614                return 0;
615            }
616            total *= (*s).div_ceil(*c);
617        }
618        total
619    }
620    /// Compute the linear index of the chunk containing the given element.
621    pub fn chunk_index(&self, element_idx: &[usize], shape: &[usize]) -> usize {
622        if shape.len() != self.chunk_dims.len() || element_idx.len() != shape.len() {
623            return 0;
624        }
625        let mut idx = 0;
626        let mut stride = 1;
627        for d in (0..shape.len()).rev() {
628            let chunk_pos = element_idx[d] / self.chunk_dims[d].max(1);
629            let n_chunks_d = (shape[d] + self.chunk_dims[d] - 1) / self.chunk_dims[d].max(1);
630            idx += chunk_pos * stride;
631            stride *= n_chunks_d;
632        }
633        idx
634    }
635    /// Default chunking: chunk size of 64 in each dimension.
636    pub fn default_for_shape(shape: &[usize]) -> Self {
637        let chunk_dims: Vec<usize> = shape.iter().map(|&s| s.min(64)).collect();
638        Self { chunk_dims }
639    }
640}
641/// Extended attribute support with typed values.
642#[derive(Debug, Clone, PartialEq)]
643#[allow(dead_code)]
644pub enum AttributeValue {
645    /// String value.
646    String(String),
647    /// 64-bit float.
648    Float64(f64),
649    /// 32-bit integer.
650    Int32(i32),
651    /// Boolean.
652    Bool(bool),
653}
654/// A group in the SHDF hierarchy, containing datasets and sub-groups.
655#[derive(Debug, Clone)]
656#[allow(dead_code)]
657pub struct ShdfGroup {
658    /// Group name.
659    pub name: String,
660    /// Datasets in this group.
661    pub datasets: Vec<Dataset>,
662    /// Sub-groups.
663    pub children: Vec<ShdfGroup>,
664    /// Group-level attributes.
665    pub attributes: Vec<(String, String)>,
666}
667#[allow(dead_code)]
668impl ShdfGroup {
669    /// Create a new empty group.
670    pub fn new(name: &str) -> Self {
671        Self {
672            name: name.to_string(),
673            datasets: Vec::new(),
674            children: Vec::new(),
675            attributes: Vec::new(),
676        }
677    }
678    /// Add a Float64 dataset to this group.
679    pub fn add_dataset_f64(&mut self, name: &str, shape: Vec<usize>, data: Vec<f64>) {
680        self.datasets.push(Dataset {
681            name: name.to_string(),
682            shape,
683            dtype: DataType::Float64,
684            data_f64: data,
685            data_i32: Vec::new(),
686            attributes: Vec::new(),
687        });
688    }
689    /// Add an Int32 dataset to this group.
690    pub fn add_dataset_i32(&mut self, name: &str, shape: Vec<usize>, data: Vec<i32>) {
691        self.datasets.push(Dataset {
692            name: name.to_string(),
693            shape,
694            dtype: DataType::Int32,
695            data_f64: Vec::new(),
696            data_i32: data,
697            attributes: Vec::new(),
698        });
699    }
700    /// Add a child group.
701    pub fn add_child(&mut self, child: ShdfGroup) {
702        self.children.push(child);
703    }
704    /// Add an attribute to this group.
705    pub fn add_attribute(&mut self, key: &str, value: &str) {
706        self.attributes.push((key.to_string(), value.to_string()));
707    }
708    /// Find a dataset by name.
709    pub fn get_dataset(&self, name: &str) -> Option<&Dataset> {
710        self.datasets.iter().find(|d| d.name == name)
711    }
712    /// Find a child group by name.
713    pub fn get_child(&self, name: &str) -> Option<&ShdfGroup> {
714        self.children.iter().find(|c| c.name == name)
715    }
716    /// Count total datasets (recursive).
717    pub fn total_datasets(&self) -> usize {
718        self.datasets.len()
719            + self
720                .children
721                .iter()
722                .map(|c| c.total_datasets())
723                .sum::<usize>()
724    }
725    /// Generate a text summary of this group hierarchy.
726    pub fn summary(&self, indent: usize) -> String {
727        let prefix = " ".repeat(indent);
728        let mut out = format!("{prefix}Group: {}\n", self.name);
729        for (k, v) in &self.attributes {
730            out.push_str(&format!("{prefix}  attr: {k} = {v}\n"));
731        }
732        for ds in &self.datasets {
733            let shape_str: Vec<String> = ds.shape.iter().map(|s| s.to_string()).collect();
734            out.push_str(&format!(
735                "{prefix}  Dataset: {} shape=[{}] dtype={:?}\n",
736                ds.name,
737                shape_str.join("x"),
738                ds.dtype,
739            ));
740        }
741        for child in &self.children {
742            out.push_str(&child.summary(indent + 2));
743        }
744        out
745    }
746}
747/// Append new 1-D f64 frames to a growing time-series dataset.
748///
749/// Each call to [`TimeSeriesAppender::append`] concatenates data to the
750/// internal buffer, tracking the number of appended frames.
751#[allow(dead_code)]
752pub struct TimeSeriesAppender {
753    /// Name of the logical dataset.
754    pub name: String,
755    /// Accumulated sample data.
756    pub data: Vec<f64>,
757    /// Number of appended frames.
758    pub n_frames: usize,
759    /// Number of values per frame (fixed at construction time).
760    pub frame_width: usize,
761}
762impl TimeSeriesAppender {
763    /// Create a new appender.
764    ///
765    /// `frame_width` is the number of f64 values in each frame.
766    pub fn new(name: impl Into<String>, frame_width: usize) -> Self {
767        Self {
768            name: name.into(),
769            data: Vec::new(),
770            n_frames: 0,
771            frame_width,
772        }
773    }
774    /// Append a single frame's worth of data.
775    ///
776    /// # Panics
777    /// Panics if `frame.len() != self.frame_width`.
778    pub fn append(&mut self, frame: &[f64]) {
779        assert_eq!(
780            frame.len(),
781            self.frame_width,
782            "frame length {} != frame_width {}",
783            frame.len(),
784            self.frame_width
785        );
786        self.data.extend_from_slice(frame);
787        self.n_frames += 1;
788    }
789    /// Return the total number of f64 samples stored.
790    pub fn total_samples(&self) -> usize {
791        self.data.len()
792    }
793    /// Retrieve a specific frame by index (0-based).
794    pub fn get_frame(&self, idx: usize) -> Option<&[f64]> {
795        let start = idx * self.frame_width;
796        let end = start + self.frame_width;
797        self.data.get(start..end)
798    }
799    /// Export the accumulated data as a 2-D [`Dataset`] (shape `[n_frames, frame_width]`).
800    pub fn to_dataset(&self) -> Dataset {
801        let mut ds = Dataset {
802            name: self.name.clone(),
803            dtype: DataType::Float64,
804            shape: vec![self.n_frames, self.frame_width],
805            data_f64: self.data.clone(),
806            data_i32: Vec::new(),
807            attributes: Vec::new(),
808        };
809        ds.attributes
810            .push(("n_frames".to_string(), self.n_frames.to_string()));
811        ds.attributes
812            .push(("frame_width".to_string(), self.frame_width.to_string()));
813        ds
814    }
815}
816/// Describes a virtual link from one dataset path to another — similar to
817/// HDF5 virtual datasets or external links.
818#[allow(dead_code)]
819#[derive(Debug, Clone)]
820pub struct VirtualLink {
821    /// The logical path within this file (e.g. `"/virtual/positions"`).
822    pub virtual_path: String,
823    /// The source file (may be the same file or an external path).
824    pub source_file: String,
825    /// The dataset path inside the source file.
826    pub source_path: String,
827    /// Optional slice: `[start, stop, step]` for each dimension.
828    pub slices: Vec<[usize; 3]>,
829}
830impl VirtualLink {
831    /// Create a new virtual link with no slicing.
832    pub fn new(
833        virtual_path: impl Into<String>,
834        source_file: impl Into<String>,
835        source_path: impl Into<String>,
836    ) -> Self {
837        Self {
838            virtual_path: virtual_path.into(),
839            source_file: source_file.into(),
840            source_path: source_path.into(),
841            slices: Vec::new(),
842        }
843    }
844    /// Add a per-dimension slice `[start, stop, step]`.
845    pub fn with_slice(mut self, start: usize, stop: usize, step: usize) -> Self {
846        self.slices.push([start, stop, step]);
847        self
848    }
849    /// Return a CDL-style description of this virtual link.
850    pub fn to_cdl(&self) -> String {
851        let slice_str = if self.slices.is_empty() {
852            "(:)".to_string()
853        } else {
854            let parts: Vec<String> = self
855                .slices
856                .iter()
857                .map(|s| format!("{}:{}:{}", s[0], s[1], s[2]))
858                .collect();
859            format!("({})", parts.join(", "))
860        };
861        format!(
862            "{} -> {}:{}{}",
863            self.virtual_path, self.source_file, self.source_path, slice_str
864        )
865    }
866}
867/// An in-memory `.shdf` file.
868#[derive(Debug, Clone)]
869#[allow(dead_code)]
870pub struct ShdfFile {
871    /// Ordered list of datasets.
872    pub datasets: Vec<Dataset>,
873    /// File-level key/value string attributes.
874    pub global_attributes: Vec<(String, String)>,
875}
876impl ShdfFile {
877    /// Create an empty [`ShdfFile`].
878    #[allow(dead_code)]
879    pub fn new() -> Self {
880        ShdfFile {
881            datasets: Vec::new(),
882            global_attributes: Vec::new(),
883        }
884    }
885    /// Append a Float64 dataset.
886    #[allow(dead_code)]
887    pub fn add_dataset_f64(&mut self, name: &str, shape: Vec<usize>, data: Vec<f64>) {
888        self.datasets.push(Dataset {
889            name: name.to_string(),
890            shape,
891            dtype: DataType::Float64,
892            data_f64: data,
893            data_i32: Vec::new(),
894            attributes: Vec::new(),
895        });
896    }
897    /// Append an Int32 dataset.
898    #[allow(dead_code)]
899    pub fn add_dataset_i32(&mut self, name: &str, shape: Vec<usize>, data: Vec<i32>) {
900        self.datasets.push(Dataset {
901            name: name.to_string(),
902            shape,
903            dtype: DataType::Int32,
904            data_f64: Vec::new(),
905            data_i32: data,
906            attributes: Vec::new(),
907        });
908    }
909    /// Add a global (file-level) key/value attribute.
910    #[allow(dead_code)]
911    pub fn add_global_attr(&mut self, key: &str, value: &str) {
912        self.global_attributes
913            .push((key.to_string(), value.to_string()));
914    }
915    /// Look up a Float64 dataset by name and return its data slice.
916    #[allow(dead_code)]
917    pub fn get_f64(&self, name: &str) -> Option<&[f64]> {
918        self.datasets
919            .iter()
920            .find(|d| d.name == name)
921            .map(|d| d.data_f64.as_slice())
922    }
923    /// Look up an Int32 dataset by name and return its data slice.
924    #[allow(dead_code)]
925    pub fn get_i32(&self, name: &str) -> Option<&[i32]> {
926        self.datasets
927            .iter()
928            .find(|d| d.name == name)
929            .map(|d| d.data_i32.as_slice())
930    }
931    /// Serialize the file to a binary blob (little-endian).
932    #[allow(dead_code)]
933    pub fn to_bytes(&self) -> Vec<u8> {
934        let mut out: Vec<u8> = Vec::new();
935        out.extend_from_slice(MAGIC);
936        out.extend_from_slice(&VERSION.to_le_bytes());
937        out.extend_from_slice(&(self.global_attributes.len() as u32).to_le_bytes());
938        for (k, v) in &self.global_attributes {
939            out.extend_from_slice(&encode_string(k));
940            out.extend_from_slice(&encode_string(v));
941        }
942        out.extend_from_slice(&(self.datasets.len() as u32).to_le_bytes());
943        for ds in &self.datasets {
944            out.extend_from_slice(&encode_string(&ds.name));
945            let dtype_byte: u8 = match ds.dtype {
946                DataType::Float64 => 0,
947                DataType::Float32 => 1,
948                DataType::Int32 => 2,
949                DataType::Int64 => 3,
950            };
951            out.push(dtype_byte);
952            out.extend_from_slice(&(ds.shape.len() as u32).to_le_bytes());
953            for &dim in &ds.shape {
954                out.extend_from_slice(&(dim as u64).to_le_bytes());
955            }
956            match ds.dtype {
957                DataType::Float64 => {
958                    out.extend_from_slice(&(ds.data_f64.len() as u64).to_le_bytes());
959                    for &v in &ds.data_f64 {
960                        out.extend_from_slice(&v.to_le_bytes());
961                    }
962                }
963                DataType::Float32 => {
964                    out.extend_from_slice(&(ds.data_f64.len() as u64).to_le_bytes());
965                    for &v in &ds.data_f64 {
966                        out.extend_from_slice(&(v as f32).to_le_bytes());
967                    }
968                }
969                DataType::Int32 => {
970                    out.extend_from_slice(&(ds.data_i32.len() as u64).to_le_bytes());
971                    for &v in &ds.data_i32 {
972                        out.extend_from_slice(&v.to_le_bytes());
973                    }
974                }
975                DataType::Int64 => {
976                    out.extend_from_slice(&(ds.data_i32.len() as u64).to_le_bytes());
977                    for &v in &ds.data_i32 {
978                        out.extend_from_slice(&(v as i64).to_le_bytes());
979                    }
980                }
981            }
982            out.extend_from_slice(&(ds.attributes.len() as u32).to_le_bytes());
983            for (k, v) in &ds.attributes {
984                out.extend_from_slice(&encode_string(k));
985                out.extend_from_slice(&encode_string(v));
986            }
987        }
988        out
989    }
990    /// Deserialize a binary blob written by [`ShdfFile::to_bytes`].
991    ///
992    /// Returns `Err(String)` on any format violation.
993    #[allow(dead_code)]
994    pub fn from_bytes(data: &[u8]) -> Result<Self, String> {
995        let mut pos: usize = 0;
996        if data.len() < 4 {
997            return Err("too short for magic".to_string());
998        }
999        if &data[pos..pos + 4] != MAGIC {
1000            return Err(format!("bad magic: {:?}", &data[pos..pos + 4]));
1001        }
1002        pos += 4;
1003        let version = read_u32(data, &mut pos)?;
1004        if version != VERSION {
1005            return Err(format!("unsupported version: {version}"));
1006        }
1007        let n_global = read_u32(data, &mut pos)? as usize;
1008        let mut global_attributes = Vec::with_capacity(n_global);
1009        for _ in 0..n_global {
1010            let k = decode_string(data, &mut pos)?;
1011            let v = decode_string(data, &mut pos)?;
1012            global_attributes.push((k, v));
1013        }
1014        let n_datasets = read_u32(data, &mut pos)? as usize;
1015        let mut datasets = Vec::with_capacity(n_datasets);
1016        for _ in 0..n_datasets {
1017            let name = decode_string(data, &mut pos)?;
1018            let dtype_byte = read_u8(data, &mut pos)?;
1019            let dtype = match dtype_byte {
1020                0 => DataType::Float64,
1021                1 => DataType::Float32,
1022                2 => DataType::Int32,
1023                3 => DataType::Int64,
1024                _ => return Err(format!("unknown dtype byte: {dtype_byte}")),
1025            };
1026            let n_dims = read_u32(data, &mut pos)? as usize;
1027            let mut shape = Vec::with_capacity(n_dims);
1028            for _ in 0..n_dims {
1029                shape.push(read_u64(data, &mut pos)? as usize);
1030            }
1031            let n_elems = read_u64(data, &mut pos)? as usize;
1032            let mut data_f64 = Vec::new();
1033            let mut data_i32 = Vec::new();
1034            match dtype {
1035                DataType::Float64 => {
1036                    data_f64.reserve(n_elems);
1037                    for _ in 0..n_elems {
1038                        data_f64.push(read_f64(data, &mut pos)?);
1039                    }
1040                }
1041                DataType::Float32 => {
1042                    data_f64.reserve(n_elems);
1043                    for _ in 0..n_elems {
1044                        data_f64.push(read_f32(data, &mut pos)? as f64);
1045                    }
1046                }
1047                DataType::Int32 => {
1048                    data_i32.reserve(n_elems);
1049                    for _ in 0..n_elems {
1050                        data_i32.push(read_i32(data, &mut pos)?);
1051                    }
1052                }
1053                DataType::Int64 => {
1054                    data_i32.reserve(n_elems);
1055                    for _ in 0..n_elems {
1056                        data_i32.push(read_i64(data, &mut pos)? as i32);
1057                    }
1058                }
1059            }
1060            let n_attrs = read_u32(data, &mut pos)? as usize;
1061            let mut attributes = Vec::with_capacity(n_attrs);
1062            for _ in 0..n_attrs {
1063                let k = decode_string(data, &mut pos)?;
1064                let v = decode_string(data, &mut pos)?;
1065                attributes.push((k, v));
1066            }
1067            datasets.push(Dataset {
1068                name,
1069                shape,
1070                dtype,
1071                data_f64,
1072                data_i32,
1073                attributes,
1074            });
1075        }
1076        Ok(ShdfFile {
1077            datasets,
1078            global_attributes,
1079        })
1080    }
1081    /// Return a human-readable summary of the file contents.
1082    #[allow(dead_code)]
1083    pub fn write_to_text(&self) -> String {
1084        let mut out = String::new();
1085        out.push_str("=== SHDF File Summary ===\n");
1086        if !self.global_attributes.is_empty() {
1087            out.push_str("Global attributes:\n");
1088            for (k, v) in &self.global_attributes {
1089                out.push_str(&format!("  {k} = {v}\n"));
1090            }
1091        }
1092        out.push_str(&format!("Datasets: {}\n", self.datasets.len()));
1093        for ds in &self.datasets {
1094            let shape_str: Vec<String> = ds.shape.iter().map(|s| s.to_string()).collect();
1095            out.push_str(&format!(
1096                "  [{}] shape=[{}] dtype={:?}\n",
1097                ds.name,
1098                shape_str.join("×"),
1099                ds.dtype,
1100            ));
1101            let preview = match ds.dtype {
1102                DataType::Float64 | DataType::Float32 => {
1103                    let vals: Vec<String> = ds
1104                        .data_f64
1105                        .iter()
1106                        .take(5)
1107                        .map(|v| format!("{v:.6}"))
1108                        .collect();
1109                    vals.join(", ")
1110                }
1111                DataType::Int32 | DataType::Int64 => {
1112                    let vals: Vec<String> =
1113                        ds.data_i32.iter().take(5).map(|v| v.to_string()).collect();
1114                    vals.join(", ")
1115                }
1116            };
1117            if !preview.is_empty() {
1118                out.push_str(&format!("    first values: [{preview}]\n"));
1119            }
1120            if !ds.attributes.is_empty() {
1121                out.push_str("    attributes:\n");
1122                for (k, v) in &ds.attributes {
1123                    out.push_str(&format!("      {k} = {v}\n"));
1124                }
1125            }
1126        }
1127        out
1128    }
1129}
1130impl Default for ShdfFile {
1131    fn default() -> Self {
1132        Self::new()
1133    }
1134}
1135/// Helper for managing typed attributes.
1136#[allow(dead_code)]
1137pub struct AttributeHelper;
1138#[allow(dead_code)]
1139impl AttributeHelper {
1140    /// Serialize an attribute value to a string.
1141    pub fn to_string(val: &AttributeValue) -> String {
1142        match val {
1143            AttributeValue::String(s) => format!("s:{s}"),
1144            AttributeValue::Float64(f) => format!("f:{f}"),
1145            AttributeValue::Int32(i) => format!("i:{i}"),
1146            AttributeValue::Bool(b) => format!("b:{b}"),
1147        }
1148    }
1149    /// Deserialize an attribute value from a string.
1150    pub fn from_string(s: &str) -> AttributeValue {
1151        if let Some(rest) = s.strip_prefix("f:")
1152            && let Ok(f) = rest.parse::<f64>()
1153        {
1154            return AttributeValue::Float64(f);
1155        }
1156        if let Some(rest) = s.strip_prefix("i:")
1157            && let Ok(i) = rest.parse::<i32>()
1158        {
1159            return AttributeValue::Int32(i);
1160        }
1161        if let Some(rest) = s.strip_prefix("b:") {
1162            return AttributeValue::Bool(rest == "true");
1163        }
1164        if let Some(rest) = s.strip_prefix("s:") {
1165            return AttributeValue::String(rest.to_string());
1166        }
1167        AttributeValue::String(s.to_string())
1168    }
1169}