Repository: PennyLaneAI/pennylane

Issue:
Add generators to some operations
PennyLane operations have a [generator](https://pennylane.readthedocs.io/en/stable/code/api/pennylane.operation.Operation.html#pennylane.operation.Operation.generator) attribute, originally added in https://github.com/PennyLaneAI/pennylane/pull/295. This attribute contains the Hamiltonian used to generate the unitary gate. The use case for this attribute is the [QNGOptimizer](https://pennylane.readthedocs.io/en/stable/code/api/pennylane.QNGOptimizer.html), which accesses the generator to construct the metric tensor.

However, some commonly-used gates in PennyLane do not yet have their generator defined. These include:
- [ ] `Rot`
- [x] `MultiRZ` (added in https://github.com/PennyLaneAI/pennylane/pull/912)
- [x] `PauliRot` (added in https://github.com/PennyLaneAI/pennylane/pull/963)
- [ ] `CRot`

Without a generator defined, these gates cannot be used within circuits with the QNG optimizer. This is a blocker for use of [StronglyEntanglingLayers](https://pennylane.readthedocs.io/en/stable/code/api/pennylane.templates.layers.StronglyEntanglingLayers.html), which have `Rot` gates contained within.

This issue requests addition of generators for the above gates.

Update: `MultiRZ` and `PauliRot` have had their generators added. This leaves `Rot` and `CRot`, but it is not clear how to proceed since these gates are multiparameter gates, and perhaps best thought of as a composition of ZYZ rotations.

Discussion:
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963?src=pr&el=h1) Report
> Merging [#963](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963?src=pr&el=desc) (fa12dfd) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/88be620a04a802380d07dbdd84acb48a8f245fe6?el=desc) (88be620) will **decrease** coverage by `0.01%`.
> The diff coverage is `84.61%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963?src=pr&el=tree)

```diff
@@            Coverage Diff             @@
##           master     #963      +/-   ##
==========================================
- Coverage   97.81%   97.80%   -0.02%     
==========================================
  Files         149      149              
  Lines       10450    10463      +13     
==========================================
+ Hits        10222    10233      +11     
- Misses        228      230       +2     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963/diff?src=pr&el=tree#diff-cGVubnlsYW5lL29wcy9xdWJpdC5weQ==) | `96.75% <84.61%> (-0.30%)` | :arrow_down: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963?src=pr&el=footer). Last update [88be620...fa12dfd](https://codecov.io/gh/PennyLaneAI/pennylane/pull/963?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).

JiahaoYao: How can i fix the `black`? 
josh146: Thanks @JiahaoYao for the contribution!

Regarding black, have you installed the black formatter (`pip install black`) and run it on the PL folder with a line length of 100?

```python
black -l 100 pennylane/
```

If you have, you may need to upgrade your `black` version and run it again:

```python
pip install black --upgrade
black -l 100 pennylane/
```
JiahaoYao: thanks @josh146 
co9olguy: Hi @JiahaoYao, thanks for the contribution! Are you ready for this one to be reviewed?
JiahaoYao: please @co9olguy 
JiahaoYao: > Thanks @JiahaoYao, will be nice to add this in! I just left a few comments, but will be happy to approve once they are sorted.
> 
> Also, don't forget to add the change and your name to the change log.

Sorry, by the way, how can I add the name to the change log? 
JiahaoYao: changed
JiahaoYao: @josh146  cool, added!

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 1779c72e8..ac81cba0d 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -98,6 +98,10 @@
   required to fully support end-to-end differentiable Mottonen and Amplitude embedding.
   [(#922)](https://github.com/PennyLaneAI/pennylane/pull/922)
 
+* Adds the `PauliRot` generator to the `qml.operation` module. This 
+  generator is required to construct the metric tensor. 
+  [(#963)](https://github.com/PennyLaneAI/pennylane/pull/963)
+
 <h3>Breaking changes</h3>
 
 <h3>Documentation</h3>
@@ -117,7 +121,7 @@
 
 This release contains contributions from (in alphabetical order):
 
-Olivia Di Matteo, Josh Izaac, Alejandro Montanez, Chase Roberts.
+Olivia Di Matteo, Josh Izaac, Alejandro Montanez, Chase Roberts, Jiahao Yao.
 
 # Release 0.13.0 (current release)
 
diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py
index cd0ebe7a7..3c493745f 100644
--- a/pennylane/ops/qubit.py
+++ b/pennylane/ops/qubit.py
@@ -972,6 +972,44 @@ class PauliRot(Operation):
             list(range(len(pauli_word))),
         )
 
+    _generator = None
+
+    @property
+    def generator(self):
+        if self._generator is None:
+            pauli_word = self.parameters[1]
+
+            # Simplest case is if the Pauli is the identity matrix
+            if pauli_word == "I" * len(pauli_word):
+                self._generator = [np.eye(2 ** len(pauli_word)), -1 / 2]
+                return self._generator
+
+            # We first generate the matrix excluding the identity parts and expand it afterwards.
+            # To this end, we have to store on which wires the non-identity parts act
+            non_identity_wires, non_identity_gates = zip(
+                *[(wire, gate) for wire, gate in enumerate(pauli_word) if gate != "I"]
+            )
+
+            # get MultiRZ's generator
+            multi_Z_rot_generator = np.diag(pauli_eigs(len(non_identity_gates)))
+
+            # now we conjugate with Hadamard and RX to create the Pauli string
+            conjugation_matrix = functools.reduce(
+                np.kron,
+                [PauliRot._PAULI_CONJUGATION_MATRICES[gate] for gate in non_identity_gates],
+            )
+
+            self._generator = [
+                expand(
+                    conjugation_matrix.T.conj() @ multi_Z_rot_generator @ conjugation_matrix,
+                    non_identity_wires,
+                    list(range(len(pauli_word))),
+                ),
+                -1 / 2,
+            ]
+
+        return self._generator
+
     @classmethod
     def _eigvals(cls, theta, pauli_word):
         # Identity must be treated specially because its eigenvalues are all the same


Test patch:
diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py
index 6261a9c84..6b47d294a 100644
--- a/tests/ops/test_qubit_ops.py
+++ b/tests/ops/test_qubit_ops.py
@@ -1046,6 +1046,43 @@ class TestPauliRot:
         ):
             qml.PauliRot(0.3, pauli_word, wires=wires)
 
+    @pytest.mark.parametrize(
+        "pauli_word",
+        [
+            ("XIZ"),
+            ("IIII"),
+            ("XIYIZI"),
+            ("IXI"),
+            ("IIIIIZI"),
+            ("XYZIII"),
+            ("IIIXYZ"),
+        ],
+    )
+    def test_multirz_generator(self, pauli_word):
+        """Test that the generator of the MultiRZ gate is correct."""
+        op = qml.PauliRot(0.3, pauli_word, wires=range(len(pauli_word)))
+        gen = op.generator
+
+        if pauli_word[0] == 'I':
+            # this is the identity
+            expected_gen = qml.Identity(wires=0) 
+        else:
+            expected_gen = getattr(
+                qml, 'Pauli{}'.format(pauli_word[0]))(wires=0)
+
+        for i, pauli in enumerate(pauli_word[1:]):
+            i += 1
+            if pauli == 'I':
+                expected_gen = expected_gen @  qml.Identity(
+                    wires=i) 
+            else:
+                expected_gen = expected_gen @ getattr(
+                    qml, 'Pauli{}'.format(pauli))(wires=i)
+
+        expected_gen_mat = expected_gen.matrix
+
+        assert np.allclose(gen[0], expected_gen_mat)
+        assert gen[1] == -0.5
 
 class TestMultiRZ:
     """Test the MultiRZ operation."""
@@ -1178,6 +1215,7 @@ class TestMultiRZ:
         spy.assert_not_called()
 
 
+
 class TestDiagonalQubitUnitary:
     """Test the DiagonalQubitUnitary operation."""