Repository: PennyLaneAI/pennylane
Pull request #3939 (2023-03-23 19:47:28)

Update `PauliSentence` and `PauliWord` to efficiently convert between `Hamiltonian` class  
**Context:**
Pauli arithmetic provides faster methods to multiply and add pauli sentences. Lower conversion overhead between the `PauliSentence` class and the `Hamiltonian` class is required for these performances improvements to be realized with qchem workflows. 

**Description of the Change:**
- Update type of coefficients in `pauli_sentence()` conversion function to be complex dtype.
- Add a `get_as_tensor=False` kwarg to the `PauliWord.hamiltonian()` method to get an operator instance.
- Update `PauliSentence.hamiltonian()` to stack the coefficients and operations then cast to Hamiltonian as opposed to casting each term to a Hamiltonian first. 

Jaybsoni: [sc-34786]
albi3ro: @Jaybsoni  Since this is to make the conversion more efficient, do you have any numbers? What's the difference in performance?
codecov[bot]: ## [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3939?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3939](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3939?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (f5a9062) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/4a001137d469c3163fadc9f64554e3e9384fd342?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (4a00113) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

```diff
@@           Coverage Diff           @@
##           master    #3939   +/-   ##
=======================================
  Coverage   99.74%   99.74%           
=======================================
  Files         345      345           
  Lines       30160    30165    +5     
=======================================
+ Hits        30082    30087    +5     
  Misses         78       78           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3939?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/identity.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3939?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9pZGVudGl0eS5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/qubit/non\_parametric\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3939?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9ub25fcGFyYW1ldHJpY19vcHMucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/pauli/conversion.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3939?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3BhdWxpL2NvbnZlcnNpb24ucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/pauli/pauli\_arithmetic.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3939?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3BhdWxpL3BhdWxpX2FyaXRobWV0aWMucHk=) | `100.00% <100.00%> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

Jaybsoni: ![](https://media.app.shortcut.com/api/attachments/files/clubhouse-assets/5f034d83-cc5b-416f-927e-8e713597fce6/641dfc56-afb9-49b3-908e-f21838b7d976/Clipboard%202023-24-03%20at%203.39.02%20PM.png)

Jaybsoni: <img width="519" alt="image" src="https://user-images.githubusercontent.com/21964864/227628035-246a392d-0e69-4954-9b71-d82c4f36cc28.png">

Performance benchmark!

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 2715e2167..727d88c41 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -28,6 +28,9 @@
 
 <h3>Improvements</h3>
 
+* Update Pauli arithmetic to more efficiently convert to a Hamiltonian.
+  [(#3939)](https://github.com/PennyLaneAI/pennylane/pull/3939)
+
 * Keras and Torch NN modules are now compatible with the new return type system.
   [(#3913)](https://github.com/PennyLaneAI/pennylane/pull/3913)
   [(#3914)](https://github.com/PennyLaneAI/pennylane/pull/3914)
diff --git a/pennylane/pauli/pauli_arithmetic.py b/pennylane/pauli/pauli_arithmetic.py
index 6026ee75d..1702dbe3b 100644
--- a/pennylane/pauli/pauli_arithmetic.py
+++ b/pennylane/pauli/pauli_arithmetic.py
@@ -217,7 +217,7 @@ class PauliWord(dict):
 
         return reduce(kron, (matrix_map[self[w]] for w in wire_order))
 
-    def operation(self, wire_order=None):
+    def operation(self, wire_order=None, get_as_tensor=False):
         """Returns a native PennyLane :class:`~pennylane.operation.Operation` representing the PauliWord."""
         if len(self) == 0:
             if wire_order in (None, [], wires.Wires([])):
@@ -225,10 +225,13 @@ class PauliWord(dict):
             return Identity(wires=wire_order)
 
         factors = [op_map[op](wire) for wire, op in self.items()]
+
+        if get_as_tensor:
+            return factors[0] if len(factors) == 1 else Tensor(*factors)
         return factors[0] if len(factors) == 1 else prod(*factors)
 
     def hamiltonian(self, wire_order=None):
-        """Return :class:`~pennylane.Hamiltonian` representing the PauliWord"""
+        """Return :class:`~pennylane.Hamiltonian` representing the PauliWord."""
         if len(self) == 0:
             if wire_order in (None, [], wires.Wires([])):
                 raise ValueError("Can't get the Hamiltonian for an empty PauliWord.")
@@ -293,7 +296,7 @@ class PauliSentence(dict):
         for pw1 in self:
             for pw2 in other:
                 prod_pw, coeff = pw1 * pw2
-                final_ps[prod_pw] += coeff * self[pw1] * other[pw2]
+                final_ps[prod_pw] = final_ps[prod_pw] + coeff * self[pw1] * other[pw2]
 
         return final_ps
 
@@ -381,8 +384,11 @@ class PauliSentence(dict):
             return Hamiltonian([], [])
 
         wire_order = wire_order or self.wires
-        return sum(
-            coeff * pw.hamiltonian(wire_order=list(wire_order)) for pw, coeff in self.items()
+        wire_order = list(wire_order)
+
+        return Hamiltonian(
+            list(self.values()),
+            [pw.operation(wire_order=wire_order, get_as_tensor=True) for pw in self],
         )
 
     def simplify(self, tol=1e-8):


Test patch:
diff --git a/tests/pauli/test_pauli_arithmetic.py b/tests/pauli/test_pauli_arithmetic.py
index c8600e69d..8a63e55db 100644
--- a/tests/pauli/test_pauli_arithmetic.py
+++ b/tests/pauli/test_pauli_arithmetic.py
@@ -181,6 +181,11 @@ class TestPauliWord:
             assert pw_op.name == op.name
             assert pw_op.wires == op.wires
 
+        if isinstance(op, qml.ops.Prod):
+            pw_tensor_op = pw.operation(get_as_tensor=True)
+            expected_tensor_op = qml.operation.Tensor(*op.operands)
+            assert qml.equal(pw_tensor_op, expected_tensor_op)
+
     def test_operation_empty(self):
         """Test that an empty PauliWord with wire_order returns Identity."""
         op = PauliWord({}).operation(wire_order=[0, 1])
@@ -321,6 +326,18 @@ class TestPauliSentence:
         (ps4, ps3, ps3),
         (ps1, ps5, ps1),
         (ps5, ps1, ps1),
+        (
+            PauliSentence(
+                {PauliWord({0: "Z"}): np.array(1.0), PauliWord({0: "Z", 1: "X"}): np.array(1.0)}
+            ),
+            PauliSentence({PauliWord({1: "Z"}): np.array(1.0), PauliWord({1: "Y"}): np.array(1.0)}),
+            PauliSentence(
+                {
+                    PauliWord({0: "Z", 1: "Z"}): np.array(1.0 + 1.0j),
+                    PauliWord({0: "Z", 1: "Y"}): np.array(1.0 - 1.0j),
+                }
+            ),
+        ),
     )
 
     @pytest.mark.parametrize("ps1, ps2, res", tup_ps_mult)