-
Notifications
You must be signed in to change notification settings - Fork 22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[Draft] Non-kerchunk backend for HDF5/netcdf4 files. #87
Open
sharkinsspatial
wants to merge
94
commits into
main
Choose a base branch
from
hdf5_reader
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from 4 commits
Commits
Show all changes
94 commits
Select commit
Hold shift + click to select a range
6b7abe2
Generate chunk manifest backed variable from HDF5 dataset.
sharkinsspatial bca0aab
Transfer dataset attrs to variable.
sharkinsspatial 384ff6b
Get virtual variables dict from HDF5 file.
sharkinsspatial 4c5f9bd
Update virtual_vars_from_hdf to use fsspec and drop_variables arg.
sharkinsspatial 1dd3370
mypy fix to use ChunkKey and empty dimensions list.
sharkinsspatial d92c75c
Extract attributes from hdf5 root group.
sharkinsspatial 0ed8362
Use hdf reader for netcdf4 files.
sharkinsspatial f4485fa
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] 3cc1254
Merge branch 'main' into hdf5_reader
sharkinsspatial 0123df7
Fix ruff complaints.
sharkinsspatial 332bcaa
First steps for handling HDF5 filters.
sharkinsspatial c51e615
Initial step for hdf5plugin supported codecs.
sharkinsspatial 0083f77
Small commit to check compression support in CI environment.
sharkinsspatial 3c00071
Merge branch 'main' into hdf5_reader
sharkinsspatial 207c4b5
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] c573800
Fix mypy complaints for hdf_filters.
sharkinsspatial ef0d7a8
Merge branch 'hdf5_reader' of https://github.com/TomNicholas/Virtuali…
sharkinsspatial 588e06b
Local pre-commit fix for hdf_filters.
sharkinsspatial 725333e
Use fsspec reader_options introduced in #37.
sharkinsspatial 72df108
Fix incorrect zarr_v3 if block position from merge commit ef0d7a8.
sharkinsspatial d1e85cb
Fix early return from hdf _extract_attrs.
sharkinsspatial 1e2b343
Test that _extract_attrs correctly handles multiple attributes.
sharkinsspatial 7f1c189
Initial attempt at scale and offset via numcodecs.
sharkinsspatial 908e332
Tests for cfcodec_from_dataset.
sharkinsspatial 0df332d
Temporarily relax integration tests to assert_allclose.
sharkinsspatial ca6b236
Add blosc_lz4 fixture parameterization to confirm libnetcdf environment.
sharkinsspatial b7426c5
Check for compatability with netcdf4 engine.
sharkinsspatial dac21dd
Use separate fixtures for h5netcdf and netcdf4 compression styles.
sharkinsspatial e968772
Print libhdf5 and libnetcdf4 versions to confirm compiled environment.
sharkinsspatial 9a98e57
Skip netcdf4 style compression tests when libhdf5 < 1.14.
sharkinsspatial 7590b87
Include imagecodecs.numcodecs to support HDF5 lzf filters.
sharkinsspatial e9fbc8a
Merge branch 'main' into hdf5_reader
sharkinsspatial 14bd709
Remove test that verifies call to read_kerchunk_references_from_file.
sharkinsspatial acdf0d7
Add additional codec support structures for imagecodecs and numcodecs.
sharkinsspatial 4ba323a
Add codec config test for Zstd.
sharkinsspatial e14e53b
Include initial cf decoding tests.
sharkinsspatial b808ded
Merge branch 'main' into hdf5_reader
sharkinsspatial b052f8c
Revert typo for scale_factor retrieval.
sharkinsspatial 01a3980
Update reader to use new numpy manifest representation.
sharkinsspatial c37d9e5
Temporarily skip test until blosc netcdf4 issue is solved.
sharkinsspatial 17b30d4
Fix Pydantic 2 migration warnings.
sharkinsspatial f6b596a
Include hdf5plugin and imagecodecs-numcodecs in mamba test environment.
sharkinsspatial eb6e24d
Mamba attempt with imagecodecs rather than imagecodecs-numcodecs.
sharkinsspatial c85bd16
Mamba attempt with latest imagecodecs release.
sharkinsspatial ca435da
Use correct iter_chunks callback function signtature.
sharkinsspatial 3017951
Include pip based imagecodecs-numcodecs until conda-forge availability.
sharkinsspatial ccf0b73
Merge branch 'main' into hdf5_reader
sharkinsspatial 32ba135
Handle non-coordinate dims which are serialized to hdf as empty dataset.
sharkinsspatial 64f446c
Use reader_options for filetype check and update failing kerchunk call.
sharkinsspatial 1c590bb
Merge branch 'main' into hdf5_reader
sharkinsspatial 9797346
Fix chunkmanifest shaping for chunked datasets.
sharkinsspatial c833e19
Handle scale_factor attribute serialization for compressed files.
sharkinsspatial 701bcfa
Include chunked roundtrip fixture.
sharkinsspatial 08c988e
Standardize xarray integration tests for hdf filters.
sharkinsspatial e6076bd
Merge branch 'hdf5_reader' of https://github.com/TomNicholas/Virtuali…
sharkinsspatial d684a84
Merge branch 'main' into hdf5_reader
sharkinsspatial 4cb4bac
Update reader selection logic for new filetype determination.
sharkinsspatial d352104
Use decode_times for integration test.
sharkinsspatial 3d89ea4
Standardize fixture names for hdf5 vs netcdf4 file types.
sharkinsspatial c9dd0d9
Handle array add_offset property for compressed data.
sharkinsspatial db5b421
Include h5py shuffle filter.
sharkinsspatial 9a1da32
Make ScaleAndOffset codec last in filters list.
sharkinsspatial 9b2b0f8
Apply ScaleAndOffset codec to _FillValue since it's value is now down…
sharkinsspatial 9ef1362
Coerce scale and add_offset values to native float for JSON serializa…
sharkinsspatial 30005bd
Merge branch 'main' into hdf5_reader
sharkinsspatial 14f7a99
Merge branch 'main' into hdf5_reader
sharkinsspatial f4f9c8f
Temporarily xfail integration tests for main
sharkinsspatial d257cb9
Merge branch 'main' into hdf5_reader
sharkinsspatial e795c2c
Merge branch 'main' into hdf5_reader
sharkinsspatial a9e59f2
Remove pydantic dependency as per pull/210.
sharkinsspatial 2b33bc2
Update test for new kerchunk reader module location.
sharkinsspatial a57ae9e
Fix branch typing errors.
sharkinsspatial e21fc69
Re-include automatic file type determination.
sharkinsspatial df69a12
Handle various hdf flavors of _FillValue storage.
sharkinsspatial 169337c
Include loadable variables in drop variables list.
sharkinsspatial bdcbfbf
Mock readers.hdf.virtual_vars_from_hdf to verify option passing.
sharkinsspatial 77f1689
Convert numpy _FillValue to native Python for serialization support.
sharkinsspatial 42c653a
Support groups with HDF5 reader.
sharkinsspatial 9c86e0d
Handle empty variables with a shape.
sharkinsspatial 001a4a7
Merge branch 'main' into hdf5_reader
sharkinsspatial 79f9921
Merge branch 'main' into hdf5_reader
sharkinsspatial 1589776
Import top-level version of xarray classes.
sharkinsspatial 772c580
Add option to explicitly specify use of an experimental hdf backend.
sharkinsspatial 3ab90c6
Include imagecodecs and hdf5plugin in all CI environments.
sharkinsspatial 150d06d
Add test_hdf_integration tests to be skipped for non-kerchunk env.
sharkinsspatial 8ccba34
Include imagecodecs in dependencies.
sharkinsspatial 81874e0
Diagnose imagecodecs-numcodecs installation failures in CI.
sharkinsspatial f87abe2
Ignore mypy complaints for VirtualBackend.
sharkinsspatial 70e7e29
Remove checksum assert which varies across different zstd versions.
sharkinsspatial 43bc0e4
Temporarily xfail integration tests with coordinate inconsistency.
sharkinsspatial 82a6321
Remove backend arg for non-hdf network file tests.
sharkinsspatial b34f260
Fix mypy comment moved by ruff formatting.
sharkinsspatial f9ead06
Make HDR reader dependencies optional.
sharkinsspatial 5608292
Handle optional imagecodecs and hdf5plugin dependency imports for tests.
sharkinsspatial File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -34,6 +34,7 @@ test = [ | |
"pytest", | ||
"scipy", | ||
"pooch", | ||
"h5netcdf", | ||
] | ||
|
||
|
||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,206 @@ | ||
from typing import List, Mapping, Optional | ||
|
||
import fsspec | ||
import h5py | ||
import numpy as np | ||
import xarray as xr | ||
|
||
from virtualizarr.manifests import ChunkEntry, ChunkManifest, ManifestArray | ||
from virtualizarr.zarr import ZArray | ||
|
||
|
||
def _dataset_chunk_manifest(path: str, dataset: h5py.Dataset) -> ChunkManifest: | ||
""" | ||
Generate ChunkManifest for HDF5 dataset. | ||
|
||
Parameters | ||
---------- | ||
path: str | ||
The path the HDF5 container file | ||
dset : h5py.Dataset | ||
HDF5 dataset for which to create a ChunkManifest | ||
|
||
Returns | ||
------- | ||
ChunkManifest | ||
A Virtualizarr ChunkManifest | ||
""" | ||
dsid = dataset.id | ||
|
||
if dataset.chunks is None: | ||
if dsid.get_offset() is None: | ||
raise ValueError("Dataset has no space allocated in the file") | ||
else: | ||
key_list = [0] * (len(dataset.shape) or 1) | ||
key = ".".join(map(str, key_list)) | ||
chunk_entry = ChunkEntry( | ||
path=path, | ||
offset=dsid.get_offset(), | ||
length=dsid.get_storage_size() | ||
) | ||
chunk_entries = {key: chunk_entry} | ||
chunk_manifest = ChunkManifest( | ||
entries=chunk_entries | ||
) | ||
return chunk_manifest | ||
else: | ||
num_chunks = dsid.get_num_chunks() | ||
if num_chunks == 0: | ||
raise ValueError("The dataset is chunked but contains no chunks") | ||
|
||
chunk_entries = dict() | ||
|
||
def get_key(blob): | ||
key_list = [a // b for a, b in zip(blob.chunk_offset, dataset.chunks)] | ||
key = ".".join(map(str, key_list)) | ||
return key | ||
|
||
def store_chunk_entry(blob): | ||
chunk_entries[get_key(blob)] = ChunkEntry( | ||
path=path, | ||
offset=blob.byte_offset, | ||
length=blob.size | ||
) | ||
|
||
has_chunk_iter = callable(getattr(dsid, "chunk_iter", None)) | ||
if has_chunk_iter: | ||
dsid.chunk_iter(store_chunk_entry) | ||
else: | ||
for index in range(num_chunks): | ||
store_chunk_entry(dsid.get_chunk_info(index)) | ||
|
||
chunk_manifest = ChunkManifest( | ||
entries=chunk_entries | ||
) | ||
return chunk_manifest | ||
|
||
|
||
def _dataset_dims(dataset: h5py.Dataset) -> List[str]: | ||
""" | ||
Get a list of dimension scale names attached to input HDF5 dataset. | ||
|
||
This is required by the xarray package to work with Zarr arrays. Only | ||
one dimension scale per dataset dimension is allowed. If dataset is | ||
dimension scale, it will be considered as the dimension to itself. | ||
|
||
Parameters | ||
---------- | ||
dataset : h5py.Dataset | ||
HDF5 dataset. | ||
|
||
Returns | ||
------- | ||
list | ||
List with HDF5 path names of dimension scales attached to input | ||
dataset. | ||
""" | ||
dims = list() | ||
rank = len(dataset.shape) | ||
if rank: | ||
for n in range(rank): | ||
num_scales = len(dataset.dims[n]) | ||
if num_scales == 1: | ||
dims.append(dataset.dims[n][0].name[1:]) | ||
elif h5py.h5ds.is_scale(dataset.id): | ||
dims.append(dataset.name[1:]) | ||
elif num_scales > 1: | ||
raise ValueError( | ||
f"{dataset.name}: {len(dataset.dims[n])} " | ||
f"dimension scales attached to dimension #{n}" | ||
) | ||
elif num_scales == 0: | ||
# Some HDF5 files do not have dimension scales. | ||
# If this is the case, `num_scales` will be 0. | ||
# In this case, we mimic netCDF4 and assign phony dimension names. | ||
# See https://github.com/fsspec/kerchunk/issues/41 | ||
dims.append(f"phony_dim_{n}") | ||
return dims | ||
|
||
|
||
def _extract_attrs(dataset: h5py.Dataset): | ||
""" | ||
Extract attributes from an HDF5 dataset. | ||
|
||
Parameters | ||
---------- | ||
dataset : h5py.Dataset | ||
An HDF5 dataset. | ||
""" | ||
_HIDDEN_ATTRS = { | ||
"REFERENCE_LIST", | ||
"CLASS", | ||
"DIMENSION_LIST", | ||
"NAME", | ||
"_Netcdf4Dimid", | ||
"_Netcdf4Coordinates", | ||
"_nc3_strict", | ||
"_NCProperties", | ||
} | ||
attrs = {} | ||
for n, v in dataset.attrs.items(): | ||
if n in _HIDDEN_ATTRS: | ||
continue | ||
# Fix some attribute values to avoid JSON encoding exceptions... | ||
if isinstance(v, bytes): | ||
v = v.decode("utf-8") or " " | ||
elif isinstance(v, (np.ndarray, np.number, np.bool_)): | ||
if v.dtype.kind == "S": | ||
v = v.astype(str) | ||
if n == "_FillValue": | ||
continue | ||
elif v.size == 1: | ||
v = v.flatten()[0] | ||
if isinstance(v, (np.ndarray, np.number, np.bool_)): | ||
v = v.tolist() | ||
else: | ||
v = v.tolist() | ||
elif isinstance(v, h5py._hl.base.Empty): | ||
v = "" | ||
if v == "DIMENSION_SCALE": | ||
continue | ||
|
||
attrs[n] = v | ||
return attrs | ||
|
||
|
||
def _dataset_to_variable(path: str, dataset: h5py.Dataset) -> xr.Variable: | ||
# This chunk determination logic mirrors zarr-python's create | ||
# https://github.com/zarr-developers/zarr-python/blob/main/zarr/creation.py#L62-L66 | ||
chunks = dataset.chunks if dataset.chunks else dataset.shape | ||
zarray = ZArray( | ||
chunks=chunks, | ||
compressor=dataset.compression, | ||
dtype=dataset.dtype, | ||
fill_value=dataset.fillvalue, | ||
filters=None, | ||
order="C", | ||
shape=dataset.shape, | ||
zarr_format=2, | ||
) | ||
manifest = _dataset_chunk_manifest(path, dataset) | ||
marray = ManifestArray(zarray=zarray, chunkmanifest=manifest) | ||
dims = _dataset_dims(dataset) | ||
attrs = _extract_attrs(dataset) | ||
variable = xr.Variable(data=marray, dims=dims, attrs=attrs) | ||
return variable | ||
|
||
|
||
def virtual_vars_from_hdf( | ||
path: str, | ||
drop_variables: Optional[List[str]] = None, | ||
) -> Mapping[str, xr.Variable]: | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I like this an a way to interface with the code in |
||
if drop_variables is None: | ||
drop_variables = [] | ||
fs, file_path = fsspec.core.url_to_fs(path) | ||
open_file = fs.open(path, "rb") | ||
f = h5py.File(open_file, mode="r") | ||
variables = {} | ||
for key in f.keys(): | ||
if key not in drop_variables: | ||
if isinstance(f[key], h5py.Dataset): | ||
variable = _dataset_to_variable(path, f[key]) | ||
variables[key] = variable | ||
else: | ||
raise NotImplementedError("Nested groups are not yet supported") | ||
|
||
return variables |
Empty file.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,119 @@ | ||
import h5py | ||
import numpy as np | ||
import pytest | ||
import xarray as xr | ||
|
||
|
||
@pytest.fixture | ||
def empty_chunks_netcdf4_file(tmpdir): | ||
ds = xr.Dataset({"data": []}) | ||
filepath = f"{tmpdir}/empty_chunks.nc" | ||
ds.to_netcdf(filepath, engine="h5netcdf") | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def empty_dataset_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/empty_dataset.nc" | ||
f = h5py.File(filepath, "w") | ||
f.create_dataset("data", shape=(0,), dtype="f") | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def no_chunks_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/no_chunks.nc" | ||
f = h5py.File(filepath, "w") | ||
data = np.random.random((10, 10)) | ||
f.create_dataset(name="data", data=data, chunks=None) | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def chunked_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/chunks.nc" | ||
f = h5py.File(filepath, "w") | ||
data = np.random.random((100, 100)) | ||
f.create_dataset(name="data", data=data, chunks=(50, 50)) | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def single_dimension_scale_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/single_dimension_scale.nc" | ||
f = h5py.File(filepath, "w") | ||
data = [1, 2] | ||
x = [0, 1] | ||
f.create_dataset(name="data", data=data) | ||
f.create_dataset(name="x", data=x) | ||
f["x"].make_scale() | ||
f["data"].dims[0].attach_scale(f["x"]) | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def is_scale_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/is_scale.nc" | ||
f = h5py.File(filepath, "w") | ||
data = [1, 2] | ||
f.create_dataset(name="data", data=data) | ||
f["data"].make_scale() | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def multiple_dimension_scales_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/multiple_dimension_scales.nc" | ||
f = h5py.File(filepath, "w") | ||
data = [1, 2] | ||
f.create_dataset(name="data", data=data) | ||
f.create_dataset(name="x", data=[0, 1]) | ||
f.create_dataset(name="y", data=[0, 1]) | ||
f["x"].make_scale() | ||
f["y"].make_scale() | ||
f["data"].dims[0].attach_scale(f["x"]) | ||
f["data"].dims[0].attach_scale(f["y"]) | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def chunked_dimensions_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/chunks_dimension.nc" | ||
f = h5py.File(filepath, "w") | ||
data = np.random.random((100, 100)) | ||
x = np.random.random((100)) | ||
y = np.random.random((100)) | ||
f.create_dataset(name="data", data=data, chunks=(50, 50)) | ||
f.create_dataset(name="x", data=x) | ||
f.create_dataset(name="y", data=y) | ||
f["data"].dims[0].attach_scale(f["x"]) | ||
f["data"].dims[1].attach_scale(f["y"]) | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def string_attribute_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/attributes.nc" | ||
f = h5py.File(filepath, "w") | ||
data = np.random.random((10, 10)) | ||
f.create_dataset(name="data", data=data, chunks=None) | ||
f["data"].attrs["attribute_name"] = "attribute_name" | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def group_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/group.nc" | ||
f = h5py.File(filepath, "w") | ||
f.create_group("group") | ||
return filepath | ||
|
||
|
||
@pytest.fixture | ||
def multiple_datasets_netcdf4_file(tmpdir): | ||
filepath = f"{tmpdir}/multiple_datasets.nc" | ||
f = h5py.File(filepath, "w") | ||
data = np.random.random((10, 10)) | ||
f.create_dataset(name="data", data=data, chunks=None) | ||
f.create_dataset(name="data2", data=data, chunks=None) | ||
return filepath |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does one need fsspec if reading a local file? Is there any other way to read from S3 without fsspec at all?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not with a filesystem-like API. You would have to use boto3 or aiobotocore directly.
This is one of the great virtues of fsspec and is not to be under-valued.