Skip to content

Commit d2ea18f

Browse files
author
Vahid Tavanashad
committed
implement dpnp.delete
1 parent d1ff2e7 commit d2ea18f

File tree

5 files changed

+370
-3
lines changed

5 files changed

+370
-3
lines changed

dpnp/dpnp_iface_manipulation.py

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@
6666
"concat",
6767
"concatenate",
6868
"copyto",
69+
"delete",
6970
"dsplit",
7071
"dstack",
7172
"expand_dims",
@@ -115,6 +116,135 @@ def _check_stack_arrays(arrays):
115116
)
116117

117118

119+
def _delete_with_slice(a, obj, axis):
120+
"""Utility function for ``dpnp.delete`` when obj is slice."""
121+
122+
a_ndim, order, axis, slobj, n, a_shape = _calc_parameters(a, axis)
123+
start, stop, step = obj.indices(n)
124+
xr = range(start, stop, step)
125+
num_del = len(xr)
126+
127+
if num_del <= 0:
128+
return a.copy(order=order)
129+
130+
# Invert if step is negative:
131+
if step < 0:
132+
step = -step
133+
start = xr[-1]
134+
stop = xr[0] + 1
135+
136+
a_shape[axis] -= num_del
137+
new = dpnp.empty(
138+
a_shape,
139+
dtype=a.dtype,
140+
order=order,
141+
sycl_queue=a.sycl_queue,
142+
usm_type=a.usm_type,
143+
)
144+
# copy initial chunk
145+
if start == 0:
146+
pass
147+
else:
148+
slobj[axis] = slice(None, start)
149+
new[tuple(slobj)] = a[tuple(slobj)]
150+
# copy end chunk
151+
if stop == n:
152+
pass
153+
else:
154+
slobj[axis] = slice(stop - num_del, None)
155+
slobj2 = [slice(None)] * a_ndim
156+
slobj2[axis] = slice(stop, None)
157+
new[tuple(slobj)] = a[tuple(slobj2)]
158+
# copy middle pieces
159+
if step == 1:
160+
pass
161+
else: # use array indexing.
162+
keep = dpnp.ones(
163+
stop - start,
164+
dtype=dpnp.bool,
165+
sycl_queue=a.sycl_queue,
166+
usm_type=a.usm_type,
167+
)
168+
keep[: stop - start : step] = False
169+
slobj[axis] = slice(start, stop - num_del)
170+
slobj2 = [slice(None)] * a_ndim
171+
slobj2[axis] = slice(start, stop)
172+
a = a[tuple(slobj2)]
173+
slobj2[axis] = keep
174+
new[tuple(slobj)] = a[tuple(slobj2)]
175+
176+
return new
177+
178+
179+
def _delete_without_slice(a, obj, axis, single_value):
180+
"""Utility function for ``dpnp.delete`` when obj is int or array of int."""
181+
182+
a_ndim, order, axis, slobj, n, a_shape = _calc_parameters(a, axis)
183+
if single_value:
184+
# optimization for a single value
185+
if obj < -n or obj >= n:
186+
raise IndexError(
187+
f"index {obj} is out of bounds for axis {axis} with "
188+
f"size {n}"
189+
)
190+
if obj < 0:
191+
obj += n
192+
a_shape[axis] -= 1
193+
new = dpnp.empty(
194+
a_shape,
195+
dtype=a.dtype,
196+
order=order,
197+
sycl_queue=a.sycl_queue,
198+
usm_type=a.usm_type,
199+
)
200+
slobj[axis] = slice(None, obj)
201+
new[tuple(slobj)] = a[tuple(slobj)]
202+
slobj[axis] = slice(obj, None)
203+
slobj2 = [slice(None)] * a_ndim
204+
slobj2[axis] = slice(obj + 1, None)
205+
new[tuple(slobj)] = a[tuple(slobj2)]
206+
else:
207+
if obj.dtype == dpnp.bool:
208+
if obj.shape != (n,):
209+
raise ValueError(
210+
"boolean array argument `obj` to delete must be "
211+
f"one-dimensional and match the axis length of {n}"
212+
)
213+
214+
# optimization, the other branch is slower
215+
keep = ~obj
216+
else:
217+
keep = dpnp.ones(
218+
n, dtype=dpnp.bool, sycl_queue=a.sycl_queue, usm_type=a.usm_type
219+
)
220+
keep[obj,] = False
221+
222+
slobj[axis] = keep
223+
new = a[tuple(slobj)]
224+
225+
return new
226+
227+
228+
def _calc_parameters(a, axis):
229+
"""Utility function for ``dpnp.delete`` and ``dpnp.insert``."""
230+
231+
a_ndim = a.ndim
232+
order = "F" if a.flags.fnc else "C"
233+
if axis is None:
234+
if a_ndim != 1:
235+
a = dpnp.ravel(a)
236+
a_ndim = 1
237+
axis = 0
238+
else:
239+
axis = normalize_axis_index(axis, a_ndim)
240+
241+
slobj = [slice(None)] * a_ndim
242+
n = a.shape[axis]
243+
a_shape = list(a.shape)
244+
245+
return a_ndim, order, axis, slobj, n, a_shape
246+
247+
118248
def _unique_1d(
119249
ar,
120250
return_index=False,
@@ -1206,6 +1336,100 @@ def copyto(dst, src, casting="same_kind", where=True):
12061336
dst_usm[mask_usm] = src_usm[mask_usm]
12071337

12081338

1339+
def delete(arr, obj, axis=None):
1340+
"""
1341+
Return a new array with sub-arrays along an axis deleted. For a one
1342+
dimensional array, this returns those entries not returned by
1343+
``arr[obj]``.
1344+
1345+
For full documentation refer to :obj:`numpy.delete`.
1346+
1347+
Parameters
1348+
----------
1349+
arr : {dpnp.ndarray, usm_ndarray}
1350+
Input array.
1351+
obj : {slice, int, array-like of ints or boolean}
1352+
Indicate indices of sub-arrays to remove along the specified axis.
1353+
Boolean indices are treated as a mask of elements to remove.
1354+
axis : int, optional
1355+
The axis along which to delete the subarray defined by `obj`.
1356+
If `axis` is ``None``, `obj` is applied to the flattened array.
1357+
Default: ``None``.
1358+
1359+
Returns
1360+
-------
1361+
out : dpnp.ndarray
1362+
A copy of `arr` with the elements specified by `obj` removed. Note
1363+
that `delete` does not occur in-place. If `axis` is ``None``, `out` is
1364+
a flattened array.
1365+
1366+
See Also
1367+
--------
1368+
:obj:`dpnp.insert` : Insert elements into an array.
1369+
:obj:`dpnp.append` : Append elements at the end of an array.
1370+
1371+
Notes
1372+
-----
1373+
Often it is preferable to use a boolean mask. For example:
1374+
1375+
>>> import dpnp as np
1376+
>>> arr = np.arange(12) + 1
1377+
>>> mask = np.ones(len(arr), dtype=np.bool)
1378+
>>> mask[0] = mask[2] = mask[4] = False
1379+
>>> result = arr[mask,...]
1380+
1381+
is equivalent to ``np.delete(arr, [0,2,4], axis=0)``, but allows further
1382+
use of `mask`.
1383+
1384+
Examples
1385+
--------
1386+
>>> import dpnp as np
1387+
>>> arr = np.array([[1, 2, 3, 4], [5, 6, 7, 8], [9, 10, 11, 12]])
1388+
>>> arr
1389+
array([[ 1, 2, 3, 4],
1390+
[ 5, 6, 7, 8],
1391+
[ 9, 10, 11, 12]])
1392+
>>> np.delete(arr, 1, 0)
1393+
array([[ 1, 2, 3, 4],
1394+
[ 9, 10, 11, 12]])
1395+
1396+
>>> np.delete(arr, slice(None, None, 2), 1)
1397+
array([[ 2, 4],
1398+
[ 6, 8],
1399+
[10, 12]])
1400+
>>> np.delete(arr, [1, 3, 5], None)
1401+
array([ 1, 3, 5, 7, 8, 9, 10, 11, 12])
1402+
1403+
"""
1404+
1405+
dpnp.check_supported_arrays_type(arr)
1406+
1407+
if isinstance(obj, slice):
1408+
return _delete_with_slice(arr, obj, axis)
1409+
1410+
if isinstance(obj, (int, dpnp.integer)) and not isinstance(obj, bool):
1411+
single_value = True
1412+
else:
1413+
single_value = False
1414+
is_array = isinstance(obj, (dpnp_array, numpy.ndarray, dpt.usm_ndarray))
1415+
obj = dpnp.asarray(
1416+
obj, sycl_queue=arr.sycl_queue, usm_type=arr.usm_type
1417+
)
1418+
# if `obj` is originally an empty list, after converting it into
1419+
# an array, it will have float dtype, so we need to change its dtype
1420+
# to integer. However, if `obj` is originally an empty array with
1421+
# float dtype, it is a mistake by user and it will raise an error later
1422+
if obj.size == 0 and not is_array:
1423+
obj = obj.astype(dpnp.intp)
1424+
elif obj.size == 1 and obj.dtype.kind in "ui":
1425+
# For a size 1 integer array we can use the single-value path
1426+
# (most dtypes, except boolean, should just fail later).
1427+
obj = obj.item()
1428+
single_value = True
1429+
1430+
return _delete_without_slice(arr, obj, axis, single_value)
1431+
1432+
12091433
def dsplit(ary, indices_or_sections):
12101434
"""
12111435
Split array into multiple sub-arrays along the 3rd axis (depth).

tests/test_manipulation.py

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -332,6 +332,124 @@ def test_no_copy(self):
332332
assert_array_equal(b, a)
333333

334334

335+
class TestDelete:
336+
def _check_inverse_of_slicing(self, indices):
337+
a = numpy.arange(5)
338+
b = numpy.arange(10).reshape(1, 5, 2)
339+
a_dp = dpnp.array(a)
340+
b_dp = dpnp.array(b)
341+
342+
expected1 = numpy.delete(a, indices)
343+
expected2 = numpy.delete(b, indices, axis=1)
344+
result1 = dpnp.delete(a_dp, indices)
345+
result2 = dpnp.delete(b_dp, indices, axis=1)
346+
assert_array_equal(result1, expected1)
347+
assert_array_equal(result2, expected2)
348+
349+
def test_slices(self):
350+
lims = [-6, -2, 0, 1, 2, 4, 5]
351+
steps = [-3, -1, 1, 3]
352+
for start in lims:
353+
for stop in lims:
354+
for step in steps:
355+
s = slice(start, stop, step)
356+
self._check_inverse_of_slicing(s)
357+
358+
def test_obj(self):
359+
self._check_inverse_of_slicing([0, -1, 2, 2])
360+
self._check_inverse_of_slicing([True, False, False, True, False])
361+
self._check_inverse_of_slicing(0)
362+
self._check_inverse_of_slicing(-4)
363+
self._check_inverse_of_slicing([])
364+
365+
def test_obj_ndarray(self):
366+
# 1D array
367+
a = numpy.arange(5)
368+
ind = numpy.array([[0, 1], [2, 1]])
369+
a_dp = dpnp.array(a)
370+
ind_dp = dpnp.array(ind)
371+
372+
expected = numpy.delete(a, ind)
373+
# both numpy.ndarray and dpnp.ndarray are supported for obj in dpnp
374+
for indices in [ind, ind_dp]:
375+
result = dpnp.delete(a_dp, indices)
376+
assert_array_equal(result, expected)
377+
378+
# N-D array
379+
b = numpy.arange(10).reshape(1, 5, 2)
380+
b_dp = dpnp.array(b)
381+
expected = numpy.delete(b, ind, axis=1)
382+
for indices in [ind, ind_dp]:
383+
result = dpnp.delete(b_dp, indices, axis=1)
384+
assert_array_equal(result, expected)
385+
386+
def test_error(self):
387+
a = dpnp.arange(5)
388+
# out of bounds index
389+
with pytest.raises(IndexError):
390+
dpnp.delete(a, [100])
391+
with pytest.raises(IndexError):
392+
dpnp.delete(a, [-100])
393+
394+
# boolean array argument obj must be one dimensional
395+
with pytest.raises(ValueError):
396+
dpnp.delete(a, True)
397+
398+
# not enough items
399+
with pytest.raises(ValueError):
400+
dpnp.delete(a, [False] * 4)
401+
402+
# 0-D array
403+
a = dpnp.array(1)
404+
with pytest.raises(AxisError):
405+
dpnp.delete(a, [], axis=0)
406+
with pytest.raises(TypeError):
407+
dpnp.delete(a, [], axis="nonsense")
408+
409+
# index float
410+
a = dpnp.array([1, 2, 3])
411+
with pytest.raises(IndexError):
412+
dpnp.delete(a, dpnp.array([1.0, 2.0]))
413+
with pytest.raises(IndexError):
414+
dpnp.delete(a, dpnp.array([], dtype=float))
415+
416+
def test_order(self):
417+
a = numpy.arange(10).reshape(2, 5, order="F")
418+
a_dp = dpnp.array(a)
419+
420+
expected = numpy.delete(a, slice(3, None), axis=1)
421+
result = dpnp.delete(a_dp, slice(3, None), axis=1)
422+
423+
assert_equal(result.flags.c_contiguous, expected.flags.c_contiguous)
424+
assert_equal(result.flags.f_contiguous, expected.flags.f_contiguous)
425+
426+
@pytest.mark.parametrize("indexer", [1, dpnp.array([1]), [1]])
427+
def test_single_item_array(self, indexer):
428+
a = numpy.arange(5)
429+
a_dp = dpnp.array(a)
430+
expected = numpy.delete(a, 1)
431+
result = dpnp.delete(a_dp, indexer)
432+
assert_equal(result, expected)
433+
434+
b = numpy.arange(10).reshape(1, 5, 2)
435+
b_dp = dpnp.array(b)
436+
expected = numpy.delete(b, 1, axis=1)
437+
result = dpnp.delete(b_dp, indexer, axis=1)
438+
assert_equal(result, expected)
439+
440+
@pytest.mark.parametrize("flag", [True, False])
441+
def test_boolean_obj(self, flag):
442+
expected = numpy.delete(numpy.ones(1), numpy.array([flag]))
443+
result = dpnp.delete(dpnp.ones(1), dpnp.array([flag]))
444+
assert_array_equal(result, expected)
445+
446+
expected = numpy.delete(
447+
numpy.ones((3, 1)), numpy.array([flag]), axis=-1
448+
)
449+
result = dpnp.delete(dpnp.ones((3, 1)), dpnp.array([flag]), axis=-1)
450+
assert_array_equal(result, expected)
451+
452+
335453
class TestDsplit:
336454
@pytest.mark.parametrize("xp", [numpy, dpnp])
337455
def test_error(self, xp):

tests/test_sycl_queue.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2046,6 +2046,20 @@ def test_concat_stack(func, data1, data2, device):
20462046
assert_sycl_queue_equal(result.sycl_queue, x2.sycl_queue)
20472047

20482048

2049+
@pytest.mark.parametrize(
2050+
"device",
2051+
valid_devices,
2052+
ids=[device.filter_string for device in valid_devices],
2053+
)
2054+
@pytest.mark.parametrize(
2055+
"obj", [slice(None, None, 2), 3, [2, 3]], ids=["slice", "int", "list"]
2056+
)
2057+
def test_delete(device, obj):
2058+
x = dpnp.arange(5, device=device)
2059+
result = dpnp.delete(x, obj)
2060+
assert_sycl_queue_equal(result.sycl_queue, x.sycl_queue)
2061+
2062+
20492063
@pytest.mark.parametrize(
20502064
"func,data1",
20512065
[

0 commit comments

Comments
 (0)