-
-
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 3 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 | ||||||||||||||
|
@@ -661,7 +662,7 @@ | |||||||||||||
{"foo": ("x", [0, 1])}, {"x": [2, 3], "y": ("a", [42]), "z": ("x", [4, 5])} | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
with self.roundtrip(original) as actual: | ||||||||||||||
Check failure on line 665 in xarray/tests/test_backends.py
|
||||||||||||||
assert_identical(original, actual) | ||||||||||||||
|
||||||||||||||
original["foo"].encoding["coordinates"] = "y" | ||||||||||||||
|
@@ -676,7 +677,7 @@ | |||||||||||||
original = Dataset( | ||||||||||||||
{"foo": ("x", [0, 1])}, {"x": [2, 3], "y": ("a", [42]), "z": ("x", [4, 5])} | ||||||||||||||
) | ||||||||||||||
with self.roundtrip(original) as actual: | ||||||||||||||
Check failure on line 680 in xarray/tests/test_backends.py
|
||||||||||||||
assert_identical(original, actual) | ||||||||||||||
|
||||||||||||||
# test that global "coordinates" is as expected | ||||||||||||||
|
@@ -693,13 +694,13 @@ | |||||||||||||
original = Dataset(coords={"x": 0, "y z": 1}) | ||||||||||||||
expected = Dataset({"y z": 1}, {"x": 0}) | ||||||||||||||
with pytest.warns(SerializationWarning): | ||||||||||||||
with self.roundtrip(original) as actual: | ||||||||||||||
Check failure on line 697 in xarray/tests/test_backends.py
|
||||||||||||||
assert_identical(expected, actual) | ||||||||||||||
|
||||||||||||||
def test_roundtrip_boolean_dtype(self) -> None: | ||||||||||||||
original = create_boolean_data() | ||||||||||||||
assert original["x"].dtype == "bool" | ||||||||||||||
with self.roundtrip(original) as actual: | ||||||||||||||
Check failure on line 703 in xarray/tests/test_backends.py
|
||||||||||||||
assert_identical(original, actual) | ||||||||||||||
assert actual["x"].dtype == "bool" | ||||||||||||||
# this checks for preserving dtype during second roundtrip | ||||||||||||||
|
@@ -3343,6 +3344,61 @@ | |||||||||||||
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( | ||||||||||||||
|
@@ -3720,7 +3776,7 @@ | |||||||||||||
) | ||||||||||||||
|
||||||||||||||
with self.create_zarr_target() as store, patched as mock: | ||||||||||||||
ds.to_zarr(store, mode="w") | ||||||||||||||
|
||||||||||||||
# We expect this to request array metadata information, so call_count should be == 1, | ||||||||||||||
xrds = xr.open_zarr(store) | ||||||||||||||
|
@@ -5827,23 +5883,23 @@ | |||||||||||||
@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 | ||||||||||||||
) | ||||||||||||||
|
||||||||||||||
|
||||||||||||||
|
@@ -5862,7 +5918,7 @@ | |||||||||||||
|
||||||||||||||
m = fsspec.filesystem("memory") | ||||||||||||||
mm = m.get_mapper("out1.zarr") | ||||||||||||||
ds.to_zarr(mm) # old interface | ||||||||||||||
Check failure on line 5921 in xarray/tests/test_backends.py
|
||||||||||||||
ds0 = ds.copy() | ||||||||||||||
# pd.to_timedelta returns ns-precision, but the example data is in second precision | ||||||||||||||
# so we need to fix this | ||||||||||||||
|
@@ -5934,7 +5990,7 @@ | |||||||||||||
} | ||||||||||||||
) | ||||||||||||||
ds["test"].encoding["chunks"] = encoded_chunks | ||||||||||||||
ds.to_zarr(tmp_path / "test.zarr") | ||||||||||||||
Check failure on line 5993 in xarray/tests/test_backends.py
|
||||||||||||||
|
||||||||||||||
with dask.config.set({"array.chunk-size": "1MiB"}): | ||||||||||||||
expected = ds.chunk(chunks) | ||||||||||||||
|
@@ -6148,7 +6204,7 @@ | |||||||||||||
def test_zarr_closing_internal_zip_store(): | ||||||||||||||
store_name = "tmp.zarr.zip" | ||||||||||||||
original_da = DataArray(np.arange(12).reshape((3, 4))) | ||||||||||||||
original_da.to_zarr(store_name, mode="w") | ||||||||||||||
|
||||||||||||||
with open_dataarray(store_name, engine="zarr") as loaded_da: | ||||||||||||||
assert_identical(original_da, loaded_da) | ||||||||||||||
|
@@ -6199,7 +6255,7 @@ | |||||||||||||
], | ||||||||||||||
) | ||||||||||||||
def test_zarr_region_auto(self, region): | ||||||||||||||
with self.create() as (target, ds): | ||||||||||||||
Check failure on line 6258 in xarray/tests/test_backends.py
|
||||||||||||||
ds_region = 1 + ds.isel(x=slice(2, 4), y=slice(6, 8)) | ||||||||||||||
self.save(target, ds_region, region=region) | ||||||||||||||
ds_updated = xr.open_zarr(target) | ||||||||||||||
|
@@ -6209,7 +6265,7 @@ | |||||||||||||
assert_identical(ds_updated, expected) | ||||||||||||||
|
||||||||||||||
def test_zarr_region_auto_noncontiguous(self): | ||||||||||||||
with self.create() as (target, ds): | ||||||||||||||
with pytest.raises(ValueError): | ||||||||||||||
self.save(target, ds.isel(x=[0, 2, 3], y=[5, 6]), region="auto") | ||||||||||||||
|
||||||||||||||
|
@@ -6222,7 +6278,7 @@ | |||||||||||||
region: Mapping[str, slice] | Literal["auto"] | ||||||||||||||
region_slice = dict(x=slice(2, 4), y=slice(6, 8)) | ||||||||||||||
|
||||||||||||||
with self.create() as (target, ds): | ||||||||||||||
ds_region = 1 + ds.isel(region_slice) | ||||||||||||||
for region in [region_slice, "auto"]: # type: ignore[assignment] | ||||||||||||||
with patch.object( | ||||||||||||||
|
@@ -6241,7 +6297,7 @@ | |||||||||||||
assert "y" not in written_variables | ||||||||||||||
|
||||||||||||||
def test_zarr_region_append(self): | ||||||||||||||
with self.create() as (target, ds): | ||||||||||||||
x_new = np.arange(40, 70, 10) | ||||||||||||||
data_new = np.ones((3, 10)) | ||||||||||||||
ds_new = xr.Dataset( | ||||||||||||||
|
@@ -6261,7 +6317,7 @@ | |||||||||||||
self.save(target, ds_new, mode="a", append_dim="x", region="auto") | ||||||||||||||
|
||||||||||||||
def test_zarr_region(self): | ||||||||||||||
with self.create() as (target, ds): | ||||||||||||||
ds_transposed = ds.transpose("y", "x") | ||||||||||||||
ds_region = 1 + ds_transposed.isel(x=[0], y=[0]) | ||||||||||||||
self.save(target, ds_region, region={"x": slice(0, 1), "y": slice(0, 1)}) | ||||||||||||||
|
@@ -6281,7 +6337,7 @@ | |||||||||||||
) | ||||||||||||||
|
||||||||||||||
with self.create_zarr_target() as target: | ||||||||||||||
self.save(target, ds.chunk(5), compute=False, mode="w") | ||||||||||||||
with pytest.raises(ValueError): | ||||||||||||||
for r in range(ds.sizes["a"]): | ||||||||||||||
self.save( | ||||||||||||||
|
@@ -6308,7 +6364,7 @@ | |||||||||||||
) | ||||||||||||||
|
||||||||||||||
with self.create_zarr_target() as target: | ||||||||||||||
self.save(target, da, mode="w", encoding={"foo": {"chunks": (5, 5, 1)}}) | ||||||||||||||
|
||||||||||||||
with pytest.raises(ValueError, match="encoding was provided"): | ||||||||||||||
self.save( | ||||||||||||||
|
Uh oh!
There was an error while loading. Please reload this page.