From 1442667126797dfa5dc03674703d29eff303b0e0 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 17 Sep 2024 11:52:21 -0500 Subject: [PATCH 01/12] implement dpnp.pad --- .github/workflows/conda-package.yml | 1 + dpnp/dpnp_iface_manipulation.py | 213 +++++ dpnp/dpnp_utils/dpnp_utils_pad.py | 784 ++++++++++++++++++ tests/test_manipulation.py | 509 +++++++++++- tests/test_sycl_queue.py | 26 + tests/test_usm_type.py | 21 + .../third_party/cupy/padding_test/__init__.py | 0 .../third_party/cupy/padding_test/test_pad.py | 457 ++++++++++ 8 files changed, 2009 insertions(+), 2 deletions(-) create mode 100644 dpnp/dpnp_utils/dpnp_utils_pad.py create mode 100644 tests/third_party/cupy/padding_test/__init__.py create mode 100644 tests/third_party/cupy/padding_test/test_pad.py diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index 66bae4c02f35..f05215e978b0 100644 --- a/.github/workflows/conda-package.yml +++ b/.github/workflows/conda-package.yml @@ -59,6 +59,7 @@ env: third_party/cupy/logic_tests third_party/cupy/manipulation_tests third_party/cupy/math_tests + third_party/cupy/padding_tests third_party/cupy/sorting_tests third_party/cupy/statistics_tests/test_histogram.py third_party/cupy/statistics_tests/test_meanvar.py diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 1e847366d14f..5ffd52d1a2d5 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -48,6 +48,7 @@ import dpnp from .dpnp_array import dpnp_array +from .dpnp_utils.dpnp_utils_pad import dpnp_pad __all__ = [ "append", @@ -74,6 +75,7 @@ "hstack", "moveaxis", "ndim", + "pad", "permute_dims", "ravel", "repeat", @@ -1837,6 +1839,217 @@ def ndim(a): return numpy.ndim(a) +def pad(array, pad_width, mode="constant", **kwargs): + """ + Pad an array. + + For full documentation refer to :obj:`numpy.pad`. + + Parameters + ---------- + array : {dpnp.ndarray, usm_ndarray} + The array to pad. + pad_width : {sequence, array_like, int} + Number of values padded to the edges of each axis. + ``((before_1, after_1), ... (before_N, after_N))`` unique pad widths + for each axis. + ``(before, after)`` or ``((before, after),)`` yields same before + and after pad for each axis. + ``(pad,)`` or ``int`` is a shortcut for ``before = after = pad`` width + for all axes. + mode : {str, function}, optional + One of the following string values or a user supplied function. + + "constant" + Pads with a constant value. + "edge" + Pads with the edge values of array. + "linear_ramp" + Pads with the linear ramp between `end_value` and the + array edge value. + "maximum" + Pads with the maximum value of all or part of the + vector along each axis. + "mean" + Pads with the mean value of all or part of the + vector along each axis. + "minimum" + Pads with the minimum value of all or part of the + vector along each axis. + "reflect" + Pads with the reflection of the vector mirrored on + the first and last values of the vector along each + axis. + "symmetric" + Pads with the reflection of the vector mirrored + along the edge of the array. + "wrap" + Pads with the wrap of the vector along the axis. + The first values are used to pad the end and the + end values are used to pad the beginning. + "empty" + Pads with undefined values. + + Padding function, see Notes. + Default: ``"constant"``. + stat_length : {None, int, sequence of ints}, optional + Used in "maximum", "mean", and "minimum". Number of + values at edge of each axis used to calculate the statistic value. + + ``((before_1, after_1), ... (before_N, after_N))`` unique statistic + lengths for each axis. + + ``(before, after)`` or ``((before, after),)`` yields same before + and after statistic lengths for each axis. + + ``(stat_length,)`` or ``int`` is a shortcut for + ``before = after = statistic`` length for all axes. + + Default: ``None``, to use the entire axis. + constant_values : {sequence, scalar}, optional + Used in "constant". The values to set the padded values for each + axis. + ``((before_1, after_1), ... (before_N, after_N))`` unique pad constants + for each axis. + ``(before, after)`` or ``((before, after),)`` yields same before + and after constants for each axis. + ``(constant,)`` or ``constant`` is a shortcut for + ``before = after = constant`` for all axes. + Default: ``0``. + end_values : {sequence, scalar}, optional + Used in "linear_ramp". The values used for the ending value of the + linear_ramp and that will form the edge of the padded array. + ``((before_1, after_1), ... (before_N, after_N))`` unique end values + for each axis. + ``(before, after)`` or ``((before, after),)`` yields same before + and after end values for each axis. + ``(constant,)`` or ``constant`` is a shortcut for + ``before = after = constant`` for all axes. + Default: ``0``. + reflect_type : {"even", "odd"}, optional + Used in "reflect", and "symmetric". The "even" style is the + default with an unaltered reflection around the edge value. For + the "odd" style, the extended part of the array is created by + subtracting the reflected values from two times the edge value. + Default: ``"even"``. + + Returns + ------- + padded array : dpnp.ndarray + Padded array of rank equal to `array` with shape increased + according to `pad_width`. + + Limitations + ----------- + Parameter `mode` as ``"median"`` is not currently supported and + ``NotImplementedError`` exception will be raised. + + Notes + ----- + For an array with rank greater than 1, some of the padding of later + axes is calculated from padding of previous axes. This is easiest to + think about with a rank 2 array where the corners of the padded array + are calculated by using padded values from the first axis. + + The padding function, if used, should modify a rank 1 array in-place. It + has the following signature:: + + padding_func(vector, iaxis_pad_width, iaxis, kwargs) + + where + + vector : dpnp.ndarray + A rank 1 array already padded with zeros. Padded values are + vector[:iaxis_pad_width[0]] and vector[-iaxis_pad_width[1]:]. + iaxis_pad_width : tuple + A 2-tuple of ints, iaxis_pad_width[0] represents the number of + values padded at the beginning of vector where + iaxis_pad_width[1] represents the number of values padded at + the end of vector. + iaxis : int + The axis currently being calculated. + kwargs : dict + Any keyword arguments the function requires. + + Examples + -------- + >>> import dpnp as np + >>> a = np.array([1, 2, 3, 4, 5]) + >>> np.pad(a, (2, 3), 'constant', constant_values=(4, 6)) + array([4, 4, 1, 2, 3, 4, 5, 6, 6, 6]) + + >>> np.pad(a, (2, 3), 'edge') + array([1, 1, 1, 2, 3, 4, 5, 5, 5, 5]) + + >>> np.pad(a, (2, 3), 'linear_ramp', end_values=(5, -4)) + array([ 5, 3, 1, 2, 3, 4, 5, 2, -1, -4]) + + >>> np.pad(a, (2,), 'maximum') + array([5, 5, 1, 2, 3, 4, 5, 5, 5]) + + >>> np.pad(a, (2,), 'mean') + array([3, 3, 1, 2, 3, 4, 5, 3, 3]) + + >>> np.pad(a, (2,), 'median') + NotImplementedError: Keyword argument `mode` does not support 'median' + + >>> a = np.array([[1, 2], [3, 4]]) + >>> np.pad(a, ((3, 2), (2, 3)), 'minimum') + array([[1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1], + [3, 3, 3, 4, 3, 3, 3], + [1, 1, 1, 2, 1, 1, 1], + [1, 1, 1, 2, 1, 1, 1]]) + + >>> a = np.array([1, 2, 3, 4, 5]) + >>> np.pad(a, (2, 3), 'reflect') + array([3, 2, 1, 2, 3, 4, 5, 4, 3, 2]) + + >>> np.pad(a, (2, 3), 'reflect', reflect_type='odd') + array([-1, 0, 1, 2, 3, 4, 5, 6, 7, 8]) + + >>> np.pad(a, (2, 3), 'symmetric') + array([2, 1, 1, 2, 3, 4, 5, 5, 4, 3]) + + >>> np.pad(a, (2, 3), 'symmetric', reflect_type='odd') + array([0, 1, 1, 2, 3, 4, 5, 5, 6, 7]) + + >>> np.pad(a, (2, 3), 'wrap') + array([4, 5, 1, 2, 3, 4, 5, 1, 2, 3]) + + >>> def pad_width(vector, pad_width, iaxis, kwargs): + ... pad_value = kwargs.get('padder', 10) + ... vector[:pad_width[0]] = pad_value + ... vector[-pad_width[1]:] = pad_value + >>> a = np.arange(6) + >>> a = a.reshape((2, 3)) + >>> np.pad(a, 2, pad_width) + array([[10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 0, 1, 2, 10, 10], + [10, 10, 3, 4, 5, 10, 10], + [10, 10, 10, 10, 10, 10, 10], + [10, 10, 10, 10, 10, 10, 10]]) + >>> np.pad(a, 2, pad_width, padder=100) + array([[100, 100, 100, 100, 100, 100, 100], + [100, 100, 100, 100, 100, 100, 100], + [100, 100, 0, 1, 2, 100, 100], + [100, 100, 3, 4, 5, 100, 100], + [100, 100, 100, 100, 100, 100, 100], + [100, 100, 100, 100, 100, 100, 100]]) + + """ + + dpnp.check_supported_arrays_type(array) + if mode == "median": + raise NotImplementedError( + "Keyword argument `mode` does not support 'median'" + ) + return dpnp_pad(array, pad_width, mode=mode, **kwargs) + + def ravel(a, order="C"): """ Return a contiguous flattened array. diff --git a/dpnp/dpnp_utils/dpnp_utils_pad.py b/dpnp/dpnp_utils/dpnp_utils_pad.py new file mode 100644 index 000000000000..1f6508777535 --- /dev/null +++ b/dpnp/dpnp_utils/dpnp_utils_pad.py @@ -0,0 +1,784 @@ +# ***************************************************************************** +# Copyright (c) 2024, Intel Corporation +# All rights reserved. +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# - Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF +# THE POSSIBILITY OF SUCH DAMAGE. +# ***************************************************************************** + +import numpy + +import dpnp + +__all__ = ["dpnp_pad"] + + +def _as_pairs(x, ndim, as_index=False): + """ + Copied from numpy/lib/_arraypad_impl.py + + Broadcasts `x` to an array with shape (`ndim`, 2). + + A helper function for `pad` that prepares and validates arguments like + `pad_width` for iteration in pairs. + + Parameters + ---------- + x : {None, scalar, array-like} + The object to broadcast to the shape (`ndim`, 2). + ndim : int + Number of pairs the broadcasted `x` will have. + as_index : bool, optional + If `x` is not ``None``, try to round each element of `x` to an integer + (dtype `dpnp.intp`) and ensure every element is positive. + Default: ``False``. + + Returns + ------- + pairs : nested iterables, shape (`ndim`, 2) + The broadcasted version of `x`. + + Raises + ------ + ValueError + If `as_index` is ``True`` and `x` contains negative elements. + Or if `x` is not broadcastable to the shape (`ndim`, 2). + + """ + + if x is None: + # Pass through None as a special case, otherwise numpy.round(x) fails + # with an TypeError + return ((None, None),) * ndim + elif dpnp.isscalar(x): + if as_index: + if x < 0: + raise ValueError("index can't contain negative values") + x = round(x) + return ((x, x),) * ndim + + x = numpy.array(x) + if as_index: + x = numpy.asarray(numpy.round(x), dtype=numpy.intp) + + if x.ndim < 3: + # Optimization: Possibly use faster paths for cases where `x` has + # only 1 or 2 elements. `numpy.broadcast_to` could handle these as well + # but is currently slower + + if x.size == 1: + # x was supplied as a single value + x = x.ravel() # Ensure x[0] works for x.ndim == 0, 1, 2 + if as_index and x < 0: + raise ValueError("index can't contain negative values") + return ((x[0], x[0]),) * ndim + + if x.size == 2 and x.shape != (2, 1): + # x was supplied with a single value for each side + # but except case when each dimension has a single value + # which should be broadcasted to a pair, + # e.g. [[1], [2]] -> [[1, 1], [2, 2]] not [[1, 2], [1, 2]] + x = x.ravel() # Ensure x[0], x[1] works + if as_index and (x[0] < 0 or x[1] < 0): + raise ValueError("index can't contain negative values") + return ((x[0], x[1]),) * ndim + + if as_index and x.min() < 0: + raise ValueError("index can't contain negative values") + + # Converting the array with `tolist` seems to improve performance + # when iterating and indexing the result (see usage in `pad`) + return numpy.broadcast_to(x, (ndim, 2)).tolist() + + +def _get_edges(padded, axis, width_pair): + """ + Copied from numpy/lib/_arraypad_impl.py + + Retrieves edge values from an empty-padded array along a given axis. + + Parameters + ---------- + padded : ndarray + Empty-padded array. + axis : int + Dimension in which the edges are considered. + width_pair : (int, int) + Pair of widths that mark the pad area on both sides in the given + dimension. + + Returns + ------- + left_edge, right_edge : ndarray + Edge values of the valid area in `padded` in the given dimension. Its + shape will always match `padded` except for the dimension given by + `axis` which will have a length of 1. + + """ + left_index = width_pair[0] + left_slice = _slice_at_axis(slice(left_index, left_index + 1), axis) + left_edge = padded[left_slice] + + right_index = padded.shape[axis] - width_pair[1] + right_slice = _slice_at_axis(slice(right_index - 1, right_index), axis) + right_edge = padded[right_slice] + + return left_edge, right_edge + + +def _get_linear_ramps(padded, axis, width_pair, end_value_pair): + """ + Copied from numpy/lib/_arraypad_impl.py + + Constructs linear ramps for an empty-padded array along a given axis. + + Parameters + ---------- + padded : ndarray + Empty-padded array. + axis : int + Dimension in which the ramps are constructed. + width_pair : (int, int) + Pair of widths that mark the pad area on both sides in the given + dimension. + end_value_pair : (scalar, scalar) + End values for the linear ramps which form the edge of the fully padded + array. These values are included in the linear ramps. + + Returns + ------- + left_ramp, right_ramp : ndarray + Linear ramps to set on both sides of `padded`. + + """ + edge_pair = _get_edges(padded, axis, width_pair) + + left_ramp = dpnp.linspace( + end_value_pair[0], + # squeeze axis replaced by linspace + edge_pair[0].squeeze(axis), + num=width_pair[0], + endpoint=False, + dtype=padded.dtype, + axis=axis, + ) + + right_ramp = dpnp.linspace( + end_value_pair[1], + # squeeze axis replaced by linspace + edge_pair[1].squeeze(axis), + num=width_pair[1], + endpoint=False, + dtype=padded.dtype, + axis=axis, + ) + # Reverse linear space in appropriate dimension + right_ramp = right_ramp[_slice_at_axis(slice(None, None, -1), axis)] + + return left_ramp, right_ramp + + +def _get_stats(padded, axis, width_pair, length_pair, stat_func): + """ + Copied from numpy/lib/_arraypad_impl.py + + Calculates a statistic for an empty-padded array along a given axis. + + Parameters + ---------- + padded : ndarray + Empty-padded array. + axis : int + Dimension in which the statistic is calculated. + width_pair : (int, int) + Pair of widths that mark the pad area on both sides in the given + dimension. + length_pair : 2-element sequence of None or int + Gives the number of values in valid area from each side that is + taken into account when calculating the statistic. If None the entire + valid area in `padded` is considered. + stat_func : function + Function to compute statistic. The expected signature is + ``stat_func(x: ndarray, axis: int, keepdims: bool) -> ndarray``. + + Returns + ------- + left_stat, right_stat : ndarray + Calculated statistic for both sides of `padded`. + + """ + + # Calculate indices of the edges of the area with original values + left_index = width_pair[0] + right_index = padded.shape[axis] - width_pair[1] + # as well as its length + max_length = right_index - left_index + + # Limit stat_lengths to max_length + left_length, right_length = length_pair + if left_length is None or max_length < left_length: + left_length = max_length + if right_length is None or max_length < right_length: + right_length = max_length + + if (left_length == 0 or right_length == 0) and stat_func in [ + dpnp.amax, + dpnp.amin, + ]: + # amax and amin can't operate on an empty array, + # raise a more descriptive warning here instead of the default one + raise ValueError("stat_length of 0 yields no value for padding") + + # Calculate statistic for the left side + left_slice = _slice_at_axis( + slice(left_index, left_index + left_length), axis + ) + left_chunk = padded[left_slice] + left_stat = stat_func(left_chunk, axis=axis, keepdims=True) + _round_if_needed(left_stat, padded.dtype) + + if left_length == right_length == max_length: + # return early as right_stat must be identical to left_stat + return left_stat, left_stat + + # Calculate statistic for the right side + right_slice = _slice_at_axis( + slice(right_index - right_length, right_index), axis + ) + right_chunk = padded[right_slice] + right_stat = stat_func(right_chunk, axis=axis, keepdims=True) + _round_if_needed(right_stat, padded.dtype) + return left_stat, right_stat + + +def _pad_simple(array, pad_width, fill_value=None): + """ + Copied from numpy/lib/_arraypad_impl.py + + Pads an array on all sides with either a constant or undefined values. + + Parameters + ---------- + array : ndarray + Array to grow. + pad_width : sequence of tuple[int, int] + Pad width on both sides for each dimension in `arr`. + fill_value : scalar, optional + If provided the padded area is filled with this value, otherwise + the pad area left undefined. + + Returns + ------- + padded : ndarray + The padded array with the same dtype as`array`. Its order will default + to C-style if `array` is not F-contiguous. + original_area_slice : tuple + A tuple of slices pointing to the area of the original array. + + """ + + # Allocate grown array + new_shape = tuple( + left + size + right + for size, (left, right) in zip(array.shape, pad_width) + ) + order = "F" if array.flags.fnc else "C" # Fortran and not also C-order + padded = dpnp.empty( + new_shape, + dtype=array.dtype, + order=order, + usm_type=array.usm_type, + sycl_queue=array.sycl_queue, + ) + + if fill_value is not None: + padded.fill(fill_value) + + # Copy old array into correct space + original_area_slice = tuple( + slice(left, left + size) + for size, (left, right) in zip(array.shape, pad_width) + ) + padded[original_area_slice] = array + + return padded, original_area_slice + + +def _round_if_needed(arr, dtype): + """ + Copied from numpy/lib/_arraypad_impl.py + + Rounds `arr` inplace if the destination dtype is an integer. + + """ + + if dpnp.issubdtype(dtype, dpnp.integer): + arr.round(out=arr) + + +def _set_pad_area(padded, axis, width_pair, value_pair): + """ + Copied from numpy/lib/_arraypad_impl.py + + Set an empty-padded area in given dimension. + + Parameters + ---------- + padded : dpnp.ndarray + Array with the pad area which is modified inplace. + axis : int + Dimension with the pad area to set. + width_pair : (int, int) + Pair of widths that mark the pad area on both sides in the given + dimension. + value_pair : tuple of scalars or ndarrays + Values inserted into the pad area on each side. It must match or be + broadcastable to the shape of `arr`. + + """ + left_slice = _slice_at_axis(slice(None, width_pair[0]), axis) + padded[left_slice] = value_pair[0] + + right_slice = _slice_at_axis( + slice(padded.shape[axis] - width_pair[1], None), axis + ) + padded[right_slice] = value_pair[1] + + +def _set_reflect_both( + padded, axis, width_pair, method, original_period, include_edge=False +): + """ + Copied from numpy/lib/_arraypad_impl.py + + Pads an `axis` of `arr` using reflection. + + Parameters + ---------- + padded : dpnp.ndarray + Input array of arbitrary shape. + axis : int + Axis along which to pad `arr`. + width_pair : (int, int) + Pair of widths that mark the pad area on both sides in the given + dimension. + method : str + Controls method of reflection; options are 'even' or 'odd'. + original_period : int + Original length of data on `axis` of `arr`. + include_edge : bool + If true, edge value is included in reflection, otherwise the edge + value forms the symmetric axis to the reflection. + + Returns + ------- + pad_amt : tuple of ints, length 2 + New index positions of padding to do along the `axis`. If these are + both 0, padding is done in this dimension. + + """ + + left_pad, right_pad = width_pair + old_length = padded.shape[axis] - right_pad - left_pad + + if include_edge: + # Avoid wrapping with only a subset of the original area + # by ensuring period can only be a multiple of the original + # area's length. + old_length = old_length // original_period * original_period + # Edge is included, we need to offset the pad amount by 1 + edge_offset = 1 + else: + # Avoid wrapping with only a subset of the original area + # by ensuring period can only be a multiple of the original + # area's length. + old_length = (old_length - 1) // (original_period - 1) * ( + original_period - 1 + ) + 1 + edge_offset = 0 # Edge is not included, no need to offset pad amount + old_length -= 1 # but must be omitted from the chunk + + if left_pad > 0: + # Pad with reflected values on left side: + # First limit chunk size which can't be larger than pad area + chunk_length = min(old_length, left_pad) + # Slice right to left, stop on or next to edge, start relative to stop + stop = left_pad - edge_offset + start = stop + chunk_length + left_slice = _slice_at_axis(slice(start, stop, -1), axis) + left_chunk = padded[left_slice] + + if method == "odd": + # Negate chunk and align with edge + edge_slice = _slice_at_axis(slice(left_pad, left_pad + 1), axis) + left_chunk = 2 * padded[edge_slice] - left_chunk + + # Insert chunk into padded area + start = left_pad - chunk_length + stop = left_pad + pad_area = _slice_at_axis(slice(start, stop), axis) + padded[pad_area] = left_chunk + # Adjust pointer to left edge for next iteration + left_pad -= chunk_length + + if right_pad > 0: + # Pad with reflected values on right side: + # First limit chunk size which can't be larger than pad area + chunk_length = min(old_length, right_pad) + # Slice right to left, start on or next to edge, stop relative to start + start = -right_pad + edge_offset - 2 + stop = start - chunk_length + right_slice = _slice_at_axis(slice(start, stop, -1), axis) + right_chunk = padded[right_slice] + + if method == "odd": + # Negate chunk and align with edge + edge_slice = _slice_at_axis(slice(-right_pad - 1, -right_pad), axis) + right_chunk = 2 * padded[edge_slice] - right_chunk + + # Insert chunk into padded area + start = padded.shape[axis] - right_pad + stop = start + chunk_length + pad_area = _slice_at_axis(slice(start, stop), axis) + padded[pad_area] = right_chunk + # Adjust pointer to right edge for next iteration + right_pad -= chunk_length + + return left_pad, right_pad + + +def _set_wrap_both(padded, axis, width_pair, original_period): + """ + Copied from numpy/lib/_arraypad_impl.py + + Pad `axis` of `arr` with wrapped values. + + Parameters + ---------- + padded : dpnp.ndarray + Input array of arbitrary shape. + axis : int + Axis along which to pad `arr`. + width_pair : (int, int) + Pair of widths that mark the pad area on both sides in the given + dimension. + original_period : int + Original length of data on `axis` of `arr`. + + Returns + ------- + pad_amt : tuple of ints, length 2 + New index positions of padding to do along the `axis`. If these are + both 0, padding is done in this dimension. + + """ + + left_pad, right_pad = width_pair + period = padded.shape[axis] - right_pad - left_pad + # Avoid wrapping with only a subset of the original area by ensuring period + # can only be a multiple of the original area's length. + period = period // original_period * original_period + + # If the current dimension of `arr` doesn't contain enough valid values + # (not part of the undefined pad area) we need to pad multiple times. + # Each time the pad area shrinks on both sides which is communicated with + # these variables. + new_left_pad = 0 + new_right_pad = 0 + + if left_pad > 0: + # Pad with wrapped values on left side + # First slice chunk from right side of the non-pad area. + # Use min(period, left_pad) to ensure that chunk is not larger than + # pad area + slice_end = left_pad + period + slice_start = slice_end - min(period, left_pad) + right_slice = _slice_at_axis(slice(slice_start, slice_end), axis) + right_chunk = padded[right_slice] + + if left_pad > period: + # Chunk is smaller than pad area + pad_area = _slice_at_axis(slice(left_pad - period, left_pad), axis) + new_left_pad = left_pad - period + else: + # Chunk matches pad area + pad_area = _slice_at_axis(slice(None, left_pad), axis) + padded[pad_area] = right_chunk + + if right_pad > 0: + # Pad with wrapped values on right side + # First slice chunk from left side of the non-pad area. + # Use min(period, right_pad) to ensure that chunk is not larger than + # pad area + slice_start = -right_pad - period + slice_end = slice_start + min(period, right_pad) + left_slice = _slice_at_axis(slice(slice_start, slice_end), axis) + left_chunk = padded[left_slice] + + if right_pad > period: + # Chunk is smaller than pad area + pad_area = _slice_at_axis( + slice(-right_pad, -right_pad + period), axis + ) + new_right_pad = right_pad - period + else: + # Chunk matches pad area + pad_area = _slice_at_axis(slice(-right_pad, None), axis) + padded[pad_area] = left_chunk + + return new_left_pad, new_right_pad + + +def _slice_at_axis(sl, axis): + """ + Copied from numpy/lib/_arraypad_impl.py + + Constructs a tuple of slices to slice an array in the given dimension. + + Parameters + ---------- + sl : slice + The slice for the given dimension. + axis : int + The axis to which `sl` is applied. All other dimensions are left + "unsliced". + + Returns + ------- + sl : tuple of slices + A tuple with slices matching `shape` in length. + + Examples + -------- + >>> import dpnp as np + >>> import dpnp.dpnp_utils.dpnp_utils_pad as np_pad + >>> np_pad._slice_at_axis(slice(None, 3, -1), 1) + (slice(None, None, None), slice(None, 3, -1), Ellipsis) + + """ + return (slice(None),) * axis + (sl,) + (Ellipsis,) + + +def _view_roi(array, original_area_slice, axis): + """ + Copied from numpy/lib/_arraypad_impl.py + + Gets a view of the current region of interest during iterative padding. + + When padding multiple dimensions iteratively corner values are + unnecessarily overwritten multiple times. This function reduces the + working area for the first dimensions so that corners are excluded. + + Parameters + ---------- + array : dpnp.ndarray + The array with the region of interest. + original_area_slice : tuple of slices + Denotes the area with original values of the unpadded array. + axis : int + The currently padded dimension assuming that `axis` is padded before + `axis` + 1. + + Returns + ------- + roi : ndarray + The region of interest of the original `array`. + + """ + + axis += 1 + sl = (slice(None),) * axis + original_area_slice[axis:] + return array[sl] + + +def dpnp_pad(array, pad_width, mode="constant", **kwargs): + """Pad an array.""" + + if isinstance(pad_width, int): + if pad_width < 0: + raise ValueError("index can't contain negative values") + pad_width = ((pad_width, pad_width),) * array.ndim + else: + if dpnp.is_supported_array_type(pad_width): + pad_width = dpnp.asnumpy(pad_width) + else: + pad_width = numpy.asarray(pad_width) + + if not pad_width.dtype.kind == "i": + raise TypeError("`pad_width` must be of integral type.") + + # Broadcast to shape (array.ndim, 2) + pad_width = _as_pairs(pad_width, array.ndim, as_index=True) + + if callable(mode): + function = mode + # Create a new zero padded array + padded, _ = _pad_simple(array, pad_width, fill_value=0) + # And apply along each axis + + for axis in range(padded.ndim): + # Iterate using ndindex as in apply_along_axis, but assuming that + # function operates inplace on the padded array. + + # view with the iteration axis at the end + view = dpnp.moveaxis(padded, axis, -1) + + # compute indices for the iteration axes, and append a trailing + # ellipsis to prevent 0d arrays decaying to scalars + inds = numpy.ndindex(view.shape[:-1]) + inds = (ind + (Ellipsis,) for ind in inds) + for ind in inds: + function(view[ind], pad_width[axis], axis, kwargs) + + return padded + + # Make sure that no unsupported keywords were passed for the current mode + allowed_kwargs = { + "empty": [], + "edge": [], + "wrap": [], + "constant": ["constant_values"], + "linear_ramp": ["end_values"], + "maximum": ["stat_length"], + "mean": ["stat_length"], + # "median": ["stat_length"], # TODO: dpnp.median is not implemented + "minimum": ["stat_length"], + "reflect": ["reflect_type"], + "symmetric": ["reflect_type"], + } + + try: + unsupported_kwargs = set(kwargs) - set(allowed_kwargs[mode]) + except KeyError: + raise ValueError(f"mode '{mode}' is not supported") from None + if unsupported_kwargs: + raise ValueError( + f"unsupported keyword arguments for mode '{mode}': {unsupported_kwargs}" + ) + + if mode == "constant": + values = kwargs.get("constant_values", 0) + if ( + dpnp.isscalar(values) + and values == 0 + and (array.ndim == 1 or array.size < 4e6) + ): + # faster path for 1d arrays or small n-dimensional arrays + return _pad_simple(array, pad_width, 0)[0] + + # TODO: add "median": dpnp.median when dpnp.median is implemented + stat_functions = { + "maximum": dpnp.amax, + "minimum": dpnp.amin, + "mean": dpnp.mean, + } + + # Create array with final shape and original values + # (padded area is undefined) + padded, original_area_slice = _pad_simple(array, pad_width) + # And prepare iteration over all dimensions + # (zipping may be more readable than using enumerate) + axes = range(padded.ndim) + + if mode == "constant": + values = _as_pairs(values, padded.ndim) + for axis, width_pair, value_pair in zip(axes, pad_width, values): + roi = _view_roi(padded, original_area_slice, axis) + _set_pad_area(roi, axis, width_pair, value_pair) + + elif mode == "empty": + pass # Do nothing as _pad_simple already returned the correct result + + elif array.size == 0: + # Only modes 'constant' and 'empty' can extend empty axes, all other + # modes depend on `array` not being empty + # -> ensure every empty axis is only 'padded with 0' + for axis, width_pair in zip(axes, pad_width): + if array.shape[axis] == 0 and any(width_pair): + raise ValueError( + f"can't extend empty axis {axis} using modes other than " + "'constant' or 'empty'" + ) + # passed, don't need to do anything more as _pad_simple already + # returned the correct result + + elif mode == "edge": + for axis, width_pair in zip(axes, pad_width): + roi = _view_roi(padded, original_area_slice, axis) + edge_pair = _get_edges(roi, axis, width_pair) + _set_pad_area(roi, axis, width_pair, edge_pair) + + elif mode == "linear_ramp": + end_values = kwargs.get("end_values", 0) + end_values = _as_pairs(end_values, padded.ndim) + for axis, width_pair, value_pair in zip(axes, pad_width, end_values): + roi = _view_roi(padded, original_area_slice, axis) + ramp_pair = _get_linear_ramps(roi, axis, width_pair, value_pair) + _set_pad_area(roi, axis, width_pair, ramp_pair) + + elif mode in stat_functions: + func = stat_functions[mode] + length = kwargs.get("stat_length", None) + length = _as_pairs(length, padded.ndim, as_index=True) + for axis, width_pair, length_pair in zip(axes, pad_width, length): + roi = _view_roi(padded, original_area_slice, axis) + stat_pair = _get_stats(roi, axis, width_pair, length_pair, func) + _set_pad_area(roi, axis, width_pair, stat_pair) + + elif mode in {"reflect", "symmetric"}: + method = kwargs.get("reflect_type", "even") + include_edge = True if mode == "symmetric" else False + for axis, (left_index, right_index) in zip(axes, pad_width): + if array.shape[axis] == 1 and (left_index > 0 or right_index > 0): + # Extending singleton dimension for 'reflect' is legacy + # behavior; it really should raise an error. + edge_pair = _get_edges(padded, axis, (left_index, right_index)) + _set_pad_area( + padded, axis, (left_index, right_index), edge_pair + ) + continue + + roi = _view_roi(padded, original_area_slice, axis) + while left_index > 0 or right_index > 0: + # Iteratively pad until dimension is filled with reflected + # values. This is necessary if the pad area is larger than + # the length of the original values in the current dimension. + left_index, right_index = _set_reflect_both( + roi, + axis, + (left_index, right_index), + method, + array.shape[axis], + include_edge, + ) + + elif mode == "wrap": + for axis, (left_index, right_index) in zip(axes, pad_width): + roi = _view_roi(padded, original_area_slice, axis) + original_period = padded.shape[axis] - right_index - left_index + while left_index > 0 or right_index > 0: + # Iteratively pad until dimension is filled with wrapped + # values. This is necessary if the pad area is larger than + # the length of the original values in the current dimension. + left_index, right_index = _set_wrap_both( + roi, axis, (left_index, right_index), original_period + ) + + return padded diff --git a/tests/test_manipulation.py b/tests/test_manipulation.py index ba0166379906..08893805a27f 100644 --- a/tests/test_manipulation.py +++ b/tests/test_manipulation.py @@ -4,9 +4,11 @@ import numpy import pytest from dpctl.tensor._numpy_helper import AxisError +from numpy.lib._arraypad_impl import _as_pairs as numpy_as_pairs from numpy.testing import assert_array_equal, assert_equal, assert_raises import dpnp +from dpnp.dpnp_utils.dpnp_utils_pad import _as_pairs as dpnp_as_pairs from tests.third_party.cupy import testing from .helper import ( @@ -448,6 +450,509 @@ def test_no_copy(self): assert_array_equal(b, a) +class TestPad: + _all_modes = { + "constant": {"constant_values": 0}, + "edge": {}, + "linear_ramp": {"end_values": 0}, + "maximum": {"stat_length": None}, + "mean": {"stat_length": None}, + "minimum": {"stat_length": None}, + "reflect": {"reflect_type": "even"}, + "symmetric": {"reflect_type": "even"}, + "wrap": {}, + "empty": {}, + } + + @pytest.mark.parametrize("mode", _all_modes.keys() - {"empty"}) + def test_basic(self, mode): + a_np = numpy.arange(100) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, (25, 20), mode=mode) + result = dpnp.pad(a_dp, (25, 20), mode=mode) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_memory_layout_persistence(self, mode): + """Test if C and F order is preserved for all pad modes.""" + x = dpnp.ones((5, 10), order="C") + assert dpnp.pad(x, 5, mode).flags.c_contiguous + x = dpnp.ones((5, 10), order="F") + assert dpnp.pad(x, 5, mode).flags.f_contiguous + + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_dtype_persistence(self, dtype, mode): + arr = dpnp.zeros((3, 2, 1), dtype=dtype) + result = dpnp.pad(arr, 1, mode=mode) + assert result.dtype == dtype + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_non_contiguous_array(self, mode): + a_np = numpy.arange(24).reshape(4, 6)[::2, ::2] + a_dp = dpnp.arange(24).reshape(4, 6)[::2, ::2] + expected = numpy.pad(a_np, (2, 3), mode=mode) + result = dpnp.pad(a_dp, (2, 3), mode=mode) + assert_array_equal(result, expected) + + # TODO: include "linear_ramp" when dpnp issue gh-2084 is resolved + @pytest.mark.parametrize("pad_width", [0, (0, 0), ((0, 0), (0, 0))]) + @pytest.mark.parametrize("mode", _all_modes.keys() - {"linear_ramp"}) + def test_zero_pad_width(self, pad_width, mode): + arr = dpnp.arange(30).reshape(6, 5) + assert_array_equal(arr, dpnp.pad(arr, pad_width, mode=mode)) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_pad_non_empty_dimension(self, mode): + a_np = numpy.ones((2, 0, 2)) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((3,), (0,), (1,)), mode=mode) + result = dpnp.pad(a_dp, ((3,), (0,), (1,)), mode=mode) + assert_array_equal(result, expected) + + @pytest.mark.parametrize( + "pad_width", + [ + (4, 5, 6, 7), + ((1,), (2,), (3,)), + ((1, 2), (3, 4), (5, 6)), + ((3, 4, 5), (0, 1, 2)), + ], + ) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_misshaped_pad_width1(self, pad_width, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = "operands could not be broadcast together" + with pytest.raises(ValueError, match=match): + dpnp.pad(arr, pad_width, mode) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_misshaped_pad_width2(self, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = ( + "input operand has more dimensions than allowed by the axis " + "remapping" + ) + with pytest.raises(ValueError, match=match): + dpnp.pad(arr, (((3,), (4,), (5,)), ((0,), (1,), (2,))), mode) + + @pytest.mark.parametrize( + "pad_width", [-2, (-2,), (3, -1), ((5, 2), (-2, 3)), ((-4,), (2,))] + ) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_negative_pad_width(self, pad_width, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = "index can't contain negative values" + with pytest.raises(ValueError, match=match): + dpnp.pad(arr, pad_width, mode) + + @pytest.mark.parametrize( + "pad_width", + ["3", "word", None, 3.4, complex(1, -1), ((-2.1, 3), (3, 2))], + ) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_bad_type(self, pad_width, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = "`pad_width` must be of integral type." + with pytest.raises(TypeError, match=match): + dpnp.pad(arr, pad_width, mode) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_kwargs(self, mode): + """Test behavior of pad's kwargs for the given mode.""" + allowed = self._all_modes[mode] + not_allowed = {} + for kwargs in self._all_modes.values(): + if kwargs != allowed: + not_allowed.update(kwargs) + # Test if allowed keyword arguments pass + dpnp.pad(dpnp.array([1, 2, 3]), 1, mode, **allowed) + # Test if prohibited keyword arguments of other modes raise an error + for key, value in not_allowed.items(): + match = f"unsupported keyword arguments for mode '{mode}'" + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.array([1, 2, 3]), 1, mode, **{key: value}) + + @pytest.mark.parametrize("mode", [1, "const", object(), None, True, False]) + def test_unsupported_mode(self, mode): + match = f"mode '{mode}' is not supported" + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.array([1, 2, 3]), 4, mode=mode) + + def test_pad_default(self): + a_np = numpy.array([1, 1]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 2) + result = dpnp.pad(a_dp, 2) + assert_array_equal(result, expected) + + @pytest.mark.parametrize( + "pad_width", + [numpy.array(((2, 3), (3, 2))), dpnp.array(((2, 3), (3, 2)))], + ) + def test_pad_width_as_ndarray(self, pad_width): + a_np = numpy.arange(12).reshape(4, 3) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((2, 3), (3, 2))) + result = dpnp.pad(a_dp, pad_width) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [5, (25, 20)]) + @pytest.mark.parametrize("constant_values", [3, (10, 20)]) + def test_constant_1d(self, pad_width, constant_values): + a_np = numpy.arange(100) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "constant", constant_values=constant_values + ) + result = dpnp.pad( + a_dp, pad_width, "constant", constant_values=constant_values + ) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((1, 2), (1, 3))]) + @pytest.mark.parametrize("constant_values", [3, ((1, 2), (3, 4))]) + def test_constant_2d(self, pad_width, constant_values): + a_np = numpy.arange(30).reshape(5, 6) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "constant", constant_values=constant_values + ) + result = dpnp.pad( + a_dp, pad_width, "constant", constant_values=constant_values + ) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("dtype", [numpy.int32, numpy.float32]) + def test_constant_float(self, dtype): + # If input array is int, but constant_values are float, the dtype of + # the array to be padded is kept + # If input array is float, and constant_values are float, the dtype of + # the array to be padded is kept - here retaining the float constants + a_np = numpy.arange(30, dtype=dtype).reshape(5, 6) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, (25, 20), "constant", constant_values=1.1) + result = dpnp.pad(a_dp, (25, 20), "constant", constant_values=1.1) + assert_array_equal(result, expected) + + def test_constant_large_integers(self): + uint64_max = 2**64 - 1 + a_np = numpy.full(5, uint64_max, dtype=numpy.uint64) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 1, "constant", constant_values=a_np.min()) + result = dpnp.pad(a_dp, 1, "constant", constant_values=a_dp.min()) + assert_array_equal(result, expected) + + int64_max = 2**63 - 1 + a_np = numpy.full(5, int64_max, dtype=numpy.int64) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 1, "constant", constant_values=a_np.min()) + result = dpnp.pad(a_dp, 1, "constant", constant_values=a_dp.min()) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((2, 3), (3, 2))]) + def test_edge(self, pad_width): + a_np = numpy.arange(12).reshape(4, 3) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, pad_width, "edge") + result = dpnp.pad(a_dp, pad_width, "edge") + assert_array_equal(result, expected) + + def test_edge_nd(self): + a_np = numpy.array([1, 2, 3]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((1, 2),), "edge") + result = dpnp.pad(a_dp, ((1, 2),), "edge") + assert_array_equal(result, expected) + + a_np = numpy.array([[1, 2, 3], [4, 5, 6]]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((1, 2),), "edge") + result = dpnp.pad(a_dp, ((1, 2), (1, 2)), "edge") + assert_array_equal(result, expected) + + a_np = numpy.arange(24).reshape(2, 3, 4) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((1, 2),), "edge") + result = dpnp.pad(a_dp, ((1, 2), (1, 2), (1, 2)), "edge") + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [5, (25, 20)]) + @pytest.mark.parametrize("end_values", [3, (10, 20)]) + def test_linear_ramp_1d(self, pad_width, end_values): + a_np = numpy.arange(100, dtype=numpy.float32) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "linear_ramp", end_values=end_values + ) + result = dpnp.pad(a_dp, pad_width, "linear_ramp", end_values=end_values) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize( + "pad_width", [(2, 2), ((1,), (2,)), ((1, 2), (1, 3))] + ) + @pytest.mark.parametrize("end_values", [3, (0, 0), ((1, 2), (3, 4))]) + def test_linear_ramp_2d(self, pad_width, end_values): + a_np = numpy.arange(20, dtype=numpy.float32).reshape(4, 5) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "linear_ramp", end_values=end_values + ) + result = dpnp.pad(a_dp, pad_width, "linear_ramp", end_values=end_values) + assert_dtype_allclose(result, expected) + + def test_linear_ramp_end_values(self): + """Ensure that end values are exact.""" + a_dp = dpnp.ones(10).reshape(2, 5) + a = dpnp.pad(a_dp, (223, 123), mode="linear_ramp") + assert_equal(a[:, 0], 0.0) + assert_equal(a[:, -1], 0.0) + assert_equal(a[0, :], 0.0) + assert_equal(a[-1, :], 0.0) + + @pytest.mark.parametrize( + "dtype", [numpy.uint32, numpy.uint64] + get_all_dtypes(no_none=True) + ) + @pytest.mark.parametrize("data, end_values", [([3], 0), ([0], 3)]) + def test_linear_ramp_negative_diff(self, dtype, data, end_values): + """ + Check correct behavior of unsigned dtypes if there is a negative + difference between the edge to pad and `end_values`. Check both cases + to be independent of implementation. Test behavior for all other dtypes + in case dtype casting interferes with complex dtypes. + """ + a_np = numpy.array(data, dtype=dtype) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 3, mode="linear_ramp", end_values=end_values) + result = dpnp.pad(a_dp, 3, mode="linear_ramp", end_values=end_values) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [5, (25, 20)]) + @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) + @pytest.mark.parametrize("stat_length", [10, (2, 3)]) + def test_stat_func_1d(self, pad_width, mode, stat_length): + a_np = numpy.arange(100) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, stat_length=stat_length + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, stat_length=stat_length) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((2, 3), (3, 2))]) + @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) + @pytest.mark.parametrize("stat_length", [(3,), (2, 3)]) + def test_stat_func_2d(self, pad_width, mode, stat_length): + a_np = numpy.arange(30).reshape(6, 5) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, stat_length=stat_length + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, stat_length=stat_length) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) + def test_same_prepend_append(self, mode): + """Test that appended and prepended values are equal""" + a = dpnp.array([-1, 2, -1]) + dpnp.array( + [0, 1e-12, 0], dtype=dpnp.float32 + ) + result = dpnp.pad(a, (1, 1), mode) + assert_equal(result[0], result[-1]) + + def test_mean_with_zero_stat_length(self): + a_np = numpy.array([1.0, 2.0]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, (1, 2), "mean") + result = dpnp.pad(a_dp, (1, 2), "mean") + assert_array_equal(result, expected) + + @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) + @pytest.mark.parametrize( + "stat_length", [-2, (-2,), (3, -1), ((5, 2), (-2, 3)), ((-4,), (2,))] + ) + def test_negative_stat_length(self, mode, stat_length): + a_dp = dpnp.arange(30).reshape((6, 5)) + match = "index can't contain negative values" + with pytest.raises(ValueError, match=match): + dpnp.pad(a_dp, 2, mode, stat_length=stat_length) + + @pytest.mark.parametrize("mode", ["minimum", "maximum"]) + def test_zero_stat_length_invalid(self, mode): + a = dpnp.array([1.0, 2.0]) + match = "stat_length of 0 yields no value for padding" + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 0, mode, stat_length=0) + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 0, mode, stat_length=(1, 0)) + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 1, mode, stat_length=0) + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 1, mode, stat_length=(1, 0)) + + @pytest.mark.parametrize("pad_width", [2, 3, 4, [1, 10], [15, 2], [45, 10]]) + @pytest.mark.parametrize("mode", ["reflect", "symmetric"]) + @pytest.mark.parametrize("reflect_type", ["even", "odd"]) + def test_reflect_symmetric_1d(self, pad_width, mode, reflect_type): + a_np = numpy.array([1, 2, 3, 4]) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, reflect_type=reflect_type + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, reflect_type=reflect_type) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("data", [[[4, 5, 6], [6, 7, 8]], [[4, 5, 6]]]) + @pytest.mark.parametrize("pad_width", [10, (5, 7)]) + @pytest.mark.parametrize("mode", ["reflect", "symmetric"]) + @pytest.mark.parametrize("reflect_type", ["even", "odd"]) + def test_reflect_symmetric_2d(self, data, pad_width, mode, reflect_type): + a_np = numpy.array(data) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, reflect_type=reflect_type + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, reflect_type=reflect_type) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [2, 3, 4, [1, 10], [15, 2], [45, 10]]) + def test_wrap_1d(self, pad_width): + a_np = numpy.array([1, 2, 3, 4]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, pad_width, "wrap") + result = dpnp.pad(a_dp, pad_width, "wrap") + assert_array_equal(result, expected) + + @pytest.mark.parametrize("data", [[[4, 5, 6], [6, 7, 8]], [[4, 5, 6]]]) + @pytest.mark.parametrize("pad_width", [10, (5, 7), (1, 3), (3, 1)]) + def test_wrap_2d(self, data, pad_width): + a_np = numpy.array(data) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, pad_width, "wrap") + result = dpnp.pad(a_dp, pad_width, "wrap") + assert_array_equal(result, expected) + + def test_empty(self): + a_np = numpy.arange(24).reshape(4, 6) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, [(2, 3), (3, 1)], "empty") + result = dpnp.pad(a_dp, [(2, 3), (3, 1)], "empty") + # omit uninitialized "empty" boundary from the comparison + assert result.shape == expected.shape + assert_equal(result[2:-3, 3:-1], expected[2:-3, 3:-1]) + + # Check how padding behaves on arrays with an empty dimension. + # empty axis can only be padded using modes 'constant' or 'empty' + @pytest.mark.parametrize("mode", ["constant", "empty"]) + def test_pad_empty_dim_valid(self, mode): + """empty axis can only be padded using modes 'constant' or 'empty'""" + a_np = numpy.zeros((3, 0, 2)) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, [(0,), (2,), (1,)], mode) + result = dpnp.pad(a_dp, [(0,), (2,), (1,)], mode) + assert_array_equal(result, expected) + + @pytest.mark.parametrize( + "mode", + _all_modes.keys() - {"constant", "empty"}, + ) + def test_pad_empty_dim_invalid(self, mode): + match = ( + "can't extend empty axis 0 using modes other than 'constant' " + "or 'empty'" + ) + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.array([]), 4, mode=mode) + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.ndarray(0), 4, mode=mode) + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.zeros((0, 3)), ((1,), (0,)), mode=mode) + + def test_vector_functionality(self): + def _padwithtens(vector, pad_width, iaxis, kwargs): + vector[: pad_width[0]] = 10 + vector[-pad_width[1] :] = 10 + + a_np = numpy.arange(6).reshape(2, 3) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 2, _padwithtens) + result = dpnp.pad(a_dp, 2, _padwithtens) + assert_array_equal(result, expected) + + # test _as_pairs an internal function used by dpnp.pad + def test_as_pairs_single_value(self): + """Test casting for a single value.""" + for x in (3, [3], [[3]]): + result = dpnp_as_pairs(x, 10) + expected = numpy_as_pairs(x, 10) + assert_equal(result, expected) + + def test_as_pairs_two_values(self): + """Test proper casting for two different values.""" + # Broadcasting in the first dimension with numbers + for x in ([3, 4], [[3, 4]]): + result = dpnp_as_pairs(x, 10) + expected = numpy_as_pairs(x, 10) + assert_equal(result, expected) + + # Broadcasting in the second / last dimension with numbers + assert_equal( + dpnp_as_pairs([[3], [4]], 2), + numpy_as_pairs([[3], [4]], 2), + ) + + def test_as_pairs_with_none(self): + assert_equal( + dpnp_as_pairs(None, 3, as_index=False), + numpy_as_pairs(None, 3, as_index=False), + ) + assert_equal( + dpnp_as_pairs(None, 3, as_index=True), + numpy_as_pairs(None, 3, as_index=True), + ) + + def test_as_pairs_pass_through(self): + """Test if `x` already matching desired output are passed through.""" + a_np = numpy.arange(12).reshape((6, 2)) + a_dp = dpnp.arange(12).reshape((6, 2)) + assert_equal( + dpnp_as_pairs(a_dp, 6), + numpy_as_pairs(a_np, 6), + ) + + def test_as_pairs_as_index(self): + """Test results if `as_index=True`.""" + assert_equal( + dpnp_as_pairs([2.6, 3.3], 10, as_index=True), + numpy_as_pairs([2.6, 3.3], 10, as_index=True), + ) + assert_equal( + dpnp_as_pairs([2.6, 4.49], 10, as_index=True), + numpy_as_pairs([2.6, 4.49], 10, as_index=True), + ) + for x in ( + -3, + [-3], + [[-3]], + [-3, 4], + [3, -4], + [[-3, 4]], + [[4, -3]], + [[1, 2]] * 9 + [[1, -2]], + ): + with pytest.raises(ValueError, match="negative values"): + dpnp_as_pairs(x, 10, as_index=True) + + def test_as_pairs_exceptions(self): + """Ensure faulty usage is discovered.""" + with pytest.raises(ValueError, match="more dimensions than allowed"): + dpnp_as_pairs([[[3]]], 10) + with pytest.raises(ValueError, match="could not be broadcast"): + dpnp_as_pairs([[1, 2], [3, 4]], 3) + with pytest.raises(ValueError, match="could not be broadcast"): + dpnp_as_pairs(dpnp.ones((2, 3)), 3) + + class TestRepeat: @pytest.mark.parametrize( "data", @@ -715,7 +1220,7 @@ def test_non_array_input(self): assert expected.flags["F"] == result.flags["F"] assert expected.flags["W"] == result.flags["W"] assert expected.dtype == result.dtype - assert_array_equal(expected, result) + assert_array_equal(result, expected) def test_C_and_F_simul(self): a = self.generate_all_false("f4") @@ -731,7 +1236,7 @@ def test_copy(self): result = dpnp.require(a_dp, requirements=["W", "C"]) # copy is done assert result is not a_dp - assert_array_equal(expected, result) + assert_array_equal(result, expected) class TestResize: diff --git a/tests/test_sycl_queue.py b/tests/test_sycl_queue.py index e1ae1d8e65dc..68f92f1b65fd 100644 --- a/tests/test_sycl_queue.py +++ b/tests/test_sycl_queue.py @@ -1291,6 +1291,32 @@ def test_out_multi_dot(device): assert_sycl_queue_equal(result.sycl_queue, exec_q) +@pytest.mark.parametrize( + "device", + valid_devices, + ids=[device.filter_string for device in valid_devices], +) +def test_pad(device): + all_modes = [ + "constant", + "edge", + "linear_ramp", + "maximum", + "mean", + "minimum", + "reflect", + "symmetric", + "wrap", + "empty", + ] + dpnp_data = dpnp.arange(100, device=device) + expected_queue = dpnp_data.sycl_queue + for mode in all_modes: + result = dpnp.pad(dpnp_data, (25, 20), mode=mode) + result_queue = result.sycl_queue + assert_sycl_queue_equal(result_queue, expected_queue) + + @pytest.mark.parametrize( "device", valid_devices, diff --git a/tests/test_usm_type.py b/tests/test_usm_type.py index 592340d6c0db..a56a571af5b6 100644 --- a/tests/test_usm_type.py +++ b/tests/test_usm_type.py @@ -1013,6 +1013,27 @@ def test_eigenvalue(func, shape, usm_type): assert a.usm_type == dp_val.usm_type +@pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types) +def test_pad(usm_type): + all_modes = [ + "constant", + "edge", + "linear_ramp", + "maximum", + "mean", + "minimum", + "reflect", + "symmetric", + "wrap", + "empty", + ] + dpnp_data = dp.arange(100, usm_type=usm_type) + assert dpnp_data.usm_type == usm_type + for mode in all_modes: + result = dp.pad(dpnp_data, (25, 20), mode=mode) + assert result.usm_type == usm_type + + @pytest.mark.parametrize("usm_type", list_of_usm_types, ids=list_of_usm_types) def test_require(usm_type): dpnp_data = dp.arange(10, usm_type=usm_type).reshape(2, 5) diff --git a/tests/third_party/cupy/padding_test/__init__.py b/tests/third_party/cupy/padding_test/__init__.py new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/tests/third_party/cupy/padding_test/test_pad.py b/tests/third_party/cupy/padding_test/test_pad.py new file mode 100644 index 000000000000..1521a3729a7f --- /dev/null +++ b/tests/third_party/cupy/padding_test/test_pad.py @@ -0,0 +1,457 @@ +import unittest +import warnings + +import numpy +import pytest + +import dpnp as cupy +from tests.helper import has_support_aspect64 +from tests.third_party.cupy import testing + +if numpy.lib.NumpyVersion(numpy.__version__) >= "2.0.0b1": + from numpy.exceptions import ComplexWarning +else: + from numpy import ComplexWarning + + +@testing.parameterize( + *testing.product( + { + "array": [numpy.arange(6).reshape([2, 3])], + "pad_width": [1, [1, 2], [[1, 2], [3, 4]]], + # mode "mean" is non-exact, so it is tested in a separate class + "mode": [ + "constant", + "edge", + "linear_ramp", + "maximum", + "minimum", + "reflect", + "symmetric", + "wrap", + ], + } + ) +) +class TestPadDefault(unittest.TestCase): + @testing.for_all_dtypes(no_bool=True) + @testing.numpy_cupy_array_equal() + def test_pad_default(self, xp, dtype): + array = xp.array(self.array, dtype=dtype) + + if xp.dtype(dtype).kind in ["i", "u"] and self.mode == "linear_ramp": + # TODO: can remove this skip once cupy/cupy/#2330 is merged + return array + + # Older version of NumPy(<1.12) can emit ComplexWarning + def f(): + return xp.pad(array, self.pad_width, mode=self.mode) + + if xp is numpy: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ComplexWarning) + return f() + else: + return f() + + +@testing.parameterize( + *testing.product( + { + "array": [numpy.arange(6).reshape([2, 3])], + "pad_width": [1, [1, 2], [[1, 2], [3, 4]]], + } + ) +) +class TestPadDefaultMean(unittest.TestCase): + @testing.for_all_dtypes(no_bool=True) + @testing.numpy_cupy_array_almost_equal(decimal=5) + def test_pad_default(self, xp, dtype): + array = xp.array(self.array, dtype=dtype) + + if xp.dtype(dtype).kind in ["i", "u"]: + # TODO: can remove this skip once cupy/cupy/#2330 is merged + return array + + # Older version of NumPy(<1.12) can emit ComplexWarning + def f(): + return xp.pad(array, self.pad_width, mode="mean") + + if xp is numpy: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ComplexWarning) + return f() + else: + return f() + + +@testing.parameterize( + # mode="constant" + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": 1, + "mode": "constant", + "constant_values": 3, + }, + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": [1, 2], + "mode": "constant", + "constant_values": [3, 4], + }, + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": [[1, 2], [3, 4]], + "mode": "constant", + "constant_values": [[3, 4], [5, 6]], + }, + # mode="reflect" + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": 1, + "mode": "reflect", + "reflect_type": "odd", + }, + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": [1, 2], + "mode": "reflect", + "reflect_type": "odd", + }, + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": [[1, 2], [3, 4]], + "mode": "reflect", + "reflect_type": "odd", + }, + # mode="symmetric" + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": 1, + "mode": "symmetric", + "reflect_type": "odd", + }, + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": [1, 2], + "mode": "symmetric", + "reflect_type": "odd", + }, + { + "array": numpy.arange(6).reshape([2, 3]), + "pad_width": [[1, 2], [3, 4]], + "mode": "symmetric", + "reflect_type": "odd", + }, + # mode="minimum" + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": 1, + "mode": "minimum", + "stat_length": 2, + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [1, 2], + "mode": "minimum", + "stat_length": (2, 4), + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [[1, 2], [3, 4]], + "mode": "minimum", + "stat_length": ((2, 4), (3, 5)), + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [[1, 2], [3, 4]], + "mode": "minimum", + "stat_length": None, + }, + # mode="maximum" + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": 1, + "mode": "maximum", + "stat_length": 2, + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [1, 2], + "mode": "maximum", + "stat_length": (2, 4), + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [[1, 2], [3, 4]], + "mode": "maximum", + "stat_length": ((2, 4), (3, 5)), + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [[1, 2], [3, 4]], + "mode": "maximum", + "stat_length": None, + }, +) +# Old numpy does not work with multi-dimensional constant_values +class TestPad(unittest.TestCase): + @testing.for_all_dtypes(no_bool=True) + @testing.numpy_cupy_array_equal() + def test_pad(self, xp, dtype): + array = xp.array(self.array, dtype=dtype) + + # Older version of NumPy(<1.12) can emit ComplexWarning + def f(): + if self.mode == "constant": + return xp.pad( + array, + self.pad_width, + mode=self.mode, + constant_values=self.constant_values, + ) + elif self.mode in ["minimum", "maximum"]: + return xp.pad( + array, + self.pad_width, + mode=self.mode, + stat_length=self.stat_length, + ) + elif self.mode in ["reflect", "symmetric"]: + return xp.pad( + array, + self.pad_width, + mode=self.mode, + reflect_type=self.reflect_type, + ) + + if xp is numpy: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ComplexWarning) + return f() + else: + return f() + + +@testing.parameterize( + # mode="mean" + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": 1, + "mode": "mean", + "stat_length": 2, + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [1, 2], + "mode": "mean", + "stat_length": (2, 4), + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [[1, 2], [3, 4]], + "mode": "mean", + "stat_length": ((2, 4), (3, 5)), + }, + { + "array": numpy.arange(60).reshape([5, 12]), + "pad_width": [[1, 2], [3, 4]], + "mode": "mean", + "stat_length": None, + }, +) +# Old numpy does not work with multi-dimensional constant_values +class TestPadMean(unittest.TestCase): + @testing.for_all_dtypes(no_bool=True) + @testing.numpy_cupy_array_almost_equal(decimal=5) + def test_pad(self, xp, dtype): + array = xp.array(self.array, dtype=dtype) + + if xp.dtype(dtype).kind in ["i", "u"]: + # TODO: can remove this skip once cupy/cupy/#2330 is merged + return array + + # Older version of NumPy(<1.12) can emit ComplexWarning + def f(): + return xp.pad( + array, + self.pad_width, + mode=self.mode, + stat_length=self.stat_length, + ) + + if xp is numpy: + with warnings.catch_warnings(): + warnings.simplefilter("ignore", ComplexWarning) + return f() + else: + return f() + + +class TestPadNumpybug(unittest.TestCase): + @testing.for_all_dtypes(no_bool=True, no_complex=True) + @testing.numpy_cupy_array_equal() + def test_pad_highdim_default(self, xp, dtype): + array = xp.arange(6, dtype=dtype).reshape([2, 3]) + pad_width = [[1, 2], [3, 4]] + constant_values = [[1, 2], [3, 4]] + a = xp.pad( + array, pad_width, mode="constant", constant_values=constant_values + ) + return a + + +class TestPadEmpty(unittest.TestCase): + @testing.with_requires("numpy>=1.17") + @testing.for_all_dtypes(no_bool=True) + @testing.numpy_cupy_array_equal() + def test_pad_empty(self, xp, dtype): + array = xp.arange(6, dtype=dtype).reshape([2, 3]) + pad_width = 2 + a = xp.pad(array, pad_width=pad_width, mode="empty") + # omit uninitialized "empty" boundary from the comparison + return a[pad_width:-pad_width, pad_width:-pad_width] + + +class TestPadCustomFunction(unittest.TestCase): + @testing.for_all_dtypes(no_bool=True) + @testing.numpy_cupy_array_equal() + def test_pad_via_func(self, xp, dtype): + def _padwithtens(vector, pad_width, iaxis, kwargs): + vector[: pad_width[0]] = 10 + vector[-pad_width[1] :] = 10 + + a = xp.arange(6, dtype=dtype).reshape(2, 3) + a = xp.pad(a, 2, _padwithtens) + return a + + +@testing.parameterize( + # mode="constant" + {"array": [], "pad_width": 1, "mode": "constant", "constant_values": 3}, + {"array": 1, "pad_width": 1, "mode": "constant", "constant_values": 3}, + { + "array": [0, 1, 2, 3], + "pad_width": 1, + "mode": "constant", + "constant_values": 3, + }, + { + "array": [0, 1, 2, 3], + "pad_width": [1, 2], + "mode": "constant", + "constant_values": 3, + }, + # mode="edge" + {"array": 1, "pad_width": 1, "mode": "edge"}, + {"array": [0, 1, 2, 3], "pad_width": 1, "mode": "edge"}, + {"array": [0, 1, 2, 3], "pad_width": [1, 2], "mode": "edge"}, + # mode="reflect" + {"array": 1, "pad_width": 1, "mode": "reflect"}, + {"array": [0, 1, 2, 3], "pad_width": 1, "mode": "reflect"}, + {"array": [0, 1, 2, 3], "pad_width": [1, 2], "mode": "reflect"}, +) +class TestPadSpecial(unittest.TestCase): + @testing.numpy_cupy_array_equal(type_check=has_support_aspect64()) + def test_pad_special(self, xp): + array = xp.array(self.array) + + if self.mode == "constant": + a = xp.pad( + array, + self.pad_width, + mode=self.mode, + constant_values=self.constant_values, + ) + elif self.mode in ["edge", "reflect"]: + a = xp.pad(array, self.pad_width, mode=self.mode) + return a + + +@testing.parameterize( + { + "array": [0, 1, 2, 3], + "pad_width": [-1, 1], + "mode": "constant", + "kwargs": {"constant_values": 3}, + }, + { + "array": [0, 1, 2, 3], + "pad_width": [[3, 4], [5, 6]], + "mode": "constant", + "kwargs": {"constant_values": 3}, + }, + { + "array": [0, 1, 2, 3], + "pad_width": [1], + "mode": "constant", + "kwargs": {"notallowedkeyword": 3}, + }, + # edge + {"array": [], "pad_width": 1, "mode": "edge", "kwargs": {}}, + { + "array": [0, 1, 2, 3], + "pad_width": [-1, 1], + "mode": "edge", + "kwargs": {}, + }, + { + "array": [0, 1, 2, 3], + "pad_width": [[3, 4], [5, 6]], + "mode": "edge", + "kwargs": {}, + }, + { + "array": [0, 1, 2, 3], + "pad_width": [1], + "mode": "edge", + "kwargs": {"notallowedkeyword": 3}, + }, + # mode="reflect" + {"array": [], "pad_width": 1, "mode": "reflect", "kwargs": {}}, + { + "array": [0, 1, 2, 3], + "pad_width": [-1, 1], + "mode": "reflect", + "kwargs": {}, + }, + { + "array": [0, 1, 2, 3], + "pad_width": [[3, 4], [5, 6]], + "mode": "reflect", + "kwargs": {}, + }, + { + "array": [0, 1, 2, 3], + "pad_width": [1], + "mode": "reflect", + "kwargs": {"notallowedkeyword": 3}, + }, +) +@testing.with_requires("numpy>=1.17") +class TestPadValueError(unittest.TestCase): + def test_pad_failure(self): + for xp in (numpy, cupy): + array = xp.array(self.array) + with pytest.raises(ValueError): + xp.pad(array, self.pad_width, self.mode, **self.kwargs) + + +@testing.parameterize( + { + "array": [0, 1, 2, 3], + "pad_width": [], + "mode": "constant", + "kwargs": {"constant_values": 3}, + }, + # edge + {"array": [0, 1, 2, 3], "pad_width": [], "mode": "edge", "kwargs": {}}, + # mode="reflect" + {"array": [0, 1, 2, 3], "pad_width": [], "mode": "reflect", "kwargs": {}}, +) +class TestPadTypeError(unittest.TestCase): + def test_pad_failure(self): + for xp in (numpy, cupy): + array = xp.array(self.array) + with pytest.raises(TypeError): + xp.pad(array, self.pad_width, self.mode, **self.kwargs) From 8e8def0a20a3e60bce50d8b90475f357479f23e0 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Mon, 7 Oct 2024 16:14:01 -0500 Subject: [PATCH 02/12] fix directory name --- .../third_party/cupy/{padding_test => padding_tests}/__init__.py | 0 .../third_party/cupy/{padding_test => padding_tests}/test_pad.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/third_party/cupy/{padding_test => padding_tests}/__init__.py (100%) rename tests/third_party/cupy/{padding_test => padding_tests}/test_pad.py (100%) diff --git a/tests/third_party/cupy/padding_test/__init__.py b/tests/third_party/cupy/padding_tests/__init__.py similarity index 100% rename from tests/third_party/cupy/padding_test/__init__.py rename to tests/third_party/cupy/padding_tests/__init__.py diff --git a/tests/third_party/cupy/padding_test/test_pad.py b/tests/third_party/cupy/padding_tests/test_pad.py similarity index 100% rename from tests/third_party/cupy/padding_test/test_pad.py rename to tests/third_party/cupy/padding_tests/test_pad.py From ad66c3a9cc84807990e3373ca0b9c7c8696cbcb3 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 8 Oct 2024 00:02:07 -0500 Subject: [PATCH 03/12] fix failed test cases --- tests/test_manipulation.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/tests/test_manipulation.py b/tests/test_manipulation.py index 08893805a27f..07ccbd2b2205 100644 --- a/tests/test_manipulation.py +++ b/tests/test_manipulation.py @@ -4,7 +4,12 @@ import numpy import pytest from dpctl.tensor._numpy_helper import AxisError -from numpy.lib._arraypad_impl import _as_pairs as numpy_as_pairs + +if numpy.lib.NumpyVersion(numpy.__version__) >= "2.0.0": + from numpy.lib._arraypad_impl import _as_pairs as numpy_as_pairs +else: + from numpy.lib.arraypad import _as_pairs as numpy_as_pairs + from numpy.testing import assert_array_equal, assert_equal, assert_raises import dpnp @@ -464,13 +469,18 @@ class TestPad: "empty": {}, } - @pytest.mark.parametrize("mode", _all_modes.keys() - {"empty"}) + @pytest.mark.parametrize("mode", _all_modes.keys()) def test_basic(self, mode): a_np = numpy.arange(100) a_dp = dpnp.array(a_np) expected = numpy.pad(a_np, (25, 20), mode=mode) result = dpnp.pad(a_dp, (25, 20), mode=mode) - assert_array_equal(result, expected) + if mode == "empty": + # omit uninitialized "empty" boundary from the comparison + assert result.shape == expected.shape + assert_equal(result[25:-20], expected[25:-20]) + else: + assert_array_equal(result, expected) @pytest.mark.parametrize("mode", _all_modes.keys()) def test_memory_layout_persistence(self, mode): @@ -493,7 +503,12 @@ def test_non_contiguous_array(self, mode): a_dp = dpnp.arange(24).reshape(4, 6)[::2, ::2] expected = numpy.pad(a_np, (2, 3), mode=mode) result = dpnp.pad(a_dp, (2, 3), mode=mode) - assert_array_equal(result, expected) + if mode == "empty": + # omit uninitialized "empty" boundary from the comparison + assert result.shape == expected.shape + assert_equal(result[2:-3, 2:-3], expected[2:-3, 2:-3]) + else: + assert_array_equal(result, expected) # TODO: include "linear_ramp" when dpnp issue gh-2084 is resolved @pytest.mark.parametrize("pad_width", [0, (0, 0), ((0, 0), (0, 0))]) @@ -850,7 +865,7 @@ def test_pad_empty_dim_valid(self, mode): a_dp = dpnp.array(a_np) expected = numpy.pad(a_np, [(0,), (2,), (1,)], mode) result = dpnp.pad(a_dp, [(0,), (2,), (1,)], mode) - assert_array_equal(result, expected) + assert_dtype_allclose(result, expected) @pytest.mark.parametrize( "mode", From 1e68bee633d9febe683a38e6415e288a7718a6a8 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 8 Oct 2024 08:31:26 -0500 Subject: [PATCH 04/12] skip tests for numpy<2.0 --- tests/test_manipulation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_manipulation.py b/tests/test_manipulation.py index 07ccbd2b2205..a32e2b844b05 100644 --- a/tests/test_manipulation.py +++ b/tests/test_manipulation.py @@ -805,6 +805,7 @@ def test_zero_stat_length_invalid(self, mode): with pytest.raises(ValueError, match=match): dpnp.pad(a, 1, mode, stat_length=(1, 0)) + @testing.with_requires("numpy>=2.0") # numpy<2 has a bug, numpy-gh-25963 @pytest.mark.parametrize("pad_width", [2, 3, 4, [1, 10], [15, 2], [45, 10]]) @pytest.mark.parametrize("mode", ["reflect", "symmetric"]) @pytest.mark.parametrize("reflect_type", ["even", "odd"]) From 7d5b47a8a5f1099722d01694f7e591ca0fe3de23 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Mon, 14 Oct 2024 15:44:24 -0500 Subject: [PATCH 05/12] address comments --- dpnp/dpnp_iface_manipulation.py | 16 +- dpnp/dpnp_utils/dpnp_utils_pad.py | 9 +- tests/test_arraypad.py | 529 ++++++++++++++++++++++++++++++ tests/test_manipulation.py | 514 ----------------------------- 4 files changed, 540 insertions(+), 528 deletions(-) create mode 100644 tests/test_arraypad.py diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 5ffd52d1a2d5..db7eee30c7b7 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1848,7 +1848,7 @@ def pad(array, pad_width, mode="constant", **kwargs): Parameters ---------- array : {dpnp.ndarray, usm_ndarray} - The array to pad. + The array of rank ``N`` to pad. pad_width : {sequence, array_like, int} Number of values padded to the edges of each axis. ``((before_1, after_1), ... (before_N, after_N))`` unique pad widths @@ -1893,21 +1893,17 @@ def pad(array, pad_width, mode="constant", **kwargs): Padding function, see Notes. Default: ``"constant"``. stat_length : {None, int, sequence of ints}, optional - Used in "maximum", "mean", and "minimum". Number of + Used in ``"maximum"``, ``"mean"``, and ``"minimum"``. Number of values at edge of each axis used to calculate the statistic value. - ``((before_1, after_1), ... (before_N, after_N))`` unique statistic lengths for each axis. - ``(before, after)`` or ``((before, after),)`` yields same before and after statistic lengths for each axis. - ``(stat_length,)`` or ``int`` is a shortcut for ``before = after = statistic`` length for all axes. - Default: ``None``, to use the entire axis. constant_values : {sequence, scalar}, optional - Used in "constant". The values to set the padded values for each + Used in ``"constant"``. The values to set the padded values for each axis. ``((before_1, after_1), ... (before_N, after_N))`` unique pad constants for each axis. @@ -1917,7 +1913,7 @@ def pad(array, pad_width, mode="constant", **kwargs): ``before = after = constant`` for all axes. Default: ``0``. end_values : {sequence, scalar}, optional - Used in "linear_ramp". The values used for the ending value of the + Used in ``"linear_ramp"``. The values used for the ending value of the linear_ramp and that will form the edge of the padded array. ``((before_1, after_1), ... (before_N, after_N))`` unique end values for each axis. @@ -1927,9 +1923,9 @@ def pad(array, pad_width, mode="constant", **kwargs): ``before = after = constant`` for all axes. Default: ``0``. reflect_type : {"even", "odd"}, optional - Used in "reflect", and "symmetric". The "even" style is the + Used in ``"reflect"``, and ``"symmetric"``. The ``"even"`` style is the default with an unaltered reflection around the edge value. For - the "odd" style, the extended part of the array is created by + the ``"odd"`` style, the extended part of the array is created by subtracting the reflected values from two times the edge value. Default: ``"even"``. diff --git a/dpnp/dpnp_utils/dpnp_utils_pad.py b/dpnp/dpnp_utils/dpnp_utils_pad.py index 1f6508777535..838af5cc3a70 100644 --- a/dpnp/dpnp_utils/dpnp_utils_pad.py +++ b/dpnp/dpnp_utils/dpnp_utils_pad.py @@ -216,11 +216,11 @@ def _get_stats(padded, axis, width_pair, length_pair, stat_func): valid area in `padded` is considered. stat_func : function Function to compute statistic. The expected signature is - ``stat_func(x: ndarray, axis: int, keepdims: bool) -> ndarray``. + ``stat_func(x: dpnp.ndarray, axis: int, keepdims: bool) -> dpnp.ndarray``. Returns ------- - left_stat, right_stat : ndarray + left_stat, right_stat : dpnp.ndarray Calculated statistic for both sides of `padded`. """ @@ -642,6 +642,7 @@ def dpnp_pad(array, pad_width, mode="constant", **kwargs): # compute indices for the iteration axes, and append a trailing # ellipsis to prevent 0d arrays decaying to scalars + # TODO: replace with dpnp.ndindex when implemented inds = numpy.ndindex(view.shape[:-1]) inds = (ind + (Ellipsis,) for ind in inds) for ind in inds: @@ -744,7 +745,7 @@ def dpnp_pad(array, pad_width, mode="constant", **kwargs): elif mode in {"reflect", "symmetric"}: method = kwargs.get("reflect_type", "even") - include_edge = True if mode == "symmetric" else False + include_edge = mode == "symmetric" for axis, (left_index, right_index) in zip(axes, pad_width): if array.shape[axis] == 1 and (left_index > 0 or right_index > 0): # Extending singleton dimension for 'reflect' is legacy @@ -769,7 +770,7 @@ def dpnp_pad(array, pad_width, mode="constant", **kwargs): include_edge, ) - elif mode == "wrap": + else: # mode == "wrap": for axis, (left_index, right_index) in zip(axes, pad_width): roi = _view_roi(padded, original_area_slice, axis) original_period = padded.shape[axis] - right_index - left_index diff --git a/tests/test_arraypad.py b/tests/test_arraypad.py new file mode 100644 index 000000000000..8060864c2bbe --- /dev/null +++ b/tests/test_arraypad.py @@ -0,0 +1,529 @@ +import numpy +import pytest + +if numpy.lib.NumpyVersion(numpy.__version__) >= "2.0.0": + from numpy.lib._arraypad_impl import _as_pairs as numpy_as_pairs +else: + from numpy.lib.arraypad import _as_pairs as numpy_as_pairs + +from numpy.testing import assert_array_equal, assert_equal + +import dpnp +from dpnp.dpnp_utils.dpnp_utils_pad import _as_pairs as dpnp_as_pairs +from tests.third_party.cupy import testing + +from .helper import assert_dtype_allclose, get_all_dtypes + + +class TestPad: + _all_modes = { + "constant": {"constant_values": 0}, + "edge": {}, + "linear_ramp": {"end_values": 0}, + "maximum": {"stat_length": None}, + "mean": {"stat_length": None}, + "minimum": {"stat_length": None}, + "reflect": {"reflect_type": "even"}, + "symmetric": {"reflect_type": "even"}, + "wrap": {}, + "empty": {}, + } + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_basic(self, mode): + a_np = numpy.arange(100) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, (25, 20), mode=mode) + result = dpnp.pad(a_dp, (25, 20), mode=mode) + if mode == "empty": + # omit uninitialized "empty" boundary from the comparison + assert result.shape == expected.shape + assert_equal(result[25:-20], expected[25:-20]) + else: + assert_array_equal(result, expected) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_memory_layout_persistence(self, mode): + """Test if C and F order is preserved for all pad modes.""" + x = dpnp.ones((5, 10), order="C") + assert dpnp.pad(x, 5, mode).flags.c_contiguous + x = dpnp.ones((5, 10), order="F") + assert dpnp.pad(x, 5, mode).flags.f_contiguous + + @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_dtype_persistence(self, dtype, mode): + arr = dpnp.zeros((3, 2, 1), dtype=dtype) + result = dpnp.pad(arr, 1, mode=mode) + assert result.dtype == dtype + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_non_contiguous_array(self, mode): + a_np = numpy.arange(24).reshape(4, 6)[::2, ::2] + a_dp = dpnp.arange(24).reshape(4, 6)[::2, ::2] + expected = numpy.pad(a_np, (2, 3), mode=mode) + result = dpnp.pad(a_dp, (2, 3), mode=mode) + if mode == "empty": + # omit uninitialized "empty" boundary from the comparison + assert result.shape == expected.shape + assert_equal(result[2:-3, 2:-3], expected[2:-3, 2:-3]) + else: + assert_array_equal(result, expected) + + # TODO: include "linear_ramp" when dpnp issue gh-2084 is resolved + @pytest.mark.parametrize("pad_width", [0, (0, 0), ((0, 0), (0, 0))]) + @pytest.mark.parametrize("mode", _all_modes.keys() - {"linear_ramp"}) + def test_zero_pad_width(self, pad_width, mode): + arr = dpnp.arange(30).reshape(6, 5) + assert_array_equal(arr, dpnp.pad(arr, pad_width, mode=mode)) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_pad_non_empty_dimension(self, mode): + a_np = numpy.ones((2, 0, 2)) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((3,), (0,), (1,)), mode=mode) + result = dpnp.pad(a_dp, ((3,), (0,), (1,)), mode=mode) + assert_array_equal(result, expected) + + @pytest.mark.parametrize( + "pad_width", + [ + (4, 5, 6, 7), + ((1,), (2,), (3,)), + ((1, 2), (3, 4), (5, 6)), + ((3, 4, 5), (0, 1, 2)), + ], + ) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_misshaped_pad_width1(self, pad_width, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = "operands could not be broadcast together" + with pytest.raises(ValueError, match=match): + dpnp.pad(arr, pad_width, mode) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_misshaped_pad_width2(self, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = ( + "input operand has more dimensions than allowed by the axis " + "remapping" + ) + with pytest.raises(ValueError, match=match): + dpnp.pad(arr, (((3,), (4,), (5,)), ((0,), (1,), (2,))), mode) + + @pytest.mark.parametrize( + "pad_width", [-2, (-2,), (3, -1), ((5, 2), (-2, 3)), ((-4,), (2,))] + ) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_negative_pad_width(self, pad_width, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = "index can't contain negative values" + with pytest.raises(ValueError, match=match): + dpnp.pad(arr, pad_width, mode) + + @pytest.mark.parametrize( + "pad_width", + ["3", "word", None, 3.4, complex(1, -1), ((-2.1, 3), (3, 2))], + ) + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_bad_type(self, pad_width, mode): + arr = dpnp.arange(30).reshape((6, 5)) + match = "`pad_width` must be of integral type." + with pytest.raises(TypeError, match=match): + dpnp.pad(arr, pad_width, mode) + + @pytest.mark.parametrize("mode", _all_modes.keys()) + def test_kwargs(self, mode): + """Test behavior of pad's kwargs for the given mode.""" + allowed = self._all_modes[mode] + not_allowed = {} + for kwargs in self._all_modes.values(): + if kwargs != allowed: + not_allowed.update(kwargs) + # Test if allowed keyword arguments pass + dpnp.pad(dpnp.array([1, 2, 3]), 1, mode, **allowed) + # Test if prohibited keyword arguments of other modes raise an error + for key, value in not_allowed.items(): + match = f"unsupported keyword arguments for mode '{mode}'" + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.array([1, 2, 3]), 1, mode, **{key: value}) + + @pytest.mark.parametrize("mode", [1, "const", object(), None, True, False]) + def test_unsupported_mode(self, mode): + match = f"mode '{mode}' is not supported" + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.array([1, 2, 3]), 4, mode=mode) + + def test_pad_default(self): + a_np = numpy.array([1, 1]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 2) + result = dpnp.pad(a_dp, 2) + assert_array_equal(result, expected) + + @pytest.mark.parametrize( + "pad_width", + [numpy.array(((2, 3), (3, 2))), dpnp.array(((2, 3), (3, 2)))], + ) + def test_pad_width_as_ndarray(self, pad_width): + a_np = numpy.arange(12).reshape(4, 3) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((2, 3), (3, 2))) + result = dpnp.pad(a_dp, pad_width) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [5, (25, 20)]) + @pytest.mark.parametrize("constant_values", [3, (10, 20)]) + def test_constant_1d(self, pad_width, constant_values): + a_np = numpy.arange(100) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "constant", constant_values=constant_values + ) + result = dpnp.pad( + a_dp, pad_width, "constant", constant_values=constant_values + ) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((1, 2), (1, 3))]) + @pytest.mark.parametrize("constant_values", [3, ((1, 2), (3, 4))]) + def test_constant_2d(self, pad_width, constant_values): + a_np = numpy.arange(30).reshape(5, 6) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "constant", constant_values=constant_values + ) + result = dpnp.pad( + a_dp, pad_width, "constant", constant_values=constant_values + ) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("dtype", [numpy.int32, numpy.float32]) + def test_constant_float(self, dtype): + # If input array is int, but constant_values are float, the dtype of + # the array to be padded is kept + # If input array is float, and constant_values are float, the dtype of + # the array to be padded is kept - here retaining the float constants + a_np = numpy.arange(30, dtype=dtype).reshape(5, 6) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, (25, 20), "constant", constant_values=1.1) + result = dpnp.pad(a_dp, (25, 20), "constant", constant_values=1.1) + assert_array_equal(result, expected) + + def test_constant_large_integers(self): + uint64_max = 2**64 - 1 + a_np = numpy.full(5, uint64_max, dtype=numpy.uint64) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 1, "constant", constant_values=a_np.min()) + result = dpnp.pad(a_dp, 1, "constant", constant_values=a_dp.min()) + assert_array_equal(result, expected) + + int64_max = 2**63 - 1 + a_np = numpy.full(5, int64_max, dtype=numpy.int64) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 1, "constant", constant_values=a_np.min()) + result = dpnp.pad(a_dp, 1, "constant", constant_values=a_dp.min()) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((2, 3), (3, 2))]) + def test_edge(self, pad_width): + a_np = numpy.arange(12).reshape(4, 3) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, pad_width, "edge") + result = dpnp.pad(a_dp, pad_width, "edge") + assert_array_equal(result, expected) + + def test_edge_nd(self): + a_np = numpy.array([1, 2, 3]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((1, 2),), "edge") + result = dpnp.pad(a_dp, ((1, 2),), "edge") + assert_array_equal(result, expected) + + a_np = numpy.array([[1, 2, 3], [4, 5, 6]]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((1, 2),), "edge") + result = dpnp.pad(a_dp, ((1, 2), (1, 2)), "edge") + assert_array_equal(result, expected) + + a_np = numpy.arange(24).reshape(2, 3, 4) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, ((1, 2),), "edge") + result = dpnp.pad(a_dp, ((1, 2), (1, 2), (1, 2)), "edge") + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [5, (25, 20)]) + @pytest.mark.parametrize("end_values", [3, (10, 20)]) + def test_linear_ramp_1d(self, pad_width, end_values): + a_np = numpy.arange(100, dtype=numpy.float32) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "linear_ramp", end_values=end_values + ) + result = dpnp.pad(a_dp, pad_width, "linear_ramp", end_values=end_values) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize( + "pad_width", [(2, 2), ((1,), (2,)), ((1, 2), (1, 3))] + ) + @pytest.mark.parametrize("end_values", [3, (0, 0), ((1, 2), (3, 4))]) + def test_linear_ramp_2d(self, pad_width, end_values): + a_np = numpy.arange(20, dtype=numpy.float32).reshape(4, 5) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, "linear_ramp", end_values=end_values + ) + result = dpnp.pad(a_dp, pad_width, "linear_ramp", end_values=end_values) + assert_dtype_allclose(result, expected) + + def test_linear_ramp_end_values(self): + """Ensure that end values are exact.""" + a_dp = dpnp.ones(10).reshape(2, 5) + a = dpnp.pad(a_dp, (223, 123), mode="linear_ramp") + assert_equal(a[:, 0], 0.0) + assert_equal(a[:, -1], 0.0) + assert_equal(a[0, :], 0.0) + assert_equal(a[-1, :], 0.0) + + @pytest.mark.parametrize( + "dtype", [numpy.uint32, numpy.uint64] + get_all_dtypes(no_none=True) + ) + @pytest.mark.parametrize("data, end_values", [([3], 0), ([0], 3)]) + def test_linear_ramp_negative_diff(self, dtype, data, end_values): + """ + Check correct behavior of unsigned dtypes if there is a negative + difference between the edge to pad and `end_values`. Check both cases + to be independent of implementation. Test behavior for all other dtypes + in case dtype casting interferes with complex dtypes. + """ + a_np = numpy.array(data, dtype=dtype) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 3, mode="linear_ramp", end_values=end_values) + result = dpnp.pad(a_dp, 3, mode="linear_ramp", end_values=end_values) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [5, (25, 20)]) + @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) + @pytest.mark.parametrize("stat_length", [10, (2, 3)]) + def test_stat_func_1d(self, pad_width, mode, stat_length): + a_np = numpy.arange(100) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, stat_length=stat_length + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, stat_length=stat_length) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((2, 3), (3, 2))]) + @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) + @pytest.mark.parametrize("stat_length", [(3,), (2, 3)]) + def test_stat_func_2d(self, pad_width, mode, stat_length): + a_np = numpy.arange(30).reshape(6, 5) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, stat_length=stat_length + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, stat_length=stat_length) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) + def test_same_prepend_append(self, mode): + """Test that appended and prepended values are equal""" + a = dpnp.array([-1, 2, -1]) + dpnp.array( + [0, 1e-12, 0], dtype=dpnp.float32 + ) + result = dpnp.pad(a, (1, 1), mode) + assert_equal(result[0], result[-1]) + + def test_mean_with_zero_stat_length(self): + a_np = numpy.array([1.0, 2.0]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, (1, 2), "mean") + result = dpnp.pad(a_dp, (1, 2), "mean") + assert_array_equal(result, expected) + + @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) + @pytest.mark.parametrize( + "stat_length", [-2, (-2,), (3, -1), ((5, 2), (-2, 3)), ((-4,), (2,))] + ) + def test_negative_stat_length(self, mode, stat_length): + a_dp = dpnp.arange(30).reshape((6, 5)) + match = "index can't contain negative values" + with pytest.raises(ValueError, match=match): + dpnp.pad(a_dp, 2, mode, stat_length=stat_length) + + @pytest.mark.parametrize("mode", ["minimum", "maximum"]) + def test_zero_stat_length_invalid(self, mode): + a = dpnp.array([1.0, 2.0]) + match = "stat_length of 0 yields no value for padding" + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 0, mode, stat_length=0) + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 0, mode, stat_length=(1, 0)) + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 1, mode, stat_length=0) + with pytest.raises(ValueError, match=match): + dpnp.pad(a, 1, mode, stat_length=(1, 0)) + + @testing.with_requires("numpy>=2.0") # numpy<2 has a bug, numpy-gh-25963 + @pytest.mark.parametrize("pad_width", [2, 3, 4, [1, 10], [15, 2], [45, 10]]) + @pytest.mark.parametrize("mode", ["reflect", "symmetric"]) + @pytest.mark.parametrize("reflect_type", ["even", "odd"]) + def test_reflect_symmetric_1d(self, pad_width, mode, reflect_type): + a_np = numpy.array([1, 2, 3, 4]) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, reflect_type=reflect_type + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, reflect_type=reflect_type) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("data", [[[4, 5, 6], [6, 7, 8]], [[4, 5, 6]]]) + @pytest.mark.parametrize("pad_width", [10, (5, 7)]) + @pytest.mark.parametrize("mode", ["reflect", "symmetric"]) + @pytest.mark.parametrize("reflect_type", ["even", "odd"]) + def test_reflect_symmetric_2d(self, data, pad_width, mode, reflect_type): + a_np = numpy.array(data) + a_dp = dpnp.array(a_np) + expected = numpy.pad( + a_np, pad_width, mode=mode, reflect_type=reflect_type + ) + result = dpnp.pad(a_dp, pad_width, mode=mode, reflect_type=reflect_type) + assert_array_equal(result, expected) + + @pytest.mark.parametrize("pad_width", [2, 3, 4, [1, 10], [15, 2], [45, 10]]) + def test_wrap_1d(self, pad_width): + a_np = numpy.array([1, 2, 3, 4]) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, pad_width, "wrap") + result = dpnp.pad(a_dp, pad_width, "wrap") + assert_array_equal(result, expected) + + @pytest.mark.parametrize("data", [[[4, 5, 6], [6, 7, 8]], [[4, 5, 6]]]) + @pytest.mark.parametrize("pad_width", [10, (5, 7), (1, 3), (3, 1)]) + def test_wrap_2d(self, data, pad_width): + a_np = numpy.array(data) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, pad_width, "wrap") + result = dpnp.pad(a_dp, pad_width, "wrap") + assert_array_equal(result, expected) + + def test_empty(self): + a_np = numpy.arange(24).reshape(4, 6) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, [(2, 3), (3, 1)], "empty") + result = dpnp.pad(a_dp, [(2, 3), (3, 1)], "empty") + # omit uninitialized "empty" boundary from the comparison + assert result.shape == expected.shape + assert_equal(result[2:-3, 3:-1], expected[2:-3, 3:-1]) + + # Check how padding behaves on arrays with an empty dimension. + # empty axis can only be padded using modes 'constant' or 'empty' + @pytest.mark.parametrize("mode", ["constant", "empty"]) + def test_pad_empty_dim_valid(self, mode): + """empty axis can only be padded using modes 'constant' or 'empty'""" + a_np = numpy.zeros((3, 0, 2)) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, [(0,), (2,), (1,)], mode) + result = dpnp.pad(a_dp, [(0,), (2,), (1,)], mode) + assert_dtype_allclose(result, expected) + + @pytest.mark.parametrize( + "mode", + _all_modes.keys() - {"constant", "empty"}, + ) + def test_pad_empty_dim_invalid(self, mode): + match = ( + "can't extend empty axis 0 using modes other than 'constant' " + "or 'empty'" + ) + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.array([]), 4, mode=mode) + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.ndarray(0), 4, mode=mode) + with pytest.raises(ValueError, match=match): + dpnp.pad(dpnp.zeros((0, 3)), ((1,), (0,)), mode=mode) + + def test_vector_functionality(self): + def _padwithtens(vector, pad_width, iaxis, kwargs): + vector[: pad_width[0]] = 10 + vector[-pad_width[1] :] = 10 + + a_np = numpy.arange(6).reshape(2, 3) + a_dp = dpnp.array(a_np) + expected = numpy.pad(a_np, 2, _padwithtens) + result = dpnp.pad(a_dp, 2, _padwithtens) + assert_array_equal(result, expected) + + # test _as_pairs an internal function used by dpnp.pad + def test_as_pairs_single_value(self): + """Test casting for a single value.""" + for x in (3, [3], [[3]]): + result = dpnp_as_pairs(x, 10) + expected = numpy_as_pairs(x, 10) + assert_equal(result, expected) + + def test_as_pairs_two_values(self): + """Test proper casting for two different values.""" + # Broadcasting in the first dimension with numbers + for x in ([3, 4], [[3, 4]]): + result = dpnp_as_pairs(x, 10) + expected = numpy_as_pairs(x, 10) + assert_equal(result, expected) + + # Broadcasting in the second / last dimension with numbers + assert_equal( + dpnp_as_pairs([[3], [4]], 2), + numpy_as_pairs([[3], [4]], 2), + ) + + def test_as_pairs_with_none(self): + assert_equal( + dpnp_as_pairs(None, 3, as_index=False), + numpy_as_pairs(None, 3, as_index=False), + ) + assert_equal( + dpnp_as_pairs(None, 3, as_index=True), + numpy_as_pairs(None, 3, as_index=True), + ) + + def test_as_pairs_pass_through(self): + """Test if `x` already matching desired output are passed through.""" + a_np = numpy.arange(12).reshape((6, 2)) + a_dp = dpnp.arange(12).reshape((6, 2)) + assert_equal( + dpnp_as_pairs(a_dp, 6), + numpy_as_pairs(a_np, 6), + ) + + def test_as_pairs_as_index(self): + """Test results if `as_index=True`.""" + assert_equal( + dpnp_as_pairs([2.6, 3.3], 10, as_index=True), + numpy_as_pairs([2.6, 3.3], 10, as_index=True), + ) + assert_equal( + dpnp_as_pairs([2.6, 4.49], 10, as_index=True), + numpy_as_pairs([2.6, 4.49], 10, as_index=True), + ) + for x in ( + -3, + [-3], + [[-3]], + [-3, 4], + [3, -4], + [[-3, 4]], + [[4, -3]], + [[1, 2]] * 9 + [[1, -2]], + ): + with pytest.raises(ValueError, match="negative values"): + dpnp_as_pairs(x, 10, as_index=True) + + def test_as_pairs_exceptions(self): + """Ensure faulty usage is discovered.""" + with pytest.raises(ValueError, match="more dimensions than allowed"): + dpnp_as_pairs([[[3]]], 10) + with pytest.raises(ValueError, match="could not be broadcast"): + dpnp_as_pairs([[1, 2], [3, 4]], 3) + with pytest.raises(ValueError, match="could not be broadcast"): + dpnp_as_pairs(dpnp.ones((2, 3)), 3) diff --git a/tests/test_manipulation.py b/tests/test_manipulation.py index a32e2b844b05..a19742532c66 100644 --- a/tests/test_manipulation.py +++ b/tests/test_manipulation.py @@ -455,520 +455,6 @@ def test_no_copy(self): assert_array_equal(b, a) -class TestPad: - _all_modes = { - "constant": {"constant_values": 0}, - "edge": {}, - "linear_ramp": {"end_values": 0}, - "maximum": {"stat_length": None}, - "mean": {"stat_length": None}, - "minimum": {"stat_length": None}, - "reflect": {"reflect_type": "even"}, - "symmetric": {"reflect_type": "even"}, - "wrap": {}, - "empty": {}, - } - - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_basic(self, mode): - a_np = numpy.arange(100) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, (25, 20), mode=mode) - result = dpnp.pad(a_dp, (25, 20), mode=mode) - if mode == "empty": - # omit uninitialized "empty" boundary from the comparison - assert result.shape == expected.shape - assert_equal(result[25:-20], expected[25:-20]) - else: - assert_array_equal(result, expected) - - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_memory_layout_persistence(self, mode): - """Test if C and F order is preserved for all pad modes.""" - x = dpnp.ones((5, 10), order="C") - assert dpnp.pad(x, 5, mode).flags.c_contiguous - x = dpnp.ones((5, 10), order="F") - assert dpnp.pad(x, 5, mode).flags.f_contiguous - - @pytest.mark.parametrize("dtype", get_all_dtypes(no_none=True)) - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_dtype_persistence(self, dtype, mode): - arr = dpnp.zeros((3, 2, 1), dtype=dtype) - result = dpnp.pad(arr, 1, mode=mode) - assert result.dtype == dtype - - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_non_contiguous_array(self, mode): - a_np = numpy.arange(24).reshape(4, 6)[::2, ::2] - a_dp = dpnp.arange(24).reshape(4, 6)[::2, ::2] - expected = numpy.pad(a_np, (2, 3), mode=mode) - result = dpnp.pad(a_dp, (2, 3), mode=mode) - if mode == "empty": - # omit uninitialized "empty" boundary from the comparison - assert result.shape == expected.shape - assert_equal(result[2:-3, 2:-3], expected[2:-3, 2:-3]) - else: - assert_array_equal(result, expected) - - # TODO: include "linear_ramp" when dpnp issue gh-2084 is resolved - @pytest.mark.parametrize("pad_width", [0, (0, 0), ((0, 0), (0, 0))]) - @pytest.mark.parametrize("mode", _all_modes.keys() - {"linear_ramp"}) - def test_zero_pad_width(self, pad_width, mode): - arr = dpnp.arange(30).reshape(6, 5) - assert_array_equal(arr, dpnp.pad(arr, pad_width, mode=mode)) - - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_pad_non_empty_dimension(self, mode): - a_np = numpy.ones((2, 0, 2)) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, ((3,), (0,), (1,)), mode=mode) - result = dpnp.pad(a_dp, ((3,), (0,), (1,)), mode=mode) - assert_array_equal(result, expected) - - @pytest.mark.parametrize( - "pad_width", - [ - (4, 5, 6, 7), - ((1,), (2,), (3,)), - ((1, 2), (3, 4), (5, 6)), - ((3, 4, 5), (0, 1, 2)), - ], - ) - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_misshaped_pad_width1(self, pad_width, mode): - arr = dpnp.arange(30).reshape((6, 5)) - match = "operands could not be broadcast together" - with pytest.raises(ValueError, match=match): - dpnp.pad(arr, pad_width, mode) - - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_misshaped_pad_width2(self, mode): - arr = dpnp.arange(30).reshape((6, 5)) - match = ( - "input operand has more dimensions than allowed by the axis " - "remapping" - ) - with pytest.raises(ValueError, match=match): - dpnp.pad(arr, (((3,), (4,), (5,)), ((0,), (1,), (2,))), mode) - - @pytest.mark.parametrize( - "pad_width", [-2, (-2,), (3, -1), ((5, 2), (-2, 3)), ((-4,), (2,))] - ) - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_negative_pad_width(self, pad_width, mode): - arr = dpnp.arange(30).reshape((6, 5)) - match = "index can't contain negative values" - with pytest.raises(ValueError, match=match): - dpnp.pad(arr, pad_width, mode) - - @pytest.mark.parametrize( - "pad_width", - ["3", "word", None, 3.4, complex(1, -1), ((-2.1, 3), (3, 2))], - ) - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_bad_type(self, pad_width, mode): - arr = dpnp.arange(30).reshape((6, 5)) - match = "`pad_width` must be of integral type." - with pytest.raises(TypeError, match=match): - dpnp.pad(arr, pad_width, mode) - - @pytest.mark.parametrize("mode", _all_modes.keys()) - def test_kwargs(self, mode): - """Test behavior of pad's kwargs for the given mode.""" - allowed = self._all_modes[mode] - not_allowed = {} - for kwargs in self._all_modes.values(): - if kwargs != allowed: - not_allowed.update(kwargs) - # Test if allowed keyword arguments pass - dpnp.pad(dpnp.array([1, 2, 3]), 1, mode, **allowed) - # Test if prohibited keyword arguments of other modes raise an error - for key, value in not_allowed.items(): - match = f"unsupported keyword arguments for mode '{mode}'" - with pytest.raises(ValueError, match=match): - dpnp.pad(dpnp.array([1, 2, 3]), 1, mode, **{key: value}) - - @pytest.mark.parametrize("mode", [1, "const", object(), None, True, False]) - def test_unsupported_mode(self, mode): - match = f"mode '{mode}' is not supported" - with pytest.raises(ValueError, match=match): - dpnp.pad(dpnp.array([1, 2, 3]), 4, mode=mode) - - def test_pad_default(self): - a_np = numpy.array([1, 1]) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, 2) - result = dpnp.pad(a_dp, 2) - assert_array_equal(result, expected) - - @pytest.mark.parametrize( - "pad_width", - [numpy.array(((2, 3), (3, 2))), dpnp.array(((2, 3), (3, 2)))], - ) - def test_pad_width_as_ndarray(self, pad_width): - a_np = numpy.arange(12).reshape(4, 3) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, ((2, 3), (3, 2))) - result = dpnp.pad(a_dp, pad_width) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("pad_width", [5, (25, 20)]) - @pytest.mark.parametrize("constant_values", [3, (10, 20)]) - def test_constant_1d(self, pad_width, constant_values): - a_np = numpy.arange(100) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, "constant", constant_values=constant_values - ) - result = dpnp.pad( - a_dp, pad_width, "constant", constant_values=constant_values - ) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((1, 2), (1, 3))]) - @pytest.mark.parametrize("constant_values", [3, ((1, 2), (3, 4))]) - def test_constant_2d(self, pad_width, constant_values): - a_np = numpy.arange(30).reshape(5, 6) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, "constant", constant_values=constant_values - ) - result = dpnp.pad( - a_dp, pad_width, "constant", constant_values=constant_values - ) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("dtype", [numpy.int32, numpy.float32]) - def test_constant_float(self, dtype): - # If input array is int, but constant_values are float, the dtype of - # the array to be padded is kept - # If input array is float, and constant_values are float, the dtype of - # the array to be padded is kept - here retaining the float constants - a_np = numpy.arange(30, dtype=dtype).reshape(5, 6) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, (25, 20), "constant", constant_values=1.1) - result = dpnp.pad(a_dp, (25, 20), "constant", constant_values=1.1) - assert_array_equal(result, expected) - - def test_constant_large_integers(self): - uint64_max = 2**64 - 1 - a_np = numpy.full(5, uint64_max, dtype=numpy.uint64) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, 1, "constant", constant_values=a_np.min()) - result = dpnp.pad(a_dp, 1, "constant", constant_values=a_dp.min()) - assert_array_equal(result, expected) - - int64_max = 2**63 - 1 - a_np = numpy.full(5, int64_max, dtype=numpy.int64) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, 1, "constant", constant_values=a_np.min()) - result = dpnp.pad(a_dp, 1, "constant", constant_values=a_dp.min()) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((2, 3), (3, 2))]) - def test_edge(self, pad_width): - a_np = numpy.arange(12).reshape(4, 3) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, pad_width, "edge") - result = dpnp.pad(a_dp, pad_width, "edge") - assert_array_equal(result, expected) - - def test_edge_nd(self): - a_np = numpy.array([1, 2, 3]) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, ((1, 2),), "edge") - result = dpnp.pad(a_dp, ((1, 2),), "edge") - assert_array_equal(result, expected) - - a_np = numpy.array([[1, 2, 3], [4, 5, 6]]) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, ((1, 2),), "edge") - result = dpnp.pad(a_dp, ((1, 2), (1, 2)), "edge") - assert_array_equal(result, expected) - - a_np = numpy.arange(24).reshape(2, 3, 4) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, ((1, 2),), "edge") - result = dpnp.pad(a_dp, ((1, 2), (1, 2), (1, 2)), "edge") - assert_array_equal(result, expected) - - @pytest.mark.parametrize("pad_width", [5, (25, 20)]) - @pytest.mark.parametrize("end_values", [3, (10, 20)]) - def test_linear_ramp_1d(self, pad_width, end_values): - a_np = numpy.arange(100, dtype=numpy.float32) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, "linear_ramp", end_values=end_values - ) - result = dpnp.pad(a_dp, pad_width, "linear_ramp", end_values=end_values) - assert_dtype_allclose(result, expected) - - @pytest.mark.parametrize( - "pad_width", [(2, 2), ((1,), (2,)), ((1, 2), (1, 3))] - ) - @pytest.mark.parametrize("end_values", [3, (0, 0), ((1, 2), (3, 4))]) - def test_linear_ramp_2d(self, pad_width, end_values): - a_np = numpy.arange(20, dtype=numpy.float32).reshape(4, 5) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, "linear_ramp", end_values=end_values - ) - result = dpnp.pad(a_dp, pad_width, "linear_ramp", end_values=end_values) - assert_dtype_allclose(result, expected) - - def test_linear_ramp_end_values(self): - """Ensure that end values are exact.""" - a_dp = dpnp.ones(10).reshape(2, 5) - a = dpnp.pad(a_dp, (223, 123), mode="linear_ramp") - assert_equal(a[:, 0], 0.0) - assert_equal(a[:, -1], 0.0) - assert_equal(a[0, :], 0.0) - assert_equal(a[-1, :], 0.0) - - @pytest.mark.parametrize( - "dtype", [numpy.uint32, numpy.uint64] + get_all_dtypes(no_none=True) - ) - @pytest.mark.parametrize("data, end_values", [([3], 0), ([0], 3)]) - def test_linear_ramp_negative_diff(self, dtype, data, end_values): - """ - Check correct behavior of unsigned dtypes if there is a negative - difference between the edge to pad and `end_values`. Check both cases - to be independent of implementation. Test behavior for all other dtypes - in case dtype casting interferes with complex dtypes. - """ - a_np = numpy.array(data, dtype=dtype) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, 3, mode="linear_ramp", end_values=end_values) - result = dpnp.pad(a_dp, 3, mode="linear_ramp", end_values=end_values) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("pad_width", [5, (25, 20)]) - @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) - @pytest.mark.parametrize("stat_length", [10, (2, 3)]) - def test_stat_func_1d(self, pad_width, mode, stat_length): - a_np = numpy.arange(100) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, mode=mode, stat_length=stat_length - ) - result = dpnp.pad(a_dp, pad_width, mode=mode, stat_length=stat_length) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((2, 3), (3, 2))]) - @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) - @pytest.mark.parametrize("stat_length", [(3,), (2, 3)]) - def test_stat_func_2d(self, pad_width, mode, stat_length): - a_np = numpy.arange(30).reshape(6, 5) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, mode=mode, stat_length=stat_length - ) - result = dpnp.pad(a_dp, pad_width, mode=mode, stat_length=stat_length) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) - def test_same_prepend_append(self, mode): - """Test that appended and prepended values are equal""" - a = dpnp.array([-1, 2, -1]) + dpnp.array( - [0, 1e-12, 0], dtype=dpnp.float32 - ) - result = dpnp.pad(a, (1, 1), mode) - assert_equal(result[0], result[-1]) - - def test_mean_with_zero_stat_length(self): - a_np = numpy.array([1.0, 2.0]) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, (1, 2), "mean") - result = dpnp.pad(a_dp, (1, 2), "mean") - assert_array_equal(result, expected) - - @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) - @pytest.mark.parametrize( - "stat_length", [-2, (-2,), (3, -1), ((5, 2), (-2, 3)), ((-4,), (2,))] - ) - def test_negative_stat_length(self, mode, stat_length): - a_dp = dpnp.arange(30).reshape((6, 5)) - match = "index can't contain negative values" - with pytest.raises(ValueError, match=match): - dpnp.pad(a_dp, 2, mode, stat_length=stat_length) - - @pytest.mark.parametrize("mode", ["minimum", "maximum"]) - def test_zero_stat_length_invalid(self, mode): - a = dpnp.array([1.0, 2.0]) - match = "stat_length of 0 yields no value for padding" - with pytest.raises(ValueError, match=match): - dpnp.pad(a, 0, mode, stat_length=0) - with pytest.raises(ValueError, match=match): - dpnp.pad(a, 0, mode, stat_length=(1, 0)) - with pytest.raises(ValueError, match=match): - dpnp.pad(a, 1, mode, stat_length=0) - with pytest.raises(ValueError, match=match): - dpnp.pad(a, 1, mode, stat_length=(1, 0)) - - @testing.with_requires("numpy>=2.0") # numpy<2 has a bug, numpy-gh-25963 - @pytest.mark.parametrize("pad_width", [2, 3, 4, [1, 10], [15, 2], [45, 10]]) - @pytest.mark.parametrize("mode", ["reflect", "symmetric"]) - @pytest.mark.parametrize("reflect_type", ["even", "odd"]) - def test_reflect_symmetric_1d(self, pad_width, mode, reflect_type): - a_np = numpy.array([1, 2, 3, 4]) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, mode=mode, reflect_type=reflect_type - ) - result = dpnp.pad(a_dp, pad_width, mode=mode, reflect_type=reflect_type) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("data", [[[4, 5, 6], [6, 7, 8]], [[4, 5, 6]]]) - @pytest.mark.parametrize("pad_width", [10, (5, 7)]) - @pytest.mark.parametrize("mode", ["reflect", "symmetric"]) - @pytest.mark.parametrize("reflect_type", ["even", "odd"]) - def test_reflect_symmetric_2d(self, data, pad_width, mode, reflect_type): - a_np = numpy.array(data) - a_dp = dpnp.array(a_np) - expected = numpy.pad( - a_np, pad_width, mode=mode, reflect_type=reflect_type - ) - result = dpnp.pad(a_dp, pad_width, mode=mode, reflect_type=reflect_type) - assert_array_equal(result, expected) - - @pytest.mark.parametrize("pad_width", [2, 3, 4, [1, 10], [15, 2], [45, 10]]) - def test_wrap_1d(self, pad_width): - a_np = numpy.array([1, 2, 3, 4]) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, pad_width, "wrap") - result = dpnp.pad(a_dp, pad_width, "wrap") - assert_array_equal(result, expected) - - @pytest.mark.parametrize("data", [[[4, 5, 6], [6, 7, 8]], [[4, 5, 6]]]) - @pytest.mark.parametrize("pad_width", [10, (5, 7), (1, 3), (3, 1)]) - def test_wrap_2d(self, data, pad_width): - a_np = numpy.array(data) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, pad_width, "wrap") - result = dpnp.pad(a_dp, pad_width, "wrap") - assert_array_equal(result, expected) - - def test_empty(self): - a_np = numpy.arange(24).reshape(4, 6) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, [(2, 3), (3, 1)], "empty") - result = dpnp.pad(a_dp, [(2, 3), (3, 1)], "empty") - # omit uninitialized "empty" boundary from the comparison - assert result.shape == expected.shape - assert_equal(result[2:-3, 3:-1], expected[2:-3, 3:-1]) - - # Check how padding behaves on arrays with an empty dimension. - # empty axis can only be padded using modes 'constant' or 'empty' - @pytest.mark.parametrize("mode", ["constant", "empty"]) - def test_pad_empty_dim_valid(self, mode): - """empty axis can only be padded using modes 'constant' or 'empty'""" - a_np = numpy.zeros((3, 0, 2)) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, [(0,), (2,), (1,)], mode) - result = dpnp.pad(a_dp, [(0,), (2,), (1,)], mode) - assert_dtype_allclose(result, expected) - - @pytest.mark.parametrize( - "mode", - _all_modes.keys() - {"constant", "empty"}, - ) - def test_pad_empty_dim_invalid(self, mode): - match = ( - "can't extend empty axis 0 using modes other than 'constant' " - "or 'empty'" - ) - with pytest.raises(ValueError, match=match): - dpnp.pad(dpnp.array([]), 4, mode=mode) - with pytest.raises(ValueError, match=match): - dpnp.pad(dpnp.ndarray(0), 4, mode=mode) - with pytest.raises(ValueError, match=match): - dpnp.pad(dpnp.zeros((0, 3)), ((1,), (0,)), mode=mode) - - def test_vector_functionality(self): - def _padwithtens(vector, pad_width, iaxis, kwargs): - vector[: pad_width[0]] = 10 - vector[-pad_width[1] :] = 10 - - a_np = numpy.arange(6).reshape(2, 3) - a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, 2, _padwithtens) - result = dpnp.pad(a_dp, 2, _padwithtens) - assert_array_equal(result, expected) - - # test _as_pairs an internal function used by dpnp.pad - def test_as_pairs_single_value(self): - """Test casting for a single value.""" - for x in (3, [3], [[3]]): - result = dpnp_as_pairs(x, 10) - expected = numpy_as_pairs(x, 10) - assert_equal(result, expected) - - def test_as_pairs_two_values(self): - """Test proper casting for two different values.""" - # Broadcasting in the first dimension with numbers - for x in ([3, 4], [[3, 4]]): - result = dpnp_as_pairs(x, 10) - expected = numpy_as_pairs(x, 10) - assert_equal(result, expected) - - # Broadcasting in the second / last dimension with numbers - assert_equal( - dpnp_as_pairs([[3], [4]], 2), - numpy_as_pairs([[3], [4]], 2), - ) - - def test_as_pairs_with_none(self): - assert_equal( - dpnp_as_pairs(None, 3, as_index=False), - numpy_as_pairs(None, 3, as_index=False), - ) - assert_equal( - dpnp_as_pairs(None, 3, as_index=True), - numpy_as_pairs(None, 3, as_index=True), - ) - - def test_as_pairs_pass_through(self): - """Test if `x` already matching desired output are passed through.""" - a_np = numpy.arange(12).reshape((6, 2)) - a_dp = dpnp.arange(12).reshape((6, 2)) - assert_equal( - dpnp_as_pairs(a_dp, 6), - numpy_as_pairs(a_np, 6), - ) - - def test_as_pairs_as_index(self): - """Test results if `as_index=True`.""" - assert_equal( - dpnp_as_pairs([2.6, 3.3], 10, as_index=True), - numpy_as_pairs([2.6, 3.3], 10, as_index=True), - ) - assert_equal( - dpnp_as_pairs([2.6, 4.49], 10, as_index=True), - numpy_as_pairs([2.6, 4.49], 10, as_index=True), - ) - for x in ( - -3, - [-3], - [[-3]], - [-3, 4], - [3, -4], - [[-3, 4]], - [[4, -3]], - [[1, 2]] * 9 + [[1, -2]], - ): - with pytest.raises(ValueError, match="negative values"): - dpnp_as_pairs(x, 10, as_index=True) - - def test_as_pairs_exceptions(self): - """Ensure faulty usage is discovered.""" - with pytest.raises(ValueError, match="more dimensions than allowed"): - dpnp_as_pairs([[[3]]], 10) - with pytest.raises(ValueError, match="could not be broadcast"): - dpnp_as_pairs([[1, 2], [3, 4]], 3) - with pytest.raises(ValueError, match="could not be broadcast"): - dpnp_as_pairs(dpnp.ones((2, 3)), 3) - - class TestRepeat: @pytest.mark.parametrize( "data", From 47ce700efbe7a9fe95a42c492678f3927f2b1f22 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Mon, 14 Oct 2024 16:38:58 -0500 Subject: [PATCH 06/12] add test_arraypad.py to TEST_SCOPE --- .github/workflows/conda-package.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/conda-package.yml b/.github/workflows/conda-package.yml index 81e4881bdbd6..d580b0a3eccc 100644 --- a/.github/workflows/conda-package.yml +++ b/.github/workflows/conda-package.yml @@ -25,6 +25,7 @@ env: test_arithmetic.py test_arraycreation.py test_arraymanipulation.py + test_arraypad.py test_bitwise.py test_copy.py test_counting.py From c3b50a2ad55a5d031d80fab0ebb13749a0e98aab Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 15 Oct 2024 02:32:00 -0500 Subject: [PATCH 07/12] add an assert --- dpnp/dpnp_utils/dpnp_utils_pad.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dpnp/dpnp_utils/dpnp_utils_pad.py b/dpnp/dpnp_utils/dpnp_utils_pad.py index 838af5cc3a70..9279d13ab883 100644 --- a/dpnp/dpnp_utils/dpnp_utils_pad.py +++ b/dpnp/dpnp_utils/dpnp_utils_pad.py @@ -771,6 +771,7 @@ def dpnp_pad(array, pad_width, mode="constant", **kwargs): ) else: # mode == "wrap": + assert mode == "wrap" for axis, (left_index, right_index) in zip(axes, pad_width): roi = _view_roi(padded, original_area_slice, axis) original_period = padded.shape[axis] - right_index - left_index From 4f5761dbfab9f0dd49ccb955df49d02530ab923f Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 15 Oct 2024 02:41:18 -0500 Subject: [PATCH 08/12] add support for mode median --- dpnp/dpnp_iface_manipulation.py | 22 ++++++++-------------- dpnp/dpnp_utils/dpnp_utils_pad.py | 4 ++-- tests/test_arraypad.py | 16 +++++++++------- 3 files changed, 19 insertions(+), 23 deletions(-) diff --git a/dpnp/dpnp_iface_manipulation.py b/dpnp/dpnp_iface_manipulation.py index 8339c00da022..f233a98ed3a9 100644 --- a/dpnp/dpnp_iface_manipulation.py +++ b/dpnp/dpnp_iface_manipulation.py @@ -1874,6 +1874,9 @@ def pad(array, pad_width, mode="constant", **kwargs): "mean" Pads with the mean value of all or part of the vector along each axis. + "median" + Pads with the median value of all or part of the + vector along each axis. "minimum" Pads with the minimum value of all or part of the vector along each axis. @@ -1894,10 +1897,10 @@ def pad(array, pad_width, mode="constant", **kwargs): Padding function, see Notes. Default: ``"constant"``. stat_length : {None, int, sequence of ints}, optional - Used in ``"maximum"``, ``"mean"``, and ``"minimum"``. Number of - values at edge of each axis used to calculate the statistic value. - ``((before_1, after_1), ... (before_N, after_N))`` unique statistic - lengths for each axis. + Used in ``"maximum"``, ``"mean"``, ``"median"``, and ``"minimum"``. + Number of values at edge of each axis used to calculate the statistic + value. ``((before_1, after_1), ... (before_N, after_N))`` unique + statistic lengths for each axis. ``(before, after)`` or ``((before, after),)`` yields same before and after statistic lengths for each axis. ``(stat_length,)`` or ``int`` is a shortcut for @@ -1936,11 +1939,6 @@ def pad(array, pad_width, mode="constant", **kwargs): Padded array of rank equal to `array` with shape increased according to `pad_width`. - Limitations - ----------- - Parameter `mode` as ``"median"`` is not currently supported and - ``NotImplementedError`` exception will be raised. - Notes ----- For an array with rank greater than 1, some of the padding of later @@ -1988,7 +1986,7 @@ def pad(array, pad_width, mode="constant", **kwargs): array([3, 3, 1, 2, 3, 4, 5, 3, 3]) >>> np.pad(a, (2,), 'median') - NotImplementedError: Keyword argument `mode` does not support 'median' + array([3, 3, 1, 2, 3, 4, 5, 3, 3]) >>> a = np.array([[1, 2], [3, 4]]) >>> np.pad(a, ((3, 2), (2, 3)), 'minimum') @@ -2040,10 +2038,6 @@ def pad(array, pad_width, mode="constant", **kwargs): """ dpnp.check_supported_arrays_type(array) - if mode == "median": - raise NotImplementedError( - "Keyword argument `mode` does not support 'median'" - ) return dpnp_pad(array, pad_width, mode=mode, **kwargs) diff --git a/dpnp/dpnp_utils/dpnp_utils_pad.py b/dpnp/dpnp_utils/dpnp_utils_pad.py index 9279d13ab883..e0316ea4a678 100644 --- a/dpnp/dpnp_utils/dpnp_utils_pad.py +++ b/dpnp/dpnp_utils/dpnp_utils_pad.py @@ -659,7 +659,7 @@ def dpnp_pad(array, pad_width, mode="constant", **kwargs): "linear_ramp": ["end_values"], "maximum": ["stat_length"], "mean": ["stat_length"], - # "median": ["stat_length"], # TODO: dpnp.median is not implemented + "median": ["stat_length"], "minimum": ["stat_length"], "reflect": ["reflect_type"], "symmetric": ["reflect_type"], @@ -684,11 +684,11 @@ def dpnp_pad(array, pad_width, mode="constant", **kwargs): # faster path for 1d arrays or small n-dimensional arrays return _pad_simple(array, pad_width, 0)[0] - # TODO: add "median": dpnp.median when dpnp.median is implemented stat_functions = { "maximum": dpnp.amax, "minimum": dpnp.amin, "mean": dpnp.mean, + "median": dpnp.median, } # Create array with final shape and original values diff --git a/tests/test_arraypad.py b/tests/test_arraypad.py index 8060864c2bbe..e737fad490f6 100644 --- a/tests/test_arraypad.py +++ b/tests/test_arraypad.py @@ -22,6 +22,7 @@ class TestPad: "linear_ramp": {"end_values": 0}, "maximum": {"stat_length": None}, "mean": {"stat_length": None}, + "median": {"stat_length": None}, "minimum": {"stat_length": None}, "reflect": {"reflect_type": "even"}, "symmetric": {"reflect_type": "even"}, @@ -303,7 +304,7 @@ def test_linear_ramp_negative_diff(self, dtype, data, end_values): assert_array_equal(result, expected) @pytest.mark.parametrize("pad_width", [5, (25, 20)]) - @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) + @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean", "median"]) @pytest.mark.parametrize("stat_length", [10, (2, 3)]) def test_stat_func_1d(self, pad_width, mode, stat_length): a_np = numpy.arange(100) @@ -315,7 +316,7 @@ def test_stat_func_1d(self, pad_width, mode, stat_length): assert_array_equal(result, expected) @pytest.mark.parametrize("pad_width", [((1,), (2,)), ((2, 3), (3, 2))]) - @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean"]) + @pytest.mark.parametrize("mode", ["maximum", "minimum", "mean", "median"]) @pytest.mark.parametrize("stat_length", [(3,), (2, 3)]) def test_stat_func_2d(self, pad_width, mode, stat_length): a_np = numpy.arange(30).reshape(6, 5) @@ -326,7 +327,7 @@ def test_stat_func_2d(self, pad_width, mode, stat_length): result = dpnp.pad(a_dp, pad_width, mode=mode, stat_length=stat_length) assert_array_equal(result, expected) - @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) + @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum", "median"]) def test_same_prepend_append(self, mode): """Test that appended and prepended values are equal""" a = dpnp.array([-1, 2, -1]) + dpnp.array( @@ -335,14 +336,15 @@ def test_same_prepend_append(self, mode): result = dpnp.pad(a, (1, 1), mode) assert_equal(result[0], result[-1]) - def test_mean_with_zero_stat_length(self): + @pytest.mark.parametrize("mode", ["mean", "median"]) + def test_zero_stat_length_valid(self): a_np = numpy.array([1.0, 2.0]) a_dp = dpnp.array(a_np) - expected = numpy.pad(a_np, (1, 2), "mean") - result = dpnp.pad(a_dp, (1, 2), "mean") + expected = numpy.pad(a_np, (1, 2), mode, stat_length=0) + result = dpnp.pad(a_dp, (1, 2), mode, stat_length=0) assert_array_equal(result, expected) - @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum"]) + @pytest.mark.parametrize("mode", ["mean", "minimum", "maximum", "median"]) @pytest.mark.parametrize( "stat_length", [-2, (-2,), (3, -1), ((5, 2), (-2, 3)), ((-4,), (2,))] ) From 2e229433e6aabd2552dfe06f08e730b0fbac9fc5 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 22 Oct 2024 12:26:52 -0500 Subject: [PATCH 09/12] update sycl_queue and usm_type tests --- tests/test_manipulation.py | 7 ------- tests/test_sycl_queue.py | 1 + tests/test_usm_type.py | 1 + 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/tests/test_manipulation.py b/tests/test_manipulation.py index 2e3a12ace23e..d88548b33ac4 100644 --- a/tests/test_manipulation.py +++ b/tests/test_manipulation.py @@ -4,16 +4,9 @@ import numpy import pytest from dpctl.tensor._numpy_helper import AxisError - -if numpy.lib.NumpyVersion(numpy.__version__) >= "2.0.0": - from numpy.lib._arraypad_impl import _as_pairs as numpy_as_pairs -else: - from numpy.lib.arraypad import _as_pairs as numpy_as_pairs - from numpy.testing import assert_array_equal, assert_equal, assert_raises import dpnp -from dpnp.dpnp_utils.dpnp_utils_pad import _as_pairs as dpnp_as_pairs from tests.third_party.cupy import testing from .helper import ( diff --git a/tests/test_sycl_queue.py b/tests/test_sycl_queue.py index 6bae35d038fa..320de8df644c 100644 --- a/tests/test_sycl_queue.py +++ b/tests/test_sycl_queue.py @@ -1313,6 +1313,7 @@ def test_pad(device): "linear_ramp", "maximum", "mean", + "median", "minimum", "reflect", "symmetric", diff --git a/tests/test_usm_type.py b/tests/test_usm_type.py index f4dad59b97ee..c5d495f836bf 100644 --- a/tests/test_usm_type.py +++ b/tests/test_usm_type.py @@ -1031,6 +1031,7 @@ def test_pad(usm_type): "linear_ramp", "maximum", "mean", + "median", "minimum", "reflect", "symmetric", From 30bb7db3191f99df920c2369749dbbdf1a905523 Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Tue, 22 Oct 2024 13:06:13 -0500 Subject: [PATCH 10/12] suppress warning --- tests/test_arraypad.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/test_arraypad.py b/tests/test_arraypad.py index e737fad490f6..45c7aee47456 100644 --- a/tests/test_arraypad.py +++ b/tests/test_arraypad.py @@ -336,8 +336,12 @@ def test_same_prepend_append(self, mode): result = dpnp.pad(a, (1, 1), mode) assert_equal(result[0], result[-1]) + @pytest.mark.usefixtures( + "suppress_invalid_numpy_warnings", + "suppress_mean_empty_slice_numpy_warnings", + ) @pytest.mark.parametrize("mode", ["mean", "median"]) - def test_zero_stat_length_valid(self): + def test_zero_stat_length_valid(self, mode): a_np = numpy.array([1.0, 2.0]) a_dp = dpnp.array(a_np) expected = numpy.pad(a_np, (1, 2), mode, stat_length=0) From 233cf5741b1f6f10a3aed5dc3078b360c72dcd9b Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Wed, 23 Oct 2024 08:32:14 -0500 Subject: [PATCH 11/12] fast path improvement --- dpnp/dpnp_utils/dpnp_utils_pad.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_utils/dpnp_utils_pad.py b/dpnp/dpnp_utils/dpnp_utils_pad.py index e0316ea4a678..d84c7d796617 100644 --- a/dpnp/dpnp_utils/dpnp_utils_pad.py +++ b/dpnp/dpnp_utils/dpnp_utils_pad.py @@ -276,7 +276,7 @@ def _pad_simple(array, pad_width, fill_value=None): Parameters ---------- - array : ndarray + array : dpnp.ndarray Array to grow. pad_width : sequence of tuple[int, int] Pad width on both sides for each dimension in `arr`. @@ -286,8 +286,8 @@ def _pad_simple(array, pad_width, fill_value=None): Returns ------- - padded : ndarray - The padded array with the same dtype as`array`. Its order will default + padded : dpnp.ndarray + The padded array with the same dtype as `array`. Its order will default to C-style if `array` is not F-contiguous. original_area_slice : tuple A tuple of slices pointing to the area of the original array. @@ -299,7 +299,7 @@ def _pad_simple(array, pad_width, fill_value=None): left + size + right for size, (left, right) in zip(array.shape, pad_width) ) - order = "F" if array.flags.fnc else "C" # Fortran and not also C-order + order = "F" if array.flags.fnc else "C" padded = dpnp.empty( new_shape, dtype=array.dtype, @@ -598,7 +598,7 @@ def _view_roi(array, original_area_slice, axis): Returns ------- - roi : ndarray + roi : dpnp.ndarray The region of interest of the original `array`. """ @@ -679,7 +679,7 @@ def dpnp_pad(array, pad_width, mode="constant", **kwargs): if ( dpnp.isscalar(values) and values == 0 - and (array.ndim == 1 or array.size < 4e6) + and (array.ndim == 1 or array.size < 3e7) ): # faster path for 1d arrays or small n-dimensional arrays return _pad_simple(array, pad_width, 0)[0] From fbd280225bc4e3bf970723116d2a628393db7fff Mon Sep 17 00:00:00 2001 From: Vahid Tavanashad Date: Mon, 28 Oct 2024 16:01:00 -0500 Subject: [PATCH 12/12] minor updates --- dpnp/dpnp_utils/dpnp_utils_pad.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/dpnp/dpnp_utils/dpnp_utils_pad.py b/dpnp/dpnp_utils/dpnp_utils_pad.py index d84c7d796617..cff48a69410c 100644 --- a/dpnp/dpnp_utils/dpnp_utils_pad.py +++ b/dpnp/dpnp_utils/dpnp_utils_pad.py @@ -79,7 +79,7 @@ def _as_pairs(x, ndim, as_index=False): x = numpy.asarray(numpy.round(x), dtype=numpy.intp) if x.ndim < 3: - # Optimization: Possibly use faster paths for cases where `x` has + # Optimization: Using faster paths for cases where `x` has # only 1 or 2 elements. `numpy.broadcast_to` could handle these as well # but is currently slower @@ -116,7 +116,7 @@ def _get_edges(padded, axis, width_pair): Parameters ---------- - padded : ndarray + padded : dpnp.ndarray Empty-padded array. axis : int Dimension in which the edges are considered. @@ -126,7 +126,7 @@ def _get_edges(padded, axis, width_pair): Returns ------- - left_edge, right_edge : ndarray + left_edge, right_edge : dpnp.ndarray Edge values of the valid area in `padded` in the given dimension. Its shape will always match `padded` except for the dimension given by `axis` which will have a length of 1. @@ -151,7 +151,7 @@ def _get_linear_ramps(padded, axis, width_pair, end_value_pair): Parameters ---------- - padded : ndarray + padded : dpnp.ndarray Empty-padded array. axis : int Dimension in which the ramps are constructed. @@ -164,7 +164,7 @@ def _get_linear_ramps(padded, axis, width_pair, end_value_pair): Returns ------- - left_ramp, right_ramp : ndarray + left_ramp, right_ramp : dpnp.ndarray Linear ramps to set on both sides of `padded`. """ @@ -203,7 +203,7 @@ def _get_stats(padded, axis, width_pair, length_pair, stat_func): Parameters ---------- - padded : ndarray + padded : dpnp.ndarray Empty-padded array. axis : int Dimension in which the statistic is calculated.