clawhdf5-netcdf4: an unlimited dimension reports its current length
CI / test-arm64 (pull_request) Successful in 1m34s
CI / test (pull_request) Successful in 30m38s

Dimension::size of an unlimited dimension was its dimension scale's
extent, which netCDF-C leaves at 0, so it read 0 for a dimension holding
records. It is now what netCDF-C reports (nc4_find_dim_len): the largest
current extent of the variables using it in any group, found through the
scale's REFERENCE_LIST, a coordinate variable's own extent included; 0
when nothing has been written.

interop_tests::unlimited_dimension_lengths_match_netcdf4_python compares
with netCDF4-python (variables of different lengths, one in a subgroup,
an unwritten dimension, a coordinate variable shorter than another
variable on its dimension, a subgroup's own dimension); before the fix it
got time 0/6, rec 3/5, srec 0/1.

The known-issues entry moves to Fixed; the crate README's warning goes.
A new open entry records a related bug found meanwhile: variables'
dimensions are matched by size, not DIMENSION_LIST.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-28 21:24:59 -05:00
co-authored by Claude Opus 5.5
parent 3eca5d8334
commit 31efac1ae1
5 changed files with 236 additions and 21 deletions
@@ -377,3 +377,87 @@ ds.close()
let names = file.variable("name").unwrap().read_string().unwrap();
assert_eq!(names, vec!["Oslo", "", "São Paulo", "x"]);
}
// ===========================================================================
// Unlimited dimensions: the length netCDF-C reports
// ===========================================================================
/// An unlimited dimension's length is the largest extent of the variables
/// using it, in any group (netCDF-C's `nc4_find_dim_len`), not its dimension
/// scale's extent (which netCDF-C leaves at 0): variables of different
/// lengths, one in a subgroup, a dimension no variable has written, a
/// coordinate variable, a subgroup's own unlimited dimension. Compared with
/// what netCDF4-python reports for the same file.
#[test]
fn unlimited_dimension_lengths_match_netcdf4_python() {
skip_if_no_netcdf4!();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("unlimited.nc");
let script = format!(
r#"
import netCDF4 as nc
import numpy as np
with nc.Dataset({path:?}, "w", format="NETCDF4") as f:
f.createDimension("time", None)
f.createDimension("empty", None)
f.createDimension("rec", None)
f.createDimension("x", 3)
f.createVariable("t", "f8", ("time", "x"))[0:2, :] = np.ones((2, 3))
f.createVariable("a", "i4", ("time",))[0:4] = np.arange(4)
f.createVariable("e", "i4", ("empty",))
f.createVariable("rec", "f4", ("rec",))[0:3] = [1, 2, 3]
f.createVariable("r", "f4", ("x", "rec"))[:, 0:5] = np.ones((3, 5))
g = f.createGroup("sub")
g.createVariable("c", "i4", ("time",))[0:6] = np.arange(6)
g.createDimension("srec", None)
g.createVariable("s", "i4", ("srec", "x"))[0:1, :] = np.ones((1, 3))
with nc.Dataset({path:?}) as f:
for grp in (f, f.groups["sub"]):
for name, d in grp.dimensions.items():
print(grp.path, name, len(d), d.isunlimited())
"#,
path = path.display().to_string()
);
let out = Command::new(python())
.args(["-c", &script])
.output()
.expect("failed to run python3");
assert!(
out.status.success(),
"{}",
String::from_utf8_lossy(&out.stderr)
);
let expected: Vec<String> = String::from_utf8(out.stdout)
.unwrap()
.lines()
.map(str::to_string)
.collect();
// time: t has 2 records, a 4 and sub/c 6; rec: the coordinate variable
// has 3, r 5; empty: nothing written.
for want in [
"/ time 6 True",
"/ empty 0 True",
"/ rec 5 True",
"/ x 3 False",
"/sub srec 1 True",
] {
assert!(
expected.iter().any(|l| l == want),
"netCDF4 reports {expected:?}"
);
}
let file = NetCDF4File::open(&path).unwrap();
let sub = file.group("sub").unwrap();
let mut got = Vec::new();
for (path, dims) in [
("/", file.dimensions().unwrap()),
("/sub", sub.dimensions().unwrap()),
] {
for d in dims {
let unlimited = if d.is_unlimited { "True" } else { "False" };
got.push(format!("{path} {} {} {unlimited}", d.name, d.size));
}
}
assert_eq!(got, expected);
}