Repository: scipy/scipy

Issue:
BUG: scipy.sparse.csgraph.laplacian raises AttributeError on sparse arrays
### Describe your issue.

Apparently, the `scipy.sparse.csgraph` submodule has not been updated to accept the new scipy sparse arrays (as a modern alternative to scipy sparse matrices).

### Reproducing Code Example

```python
import scipy.sparse as sp
from scipy.sparse.csgraph import laplacian

laplacian(sp.csr_array(np.arange(4) * np.arange(4)[:, np.newaxis]))
```


### Error message

```shell
Traceback (most recent call last):
  Cell In[31], line 1
    laplacian(sp.csr_array(np.arange(4) * np.arange(4)[:, np.newaxis]))
  File ~/mambaforge/envs/dev/lib/python3.11/site-packages/scipy/sparse/csgraph/_laplacian.py:354 in laplacian
    lap, d = create_lap(
  File ~/mambaforge/envs/dev/lib/python3.11/site-packages/scipy/sparse/csgraph/_laplacian.py:459 in _laplacian_sparse
    w = m.sum(axis=axis).getA1() - m.diagonal()
AttributeError: 'numpy.ndarray' object has no attribute 'getA1'
```


### SciPy/NumPy/Python version and system information

<details>

```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'
```

</details>


Discussion:
Note that the docstring of this function is phrased in such a way that we might expect sparse arrays to be accepted.

This issue was discovered when attempting to add support for the new scipy sparse array API to scikit-learn estimators:

- https://github.com/scikit-learn/scikit-learn/pull/27161

The offending line of the traceback above could probably be rewritten as:

```python
w = np.asarray(m.sum(axis=axis)).ravel() - m.diagonal()
```

to accept both sparse matrices and sparse arrays without branching on the container type.
Thanks for the report and proposed fix. Your rewrite looks good to me.

Resolving patch:
diff --git a/scipy/sparse/csgraph/_laplacian.py b/scipy/sparse/csgraph/_laplacian.py
--- a/scipy/sparse/csgraph/_laplacian.py
+++ b/scipy/sparse/csgraph/_laplacian.py
@@ -403,11 +403,11 @@ def _laplacian_sparse_flo(graph, normed, axis, copy, form, dtype, symmetrized):
     if dtype is None:
         dtype = graph.dtype
 
-    graph_sum = graph.sum(axis=axis).getA1()
+    graph_sum = np.asarray(graph.sum(axis=axis)).ravel()
     graph_diagonal = graph.diagonal()
     diag = graph_sum - graph_diagonal
     if symmetrized:
-        graph_sum += graph.sum(axis=1 - axis).getA1()
+        graph_sum += np.asarray(graph.sum(axis=1 - axis)).ravel()
         diag = graph_sum - graph_diagonal - graph_diagonal
 
     if normed:
@@ -456,7 +456,7 @@ def _laplacian_sparse(graph, normed, axis, copy, form, dtype, symmetrized):
     if symmetrized:
         m += m.T.conj()
 
-    w = m.sum(axis=axis).getA1() - m.diagonal()
+    w = np.asarray(m.sum(axis=axis)).ravel() - m.diagonal()
     if normed:
         m = m.tocoo(copy=needs_copy)
         isolated_node_mask = (w == 0)


Test patch:
diff --git a/scipy/sparse/csgraph/tests/test_graph_laplacian.py b/scipy/sparse/csgraph/tests/test_graph_laplacian.py
--- a/scipy/sparse/csgraph/tests/test_graph_laplacian.py
+++ b/scipy/sparse/csgraph/tests/test_graph_laplacian.py
@@ -170,7 +170,9 @@ def _check_laplacian_dtype(
 @pytest.mark.parametrize("dtype", DTYPES)
 @pytest.mark.parametrize("arr_type", [np.array,
                                       sparse.csr_matrix,
-                                      sparse.coo_matrix])
+                                      sparse.coo_matrix,
+                                      sparse.csr_array,
+                                      sparse.coo_array])
 @pytest.mark.parametrize("copy", [True, False])
 @pytest.mark.parametrize("normed", [True, False])
 @pytest.mark.parametrize("use_out_degree", [True, False])
@@ -244,7 +246,11 @@ def test_sparse_formats(fmt, normed, copy):
 
 
 @pytest.mark.parametrize(
-    "arr_type", [np.asarray, sparse.csr_matrix, sparse.coo_matrix]
+    "arr_type", [np.asarray,
+                 sparse.csr_matrix,
+                 sparse.coo_matrix,
+                 sparse.csr_array,
+                 sparse.coo_array]
 )
 @pytest.mark.parametrize("form", ["array", "function", "lo"])
 def test_laplacian_symmetrized(arr_type, form):
@@ -301,7 +307,11 @@ def test_laplacian_symmetrized(arr_type, form):
 
 
 @pytest.mark.parametrize(
-    "arr_type", [np.asarray, sparse.csr_matrix, sparse.coo_matrix]
+    "arr_type", [np.asarray,
+                 sparse.csr_matrix,
+                 sparse.coo_matrix,
+                 sparse.csr_array,
+                 sparse.coo_array]
 )
 @pytest.mark.parametrize("dtype", DTYPES)
 @pytest.mark.parametrize("normed", [True, False])