Repository: PennyLaneAI/pennylane
Pull request #1157 (2021-03-23 00:38:08)

Control transform
**Context:**
This takes heavy inspiration from https://github.com/PennyLaneAI/pennylane/pull/1134 and is based off the discussion at https://github.com/PennyLaneAI/adrs/pull/9

**Description of the Change:**
One of the main features requested at QHack was the ability to perform control based on subcircuits/subroutines. Here, we add a `qml.ctrl` transform function and a new `ControlledOperation` operation to support such a feature.

Example:

```python
def my_anzats(params):
   qml.RX(params[0], wires=0)
   qml.RZ(params[1], wires=1)

# Create a new method that applies `my_anzats`
# controlled by the "2" wire.
my_anzats2 = ctrl(my_anzats, control=2)

@qml.qnode(...)
def circuit(params):
    my_anzats2(params)
    return qml.state() 
```

The `circuit` function above is mathematically equivalent to the below code.

```python

  @qml.qnode(...)
  def circuit(params):
      qml.CRX(params[0], wires=[2, 0])
      qml.CRZ(params[1], wires=[2, 1])
      return qml.state()
```

One of the major benefits of this design is that devices now have the ability to exploit control sparsity on large subroutines. This could potentially make future simulators much faster. For devices that don't support `ControlledOperation`, they can simply expand the operation into native gates.

**Benefits:**
Users can now add control to any sequence of operations very easily. In the future, devices can exploit the additional control information in order to execute more efficient circuits or simulations.

**Possible Drawbacks:**
Right now, the control operation has to be expanded before execution, and none of the existing devices support specialized control yet. This could hurt performance in the short term. 

**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/1157?src=pr&el=h1) Report
> Merging [#1157](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157?src=pr&el=desc) (ab644f1) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/9a3cee8ba0e1dc4b6391ee82aa29ee424b953125?el=desc) (9a3cee8) will **increase** coverage by `0.02%`.
> The diff coverage is `98.43%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1157      +/-   ##
==========================================
+ Coverage   97.97%   97.99%   +0.02%     
==========================================
  Files         142      144       +2     
  Lines       10279    10440     +161     
==========================================
+ Hits        10071    10231     +160     
- Misses        208      209       +1     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/transforms/control.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvY29udHJvbC5weQ==) | `97.82% <97.82%> (ø)` | |
| [pennylane/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL19faW5pdF9fLnB5) | `98.57% <100.00%> (+0.04%)` | :arrow_up: |
| [pennylane/ops/qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL29wcy9xdWJpdC5weQ==) | `98.05% <100.00%> (+0.14%)` | :arrow_up: |
| [pennylane/transforms/registrations.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RyYW5zZm9ybXMvcmVnaXN0cmF0aW9ucy5weQ==) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/cv.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL29wcy9jdi5weQ==) | `100.00% <0.00%> (ø)` | |
| [pennylane/math/fn.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL21hdGgvZm4ucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/math/tf\_box.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL21hdGgvdGZfYm94LnB5) | `100.00% <0.00%> (ø)` | |
| [pennylane/math/jax\_box.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL21hdGgvamF4X2JveC5weQ==) | `100.00% <0.00%> (ø)` | |
| [pennylane/math/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL21hdGgvX19pbml0X18ucHk=) | `100.00% <0.00%> (ø)` | |
| [pennylane/devices/tf\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree#diff-cGVubnlsYW5lL2RldmljZXMvdGZfb3BzLnB5) | `100.00% <0.00%> (ø)` | |
| ... and [14 more](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1157/diff?src=pr&el=tree-more) | |

------

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

Thenerdstation: > To still maintain custom mappings for the user, I'd find it much better to allow for a kwarg in ctrl which defines a custom mapping, and when going through the operations to control one can replace the ones specified with whatever the user wants...this could be a more general design for other transforms too...

That's a good idea
Thenerdstation: @mariaschuld I replaced the `register_control_transfrom` with just an object method.

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 2ed70fb58..ee66d45f4 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,64 @@
 
 <h3>New features since last release</h3>
 
+* Adds a new transform `qml.ctrl` that adds control wires to subroutines.
+  [(#1157)](https://github.com/PennyLaneAI/pennylane/pull/1157)
+
+  Here's a simple usage example:
+
+  ```python
+  def my_ansatz(params):
+     qml.RX(params[0], wires=0)
+     qml.RZ(params[1], wires=1)
+
+  # Create a new method that applies `my_ansatz`
+  # controlled by the "2" wire.
+  my_anzats2 = qml.ctrl(my_ansatz, control=2)
+
+  @qml.qnode(...)
+  def circuit(params):
+      my_ansatz2(params)
+      return qml.state()
+  ```
+
+  The above `circuit` would be equivalent to:
+
+  ```python
+  @qml.qnode(...)
+  def circuit(params):
+      qml.CRX(params[0], wires=[2, 0])
+      qml.CRZ(params[1], wires=[2, 1])
+      return qml.state()
+  ```
+
+  The `qml.ctrl` transform is especially useful to repeatedly apply an
+  operation which is controlled by different qubits in each repetition. A famous example is Shor's algorithm.
+
+  ```python
+  def modmul(a, mod, wires):
+      # Some complex set of gates that implements modular multiplcation.
+      # qml.CNOT(...); qml.Toffoli(...); ... 
+ 
+  @qml.qnode(...)
+  def shor(a, mod, scratch_wires, qft_wires):
+      for i, wire in enumerate(qft_wires):
+          qml.Hadamard(wire)
+
+          # Create the controlled modular multiplication 
+          # subroutine based on the control wire.
+          cmodmul = qml.ctrl(modmul, control=wire)
+
+          # Execute the controlled modular multiplication.
+          cmodmul(a ** i, mod, scratch_wires)
+ 
+      qml.adjoint(qml.QFT)(qft_wires)
+      return qml.sample()
+
+  ```
+
+  In the future, devices will be able to exploit the sparsity of controlled operations to 
+  improve simulation performance. 
+
 * Adds a new optimizer `qml.ShotAdaptiveOptimizer`, a gradient-descent optimizer where
   the shot rate is adaptively calculated using the variances of the parameter-shift gradient.
   [(#1139)](https://github.com/PennyLaneAI/pennylane/pull/1139)
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 08c81aa78..ccb7af3b4 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -40,7 +40,9 @@ from pennylane.ops import *
 from pennylane.optimize import *
 from pennylane.qnode import QNode, qnode
 from pennylane.templates import broadcast, layer, template
-from pennylane.transforms import adjoint, draw, measurement_grouping, metric_tensor
+from pennylane.transforms import draw, measurement_grouping, metric_tensor
+from pennylane.transforms.adjoint import adjoint
+from pennylane.transforms.control import ctrl, ControlledOperation
 from pennylane.utils import inv
 from pennylane.vqe import ExpvalCost, Hamiltonian, VQECost
 
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 2498c24e9..f0258517d 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -623,7 +623,7 @@ class Operation(Operator):
             operations decomposition, or if not implemented, simply
             the operation itself.
         """
-        tape = qml.tape.QuantumTape()
+        tape = qml.tape.QuantumTape(do_queue=False)
 
         with tape:
             self.decomposition(*self.data, wires=self.wires)
diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py
index e3f8619bc..5ed613ba2 100644
--- a/pennylane/ops/qubit.py
+++ b/pennylane/ops/qubit.py
@@ -150,6 +150,9 @@ class PauliX(Observable, Operation):
     def adjoint(self, do_queue=False):
         return PauliX(wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CNOT(wires=Wires(wire) + self.wires)
+
 
 class PauliY(Observable, Operation):
     r"""PauliY(wires)
@@ -207,6 +210,9 @@ class PauliY(Observable, Operation):
     def adjoint(self, do_queue=False):
         return PauliY(wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CY(wires=Wires(wire) + self.wires)
+
 
 class PauliZ(Observable, DiagonalOperation):
     r"""PauliZ(wires)
@@ -247,6 +253,9 @@ class PauliZ(Observable, DiagonalOperation):
     def adjoint(self, do_queue=False):
         return PauliZ(wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CZ(wires=Wires(wire) + self.wires)
+
 
 class S(DiagonalOperation):
     r"""S(wires)
@@ -400,6 +409,9 @@ class CNOT(Operation):
     def adjoint(self, do_queue=False):
         return CNOT(wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        Toffoli(wires=Wires(wire) + self.wires)
+
 
 class CZ(DiagonalOperation):
     r"""CZ(wires)
@@ -517,6 +529,9 @@ class SWAP(Operation):
     def adjoint(self, do_queue=False):
         return SWAP(wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CSWAP(wires=wire + self.wires)
+
 
 class CSWAP(Operation):
     r"""CSWAP(wires)
@@ -654,6 +669,9 @@ class RX(Operation):
     def adjoint(self, do_queue=False):
         return RX(-self.data[0], wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CRX(*self.parameters, wires=wire + self.wires)
+
 
 class RY(Operation):
     r"""RY(phi, wires)
@@ -692,6 +710,9 @@ class RY(Operation):
     def adjoint(self, do_queue=False):
         return RY(-self.data[0], wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CRY(*self.parameters, wires=wire + self.wires)
+
 
 class RZ(DiagonalOperation):
     r"""RZ(phi, wires)
@@ -736,6 +757,9 @@ class RZ(DiagonalOperation):
     def adjoint(self, do_queue=False):
         return RZ(-self.data[0], wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CRZ(*self.parameters, wires=wire + self.wires)
+
 
 class PhaseShift(DiagonalOperation):
     r"""PhaseShift(phi, wires)
@@ -781,6 +805,9 @@ class PhaseShift(DiagonalOperation):
     def adjoint(self, do_queue=False):
         return PhaseShift(-self.data[0], wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        ControlledPhaseShift(*self.parameters, wires=wire + self.wires)
+
 
 class ControlledPhaseShift(DiagonalOperation):
     r"""ControlledPhaseShift(phi, wires)
@@ -894,6 +921,9 @@ class Rot(Operation):
         phi, theta, omega = self.parameters
         return Rot(-omega, -theta, -phi, wires=self.wires, do_queue=do_queue)
 
+    def _controlled(self, wire):
+        CRot(*self.parameters, wires=wire + self.wires)
+
 
 class MultiRZ(DiagonalOperation):
     r"""MultiRZ(theta, wires)
@@ -1189,7 +1219,7 @@ class CRX(Operation):
     .. math::
 
         \begin{align}
-            CR_x(\phi) &= 
+            CR_x(\phi) &=
             \begin{bmatrix}
             & 1 & 0 & 0 & 0 \\
             & 0 & 1 & 0 & 0\\
@@ -1248,7 +1278,7 @@ class CRY(Operation):
     .. math::
 
         \begin{align}
-            CR_y(\phi) &= 
+            CR_y(\phi) &=
             \begin{bmatrix}
                 1 & 0 & 0 & 0 \\
                 0 & 1 & 0 & 0\\
@@ -1305,7 +1335,7 @@ class CRZ(DiagonalOperation):
     .. math::
 
         \begin{align}
-             CR_z(\phi) &= 
+             CR_z(\phi) &=
              \begin{bmatrix}
                 1 & 0 & 0 & 0 \\
                 0 & 1 & 0 & 0\\
@@ -1814,6 +1844,9 @@ class QubitUnitary(Operation):
             qml.math.T(qml.math.conj(self.data[0])), wires=self.wires, do_queue=do_queue
         )
 
+    def _controlled(self, wire):
+        ControlledQubitUnitary(*self.parameters, control_wires=wire, wires=self.wires)
+
 
 class ControlledQubitUnitary(QubitUnitary):
     r"""ControlledQubitUnitary(U, control_wires, wires, control_values)
@@ -1908,7 +1941,7 @@ class ControlledQubitUnitary(QubitUnitary):
                 raise ValueError("Length of control bit string must equal number of control wires.")
 
             # Make sure all values are either 0 or 1
-            if any([x not in ["0", "1"] for x in control_values]):
+            if any(x not in ["0", "1"] for x in control_values):
                 raise ValueError("String of control values can contain only '0' or '1'.")
 
             control_int = int(control_values, 2)
@@ -1917,6 +1950,9 @@ class ControlledQubitUnitary(QubitUnitary):
 
         return control_int
 
+    def _controlled(self, wire):
+        ControlledQubitUnitary(*self.parameters, control_wires=wire, wires=self.wires)
+
 
 class MultiControlledX(ControlledQubitUnitary):
     r"""MultiControlledX(control_wires, wires, control_values)
@@ -1997,7 +2033,15 @@ class DiagonalQubitUnitary(DiagonalOperation):
         return [QubitUnitary(np.diag(D), wires=wires)]
 
     def adjoint(self, do_queue=False):
-        return DiagonalQubitUnitary(self.parameters[0].conj(), wires=self.wires, do_queue=do_queue)
+        return DiagonalQubitUnitary(
+            qml.math.conj(self.parameters[0]), wires=self.wires, do_queue=do_queue
+        )
+
+    def _controlled(self, control):
+        DiagonalQubitUnitary(
+            qml.math.concatenate([np.array([1, 1]), self.parameters[0]]),
+            wires=Wires(control) + self.wires,
+        )
 
 
 class QFT(Operation):
diff --git a/pennylane/transforms/control.py b/pennylane/transforms/control.py
new file mode 100644
index 000000000..a651835e6
--- /dev/null
+++ b/pennylane/transforms/control.py
@@ -0,0 +1,183 @@
+# 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.
+"""
+Contains the control transform.
+"""
+from functools import wraps
+from pennylane.tape import QuantumTape
+from pennylane.operation import Operation, AnyWires
+from pennylane.wires import Wires
+from pennylane.transforms.adjoint import adjoint
+
+
+def requeue_ops_in_tape(tape):
+    """Requeue all of the operations in a tape directly to the current tape context"""
+    for op in tape.operations:
+        op.queue()
+
+
+def expand_with_control(tape, control_wire):
+    """Expand a tape to include a control wire on all queued operations.
+
+    Args:
+        tape (.QuantumTape): quantum tape to be controlled
+        control_wire (int): a single wire to use as the control wire
+
+    Returns:
+        .QuantumTape: A new QuantumTape with the controlled operations.
+    """
+    with QuantumTape(do_queue=False) as new_tape:
+        for op in tape.operations:
+            if hasattr(op, "_controlled"):
+                # Execute the controlled version of the operation
+                # and add that the to the tape context.
+                # pylint: disable=protected-access
+                op._controlled(control_wire)
+            else:
+                tmp_tape = op.expand()
+                tmp_tape = expand_with_control(tmp_tape, control_wire)
+                requeue_ops_in_tape(tmp_tape)
+    return new_tape
+
+
+class ControlledOperation(Operation):
+    """A Controlled Operation.
+
+    Unless you are a Pennylane plugin developer, **you should NOT directly use this class**,
+    instead, use the :func:`qml.ctrl <.ctrl>` function.
+
+    The ``ControlledOperation`` class is a container class that defines a set of operations that
+    should by applied relative to a single control wire or a list of control wires.
+
+    Certain simulators and quantum computers can take advantage of the controlled gate sparsity,
+    while other devices must rely on the op-by-op decomposition defined by the ``op.expand``
+    method.
+
+    Args:
+        tape: A QuantumTape. This tape defines the unitary that should be applied relative
+            to the control wires.
+        control_wires: A wire or set of wires.
+    """
+
+    par_domain = "A"
+    num_wires = AnyWires
+    num_params = property(lambda self: self.subtape.num_params)
+
+    def __init__(self, tape, control_wires, do_queue=True):
+        self.subtape = tape
+        """QuantumTape: The tape that defines the underlying operation."""
+
+        self.control_wires = Wires(control_wires)
+        """Wires: The control wires."""
+
+        wires = self.control_wires + tape.wires
+        super().__init__(*tape.get_parameters(), wires=wires, do_queue=do_queue)
+
+    def expand(self):
+        tape = self.subtape
+        for wire in self.control_wires:
+            tape = expand_with_control(tape, wire)
+        return tape
+
+    def adjoint(self, do_queue=False):
+        with QuantumTape(do_queue=False) as new_tape:
+            # Execute all ops adjointed.
+            adjoint(requeue_ops_in_tape)(self.subtape)
+        return ControlledOperation(new_tape, self.control_wires, do_queue=do_queue)
+
+    def _controlled(self, wires):
+        ControlledOperation(tape=self.subtape, control_wires=Wires(wires) + self.control_wires)
+
+
+def ctrl(fn, control):
+    """Create a method that applies a controlled version of the provided method.
+
+    Args:
+        fn (function): Any python function that applies pennylane operations.
+        control (Wires): The control wire(s).
+
+    Returns:
+        function: A new function that applies the controlled equivalent of ``fn``. The returned
+        function takes the same input arguments as ``fn``.
+
+    **Example**
+
+    .. code-block:: python3
+
+        def ops(params):
+            qml.RX(params[0], wires=0)
+            qml.RZ(params[1] wires=3)
+
+        ops1 = qml.ctrl(ops, control=1)
+        ops2 = qml.ctrl(ops, control=2)
+
+        @qml.qnode(dev)
+        def my_circuit():
+            ops1(params=[0.123, 0.456])
+            ops1(params=[0.789, 1.234])
+            ops2(params=[2.987, 3.654])
+            ops2(params=[2.321, 1.111])
+            return qml.state()
+
+    The above code would be equivalent to
+
+    .. code-block:: python3
+
+        @qml.qnode(dev)
+        def my_circuit(params):
+            # ops1(params=[0.123, 0.456])
+            qml.CRX(0.123, wires=[1, 0])
+            qml.CRZ(0.456, wires=[1, 3])
+
+            # ops1(params=[0.789, 1.234])
+            qml.CRX(0.789, wires=[1, 0])
+            qml.CRZ(1.234, (wires=[1, 3])
+
+            # ops2(params=[2.987, 3.654])
+            qml.CRX(2.987, wires=[2, 0])
+            qml.CRZ(3.654, wires=[2, 3])
+
+            # ops2(params=[2.321, 1.111])
+            qml.CRX(2.321, wires=[2, 0])
+            qml.CRZ(1.111, wires=[2, 3])
+            return qml.state()
+
+    .. Note::
+
+        Some devices are able to take advantage of the inherient sparsity of a
+        controlled operation. In those cases, it may be more efficient to use
+        this transform rather than adding controls by hand. For devices that don't
+        have special control support, the operation is expanded to add control wires
+        to each underlying op individually.
+
+    .. UsageDetails::
+
+        **Nesting Controls**
+
+        The ``ctrl`` transform can be nested with itself arbitrarily.
+
+        .. code-block:: python3
+
+            # These two ops are equivalent.
+            op1 = qml.ctrl(qml.ctrl(my_ops, 1), 2)
+            op2 = qml.ctrl(my_ops, [2, 1])
+    """
+
+    @wraps(fn)
+    def wrapper(*args, **kwargs):
+        with QuantumTape(do_queue=False) as tape:
+            fn(*args, **kwargs)
+        return ControlledOperation(tape, control)
+
+    return wrapper


Test patch:
diff --git a/tests/transforms/test_control.py b/tests/transforms/test_control.py
new file mode 100644
index 000000000..91608f72a
--- /dev/null
+++ b/tests/transforms/test_control.py
@@ -0,0 +1,212 @@
+from functools import partial
+import numpy as np
+import pennylane as qml
+from pennylane.tape import QuantumTape
+from pennylane.transforms.control import ctrl, ControlledOperation
+from pennylane.tape.tape import expand_tape
+
+
+def assert_equal_operations(ops1, ops2):
+    """Assert that two list of operations are equivalent"""
+    assert len(ops1) == len(ops2)
+    for op1, op2 in zip(ops1, ops2):
+        assert type(op1) == type(op2)
+        assert op1.wires == op2.wires
+        np.testing.assert_allclose(op1.parameters, op2.parameters)
+
+
+def test_control_sanity_check():
+    """Test that control works on a very standard usecase."""
+    def make_ops():
+        qml.RX(0.123, wires=0)
+        qml.RY(0.456, wires=2)
+        qml.RX(0.789, wires=0)
+        qml.Rot(0.111, 0.222, 0.333, wires=2),
+        qml.PauliX(wires=2)
+        qml.PauliY(wires=4)
+        qml.PauliZ(wires=0)
+
+    with QuantumTape() as tape:
+        cmake_ops = ctrl(make_ops, control=1)
+        #Execute controlled version.
+        cmake_ops()
+
+    expected = [
+        qml.CRX(0.123, wires=[1, 0]),
+        qml.CRY(0.456, wires=[1, 2]),
+        qml.CRX(0.789, wires=[1, 0]),
+        qml.CRot(0.111, 0.222, 0.333, wires=[1, 2]),
+        qml.CNOT(wires=[1, 2]),
+        qml.CY(wires=[1, 4]),
+        qml.CZ(wires=[1, 0]),
+    ]
+    assert len(tape.operations) == 1
+    ctrl_op = tape.operations[0]
+    assert isinstance(ctrl_op, ControlledOperation)
+    expanded = ctrl_op.expand()
+    assert_equal_operations(expanded.operations, expected)
+
+
+def test_adjoint_of_control():
+    """Test adjoint(ctrl(fn)) and ctrl(adjoint(fn))"""
+    def my_op(a, b, c):
+        qml.RX(a, wires=2)
+        qml.RY(b, wires=3)
+        qml.RZ(c, wires=0)
+
+    with QuantumTape() as tape1:
+        cmy_op_dagger = qml.adjoint(ctrl(my_op, 5))
+        # Execute controlled and adjointed version of my_op.
+        cmy_op_dagger(0.789, 0.123, c=0.456)
+
+    with QuantumTape() as tape2:
+        cmy_op_dagger = ctrl(qml.adjoint(my_op), 5)
+        # Execute adjointed and controlled version of my_op.
+        cmy_op_dagger(0.789, 0.123, c=0.456)
+
+    expected = [
+        qml.CRZ(-0.456, wires=[5, 0]),
+        qml.CRY(-0.123, wires=[5, 3]),
+        qml.CRX(-0.789, wires=[5, 2]),
+    ]
+    for tape in [tape1, tape2]:
+        assert len(tape.operations) == 1
+        ctrl_op = tape.operations[0]
+        assert isinstance(ctrl_op, ControlledOperation)
+        expanded = ctrl_op.expand()
+        assert_equal_operations(expanded.operations, expected)
+
+def test_nested_control():
+    """Test nested use of control"""
+    with QuantumTape() as tape:
+        CCX = ctrl(ctrl(qml.PauliX, 7), 3)
+        CCX(wires=0)
+    assert len(tape.operations) == 1
+    op = tape.operations[0]
+    assert isinstance(op, ControlledOperation)
+    new_tape = expand_tape(tape, 3)
+    assert_equal_operations(new_tape.operations, [qml.Toffoli(wires=[7, 3, 0])])
+
+def test_multi_control():
+    """Test control with a list of wires."""
+    with QuantumTape() as tape:
+        CCX = ctrl(qml.PauliX, control=[3, 7])
+        CCX(wires=0)
+    assert len(tape.operations) == 1
+    op = tape.operations[0]
+    assert isinstance(op, ControlledOperation)
+    new_tape = expand_tape(tape, 3)
+    assert_equal_operations(new_tape.operations, [qml.Toffoli(wires=[7, 3, 0])])
+
+def test_control_with_qnode():
+    """Test ctrl works when in a qnode cotext."""
+    dev = qml.device("default.qubit", wires=3)
+
+    def my_ansatz(params):
+        qml.RY(params[0], wires=0)
+        qml.RY(params[1], wires=1)
+        qml.CNOT(wires=[0, 1])
+        qml.RX(params[2], wires=1)
+        qml.RX(params[3], wires=0)
+        qml.CNOT(wires=[1, 0])
+
+    def controlled_ansatz(params):
+        qml.CRY(params[0], wires=[2, 0])
+        qml.CRY(params[1], wires=[2, 1])
+        qml.Toffoli(wires=[2, 0, 1])
+        qml.CRX(params[2], wires=[2, 1])
+        qml.CRX(params[3], wires=[2, 0])
+        qml.Toffoli(wires=[2, 1, 0])
+
+    def circuit(ansatz, params):
+        qml.RX(np.pi/4.0, wires=2)
+        ansatz(params)
+        return qml.state()
+
+    params = [0.123, 0.456, 0.789, 1.345]
+    circuit1 = qml.qnode(dev)(partial(circuit, ansatz=ctrl(my_ansatz, 2)))
+    circuit2 = qml.qnode(dev)(partial(circuit, ansatz=controlled_ansatz))
+    res1 = circuit1(params=params)
+    res2 = circuit2(params=params)
+    np.testing.assert_allclose(res1, res2) 
+
+
+def test_ctrl_within_ctrl():
+    """Test using ctrl on a method that uses ctrl."""
+    def ansatz(params):
+        qml.RX(params[0], wires=0)
+        ctrl(qml.PauliX, control=0)(wires=1)
+        qml.RX(params[1], wires=0)
+
+    controlled_ansatz = ctrl(ansatz, 2)
+
+    with QuantumTape() as tape:
+        controlled_ansatz([0.123, 0.456])
+
+    tape = expand_tape(tape, 2, stop_at=lambda op: not isinstance(op, ControlledOperation))
+
+    expected = [
+        qml.CRX(0.123, wires=[2, 0]),
+        qml.Toffoli(wires=[0, 2, 1]),
+        qml.CRX(0.456, wires=[2, 0])
+    ]
+    assert_equal_operations(tape.operations, expected)
+
+def test_diagonal_ctrl():
+    """Test ctrl on diagonal gates."""
+    with QuantumTape() as tape:
+        ctrl(qml.DiagonalQubitUnitary, 1)(np.array([-1.0, 1.0j]), wires=0)
+    tape = expand_tape(tape, 3, stop_at=lambda op: not isinstance(op, ControlledOperation))
+    assert_equal_operations(
+        tape.operations, 
+        [
+            qml.DiagonalQubitUnitary(
+                np.array([1.0, 1.0, -1.0, 1.0j]),
+                wires=[1, 0])
+        ])
+
+def test_qubit_unitary():
+    """Test ctrl on QubitUnitary and ControlledQubitUnitary"""
+    with QuantumTape() as tape:
+        ctrl(qml.QubitUnitary, 1)(
+            np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0), 
+            wires=0)
+
+    tape = expand_tape(tape, 3, stop_at=lambda op: not isinstance(op, ControlledOperation))
+    assert_equal_operations(
+        tape.operations, 
+        [
+            qml.ControlledQubitUnitary(
+                np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0),
+                control_wires=1,
+                wires=0)
+        ])
+
+    with QuantumTape() as tape:
+        ctrl(qml.ControlledQubitUnitary, 1)(
+            np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0),
+            control_wires=2,
+            wires=0)
+
+    tape = expand_tape(tape, 3, stop_at=lambda op: not isinstance(op, ControlledOperation))
+    assert_equal_operations(
+        tape.operations, 
+        [
+            qml.ControlledQubitUnitary(
+                np.array([[1.0, 1.0], [1.0, -1.0]]) / np.sqrt(2.0),
+                control_wires=[1, 2],
+                wires=0)
+        ])
+
+
+def test_no_control_defined():
+    """Test a custom operation with no control transform defined."""
+    # QFT has no control rule defined.
+    with QuantumTape() as tape:
+        ctrl(qml.QFT, 2)(wires=[0, 1])
+    tape = expand_tape(tape)
+    assert len(tape.operations) == 12
+    # Check that all operations are updated to their controlled version.
+    for op in tape.operations:
+        assert type(op) in {qml.ControlledPhaseShift, qml.Toffoli, qml.CRX, qml.CSWAP}
+