From d7635b6a39ad10d6b9c72305fffd257a45ccc56c Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Thu, 1 Oct 2026 13:55:30 +0200 Subject: [PATCH 1/2] Fix median/nanmedian for a tuple axis with an empty kept dimension `_flatten_array_along_axes` merged the reduced axes with a trailing `-1`, which `reshape` cannot infer once the array has size 0 and a kept axis is also 0. Reducing e.g. an array of shape `(0, 3, 4)` over `axis=(1, 2)` raised `ValueError: cannot reshape array of size 0 into shape (0,newaxis)`. Compute the merged length explicitly so the empty case returns the expected empty result. --- CHANGELOG.md | 1 + dpnp/dpnp_utils/dpnp_utils_statistics.py | 6 +++++- dpnp/tests/test_nanfunctions.py | 14 ++++++++++++++ dpnp/tests/test_statistics.py | 14 ++++++++++++++ 4 files changed, 34 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c49bc020827c..fbece9fe0dcb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -108,6 +108,7 @@ This release is compatible with NumPy 2.5. * Fixed the list of events the copy kernels of `dpnp.reshape`, `dpnp.tensor.reshape`, `dpnp.roll` and `dpnp.tensor.roll` wait on being padded with default-constructed events [#3072](https://github.com/IntelPython/dpnp/pull/3072) * Fixed `simplify_iteration_three_strides` and `simplify_iteration_four_strides` accumulating into their third and fourth output displacements without zeroing them first, which required the caller to initialize them [#3072](https://github.com/IntelPython/dpnp/pull/3072) * Fixed `dpnp.ndarray.flat` indexing and assignment edge cases, adding support for slices, ellipsis, and integer/boolean array indices [#3045](https://github.com/IntelPython/dpnp/pull/3045) +* Fixed `dpnp.median` and `dpnp.nanmedian` raising a `ValueError` for a tuple `axis` when a kept dimension has size 0 [#3081](https://github.com/IntelPython/dpnp/pull/3081) ### Security diff --git a/dpnp/dpnp_utils/dpnp_utils_statistics.py b/dpnp/dpnp_utils/dpnp_utils_statistics.py index ac62ddcc2766..f838f00d98e5 100644 --- a/dpnp/dpnp_utils/dpnp_utils_statistics.py +++ b/dpnp/dpnp_utils/dpnp_utils_statistics.py @@ -26,6 +26,7 @@ # THE POSSIBILITY OF SUCH DAMAGE. # ***************************************************************************** +import math import warnings import dpnp @@ -102,7 +103,10 @@ def _flatten_array_along_axes(a, axes_to_flatten, overwrite_input): # Move the axes_to_flatten to the end destination = list(range(len(axes_to_keep), a_ndim)) a_moved = dpnp.moveaxis(a, axes_to_flatten, destination) - new_shape = tuple(a.shape[axis] for axis in axes_to_keep) + (-1,) + # Compute the merged length explicitly instead of letting `reshape` infer + # it with -1, since -1 is ambiguous when a kept axis has size 0 + merged = math.prod(a.shape[axis] for axis in axes_to_flatten) + new_shape = tuple(a.shape[axis] for axis in axes_to_keep) + (merged,) a_flatten = a_moved.reshape(new_shape) # Note that the output of a_flatten is not necessarily a view of the input diff --git a/dpnp/tests/test_nanfunctions.py b/dpnp/tests/test_nanfunctions.py index 598d1c2678ec..6a203426adb5 100644 --- a/dpnp/tests/test_nanfunctions.py +++ b/dpnp/tests/test_nanfunctions.py @@ -413,6 +413,20 @@ def test_empty(self, axis, shape): expected = numpy.nanmedian(a, axis=axis) assert_dtype_allclose(result, expected) + @pytest.mark.usefixtures("suppress_mean_empty_slice_numpy_warnings") + @pytest.mark.parametrize( + "keepdims, out_shape", [(False, (0,)), (True, (0, 1, 1))] + ) + def test_empty_kept_dim(self, keepdims, out_shape): + a = numpy.empty((0, 3, 4)) + ia = dpnp.array(a) + + result = dpnp.nanmedian(ia, axis=(1, 2), keepdims=keepdims) + assert result.shape == out_shape + if numpy_version() >= "2.5.4": + expected = numpy.nanmedian(a, axis=(1, 2), keepdims=keepdims) + assert_dtype_allclose(result, expected) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) @pytest.mark.parametrize("axis", [None, 0, (-1,), [0, 1], (0, -2, -1)]) def test_no_nan(self, dtype, axis): diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index a02adfac2ecb..0c475e6fbb31 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -944,6 +944,20 @@ def test_empty(self, axis, shape): expected = numpy.median(a, axis=axis) assert_dtype_allclose(result, expected) + @pytest.mark.usefixtures("suppress_mean_empty_slice_numpy_warnings") + @pytest.mark.parametrize( + "keepdims, out_shape", [(False, (0,)), (True, (0, 1, 1))] + ) + def test_empty_kept_dim(self, keepdims, out_shape): + a = numpy.empty((0, 3, 4)) + ia = dpnp.array(a) + + result = dpnp.median(ia, axis=(1, 2), keepdims=keepdims) + assert result.shape == out_shape + if numpy_version() >= "2.5.4": + expected = numpy.median(a, axis=(1, 2), keepdims=keepdims) + assert_dtype_allclose(result, expected) + @pytest.mark.parametrize("dtype", get_all_dtypes()) @pytest.mark.parametrize( "axis, out_shape", [(0, (3,)), (1, (2,)), ((0, 1), ())] From f8734462b1dc5a8a8837f19b6ad5634045f39a6e Mon Sep 17 00:00:00 2001 From: Anton Volkov Date: Fri, 2 Oct 2026 20:35:52 +0200 Subject: [PATCH 2/2] Keep size-1 kept dimensions in nanmedian `_calc_nanmedian` squeezed every size-1 axis, which also dropped kept (non-reduced) dimensions of size 1 and produced a wrong result shape (e.g. a (1, 5) array reduced over axis=1 returned shape () instead of (1,)). Squeeze only the reduced trailing axis. --- CHANGELOG.md | 1 + dpnp/dpnp_utils/dpnp_utils_statistics.py | 3 ++- dpnp/tests/test_nanfunctions.py | 14 ++++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71caf77a95a7..13153a8a87f5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -110,6 +110,7 @@ This release is compatible with NumPy 2.5. * Fixed `simplify_iteration_three_strides` and `simplify_iteration_four_strides` accumulating into their third and fourth output displacements without zeroing them first, which required the caller to initialize them [#3072](https://github.com/IntelPython/dpnp/pull/3072) * Fixed `dpnp.ndarray.flat` indexing and assignment edge cases, adding support for slices, ellipsis, and integer/boolean array indices [#3045](https://github.com/IntelPython/dpnp/pull/3045) * Fixed `dpnp.median` and `dpnp.nanmedian` raising a `ValueError` for a tuple `axis` when a kept dimension has size 0 [#3081](https://github.com/IntelPython/dpnp/pull/3081) +* Fixed `dpnp.nanmedian` dropping kept dimensions of size 1, which produced a wrong result shape [#3081](https://github.com/IntelPython/dpnp/pull/3081) ### Security diff --git a/dpnp/dpnp_utils/dpnp_utils_statistics.py b/dpnp/dpnp_utils/dpnp_utils_statistics.py index f838f00d98e5..98d10bdd4afd 100644 --- a/dpnp/dpnp_utils/dpnp_utils_statistics.py +++ b/dpnp/dpnp_utils/dpnp_utils_statistics.py @@ -89,7 +89,8 @@ def _calc_nanmedian(a, out=None): if mask.all(axis=-1).any(): warnings.warn("All-NaN slice encountered", RuntimeWarning, stacklevel=6) - return dpnp.squeeze(res) + # only drop the reduced axis, keep size-1 dimensions that are not reduced + return dpnp.squeeze(res, axis=-1) def _flatten_array_along_axes(a, axes_to_flatten, overwrite_input): diff --git a/dpnp/tests/test_nanfunctions.py b/dpnp/tests/test_nanfunctions.py index 6a203426adb5..354fecaeb951 100644 --- a/dpnp/tests/test_nanfunctions.py +++ b/dpnp/tests/test_nanfunctions.py @@ -427,6 +427,20 @@ def test_empty_kept_dim(self, keepdims, out_shape): expected = numpy.nanmedian(a, axis=(1, 2), keepdims=keepdims) assert_dtype_allclose(result, expected) + @pytest.mark.usefixtures("suppress_mean_empty_slice_numpy_warnings") + @pytest.mark.parametrize( + "shape, axis", [((1, 5), 1), ((3, 1, 4), 2), ((2, 1, 5), (0, 2))] + ) + @pytest.mark.parametrize("keepdims", [True, False]) + def test_size1_kept_dim(self, shape, axis, keepdims): + a = generate_random_numpy_array(shape) + a.flat[0] = numpy.nan + ia = dpnp.array(a) + + result = dpnp.nanmedian(ia, axis=axis, keepdims=keepdims) + expected = numpy.nanmedian(a, axis=axis, keepdims=keepdims) + assert_dtype_allclose(result, expected) + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) @pytest.mark.parametrize("axis", [None, 0, (-1,), [0, 1], (0, -2, -1)]) def test_no_nan(self, dtype, axis):