-
-
Notifications
You must be signed in to change notification settings - Fork 1.2k
Allow setting fill_value
on Zarr format 3 arrays
#10161
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
Changes from 4 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
@@ -13,6 +13,7 @@ | |||||||||||||
import tempfile | ||||||||||||||
import uuid | ||||||||||||||
import warnings | ||||||||||||||
from collections import ChainMap | ||||||||||||||
from collections.abc import Generator, Iterator, Mapping | ||||||||||||||
from contextlib import ExitStack | ||||||||||||||
from io import BytesIO | ||||||||||||||
|
@@ -3343,6 +3344,61 @@ def test_cache_members(self) -> None: | |||||||||||||
observed_keys_2 = sorted(zstore_mut.array_keys()) | ||||||||||||||
assert observed_keys_2 == sorted(array_keys + [new_key]) | ||||||||||||||
|
||||||||||||||
@requires_dask | ||||||||||||||
@pytest.mark.parametrize("dtype", [int, float]) | ||||||||||||||
def test_zarr_fill_value_setting(self, dtype): | ||||||||||||||
# When zarr_format=2, _FillValue sets fill_value | ||||||||||||||
# When zarr_format=3, fill_value is set independently | ||||||||||||||
# We test this by writing a dask array with compute=False, | ||||||||||||||
# on read we should receive chunks filled with `fill_value` | ||||||||||||||
dcherian marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
fv = -1 | ||||||||||||||
ds = xr.Dataset( | ||||||||||||||
{"foo": ("x", dask.array.from_array(np.array([0, 0, 0], dtype=dtype)))} | ||||||||||||||
) | ||||||||||||||
expected = xr.Dataset({"foo": ("x", [fv] * 3)}) | ||||||||||||||
|
||||||||||||||
zarr_format_2 = ( | ||||||||||||||
has_zarr_v3 and zarr.config.get("default_zarr_format") == 2 | ||||||||||||||
) or not has_zarr_v3 | ||||||||||||||
if zarr_format_2: | ||||||||||||||
attr = "_FillValue" | ||||||||||||||
expected.foo.attrs[attr] = fv | ||||||||||||||
else: | ||||||||||||||
attr = "fill_value" | ||||||||||||||
if dtype is float: | ||||||||||||||
# for floats, Xarray inserts a default `np.nan` | ||||||||||||||
expected.foo.attrs["_FillValue"] = np.nan | ||||||||||||||
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. does this assume that 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. Yes. I think so: xarray/xarray/backends/zarr.py Lines 1150 to 1155 in cdebbf5
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. since the zarr spec only defines a special JSON encoding for nan in the specific context of the Also, In zarr-developers/zarr-python#2874 I remove the behavior where all numpy scalars get special serialization to JSON (instead, the dtype object is responsible for that). But it seems like we will break xarray if we move forward with that change. 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 checked what zarr v3 does today -- although we apply special JSON encoding to any nan, +infinity, -inifinity when writing JSON (e.g., {"foo": np.nan} -> {"foo": "NaN"}), we only apply special decoding when reading the fill value. Special nan values in 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. Yes looks like Ryan added base64 encode/decode. 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. It would be great if we could take that custom attribute encoding decoding stuff out of Xarray and replace it with @d-v-b's new ZarrDType machinery. 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 have stand-alone functions that handle this, but they are currently not public API. Would be happy to make them public though! 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. ("this" meaning "convert special floats to / from JSON") |
||||||||||||||
|
||||||||||||||
open_kwargs = {"mask_and_scale": False, "consolidated": False} | ||||||||||||||
save_kwargs = dict(compute=False, consolidated=False) | ||||||||||||||
with self.roundtrip( | ||||||||||||||
ds, | ||||||||||||||
save_kwargs=ChainMap(save_kwargs, dict(encoding={"foo": {attr: fv}})), | ||||||||||||||
open_kwargs=open_kwargs, | ||||||||||||||
dcherian marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
) as actual: | ||||||||||||||
assert_identical(actual, expected) | ||||||||||||||
|
||||||||||||||
ds.foo.encoding[attr] = fv | ||||||||||||||
with self.roundtrip( | ||||||||||||||
ds, save_kwargs=save_kwargs, open_kwargs=open_kwargs | ||||||||||||||
) as actual: | ||||||||||||||
assert_identical(actual, expected) | ||||||||||||||
|
||||||||||||||
if zarr_format_2: | ||||||||||||||
ds = ds.drop_encoding() | ||||||||||||||
with pytest.raises(ValueError, match="_FillValue"): | ||||||||||||||
with self.roundtrip( | ||||||||||||||
ds, | ||||||||||||||
save_kwargs=ChainMap( | ||||||||||||||
save_kwargs, dict(encoding={"foo": {"fill_value": fv}}) | ||||||||||||||
), | ||||||||||||||
open_kwargs=open_kwargs, | ||||||||||||||
): | ||||||||||||||
pass | ||||||||||||||
# TODO: this doesn't fail because of the | ||||||||||||||
# ``raise_on_invalid=vn in check_encoding_set`` line in zarr.py | ||||||||||||||
# ds.foo.encoding["fill_value"] = fv | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
@requires_zarr | ||||||||||||||
@pytest.mark.skipif( | ||||||||||||||
|
@@ -5827,23 +5883,23 @@ def test_encode_zarr_attr_value() -> None: | |||||||||||||
@requires_zarr | ||||||||||||||
def test_extract_zarr_variable_encoding() -> None: | ||||||||||||||
var = xr.Variable("x", [1, 2]) | ||||||||||||||
actual = backends.zarr.extract_zarr_variable_encoding(var) | ||||||||||||||
actual = backends.zarr.extract_zarr_variable_encoding(var, zarr_format=3) | ||||||||||||||
assert "chunks" in actual | ||||||||||||||
assert actual["chunks"] == ("auto" if has_zarr_v3 else None) | ||||||||||||||
|
||||||||||||||
var = xr.Variable("x", [1, 2], encoding={"chunks": (1,)}) | ||||||||||||||
actual = backends.zarr.extract_zarr_variable_encoding(var) | ||||||||||||||
actual = backends.zarr.extract_zarr_variable_encoding(var, zarr_format=3) | ||||||||||||||
assert actual["chunks"] == (1,) | ||||||||||||||
|
||||||||||||||
# does not raise on invalid | ||||||||||||||
var = xr.Variable("x", [1, 2], encoding={"foo": (1,)}) | ||||||||||||||
actual = backends.zarr.extract_zarr_variable_encoding(var) | ||||||||||||||
actual = backends.zarr.extract_zarr_variable_encoding(var, zarr_format=3) | ||||||||||||||
|
||||||||||||||
# raises on invalid | ||||||||||||||
var = xr.Variable("x", [1, 2], encoding={"foo": (1,)}) | ||||||||||||||
with pytest.raises(ValueError, match=r"unexpected encoding parameters"): | ||||||||||||||
actual = backends.zarr.extract_zarr_variable_encoding( | ||||||||||||||
var, raise_on_invalid=True | ||||||||||||||
var, raise_on_invalid=True, zarr_format=3 | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.