Repository: PennyLaneAI/pennylane
Pull request #1475 (2021-07-26 12:57:41)

Adds top-level `qml.compile` transform.
**Context:** Addition of circuit compilation and optimization tools to PennyLane.

**Description of the Change:** Adds the top-level `qml.compile` function transform to apply arbitrary pipelines of qfunc transforms to quantum functions. 

**Benefits:** Circuits can now be optimized in a flexible manner, using both built-in and custom transforms.

**Possible Drawbacks:** None

**Related GitHub Issues:** None


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/1475?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1475](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (ffc5a10) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/9a032501cb4162410cafec8c4d907a6b4b8d2414?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (9a03250) will **decrease** coverage by `0.09%`.
> The diff coverage is `52.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475/graphs/tree.svg?width=650&height=150&src=pr&token=9S3YzlUgGk&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #1475      +/-   ##
==========================================
- Coverage   98.30%   98.20%   -0.10%     
==========================================
  Files         170      171       +1     
  Lines       12483    12508      +25     
==========================================
+ Hits        12271    12284      +13     
- Misses        212      224      +12     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL19faW5pdF9fLnB5) | `98.57% <ø> (ø)` | |
| [pennylane/transforms/compile.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY29tcGlsZS5weQ==) | `47.82% <47.82%> (ø)` | |
| [pennylane/transforms/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/transforms/qfunc\_transforms.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvcWZ1bmNfdHJhbnNmb3Jtcy5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [9a03250...ffc5a10](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1475?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).


Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index d2e09ece1..54876bbe0 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,61 @@
 
 <h3>New features since last release</h3>
 
+* PennyLane can now perform quantum circuit optimization using the
+  top-level transform `qml.compile`. The `compile` transform allows you
+  to chain together sequences of tape and quantum function transforms
+  into custom circuit optimization pipelines.
+  [(#1475)](https://github.com/PennyLaneAI/pennylane/pull/1475)
+
+  For example, take the following quantum function:
+
+  ```python
+  def qfunc(x, y, z):
+      qml.Hadamard(wires=0)
+      qml.Hadamard(wires=1)
+      qml.Hadamard(wires=2)
+      qml.RZ(z, wires=2)
+      qml.CNOT(wires=[2, 1])
+      qml.RX(z, wires=0)
+      qml.CNOT(wires=[1, 0])
+      qml.RX(x, wires=0)
+      qml.CNOT(wires=[1, 0])
+      qml.RZ(-z, wires=2)
+      qml.RX(y, wires=2)
+      qml.PauliY(wires=2)
+      qml.CY(wires=[1, 2])
+      return qml.expval(qml.PauliZ(wires=0))
+  ```
+
+  The default behaviour of `qml.compile` is to apply a sequence of three
+  transforms: `commute_controlled`, `cancel_inverses`, and then `merge_rotations`.
+
+  ```pycon
+  >>> dev = qml.device('default.qubit', wires=[0, 1, 2])
+  >>> compiled_qfunc = qml.compile()(qfunc)
+  >>> compiled_qnode = qml.QNode(compiled_qfunc, dev)
+  >>> print(qml.draw(compiled_qnode)(0.2, 0.3, 0.4))
+   0: ──H───RX(0.6)───────────────────┤ ⟨Z⟩
+   1: ──H──╭X─────────────────╭CY─────┤
+   2: ──H──╰C────────RX(0.3)──╰CY──Y──┤
+  ```
+
+  The ``qml.compile`` transform is flexible and accepts a custom pipeline
+  of tape and quantum function transforms (you can even write your own!).
+  For example, if we wanted to only push single-qubit gates through
+  controlled gates and cancel adjacent inverses, we could do:
+
+  ```pycon
+  >>> from qml.transforms import commute_controlled, cancel_inverses
+  >>> pipeline = [commute_controlled, cancel_inverses]
+  >>> compiled_qfunc = qml.compile(pipeline=pipeline)(qfunc)
+  >>> compiled_qnode = qml.QNode(compiled_qfunc, dev)
+  >>> print(qml.draw(compiled_qnode)(0.2, 0.3, 0.4))
+   0: ──H───RX(0.4)──RX(0.2)─────────────────────────────┤ ⟨Z⟩
+   1: ──H──╭X────────────────────────────────────╭CY─────┤
+   2: ──H──╰C────────RZ(0.4)──RZ(-0.4)──RX(0.3)──╰CY──Y──┤
+  ```
+
 * The new Device Tracker capabilities allows for flexible and versatile tracking of executions,
   even inside parameter-shift gradients. This functionality will improve the ease of monitoring
   large batches and remote jobs.
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 9164dd479..1c96c00cb 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -50,6 +50,7 @@ from pennylane.transforms import (
     adjoint,
     draw,
     ControlledOperation,
+    compile,
     ctrl,
     measurement_grouping,
     metric_tensor,
diff --git a/pennylane/transforms/__init__.py b/pennylane/transforms/__init__.py
index 3058e054f..d759b8411 100644
--- a/pennylane/transforms/__init__.py
+++ b/pennylane/transforms/__init__.py
@@ -44,15 +44,25 @@ containing quantum operations) that are used to construct QNodes.
     :toctree: api
 
     ~adjoint
+    ~ctrl
+    ~transforms.invisible
+    ~apply_controlled_Q
+    ~quantum_monte_carlo
+
+Transforms for circuit compilation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This set of transforms accept quantum functions, and perform basic circuit compilation tasks.
+
+.. autosummary::
+    :toctree: api
+
+    ~compile
     ~transforms.cancel_inverses
     ~transforms.commute_controlled
-    ~ctrl
     ~transforms.merge_rotations
     ~transforms.single_qubit_fusion
     ~transforms.unitary_to_rot
-    ~transforms.invisible
-    ~apply_controlled_Q
-    ~quantum_monte_carlo
 
 There are also utility functions and decompositions available that assist with
 both transforms, and decompositions within the larger PennyLane codebase.
@@ -92,6 +102,7 @@ to help build custom QNode, quantum function, and tape transforms:
 from .qfunc_transforms import make_tape, single_tape_transform, qfunc_transform
 from .adjoint import adjoint
 from .classical_jacobian import classical_jacobian
+from .compile import compile
 from .control import ControlledOperation, ctrl
 from .decompositions import zyz_decomposition
 from .draw import draw
diff --git a/pennylane/transforms/compile.py b/pennylane/transforms/compile.py
new file mode 100644
index 000000000..9adf96d82
--- /dev/null
+++ b/pennylane/transforms/compile.py
@@ -0,0 +1,164 @@
+# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+"""Code for the high-level quantum function transform that executes compilation."""
+# pylint: disable=too-many-branches
+from functools import partial
+
+from pennylane import apply
+from pennylane.tape import get_active_tape
+from pennylane.ops import __all__ as all_ops
+
+from pennylane.transforms import single_tape_transform, qfunc_transform
+from pennylane.transforms.optimization import cancel_inverses, commute_controlled, merge_rotations
+
+
+default_pipeline = [commute_controlled, cancel_inverses, merge_rotations]
+
+
+@qfunc_transform
+def compile(tape, pipeline=None, basis_set=None, num_passes=1, expand_depth=5):
+    """Compile a circuit by applying a series of transforms to a quantum function.
+
+    The default set of transforms includes (in order):
+
+    - pushing all commuting single-qubit gates as far right as possible
+      (:func:`~pennylane.transforms.commute_controlled`)
+    - cancellation of adjacent inverse gates
+      (:func:`~pennylane.transforms.cancel_inverses`)
+    - merging adjacent rotations of the same type
+      (:func:`~pennylane.transforms.merge_rotations`)
+
+    Args:
+        qfunc (function): A quantum function.
+        pipeline (list[single_tape_transform, qfunc_transform]): A list of
+            tape and/or quantum function transforms to apply.
+        basis_set (list[str]): A list of basis gates. When expanding the tape,
+            expansion will continue until gates in the specific set are
+            reached. If no basis set is specified, no expansion will be done.
+        num_passes (int): The number of times to apply the set of transforms in
+            ``pipeline``. The default is to perform each transform once;
+            however, doing so may produce a new circuit where applying the set
+            of transforms again may yield further improvement, so the number of
+            such passes can be adjusted.
+        expand_depth (int): When ``basis_set`` is specified, the depth to use
+            for tape expansion into the basis gates.
+
+    Returns:
+        function: the transformed quantum function
+
+    **Example**
+
+    Consider the following quantum function:
+
+    .. code-block:: python
+
+        def qfunc(x, y, z):
+            qml.Hadamard(wires=0)
+            qml.Hadamard(wires=1)
+            qml.Hadamard(wires=2)
+            qml.RZ(z, wires=2)
+            qml.CNOT(wires=[2, 1])
+            qml.RX(z, wires=0)
+            qml.CNOT(wires=[1, 0])
+            qml.RX(x, wires=0)
+            qml.CNOT(wires=[1, 0])
+            qml.RZ(-z, wires=2)
+            qml.RX(y, wires=2)
+            qml.PauliY(wires=2)
+            qml.CY(wires=[1, 2])
+            return qml.expval(qml.PauliZ(wires=0))
+
+    Visually, the original function looks like this:
+
+    >>> dev = qml.device('default.qubit', wires=[0, 1, 2])
+    >>> qnode = qml.QNode(qfunc, dev)
+    >>> print(qml.draw(qnode)(0.2, 0.3, 0.4))
+     0: ──H──RX(0.4)──────╭X─────────RX(0.2)──╭X───────┤ ⟨Z⟩
+     1: ──H───────────╭X──╰C──────────────────╰C──╭CY──┤
+     2: ──H──RZ(0.4)──╰C───RZ(-0.4)──RX(0.3)───Y──╰CY──┤
+
+    We can compile it down to a smaller set of gates using the ``qml.compile``
+    transform.
+
+    >>> compiled_qfunc = qml.compile()(qfunc)
+    >>> compiled_qnode = qml.QNode(compiled_qfunc, dev)
+    >>> print(qml.draw(compiled_qnode)(0.2, 0.3, 0.4))
+     0: ──H───RX(0.6)───────────────────┤ ⟨Z⟩
+     1: ──H──╭X─────────────────╭CY─────┤
+     2: ──H──╰C────────RX(0.3)──╰CY──Y──┤
+
+    You can change up the set of transforms by passing a custom ``pipeline`` to
+    ``qml.compile``. The pipeline is a list of transform functions. Furthermore,
+    you can specify a number of passes (repetitions of the pipeline), and a list
+    of gates into which the compiler will first attempt to decompose the
+    existing operations prior to applying any optimization transforms.
+
+    .. code-block:: python3
+
+        compiled_qfunc = qml.compile(
+            pipeline=[
+                qml.transforms.commute_controlled(direction="left"),
+                qml.transforms.merge_rotations(atol=1e-6),
+                qml.transforms.cancel_inverses
+            ],
+            basis_set=["CNOT", "RX", "RY", "RZ"],
+            num_passes=2
+        )(qfunc)
+
+        compiled_qnode = qml.QNode(compiled_qfunc, dev)
+
+    >>> print(qml.draw(compiled_qnode)(0.2, 0.3, 0.4))
+     0: ──RZ(1.57)──RX(1.57)──RZ(1.57)───RX(0.6)───────────────────────────────────────────────────────────────────────┤ ⟨Z⟩
+     1: ──RZ(1.57)──RX(1.57)──RZ(1.57)──╭X────────RZ(1.57)──────────────────────────────────────────╭C─────────────╭C──┤
+     2: ──RZ(1.57)──RX(1.57)──RZ(1.57)──╰C────────RX(0.3)───RZ(1.57)──RY(3.14)──RZ(1.57)──RY(1.57)──╰X──RY(-1.57)──╰X──┤
+
+    """
+    # Ensure that everything in the pipeline is a valid qfunc or tape transform
+    if pipeline is None:
+        pipeline = default_pipeline
+    else:
+        for p in pipeline:
+            p_func = p.func if isinstance(p, partial) else p
+            if not isinstance(p_func, single_tape_transform) and not hasattr(p_func, "tape_fn"):
+                raise ValueError("Invalid transform function {p} passed to compile.")
+
+    if num_passes < 1 or not isinstance(num_passes, int):
+        raise ValueError("Number of passes must be an integer with value at least 1.")
+
+    # Expand the tape; this is done to unroll any templates that may be present,
+    # as well as to decompose over a specified basis set
+    # First, though, we have to stop whatever tape may be recording so that we
+    # don't queue anything as a result of the expansion or transform pipeline
+    current_tape = get_active_tape()
+
+    with current_tape.stop_recording():
+        if basis_set is not None:
+            expanded_tape = tape.expand(
+                depth=expand_depth, stop_at=lambda obj: obj.name in basis_set
+            )
+        else:
+            # Expands out anything that is not a single operation (i.e., the templates)
+            expanded_tape = tape.expand(stop_at=lambda obj: obj.name in all_ops)
+
+        # Apply the full set of compilation transforms num_passes times
+        for _ in range(num_passes):
+            for transform in pipeline:
+                if isinstance(transform, (single_tape_transform, partial)):
+                    expanded_tape = transform(expanded_tape)
+                else:
+                    expanded_tape = transform.tape_fn(expanded_tape)
+
+    # Queue the operations on the optimized tape
+    for op in expanded_tape.operations + expanded_tape.measurements:
+        apply(op)
diff --git a/pennylane/transforms/qfunc_transforms.py b/pennylane/transforms/qfunc_transforms.py
index 13263af30..b631f39c9 100644
--- a/pennylane/transforms/qfunc_transforms.py
+++ b/pennylane/transforms/qfunc_transforms.py
@@ -378,6 +378,8 @@ def qfunc_transform(tape_transform):
             def wrapper(fn):
                 return _create_qfunc_internal_wrapper(fn, tape_transform, targs, tkwargs)
 
+            wrapper.tape_fn = functools.partial(tape_transform, *targs, **tkwargs)
+
             return wrapper
 
     elif len(params) == 1:


Test patch:
diff --git a/tests/transforms/test_compile.py b/tests/transforms/test_compile.py
new file mode 100644
index 000000000..f1fbb0ec8
--- /dev/null
+++ b/tests/transforms/test_compile.py
@@ -0,0 +1,503 @@
+# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+
+#     http://www.apache.org/licenses/LICENSE-2.0
+
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest
+
+from pennylane import numpy as np
+
+import pennylane as qml
+from pennylane.wires import Wires
+
+from pennylane.transforms.compile import compile
+from pennylane.transforms.optimization import cancel_inverses, commute_controlled, merge_rotations
+
+from test_optimization.utils import compare_operation_lists
+
+
+def build_qfunc(wires):
+    def qfunc(x, y, z):
+        qml.Hadamard(wires=wires[0])
+        qml.RZ(z, wires=wires[2])
+        qml.CNOT(wires=[wires[2], wires[1]])
+        qml.CNOT(wires=[wires[1], wires[0]])
+        qml.RX(x, wires=wires[0])
+        qml.CNOT(wires=[wires[1], wires[0]])
+        qml.RZ(-z, wires=wires[2])
+        qml.RX(y, wires=wires[0])
+        qml.PauliY(wires=wires[2])
+        qml.CY(wires=[wires[1], wires[2]])
+        return qml.expval(qml.PauliZ(wires=wires[0]))
+
+    return qfunc
+
+
+class TestCompile:
+    """Unit tests for compile function."""
+
+    def test_compile_invalid_pipeline(self):
+        """Test that error is raised for an invalid function in the pipeline"""
+        qfunc = build_qfunc([0, 1, 2])
+        dev = qml.device("default.qubit", wires=[0, 1, 2])
+
+        transformed_qfunc = compile(pipeline=[cancel_inverses, isinstance])(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        with pytest.raises(ValueError, match="Invalid transform function"):
+            transformed_qnode(0.1, 0.2, 0.3)
+
+    def test_compile_invalid_num_passes(self):
+        """Test that error is raised for an invalid number of passes."""
+        qfunc = build_qfunc([0, 1, 2])
+        dev = qml.device("default.qubit", wires=[0, 1, 2])
+
+        transformed_qfunc = compile(num_passes=1.3)(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        with pytest.raises(ValueError, match="Number of passes must be an integer"):
+            transformed_qnode(0.1, 0.2, 0.3)
+
+    def test_compile_mixed_tape_qfunc_transform(self):
+        """Test that we can interchange tape and qfunc transforms."""
+
+        wires = [0, 1, 2]
+        qfunc = build_qfunc(wires)
+        dev = qml.device("default.qubit", wires=wires)
+
+        pipeline = [
+            commute_controlled(direction="right").tape_fn,
+            cancel_inverses,
+            merge_rotations().tape_fn,
+        ]
+
+        transformed_qfunc = compile(pipeline=pipeline)(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+        transformed_result = transformed_qnode(0.3, 0.4, 0.5)
+
+        names_expected = ["Hadamard", "CNOT", "RX", "CY", "PauliY"]
+        wires_expected = [
+            Wires(wires[0]),
+            Wires([wires[2], wires[1]]),
+            Wires(wires[0]),
+            Wires([wires[1], wires[2]]),
+            Wires(wires[2]),
+        ]
+
+        compare_operation_lists(transformed_qnode.qtape.operations, names_expected, wires_expected)
+
+    @pytest.mark.parametrize(
+        "transform_name,num_passes",
+        [
+            ("merge_rotations", 1),
+            ("commute_controlled", 1),
+            ("merge_rotations", 3),
+            ("commute_controlled", 2),
+        ],
+    )
+    def test_compile_mock_calls(self, transform_name, num_passes, mocker):
+        """Test that functions in the pipeline are called the correct number of times."""
+
+        class DummyTransforms:
+            def run_pipeline(self):
+                pipeline = [
+                    qml.transforms.single_tape_transform(DummyTransforms.merge_rotations),
+                    qml.transforms.single_tape_transform(DummyTransforms.commute_controlled),
+                ]
+
+                wires = [0, 1, 2]
+                qfunc = build_qfunc(wires)
+                dev = qml.device("default.qubit", wires=Wires(wires))
+
+                transformed_qfunc = compile(pipeline=pipeline, num_passes=num_passes)(qfunc)
+                transformed_qnode = qml.QNode(transformed_qfunc, dev)
+                transformed_result = transformed_qnode(0.3, 0.4, 0.5)
+
+            @staticmethod
+            def merge_rotations(tape):
+                return qml.transforms.merge_rotations.tape_fn(tape)
+
+            @staticmethod
+            def commute_controlled(tape):
+                return qml.transforms.commute_controlled.tape_fn(tape, direction="left")
+
+        spy = mocker.spy(DummyTransforms, transform_name)
+
+        d = DummyTransforms()
+        d.run_pipeline()
+
+        assert len(spy.call_args_list) == num_passes
+
+
+class TestCompileIntegration:
+    """Integration tests to verify outputs of compilation pipelines."""
+
+    @pytest.mark.parametrize(("wires"), [["a", "b", "c"], [0, 1, 2], [3, 1, 2], [0, "a", 4]])
+    def test_compile_empty_pipeline(self, wires):
+        """Test that an empty pipeline returns the original function."""
+
+        qfunc = build_qfunc(wires)
+        dev = qml.device("default.qubit", wires=wires)
+
+        qnode = qml.QNode(qfunc, dev)
+
+        transformed_qfunc = compile(pipeline=[])(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        original_result = qnode(0.3, 0.4, 0.5)
+        transformed_result = transformed_qnode(0.3, 0.4, 0.5)
+        assert np.allclose(original_result, transformed_result)
+
+        names_expected = [op.name for op in qnode.qtape.operations]
+        wires_expected = [op.wires for op in qnode.qtape.operations]
+
+        compare_operation_lists(transformed_qnode.qtape.operations, names_expected, wires_expected)
+
+    @pytest.mark.parametrize(("wires"), [["a", "b", "c"], [0, 1, 2], [3, 1, 2], [0, "a", 4]])
+    def test_compile_default_pipeline(self, wires):
+        """Test that the default pipeline returns the correct results."""
+
+        qfunc = build_qfunc(wires)
+        dev = qml.device("default.qubit", wires=Wires(wires))
+
+        qnode = qml.QNode(qfunc, dev)
+
+        transformed_qfunc = compile()(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        original_result = qnode(0.3, 0.4, 0.5)
+        transformed_result = transformed_qnode(0.3, 0.4, 0.5)
+        assert np.allclose(original_result, transformed_result)
+
+        names_expected = ["Hadamard", "CNOT", "RX", "CY", "PauliY"]
+        wires_expected = [
+            Wires(wires[0]),
+            Wires([wires[2], wires[1]]),
+            Wires(wires[0]),
+            Wires([wires[1], wires[2]]),
+            Wires(wires[2]),
+        ]
+
+        compare_operation_lists(transformed_qnode.qtape.operations, names_expected, wires_expected)
+
+    @pytest.mark.parametrize(("wires"), [["a", "b", "c"], [0, 1, 2], [3, 1, 2], [0, "a", 4]])
+    def test_compile_pipeline_with_non_default_arguments(self, wires):
+        """Test that using non-default arguments returns the correct results."""
+
+        qfunc = build_qfunc(wires)
+        dev = qml.device("default.qubit", wires=Wires(wires))
+
+        qnode = qml.QNode(qfunc, dev)
+
+        pipeline = [
+            commute_controlled(direction="left"),
+            cancel_inverses,
+            merge_rotations(atol=1e-6),
+        ]
+
+        transformed_qfunc = compile(pipeline=pipeline)(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        original_result = qnode(0.3, 0.4, 0.5)
+        transformed_result = transformed_qnode(0.3, 0.4, 0.5)
+        assert np.allclose(original_result, transformed_result)
+
+        names_expected = ["Hadamard", "CNOT", "RX", "PauliY", "CY"]
+        wires_expected = [
+            Wires(wires[0]),
+            Wires([wires[2], wires[1]]),
+            Wires(wires[0]),
+            Wires(wires[2]),
+            Wires([wires[1], wires[2]]),
+        ]
+
+        compare_operation_lists(transformed_qnode.qtape.operations, names_expected, wires_expected)
+
+    @pytest.mark.parametrize(("wires"), [["a", "b", "c"], [0, 1, 2], [3, 1, 2], [0, "a", 4]])
+    def test_compile_multiple_passes(self, wires):
+        """Test that running multiple passes produces the correct results."""
+
+        qfunc = build_qfunc(wires)
+        dev = qml.device("default.qubit", wires=Wires(wires))
+
+        qnode = qml.QNode(qfunc, dev)
+
+        # Rotation merging will not occur at all until commuting gates are
+        # pushed through
+        pipeline = [merge_rotations, commute_controlled(direction="left"), cancel_inverses]
+
+        transformed_qfunc = compile(pipeline=pipeline, num_passes=2)(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        original_result = qnode(0.3, 0.4, 0.5)
+        transformed_result = transformed_qnode(0.3, 0.4, 0.5)
+        assert np.allclose(original_result, transformed_result)
+
+        names_expected = ["Hadamard", "CNOT", "RX", "PauliY", "CY"]
+        wires_expected = [
+            Wires(wires[0]),
+            Wires([wires[2], wires[1]]),
+            Wires(wires[0]),
+            Wires(wires[2]),
+            Wires([wires[1], wires[2]]),
+        ]
+
+        compare_operation_lists(transformed_qnode.qtape.operations, names_expected, wires_expected)
+
+    @pytest.mark.parametrize(("wires"), [["a", "b", "c"], [0, 1, 2], [3, 1, 2], [0, "a", 4]])
+    def test_compile_decompose_into_basis_gates(self, wires):
+        """Test that running multiple passes produces the correct results."""
+
+        qfunc = build_qfunc(wires)
+        dev = qml.device("default.qubit", wires=Wires(wires))
+
+        qnode = qml.QNode(qfunc, dev)
+
+        pipeline = [commute_controlled(direction="left"), cancel_inverses, merge_rotations]
+
+        basis_set = ["CNOT", "RX", "RY", "RZ"]
+
+        transformed_qfunc = compile(pipeline=pipeline, basis_set=basis_set)(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        original_result = qnode(0.3, 0.4, 0.5)
+        transformed_result = transformed_qnode(0.3, 0.4, 0.5)
+        assert np.allclose(original_result, transformed_result)
+
+        names_expected = [
+            "RZ",
+            "RX",
+            "RZ",
+            "RZ",
+            "CNOT",
+            "RX",
+            "RZ",
+            "RY",
+            "RZ",
+            "RY",
+            "CNOT",
+            "RY",
+            "CNOT",
+        ]
+
+        wires_expected = [
+            Wires(wires[0]),
+            Wires(wires[0]),
+            Wires(wires[0]),
+            Wires(wires[2]),
+            Wires([wires[2], wires[1]]),
+            Wires(wires[0]),
+            Wires(wires[1]),
+            Wires(wires[2]),
+            Wires(wires[2]),
+            Wires(wires[2]),
+            Wires([wires[1], wires[2]]),
+            Wires(wires[2]),
+            Wires([wires[1], wires[2]]),
+        ]
+
+        compare_operation_lists(transformed_qnode.qtape.operations, names_expected, wires_expected)
+
+    def test_compile_template(self):
+        """Test that functions with templates are correctly expanded and compiled."""
+
+        # Push commuting gates to the right and merging rotations gives a circuit
+        # with alternating RX and CNOT gates
+        def qfunc(x, params):
+            qml.templates.AngleEmbedding(x, wires=range(3))
+            qml.templates.BasicEntanglerLayers(params, wires=range(3))
+            return qml.expval(qml.PauliZ(wires=2))
+
+        dev = qml.device("default.qubit", wires=3)
+        qnode = qml.QNode(qfunc, dev)
+
+        pipeline = [commute_controlled, merge_rotations]
+        transformed_qfunc = compile(pipeline=pipeline)(qfunc)
+        transformed_qnode = qml.QNode(transformed_qfunc, dev)
+
+        x = np.array([0.1, 0.2, 0.3])
+        params = np.ones((2, 3))
+
+        original_result = qnode(x, params)
+        transformed_result = transformed_qnode(x, params)
+        assert np.allclose(original_result, transformed_result)
+
+        names_expected = ["RX", "CNOT"] * 6
+        wires_expected = [
+            Wires(0),
+            Wires([0, 1]),
+            Wires(1),
+            Wires([1, 2]),
+            Wires(2),
+            Wires([2, 0]),
+        ] * 2
+
+        compare_operation_lists(transformed_qnode.qtape.operations, names_expected, wires_expected)
+
+
+def qfunc(x, params):
+    qml.templates.AngleEmbedding(x, wires=range(3))
+    qml.templates.BasicEntanglerLayers(params, wires=range(3))
+    return qml.expval(qml.PauliZ(wires=2))
+
+
+pipeline = [commute_controlled(direction="left"), merge_rotations]
+
+transformed_qfunc = compile(pipeline=pipeline)(qfunc)
+
+dev = qml.device("default.qubit", wires=3)
+
+expected_op_list = ["RX"] * 3 + ["CNOT", "CNOT", "RX", "CNOT", "RX", "RX"] + ["CNOT"] * 3
+
+expected_wires_list = [
+    Wires(0),
+    Wires(1),
+    Wires(2),
+    Wires([0, 1]),
+    Wires([1, 2]),
+    Wires(0),
+    Wires([2, 0]),
+    Wires(1),
+    Wires(2),
+    Wires([0, 1]),
+    Wires([1, 2]),
+    Wires([2, 0]),
+]
+
+
+class TestCompileInterfaces:
+    """Test that the top-level compile function works across all interfaces."""
+
+    @pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift"])
+    def test_compile_autograd(self, diff_method):
+        """Test QNode and gradient in autograd interface."""
+
+        original_qnode = qml.QNode(qfunc, dev, interface="autograd", diff_method=diff_method)
+        transformed_qnode = qml.QNode(
+            transformed_qfunc, dev, interface="autograd", diff_method=diff_method
+        )
+
+        x = np.array([0.1, 0.2, 0.3], requires_grad=False)
+        params = np.ones((2, 3))
+
+        # Check that the numerical output is the same
+        assert qml.math.allclose(original_qnode(x, params), transformed_qnode(x, params))
+
+        # Check that the gradient is the same
+        assert qml.math.allclose(
+            qml.grad(original_qnode)(x, params), qml.grad(transformed_qnode)(x, params)
+        )
+
+        # Check operation list
+        ops = transformed_qnode.qtape.operations
+        compare_operation_lists(ops, expected_op_list, expected_wires_list)
+
+    def test_compile_torch(self):
+        """Test QNode and gradient in torch interface."""
+        torch = pytest.importorskip("torch", minversion="1.8")
+
+        original_qnode = qml.QNode(qfunc, dev, interface="torch", diff_method="parameter-shift")
+        transformed_qnode = qml.QNode(
+            transformed_qfunc, dev, interface="torch", diff_method="parameter-shift"
+        )
+
+        original_x = torch.tensor([0.3, -0.2, 0.8], requires_grad=False)
+        original_params = torch.ones((2, 3), requires_grad=True)
+
+        transformed_x = torch.tensor([0.3, -0.2, 0.8], requires_grad=False)
+        transformed_params = torch.ones((2, 3), requires_grad=True)
+
+        original_result = original_qnode(original_x, original_params)
+        transformed_result = transformed_qnode(transformed_x, transformed_params)
+
+        # Check that the numerical output is the same
+        assert qml.math.allclose(original_result, transformed_result)
+
+        # Check that the gradient is the same
+        original_result.backward()
+        transformed_result.backward()
+
+        assert qml.math.allclose(original_params.grad, transformed_params.grad)
+
+        # Check operation list
+        ops = transformed_qnode.qtape.operations
+        compare_operation_lists(ops, expected_op_list, expected_wires_list)
+
+    @pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift"])
+    def test_compile_tf(self, diff_method):
+        """Test QNode and gradient in tensorflow interface."""
+        tf = pytest.importorskip("tensorflow")
+
+        original_qnode = qml.QNode(qfunc, dev, interface="tf", diff_method=diff_method)
+        transformed_qnode = qml.QNode(
+            transformed_qfunc, dev, interface="tf", diff_method=diff_method
+        )
+
+        original_x = tf.Variable([0.8, -0.6, 0.4])
+        original_params = tf.Variable(tf.ones((2, 3)))
+
+        transformed_x = tf.Variable([0.8, -0.6, 0.4])
+        transformed_params = tf.Variable(tf.ones((2, 3)))
+
+        original_result = original_qnode(original_x, original_params)
+        transformed_result = transformed_qnode(transformed_x, transformed_params)
+
+        # Check that the numerical output is the same
+        assert qml.math.allclose(original_result, transformed_result)
+
+        # Check that the gradient is the same
+        with tf.GradientTape() as tape:
+            loss = original_qnode(original_x, original_params)
+        original_grad = tape.gradient(loss, original_params)
+
+        with tf.GradientTape() as tape:
+            loss = transformed_qnode(transformed_x, transformed_params)
+        transformed_grad = tape.gradient(loss, transformed_params)
+
+        assert qml.math.allclose(original_grad, transformed_grad)
+
+        # Check operation list
+        ops = transformed_qnode.qtape.operations
+        compare_operation_lists(ops, expected_op_list, expected_wires_list)
+
+    @pytest.mark.parametrize("diff_method", ["backprop", "parameter-shift"])
+    def test_compile_jax(self, diff_method):
+        """Test QNode and gradient in JAX interface."""
+        jax = pytest.importorskip("jax")
+        from jax import numpy as jnp
+
+        from jax.config import config
+
+        remember = config.read("jax_enable_x64")
+        config.update("jax_enable_x64", True)
+
+        original_qnode = qml.QNode(qfunc, dev, interface="jax", diff_method=diff_method)
+        transformed_qnode = qml.QNode(
+            transformed_qfunc, dev, interface="jax", diff_method=diff_method
+        )
+
+        x = jnp.array([0.1, 0.2, 0.3], dtype=jnp.float64)
+        params = jnp.ones((2, 3), dtype=jnp.float64)
+
+        # Check that the numerical output is the same
+        assert qml.math.allclose(original_qnode(x, params), transformed_qnode(x, params))
+
+        # Check that the gradient is the same
+        assert qml.math.allclose(
+            jax.grad(original_qnode, argnums=(1))(x, params),
+            jax.grad(transformed_qnode, argnums=(1))(x, params),
+            atol=1e-7,
+        )
+
+        # Check operation list
+        ops = transformed_qnode.qtape.operations
+        compare_operation_lists(ops, expected_op_list, expected_wires_list)