Repository: PennyLaneAI/pennylane
Pull request #4989 (2024-01-04 12:11:50)

[Pauli1] Add scalar product to PauliWord and PauliSentence
Adding a scalar product to PauliWord and PauliSentence.
This includes moving `*` $\Rightarrow$ `@` with legacy support for the case of multiplying `pw1 * pw2`

This also introduces a deprecation cycle for `pw1 * pw2` type operations

see also [Pauli1](https://github.com/PennyLaneAI/pennylane/pull/4989), [Pauli2](https://github.com/PennyLaneAI/pennylane/pull/5001), [Pauli3](https://github.com/PennyLaneAI/pennylane/pull/5003), [Pauli4](https://github.com/PennyLaneAI/pennylane/pull/5017), [Pauli5](https://github.com/PennyLaneAI/pennylane/pull/5018)

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4989?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: `1 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`7242786`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/7242786664deb97a82d48ad62a4c55492f94a176?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.67% compared to head [(`22ce26c`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4989?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.66%.

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4989?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/pauli/pauli\_arithmetic.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4989?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3BhdWxpL3BhdWxpX2FyaXRobWV0aWMucHk=) | 94.11% | [1 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4989?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4989      +/-   ##
==========================================
- Coverage   99.67%   99.66%   -0.02%     
==========================================
  Files         392      392              
  Lines       35575    35321     -254     
==========================================
- Hits        35461    35203     -258     
- Misses        114      118       +4     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4989?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

Qottmann: [sc-53279]
Qottmann: I just added a changelog entry summarizing the changes in [Pauli1](https://github.com/PennyLaneAI/pennylane/pull/4989), [Pauli2](https://github.com/PennyLaneAI/pennylane/pull/5001), [Pauli3](https://github.com/PennyLaneAI/pennylane/pull/5003), [Pauli4](https://github.com/PennyLaneAI/pennylane/pull/5017) in the latter (https://github.com/PennyLaneAI/pennylane/pull/5017)

Patch:
diff --git a/doc/development/deprecations.rst b/doc/development/deprecations.rst
index bdb86df5c..0ded796dd 100644
--- a/doc/development/deprecations.rst
+++ b/doc/development/deprecations.rst
@@ -105,6 +105,12 @@ Pending deprecations
   - Deprecated in v0.34
   - Will be removed in v0.35
 
+* ``PauliWord`` and ``PauliSentence`` no longer use ``*`` for matrix and tensor products,
+  but instead use ``@`` to conform with the PennyLane convention.
+
+  - Deprecated in v0.35
+  - Will be removed in v0.36
+
 Completed deprecation cycles
 ----------------------------
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 1c766190c..110e7ac85 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -9,6 +9,9 @@
 * Update `tests/ops/functions/conftest.py` to ensure all operator types are tested for validity.
   [(#4978)](https://github.com/PennyLaneAI/pennylane/pull/4978)
 
+* Upgrade Pauli arithmetic with multiplying by scalars, e.g. `0.5 * PauliWord({0:"X"})` or `0.5 * PauliSentence({PauliWord({0:"X"}): 1.})`.
+  [(#4989)](https://github.com/PennyLaneAI/pennylane/pull/4989)
+  
 * A new `pennylane.workflow` module is added. This module now contains `qnode.py`, `execution.py`, `set_shots.py`, `jacobian_products.py`, and the submodule `interfaces`.
 
 * Composite operations (eg. those made with `qml.prod` and `qml.sum`) convert `Hamiltonian` and
@@ -25,6 +28,9 @@
 
 <h3>Deprecations 👋</h3>
 
+* Matrix and tensor products between `PauliWord` and `PauliSentence` instances are done using the `@` operator, `*` will be used only for scalar multiplication.
+  [(#4989)](https://github.com/PennyLaneAI/pennylane/pull/4989)
+
 <h3>Documentation 📝</h3>
 
 * A typo in a code example in the `qml.transforms` API has been fixed.
@@ -42,4 +48,5 @@ This release contains contributions from (in alphabetical order):
 Abhishek Abhishek,
 Christina Lee,
 Isaac De Vlugt,
+Korbinian Kottmann,
 Matthew Silverman.
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 16c65a686..f1de0cfcc 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -12,6 +12,7 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """The Pauli arithmetic abstract reduced representation classes"""
+import warnings
 from copy import copy
 from functools import reduce, lru_cache
 from typing import Iterable
@@ -21,6 +22,7 @@ from scipy import sparse
 
 import pennylane as qml
 from pennylane import math
+from pennylane.typing import TensorLike
 from pennylane.wires import Wires
 from pennylane.operation import Tensor
 from pennylane.ops import Hamiltonian, Identity, PauliX, PauliY, PauliZ, prod, s_prod
@@ -138,6 +140,10 @@ class PauliWord(dict):
     X(a) @ Y(2) @ Z(3)
     """
 
+    # this allows scalar multiplication from left with numpy arrays np.array(0.5) * pw1
+    # taken from [stackexchange](https://stackoverflow.com/questions/40694380/forcing-multiplication-to-use-rmul-instead-of-numpy-array-mul-or-byp/44634634#44634634)
+    __array_priority__ = 1000
+
     def __missing__(self, key):
         """If the wire is not in the Pauli word,
         then no operator acts on it, so return the Identity."""
@@ -177,7 +183,7 @@ class PauliWord(dict):
     def __hash__(self):
         return hash(frozenset(self.items()))
 
-    def __mul__(self, other):
+    def __matmul__(self, other):
         """Multiply two Pauli words together using the matrix product if wires overlap
         and the tensor product otherwise.
 
@@ -209,6 +215,44 @@ class PauliWord(dict):
 
         return PauliWord(result), coeff
 
+    def __mul__(self, other):
+        """Multiply a PauliWord by a scalar
+
+        Args:
+            other (Scalar): The scalar to multiply the PauliWord with
+
+        Returns:
+            PauliSentence
+        """
+        if isinstance(other, PauliWord):
+            # this is legacy support and will be removed after a deprecation cycle
+            warnings.warn(
+                "Matrix/Tensor multiplication using the * operator on PauliWords and PauliSentences is deprecated, use @ instead.",
+                qml.PennyLaneDeprecationWarning,
+            )
+            return self @ other
+
+        if isinstance(other, TensorLike):
+            if not qml.math.ndim(other) == 0:
+                raise ValueError(
+                    f"Attempting to multiply a PauliWord with an array of dimension {qml.math.ndim(other)}"
+                )
+
+            return PauliSentence({self: other})
+        raise TypeError(
+            f"PauliWord can only be multiplied by numerical data. Attempting to multiply by {other} of type {type(other)}"
+        )
+
+    __rmul__ = __mul__
+
+    def __truediv__(self, other):
+        """Divide a PauliWord by a scalar"""
+        if isinstance(other, TensorLike):
+            return self * (1 / other)
+        raise TypeError(
+            f"PauliWord can only be divided by numerical data. Attempting to divide by {other} of type {type(other)}"
+        )
+
     def __str__(self):
         """String representation of a PauliWord."""
         if len(self) == 0:
@@ -369,6 +413,10 @@ class PauliSentence(dict):
     + (-0-0.45j) * Z(2) @ Y(0)
     """
 
+    # this allows scalar multiplication from left with numpy arrays np.array(0.5) * ps1
+    # taken from [stackexchange](https://stackoverflow.com/questions/40694380/forcing-multiplication-to-use-rmul-instead-of-numpy-array-mul-or-byp/44634634#44634634)
+    __array_priority__ = 1000
+
     def __missing__(self, key):
         """If the PauliWord is not in the sentence then the coefficient
         associated with it should be 0."""
@@ -410,13 +458,9 @@ class PauliSentence(dict):
         memo[id(self)] = res
         return res
 
-    def __mul__(self, other):
-        """Multiply two Pauli sentences by iterating over each sentence and multiplying
-        the Pauli words pair-wise.
-
-        Empty Pauli sentences are treated as 0. The product returns another
-        empty Pauli sentence (i.e 0).
-        """
+    def __matmul__(self, other):
+        """Matrix / tensor product between two PauliSentences by iterating over each sentence and multiplying
+        the Pauli words pair-wise"""
         final_ps = PauliSentence()
 
         if len(self) == 0 or len(other) == 0:
@@ -424,11 +468,50 @@ class PauliSentence(dict):
 
         for pw1 in self:
             for pw2 in other:
-                prod_pw, coeff = pw1 * pw2
+                prod_pw, coeff = pw1 @ pw2
                 final_ps[prod_pw] = final_ps[prod_pw] + coeff * self[pw1] * other[pw2]
 
         return final_ps
 
+    def __mul__(self, other):
+        """Multiply a PauliWord by a scalar
+
+        Args:
+            other (Scalar): The scalar to multiply the PauliWord with
+
+        Returns:
+            PauliSentence
+        """
+        if isinstance(other, PauliSentence):
+            # this is legacy support and will be removed after a deprecation cycle
+            warnings.warn(
+                "Matrix/Tensor multiplication using the * operator on PauliWords and PauliSentences is deprecated, use @ instead.",
+                qml.PennyLaneDeprecationWarning,
+            )
+            return self @ other
+
+        if isinstance(other, TensorLike):
+            if not qml.math.ndim(other) == 0:
+                raise ValueError(
+                    f"Attempting to multiply a PauliSentence with an array of dimension {qml.math.ndim(other)}"
+                )
+
+            return PauliSentence({key: other * value for key, value in self.items()})
+
+        raise TypeError(
+            f"PauliSentence can only be multiplied by numerical data. Attempting to multiply by {other} of type {type(other)}"
+        )
+
+    __rmul__ = __mul__
+
+    def __truediv__(self, other):
+        """Divide a PauliSentence by a scalar"""
+        if isinstance(other, TensorLike):
+            return self * (1 / other)
+        raise TypeError(
+            f"PauliSentence can only be divided by numerical data. Attempting to divide by {other} of type {type(other)}"
+        )
+
     def __str__(self):
         """String representation of the PauliSentence."""
         if len(self) == 0:


Test patch:
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index 23f2a9500..c09652d12 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -39,6 +39,8 @@ pw2 = PauliWord({"a": X, "b": X, "c": Z})
 pw3 = PauliWord({0: Z, "b": Z, "c": Z})
 pw4 = PauliWord({})
 
+words = [pw1, pw2, pw3, pw4]
+
 ps1 = PauliSentence({pw1: 1.23, pw2: 4j, pw3: -0.5})
 ps2 = PauliSentence({pw1: -1.23, pw2: -4j, pw3: 0.5})
 ps1_hamiltonian = PauliSentence({pw1: 1.23, pw2: 4, pw3: -0.5})
@@ -47,6 +49,75 @@ ps3 = PauliSentence({pw3: -0.5, pw4: 1})
 ps4 = PauliSentence({pw4: 1})
 ps5 = PauliSentence({})
 
+sentences = [ps1, ps2, ps3, ps4, ps5, ps1_hamiltonian, ps2_hamiltonian]
+
+
+class TestDeprecations:
+    def test_deprecation_warning_PauliWord(
+        self,
+    ):
+        """Test that a PennyLaneDeprecationWarning is raised when using * for matrix multiplication of two PauliWords"""
+        pauli1 = PauliWord({0: "X"})
+        pauli2 = PauliWord({0: "Y"})
+
+        with pytest.warns(
+            qml.PennyLaneDeprecationWarning, match="Matrix/Tensor multiplication using"
+        ):
+            _ = pauli1 * pauli2
+
+    def test_deprecation_warning_PauliSentence(
+        self,
+    ):
+        """Test that a PennyLaneDeprecationWarning is raised when using * for matrix multiplication of two PauliSentences"""
+        pauli1 = PauliSentence({PauliWord({0: "X"}): 1})
+        pauli2 = PauliSentence({PauliWord({0: "Y"}): 1})
+
+        with pytest.warns(
+            qml.PennyLaneDeprecationWarning, match="Matrix/Tensor multiplication using"
+        ):
+            _ = pauli1 * pauli2
+
+
+@pytest.mark.parametrize("pauli1", words)
+@pytest.mark.parametrize("pauli2", words)
+def test_legacy_multiplication_pwords(pauli1, pauli2):
+    """Test the legacy behavior for using the star operator for matrix multiplication of pauli words"""
+    res1, coeff1 = pauli1 * pauli2
+    res2, coeff2 = pauli1 @ pauli2
+    assert res1 == res2
+    assert coeff1 == coeff2
+
+
+@pytest.mark.parametrize("pauli1", sentences)
+@pytest.mark.parametrize("pauli2", sentences)
+def test_legacy_multiplication_psentences(pauli1, pauli2):
+    """Test the legacy behavior for using the star operator for matrix multiplication of pauli sentences"""
+    assert pauli1 * pauli2 == pauli1 @ pauli2
+
+
+def test_legacy_pw_pw_multiplication_non_commutativity():
+    """Test that legacy pauli word matrix multiplication is non-commutative and returns correct result"""
+    pauliX = PauliWord({0: "X"})
+    pauliY = PauliWord({0: "Y"})
+    pauliZ = PauliWord({0: "Z"})
+
+    res1 = pauliX * pauliY
+    res2 = pauliY * pauliX
+    assert res1 == (pauliZ, 1j)
+    assert res2 == (pauliZ, -1j)
+
+
+def test_legacy_ps_ps_multiplication_non_commutativity():
+    """Test that legacy pauli sentence matrix multiplication is non-commutative and returns correct result"""
+    pauliX = PauliSentence({PauliWord({0: "X"}): 1.0})
+    pauliY = PauliSentence({PauliWord({0: "Y"}): 1.0})
+    pauliZ = PauliSentence({PauliWord({0: "Z"}): 1j})
+
+    res1 = pauliX * pauliY
+    res2 = pauliY * pauliX
+    assert res1 == pauliZ
+    assert res2 == -1 * pauliZ
+
 
 class TestPauliWord:
     def test_identity_removed_on_init(self):
@@ -114,22 +185,62 @@ class TestPauliWord:
         assert str(pw) == str_rep
         assert repr(pw) == str_rep
 
-    tup_pws_mult = (
+    tup_pws_matmult = (
         (pw1, pw1, PauliWord({}), 1.0),  # identities are automatically removed !
         (pw1, pw3, PauliWord({0: Z, 1: X, 2: Y, "b": Z, "c": Z}), 1.0),
         (pw2, pw3, PauliWord({"a": X, "b": Y, 0: Z}), -1.0j),
         (pw3, pw4, pw3, 1.0),
     )
 
-    @pytest.mark.parametrize("word1, word2, result_pw, coeff", tup_pws_mult)
-    def test_mul(self, word1, word2, result_pw, coeff):
+    @pytest.mark.parametrize("word1, word2, result_pw, coeff", tup_pws_matmult)
+    def test_matmul(self, word1, word2, result_pw, coeff):
         copy_pw1 = copy(word1)
         copy_pw2 = copy(word2)
 
-        assert word1 * word2 == (result_pw, coeff)
+        assert word1 @ word2 == (result_pw, coeff)
         assert copy_pw1 == word1  # check for mutation of the pw themselves
         assert copy_pw2 == word2
 
+    @pytest.mark.parametrize("pw", words)
+    @pytest.mark.parametrize("scalar", [0.0, 0.5, 1, 1j, 0.5j + 1.0])
+    def test_mul(self, pw, scalar):
+        """Test scalar multiplication"""
+        res1 = scalar * pw
+        res2 = pw * scalar
+        assert isinstance(res1, PauliSentence)
+        assert list(res1.values()) == [scalar]
+        assert isinstance(res2, PauliSentence)
+        assert list(res2.values()) == [scalar]
+
+    @pytest.mark.parametrize("pw", words)
+    @pytest.mark.parametrize("scalar", [0.5, 1, 1j, 0.5j + 1.0])
+    def test_truediv(self, pw, scalar):
+        """Test scalar multiplication"""
+        res1 = pw / scalar
+        assert isinstance(res1, PauliSentence)
+        assert list(res1.values()) == [1 / scalar]
+
+    @pytest.mark.parametrize("pw", words)
+    def test_raise_error_for_non_scalar(self, pw):
+        """Test that the correct error is raised when attempting to multiply a PauliWord by a sclar"""
+        with pytest.raises(ValueError, match="Attempting to multiply"):
+            _ = [0.5] * pw
+
+    def test_mul_raise_not_implemented_non_numerical_data_recursive(self):
+        """Test that TypeError is raised when trying to multiply by non-numerical data"""
+        with pytest.raises(TypeError, match="PauliWord can only"):
+            _ = "0.5" * pw1
+
+    def test_mul_raise_not_implemented_non_numerical_data(self):
+        """Test that TypeError is raised when trying to multiply by non-numerical data"""
+        with pytest.raises(TypeError, match="PauliWord can only"):
+            _ = pw1 * "0.5"
+
+    def test_truediv_raise_not_implemented_non_numerical_data(self):
+        """Test that TypeError is raised when trying to divide by non-numerical data"""
+        with pytest.raises(TypeError, match="PauliWord can only be"):
+            _ = pw1 / "0.5"
+
     tup_pws_mat_wire = (
         (pw1, [2, 0, 1], np.kron(np.kron(matY, matI), matX)),
         (pw1, [2, 1, 0], np.kron(np.kron(matY, matX), matI)),
@@ -372,18 +483,48 @@ class TestPauliSentence:
         ),
     )
 
-    @pytest.mark.parametrize("string1, string2, res", tup_ps_mult)
-    def test_mul(self, string1, string2, res):
-        """Test that the correct result of multiplication is produced."""
-        copy_ps1 = copy(string1)
-        copy_ps2 = copy(string2)
+    @pytest.mark.parametrize("pauli1, pauli2, res", tup_ps_mult)
+    def test_matmul(self, pauli1, pauli2, res):
+        """Test that the correct result of matrix multiplication is produced."""
+        copy_ps1 = copy(pauli1)
+        copy_ps2 = copy(pauli2)
 
-        simplified_product = string1 * string2
+        simplified_product = pauli1 @ pauli2
         simplified_product.simplify()
 
         assert simplified_product == res
-        assert string1 == copy_ps1
-        assert string2 == copy_ps2
+        assert pauli1 == copy_ps1
+        assert pauli2 == copy_ps2
+
+    @pytest.mark.parametrize("ps", sentences)
+    @pytest.mark.parametrize("scalar", [0.0, 0.5, 1, 1j, 0.5j + 1.0])
+    def test_mul(self, ps, scalar):
+        """Test scalar multiplication"""
+        res1 = scalar * ps
+        res2 = ps * scalar
+        assert list(res1.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res2.values()) == [scalar * coeff for coeff in ps.values()]
+
+    def test_mul_raise_not_implemented_non_numerical_data_recursive(self):
+        """Test that TypeError is raised when trying to multiply by non-numerical data"""
+        with pytest.raises(TypeError, match="PauliSentence can only"):
+            _ = "0.5" * ps1
+
+    def test_mul_raise_not_implemented_non_numerical_data(self):
+        """Test that TypeError is raised when trying to multiply by non-numerical data"""
+        with pytest.raises(TypeError, match="PauliSentence can only"):
+            _ = ps1 * "0.5"
+
+    def test_truediv_raise_not_implemented_non_numerical_data(self):
+        """Test that TypeError is raised when trying to divide by non-numerical data"""
+        with pytest.raises(TypeError, match="PauliSentence can only"):
+            _ = ps1 / "0.5"
+
+    @pytest.mark.parametrize("ps", sentences)
+    def test_raise_error_for_non_scalar(self, ps):
+        """Test that the correct error is raised when attempting to multiply a PauliSentence by a sclar"""
+        with pytest.raises(ValueError, match="Attempting to multiply"):
+            _ = [0.5] * ps
 
     tup_ps_add = (  # computed by hand
         (ps1, ps1, PauliSentence({pw1: 2.46, pw2: 8j, pw3: -1})),
@@ -687,3 +828,126 @@ class TestPauliSentence:
                 PauliWord({0: Z, 2: Z, 3: Z}): -0.5,
             }
         )
+
+
+@pytest.mark.all_interfaces
+class TestPauliArithmeticWithADInterfaces:
+    """Test pauli arithmetic with different automatic differentiation interfaces"""
+
+    @pytest.mark.torch
+    @pytest.mark.parametrize("scalar", [0.0, 0.5, 1, 1j, 0.5j + 1.0])
+    def test_torch_initialization(self, scalar):
+        """Test initializing PauliSentence from torch tensor"""
+        import torch
+
+        tensor = scalar * torch.ones(4)
+        res = PauliSentence(dict(zip(words, tensor)))
+        assert all(isinstance(val, torch.Tensor) for val in res.values())
+
+    @pytest.mark.autograd
+    @pytest.mark.parametrize("scalar", [0.0, 0.5, 1, 1j, 0.5j + 1.0])
+    def test_autograd_initialization(self, scalar):
+        """Test initializing PauliSentence from autograd array"""
+
+        tensor = scalar * np.ones(4)
+        res = PauliSentence(dict(zip(words, tensor)))
+        assert all(isinstance(val, np.ndarray) for val in res.values())
+
+    @pytest.mark.jax
+    @pytest.mark.parametrize("scalar", [0.0, 0.5, 1, 1j, 0.5j + 1.0])
+    def test_jax_initialization(self, scalar):
+        """Test initializing PauliSentence from jax array"""
+        import jax.numpy as jnp
+
+        tensor = scalar * jnp.ones(4)
+        res = PauliSentence(dict(zip(words, tensor)))
+        assert all(isinstance(val, jnp.ndarray) for val in res.values())
+
+    @pytest.mark.tf
+    @pytest.mark.parametrize("scalar", [0.0, 0.5, 1, 1j, 0.5j + 1.0])
+    def test_tf_initialization(self, scalar):
+        """Test initializing PauliSentence from tf tensor"""
+        import tensorflow as tf
+
+        tensor = scalar * tf.ones(4, dtype=tf.complex64)
+        res = PauliSentence(dict(zip(words, tensor)))
+        assert all(isinstance(val, tf.Tensor) for val in res.values())
+
+    @pytest.mark.torch
+    @pytest.mark.parametrize("ps", sentences)
+    @pytest.mark.parametrize("scalar", [0.5, 1, 1j, 0.5j + 1.0])
+    def test_torch_scalar_multiplication(self, ps, scalar):
+        """Test that multiplying with a torch tensor works and results in the correct types"""
+        import torch
+
+        res1 = torch.tensor(scalar) * ps
+        res2 = ps * torch.tensor(scalar)
+        res3 = ps / torch.tensor(scalar)
+        assert isinstance(res1, PauliSentence)
+        assert isinstance(res2, PauliSentence)
+        assert isinstance(res3, PauliSentence)
+        assert list(res1.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res2.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res3.values()) == [coeff / scalar for coeff in ps.values()]
+        assert all(isinstance(val, torch.Tensor) for val in res1.values())
+        assert all(isinstance(val, torch.Tensor) for val in res2.values())
+        assert all(isinstance(val, torch.Tensor) for val in res3.values())
+
+    @pytest.mark.autograd
+    @pytest.mark.parametrize("ps", sentences)
+    @pytest.mark.parametrize("scalar", [0.5, 1, 1j, 0.5j + 1.0])
+    def test_autograd_scalar_multiplication(self, ps, scalar):
+        """Test that multiplying with an autograd array works and results in the correct types"""
+
+        res1 = np.array(scalar) * ps
+        res2 = ps * np.array(scalar)
+        res3 = ps / np.array(scalar)
+        assert isinstance(res1, PauliSentence)
+        assert isinstance(res2, PauliSentence)
+        assert isinstance(res3, PauliSentence)
+        assert list(res1.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res2.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res3.values()) == [coeff / scalar for coeff in ps.values()]
+        assert all(isinstance(val, np.ndarray) for val in res1.values())
+        assert all(isinstance(val, np.ndarray) for val in res2.values())
+        assert all(isinstance(val, np.ndarray) for val in res3.values())
+
+    @pytest.mark.jax
+    @pytest.mark.parametrize("ps", sentences)
+    @pytest.mark.parametrize("scalar", [0.5, 1, 1j, 0.5j + 1.0])
+    def test_jax_scalar_multiplication(self, ps, scalar):
+        """Test that multiplying with a jax array works and results in the correct types"""
+        import jax.numpy as jnp
+
+        res1 = jnp.array(scalar) * ps
+        res2 = ps * jnp.array(scalar)
+        res3 = ps / jnp.array(scalar)
+        assert isinstance(res1, PauliSentence)
+        assert isinstance(res2, PauliSentence)
+        assert isinstance(res3, PauliSentence)
+        assert list(res1.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res2.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res3.values()) == [coeff / scalar for coeff in ps.values()]
+        assert all(isinstance(val, jnp.ndarray) for val in res1.values())
+        assert all(isinstance(val, jnp.ndarray) for val in res2.values())
+        assert all(isinstance(val, jnp.ndarray) for val in res3.values())
+
+    @pytest.mark.tf
+    @pytest.mark.parametrize("ps", sentences)
+    @pytest.mark.parametrize("scalar", [0.5, 1, 1j, 0.5j + 1.0])
+    def test_tf_scalar_multiplication(self, ps, scalar):
+        """Test that multiplying with a tf tensor works and results in the correct types"""
+        import tensorflow as tf
+
+        res1 = tf.constant(scalar, dtype=tf.complex64) * ps
+        res2 = ps * tf.constant(scalar, dtype=tf.complex64)
+        res3 = ps / tf.constant(scalar, dtype=tf.complex64)
+        assert isinstance(res1, PauliSentence)
+        assert isinstance(res2, PauliSentence)
+        assert isinstance(res3, PauliSentence)
+        assert list(res1.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res2.values()) == [scalar * coeff for coeff in ps.values()]
+        assert list(res3.values()) == [coeff / scalar for coeff in ps.values()]
+        assert all(isinstance(val, tf.Tensor) for val in res1.values())
+        assert all(isinstance(val, tf.Tensor) for val in res2.values())
+        assert all(isinstance(val, tf.Tensor) for val in res3.values())