Repository: scipy/scipy

Issue:
BUG: upcasting of indices dtype from DIA to COO/CSR/BSR arrays
### Describe your issue.

While I understand the decision of not using downcasting for indices (cf. https://github.com/scipy/scipy/pull/18509), it seems that the conversion from DIA to COO/CSR/BSR matrices will make an upcast from `np.int32` to `np.int64` while it should not be needed. I assume that using the `dtype` of the `offsets` should be enough to be used during the conversion.

### Reproducing Code Example

```python
# %%
import numpy as np
from scipy import sparse

data = np.array([[1, 2, 3, 4]]).repeat(3, axis=0)
offsets = np.array([0, -1, 2], dtype=np.int32)
dia = sparse.dia_array((data, offsets), shape=(4, 4))
dia.__dict__

# %%
coo = dia.tocoo()
assert coo.col.dtype == np.int32, f"Wrong dtype: {coo.col.dtype}"

# %%
csr = dia.tocsr()
assert csr.indices.dtype == np.int32, f"Wrong dtype: {csr.indices.dtype}"

# %%
csc = dia.tocsc()
assert csc.indices.dtype == np.int32, f"Wrong dtype: {csc.indices.dtype}"
```

### Error message

```shell
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
[/Users/glemaitre/Documents/scratch/tmp.py](https://file+.vscode-resource.vscode-cdn.net/Users/glemaitre/Documents/scratch/tmp.py) in line 3
      [10](file:///Users/glemaitre/Documents/scratch/tmp.py?line=9) # %%
      [11](file:///Users/glemaitre/Documents/scratch/tmp.py?line=10) coo = dia.tocoo()
----> [12](file:///Users/glemaitre/Documents/scratch/tmp.py?line=11) assert coo.col.dtype == np.int32, f"Wrong dtype: {coo.col.dtype}"

AssertionError: Wrong dtype: int64

---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
[/Users/glemaitre/Documents/scratch/tmp.py](https://file+.vscode-resource.vscode-cdn.net/Users/glemaitre/Documents/scratch/tmp.py) in line 3
      [14](file:///Users/glemaitre/Documents/scratch/tmp.py?line=13) # %%
      [15](file:///Users/glemaitre/Documents/scratch/tmp.py?line=14) csr = dia.tocsr()
----> [16](file:///Users/glemaitre/Documents/scratch/tmp.py?line=15) assert csr.indices.dtype == np.int32, f"Wrong dtype: {csr.indices.dtype}"

AssertionError: Wrong dtype: int64
```


### SciPy/NumPy/Python version and system information

```shell
SciPy version: '1.11.2'
```

BUG: dia_array(numpy_data).tocoo() uses np.int64 indices while coo_array(numpy_data) uses np.int32 indices
### Describe your issue.

This might be an surprising unintended consequence of #18509.

The `.tocoo()` method of `dia_array` calls `np.arange()` without a `dtype` argument causing the resulting `coo_array` to use `np.int64` indices while creating the same COO array directly from the small numpy array would have used a more memory efficient `np.int32-based` indexing.

This was discovered in https://github.com/scikit-learn/scikit-learn/pull/27240#issuecomment-1719311737 when investigating unexpected test failures when adding sparse array support to scikit-learn.

See the reproducer below:

### Reproducing Code Example

```python
>>> import numpy as np
>>> from scipy.sparse import coo_array, dia_array
>>> from pprint import pprint
>>>
>>> small_array = np.ones(shape=(2, 3))
>>> pprint(dia_array(small_array).__dict__)
{'_shape': (2, 3),
 'data': array([[1., 0., 0.],
       [1., 1., 0.],
       [0., 1., 1.],
       [0., 0., 1.]]),
 'maxprint': 50,
 'offsets': array([-1,  0,  1,  2], dtype=int32)}
>>> pprint(coo_array(small_array).__dict__)
{'_shape': (2, 3),
 'col': array([0, 1, 2, 0, 1, 2], dtype=int32),
 'data': array([1., 1., 1., 1., 1., 1.]),
 'has_canonical_format': True,
 'maxprint': 50,
 'row': array([0, 0, 0, 1, 1, 1], dtype=int32)}

>>> pprint(dia_array(small_array).tocoo().__dict__)
{'_shape': (2, 3),
 'col': array([0, 0, 1, 1, 2, 2]),
 'data': array([1., 1., 1., 1., 1., 1.]),
 'has_canonical_format': False,
 'maxprint': 50,
 'row': array([1, 0, 1, 0, 1, 0])}
```


### Error message

```shell
No error message.
```


### SciPy/NumPy/Python version and system information

```shell
1.11.1 1.25.0 sys.version_info(major=3, minor=11, micro=0, releaselevel='final', serial=0)
Build Dependencies:
  blas:
    detection method: pkgconfig
    found: true
    include directory: /Users/ogrisel/mambaforge/envs/dev/include
    lib directory: /Users/ogrisel/mambaforge/envs/dev/lib
    name: blas
    openblas configuration: unknown
    pc file directory: /Users/ogrisel/mambaforge/envs/dev/lib/pkgconfig
    version: 3.9.0
  lapack:
    detection method: pkgconfig
    found: true
    include directory: /Users/ogrisel/mambaforge/envs/dev/include
    lib directory: /Users/ogrisel/mambaforge/envs/dev/lib
    name: lapack
    openblas configuration: unknown
    pc file directory: /Users/ogrisel/mambaforge/envs/dev/lib/pkgconfig
    version: 3.9.0
  pybind11:
    detection method: pkgconfig
    include directory: /Users/ogrisel/mambaforge/envs/dev/include
    name: pybind11
    version: 2.10.4
Compilers:
  c:
    commands: arm64-apple-darwin20.0.0-clang
    linker: ld64
    name: clang
    version: 15.0.7
  c++:
    commands: arm64-apple-darwin20.0.0-clang++
    linker: ld64
    name: clang
    version: 15.0.7
  cython:
    commands: cython
    linker: cython
    name: cython
    version: 0.29.35
  fortran:
    commands: /Users/runner/miniforge3/conda-bld/scipy-split_1687995684757/_build_env/bin/arm64-apple-darwin20.0.0-gfortran
    linker: ld64
    name: gcc
    version: 12.2.0
  pythran:
    include directory: ../../../_build_env/venv/lib/python3.11/site-packages/pythran
    version: 0.13.1
Machine Information:
  build:
    cpu: x86_64
    endian: little
    family: x86_64
    system: darwin
  cross-compiled: true
  host:
    cpu: arm64
    endian: little
    family: aarch64
    system: darwin
Python Information:
  path: /Users/ogrisel/mambaforge/envs/dev/bin/python
  version: '3.11'
```



Discussion:
I believe something like
```diff
diff --git a/scipy/sparse/_dia.py b/scipy/sparse/_dia.py
index 20c22f2ba..1ae9c3978 100644
--- a/scipy/sparse/_dia.py
+++ b/scipy/sparse/_dia.py
@@ -332,6 +332,9 @@ class _dia_base(_data_matrix):
         mask &= (self.data != 0)
         row = row[mask]
         col = np.tile(offset_inds, num_offsets)[mask.ravel()]
+        idx_dtype = self._get_index_dtype(arrays=(self.offsets,), maxval=max(self.shape))
+        row = row.astype(idx_dtype, copy=False)
+        col = col.astype(idx_dtype, copy=False)
         data = self.data[mask]
         # Note: this cannot set has_canonical_format=True, because despite the
         # lack of duplicates, we do not generate sorted indices.
```

would fix the issue
this is the same issue as #19246 
I also checked that this problem still happens with scipy 1.11.2.
Sorry for the duplicate report. Same as #19245.

Resolving patch:
diff --git a/scipy/sparse/_dia.py b/scipy/sparse/_dia.py
--- a/scipy/sparse/_dia.py
+++ b/scipy/sparse/_dia.py
@@ -87,7 +87,7 @@ def __init__(self, arg1, shape=None, dtype=None, copy=False):
             raise ValueError('offset array contains duplicate values')
 
     def __repr__(self):
-        format = _formats[self.getformat()][1]
+        format = _formats[self.format][1]
         return "<%dx%d sparse matrix of type '%s'\n" \
                "\twith %d stored elements (%d diagonals) in %s format>" % \
                (self.shape + (self.dtype.type, self.nnz, self.data.shape[0],
@@ -332,6 +332,11 @@ def tocoo(self, copy=False):
         mask &= (self.data != 0)
         row = row[mask]
         col = np.tile(offset_inds, num_offsets)[mask.ravel()]
+        idx_dtype = self._get_index_dtype(
+            arrays=(self.offsets,), maxval=max(self.shape)
+        )
+        row = row.astype(idx_dtype, copy=False)
+        col = col.astype(idx_dtype, copy=False)
         data = self.data[mask]
         # Note: this cannot set has_canonical_format=True, because despite the
         # lack of duplicates, we do not generate sorted indices.


Test patch:
diff --git a/scipy/sparse/tests/test_base.py b/scipy/sparse/tests/test_base.py
--- a/scipy/sparse/tests/test_base.py
+++ b/scipy/sparse/tests/test_base.py
@@ -4462,6 +4462,19 @@ def test_tocoo_gh10050(self):
         inds_are_sorted = np.all(np.diff(flat_inds) > 0)
         assert m.has_canonical_format == inds_are_sorted
 
+    def test_tocoo_tocsr_tocsc_gh19245(self):
+        # test index_dtype with tocoo, tocsr, tocsc
+        data = np.array([[1, 2, 3, 4]]).repeat(3, axis=0)
+        offsets = np.array([0, -1, 2], dtype=np.int32)
+        dia = sparse.dia_array((data, offsets), shape=(4, 4))
+
+        coo = dia.tocoo()
+        assert coo.col.dtype == np.int32
+        csr = dia.tocsr()
+        assert csr.indices.dtype == np.int32
+        csc = dia.tocsc()
+        assert csc.indices.dtype == np.int32
+
 
 TestDIA.init_class()
 
@@ -4915,6 +4928,7 @@ def cases_64bit():
         'test_large_dimensions_reshape': 'test actually requires 64-bit to work',
         'test_constructor_smallcol': 'test verifies int32 indexes',
         'test_constructor_largecol': 'test verifies int64 indexes',
+        'test_tocoo_tocsr_tocsc_gh19245': 'test verifies int32 indexes',
     }
 
     for cls in TEST_CLASSES: