Variables got the first unused dimension of equal size, so a variable on an unlimited dimension with fewer records got an anonymous dim_<n>, and dimensions of one size could be swapped. Resolve them as netCDF-C does (libhdf5/hdf5open.c): _Netcdf4Coordinates ids, else the scales DIMENSION_LIST references (the last one attached to an axis), searched in the variable's group and its parents; a coordinate variable is on its own scale. Size matching remains only for axes the file names nothing for. variables()/variable_names() leave out dimension scales that are only dimensions, and _nc4_non_coord_<name> is the variable <name>. Variable::shape is the netCDF shape (an unlimited dimension's length) and the reads pad unwritten records with the fill value (_FillValue, else NC_FILL_*; NaN from read_f64); Variable::stored_shape is the HDF5 extent. New NetCDF4File::variable_names. Tests compare with netCDF4-python variable by variable: the known-issues reproducer, equal sizes, (p, p), scalars, inherited dimensions, unwritten records, h5py dimension scales, h5netcdf and xarray files. CI installs h5netcdf. known-issues entry moved to Fixed (history); stale open-table row for the unlimited-size fix removed. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
137 lines
4.2 KiB
Rust
137 lines
4.2 KiB
Rust
//! NetCDF-4 read support built on clawhdf5.
|
|
//!
|
|
//! NetCDF-4 files are HDF5 files following specific conventions. This crate
|
|
//! provides a high-level API for reading NetCDF-4 metadata (dimensions,
|
|
//! variables, CF attributes, groups) from HDF5 files written by netCDF4-python,
|
|
//! nco, CDO, or any compliant NetCDF-4 writer.
|
|
//!
|
|
//! # Example
|
|
//!
|
|
//! ```no_run
|
|
//! use clawhdf5_netcdf4::NetCDF4File;
|
|
//!
|
|
//! let file = NetCDF4File::open("data.nc").unwrap();
|
|
//! for dim in file.dimensions().unwrap() {
|
|
//! println!("{}: {} (unlimited={})", dim.name, dim.size, dim.is_unlimited);
|
|
//! }
|
|
//! for mut var in file.variables().unwrap() {
|
|
//! println!("{}: {:?}", var.name(), var.shape().unwrap());
|
|
//! let cf = var.cf_attributes().unwrap();
|
|
//! if let Some(units) = &cf.units {
|
|
//! println!(" units: {units}");
|
|
//! }
|
|
//! }
|
|
//! ```
|
|
|
|
pub mod cf;
|
|
pub mod dimension;
|
|
pub mod error;
|
|
pub mod group;
|
|
mod scope;
|
|
pub mod types;
|
|
pub mod variable;
|
|
|
|
pub use cf::{CfAttributes, FillValue};
|
|
pub use clawhdf5::AttrValue;
|
|
pub use dimension::Dimension;
|
|
pub use error::Error;
|
|
pub use group::NetCDF4Group;
|
|
pub use types::NcType;
|
|
pub use variable::Variable;
|
|
|
|
use std::collections::HashMap;
|
|
|
|
/// A NetCDF-4 file reader.
|
|
///
|
|
/// Wraps a clawhdf5 File and provides NetCDF-4 semantics: dimensions,
|
|
/// variables with CF attributes, groups, and type mapping.
|
|
pub struct NetCDF4File {
|
|
hdf5: clawhdf5::File,
|
|
}
|
|
|
|
impl NetCDF4File {
|
|
/// Open a NetCDF-4 file from a filesystem path.
|
|
pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
|
|
let hdf5 = clawhdf5::File::open(path)?;
|
|
Ok(Self { hdf5 })
|
|
}
|
|
|
|
/// Open a NetCDF-4 file from in-memory bytes.
|
|
pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> {
|
|
let hdf5 = clawhdf5::File::from_bytes(data)?;
|
|
Ok(Self { hdf5 })
|
|
}
|
|
|
|
/// Get the _NCProperties root attribute, if present.
|
|
///
|
|
/// This attribute is written by standard NetCDF-4 libraries and contains
|
|
/// provenance information (library version, HDF5 version, etc.).
|
|
pub fn nc_properties(&self) -> Result<Option<String>, Error> {
|
|
let attrs = self.hdf5.root().attrs()?;
|
|
match attrs.get("_NCProperties") {
|
|
Some(AttrValue::String(s)) => Ok(Some(s.clone())),
|
|
_ => Ok(None),
|
|
}
|
|
}
|
|
|
|
/// List dimensions defined in the root group.
|
|
pub fn dimensions(&self) -> Result<Vec<Dimension>, Error> {
|
|
Ok(dimension::group_dims(&self.hdf5, &self.hdf5.root())?.dims)
|
|
}
|
|
|
|
/// List all variables in the root group: its datasets, except the
|
|
/// dimension scales that are only dimensions (netCDF-C does not list
|
|
/// them either).
|
|
pub fn variables(&self) -> Result<Vec<Variable<'_>>, Error> {
|
|
scope::Scope::new(&self.hdf5, "/")?.variables()
|
|
}
|
|
|
|
/// The names of the root group's variables (see
|
|
/// [`variables`](Self::variables)).
|
|
pub fn variable_names(&self) -> Result<Vec<String>, Error> {
|
|
scope::Scope::new(&self.hdf5, "/")?.variable_names()
|
|
}
|
|
|
|
/// Get a specific variable by name from the root group.
|
|
pub fn variable(&self, name: &str) -> Result<Variable<'_>, Error> {
|
|
scope::variable_at(&self.hdf5, "", name)
|
|
}
|
|
|
|
/// Read all global (root group) attributes.
|
|
pub fn global_attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
|
|
Ok(self.hdf5.root().attrs()?)
|
|
}
|
|
|
|
/// List subgroup names in the root group.
|
|
pub fn group_names(&self) -> Result<Vec<String>, Error> {
|
|
Ok(self.hdf5.root().groups()?)
|
|
}
|
|
|
|
/// Get a subgroup by name.
|
|
pub fn group(&self, name: &str) -> Result<NetCDF4Group<'_>, Error> {
|
|
let hdf5_group = self
|
|
.hdf5
|
|
.group(name)
|
|
.map_err(|_| Error::GroupNotFound(name.to_string()))?;
|
|
Ok(NetCDF4Group::new(
|
|
name.to_string(),
|
|
name.to_string(),
|
|
&self.hdf5,
|
|
hdf5_group,
|
|
))
|
|
}
|
|
|
|
/// Access the underlying HDF5 file for advanced operations.
|
|
pub fn hdf5_file(&self) -> &clawhdf5::File {
|
|
&self.hdf5
|
|
}
|
|
}
|
|
|
|
impl std::fmt::Debug for NetCDF4File {
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
f.debug_struct("NetCDF4File")
|
|
.field("hdf5", &self.hdf5)
|
|
.finish()
|
|
}
|
|
}
|