Repository: netket/netket

Issue:
LocalOperator does not handle well having mixed sparse and dense terms
```python
import netket as nk

L = 4
hi = nk.hilbert.Spin(0.5, 3)

op = nk.operator.spin.sigmax(hi, 0)
mat = op.operators[0].todense()
op2 = nk.operator.LocalOperator(hi, mat, 1)

op3 = op - op2

op3@op3
```

crashes with 
```python
File ~/Dropbox/Ricerca/Codes/Python/netket/netket/operator/_discrete_operator.py:259, in DiscreteOperator.__matmul__(self, other)
    257     return self.apply(other)
    258 elif isinstance(other, AbstractOperator):
--> 259     return self._op__matmul__(other)
    260 else:
    261     return NotImplemented

File ~/Dropbox/Ricerca/Codes/Python/netket/netket/operator/_local_operator/base.py:362, in LocalOperator._op__matmul__(self, other)
    360     return NotImplemented
    361 op = self.copy(dtype=jnp.promote_types(self.dtype, _dtype(other)))
--> 362 return op._op_imatmul_(other)

File ~/Dropbox/Ricerca/Codes/Python/netket/netket/operator/_local_operator/base.py:390, in LocalOperator._op_imatmul_(self, other)
    387 for supp_A_i, A_i in A_op_dict.items():
    388     for supp_B_j, B_j in B_op_dict.items():
    389         self._add_operator(
--> 390             *_multiply_operators(
    391                 self.hilbert, supp_A_i, A_i, supp_B_j, B_j, dtype=self.dtype
    392             )
    393         )
    395 is_hermitian_A = self._is_hermitian
    396 is_hermitian_B = other._is_hermitian

File ~/Dropbox/Ricerca/Codes/Python/netket/netket/operator/_local_operator/helpers.py:243, in _multiply_operators(hilbert, support_A, A, support_B, B, dtype)
    241     support = tuple(np.concatenate([support_A, support_B]))
    242     operator = _kron(A, B)
--> 243     operator, support = _reorder_kronecker_product(hilbert, operator, support)
    244     return tuple(support), operator
    245 else:

File ~/Dropbox/Ricerca/Codes/Python/netket/netket/operator/_local_operator/helpers.py:215, in _reorder_kronecker_product(hi, mat, acting_on)
    213 else:
    214     print(mat)
--> 215     mat_sorted = mat[n_unsorted, :][:, n_unsorted]
    217 return mat_sorted, tuple(acting_on_sorted)

IndexError: index 2 is out of bounds for axis 0 with size 2
```


Discussion:

Resolving patch:
diff --git a/netket/operator/_local_operator/helpers.py b/netket/operator/_local_operator/helpers.py
--- a/netket/operator/_local_operator/helpers.py
+++ b/netket/operator/_local_operator/helpers.py
@@ -49,6 +49,9 @@ def _standardize_matrix_input_type(op):
         return np.asarray(op)
     elif sparse.issparse(op):
         return op.tocoo()
+    elif isinstance(op, np.matrix):
+        # np.matrix does not respect the ndarray interface
+        return np.asarray(op)
     else:
         return op
 
@@ -56,6 +59,10 @@ def _standardize_matrix_input_type(op):
 def _kron(A, B):
     if isinstance(A, spmatrix) and isinstance(B, spmatrix):
         res = sparse.kron(A, B, format="coo")
+    elif isinstance(A, spmatrix) ^ isinstance(B, spmatrix):
+        # XOR, meaning only one is sparse matrix
+        # Then we maintain sparse format (Is this a good idea?)
+        res = sparse.kron(A, B, format="coo")
     else:
         res = np.kron(np.asarray(A), np.asarray(B))
     return res


Test patch:
diff --git a/test/operator/test_local_operator.py b/test/operator/test_local_operator.py
--- a/test/operator/test_local_operator.py
+++ b/test/operator/test_local_operator.py
@@ -548,6 +548,31 @@ def test_duplicate_sites():
         nk.operator.LocalOperator(hi, mat, [0, 0])
 
 
+def test_numpy_matrix():
+    # np.matrix dont respect the API of ndarray. They
+    # must be specially handled
+    hi = nk.hilbert.Spin(0.5, 1)
+    mat = np.matrix([[1, 0], [0, 1]])
+    op = nk.operator.LocalOperator(hi, mat, 0)
+    assert_same_matrices(mat, op)
+
+
+def test_mixed_sparse_dense_terms():
+    # bug #1596
+    # tests for https://github.com/netket/netket/issues/1596
+
+    hi = nk.hilbert.Spin(0.5, 3)
+    op = nk.operator.spin.sigmax(hi, 0)
+    mat = op.operators[0].todense()
+
+    op2 = nk.operator.LocalOperator(hi, mat, 1)
+    op3 = op - op2
+
+    res = op3 @ op3
+    res2 = op3.to_pauli_strings() @ op3.to_pauli_strings()
+    assert_same_matrices(res, res2)
+
+
 def test_pauli_strings_conversion():
     hi = nk.hilbert.Spin(1 / 2, N=5)