Repository: scipy/scipy

Issue:
BUG: A sparse matrix raises a ValueError when `__rmul__` with a value as `None`
### Describe your issue.

A sparse matrix multiplied with a value of an unsupported operand type raises TypeError. However, in SciPy 1.8.0rc1, a sparse matrix raises ValueError when it is called `__rmul__` with a value that is not interpretable as an array,  e.g. `None`. It should raise TypeError instead as `__mul__` does.


### Reproducing Code Example

```python
import numpy as np
import scipy.sparse as sp
m = np.eye(3)
m = sp.coo_matrix(m)
None * m
```


### Error message

```shell
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/home/ext-mtakagi/.pyenv/versions/nvidia/lib/python3.9/site-packages/scipy/sparse/_base.py", line 610, in __rmul__
    return self._rmul_dispatch(other)
  File "/home/ext-mtakagi/.pyenv/versions/nvidia/lib/python3.9/site-packages/scipy/sparse/_base.py", line 607, in _rmul_dispatch
    return (self.transpose() @ tr).transpose()
  File "/home/ext-mtakagi/.pyenv/versions/nvidia/lib/python3.9/site-packages/scipy/sparse/_base.py", line 618, in __matmul__
    raise ValueError("Scalar operands are not allowed, "
ValueError: Scalar operands are not allowed, use '*' instead
```


### SciPy/NumPy/Python version information

1.8.0rc1 1.22.0rc1 sys.version_info(major=3, minor=9, micro=7, releaselevel='final', serial=0)


Discussion:
Thanks for reporting this bug, @takagi.   You are correct, that should be a `TypeError`.
I'm adding new issues that should be fixed for 1.8.0rc2 (or 1.8.0 final) to the 1.8.0 milestone.  If a different procedure is preferred, let me know.
xref: https://github.com/cupy/cupy/issues/6233
Thanks, it is definitely fine for us.
The problem is that `None` can be coerced into an array:

```
In [2]: np.array(None)
Out[2]: array(None, dtype=object)
```

I can add a specific check for None, but it sounds like you want a wider class of objects excluded?

I you *only* want None to be excluded, I can easily add that back in with a special check.
Here's the offending code from `_base.py`:

```python
        # If it's a list or whatever, treat it like a matrix
        other_a = np.asanyarray(other)
```
How about reverting this line https://github.com/scipy/scipy/pull/14822/files#diff-8aed1ba485f8d01bc74679865b6ca8fa04efde8b6fef5a57f587b61eb7a8734cR607 to

```python
return (self.transpose() * tr).transpose()
```
or
```python
return self.transpose()._mul_dispatch(tr).transpose()
```

We cannot revert that line, or it will break for arrays. I think by the time we get to the point where we think `array(None, dtype=object)` is another array we should interact with, we're in trouble already. I think this goes back to an issue with NumPy where it is a bit over-eager in converting objects to arrays.  @seberg Thoughts?


I got
```
>>> np.eye(2) * sparse.csr_array(np.arange(6).reshape(2, 3))
array([[0., 1., 2.],
       [3., 4., 5.]])
>>> np.__version__, scipy.__version__
('1.22.0rc2', '1.8.0rc1')
```
but I expect `dense * sparse` computes
- elementwise mul if both operands are "array"
- matmul if any operand is a "matrix"

I think the problem is that `isscalar` is not just not very useful.  You can't know that the result `asarray` result won't be 0-D, and if it is you have to go into the normal multiply path.  (There might be more subtleties, in that `None * m` used to fail earlier and not try to go to object arrays, but that seems OK?)
So, you can't commit to using `@` quite there, calling the `_mul_dispatcher` may well be reasonable.  By that time, the other object got a chance to deal with it, so converting it to a dense array should not be terrible (but that could be an object array of course).
> but I expect `dense * sparse` computes
> 
> * elementwise mul if both operands are "array"
> * matmul if any operand is a "matrix"

This is a bug, for sure!
OK, figured it out: rmul is missing from the array interface :roll_eyes: 

Resolving patch:
diff --git a/scipy/sparse/_arrays.py b/scipy/sparse/_arrays.py
--- a/scipy/sparse/_arrays.py
+++ b/scipy/sparse/_arrays.py
@@ -46,6 +46,10 @@ def _lil_container(self):
     def __mul__(self, *args, **kwargs):
         return self.multiply(*args, **kwargs)
 
+    def __rmul__(self, *args, **kwargs):
+        return self.multiply(*args, **kwargs)
+
+
 
 def _matrix_doc_to_array(docstr):
     # For opimized builds with stripped docstrings


Test patch:
diff --git a/scipy/sparse/tests/test_array_api.py b/scipy/sparse/tests/test_array_api.py
--- a/scipy/sparse/tests/test_array_api.py
+++ b/scipy/sparse/tests/test_array_api.py
@@ -103,6 +103,19 @@ def test_elementwise_mul(A):
     assert np.all((A * A).todense() == A.power(2).todense())
 
 
+@parametrize_sparrays
+def test_elementwise_rmul(A):
+    with pytest.raises(TypeError):
+        None * A
+
+    with pytest.raises(ValueError):
+        np.eye(3) * scipy.sparse.csr_array(np.arange(6).reshape(2, 3))
+
+    assert np.all((2 * A) == (A.todense() * 2))
+
+    assert np.all((A.todense() * A) == (A.todense() ** 2))
+
+
 @parametrize_sparrays
 def test_matmul(A):
     assert np.all((A @ A.T).todense() == A.dot(A.T).todense())