Repository: PennyLaneAI/pennylane
Pull request #1153 (2021-03-18 19:35:24)

[TEMPLATES] Rewrite QuantumPhaseEstimation and QuantumMonteCarlo
Based on https://github.com/PennyLaneAI/pennylane/pull/1138, this PR investigates converting `QuantumPhaseEstimation` and `QuantumMonteCarlo` to operations.

- Like 1138, we're branching off `rip-out-core` (although I missed the reason why)
- The `expand()` method of `QuantumMonteCarlo` expands to include `QuantumPhaseEstimation`. Does `expand()` have to expand directly to fundamental gates, or can there be an intermediate? Currently, it works ok!

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [.github/CHANGELOG.md](/PennyLaneAI/pennylane/blob/master/.github/CHANGELOG.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://codecov.io/gh/PennyLaneAI/pennylane/pull/1153?src=pr&el=h1) Report
> Merging [#1153](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1153?src=pr&el=desc) (93efbd8) into [rip-out-core](https://codecov.io/gh/PennyLaneAI/pennylane/commit/25555cb50255a48e2f62d6ce7de73b05c224998f?el=desc) (25555cb) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

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

```diff
@@              Coverage Diff              @@
##           rip-out-core    #1153   +/-   ##
=============================================
  Coverage         97.96%   97.96%           
=============================================
  Files               142      142           
  Lines             10268    10286   +18     
=============================================
+ Hits              10059    10077   +18     
  Misses              209      209           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1153?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/templates/subroutines/qmc.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1153/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9zdWJyb3V0aW5lcy9xbWMucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/templates/subroutines/qpe.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1153/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RlbXBsYXRlcy9zdWJyb3V0aW5lcy9xcGUucHk=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1153?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/1153?src=pr&el=footer). Last update [25555cb...93efbd8](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1153?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/doc/code/qml_templates.rst b/doc/code/qml_templates.rst
index 2eae7b980..74831b580 100644
--- a/doc/code/qml_templates.rst
+++ b/doc/code/qml_templates.rst
@@ -77,4 +77,4 @@ Utility functions for quantum Monte Carlo
 .. automodapi:: pennylane.templates.subroutines.qmc
     :no-heading:
     :no-main-docstr:
-    :skip: QuantumMonteCarlo, template
+    :skip: QuantumMonteCarlo, Operation, Wires
diff --git a/pennylane/templates/subroutines/qmc.py b/pennylane/templates/subroutines/qmc.py
index 9f0dc7f13..a4cd4c868 100644
--- a/pennylane/templates/subroutines/qmc.py
+++ b/pennylane/templates/subroutines/qmc.py
@@ -12,12 +12,12 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """
-Contains the ``QuantumMonteCarlo`` template and utility functions.
+Contains the QuantumMonteCarlo template and utility functions.
 """
 import numpy as np
 
 import pennylane as qml
-from pennylane.templates.decorator import template
+from pennylane.operation import AnyWires, Operation
 from pennylane.wires import Wires
 
 
@@ -202,8 +202,7 @@ def make_Q(A, R):
     return UV @ UV
 
 
-@template
-def QuantumMonteCarlo(probs, func, target_wires, estimation_wires):
+class QuantumMonteCarlo(Operation):
     r"""Performs the `quantum Monte Carlo estimation <https://arxiv.org/abs/1805.00109>`__
     algorithm.
 
@@ -270,8 +269,9 @@ def QuantumMonteCarlo(probs, func, target_wires, estimation_wires):
     .. note::
 
         This template is only compatible with simulators because the algorithm is performed using
-        unitary matrices. To implement the quantum Monte Carlo algorithm on hardware requires
-        breaking down the unitary matrices into hardware-compatible gates.
+        unitary matrices. Additionally, this operation is not differentiable. To implement the
+        quantum Monte Carlo algorithm on hardware requires breaking down the unitary matrices into
+        hardware-compatible gates.
 
     .. UsageDetails::
 
@@ -326,32 +326,46 @@ def QuantumMonteCarlo(probs, func, target_wires, estimation_wires):
         >>> (1 - np.cos(np.pi * phase_estimated)) / 2
         0.4327096457464369
     """
-    if isinstance(probs, np.ndarray) and probs.ndim != 1:
-        raise ValueError("The probability distribution must be specified as a flat array")
-
-    dim_p = len(probs)
-    num_target_wires = float(np.log2(2 * dim_p))
-
-    if not num_target_wires.is_integer():
-        raise ValueError("The probability distribution must have a length that is a power of two")
-
-    num_target_wires = int(num_target_wires)
-
-    target_wires = Wires(target_wires)
-    estimation_wires = Wires(estimation_wires)
-
-    if num_target_wires != len(target_wires):
-        raise ValueError(
-            f"The probability distribution of dimension {dim_p} requires"
-            f" {num_target_wires} target wires"
-        )
-
-    A = probs_to_unitary(probs)
-    R = func_to_unitary(func, dim_p)
-    Q = make_Q(A, R)
-
-    qml.QubitUnitary(A, wires=target_wires[:-1])
-    qml.QubitUnitary(R, wires=target_wires)
-    qml.templates.QuantumPhaseEstimation(
-        Q, target_wires=target_wires, estimation_wires=estimation_wires
-    )
+    num_params = 3
+    num_wires = AnyWires
+    par_domain = "A"
+
+    def __init__(self, probs, func, target_wires, estimation_wires, do_queue=True):
+        if isinstance(probs, np.ndarray) and probs.ndim != 1:
+            raise ValueError("The probability distribution must be specified as a flat array")
+
+        dim_p = len(probs)
+        num_target_wires_ = np.log2(2 * dim_p)
+        num_target_wires = int(num_target_wires_)
+
+        if not np.allclose(num_target_wires_, num_target_wires):
+            raise ValueError(
+                "The probability distribution must have a length that is a power of two"
+            )
+
+        self.target_wires = Wires(target_wires)
+        self.estimation_wires = Wires(estimation_wires)
+        wires = self.target_wires + self.estimation_wires
+
+        if num_target_wires != len(self.target_wires):
+            raise ValueError(
+                f"The probability distribution of dimension {dim_p} requires"
+                f" {num_target_wires} target wires"
+            )
+
+        A = probs_to_unitary(probs)
+        R = func_to_unitary(func, dim_p)
+        Q = make_Q(A, R)
+        super().__init__(A, R, Q, wires=wires, do_queue=do_queue)
+
+    def expand(self):
+        A, R, Q = self.parameters
+
+        with qml.tape.QuantumTape() as tape:
+            qml.QubitUnitary(A, wires=self.target_wires[:-1])
+            qml.QubitUnitary(R, wires=self.target_wires)
+            qml.templates.QuantumPhaseEstimation(
+                Q, target_wires=self.target_wires, estimation_wires=self.estimation_wires
+            )
+
+        return tape
diff --git a/pennylane/templates/subroutines/qpe.py b/pennylane/templates/subroutines/qpe.py
index b83544bc3..6e19f56c8 100644
--- a/pennylane/templates/subroutines/qpe.py
+++ b/pennylane/templates/subroutines/qpe.py
@@ -12,15 +12,14 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 """
-Contains the ``QuantumPhaseEstimation`` template.
+Contains the QuantumPhaseEstimation template.
 """
 import pennylane as qml
-from pennylane.templates.decorator import template
+from pennylane.operation import AnyWires, Operation
 from pennylane.wires import Wires
 
 
-@template
-def QuantumPhaseEstimation(unitary, target_wires, estimation_wires):
+class QuantumPhaseEstimation(Operation):
     r"""Performs the
     `quantum phase estimation <https://en.wikipedia.org/wiki/Quantum_phase_estimation_algorithm>`__
     circuit.
@@ -104,21 +103,38 @@ def QuantumPhaseEstimation(unitary, target_wires, estimation_wires):
             # Need to rescale phase due to convention of RX gate
             phase_estimated = 4 * np.pi * (1 - phase_estimated)
     """
+    num_params = 1
+    num_wires = AnyWires
+    par_domain = "A"
 
-    target_wires = Wires(target_wires)
-    estimation_wires = Wires(estimation_wires)
+    def __init__(self, unitary, target_wires, estimation_wires, do_queue=True):
+        self.target_wires = Wires(target_wires)
+        self.estimation_wires = Wires(estimation_wires)
 
-    if len(Wires.shared_wires([target_wires, estimation_wires])) != 0:
-        raise qml.QuantumFunctionError("The target wires and estimation wires must be different")
+        wires = self.target_wires + self.estimation_wires
 
-    unitary_powers = [unitary]
+        if len(Wires.shared_wires([self.target_wires, self.estimation_wires])) != 0:
+            raise qml.QuantumFunctionError(
+                "The target wires and estimation wires must be different"
+            )
 
-    for _ in range(len(estimation_wires) - 1):
-        new_power = unitary_powers[-1] @ unitary_powers[-1]
-        unitary_powers.append(new_power)
+        super().__init__(unitary, wires=wires, do_queue=do_queue)
 
-    for wire in estimation_wires:
-        qml.Hadamard(wire)
-        qml.ControlledQubitUnitary(unitary_powers.pop(), control_wires=wire, wires=target_wires)
+    def expand(self):
+        unitary = self.parameters[0]
+        unitary_powers = [unitary]
 
-    qml.QFT(wires=estimation_wires).inv()
+        for _ in range(len(self.estimation_wires) - 1):
+            new_power = unitary_powers[-1] @ unitary_powers[-1]
+            unitary_powers.append(new_power)
+
+        with qml.tape.QuantumTape() as tape:
+            for wire in self.estimation_wires:
+                qml.Hadamard(wire)
+                qml.ControlledQubitUnitary(
+                    unitary_powers.pop(), control_wires=wire, wires=self.target_wires
+                )
+
+            qml.QFT(wires=self.estimation_wires).inv()
+
+        return tape


Test patch:
diff --git a/tests/templates/test_subroutines.py b/tests/templates/test_subroutines.py
index 693ba96c6..ef3f4a8e9 100644
--- a/tests/templates/test_subroutines.py
+++ b/tests/templates/test_subroutines.py
@@ -1487,8 +1487,8 @@ class TestQuantumPhaseEstimation:
 
         m = qml.RX(0.3, wires=0).matrix
 
-        with qml.tape.QuantumTape() as tape:
-            QuantumPhaseEstimation(m, target_wires=[0], estimation_wires=[1, 2])
+        op = QuantumPhaseEstimation(m, target_wires=[0], estimation_wires=[1, 2])
+        tape = op.expand()
 
         with qml.tape.QuantumTape() as tape2:
             qml.Hadamard(1),
@@ -1524,6 +1524,7 @@ class TestQuantumPhaseEstimation:
                 )
                 qml.probs(estimation_wires)
 
+            tape = tape.expand()
             res = tape.execute(dev).flatten()
             initial_estimate = np.argmax(res) / 2 ** (wires - 1)
 
@@ -1571,6 +1572,7 @@ class TestQuantumPhaseEstimation:
                 )
                 qml.probs(estimation_wires)
 
+            tape = tape.expand()
             res = tape.execute(dev).flatten()
 
             if phase < 0:
diff --git a/tests/templates/test_subroutines_qmc.py b/tests/templates/test_subroutines_qmc.py
index a64b111bd..f19ee38af 100644
--- a/tests/templates/test_subroutines_qmc.py
+++ b/tests/templates/test_subroutines_qmc.py
@@ -240,11 +240,11 @@ class TestQuantumMonteCarlo:
         p = np.ones(4) / 4
         target_wires, estimation_wires = Wires(range(3)), Wires(range(3, 5))
 
-        with qml.tape.QuantumTape() as tape:
-            QuantumMonteCarlo(p, self.func, target_wires, estimation_wires)
+        op = QuantumMonteCarlo(p, self.func, target_wires, estimation_wires)
+        tape = op.expand().expand()
 
-        queue_before_qpe = tape.queue[:2]
-        queue_after_qpe = tape.queue[2:]
+        queue_before_qpe = tape.operations[:2]
+        queue_after_qpe = tape.operations[2:]
 
         A = probs_to_unitary(p)
         R = func_to_unitary(self.func, 4)
@@ -262,12 +262,14 @@ class TestQuantumMonteCarlo:
         with qml.tape.QuantumTape() as qpe_tape:
             qml.templates.QuantumPhaseEstimation(Q, target_wires, estimation_wires)
 
-        assert len(queue_after_qpe) == len(qpe_tape.queue)
-        assert all(o1.name == o2.name for o1, o2 in zip(queue_after_qpe, qpe_tape.queue))
+        qpe_tape = qpe_tape.expand()
+
+        assert len(queue_after_qpe) == len(qpe_tape.operations)
+        assert all(o1.name == o2.name for o1, o2 in zip(queue_after_qpe, qpe_tape.operations))
         assert all(
-            np.allclose(o1.matrix, o2.matrix) for o1, o2 in zip(queue_after_qpe, qpe_tape.queue)
+            np.allclose(o1.matrix, o2.matrix) for o1, o2 in zip(queue_after_qpe, qpe_tape.operations)
         )
-        assert all(o1.wires == o2.wires for o1, o2 in zip(queue_after_qpe, qpe_tape.queue))
+        assert all(o1.wires == o2.wires for o1, o2 in zip(queue_after_qpe, qpe_tape.operations))
 
     def test_expected_value(self):
         """Test that the QuantumMonteCarlo template can correctly estimate the expectation value