Skip to main content

rustyhdf5_netcdf4/
variable.rs

1//! NetCDF-4 variable representation.
2//!
3//! Variables in NetCDF-4 are HDF5 datasets. This module wraps them with
4//! dimension associations and CF attribute support.
5
6use std::collections::HashMap;
7
8use rustyhdf5::AttrValue;
9
10use crate::cf::{self, CfAttributes};
11use crate::dimension::Dimension;
12use crate::error::Error;
13use crate::types::{dtype_to_nctype, NcType};
14
15/// A NetCDF-4 variable backed by an HDF5 dataset.
16pub struct Variable<'f> {
17    /// Variable name.
18    name: String,
19    /// Underlying HDF5 dataset.
20    dataset: rustyhdf5::Dataset<'f>,
21    /// Dimensions associated with this variable.
22    dims: Vec<Dimension>,
23    /// Cached attributes.
24    attrs_cache: Option<HashMap<String, AttrValue>>,
25}
26
27impl<'f> Variable<'f> {
28    /// Create a new Variable wrapping an HDF5 dataset.
29    pub(crate) fn new(
30        name: String,
31        dataset: rustyhdf5::Dataset<'f>,
32        dims: Vec<Dimension>,
33    ) -> Self {
34        Self {
35            name,
36            dataset,
37            dims,
38            attrs_cache: None,
39        }
40    }
41
42    /// Variable name.
43    pub fn name(&self) -> &str {
44        &self.name
45    }
46
47    /// The dimensions of this variable.
48    pub fn dimensions(&self) -> &[Dimension] {
49        &self.dims
50    }
51
52    /// The shape of this variable (dimension sizes).
53    pub fn shape(&self) -> Result<Vec<u64>, Error> {
54        Ok(self.dataset.shape()?)
55    }
56
57    /// The NetCDF data type of this variable.
58    pub fn nc_type(&self) -> Result<NcType, Error> {
59        let dtype = self.dataset.dtype()?;
60        Ok(dtype_to_nctype(&dtype))
61    }
62
63    /// Read all attributes as a HashMap.
64    pub fn attrs(&mut self) -> Result<&HashMap<String, AttrValue>, Error> {
65        if self.attrs_cache.is_none() {
66            self.attrs_cache = Some(self.dataset.attrs()?);
67        }
68        Ok(self.attrs_cache.as_ref().unwrap())
69    }
70
71    /// Extract CF convention attributes.
72    pub fn cf_attributes(&mut self) -> Result<CfAttributes, Error> {
73        let attrs = self.attrs()?;
74        Ok(cf::extract_cf_attributes(attrs))
75    }
76
77    /// Read data as f64 with scale_factor/add_offset applied.
78    ///
79    /// Missing values (matching `_FillValue` or `missing_value`) become NaN.
80    /// If no scale_factor or add_offset attributes exist, returns the raw f64 data.
81    pub fn read_f64(&mut self) -> Result<Vec<f64>, Error> {
82        let raw = self.dataset.read_f64()?;
83        let cf = self.cf_attributes()?;
84        Ok(cf::apply_scale_offset(&raw, &cf))
85    }
86
87    /// Read raw data as f64 without any scale/offset transformation.
88    pub fn read_raw_f64(&self) -> Result<Vec<f64>, Error> {
89        Ok(self.dataset.read_f64()?)
90    }
91
92    /// Read raw data as f32 without any scale/offset transformation.
93    pub fn read_raw_f32(&self) -> Result<Vec<f32>, Error> {
94        Ok(self.dataset.read_f32()?)
95    }
96
97    /// Read raw data as i32 without any scale/offset transformation.
98    pub fn read_raw_i32(&self) -> Result<Vec<i32>, Error> {
99        Ok(self.dataset.read_i32()?)
100    }
101
102    /// Read raw data as i64 without any scale/offset transformation.
103    pub fn read_raw_i64(&self) -> Result<Vec<i64>, Error> {
104        Ok(self.dataset.read_i64()?)
105    }
106
107    /// Read raw data as u64 without any scale/offset transformation.
108    pub fn read_raw_u64(&self) -> Result<Vec<u64>, Error> {
109        Ok(self.dataset.read_u64()?)
110    }
111
112    /// Read raw data as strings.
113    pub fn read_string(&self) -> Result<Vec<String>, Error> {
114        Ok(self.dataset.read_string()?)
115    }
116
117    /// Read raw bytes without any type conversion.
118    pub fn read_raw(&self) -> Result<Vec<u8>, Error> {
119        // Use the low-level format read to get raw bytes.
120        // The high-level API doesn't expose read_raw directly,
121        // so we read as the smallest numeric type that matches the element size.
122        // For the NetCDF use case, callers should prefer typed reads.
123        let dtype = self.dataset.dtype()?;
124        match dtype {
125            rustyhdf5::DType::F64 => {
126                let vals = self.dataset.read_f64()?;
127                Ok(vals.iter().flat_map(|v| v.to_le_bytes()).collect())
128            }
129            rustyhdf5::DType::F32 => {
130                let vals = self.dataset.read_f32()?;
131                Ok(vals.iter().flat_map(|v| v.to_le_bytes()).collect())
132            }
133            rustyhdf5::DType::I32 => {
134                let vals = self.dataset.read_i32()?;
135                Ok(vals.iter().flat_map(|v| v.to_le_bytes()).collect())
136            }
137            rustyhdf5::DType::I64 => {
138                let vals = self.dataset.read_i64()?;
139                Ok(vals.iter().flat_map(|v| v.to_le_bytes()).collect())
140            }
141            rustyhdf5::DType::U64 => {
142                let vals = self.dataset.read_u64()?;
143                Ok(vals.iter().flat_map(|v| v.to_le_bytes()).collect())
144            }
145            _ => {
146                // Fallback: try reading as f64 and convert to bytes
147                let vals = self.dataset.read_f64()?;
148                Ok(vals.iter().flat_map(|v| v.to_le_bytes()).collect())
149            }
150        }
151    }
152
153    /// Whether this is a coordinate variable (name matches a dimension name).
154    pub fn is_coordinate(&self) -> bool {
155        self.dims.len() == 1 && self.dims[0].name == self.name
156    }
157}
158
159impl std::fmt::Debug for Variable<'_> {
160    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
161        f.debug_struct("Variable")
162            .field("name", &self.name)
163            .field("dims", &self.dims)
164            .finish()
165    }
166}
167
168/// Build variables from a group's datasets and associated dimensions.
169pub(crate) fn build_variables<'f>(
170    group: &rustyhdf5::Group<'f>,
171    available_dims: &[Dimension],
172) -> Result<Vec<Variable<'f>>, Error> {
173    let dataset_names = group.datasets()?;
174    let mut variables = Vec::new();
175
176    for ds_name in &dataset_names {
177        let ds = group.dataset(ds_name)?;
178        let shape = ds.shape()?;
179
180        // Associate dimensions with this variable.
181        // First try DIMENSION_LIST attribute, then fall back to shape matching.
182        let var_dims = match_dimensions_to_variable(&shape, available_dims);
183
184        variables.push(Variable::new(ds_name.clone(), ds, var_dims));
185    }
186
187    Ok(variables)
188}
189
190/// Match dimensions to a variable based on shape.
191///
192/// For each axis of the variable, find a dimension with matching size.
193/// If multiple dimensions have the same size, prefer exact name matching
194/// from the convention order.
195pub(crate) fn match_dimensions_to_variable(shape: &[u64], available_dims: &[Dimension]) -> Vec<Dimension> {
196    let mut result = Vec::with_capacity(shape.len());
197
198    // Track which dimensions have been used to avoid duplicates
199    let mut used = vec![false; available_dims.len()];
200
201    for &dim_size in shape {
202        let mut matched = false;
203
204        // Find a dimension with matching size that hasn't been used yet
205        for (i, dim) in available_dims.iter().enumerate() {
206            if !used[i] && dim.size == dim_size {
207                result.push(dim.clone());
208                used[i] = true;
209                matched = true;
210                break;
211            }
212        }
213
214        if !matched {
215            // Create an anonymous dimension for unmatched sizes
216            result.push(Dimension {
217                name: format!("dim_{dim_size}"),
218                size: dim_size,
219                is_unlimited: false,
220            });
221        }
222    }
223
224    result
225}