Skip to content

Commit a4ff4a9

Browse files
committed
Fix fill type issue for the Structured Types
1 parent 7b3ba70 commit a4ff4a9

3 files changed

Lines changed: 149 additions & 41 deletions

File tree

src/mdio/constants.py

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -45,14 +45,14 @@
4545
ScalarType.FLOAT16: np_nan,
4646
ScalarType.FLOAT32: np_nan,
4747
ScalarType.FLOAT64: np_nan,
48-
ScalarType.UINT8: 2**8 - 1, # Max value for uint8
49-
ScalarType.UINT16: 2**16 - 1, # Max value for uint16
50-
ScalarType.UINT32: 2**32 - 1, # Max value for uint32
51-
ScalarType.UINT64: 2**64 - 1, # Max value for uint64
52-
ScalarType.INT8: 2**7 - 1, # Max value for int8
53-
ScalarType.INT16: 2**15 - 1, # Max value for int16
54-
ScalarType.INT32: 2**31 - 1, # Max value for int32
55-
ScalarType.INT64: 2**63 - 1, # Max value for int64
48+
ScalarType.UINT8: UINT8_MAX,
49+
ScalarType.UINT16: UINT16_MAX,
50+
ScalarType.UINT32: UINT32_MAX,
51+
ScalarType.UINT64: UINT64_MAX,
52+
ScalarType.INT8: INT8_MAX,
53+
ScalarType.INT16: INT16_MAX,
54+
ScalarType.INT32: INT32_MAX,
55+
ScalarType.INT64: INT64_MAX,
5656
ScalarType.COMPLEX64: complex(np_nan, np_nan),
5757
ScalarType.COMPLEX128: complex(np_nan, np_nan),
5858
ScalarType.COMPLEX256: complex(np_nan, np_nan),

src/mdio/schemas/v1/dataset_serializer.py

Lines changed: 19 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
from collections.abc import Mapping
44

5-
from dask import array as dask_array
5+
import numpy as np
66
from numcodecs import Blosc as nc_Blosc
77
from numpy import dtype as np_dtype
88
from xarray import DataArray as xr_DataArray
99
from xarray import Dataset as xr_Dataset
10+
from zarr import zeros as zarr_zeros
1011
from zarr.core.chunk_key_encodings import V2ChunkKeyEncoding
1112

1213
try:
@@ -90,15 +91,14 @@ def _get_coord_names(var: Variable) -> list[str]:
9091
return coord_names
9192

9293

93-
def _get_np_datatype(var: Variable) -> np_dtype:
94+
def _get_np_datatype(data_type: ScalarType | StructuredType) -> np_dtype:
9495
"""Get the numpy dtype for a variable."""
95-
data_type = var.data_type
9696
if isinstance(data_type, ScalarType):
9797
return np_dtype(data_type.value)
9898
if isinstance(data_type, StructuredType):
9999
return np_dtype([(f.name, f.format.value) for f in data_type.fields])
100-
err = f"Unsupported data type: {type(data_type)} in variable {var.name}"
101-
raise TypeError(err)
100+
msg = f"Expected ScalarType or StructuredType, got {type(data_type).__name__}"
101+
raise ValueError(msg)
102102

103103

104104
def _get_zarr_shape(var: Variable, all_named_dims: dict[str, NamedDimension]) -> tuple[int, ...]:
@@ -170,8 +170,9 @@ def _get_fill_value(data_type: ScalarType | StructuredType | str) -> any:
170170
if isinstance(data_type, ScalarType):
171171
return fill_value_map.get(data_type)
172172
if isinstance(data_type, StructuredType):
173-
return tuple(fill_value_map.get(field.format) for field in data_type.fields)
174-
if isinstance(data_type, str):
173+
d_type = _get_np_datatype(data_type)
174+
return np.zeros((), dtype=d_type)
175+
if isinstance(data_type, str):
175176
return ""
176177
# If we do not have a fill value for this type, use None
177178
return None
@@ -186,6 +187,10 @@ def to_xarray_dataset(mdio_ds: Dataset) -> xr_DataArray: # noqa: PLR0912
186187
Args:
187188
mdio_ds: The source MDIO dataset to construct from.
188189
190+
Notes:
191+
- We can't use Dask (e.g., dask_array.zeros) because of the problems with
192+
structured type support. We will uze zarr.zeros instead
193+
189194
Returns:
190195
The constructed dataset with proper MDIO structure and metadata.
191196
"""
@@ -197,15 +202,16 @@ def to_xarray_dataset(mdio_ds: Dataset) -> xr_DataArray: # noqa: PLR0912
197202
# First pass: Build all variables
198203
data_arrays: dict[str, xr_DataArray] = {}
199204
for v in mdio_ds.variables:
200-
# Use dask array instead of numpy array for lazy evaluation
201205
shape = _get_zarr_shape(v, all_named_dims=all_named_dims)
202-
dtype = _get_np_datatype(v)
206+
dtype = _get_np_datatype(v.data_type)
203207
chunks = _get_zarr_chunks(v, all_named_dims=all_named_dims)
204-
arr = dask_array.zeros(shape, dtype=dtype, chunks=chunks)
205208

209+
# Use zarr.zeros to create an empty array with the specified shape and dtype
210+
# NOTE: zarr_format=2 is essential, to_zarr() will fail if zarr_format=2 is used
211+
data = zarr_zeros(shape=shape, dtype=dtype, zarr_format=2)
206212
# Create a DataArray for the variable. We will set coords in the second pass
207213
dim_names = _get_dimension_names(v)
208-
data_array = xr_DataArray(arr, dims=dim_names)
214+
data_array = xr_DataArray(data, dims=dim_names)
209215

210216
# Add array attributes
211217
if v.metadata is not None:
@@ -222,13 +228,8 @@ def to_xarray_dataset(mdio_ds: Dataset) -> xr_DataArray: # noqa: PLR0912
222228
# Create a custom chunk key encoding with "/" as separator
223229
chunk_key_encoding = V2ChunkKeyEncoding(separator="/").to_dict()
224230
encoding = {
225-
# Is this a bug in Zarr? For datatype:
226-
# dtype([('cdp-x', '<i4'), ('cdp-y', '<i4'), ('elevation', '<f2'), ('some_scalar', '<f2')])
227-
# I specify fill_value as
228-
# (2147483647, 2147483647, nan, nan)
229-
# But the fill_value stored in .zmetadata as
230-
# "fill_value": null
231-
"fill_value": _get_fill_value(v.data_type),
231+
# NOTE: See Zarr documentation on use of fill_value and _FillValue in Zarr v2 vs v3
232+
"_FillValue": _get_fill_value(v.data_type),
232233
"chunks": chunks,
233234
"chunk_key_encoding": chunk_key_encoding,
234235
"compressor": _convert_compressor(v.compressor),

tests/unit/v1/test_dataset_serializer.py

Lines changed: 122 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,13 @@
55
"""Tests the schema v1 dataset_serializer public API."""
66

77
import pytest
8+
from dask import array as dask_array
9+
from numpy import array as np_array
810
from numpy import dtype as np_dtype
9-
from numpy import nan as np_nan
1011
from numpy import isnan as np_isnan
12+
from numpy import zeros as np_zeros
13+
from xarray import DataArray as xr_DataArray
14+
from zarr import zeros as zarr_zeros
1115

1216
from mdio.constants import fill_value_map
1317
from mdio.schemas.chunk_grid import RegularChunkGrid
@@ -158,9 +162,7 @@ def test__get_np_datatype() -> None:
158162
]
159163

160164
for scalar_type, expected_numpy_type in scalar_type_tests:
161-
variable = Variable(name="test_var", dimensions=[], data_type=scalar_type)
162-
163-
result = _get_np_datatype(variable)
165+
result = _get_np_datatype(scalar_type)
164166
expected = np_dtype(expected_numpy_type)
165167

166168
assert result == expected
@@ -176,12 +178,7 @@ def test__get_np_datatype() -> None:
176178
StructuredField(name="valid", format=ScalarType.BOOL),
177179
]
178180
structured_multi = StructuredType(fields=multi_fields)
179-
180-
variable_multi_struct = Variable(
181-
name="multi_struct_var", dimensions=[], data_type=structured_multi
182-
)
183-
184-
result_multi = _get_np_datatype(variable_multi_struct)
181+
result_multi = _get_np_datatype(structured_multi)
185182
expected_multi = np_dtype(
186183
[("x", "float64"), ("y", "float64"), ("z", "float64"), ("id", "int32"), ("valid", "bool")]
187184
)
@@ -254,7 +251,8 @@ def test__get_fill_value() -> None:
254251
ScalarType.INT32,
255252
]
256253
for scalar_type in scalar_types:
257-
assert fill_value_map[scalar_type] == _get_fill_value(scalar_type)
254+
fill_value = _get_fill_value(scalar_type)
255+
assert fill_value_map[scalar_type] == fill_value
258256

259257
scalar_types = [
260258
ScalarType.COMPLEX64,
@@ -265,16 +263,23 @@ def test__get_fill_value() -> None:
265263
val = _get_fill_value(scalar_type)
266264
assert isinstance(val, complex)
267265
assert np_isnan(val.real)
268-
assert np_isnan(val.imag)
266+
assert np_isnan(val.imag)
269267

270268
# Test 2: StructuredType
271269
f1 = StructuredField(name="cdp-x", format=ScalarType.INT32)
272270
f2 = StructuredField(name="cdp-y", format=ScalarType.INT32)
273271
f3 = StructuredField(name="elevation", format=ScalarType.FLOAT16)
274272
f4 = StructuredField(name="some_scalar", format=ScalarType.FLOAT16)
275273
structured_type = StructuredType(fields=[f1, f2, f3, f4])
276-
result_structured = _get_fill_value(structured_type)
277-
assert result_structured == (2147483647, 2147483647, np_nan, np_nan)
274+
275+
expected = np_array(
276+
(0, 0, 0.0, 0.0),
277+
dtype=np_dtype(
278+
[("cdp-x", "<i4"), ("cdp-y", "<i4"), ("elevation", "<f2"), ("some_scalar", "<f2")]
279+
),
280+
)
281+
result = _get_fill_value(structured_type)
282+
assert expected == result
278283

279284
# Test 3: String type - should return empty string
280285
result_string = _get_fill_value("string_type")
@@ -382,5 +387,107 @@ def test_seismic_poststack_3d_acceptance_to_xarray_dataset(tmp_path) -> None: #
382387

383388
xr_ds = to_xarray_dataset(dataset)
384389

385-
file_path = output_path(tmp_path, f"{xr_ds.attrs['name']}", debugging=True)
390+
file_path = output_path(tmp_path, f"{xr_ds.attrs['name']}", debugging=False)
386391
to_zarr(xr_ds, file_path, mode="w")
392+
393+
394+
@pytest.mark.skip(reason="Issues serializing dask arrays of structured types to dask.")
395+
def test_to_zarr_dask(tmp_path) -> None: # noqa: ANN001
396+
"""Test writing XArray dataset with data as dask array to Zar."""
397+
# Create a data type and the fill value
398+
dtype = np_dtype([("inline", "int32"), ("cdp-x", "float64")])
399+
dtype_fill_value = np_zeros((), dtype=dtype)
400+
401+
# Use '_FillValue' instead of 'fill_value'
402+
# 'fill_value' is not a valid encoding key in Zarr v2
403+
my_attr_encoding = {
404+
"_FillValue": dtype_fill_value,
405+
"chunk_key_encoding": {"name": "v2", "separator": "/"},
406+
}
407+
408+
# Create a dask array using the data type
409+
# Do not specify encoding as the array attribute
410+
data = dask_array.zeros((36,), dtype=dtype, chunks=(36,))
411+
aa = xr_DataArray(name="myattr", data=data)
412+
413+
# Specify encoding per array
414+
encoding = {"myattr": my_attr_encoding}
415+
file_path = output_path(tmp_path, "to_zarr/zarr_dask", debugging=False)
416+
aa.to_zarr(file_path, mode="w", zarr_format=2, encoding=encoding, compute=False)
417+
418+
419+
def test_to_zarr_zarr_zerros_1(tmp_path) -> None: # noqa: ANN001
420+
"""Test writing XArray dataset with data as Zarr zero array to Zar.
421+
422+
Set encoding in as DataArray attributes
423+
"""
424+
# Create a data type and the fill value
425+
dtype = np_dtype([("inline", "int32"), ("cdp-x", "float64")])
426+
dtype_fill_value = np_zeros((), dtype=dtype)
427+
428+
# Use '_FillValue' instead of 'fill_value'
429+
# 'fill_value' is not a valid encoding key in Zarr v2
430+
my_attr_encoding = {
431+
"_FillValue": dtype_fill_value,
432+
"chunk_key_encoding": {"name": "v2", "separator": "/"},
433+
}
434+
435+
# Create a zarr array using the data type,
436+
# Specify encoding as the array attribute
437+
data = zarr_zeros((36, 36), dtype=dtype, zarr_format=2)
438+
aa = xr_DataArray(name="myattr", data=data)
439+
aa.encoding = my_attr_encoding
440+
441+
file_path = output_path(tmp_path, "to_zarr/zarr_zarr_zerros_1", debugging=False)
442+
aa.to_zarr(file_path, mode="w", zarr_format=2, compute=False)
443+
444+
445+
def test_to_zarr_zarr_zerros_2(tmp_path) -> None: # noqa: ANN001
446+
"""Test writing XArray dataset with data as Zarr zero array to Zar.
447+
448+
Set encoding in the to_zar method
449+
"""
450+
# Create a data type and the fill value
451+
dtype = np_dtype([("inline", "int32"), ("cdp-x", "float64")])
452+
dtype_fill_value = np_zeros((), dtype=dtype)
453+
454+
# Use '_FillValue' instead of 'fill_value'
455+
# 'fill_value' is not a valid encoding key in Zarr v2
456+
my_attr_encoding = {
457+
"_FillValue": dtype_fill_value,
458+
"chunk_key_encoding": {"name": "v2", "separator": "/"},
459+
}
460+
461+
# Create a zarr array using the data type,
462+
# Do not specify encoding as the array attribute
463+
data = zarr_zeros((36, 36), dtype=dtype, zarr_format=2)
464+
aa = xr_DataArray(name="myattr", data=data)
465+
466+
file_path = output_path(tmp_path, "to_zarr/zarr_zarr_zerros_2", debugging=False)
467+
# Specify encoding per array
468+
encoding = {"myattr": my_attr_encoding}
469+
aa.to_zarr(file_path, mode="w", zarr_format=2, encoding=encoding, compute=False)
470+
471+
472+
def test_to_zarr_np(tmp_path) -> None: # noqa: ANN001
473+
"""Test writing XArray dataset with data as NumPy array to Zar."""
474+
# Create a data type and the fill value
475+
dtype = np_dtype([("inline", "int32"), ("cdp-x", "float64")])
476+
dtype_fill_value = np_zeros((), dtype=dtype)
477+
478+
# Use '_FillValue' instead of 'fill_value'
479+
# 'fill_value' is not a valid encoding key in Zarr v2
480+
my_attr_encoding = {
481+
"_FillValue": dtype_fill_value,
482+
"chunk_key_encoding": {"name": "v2", "separator": "/"},
483+
}
484+
485+
# Create a zarr array using the data type
486+
# Do not specify encoding as the array attribute
487+
data = np_zeros((36, 36), dtype=dtype)
488+
aa = xr_DataArray(name="myattr", data=data)
489+
490+
file_path = output_path(tmp_path, "to_zarr/zarr_np", debugging=False)
491+
# Specify encoding per array
492+
encoding = {"myattr": my_attr_encoding}
493+
aa.to_zarr(file_path, mode="w", zarr_format=2, encoding=encoding, compute=False)

0 commit comments

Comments
 (0)