diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d2ab51837bf..03f3bfb4eee7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,6 +15,7 @@ This release is compatible with NumPy 2.5. * Added support for buffer protocol objects as advanced index keys in `dpnp.ndarray` [#2889](https://github.com/IntelPython/dpnp/pull/2889) * Added `--includes` and `--include-dir` options to the `dpnp` CLI [#2916](https://github.com/IntelPython/dpnp/pull/2916) * Added `dpnp-config.cmake` to make `find_package(Dpnp)` work out of the box, and an example which uses it [#2941](https://github.com/IntelPython/dpnp/pull/2941) +* Added implementation of `dpnp.lib.stride_tricks.as_strided` [#2991](https://github.com/IntelPython/dpnp/pull/2991) ### Changed diff --git a/doc/reference/lib.rst b/doc/reference/lib.rst new file mode 100644 index 000000000000..fb645ff713b1 --- /dev/null +++ b/doc/reference/lib.rst @@ -0,0 +1,15 @@ +.. _routines.lib: +.. module:: dpnp.lib + +Lib module (:mod:`dpnp.lib`) +============================ + +.. hint:: `NumPy API Reference: Lib module (numpy.lib) `_ + +Submodules +---------- + +.. autosummary:: + :toctree: generated/ + + stride_tricks diff --git a/doc/reference/routines.rst b/doc/reference/routines.rst index e8013e134273..a27cdb75f151 100644 --- a/doc/reference/routines.rst +++ b/doc/reference/routines.rst @@ -20,6 +20,7 @@ These functions cover a subset of functional io indexing + lib linalg logic math diff --git a/dpnp/__init__.py b/dpnp/__init__.py index 38a210af472f..ec92d7ae9c31 100644 --- a/dpnp/__init__.py +++ b/dpnp/__init__.py @@ -67,6 +67,7 @@ from ._version import get_versions from . import exceptions as exceptions from . import fft as fft +from . import lib as lib from . import linalg as linalg from . import random as random from . import scipy as scipy diff --git a/dpnp/lib/__init__.py b/dpnp/lib/__init__.py new file mode 100644 index 000000000000..f5b5c8dd8ce8 --- /dev/null +++ b/dpnp/lib/__init__.py @@ -0,0 +1,40 @@ +# ***************************************************************************** +# Copyright (c) 2026, 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. +# - Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. +# ***************************************************************************** + +""" +``dpnp.lib`` +============ + +The library of stride-manipulation utilities of DPNP. + +This namespace mimics parts of ``numpy.lib`` on top of DPNP functionality. +""" + +from . import stride_tricks + +__all__ = ["stride_tricks"] diff --git a/dpnp/lib/stride_tricks.py b/dpnp/lib/stride_tricks.py new file mode 100644 index 000000000000..463e657efee1 --- /dev/null +++ b/dpnp/lib/stride_tricks.py @@ -0,0 +1,172 @@ +# ***************************************************************************** +# Copyright (c) 2026, 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. +# - Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# 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. +# ***************************************************************************** + +"""Utilities that manipulate strides to achieve desirable effects.""" + +import dpnp + +__all__ = ["as_strided"] + + +def as_strided( + x, + shape=None, + strides=None, + subok=False, + writeable=True, + *, + check_bounds=None, +): + """ + Create a view into the array with the given shape and strides. + + For full documentation refer to :obj:`numpy.lib.stride_tricks.as_strided`. + + Warnings + -------- + This function has to be used with extreme care, see notes. + + Parameters + ---------- + x : {dpnp.ndarray, usm_ndarray} + Array to create a new view from. + shape : {None, sequence of ints}, optional + The shape of the new array. + + Default: ``x.shape``. + strides : {None, sequence of ints}, optional + The strides of the new array, expressed in bytes. + + Default: ``x.strides``. + writeable : bool, optional + If set to ``False``, the returned array will always be read-only. + Otherwise it will be writable if the original array was. + + Default: ``True``. + check_bounds : {None, bool}, optional + Ignored as no effect, the underlying USM array cannot be constructed + over out-of-bounds memory. + + Default: ``None``. + + Returns + ------- + view : dpnp.ndarray + A view into the memory of `x` with the requested `shape` and `strides`, + sharing the same data. + + Limitations + ----------- + Parameter `subok` is supported with default value. + Otherwise ``NotImplementedError`` exception will be raised. + + See Also + -------- + :obj:`dpnp.broadcast_to` : Broadcast an array to a given shape. + :obj:`dpnp.reshape` : Give a new shape to an array without changing its + data. + + Notes + ----- + :obj:`dpnp.lib.stride_tricks.as_strided` creates a view into the array + given the exact strides and shape. This means it manipulates the internal + data structure of the array and, if done incorrectly, the array elements + can point to the wrong data and silently produce incorrect results. It is + advisable to always use the original ``x.strides`` when calculating new + strides to avoid reliance on a contiguous memory layout. + + Furthermore, arrays created with this function often contain self + overlapping memory, so that two elements are identical. Writing to a shared + element then changes every position that references it, so element-wise + write operations on such arrays are typically unpredictable. A bulk write + over an overlapping view is rejected, because it would address more memory + than the base allocation holds. + + Since writing to these arrays has to be tested and done with great care, + you may want to use ``writeable=False`` to avoid accidental write + operations. + + For these reasons it is advisable to avoid + :obj:`dpnp.lib.stride_tricks.as_strided` when possible. + + Examples + -------- + >>> import dpnp as np + >>> x = np.array([1, 2, 3, 4], dtype=np.int32) + + Downsample the array by taking every second element: + + >>> np.lib.stride_tricks.as_strided(x, shape=(2,), + ... strides=(2 * x.itemsize,)) + array([1, 3], dtype=int32) + + Broadcast the array along a new leading axis using a zero stride: + + >>> np.lib.stride_tricks.as_strided(x, shape=(3, 4), + ... strides=(0, x.itemsize)) + array([[1, 2, 3, 4], + [1, 2, 3, 4], + [1, 2, 3, 4]], dtype=int32) + + Build a self-overlapping sliding-window view, where a single element maps + onto several positions. Here a length-5 array yields a ``3x3`` window in + which each value repeats along the anti-diagonals: + + >>> y = np.arange(5, dtype=np.int32) + >>> np.lib.stride_tricks.as_strided(y, shape=(3, 3), + ... strides=(y.itemsize, y.itemsize)) + array([[0, 1, 2], + [1, 2, 3], + [2, 3, 4]], dtype=int32) + + Attempting to create an out-of-bounds view: + + >>> np.lib.stride_tricks.as_strided(y, shape=(10,), + ... strides=(y.itemsize,)) + Traceback (most recent call last): + ... + ValueError: buffer='[0 1 2 3 4]' can not accommodate the requested array. + + """ + + dpnp.check_supported_arrays_type(x) + dpnp.check_limitations(subok=subok) + + shape = x.shape if shape is None else tuple(shape) + strides = x.strides if strides is None else tuple(strides) + + view = dpnp.ndarray( + shape, + dtype=x.dtype, + buffer=x, + strides=strides, + ) + + if view.flags.writable and not writeable: + view.flags.writable = False + return view diff --git a/dpnp/tests/test_stride_tricks.py b/dpnp/tests/test_stride_tricks.py new file mode 100644 index 000000000000..244d52af1a59 --- /dev/null +++ b/dpnp/tests/test_stride_tricks.py @@ -0,0 +1,151 @@ +import numpy +import pytest +from numpy.lib.stride_tricks import as_strided as np_as_strided +from numpy.testing import assert_array_equal, assert_raises + +import dpnp +from dpnp.lib.stride_tricks import as_strided + +from .helper import generate_random_numpy_array, get_all_dtypes + + +class TestAsStrided: + @pytest.mark.parametrize("dt", get_all_dtypes(no_none=True)) + def test_basic(self, dt): + a = generate_random_numpy_array((4,), dtype=dt) + ia = dpnp.array(a) + + result = as_strided(ia, shape=(2,), strides=(2 * ia.itemsize,)) + expected = np_as_strided(a, shape=(2,), strides=(2 * a.itemsize,)) + assert_array_equal(result, expected) + + def test_broadcast_via_zero_stride(self): + a = numpy.array([1, 2, 3, 4], dtype=numpy.int32) + ia = dpnp.array(a) + + result = as_strided(ia, shape=(3, 4), strides=(0, ia.itemsize)) + expected = np_as_strided(a, shape=(3, 4), strides=(0, a.itemsize)) + assert_array_equal(result, expected) + + def test_default_shape_and_strides(self): + a = numpy.arange(6).reshape(2, 3) + ia = dpnp.array(a) + + result = as_strided(ia) + assert result.shape == ia.shape + assert result.strides == ia.strides + assert_array_equal(result, a) + + def test_self_overlapping_view(self): + a = numpy.arange(5, dtype=numpy.int32) + ia = dpnp.array(a) + + result = as_strided( + ia, shape=(3, 3), strides=(ia.itemsize, ia.itemsize) + ) + expected = np_as_strided( + a, shape=(3, 3), strides=(a.itemsize, a.itemsize) + ) + assert_array_equal(result, expected) + + # element (i, j) maps to base offset (i + j), so (0, 1) and (1, 0) + # share the same element; writing one changes the other + result[0, 1] = 100 + expected[0, 1] = 100 + assert_array_equal(result, expected) + + def test_overlapping_bulk_write_rejected(self): + a = dpnp.arange(5, dtype=dpnp.int32) + view = as_strided(a, shape=(3, 3), strides=(a.itemsize, a.itemsize)) + + with pytest.raises( + ValueError, match="output array is not sufficiently ample" + ): + view[...] = dpnp.full((3, 3), 7, dtype=a.dtype) + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + def test_writeable_false(self, xp): + a = xp.array([1, 2, 3, 4], dtype=xp.int32) + view = xp.lib.stride_tricks.as_strided( + a, shape=(2,), strides=(2 * a.itemsize,), writeable=False + ) + assert view.flags["W"] is False + assert a.flags["W"] is True + + # writing through a read-only view is rejected + with pytest.raises(ValueError, match="read-only"): + view[...] = 5 + + @pytest.mark.parametrize("xp", [dpnp, numpy]) + def test_writeable_true_readonly_base(self, xp): + a = xp.arange(10, dtype=xp.int32) + a.flags["W"] = False + + view = xp.lib.stride_tricks.as_strided(a, writeable=True) + assert view.flags["W"] is False + + def test_subok_not_supported(self): + ia = dpnp.array([1, 2, 3, 4], dtype=dpnp.int32) + assert_raises(NotImplementedError, as_strided, ia, subok=True) + + @pytest.mark.parametrize( + "shape, strides", + [ + ((2000,), (8,)), # overrun past the highest address + ((2,), (-8,)), # underrun before the lowest address + ], + ) + @pytest.mark.parametrize("xp", [dpnp, numpy]) + def test_out_of_bounds(self, shape, strides, xp): + a = xp.arange(1000, dtype=xp.int64) + with pytest.raises(ValueError): + _ = xp.lib.stride_tricks.as_strided( + a, shape=shape, strides=strides, check_bounds=True + ) + + def test_bounds_use_base_allocation(self): + # bounds are validated against the whole base allocation, so a view + # reaching beyond a slice but within its parent is accepted + a = numpy.arange(1000, dtype=numpy.int64) + ia = dpnp.array(a) + b, ib = a[:2], ia[:2] + + result = as_strided(ib, shape=(2,), strides=(400,)) + expected = np_as_strided(b, shape=(2,), strides=(400,)) + assert_array_equal(result, expected) + + @pytest.mark.parametrize( + "start, strides", + [ + (95, (200,)), # positive stride overruns the base allocation + (5, (-48,)), # negative stride underruns the base allocation + ], + ) + @pytest.mark.parametrize("xp", [dpnp, numpy]) + def test_out_of_bounds_over_slice(self, start, strides, xp): + a = xp.arange(100, dtype=xp.int64) + b = a[start : start + 2] + + with pytest.raises(ValueError): + _ = xp.lib.stride_tricks.as_strided( + b, shape=(2,), strides=strides, check_bounds=True + ) + + def test_view_with_offset(self): + a = numpy.arange(1000, dtype=numpy.int64) + ia = dpnp.array(a) + b, ib = a[100:102], ia[100:102] + + result = as_strided(ib, shape=(2,), strides=(80,)) + expected = np_as_strided(b, shape=(2,), strides=(80,)) + assert_array_equal(result, expected) + + def test_nested_views(self): + a = numpy.arange(1000, dtype=numpy.int64) + ia = dpnp.array(a) + b, ib = a[10:100], ia[10:100] + c, ic = b[5:10], ib[5:10] + + result = as_strided(ic, shape=(2,), strides=(160,)) + expected = np_as_strided(c, shape=(2,), strides=(160,)) + assert_array_equal(result, expected) diff --git a/dpnp/tests/third_party/cupy/indexing_tests/test_indexing.py b/dpnp/tests/third_party/cupy/indexing_tests/test_indexing.py index bec8f4204b33..4ab88f634f1f 100644 --- a/dpnp/tests/third_party/cupy/indexing_tests/test_indexing.py +++ b/dpnp/tests/third_party/cupy/indexing_tests/test_indexing.py @@ -388,7 +388,6 @@ def test_select_default_scalar(self, dtype): with pytest.raises(TypeError): cupy.select(condlist, choicelist, [dtype(2)]) - @pytest.mark.skip("as_strided() is not implemented yet") @testing.numpy_cupy_array_equal() def test_indexing_overflows(self, xp): a = xp.arange(2, dtype=xp.int32) diff --git a/dpnp/tests/third_party/cupy/lib_tests/test_strided_tricks.py b/dpnp/tests/third_party/cupy/lib_tests/test_strided_tricks.py index eb8f73244b17..f931343ca9a7 100644 --- a/dpnp/tests/third_party/cupy/lib_tests/test_strided_tricks.py +++ b/dpnp/tests/third_party/cupy/lib_tests/test_strided_tricks.py @@ -4,13 +4,9 @@ import pytest import dpnp as cupy +from dpnp.lib import stride_tricks from dpnp.tests.third_party.cupy import testing -# from cupy.lib import stride_tricks - - -pytest.skip("stride tricks are not supported yet", allow_module_level=True) - class TestAsStrided(unittest.TestCase): def test_as_strided(self): @@ -28,6 +24,7 @@ def test_as_strided(self): expected = cupy.array([[1, 2, 3, 4], [1, 2, 3, 4], [1, 2, 3, 4]]) testing.assert_array_equal(a_view, expected) + @pytest.mark.skip(reason="sliding_window_view is not supported yet") @testing.numpy_cupy_array_equal() def test_rolling_window(self, xp): a = testing.shaped_arange((3, 4), xp) @@ -35,6 +32,7 @@ def test_rolling_window(self, xp): return a_rolling +@pytest.mark.skip(reason="sliding_window_view is not supported yet") class TestSlidingWindowView(unittest.TestCase): @testing.numpy_cupy_array_equal() def test_1d(self, xp): diff --git a/setup.py b/setup.py index 37e22207891f..5412d3ad7c0c 100644 --- a/setup.py +++ b/setup.py @@ -39,6 +39,7 @@ "dpnp.dpnp_utils", "dpnp.exceptions", "dpnp.fft", + "dpnp.lib", "dpnp.linalg", "dpnp.memory", "dpnp.random",