Skip to main content

netcdf_reader/nc4/
mod.rs

1//! NetCDF-4 (HDF5-backed) format support.
2//!
3//! This module maps the HDF5 data model to the NetCDF data model:
4//! - HDF5 groups become NetCDF groups
5//! - HDF5 datasets become NetCDF variables
6//! - HDF5 attributes become NetCDF attributes
7//! - Dimensions are reconstructed from `DIMENSION_LIST` and `_Netcdf4Dimid` attributes
8//!
9//! Requires the `netcdf4` feature (enabled by default).
10
11pub mod attributes;
12pub mod dimensions;
13pub mod groups;
14pub mod types;
15pub mod variables;
16
17use std::path::Path;
18
19use hdf5_reader::datatype_api::H5Type;
20use hdf5_reader::Hdf5File;
21use ndarray::ArrayD;
22#[cfg(feature = "rayon")]
23use rayon::ThreadPool;
24
25use crate::error::{Error, Result};
26use crate::types::NcGroup;
27
28/// Dispatch on `NcType` to read data and promote to `f64`.
29///
30/// `$dtype` must be an expression of type `&NcType`.
31/// `$read_expr` is a macro-like callback: for each numeric type `$T`,
32/// the macro evaluates `$read_expr` with `$T` substituted in.
33///
34/// Usage:
35/// ```ignore
36/// dispatch_read_as_f64!(&var.dtype, |$T| dataset.read_array::<$T>())
37/// ```
38macro_rules! dispatch_read_as_f64 {
39    ($dtype:expr, |$T:ident| $read_expr:expr) => {{
40        use crate::types::NcType;
41        match $dtype {
42            NcType::Byte => {
43                type $T = i8;
44                let arr = $read_expr?;
45                Ok(arr.mapv(|v| v as f64))
46            }
47            NcType::Short => {
48                type $T = i16;
49                let arr = $read_expr?;
50                Ok(arr.mapv(|v| v as f64))
51            }
52            NcType::Int => {
53                type $T = i32;
54                let arr = $read_expr?;
55                Ok(arr.mapv(|v| v as f64))
56            }
57            NcType::Float => {
58                type $T = f32;
59                let arr = $read_expr?;
60                Ok(arr.mapv(|v| v as f64))
61            }
62            NcType::Double => {
63                type $T = f64;
64                Ok($read_expr?)
65            }
66            NcType::UByte => {
67                type $T = u8;
68                let arr = $read_expr?;
69                Ok(arr.mapv(|v| v as f64))
70            }
71            NcType::UShort => {
72                type $T = u16;
73                let arr = $read_expr?;
74                Ok(arr.mapv(|v| v as f64))
75            }
76            NcType::UInt => {
77                type $T = u32;
78                let arr = $read_expr?;
79                Ok(arr.mapv(|v| v as f64))
80            }
81            NcType::Int64 => {
82                type $T = i64;
83                let arr = $read_expr?;
84                Ok(arr.mapv(|v| v as f64))
85            }
86            NcType::UInt64 => {
87                type $T = u64;
88                let arr = $read_expr?;
89                Ok(arr.mapv(|v| v as f64))
90            }
91            NcType::Char => Err(Error::TypeMismatch {
92                expected: "numeric type".to_string(),
93                actual: "Char".to_string(),
94            }),
95            NcType::String => Err(Error::TypeMismatch {
96                expected: "numeric type".to_string(),
97                actual: "String".to_string(),
98            }),
99            other => Err(Error::TypeMismatch {
100                expected: "numeric type".to_string(),
101                actual: format!("{:?}", other),
102            }),
103        }
104    }};
105}
106
107/// An opened NetCDF-4 file (backed by HDF5).
108pub struct Nc4File {
109    hdf5: Hdf5File,
110    root_group: NcGroup,
111}
112
113impl Nc4File {
114    /// Create from an already-opened HDF5 file and pre-built root group.
115    pub(crate) fn from_hdf5(hdf5: Hdf5File, root_group: NcGroup) -> Self {
116        Nc4File { hdf5, root_group }
117    }
118
119    /// Open a NetCDF-4 file from disk.
120    pub fn open(path: &Path) -> Result<Self> {
121        let hdf5 = Hdf5File::open(path)?;
122        let root_group = groups::build_root_group(&hdf5)?;
123        Ok(Nc4File { hdf5, root_group })
124    }
125
126    /// Open a NetCDF-4 file from in-memory bytes.
127    pub fn from_bytes(data: &[u8]) -> Result<Self> {
128        let hdf5 = Hdf5File::from_bytes(data)?;
129        let root_group = groups::build_root_group(&hdf5)?;
130        Ok(Nc4File { hdf5, root_group })
131    }
132
133    /// The root group.
134    pub fn root_group(&self) -> &NcGroup {
135        &self.root_group
136    }
137
138    /// Check if this file uses the classic data model (`_nc3_strict`).
139    ///
140    /// This checks the raw HDF5 root group attributes (before the internal
141    /// attribute filter removes `_nc3_strict`).
142    pub fn is_classic_model(&self) -> bool {
143        self.hdf5
144            .root_group()
145            .ok()
146            .and_then(|g| g.attribute("_nc3_strict").ok())
147            .is_some()
148    }
149
150    /// Read a variable's data as a typed array.
151    ///
152    /// Looks up the variable by path relative to the root group, then opens the
153    /// matching HDF5 dataset and reads the data.
154    pub fn read_variable<T: H5Type>(&self, path: &str) -> Result<ArrayD<T>> {
155        let normalized = normalize_dataset_path(path)?;
156        let dataset = self.hdf5.dataset(normalized)?;
157        Ok(dataset.read_array::<T>()?)
158    }
159
160    #[cfg(feature = "rayon")]
161    pub fn read_variable_parallel<T: H5Type>(&self, path: &str) -> Result<ArrayD<T>> {
162        let normalized = normalize_dataset_path(path)?;
163        let dataset = self.hdf5.dataset(normalized)?;
164        Ok(dataset.read_array_parallel::<T>()?)
165    }
166
167    #[cfg(feature = "rayon")]
168    pub fn read_variable_in_pool<T: H5Type>(
169        &self,
170        path: &str,
171        pool: &ThreadPool,
172    ) -> Result<ArrayD<T>> {
173        let normalized = normalize_dataset_path(path)?;
174        let dataset = self.hdf5.dataset(normalized)?;
175        Ok(dataset.read_array_in_pool::<T>(pool)?)
176    }
177}
178
179impl Nc4File {
180    /// Read a variable with automatic type promotion to f64.
181    ///
182    /// Reads in the native HDF5 type and promotes to f64 via `mapv`.
183    pub fn read_variable_as_f64(&self, path: &str) -> Result<ArrayD<f64>> {
184        let normalized = normalize_dataset_path(path)?;
185        let var = self
186            .root_group
187            .variable(normalized)
188            .ok_or_else(|| Error::VariableNotFound(path.to_string()))?;
189        let dataset = self.hdf5.dataset(normalized)?;
190
191        debug_assert_eq!(dataset.shape(), &var.shape()[..]);
192
193        dispatch_read_as_f64!(&var.dtype, |T| dataset.read_array::<T>())
194    }
195
196    /// Read a slice of a variable with automatic type promotion to f64.
197    pub fn read_variable_slice_as_f64(
198        &self,
199        path: &str,
200        selection: &crate::types::NcSliceInfo,
201    ) -> Result<ArrayD<f64>> {
202        let normalized = normalize_dataset_path(path)?;
203        let var = self
204            .root_group
205            .variable(normalized)
206            .ok_or_else(|| Error::VariableNotFound(path.to_string()))?;
207        let dataset = self.hdf5.dataset(normalized)?;
208        let hdf5_sel = selection.to_hdf5_slice_info();
209
210        dispatch_read_as_f64!(&var.dtype, |T| dataset.read_slice::<T>(&hdf5_sel))
211    }
212
213    /// Read a typed slice of a variable (NC4 delegation).
214    pub fn read_variable_slice<T: H5Type>(
215        &self,
216        path: &str,
217        selection: &crate::types::NcSliceInfo,
218    ) -> Result<ArrayD<T>> {
219        let normalized = normalize_dataset_path(path)?;
220        let dataset = self.hdf5.dataset(normalized)?;
221        let hdf5_sel = selection.to_hdf5_slice_info();
222        Ok(dataset.read_slice::<T>(&hdf5_sel)?)
223    }
224
225    /// Read a typed slice of a variable using chunk-level parallelism.
226    ///
227    /// Chunked datasets decompress overlapping chunks in parallel via Rayon.
228    /// Non-chunked layouts fall back to `read_variable_slice`.
229    #[cfg(feature = "rayon")]
230    pub fn read_variable_slice_parallel<T: H5Type>(
231        &self,
232        path: &str,
233        selection: &crate::types::NcSliceInfo,
234    ) -> Result<ArrayD<T>> {
235        let normalized = normalize_dataset_path(path)?;
236        let dataset = self.hdf5.dataset(normalized)?;
237        let hdf5_sel = selection.to_hdf5_slice_info();
238        Ok(dataset.read_slice_parallel::<T>(&hdf5_sel)?)
239    }
240}
241
242fn normalize_dataset_path(path: &str) -> Result<&str> {
243    let trimmed = path.trim_matches('/');
244    if trimmed.is_empty() {
245        return Err(Error::VariableNotFound(path.to_string()));
246    }
247    Ok(trimmed)
248}