Repository: PennyLaneAI/pennylane
Pull request #5253 (2024-02-23 18:51:52)

`generator` methods of operators should return new opmath instances
**Context:** Some operators have a generator method. We want to ensure they return the new opmath instances (`SProd,` `Prod,` `Sum`). In particular, no `Tensor` or `Hamiltonian` instances exist.

**Description of the Change:** All generators in the code have been modified, except those in the `qchem_ops` module.
If we modify them, we will have problems with the associated tests since `tapering` is still not fully compatible with the new opmath. Such generators will be replaced in the upcoming future.

**Benefits:** Modifying the generators is necessary to enable the new opmath by default.

**Possible Drawbacks:** Since the returned generators are instances of a different class, drawbacks can arise if we try to use `Tensor` or `Hamiltonian` methods for the 'new' generators not implemented in the new opmath. Similarly, we get unexpected behavior if the methods are the same but return the results differently.

**Related GitHub Issues:** None

[sc-54079]

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/5253?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `77.41935%` with `7 lines` in your changes are missing coverage. Please review.
> Project coverage is 99.62%. Comparing base [(`d7593a3`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/d7593a30739910dab72f09e05130cda029f57de8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`60ee5c0`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5253?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5253?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/ops/functions/generator.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5253?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvZ2VuZXJhdG9yLnB5) | 56.25% | [7 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5253?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    #5253      +/-   ##
==========================================
- Coverage   99.65%   99.62%   -0.03%     
==========================================
  Files         399      399              
  Lines       36890    36624     -266     
==========================================
- Hits        36764    36488     -276     
- Misses        126      136      +10     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5253?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).

PietropaoloFrisoni: @Qottmann  Sure, I'll replace the generators in `qchem` after solving the two related issues in `tapering`. I'll create a story for that to make sure we do not forget.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index c70e10e1d..03f2ec08c 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -11,6 +11,10 @@
 
 * The `molecular_hamiltonian` function calls `PySCF` directly when `method='pyscf'` is selected.
   [(#5118)](https://github.com/PennyLaneAI/pennylane/pull/5118)
+  
+* All generators in the source code (except those in the `qchem` module) no longer return 
+  `Hamiltonian` or `Tensor` instances. Wherever possible, these return `Sum`, `SProd`, and `Prod` instances.
+  [(#5253)](https://github.com/PennyLaneAI/pennylane/pull/5253)
 
 * Upgraded `null.qubit` to the new device API. Also, added support for all measurements and various modes of differentiation.
   [(#5211)](https://github.com/PennyLaneAI/pennylane/pull/5211)
@@ -30,6 +34,7 @@
 
 This release contains contributions from (in alphabetical order):
 
+Pietropaolo Frisoni,
 Soran Jahangiri,
 Korbinian Kottmann,
 Matthew Silverman.
diff --git a/pennylane/gradients/hadamard_gradient.py b/pennylane/gradients/hadamard_gradient.py
index 2ca957906..56d9ef0a5 100644
--- a/pennylane/gradients/hadamard_gradient.py
+++ b/pennylane/gradients/hadamard_gradient.py
@@ -460,6 +460,9 @@ def _get_generators(trainable_op):
     elif isinstance(trainable_op, qml.Rot):
         generators = [qml.Z(trainable_op.wires)]
         coeffs = [-0.5]
+    elif isinstance(trainable_op, (qml.RX, qml.RY, qml.RZ)):
+        generators = [trainable_op.generator().base]
+        coeffs = [trainable_op.generator().scalar]
     else:
         generators = trainable_op.generator().ops
         coeffs = trainable_op.generator().coeffs
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 661d94da5..d1265bd5e 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -1397,8 +1397,7 @@ class Operator(abc.ABC):
         we get the generator
 
         >>> U.generator()
-          (0.5) [Y0]
-        + (1.0) [Z0 X1]
+          0.5 * Y(0) + Z(0) @ X(1)
 
         The generator may also be provided in the form of a dense or sparse Hamiltonian
         (using :class:`.Hermitian` and :class:`.SparseHamiltonian` respectively).
diff --git a/pennylane/ops/functions/generator.py b/pennylane/ops/functions/generator.py
index 9e1dd4103..aff3d8af0 100644
--- a/pennylane/ops/functions/generator.py
+++ b/pennylane/ops/functions/generator.py
@@ -24,6 +24,7 @@ import pennylane as qml
 from pennylane.ops import Hamiltonian, SProd, Prod, Sum
 
 
+# pylint: disable=too-many-branches
 def _generator_hamiltonian(gen, op):
     """Return the generator as type :class:`~.Hamiltonian`."""
     wires = op.wires
@@ -43,6 +44,15 @@ def _generator_hamiltonian(gen, op):
     elif isinstance(gen, qml.operation.Observable):
         H = 1.0 * gen
 
+    # TODO: remove this once test_tapering supports new opmath
+    elif isinstance(gen, SProd):
+
+        if gen.arithmetic_depth == 1:
+            H = gen.scalar * gen.base
+        elif gen.arithmetic_depth == 2:
+            if isinstance(gen.base, Prod):
+                H = qml.Hamiltonian([gen.scalar], [gen.base[0] @ gen.base[1]])
+
     return H
 
 
@@ -165,7 +175,7 @@ def generator(op: qml.operation.Operator, format="prefactor"):
     >>> qml.generator(op, format="prefactor")  # output will always be (obs, prefactor)
     (X(0), -0.5)
     >>> qml.generator(op, format="hamiltonian")  # output will always be a Hamiltonian
-    <Hamiltonian: terms=1, wires=[0]>
+    (-0.5) [X0]
     >>> qml.generator(qml.PhaseShift(0.1, wires=0), format="observable")  # ouput will be a simplified obs where possible
     Projector([1], wires=[0])
     >>> qml.generator(op, format="arithmetic")  # output is an instance of `SProd`
diff --git a/pennylane/ops/op_math/adjoint.py b/pennylane/ops/op_math/adjoint.py
index 73b73bd6e..b7b71cf5b 100644
--- a/pennylane/ops/op_math/adjoint.py
+++ b/pennylane/ops/op_math/adjoint.py
@@ -401,7 +401,7 @@ class AdjointOperation(Adjoint, Operation):
         return self.base.has_generator
 
     def generator(self):
-        return -1.0 * self.base.generator()
+        return qml.s_prod(-1.0, self.base.generator())
 
 
 class AdjointObs(Adjoint, Observable):
diff --git a/pennylane/ops/op_math/evolution.py b/pennylane/ops/op_math/evolution.py
index 6cc86fd83..6a01f17f2 100644
--- a/pennylane/ops/op_math/evolution.py
+++ b/pennylane/ops/op_math/evolution.py
@@ -137,8 +137,7 @@ class Evolution(Exp):
         we get the generator
 
         >>> U.generator()
-          (0.5) [Y0]
-        + (1.0) [Z0 X1]
+          0.5 * Y(0) + Z(0) @ X(1)
 
         """
         if not self.base.is_hermitian:
diff --git a/pennylane/ops/op_math/exp.py b/pennylane/ops/op_math/exp.py
index f38aac028..ae7829866 100644
--- a/pennylane/ops/op_math/exp.py
+++ b/pennylane/ops/op_math/exp.py
@@ -484,8 +484,7 @@ class Exp(ScalarSymbolicOp, Operation):
         we get the generator
 
         >>> U.generator()
-          (0.5) [Y0]
-        + (1.0) [Z0 X1]
+          0.5 * Y(0) + Z(0) @ X(1)
 
         """
         if self.base.is_hermitian and not np.real(self.coeff):
diff --git a/pennylane/ops/op_math/pow.py b/pennylane/ops/op_math/pow.py
index 56c2cb53b..fb66f7796 100644
--- a/pennylane/ops/op_math/pow.py
+++ b/pennylane/ops/op_math/pow.py
@@ -329,7 +329,7 @@ class Pow(ScalarSymbolicOp):
 
         See also :func:`~.generator`
         """
-        return self.z * self.base.generator()
+        return qml.s_prod(self.z, self.base.generator(), lazy=False)
 
     def pow(self, z):
         return [Pow(base=self.base, z=self.z * z)]
diff --git a/pennylane/ops/qubit/parametric_ops_multi_qubit.py b/pennylane/ops/qubit/parametric_ops_multi_qubit.py
index ab4ddbde2..1da176c1d 100644
--- a/pennylane/ops/qubit/parametric_ops_multi_qubit.py
+++ b/pennylane/ops/qubit/parametric_ops_multi_qubit.py
@@ -115,7 +115,7 @@ class MultiRZ(Operation):
         )
 
     def generator(self):
-        return -0.5 * functools.reduce(matmul, [PauliZ(w) for w in self.wires])
+        return qml.s_prod(-0.5, functools.reduce(matmul, [PauliZ(w) for w in self.wires]))
 
     @staticmethod
     def compute_eigvals(theta, num_wires):  # pylint: disable=arguments-differ
@@ -406,7 +406,7 @@ class PauliRot(Operation):
     def generator(self):
         pauli_word = self.hyperparameters["pauli_word"]
         wire_map = {w: i for i, w in enumerate(self.wires)}
-        return -0.5 * qml.pauli.string_to_pauli_word(pauli_word, wire_map=wire_map)
+        return qml.s_prod(-0.5, qml.pauli.string_to_pauli_word(pauli_word, wire_map=wire_map))
 
     @staticmethod
     def compute_eigvals(theta, pauli_word):  # pylint: disable=arguments-differ
@@ -774,7 +774,7 @@ class IsingXX(Operation):
     parameter_frequencies = [(1,)]
 
     def generator(self):
-        return -0.5 * PauliX(wires=self.wires[0]) @ PauliX(wires=self.wires[1])
+        return qml.s_prod(-0.5, qml.prod(PauliX(wires=self.wires[0]), PauliX(wires=self.wires[1])))
 
     def __init__(self, phi, wires, id=None):
         super().__init__(phi, wires=wires, id=id)
@@ -910,7 +910,7 @@ class IsingYY(Operation):
     parameter_frequencies = [(1,)]
 
     def generator(self):
-        return -0.5 * PauliY(wires=self.wires[0]) @ PauliY(wires=self.wires[1])
+        return qml.s_prod(-0.5, qml.prod(PauliY(wires=self.wires[0]), PauliY(wires=self.wires[1])))
 
     def __init__(self, phi, wires, id=None):
         super().__init__(phi, wires=wires, id=id)
@@ -1053,7 +1053,7 @@ class IsingZZ(Operation):
     parameter_frequencies = [(1,)]
 
     def generator(self):
-        return -0.5 * PauliZ(wires=self.wires[0]) @ PauliZ(wires=self.wires[1])
+        return qml.s_prod(-0.5, qml.prod(PauliZ(wires=self.wires[0]), PauliZ(wires=self.wires[1])))
 
     def __init__(self, phi, wires, id=None):
         super().__init__(phi, wires=wires, id=id)
@@ -1236,9 +1236,12 @@ class IsingXY(Operation):
     parameter_frequencies = [(0.5, 1.0)]
 
     def generator(self):
-        return 0.25 * (
-            PauliX(wires=self.wires[0]) @ PauliX(wires=self.wires[1])
-            + PauliY(wires=self.wires[0]) @ PauliY(wires=self.wires[1])
+        return qml.s_prod(
+            0.25,
+            qml.sum(
+                qml.prod(PauliX(wires=self.wires[0]), PauliX(wires=self.wires[1])),
+                qml.prod(PauliY(wires=self.wires[0]), PauliY(wires=self.wires[1])),
+            ),
         )
 
     def __init__(self, phi, wires, id=None):
diff --git a/pennylane/ops/qubit/parametric_ops_single_qubit.py b/pennylane/ops/qubit/parametric_ops_single_qubit.py
index 5ca6ff93f..02b913cb4 100644
--- a/pennylane/ops/qubit/parametric_ops_single_qubit.py
+++ b/pennylane/ops/qubit/parametric_ops_single_qubit.py
@@ -70,7 +70,7 @@ class RX(Operation):
     parameter_frequencies = [(1,)]
 
     def generator(self):
-        return -0.5 * PauliX(wires=self.wires)
+        return qml.s_prod(-0.5, PauliX(wires=self.wires))
 
     def __init__(self, phi, wires, id=None):
         super().__init__(phi, wires=wires, id=id)
@@ -166,7 +166,7 @@ class RY(Operation):
     parameter_frequencies = [(1,)]
 
     def generator(self):
-        return -0.5 * PauliY(wires=self.wires)
+        return qml.s_prod(-0.5, PauliY(wires=self.wires))
 
     def __init__(self, phi, wires, id=None):
         super().__init__(phi, wires=wires, id=id)
@@ -261,7 +261,7 @@ class RZ(Operation):
     parameter_frequencies = [(1,)]
 
     def generator(self):
-        return -0.5 * PauliZ(wires=self.wires)
+        return qml.s_prod(-0.5, PauliZ(wires=self.wires))
 
     def __init__(self, phi, wires, id=None):
         super().__init__(phi, wires=wires, id=id)


Test patch:
diff --git a/tests/ops/op_math/test_adjoint.py b/tests/ops/op_math/test_adjoint.py
index 71d47909f..dee5f6308 100644
--- a/tests/ops/op_math/test_adjoint.py
+++ b/tests/ops/op_math/test_adjoint.py
@@ -491,7 +491,7 @@ class TestAdjointOperation:
         base = qml.RX(1.23, wires=0)
         op = Adjoint(base)
 
-        assert base.generator().compare(-1.0 * op.generator())
+        assert qml.equal(base.generator(), qml.s_prod(-1.0, op.generator()))
 
     def test_no_generator(self):
         """Test that an adjointed non-Operation raises a GeneratorUndefinedError."""
diff --git a/tests/ops/qubit/test_parametric_ops.py b/tests/ops/qubit/test_parametric_ops.py
index 192c80c4d..48115c948 100644
--- a/tests/ops/qubit/test_parametric_ops.py
+++ b/tests/ops/qubit/test_parametric_ops.py
@@ -29,6 +29,9 @@ from pennylane.ops.qubit import (
     RX as old_loc_RX,
     MultiRZ as old_loc_MultiRZ,
 )
+
+from pennylane.ops.op_math.sprod import SProd
+
 from pennylane.wires import Wires
 
 PARAMETRIZED_OPERATIONS = [
@@ -2976,6 +2979,8 @@ class TestPauliRot:
         op = qml.PauliRot(0.3, pauli_word, wires=range(len(pauli_word)))
         gen = op.generator()
 
+        assert isinstance(gen, SProd)
+
         if pauli_word[0] == "I":
             # this is the identity
             expected_gen = qml.Identity(wires=0)
@@ -2989,7 +2994,7 @@ class TestPauliRot:
             else:
                 expected_gen = expected_gen @ getattr(qml, f"Pauli{pauli}")(wires=i)
 
-        assert gen.compare(-0.5 * expected_gen)
+        assert qml.equal(gen, qml.s_prod(-0.5, expected_gen))
 
     @pytest.mark.torch
     @pytest.mark.gpu
@@ -3178,11 +3183,13 @@ class TestMultiRZ:
         op = qml.MultiRZ(0.3, wires=range(qubits))
         gen = op.generator()
 
+        assert isinstance(gen, SProd)
+
         expected_gen = qml.PauliZ(wires=0)
         for i in range(1, qubits):
             expected_gen = expected_gen @ qml.PauliZ(wires=i)
 
-        assert gen.compare(-0.5 * expected_gen)
+        assert qml.equal(gen, qml.s_prod(-0.5, expected_gen))
 
         spy = mocker.spy(qml.utils, "pauli_eigs")