Skip to content

Commit 866eb3e

Browse files
bruAristimunhaqbarthelemylucascolley
authored
ENH: add diag_indices, tril_indices, triu_indices (#692)
Co-authored-by: Quentin Barthélemy <q.barthelemy@gmail.com> Co-authored-by: Lucas Colley <lucas.colley8@gmail.com>
1 parent 064cbf7 commit 866eb3e

7 files changed

Lines changed: 401 additions & 12 deletions

File tree

docs/api-assorted.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
cov
1616
create_diagonal
1717
default_dtype
18+
diag_indices
1819
expand_dims
1920
isclose
2021
isin
@@ -27,6 +28,8 @@
2728
searchsorted
2829
setdiff1d
2930
sinc
31+
tril_indices
32+
triu_indices
3033
union1d
3134
unravel_index
3235
```

src/array_api_extra/__init__.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
broadcast_shapes,
88
cov,
99
create_diagonal,
10+
diag_indices,
1011
expand_dims,
1112
isclose,
1213
isin,
@@ -18,6 +19,8 @@
1819
searchsorted,
1920
setdiff1d,
2021
sinc,
22+
tril_indices,
23+
triu_indices,
2124
union1d,
2225
unravel_index,
2326
)
@@ -44,6 +47,7 @@
4447
"cov",
4548
"create_diagonal",
4649
"default_dtype",
50+
"diag_indices",
4751
"expand_dims",
4852
"isclose",
4953
"isin",
@@ -58,6 +62,8 @@
5862
"setdiff1d",
5963
"sinc",
6064
"testing",
65+
"tril_indices",
66+
"triu_indices",
6167
"union1d",
6268
"unravel_index",
6369
]

src/array_api_extra/_delegation.py

Lines changed: 196 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,14 @@
2121
eager_shape,
2222
normalize_pad_width,
2323
)
24-
from ._lib._utils._typing import Array, DType
24+
from ._lib._utils._typing import Array, Device, DType
2525

2626
__all__ = [
2727
"atleast_nd",
2828
"broadcast_shapes",
2929
"cov",
3030
"create_diagonal",
31+
"diag_indices",
3132
"expand_dims",
3233
"isclose",
3334
"kron",
@@ -36,6 +37,8 @@
3637
"pad",
3738
"searchsorted",
3839
"sinc",
40+
"tril_indices",
41+
"triu_indices",
3942
"unravel_index",
4043
]
4144

@@ -310,6 +313,55 @@ def create_diagonal(
310313
return _funcs.create_diagonal(x, offset=offset, xp=xp)
311314

312315

316+
def diag_indices(
317+
n: int, /, *, ndim: int = 2, device: Device | None = None, xp: ModuleType
318+
) -> tuple[Array, ...]:
319+
"""
320+
Return the indices to access the main diagonal of an array.
321+
322+
Equivalent to :func:`numpy.diag_indices`.
323+
324+
Parameters
325+
----------
326+
n : int
327+
The size of each dimension of the (hyper-)cube ``(n, n, ..., n)``
328+
that the returned indices index into.
329+
ndim : int, optional
330+
The number of dimensions. Default: ``2``.
331+
device : Device, optional
332+
The device on which to place the returned arrays. Default: current device.
333+
xp : array_namespace
334+
The standard-compatible namespace to create the indices in.
335+
336+
Returns
337+
-------
338+
tuple of array
339+
1-D integer arrays of length ``n`` that together index
340+
the main diagonal of an array of shape ``(n,) * ndim``.
341+
342+
Examples
343+
--------
344+
>>> import array_api_strict as xp
345+
>>> import array_api_extra as xpx
346+
>>> rows, cols = xpx.diag_indices(3, xp=xp)
347+
>>> rows
348+
Array([0, 1, 2], dtype=array_api_strict.int64)
349+
>>> cols
350+
Array([0, 1, 2], dtype=array_api_strict.int64)
351+
"""
352+
if n < 0:
353+
msg = f"`n` must be non-negative, got {n}"
354+
raise ValueError(msg)
355+
if ndim < 1:
356+
msg = f"`ndim` must be >= 1, got {ndim}"
357+
raise ValueError(msg)
358+
if device is None and (
359+
is_numpy_namespace(xp) or is_cupy_namespace(xp) or is_jax_namespace(xp)
360+
):
361+
return xp.diag_indices(n, ndim=ndim)
362+
return _funcs.diag_indices(n, ndim=ndim, device=device, xp=xp)
363+
364+
313365
@deprecated(
314366
"`xpx.expand_dims` is deprecated and will be removed in v1.0.0. "
315367
"`xp.expand_dims` with support for a tuple of ints in `axis` "
@@ -802,11 +854,7 @@ def pad(
802854
# `torch/_numpy`'s implementation (avoids device transfers)
803855
pad_width_seq = normalize_pad_width(pad_width, x.ndim)
804856
# torch.nn.functional.pad counts dimensions from the last one
805-
flat_pad_width = [
806-
w
807-
for pair in reversed(pad_width_seq)
808-
for w in pair
809-
]
857+
flat_pad_width = [w for pair in reversed(pad_width_seq) for w in pair]
810858
return xp.nn.functional.pad(x, tuple(flat_pad_width), value=constant_values)
811859

812860
return _funcs.pad(x, pad_width, constant_values=constant_values, xp=xp)
@@ -1331,6 +1379,148 @@ def union1d(a: Array, b: Array, /, *, xp: ModuleType | None = None) -> Array:
13311379
return _funcs.union1d(a, b, xp=xp)
13321380

13331381

1382+
def tril_indices(
1383+
n: int,
1384+
/,
1385+
*,
1386+
offset: int = 0,
1387+
m: int | None = None,
1388+
device: Device | None = None,
1389+
xp: ModuleType,
1390+
) -> tuple[Array, Array]:
1391+
"""
1392+
Return the indices of the lower triangle of an ``(n, m)`` array.
1393+
1394+
Equivalent to :func:`numpy.tril_indices` with parameter ``k`` renamed to
1395+
``offset`` to match :func:`array_api.linalg.diagonal`'s naming.
1396+
1397+
Parameters
1398+
----------
1399+
n : int
1400+
The row dimension of the array.
1401+
offset : int, optional
1402+
Diagonal offset; ``0`` (default) is the main diagonal. Corresponds
1403+
to ``k`` in :func:`numpy.tril_indices`.
1404+
m : int, optional
1405+
The column dimension. If ``None`` (default), assumed equal to `n`.
1406+
device : Device, optional
1407+
The device on which to place the returned arrays. Default: current device.
1408+
xp : array_namespace
1409+
The standard-compatible namespace to create the indices in.
1410+
1411+
Returns
1412+
-------
1413+
tuple of array
1414+
Row and column indices ``(rows, cols)`` of the lower triangle of
1415+
the ``(n, m)`` matrix, shifted by `offset`.
1416+
1417+
Notes
1418+
-----
1419+
The generic fallback uses :func:`array_api.nonzero`, so namespaces without
1420+
``nonzero`` are not supported on that path.
1421+
1422+
Examples
1423+
--------
1424+
>>> import array_api_strict as xp
1425+
>>> import array_api_extra as xpx
1426+
>>> rows, cols = xpx.tril_indices(3, xp=xp)
1427+
>>> rows
1428+
Array([0, 1, 1, 2, 2, 2], dtype=array_api_strict.int64)
1429+
>>> cols
1430+
Array([0, 0, 1, 0, 1, 2], dtype=array_api_strict.int64)
1431+
"""
1432+
if n < 0:
1433+
msg = f"`n` must be non-negative, got {n}"
1434+
raise ValueError(msg)
1435+
if m is not None and m < 0:
1436+
msg = f"`m` must be non-negative, got {m}"
1437+
raise ValueError(msg)
1438+
if device is None and (
1439+
is_numpy_namespace(xp)
1440+
or is_cupy_namespace(xp)
1441+
or is_jax_namespace(xp)
1442+
or is_dask_namespace(xp)
1443+
):
1444+
return xp.tril_indices(n, k=offset, m=m)
1445+
if is_torch_namespace(xp):
1446+
# `torch.tril_indices` returns a 2xN tensor, not a tuple, and
1447+
# takes (row, col) rather than (n, *, m=None).
1448+
cols = n if m is None else m
1449+
idx = xp.tril_indices(n, cols, offset=offset, device=device)
1450+
return (idx[0], idx[1])
1451+
return _funcs.tril_indices(n, offset=offset, m=m, device=device, xp=xp)
1452+
1453+
1454+
def triu_indices(
1455+
n: int,
1456+
/,
1457+
*,
1458+
offset: int = 0,
1459+
m: int | None = None,
1460+
device: Device | None = None,
1461+
xp: ModuleType,
1462+
) -> tuple[Array, Array]:
1463+
"""
1464+
Return the indices of the upper triangle of an ``(n, m)`` array.
1465+
1466+
Equivalent to :func:`numpy.triu_indices` with parameter ``k`` renamed to
1467+
``offset`` to match :func:`array_api.linalg.diagonal`'s naming.
1468+
1469+
Parameters
1470+
----------
1471+
n : int
1472+
The row dimension of the array.
1473+
offset : int, optional
1474+
Diagonal offset; ``0`` (default) is the main diagonal. Corresponds
1475+
to ``k`` in :func:`numpy.triu_indices`.
1476+
m : int, optional
1477+
The column dimension. If ``None`` (default), assumed equal to `n`.
1478+
device : Device, optional
1479+
The device on which to place the returned arrays. Default: current device.
1480+
xp : array_namespace
1481+
The standard-compatible namespace to create the indices in.
1482+
1483+
Returns
1484+
-------
1485+
tuple of array
1486+
Row and column indices ``(rows, cols)`` of the upper triangle of
1487+
the ``(n, m)`` matrix, shifted by `offset`.
1488+
1489+
Notes
1490+
-----
1491+
The generic fallback uses :func:`array_api.nonzero`, so namespaces without
1492+
``nonzero`` are not supported on that path.
1493+
1494+
Examples
1495+
--------
1496+
>>> import array_api_strict as xp
1497+
>>> import array_api_extra as xpx
1498+
>>> rows, cols = xpx.triu_indices(3, xp=xp)
1499+
>>> rows
1500+
Array([0, 0, 0, 1, 1, 2], dtype=array_api_strict.int64)
1501+
>>> cols
1502+
Array([0, 1, 2, 1, 2, 2], dtype=array_api_strict.int64)
1503+
"""
1504+
if n < 0:
1505+
msg = f"`n` must be non-negative, got {n}"
1506+
raise ValueError(msg)
1507+
if m is not None and m < 0:
1508+
msg = f"`m` must be non-negative, got {m}"
1509+
raise ValueError(msg)
1510+
if device is None and (
1511+
is_numpy_namespace(xp)
1512+
or is_cupy_namespace(xp)
1513+
or is_jax_namespace(xp)
1514+
or is_dask_namespace(xp)
1515+
):
1516+
return xp.triu_indices(n, k=offset, m=m)
1517+
if is_torch_namespace(xp):
1518+
cols = n if m is None else m
1519+
idx = xp.triu_indices(n, cols, offset=offset, device=device)
1520+
return (idx[0], idx[1])
1521+
return _funcs.triu_indices(n, offset=offset, m=m, device=device, xp=xp)
1522+
1523+
13341524
def unravel_index(
13351525
indices: Array,
13361526
shape: tuple[int, ...],

src/array_api_extra/_lib/_funcs.py

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,13 +30,16 @@
3030
"broadcast_shapes",
3131
"cov",
3232
"create_diagonal",
33+
"diag_indices",
3334
"expand_dims",
3435
"kron",
3536
"nunique",
3637
"pad",
3738
"searchsorted",
3839
"setdiff1d",
3940
"sinc",
41+
"tril_indices",
42+
"triu_indices",
4043
]
4144

4245

@@ -312,6 +315,59 @@ def create_diagonal(
312315
return xp.reshape(diag, (*batch_dims, n, n))
313316

314317

318+
def diag_indices(
319+
n: int, /, *, ndim: int, device: Device | None, xp: ModuleType
320+
) -> tuple[Array, ...]: # numpydoc ignore=PR01,RT01
321+
"""See docstring in array_api_extra._delegation."""
322+
idx = xp.arange(n, device=device)
323+
return (idx,) * ndim
324+
325+
326+
def _tri_indices(
327+
n: int,
328+
*,
329+
offset: int,
330+
m: int | None,
331+
upper: bool,
332+
device: Device | None,
333+
xp: ModuleType,
334+
) -> tuple[Array, Array]: # numpydoc ignore=PR01,RT01
335+
"""Shared implementation for `tril_indices` and `triu_indices`."""
336+
cols = n if m is None else m
337+
rows = xp.arange(n, device=device)[:, xp.newaxis]
338+
cols_a = xp.arange(cols, device=device)[xp.newaxis, :]
339+
delta = cols_a - rows
340+
mask = delta >= offset if upper else delta <= offset
341+
r, c = xp.nonzero(mask)
342+
return (r, c)
343+
344+
345+
def tril_indices(
346+
n: int,
347+
/,
348+
*,
349+
offset: int,
350+
m: int | None,
351+
device: Device | None,
352+
xp: ModuleType,
353+
) -> tuple[Array, Array]: # numpydoc ignore=PR01,RT01
354+
"""See docstring in array_api_extra._delegation."""
355+
return _tri_indices(n, offset=offset, m=m, upper=False, device=device, xp=xp)
356+
357+
358+
def triu_indices(
359+
n: int,
360+
/,
361+
*,
362+
offset: int,
363+
m: int | None,
364+
device: Device | None,
365+
xp: ModuleType,
366+
) -> tuple[Array, Array]: # numpydoc ignore=PR01,RT01
367+
"""See docstring in array_api_extra._delegation."""
368+
return _tri_indices(n, offset=offset, m=m, upper=True, device=device, xp=xp)
369+
370+
315371
def default_dtype(
316372
xp: ModuleType,
317373
kind: Literal[

0 commit comments

Comments
 (0)