Repository: pydata/sparse
Pull request #880 (2025-06-29 18:19:57)

Adding `tile` function
<!--
# Thanks for contributing a pull request!
## Please make sure you see our contribution guidelines: https://github.com/pydata/sparse/blob/main/docs/contributing.md

Your PR title should start with any of these abbreviatons: `build`, `chore`, `ci`, `depr`, `docs`, `feat`, `fix`, `perf`, `refactor`, `release`, `test`. Add a `!`at the end, if it is a breaking change.
-->

## What type of PR is this? (check all applicable)

- [ ] 💾 Refactor
- [x] 🪄 Feature
- [ ] 🐞 Bug Fix
- [ ] 🔧 Optimization
- [ ] 📚 Documentation
- [x] 🧪 Test
- [ ] 🛠️ Other

## Related issues

- Related issue #
- Closes #

## Checklist

- [x] Code follows style guide
- [x] Tests added
- [ ] Documented the changes

***

## Please explain your changes below.

Added the suppport for `tile` function (np.tile equivalent).

codspeed-hq[bot]: <!-- __CODSPEED_PERFORMANCE_REPORT_COMMENT__ -->
<!-- __CODSPEED_INSTRUMENTATION_PERFORMANCE_REPORT_COMMENT__ -->

## [CodSpeed Performance Report](https://codspeed.io/pydata/sparse/branches/prady0t%3Atile-function)

### Merging #880 will **not alter performance**

<sub>Comparing <code>prady0t:tile-function</code> (f821e5b) with <code>main</code> (befe38a)</sub>



### Summary

`✅ 340` untouched benchmarks  





Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 85a1311..6fa7ca3 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -18,7 +18,7 @@ repos:
       exclude: ".ipynb"
 
 - repo: https://github.com/astral-sh/ruff-pre-commit
-  rev: v0.12.0
+  rev: v0.12.1
   hooks:
     - id: ruff-check
       args: ["--fix"]
diff --git a/ci/Numba-array-api-xfails.txt b/ci/Numba-array-api-xfails.txt
index 66bd640..dd8ec9e 100644
--- a/ci/Numba-array-api-xfails.txt
+++ b/ci/Numba-array-api-xfails.txt
@@ -70,7 +70,6 @@ array_api_tests/test_creation_functions.py::test_empty_like
 array_api_tests/test_data_type_functions.py::test_finfo[complex64]
 array_api_tests/test_manipulation_functions.py::test_squeeze
 array_api_tests/test_has_names.py::test_has_names[utility-diff]
-array_api_tests/test_has_names.py::test_has_names[manipulation-tile]
 array_api_tests/test_has_names.py::test_has_names[manipulation-unstack]
 array_api_tests/test_has_names.py::test_has_names[statistical-cumulative_sum]
 array_api_tests/test_has_names.py::test_has_names[statistical-cumulative_prod]
@@ -78,7 +77,6 @@ array_api_tests/test_has_names.py::test_has_names[indexing-take_along_axis]
 array_api_tests/test_has_names.py::test_has_names[searching-count_nonzero]
 array_api_tests/test_has_names.py::test_has_names[searching-searchsorted]
 array_api_tests/test_signatures.py::test_func_signature[diff]
-array_api_tests/test_signatures.py::test_func_signature[tile]
 array_api_tests/test_signatures.py::test_func_signature[unstack]
 array_api_tests/test_signatures.py::test_func_signature[take_along_axis]
 array_api_tests/test_special_cases.py::test_binary[floor_divide(x1_i is +infinity and isfinite(x2_i) and x2_i > 0) -> +infinity]
@@ -107,7 +105,6 @@ array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_2[1]
 array_api_tests/test_array_object.py::test_getitem_arrays_and_ints_2[None]
 array_api_tests/test_searching_functions.py::test_count_nonzero
 array_api_tests/test_searching_functions.py::test_searchsorted
-array_api_tests/test_manipulation_functions.py::test_tile
 array_api_tests/test_signatures.py::test_func_signature[cumulative_sum]
 array_api_tests/test_signatures.py::test_func_signature[cumulative_prod]
 array_api_tests/test_manipulation_functions.py::test_unstack
diff --git a/sparse/numba_backend/__init__.py b/sparse/numba_backend/__init__.py
index dae4b06..efbfa1c 100644
--- a/sparse/numba_backend/__init__.py
+++ b/sparse/numba_backend/__init__.py
@@ -127,6 +127,7 @@ from ._common import (
     std,
     sum,
     tensordot,
+    tile,
     var,
     vecdot,
     zeros,
@@ -337,6 +338,7 @@ __all__ = [
     "zeros",
     "zeros_like",
     "repeat",
+    "tile",
 ]
 
 
diff --git a/sparse/numba_backend/_common.py b/sparse/numba_backend/_common.py
index abd581d..9f1b051 100644
--- a/sparse/numba_backend/_common.py
+++ b/sparse/numba_backend/_common.py
@@ -3147,3 +3147,42 @@ def repeat(a, repeats, axis=None):
     if not axis_is_none:
         return a.reshape(new_shape)
     return a.reshape(new_shape).flatten()
+
+
+def tile(a, reps):
+    """
+    Constructs an array by tiling an input array.
+
+    Parameters
+    ----------
+    a : SparseArray
+        Input sparse arrays.
+    reps : int or tuple[int, ...]
+        The number of repetitions for each dimension.
+        If an integer, the same number of repetitions is applied to all dimensions.
+
+    Returns
+    -------
+    out : SparseArray
+        A tiled output array.
+    """
+    if not isinstance(a, SparseArray):
+        a = as_coo(a)
+
+    if isinstance(reps, int):
+        reps = (reps,)
+    reps = tuple(reps)
+
+    if a.ndim == 0:
+        a = a.reshape((1,))
+
+    if len(reps) < a.ndim:
+        reps = (1,) * (a.ndim - len(reps)) + reps
+    elif len(reps) > a.ndim:
+        a = a.reshape((1,) * (len(reps) - a.ndim) + a.shape)
+
+    shape = a.shape
+    ndim = len(reps)
+    a = a.reshape(tuple(np.column_stack(([1] * ndim, shape)).reshape(-1)))
+    a = a.broadcast_to(tuple(np.column_stack((reps, shape)).reshape(-1)))
+    return a.reshape(tuple(np.multiply(reps, shape)))


Test patch:
diff --git a/sparse/numba_backend/tests/test_coo.py b/sparse/numba_backend/tests/test_coo.py
index ccc0bef..767ba83 100644
--- a/sparse/numba_backend/tests/test_coo.py
+++ b/sparse/numba_backend/tests/test_coo.py
@@ -1956,3 +1956,28 @@ def test_repeat(ndim, repeats):
     print(f"Expected: {expected}, Actual: {actual}")
     assert actual.shape == expected.shape
     np.testing.assert_array_equal(actual, expected)
+
+
+def test_tile_invalid_input():
+    a = np.eye(3)
+    assert isinstance(sparse.tile(a, 2), sparse.COO)
+
+
+@pytest.mark.parametrize(
+    "arr,reps",
+    [
+        (np.array([1, 2, 3]), (3,)),
+        (np.array([4, 5, 6, 7]), 3),
+        (np.array(1), 3),
+        (np.array([[1, 2], [3, 4]]), (2, 2)),
+        (np.array([[[1], [2]], [[3], [4]]]), (2, 1, 2)),
+        (np.random.default_rng(0).integers(0, 10, (2, 1, 3)), (2, 2, 2)),
+        (np.random.default_rng(1).integers(0, 5, (1, 3, 1, 2)), (2, 1, 3, 1)),
+    ],
+)
+def test_tile(arr, reps):
+    sparse_arr = sparse.COO.from_numpy(arr)
+    expected = np.tile(arr, reps)
+    result = sparse.tile(sparse_arr, reps).todense()
+
+    np.testing.assert_array_equal(result, expected, err_msg=f"Mismatch for shape={arr.shape}, reps={reps}")
diff --git a/sparse/numba_backend/tests/test_namespace.py b/sparse/numba_backend/tests/test_namespace.py
index 5aaa074..40beed8 100644
--- a/sparse/numba_backend/tests/test_namespace.py
+++ b/sparse/numba_backend/tests/test_namespace.py
@@ -156,6 +156,7 @@ def test_namespace():
         "tan",
         "tanh",
         "tensordot",
+        "tile",
         "tril",
         "triu",
         "trunc",