Correctly handle HDF5 fillvalue for string dtype arrays.#988
Correctly handle HDF5 fillvalue for string dtype arrays.#988sharkinsspatial wants to merge 10 commits into
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #988 +/- ##
==========================================
- Coverage 90.36% 90.30% -0.06%
==========================================
Files 37 37
Lines 2242 2260 +18
==========================================
+ Hits 2026 2041 +15
- Misses 216 219 +3
🚀 New features to boost your workflow:
|
…llback (#993) * fix: handle H5Dataset missing fillvalue attribute with dtype-based fallback Some h5py Dataset objects do not expose a fillvalue attribute. Guard against AttributeError in _get_fill_value and fall back to np.ma.default_fill_value for the dataset's dtype. Adds a unit test covering this path. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * Add release note * fix: catch RuntimeError (not AttributeError) from h5py fillvalue h5py.Dataset.fillvalue delegates to the HDF5 C library via Cython and raises RuntimeError for unsupported dtypes (e.g. variable-length strings, compound types). AttributeError can never fire on a real h5py.Dataset since fillvalue is always a defined property. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Good to merge now so I can include it in the release? |
let me take a quick look at the codecs changes. Assigning vlen bytes to all object dtypes seems a bit weird to me. |
| try: | ||
| raw = dataset.fillvalue | ||
| except RuntimeError: | ||
| return np.ma.default_fill_value(dataset.dtype) |
There was a problem hiding this comment.
I'm wondering if this could be introducing a different type of bug where now we are introducing a value for the zarr metadata fill_value that could differ from what is in the dataset's original metadata. I realized today that for GPM IMERG HH precipitation the attrs from the h5py Dataset include both _FillValue and CodeMissingValue which both are -9999.0:
h5ds = h5py.File(s3fs.open(gpm_imerg_s3_url))
h5ds['/Grid/precipitation'].attrs['CodeMissingValue']
# np.bytes_(b'-9999.9')
h5ds['/Grid/precipitation'].attrs['_FillValue']
# np.float32(-9999.9)Whereas this fallback returns 1e+20:
np.ma.default_fill_value(h5ds['/Grid/precipitation'].dtype)
# 1e+20
I recognize that CodeMissingValue is not a standard attribute (as far as I can tell), but should the zarr fill_value attribute fall back to the _FillValue attribute picked up by h5py?
Looking at NASA-IMPACT/veda-odd#371 would this be handled by the more general purpose phase 3 "fill_value_defined() distinction: stop propagating h5py-default fills to zarr storage."
There was a problem hiding this comment.
@abarciauskas-bgse For clarification, fill_value and _FillValue are distinct semantic concepts https://virtualizarr.readthedocs.io/en/stable/custom_parsers.html#fill-values. fill_value is always set by HDF5 but as you noted above, not all types of fill_value that HDF5 can produce are supported by Zarr v3.
There was a problem hiding this comment.
Right ok, thanks for reminding me about that distinction -- I recalled there were 2 semantic concepts but forgot about the details and that excellent documentation. In that case, since this function is for the first fill value concept -- Value for uninitialized chunks - (e.g., Zarr fill_value) -- then probably this fallback is reasonable?
And I recognize that the h5ds['/Grid/precipitation'].attrs['_FillValue'] is for the 2nd concept Sentinel value - (e.g., CF _FillValue ))
There was a problem hiding this comment.
The CodeMissingValue is a different issue, but this is a good flag since Zarr's default fill values could differ from numpy or support dtypes not defined in np.ma.default_fill_value
@sharkinsspatial why did you chose to use numpy's default fill values here?
There was a problem hiding this comment.
@maxrjones @abarciauskas-bgse I'm just returning to this now (I mistakenly thought this had been altered and merged). I just did some investigation and found that
- Very old versions of h5py would throw on
fillvalueforvlenstrings and object types but this was fixed by Test default fill values for some common types h5py/h5py#2132. - I ran tests with Claude for a wide range of
dtypesand was unable to force an exception. - It is theoretically possible for a non-HDF5 library (Matlab, netcdf-4) to create a non-compliant HDF dataset that has no
fillvaluebut I think the likelihood of encountering this is very low. - As @maxrjones note it might be best for us to avoid injecting our own default
fillvaluehere in favor of just erroring on this actually exceptional case.
If you're ok I will revert the addition from your PR.
There was a problem hiding this comment.
FWIW I encountered this issue (where the fillvalue attribute was absent on an HDF5 dataset, causing the runtime error) while working on the virtual icechunk store for the GPM IMERG HH dataset (see https://github.com/virtual-zarr/gpmimerghh-virtualizarr-data-pipeline/blob/main/lambda/virtualizarr-processor/pyproject.toml#L10).
I don't know about the likelihood of encountering other HDF datasets with no fillvalue attribute. But just to be clear I wasn't trying to handle cases 1 and 2 from the bullets above.
It's fine with me if you need to revert the change, but I am curious what the path is for HDF datasets which have no fillvalue attribute.
There was a problem hiding this comment.
Only HDF datasets that are variable length arrays, object references or region references will return None for fillvalue. In h5py these are all represented as the numpy kind O with no way to distinguish between them. @maxrjones 's suggestion added a check first for string dtypes (because unfortunately vlen strings are also represented as O) and the throws NotImplementedError for any other type of dataset.
Theoretically we can expose vlen_dtype and ref_dtype through v3's VariableLengthBytes but this will likely be pretty complex and we can defer for a separate PR.
There was a problem hiding this comment.
I think what I encountered was different, where the fillvalue attribute was missing entirely, i.e.:
# when fillvalue is `None`
hasattr(dataset, 'fillvalue') => True
# when fillvalue is missing
hasattr(dataset, 'fillvalue') => FalseSo one workaround could be to collapse both scenarios to None:
raw = getattr(dataset, 'fillvalue', None)however I realize the missing attribute scenario may not be likely or the purview of this PR (and like you both mentioned we may not want to inject the numpy default in this case) so fine with me if you want to revert this change.
There was a problem hiding this comment.
@abarciauskas-bgse In this case we are unconcerned with the arbitrary attrs and only concerned with the HDF 5 dataset's fillvalue property. In the attrs case fillvalue is semantically meaningless as the correct CF convention attr for sentinel masking would be _FillValue https://virtualizarr.readthedocs.io/en/stable/explanation/custom_parsers.html#fill-values The parser has separate logic for handling and encoding the _FillValue attr so that it can be used by xarray and other downstream libs for sentinel masking https://github.com/zarr-developers/VirtualiZarr/blob/main/virtualizarr/parsers/hdf/hdf.py#L74-L76
Co-authored-by: Max Jones <14077947+maxrjones@users.noreply.github.com>
|
@TomNicholas This is ready for final review. |
What I did
Updated the HDF5 parser to support string dtype arrays with a
bytesorstrfillvalue.Acceptance criteria: