From f8564b110afa0e903bc12a8e1e3ed7a1e526a426 Mon Sep 17 00:00:00 2001 From: Raphael Jolivet Date: Wed, 5 Aug 2026 13:06:11 +0200 Subject: [PATCH 1/3] Fix cast_value contiguous input when paired with transpose codec Use np.ascontiguousarray before calling cast-value-rs, and add regression tests for cast_value + transpose filter orderings. Fixes #4237 --- src/zarr/codecs/cast_value.py | 8 +++-- tests/test_codecs/test_cast_value.py | 52 ++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/zarr/codecs/cast_value.py b/src/zarr/codecs/cast_value.py index eb8a4de248..4379dca61c 100644 --- a/src/zarr/codecs/cast_value.py +++ b/src/zarr/codecs/cast_value.py @@ -359,7 +359,9 @@ def _encode_sync( target_native = self.dtype.to_native_dtype() result = self._do_cast( - np.asarray(arr), target_dtype=target_native, scalar_map=self._get_scalar_map("encode") + np.ascontiguousarray(arr), + target_dtype=target_native, + scalar_map=self._get_scalar_map("encode"), ) return chunk_array.__class__.from_ndarray_like(result) @@ -379,7 +381,9 @@ def _decode_sync( target_native = chunk_spec.dtype.to_native_dtype() result = self._do_cast( - np.asarray(arr), target_dtype=target_native, scalar_map=self._get_scalar_map("decode") + np.ascontiguousarray(arr), + target_dtype=target_native, + scalar_map=self._get_scalar_map("decode"), ) return chunk_array.__class__.from_ndarray_like(result) diff --git a/tests/test_codecs/test_cast_value.py b/tests/test_codecs/test_cast_value.py index c43edb76e8..3ea08f0fc4 100644 --- a/tests/test_codecs/test_cast_value.py +++ b/tests/test_codecs/test_cast_value.py @@ -4,10 +4,13 @@ import numpy as np import pytest +from numpy.ma.testutils import assert_array_equal import zarr from tests.conftest import Expect, ExpectFail +from zarr.codecs import BytesCodec, TransposeCodec from zarr.codecs.cast_value import CastValue +from zarr.storage import MemoryStore try: import cast_value_rs # noqa: F401 @@ -477,3 +480,52 @@ def test_parse_scalar_map(case: Expect[Any, Any]) -> None: from zarr.codecs.cast_value import parse_scalar_map assert parse_scalar_map(case.input) == case.output + + +@requires_cast_value_rs +def test_enforce_contiguous_arrays() -> None: + """ + Transpose codec produces non-contiguous arrays. + Ensure cast_value makes them contiguous before processing. + """ + data = np.arange(20, dtype=np.float32).reshape(5, 2, 2) + + def make_array(filters: list[Any]) -> Any: + return zarr.create_array( + store=MemoryStore(), + shape=data.shape, + dtype=data.dtype, + chunks=data.shape, + filters=filters, + serializer=BytesCodec(endian="little"), + compressors=None, + zarr_format=3, + ) + + # Cast before transpose: may fail on read. + array = make_array( + [ + CastValue(data_type="uint16"), + TransposeCodec(order=(1, 2, 0)), + ] + ) + + # Write should be ok + array[:] = data + + # Read may fail + assert_array_equal(array[:], data) + + # Cast after transpose: may fail on write + array = make_array( + [ + TransposeCodec(order=(1, 2, 0)), + CastValue(data_type="uint16"), + ] + ) + + # Write may fail + array[:] = data + + # Read should be ok + assert_array_equal(array[:], data) From 9ac20c7673cbef17b2b0321e5cdda8b6c3ed0f89 Mon Sep 17 00:00:00 2001 From: "Raphael Jolivet [Mines Paris | PSL | OIE]" Date: Wed, 5 Aug 2026 17:30:15 +0200 Subject: [PATCH 2/3] Move fix to _do_cast. Add release note --- changes/4238.bugfix.md | 3 +++ src/zarr/codecs/cast_value.py | 10 +++------- tests/test_codecs/test_cast_value.py | 13 +++---------- 3 files changed, 9 insertions(+), 17 deletions(-) create mode 100644 changes/4238.bugfix.md diff --git a/changes/4238.bugfix.md b/changes/4238.bugfix.md new file mode 100644 index 0000000000..6c7b4260d6 --- /dev/null +++ b/changes/4238.bugfix.md @@ -0,0 +1,3 @@ +The codec *cast_value* expects a contiguous numpy array to work properly. +It used to break when place before or after a *transpose* codec, which produces non-contiguous arrays. +This fix resolves the issue. diff --git a/src/zarr/codecs/cast_value.py b/src/zarr/codecs/cast_value.py index 4379dca61c..4faec27fbc 100644 --- a/src/zarr/codecs/cast_value.py +++ b/src/zarr/codecs/cast_value.py @@ -317,7 +317,7 @@ def _do_cast( to_tgt = int if np.issubdtype(target_dtype, np.integer) else float scalar_map_entries = {to_src(k): to_tgt(v) for k, v in scalar_map.items()} return cast_array_rs( # type: ignore[no-any-return] - arr, + np.ascontiguousarray(arr), target_dtype=target_dtype, rounding_mode=self.rounding, out_of_range_mode=self.out_of_range, @@ -359,9 +359,7 @@ def _encode_sync( target_native = self.dtype.to_native_dtype() result = self._do_cast( - np.ascontiguousarray(arr), - target_dtype=target_native, - scalar_map=self._get_scalar_map("encode"), + np.asarray(arr), target_dtype=target_native, scalar_map=self._get_scalar_map("encode") ) return chunk_array.__class__.from_ndarray_like(result) @@ -381,9 +379,7 @@ def _decode_sync( target_native = chunk_spec.dtype.to_native_dtype() result = self._do_cast( - np.ascontiguousarray(arr), - target_dtype=target_native, - scalar_map=self._get_scalar_map("decode"), + np.asarray(arr), target_dtype=target_native, scalar_map=self._get_scalar_map("decode") ) return chunk_array.__class__.from_ndarray_like(result) diff --git a/tests/test_codecs/test_cast_value.py b/tests/test_codecs/test_cast_value.py index 3ea08f0fc4..07b10074d1 100644 --- a/tests/test_codecs/test_cast_value.py +++ b/tests/test_codecs/test_cast_value.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from numpy.ma.testutils import assert_array_equal +from numpy.testutils import assert_array_equal import zarr from tests.conftest import Expect, ExpectFail @@ -502,21 +502,17 @@ def make_array(filters: list[Any]) -> Any: zarr_format=3, ) - # Cast before transpose: may fail on read. + # Cast before transpose array = make_array( [ CastValue(data_type="uint16"), TransposeCodec(order=(1, 2, 0)), ] ) - - # Write should be ok array[:] = data - - # Read may fail assert_array_equal(array[:], data) - # Cast after transpose: may fail on write + # Cast after transpose array = make_array( [ TransposeCodec(order=(1, 2, 0)), @@ -524,8 +520,5 @@ def make_array(filters: list[Any]) -> Any: ] ) - # Write may fail array[:] = data - - # Read should be ok assert_array_equal(array[:], data) From d8e4b78328c9c9cbee37b9efbb92a69e3e2107cd Mon Sep 17 00:00:00 2001 From: "Raphael Jolivet [Mines Paris | PSL | OIE]" Date: Wed, 5 Aug 2026 17:38:51 +0200 Subject: [PATCH 3/3] Fixing unit test --- tests/test_codecs/test_cast_value.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_codecs/test_cast_value.py b/tests/test_codecs/test_cast_value.py index 07b10074d1..58a8d6733f 100644 --- a/tests/test_codecs/test_cast_value.py +++ b/tests/test_codecs/test_cast_value.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from numpy.testutils import assert_array_equal +from numpy.testing import assert_array_equal import zarr from tests.conftest import Expect, ExpectFail