diff --git a/dpnp/dpnp_iface_nanfunctions.py b/dpnp/dpnp_iface_nanfunctions.py index 4e0cde48e449..a808617469fc 100644 --- a/dpnp/dpnp_iface_nanfunctions.py +++ b/dpnp/dpnp_iface_nanfunctions.py @@ -40,6 +40,7 @@ import warnings import dpnp +from dpnp.dpnp_utils.dpnp_utils_statistics import dpnp_median __all__ = [ "nanargmax", @@ -48,6 +49,7 @@ "nancumsum", "nanmax", "nanmean", + "nanmedian", "nanmin", "nanprod", "nanstd", @@ -568,6 +570,107 @@ def nanmean(a, axis=None, dtype=None, out=None, keepdims=False, *, where=True): return avg +def nanmedian(a, axis=None, out=None, overwrite_input=False, keepdims=False): + """ + Compute the median along the specified axis, while ignoring NaNs. + + For full documentation refer to :obj:`numpy.nanmedian`. + + Parameters + ---------- + a : {dpnp.ndarray, usm_ndarray} + Input array. + axis : {None, int, tuple or list of ints}, optional + Axis or axes along which the medians are computed. The default, + ``axis=None``, will compute the median along a flattened version of + the array. If a sequence of axes, the array is first flattened along + the given axes, then the median is computed along the resulting + flattened axis. + Default: ``None``. + out : {None, dpnp.ndarray, usm_ndarray}, optional + Alternative output array in which to place the result. It must have + the same shape as the expected output but the type (of the calculated + values) will be cast if necessary. + Default: ``None``. + overwrite_input : bool, optional + If ``True``, then allow use of memory of input array `a` for + calculations. The input array will be modified by the call to + :obj:`dpnp.nanmedian`. This will save memory when you do not need to + preserve the contents of the input array. Treat the input as undefined, + but it will probably be fully or partially sorted. + Default: ``False``. + keepdims : bool, optional + If ``True``, the reduced axes (dimensions) are included in the result + as singleton dimensions, so that the returned array remains + compatible with the input array according to Array Broadcasting + rules. Otherwise, if ``False``, the reduced axes are not included in + the returned array. + Default: ``False``. + + Returns + ------- + out : dpnp.ndarray + A new array holding the result. If `a` has a floating-point data type, + the returned array will have the same data type as `a`. If `a` has a + boolean or integral data type, the returned array will have the + default floating point data type for the device where input array `a` + is allocated. + + See Also + -------- + :obj:`dpnp.mean` : Compute the arithmetic mean along the specified axis. + :obj:`dpnp.median` : Compute the median along the specified axis. + :obj:`dpnp.percentile` : Compute the q-th percentile of the data + along the specified axis. + + Notes + ----- + Given a vector ``V`` of length ``N``, the median of ``V`` is the + middle value of a sorted copy of ``V``, ``V_sorted`` - i.e., + ``V_sorted[(N-1)/2]``, when ``N`` is odd, and the average of the + two middle values of ``V_sorted`` when ``N`` is even. + + Examples + -------- + >>> import dpnp as np + >>> a = np.array([[10.0, 7, 4], [3, 2, 1]]) + >>> a[0, 1] = np.nan + >>> a + array([[10., nan, 4.], + [ 3., 2., 1.]]) + >>> np.median(a) + array(nan) + >>> np.nanmedian(a) + array(3.) + + >>> np.nanmedian(a, axis=0) + array([6.5, 2., 2.5]) + >>> np.nanmedian(a, axis=1) + array([7., 2.]) + + >>> b = a.copy() + >>> np.nanmedian(b, axis=1, overwrite_input=True) + array([7., 2.]) + >>> assert not np.all(a==b) + >>> b = a.copy() + >>> np.nanmedian(b, axis=None, overwrite_input=True) + array(3.) + >>> assert not np.all(a==b) + + """ + + dpnp.check_supported_arrays_type(a) + ignore_nan = False + if dpnp.issubdtype(a.dtype, dpnp.inexact): + mask = dpnp.isnan(a) + if dpnp.any(mask): + ignore_nan = True + + return dpnp_median( + a, axis, out, overwrite_input, keepdims, ignore_nan=ignore_nan + ) + + def nanmin(a, axis=None, out=None, keepdims=False, initial=None, where=True): """ Return the minimum of an array or minimum along an axis, ignoring any NaNs. diff --git a/dpnp/dpnp_iface_statistics.py b/dpnp/dpnp_iface_statistics.py index c266f7c397e6..68aea4eee8d7 100644 --- a/dpnp/dpnp_iface_statistics.py +++ b/dpnp/dpnp_iface_statistics.py @@ -39,19 +39,15 @@ import dpctl.tensor as dpt import numpy -from dpctl.tensor._numpy_helper import ( - normalize_axis_index, - normalize_axis_tuple, -) +from dpctl.tensor._numpy_helper import normalize_axis_index import dpnp # pylint: disable=no-name-in-module from .dpnp_algo import dpnp_correlate -from .dpnp_array import dpnp_array from .dpnp_utils import call_origin, get_usm_allocations from .dpnp_utils.dpnp_utils_reduction import dpnp_wrap_reduction_call -from .dpnp_utils.dpnp_utils_statistics import dpnp_cov +from .dpnp_utils.dpnp_utils_statistics import dpnp_cov, dpnp_median __all__ = [ "amax", @@ -113,22 +109,6 @@ def _count_reduce_items(arr, axis, where=True): return items -def _flatten_array_along_axes(arr, axes_to_flatten): - """Flatten an array along a specific set of axes.""" - - axes_to_keep = ( - axis for axis in range(arr.ndim) if axis not in axes_to_flatten - ) - - # Move the axes_to_flatten to the front - arr_moved = dpnp.moveaxis(arr, axes_to_flatten, range(len(axes_to_flatten))) - - new_shape = (-1,) + tuple(arr.shape[axis] for axis in axes_to_keep) - flattened_arr = arr_moved.reshape(new_shape) - - return flattened_arr - - def _get_comparison_res_dt(a, _dtype, _out): """Get a data type used by dpctl for result array in comparison function.""" @@ -765,7 +745,7 @@ def median(a, axis=None, out=None, overwrite_input=False, keepdims=False): preserve the contents of the input array. Treat the input as undefined, but it will probably be fully or partially sorted. Default: ``False``. - keepdims : {None, bool}, optional + keepdims : bool, optional If ``True``, the reduced axes (dimensions) are included in the result as singleton dimensions, so that the returned array remains compatible with the input array according to Array Broadcasting @@ -775,7 +755,7 @@ def median(a, axis=None, out=None, overwrite_input=False, keepdims=False): Returns ------- - dpnp.median : dpnp.ndarray + out : dpnp.ndarray A new array holding the result. If `a` has a floating-point data type, the returned array will have the same data type as `a`. If `a` has a boolean or integral data type, the returned array will have the @@ -808,20 +788,20 @@ def median(a, axis=None, out=None, overwrite_input=False, keepdims=False): >>> np.median(a, axis=0) array([6.5, 4.5, 2.5]) >>> np.median(a, axis=1) - array([7., 2.]) + array([7., 2.]) >>> np.median(a, axis=(0, 1)) array(3.5) >>> m = np.median(a, axis=0) >>> out = np.zeros_like(m) >>> np.median(a, axis=0, out=m) - array([6.5, 4.5, 2.5]) + array([6.5, 4.5, 2.5]) >>> m - array([6.5, 4.5, 2.5]) + array([6.5, 4.5, 2.5]) >>> b = a.copy() >>> np.median(b, axis=1, overwrite_input=True) - array([7., 2.]) + array([7., 2.]) >>> assert not np.all(a==b) >>> b = a.copy() >>> np.median(b, axis=None, overwrite_input=True) @@ -831,62 +811,9 @@ def median(a, axis=None, out=None, overwrite_input=False, keepdims=False): """ dpnp.check_supported_arrays_type(a) - a_ndim = a.ndim - a_shape = a.shape - _axis = range(a_ndim) if axis is None else axis - _axis = normalize_axis_tuple(_axis, a_ndim) - - if isinstance(axis, (tuple, list)): - if len(axis) == 1: - axis = axis[0] - else: - # Need to flatten if `axis` is a sequence of axes since `dpnp.sort` - # only accepts integer `axis` - # Note that the output of _flatten_array_along_axes is not - # necessarily a view of the input since `reshape` is used there. - # If this is the case, using overwrite_input is meaningless - a = _flatten_array_along_axes(a, _axis) - axis = 0 - - if overwrite_input: - if axis is None: - a_sorted = dpnp.ravel(a) - a_sorted.sort() - else: - if isinstance(a, dpt.usm_ndarray): - # dpnp.ndarray.sort only works with dpnp_array - a = dpnp_array._create_from_usm_ndarray(a) - a.sort(axis=axis) - a_sorted = a - else: - a_sorted = dpnp.sort(a, axis=axis) - - if axis is None: - axis = 0 - indexer = [slice(None)] * a_sorted.ndim - index, remainder = divmod(a_sorted.shape[axis], 2) - if remainder == 1: - # index with slice to allow mean (below) to work - indexer[axis] = slice(index, index + 1) - else: - indexer[axis] = slice(index - 1, index + 1) - - # Use `mean` in odd and even case to coerce data type and use `out` array - res = dpnp.mean(a_sorted[tuple(indexer)], axis=axis, out=out) - nan_mask = dpnp.isnan(a_sorted).any(axis=axis) - if nan_mask.any(): - res[nan_mask] = dpnp.nan - - if keepdims: - # We can't use dpnp.mean(..., keepdims) and dpnp.any(..., keepdims) - # above because of the reshape hack might have been used in - # `_flatten_array_along_axes` to handle cases when axis is a tuple. - res_shape = list(a_shape) - for i in _axis: - res_shape[i] = 1 - res = res.reshape(tuple(res_shape)) - - return res + return dpnp_median( + a, axis, out, overwrite_input, keepdims, ignore_nan=False + ) def min(a, axis=None, out=None, keepdims=False, initial=None, where=True): diff --git a/dpnp/dpnp_utils/dpnp_utils_statistics.py b/dpnp/dpnp_utils/dpnp_utils_statistics.py index 519f064615df..8f44f8d8b0b8 100644 --- a/dpnp/dpnp_utils/dpnp_utils_statistics.py +++ b/dpnp/dpnp_utils/dpnp_utils_statistics.py @@ -23,11 +23,108 @@ # THE POSSIBILITY OF SUCH DAMAGE. # ***************************************************************************** +import warnings + +import dpctl +import dpctl.tensor as dpt +from dpctl.tensor._numpy_helper import normalize_axis_tuple +from dpctl.utils import ExecutionPlacementError import dpnp +from dpnp.dpnp_array import dpnp_array from dpnp.dpnp_utils import get_usm_allocations, map_dtype_to_device -__all__ = ["dpnp_cov"] +__all__ = ["dpnp_cov", "dpnp_median"] + + +def _calc_median(a, axis, out=None): + """Compute the median of an array along a specified axis.""" + + indexer = [slice(None)] * a.ndim + index, remainder = divmod(a.shape[axis], 2) + if remainder == 1: + # index with slice to allow mean (below) to work + indexer[axis] = slice(index, index + 1) + else: + indexer[axis] = slice(index - 1, index + 1) + + # Use `mean` in odd and even case to coerce data type and use `out` array + res = dpnp.mean(a[tuple(indexer)], axis=axis, out=out) + nan_mask = dpnp.isnan(a).any(axis=axis) + if nan_mask.any(): + res[nan_mask] = dpnp.nan + + return res + + +def _calc_nanmedian(a, axis, out=None): + """Compute the median of an array along a specified axis, ignoring NaNs.""" + mask = dpnp.isnan(a) + valid_counts = dpnp.sum(~mask, axis=axis) + if out is None: + res = dpnp.empty_like(valid_counts, dtype=a.dtype) + else: + dpnp.check_supported_arrays_type(out) + exec_q = dpctl.utils.get_execution_queue((a.sycl_queue, out.sycl_queue)) + if exec_q is None: + raise ExecutionPlacementError( + "Input and output allocation queues are not compatible" + ) + if out.shape != valid_counts.shape: + raise ValueError( + f"Output array of shape {valid_counts.shape} is needed, got {out.shape}." + ) + res = out + + # Iterate over all indices of the output shape + for idx in dpnp.ndindex(res.shape): + current_valid_counts = valid_counts[idx] + + if current_valid_counts > 0: + # Extract the corresponding slice from the last axis of `a` + data = a[idx][:current_valid_counts] + left = (current_valid_counts - 1) // 2 + right = current_valid_counts // 2 + + if left == right: + res[idx] = data[left] + else: + res[idx] = (data[left] + data[right]) / 2.0 + else: + warnings.warn( + "All-NaN slice encountered", RuntimeWarning, stacklevel=6 + ) + res[idx] = dpnp.nan + + return res + + +def _flatten_array_along_axes(a, axes_to_flatten, overwrite_input): + """Flatten an array along a specific set of axes.""" + + a_ndim = a.ndim + axes_to_keep = tuple( + axis for axis in range(a_ndim) if axis not in axes_to_flatten + ) + + # 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,) + a_flatten = a_moved.reshape(new_shape) + + # Note that the output of a_flatten is not necessarily a view of the input + # since `reshape` is used here. If this is the case, we can safely use + # overwrite_input=True in calculating median + if ( + dpnp.get_usm_ndarray(a)._pointer + == dpnp.get_usm_ndarray(a_flatten)._pointer + ): + overwrite_input = overwrite_input + else: + overwrite_input = True + + return a_flatten, overwrite_input def dpnp_cov(m, y=None, rowvar=True, dtype=None): @@ -90,3 +187,62 @@ def _get_2dmin_array(x, dtype): c *= 1 / fact if fact != 0 else dpnp.nan return dpnp.squeeze(c) + + +def dpnp_median( + a, + axis=None, + out=None, + overwrite_input=False, + keepdims=False, + ignore_nan=False, +): + """Compute the median of an array along a specified axis.""" + + a_ndim = a.ndim + a_shape = a.shape + _axis = range(a_ndim) if axis is None else axis + _axis = normalize_axis_tuple(_axis, a_ndim) + + if len(_axis) == 1: + if ignore_nan: + a = dpnp.moveaxis(a, _axis[0], -1) + axis = -1 + else: + axis = _axis[0] + else: + # Need to flatten `a` if `_axis` is a sequence of axes + # since `dpnp.sort` only accepts integer for `axis` kwarg + if axis is None: + a = dpnp.ravel(a) + else: + a, overwrite_input = _flatten_array_along_axes( + a, _axis, overwrite_input + ) + axis = -1 + + if overwrite_input: + if isinstance(a, dpt.usm_ndarray): + # dpnp.ndarray.sort only works with dpnp_array + a = dpnp_array._create_from_usm_ndarray(a) + a.sort(axis=axis) + a_sorted = a + else: + a_sorted = dpnp.sort(a, axis=axis) + + if ignore_nan: + # sorting puts NaNs at the end + res = _calc_nanmedian(a_sorted, axis=axis, out=out) + else: + # We can't pass keepdims and use it in dpnp.mean and dpnp.any + # because of the reshape hack that might have been used in + # `_flatten_array_along_axes`. + res = _calc_median(a_sorted, axis=axis, out=out) + + if keepdims: + res_shape = list(a_shape) + for i in _axis: + res_shape[i] = 1 + res = res.reshape(tuple(res_shape)) + + return dpnp.get_result_array(res, out) diff --git a/dpnp/tests/test_nanfunctions.py b/dpnp/tests/test_nanfunctions.py index 80a169fee860..5645586219e4 100644 --- a/dpnp/tests/test_nanfunctions.py +++ b/dpnp/tests/test_nanfunctions.py @@ -1,6 +1,8 @@ +import dpctl import dpctl.tensor as dpt import numpy import pytest +from dpctl.utils import ExecutionPlacementError from numpy.testing import ( assert_allclose, assert_almost_equal, @@ -402,6 +404,121 @@ def test_nanmean_error(self): dpnp.nanmean(ia, out=res) +class TestNanMedian: + @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) + @pytest.mark.parametrize("axis", [None, 0, (-1,), [0, 1], (0, -2, -1)]) + @pytest.mark.parametrize("keepdims", [True, False]) + def test_basic(self, dtype, axis, keepdims): + x = numpy.random.uniform(-5, 5, 24) + a = numpy.array(x, dtype=dtype).reshape(2, 3, 4) + a[0, 0, 0] = a[-2, -2, -2] = numpy.nan + ia = dpnp.array(a) + + expected = numpy.nanmedian(a, axis=axis, keepdims=keepdims) + result = dpnp.nanmedian(ia, axis=axis, keepdims=keepdims) + + assert_dtype_allclose(result, expected) + + @pytest.mark.usefixtures( + "suppress_mean_empty_slice_numpy_warnings", + ) + @pytest.mark.parametrize("axis", [0, 1, (0, 1)]) + @pytest.mark.parametrize("shape", [(2, 0), (0, 3)]) + def test_empty(self, axis, shape): + a = numpy.empty(shape) + ia = dpnp.array(a) + + result = dpnp.nanmedian(ia, axis=axis) + expected = numpy.nanmedian(a, axis=axis) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize("dtype", get_all_dtypes()) + @pytest.mark.parametrize("axis", [None, 0, (-1,), [0, 1], (0, -2, -1)]) + def test_no_nan(self, dtype, axis): + x = numpy.random.uniform(-5, 5, 24) + a = numpy.array(x, dtype=dtype).reshape(2, 3, 4) + ia = dpnp.array(a) + + expected = numpy.nanmedian(a, axis=axis) + result = dpnp.nanmedian(ia, axis=axis) + + assert_dtype_allclose(result, expected) + + @pytest.mark.filterwarnings("ignore:All-NaN slice:RuntimeWarning") + def test_all_nan(self): + a = numpy.array(numpy.nan) + ia = dpnp.array(a) + + result = dpnp.nanmedian(ia) + expected = numpy.nanmedian(a) + assert_dtype_allclose(result, expected) + + a = numpy.random.uniform(-5, 5, 24).reshape(2, 3, 4) + a[:, :, 2] = numpy.nan + ia = dpnp.array(a) + + result = dpnp.nanmedian(ia, axis=1) + expected = numpy.nanmedian(a, axis=1) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize("axis", [None, 0, -1, (0, -2, -1)]) + def test_overwrite_input(self, axis): + a = numpy.random.uniform(-5, 5, 24).reshape(2, 3, 4) + a[0, 0, 0] = a[-2, -2, -2] = numpy.nan + ia = dpnp.array(a) + + b = a.copy() + ib = ia.copy() + expected = numpy.nanmedian(b, axis=axis, overwrite_input=True) + result = dpnp.nanmedian(ib, axis=axis, overwrite_input=True) + assert not numpy.all(a == b) + assert not dpnp.all(ia == ib) + + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize("axis", [None, 0, (-1,), [0, 1]]) + @pytest.mark.parametrize("overwrite_input", [True, False]) + def test_usm_ndarray(self, axis, overwrite_input): + a = numpy.random.uniform(-5, 5, 24).reshape(2, 3, 4) + a[0, 0, 0] = a[-2, -2, -2] = numpy.nan + ia = dpt.asarray(a) + + expected = numpy.nanmedian( + a, axis=axis, overwrite_input=overwrite_input + ) + result = dpnp.nanmedian(ia, axis=axis, overwrite_input=overwrite_input) + + @pytest.mark.parametrize("dtype", get_float_complex_dtypes()) + @pytest.mark.parametrize( + "axis, out_shape", [(0, (3,)), (1, (2,)), ((0, 1), ())] + ) + def test_out(self, dtype, axis, out_shape): + a = numpy.array([[5, numpy.nan, 2], [8, 4, numpy.nan]], dtype=dtype) + ia = dpnp.array(a) + + out_np = numpy.empty_like(a, shape=out_shape) + out_dp = dpnp.empty_like(ia, shape=out_shape) + expected = numpy.nanmedian(a, axis=axis, out=out_np) + result = dpnp.nanmedian(ia, axis=axis, out=out_dp) + assert result is out_dp + assert_dtype_allclose(result, expected) + + def test_error(self): + a = dpnp.arange(6.0).reshape(2, 3) + a[0, 0] = a[-1, -1] = numpy.nan + + # out shape is incorrect + res = dpnp.empty(3, dtype=a.dtype) + with pytest.raises(ValueError): + dpnp.nanmedian(a, axis=1, out=res) + + # out has a different queue + exec_q = dpctl.SyclQueue() + res = dpnp.empty(2, dtype=a.dtype, sycl_queue=exec_q) + with pytest.raises(ExecutionPlacementError): + dpnp.nanmedian(a, axis=1, out=res) + + class TestNanProd: @pytest.mark.parametrize("axis", [None, 0, 1, -1, 2, -2, (1, 2), (0, -2)]) @pytest.mark.parametrize("keepdims", [False, True]) diff --git a/dpnp/tests/test_statistics.py b/dpnp/tests/test_statistics.py index 4b67a8c84fa6..8dbccb379d9d 100644 --- a/dpnp/tests/test_statistics.py +++ b/dpnp/tests/test_statistics.py @@ -331,7 +331,7 @@ def test_axis(self, axis, keepdims): "suppress_mean_empty_slice_numpy_warnings", ) @pytest.mark.parametrize("axis", [0, 1, (0, 1)]) - @pytest.mark.parametrize("shape", [(2, 3), (2, 0), (0, 3)]) + @pytest.mark.parametrize("shape", [(2, 0), (0, 3)]) def test_empty(self, axis, shape): a = numpy.empty(shape) ia = dpnp.array(a) diff --git a/dpnp/tests/test_sycl_queue.py b/dpnp/tests/test_sycl_queue.py index 74ec14c1b820..9b917c29fd27 100644 --- a/dpnp/tests/test_sycl_queue.py +++ b/dpnp/tests/test_sycl_queue.py @@ -499,6 +499,7 @@ def test_meshgrid(device): pytest.param("nancumsum", [1.0, dpnp.nan]), pytest.param("nanmax", [1.0, 2.0, 4.0, dpnp.nan]), pytest.param("nanmean", [1.0, 2.0, 4.0, dpnp.nan]), + pytest.param("nanmedian", [1.0, 2.0, 4.0, dpnp.nan]), pytest.param("nanmin", [1.0, 2.0, 4.0, dpnp.nan]), pytest.param("nanprod", [1.0, dpnp.nan]), pytest.param("nanstd", [1.0, 2.0, 4.0, dpnp.nan]), diff --git a/dpnp/tests/test_usm_type.py b/dpnp/tests/test_usm_type.py index 8e06639a97c6..7fc2abc7a795 100644 --- a/dpnp/tests/test_usm_type.py +++ b/dpnp/tests/test_usm_type.py @@ -621,6 +621,7 @@ def test_norm(usm_type, ord, axis): pytest.param("nancumsum", [3.0, dp.nan]), pytest.param("nanmax", [1.0, 2.0, 4.0, dp.nan]), pytest.param("nanmean", [1.0, 2.0, 4.0, dp.nan]), + pytest.param("nanmedian", [1.0, 2.0, 4.0, dp.nan]), pytest.param("nanmin", [1.0, 2.0, 4.0, dp.nan]), pytest.param("nanprod", [1.0, 2.0, dp.nan]), pytest.param("nanstd", [1.0, 2.0, 4.0, dp.nan]), diff --git a/dpnp/tests/third_party/cupy/statistics_tests/test_meanvar.py b/dpnp/tests/third_party/cupy/statistics_tests/test_meanvar.py index ffed230eb822..e10a05a4763b 100644 --- a/dpnp/tests/third_party/cupy/statistics_tests/test_meanvar.py +++ b/dpnp/tests/third_party/cupy/statistics_tests/test_meanvar.py @@ -90,6 +90,55 @@ def test_median_axis_sequence(self, xp, dtype): return xp.median(a, self.axis, keepdims=self.keepdims) +@testing.parameterize( + *testing.product( + { + "shape": [(3, 4, 5)], + "axis": [None, 0, 1, -1, (0, 1), (0, 2), (-1, -2), [0, 1]], + "keepdims": [True, False], + "overwrite_input": [True, False], + } + ) +) +class TestNanMedian: + + zero_density = 0.25 + + def _make_array(self, dtype): + dtype = numpy.dtype(dtype) + if dtype.char in "efdFD": + r_dtype = dtype.char.lower() + a = testing.shaped_random(self.shape, numpy, dtype=r_dtype, scale=1) + if dtype.char in "FD": + ai = a + aj = testing.shaped_random( + self.shape, numpy, dtype=r_dtype, scale=1 + ) + ai[ai < math.sqrt(self.zero_density)] = 0 + aj[aj < math.sqrt(self.zero_density)] = 0 + a = ai + 1j * aj + else: + a[a < self.zero_density] = 0 + a = a / a + else: + a = testing.shaped_random(self.shape, numpy, dtype=dtype) + return a + + @pytest.mark.filterwarnings("ignore:All-NaN slice:RuntimeWarning") + @pytest.mark.filterwarnings("ignore:invalid value:RuntimeWarning") + @testing.for_all_dtypes() + @testing.numpy_cupy_allclose(type_check=has_support_aspect64()) + def test_nanmedian(self, xp, dtype): + a = xp.array(self._make_array(dtype)) + out = xp.nanmedian( + a, + self.axis, + keepdims=self.keepdims, + overwrite_input=self.overwrite_input, + ) + return xp.ascontiguousarray(out) + + class TestAverage: _multiprocess_can_split_ = True