Repository: PennyLaneAI/pennylane

Issue:
[BUG] `SProd` matrix with tensorflow does not cast types properly
### Expected behavior

The matrix function should type promote the matrix to either complex64 or complex128, instead of casting the coefficient to an integer.

```
<tf.Tensor: shape=(2, 2), dtype=complex128, numpy=
array([[0. +0.j, 0.1+0.j],
       [0.1+0.j, 0. +0.j]])>
```

### Actual behavior

```
>>> op = tf.Variable(0.1) * qml.PauliX(0)
>>> op.matrix()
<tf.Tensor: shape=(2, 2), dtype=int64, numpy=
array([[0, 0],
       [0, 0]])>
```

### Additional information

See PR #4169  @timmysilv 

We can quickly get a fix in for this release, but we should start thinking more about the root of this problem.

How do we handle type casting with tensorflow?

Should we restrict matrices to always be `complex128`? Or is it ok for matrices to be `int64` or `float64`? Other interfaces handle this type promotion implicitly, but that doesn't mean it won't sometimes cause problems.

### Source code

_No response_

### Tracebacks

_No response_

### System information

```shell
master
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.


Discussion:

Resolving patch:
diff --git a/pennylane/ops/op_math/sprod.py b/pennylane/ops/op_math/sprod.py
--- a/pennylane/ops/op_math/sprod.py
+++ b/pennylane/ops/op_math/sprod.py
@@ -254,9 +254,6 @@ def has_matrix(self):
 
     @staticmethod
     def _matrix(scalar, mat):
-        if qml.math.get_interface(scalar) == "tensorflow":
-            # we must cast ``scalar`` to complex to avoid an error
-            scalar = qml.math.cast_like(scalar, mat)
         return scalar * mat
 
     @property
diff --git a/pennylane/ops/op_math/symbolicop.py b/pennylane/ops/op_math/symbolicop.py
--- a/pennylane/ops/op_math/symbolicop.py
+++ b/pennylane/ops/op_math/symbolicop.py
@@ -258,27 +258,30 @@ def matrix(self, wire_order=None):
             base_matrix = self.base.matrix()
 
         scalar_interface = qml.math.get_interface(self.scalar)
+        scalar = self.scalar
         if scalar_interface == "torch":
             # otherwise get `RuntimeError: Can't call numpy() on Tensor that requires grad.`
             base_matrix = qml.math.convert_like(base_matrix, self.scalar)
-        elif scalar_interface == "tensorflow" and not qml.math.iscomplex(self.scalar):
-            # cast scalar to complex to avoid an error
-            self.scalar = qml.math.cast_like(self.scalar, base_matrix)
+        elif scalar_interface == "tensorflow":
+            # just cast everything to complex128. Otherwise we may have casting problems
+            # where things get truncated like in SProd(tf.Variable(0.1), qml.PauliX(0))
+            scalar = qml.math.cast(scalar, "complex128")
+            base_matrix = qml.math.cast(base_matrix, "complex128")
 
         # compute scalar operation on base matrix taking batching into account
-        scalar_size = qml.math.size(self.scalar)
+        scalar_size = qml.math.size(scalar)
         if scalar_size != 1:
             if scalar_size == self.base.batch_size:
                 # both base and scalar are broadcasted
-                mat = qml.math.stack([self._matrix(s, m) for s, m in zip(self.scalar, base_matrix)])
+                mat = qml.math.stack([self._matrix(s, m) for s, m in zip(scalar, base_matrix)])
             else:
                 # only scalar is broadcasted
-                mat = qml.math.stack([self._matrix(s, base_matrix) for s in self.scalar])
+                mat = qml.math.stack([self._matrix(s, base_matrix) for s in scalar])
         elif self.base.batch_size is not None:
             # only base is broadcasted
-            mat = qml.math.stack([self._matrix(self.scalar, ar2) for ar2 in base_matrix])
+            mat = qml.math.stack([self._matrix(scalar, ar2) for ar2 in base_matrix])
         else:
             # none are broadcasted
-            mat = self._matrix(self.scalar, base_matrix)
+            mat = self._matrix(scalar, base_matrix)
 
         return qml.math.expand_matrix(mat, wires=self.wires, wire_order=wire_order)


Test patch:
diff --git a/tests/ops/op_math/test_sprod.py b/tests/ops/op_math/test_sprod.py
--- a/tests/ops/op_math/test_sprod.py
+++ b/tests/ops/op_math/test_sprod.py
@@ -492,6 +492,32 @@ def test_sprod_tf(self):
         assert mat.dtype == true_mat.dtype
         assert np.allclose(mat, true_mat)
 
+    @pytest.mark.tf
+    def test_tf_matrix_type_casting(self):
+        """Test that types for the matrix are always converted to complex128 and parameters aren't truncated."""
+        import tensorflow as tf
+
+        coeff = tf.Variable(0.1)
+        op = qml.PauliX(0)
+
+        sprod_op = SProd(coeff, op)
+        mat = sprod_op.matrix()
+
+        assert mat.dtype == tf.complex128
+        expected = np.array([[0, 0.1], [0.1, 0.0]], dtype="complex128")
+        assert qml.math.allclose(mat, expected)
+        assert sprod_op.data[0].dtype == coeff.dtype  # coeff not modified by calling the matrix
+
+        op = qml.PauliY(0)
+
+        sprod_op = SProd(coeff, op)
+        mat = sprod_op.matrix()
+
+        assert mat.dtype == tf.complex128
+        expected = np.array([[0, -0.1j], [0.1j, 0.0]], dtype="complex128")
+        assert qml.math.allclose(mat, expected)
+        assert sprod_op.data[0].dtype == coeff.dtype  # coeff not modified by calling the matrix
+
 
 class TestSparseMatrix:
     sparse_ops = (