diff --git a/docs/release.md b/docs/release.md index 78e8901c..b0e830ff 100644 --- a/docs/release.md +++ b/docs/release.md @@ -14,6 +14,12 @@ ## Unreleased +### Enhancements + +* Add a `keep_dimensions` option to `FixedScaleOffset` for preserving multidimensional + array shapes during encoding and decoding. + By {user}`shixi-li `, {issue}`852` + ### Maintenance * **Migrate build system from setuptools/setup.py to meson-python.** This replaces the diff --git a/src/numcodecs/fixedscaleoffset.py b/src/numcodecs/fixedscaleoffset.py index b1db5205..ef192f19 100644 --- a/src/numcodecs/fixedscaleoffset.py +++ b/src/numcodecs/fixedscaleoffset.py @@ -20,6 +20,9 @@ class FixedScaleOffset(Codec): Data type to use for decoded data. astype : dtype, optional Data type to use for encoded data. + keep_dimensions : bool, optional + Preserve the input array dimensions when encoding and decoding. + Defaults to False for backwards compatibility. Notes ----- @@ -69,10 +72,11 @@ class FixedScaleOffset(Codec): codec_id = 'fixedscaleoffset' - def __init__(self, offset, scale, dtype, astype=None): + def __init__(self, offset, scale, dtype, astype=None, keep_dimensions=False): self.offset = offset self.scale = scale self.dtype = np.dtype(dtype) + self.keep_dimensions = keep_dimensions if astype is None: self.astype = self.dtype else: @@ -84,8 +88,9 @@ def encode(self, buf): # normalise input arr = ensure_ndarray(buf).view(self.dtype) - # flatten to simplify implementation - arr = arr.reshape(-1, order='A') + if not self.keep_dimensions: + # flatten to simplify implementation + arr = arr.reshape(-1, order='A') # compute scale offset enc = (arr - self.offset) * self.scale @@ -100,8 +105,9 @@ def decode(self, buf, out=None): # interpret buffer as numpy array enc = ensure_ndarray(buf).view(self.astype) - # flatten to simplify implementation - enc = enc.reshape(-1, order='A') + if not self.keep_dimensions: + # flatten to simplify implementation + enc = enc.reshape(-1, order='A') # decode scale offset dec = (enc / self.scale) + self.offset @@ -109,6 +115,17 @@ def decode(self, buf, out=None): # convert dtype dec = dec.astype(self.dtype, copy=False) + if ( + self.keep_dimensions + and isinstance(out, np.ndarray) + and dec.ndim > 1 + and out.shape == dec.shape + and out.dtype == dec.dtype + ): + # Preserve logical coordinates when source and destination memory orders differ. + np.copyto(out, dec) + return out + # handle output return ndarray_copy(dec, out) @@ -120,11 +137,14 @@ def get_config(self): 'offset': self.offset, 'dtype': self.dtype.str, 'astype': self.astype.str, + 'keep_dimensions': self.keep_dimensions, } def __repr__(self): r = f'{type(self).__name__}(scale={self.scale}, offset={self.offset}, dtype={self.dtype.str!r}' if self.astype != self.dtype: r += f', astype={self.astype.str!r}' + if self.keep_dimensions: + r += ', keep_dimensions=True' r += ')' return r diff --git a/tests/test_fixedscaleoffset.py b/tests/test_fixedscaleoffset.py index 343d3026..17fda163 100644 --- a/tests/test_fixedscaleoffset.py +++ b/tests/test_fixedscaleoffset.py @@ -1,10 +1,12 @@ import itertools +from typing import Literal import numpy as np import pytest -from numpy.testing import assert_array_equal +from numpy.testing import assert_array_almost_equal, assert_array_equal from numcodecs.fixedscaleoffset import FixedScaleOffset +from numcodecs.registry import get_codec from tests.common import ( check_backwards_compatibility, check_config, @@ -55,15 +57,89 @@ def test_encode(offset: float, scale: float, expected: list[int]): assert np.dtype(astype) == actual.dtype +@pytest.mark.parametrize("order", ["C", "F"]) +def test_keep_dimensions(order: Literal["C", "F"]): + shape = (8, 4, 5) + arr = 200.0 + np.arange(np.prod(shape), dtype="