Repository: PennyLaneAI/pennylane
Pull request #1111 (2021-02-24 23:18:25)

adjoint transform
**Context:**
Inverting/adjointing gates has been a feature request for a while. This PR adds a transform paradigm for adjointing arbitrary tapes. By creating it as a transform rather than modifying gates in place, no modifications to existing devices need to be done.

**Description of the Change:**
Arbitrary tape can be inverted by wrapping the executing method in `qml.adjoint`

```python

def my_anzats(weights):
    qml.RX(...)
    qml.CNOT(...)

@qml.qnode(dev)
def my_circuit(weights):
  # Apply the anzats
  my_anzats(weights)
  # Do some other stuff
  # ....
  # Undo the anzats computation
  qml.adjoint(my_anzats)(weights)
  return qml.expval(...)
 
 ```

You can also apply it directly to individual gates

```python
@qml.qnod(...)
def my_circuit(...):
  qml.RX(*args)
  # Undo the above operation
  qml.adjoint(qml.RX)(*args)
 ```

Works out of the box with `qml.draw` too.

```python
@qml.qnode(dev)
def circuit():
  qml.RX(0.123, wires=0)
  qml.adjoint(qml.RX)(0.456, wires=0)
 
 qml.draw(circuit)() #  0: ──RX(0.123)──RX(-0.456)──┤ 
```
**Benefits:**
This allows inverting of nearly any circuit, regardless of the underlying tape. 

**Possible Drawbacks:**
None that I can think of.

**Related GitHub Issues:**
 #377 #1044


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/1111?src=pr&el=h1) Report
> Merging [#1111](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1111?src=pr&el=desc) (b35fc38) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/5095918c03da1b82bbdbe94055d7846244127dc0?el=desc) (5095918) will **decrease** coverage by `0.25%`.
> The diff coverage is `63.21%`.

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

```diff
@@            Coverage Diff             @@
##           master    #1111      +/-   ##
==========================================
- Coverage   97.73%   97.47%   -0.26%     
==========================================
  Files         154      155       +1     
  Lines       11683    11766      +83     
==========================================
+ Hits        11418    11469      +51     
- Misses        265      297      +32     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1111?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pennylane/operation.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1111/diff?src=pr&el=tree#diff-cGVubnlsYW5lL29wZXJhdGlvbi5weQ==) | `95.48% <50.00%> (-0.19%)` | :arrow_down: |
| [pennylane/ops/qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1111/diff?src=pr&el=tree#diff-cGVubnlsYW5lL29wcy9xdWJpdC5weQ==) | `93.25% <55.07%> (-4.20%)` | :arrow_down: |
| [pennylane/tape/tapes/jacobian\_tape.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1111/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RhcGUvdGFwZXMvamFjb2JpYW5fdGFwZS5weQ==) | `98.34% <100.00%> (ø)` | |
| [pennylane/tape/tapes/tape.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1111/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RhcGUvdGFwZXMvdGFwZS5weQ==) | `98.35% <100.00%> (+0.01%)` | :arrow_up: |
| [pennylane/templates/transforms.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1111/diff?src=pr&el=tree#diff-cGVubnlsYW5lL3RlbXBsYXRlcy90cmFuc2Zvcm1zLnB5) | `100.00% <100.00%> (ø)` | |

------

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

Thenerdstation: Should be ready for final review. the 4 uncovered lines are from NotImplemented methods.
Thenerdstation: > Just one thing, would it be useful to benchmark a circuit with lots of inversions?

Yes, I can add that to the benchmarking suite. 

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index 617bc064a..2968d25a6 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -2,6 +2,37 @@
 
 <h3>New features since last release</h3>
 
+* A new adjoint transform has been added. 
+  [(#1111)](https://github.com/PennyLaneAI/pennylane/pull/1111)
+
+  This new method allows users to apply the adjoint of an arbitrary sequence of operations.
+
+  ```python
+  def subroutine(wire):
+      qml.RX(0.123, wires=wire)
+      qml.RY(0.456, wires=wire)
+
+  dev = qml.device('default.qubit', wires=1)
+  @qml.qnode(dev)
+  def circuit():
+      subroutine(0)
+      qml.adjoint(subroutine)(0)
+      return qml.expval(qml.PauliZ(0))
+  ```
+
+  This creates the following circuit:
+
+  ```pycon
+  >>> print(qml.draw(circuit)())
+  0: --RX(0.123)--RY(0.456)--RY(-0.456)--RX(-0.123)--| <Z>
+  ```
+
+  Directly applying to a gate also works as expected.
+
+  ```python
+  qml.adjoint(qml.RX)(0.123, wires=0) # Really applies RX(-0.123).
+  ```
+
 - Added the `QuantumPhaseEstimation` template for performing quantum phase estimation for an input
   unitary matrix.
   [(#1095)](https://github.com/PennyLaneAI/pennylane/pull/1095)
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 8ed9758be..485d0213a 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -52,6 +52,7 @@ import pennylane.math  # pylint: disable=wrong-import-order
 import pennylane.tape  # pylint: disable=wrong-import-order
 from .tape import enable_tape, disable_tape, tape_mode_active
 from .tape.qnode import draw, metric_tensor
+from .tape.transforms.adjoint import adjoint
 
 # Look for an existing configuration file
 default_config = Configuration("config.toml")
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 7910f46c3..c3480e223 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -713,6 +713,21 @@ class Operation(Operator):
         """Boolean determining if the inverse of the operation was requested."""
         return self._inverse
 
+    def adjoint(self, do_queue=False):
+        """Create an operation that is the adjoint of this one.
+
+        Adjointed operations are the conjugated and transposed version of the
+        original operation. Adjointed ops are equivalent to the inverted operation for unitary
+        gates.
+
+        Args:
+            do_queue: Whether to add the adjointed gate to the context queue.
+
+        Returns:
+            The adjointed operation.
+        """
+        raise NotImplementedError
+
     @inverse.setter
     def inverse(self, boolean):
         self._inverse = boolean
diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py
index d26c04fdb..39d1d7e68 100644
--- a/pennylane/ops/qubit.py
+++ b/pennylane/ops/qubit.py
@@ -20,7 +20,6 @@ import functools
 
 # pylint:disable=abstract-method,arguments-differ,protected-access
 import math
-
 import numpy as np
 from scipy.linalg import block_diag
 
@@ -34,6 +33,12 @@ from pennylane.wires import Wires
 INV_SQRT2 = 1 / math.sqrt(2)
 
 
+class AdjointError(Exception):
+    """Exception for non-adjointable operations."""
+
+    pass
+
+
 class Hadamard(Observable, Operation):
     r"""Hadamard(wires)
     The Hadamard operator
@@ -87,6 +92,9 @@ class Hadamard(Observable, Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return Hadamard(wires=self.wires, do_queue=do_queue)
+
 
 class PauliX(Observable, Operation):
     r"""PauliX(wires)
@@ -139,6 +147,9 @@ class PauliX(Observable, Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return PauliX(wires=self.wires, do_queue=do_queue)
+
 
 class PauliY(Observable, Operation):
     r"""PauliY(wires)
@@ -193,6 +204,9 @@ class PauliY(Observable, Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return PauliY(wires=self.wires, do_queue=do_queue)
+
 
 class PauliZ(Observable, DiagonalOperation):
     r"""PauliZ(wires)
@@ -230,6 +244,9 @@ class PauliZ(Observable, DiagonalOperation):
         decomp_ops = [PhaseShift(np.pi, wires=wires)]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return PauliZ(wires=self.wires, do_queue=do_queue)
+
 
 class S(DiagonalOperation):
     r"""S(wires)
@@ -265,6 +282,9 @@ class S(DiagonalOperation):
         decomp_ops = [PhaseShift(np.pi / 2, wires=wires)]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return S(wires=self.wires, do_queue=do_queue).inv()
+
 
 class T(DiagonalOperation):
     r"""T(wires)
@@ -300,6 +320,9 @@ class T(DiagonalOperation):
         decomp_ops = [PhaseShift(np.pi / 4, wires=wires)]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return T(wires=self.wires, do_queue=do_queue).inv()
+
 
 class SX(Operation):
     r"""SX(wires)
@@ -340,6 +363,9 @@ class SX(Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return SX(wires=self.wires, do_queue=do_queue).inv()
+
 
 class CNOT(Operation):
     r"""CNOT(wires)
@@ -371,6 +397,9 @@ class CNOT(Operation):
     def _matrix(cls, *params):
         return CNOT.matrix
 
+    def adjoint(self, do_queue=False):
+        return CNOT(wires=self.wires, do_queue=do_queue)
+
 
 class CZ(DiagonalOperation):
     r"""CZ(wires)
@@ -407,6 +436,9 @@ class CZ(DiagonalOperation):
     def _eigvals(cls, *params):
         return cls.eigvals
 
+    def adjoint(self, do_queue=False):
+        return CZ(wires=self.wires, do_queue=do_queue)
+
 
 class CY(Operation):
     r"""CY(wires)
@@ -450,6 +482,9 @@ class CY(Operation):
         decomp_ops = [CRY(np.pi, wires=wires), S(wires=wires[0])]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return CY(wires=self.wires, do_queue=do_queue)
+
 
 class SWAP(Operation):
     r"""SWAP(wires)
@@ -479,6 +514,9 @@ class SWAP(Operation):
     def _matrix(cls, *params):
         return cls.matrix
 
+    def adjoint(self, do_queue=False):
+        return SWAP(wires=self.wires, do_queue=do_queue)
+
 
 class CSWAP(Operation):
     r"""CSWAP(wires)
@@ -525,6 +563,9 @@ class CSWAP(Operation):
     def _matrix(cls, *params):
         return cls.matrix
 
+    def adjoint(self, do_queue=False):
+        return CSWAP(wires=self.wires, do_queue=do_queue)
+
 
 class Toffoli(Operation):
     r"""Toffoli(wires)
@@ -572,6 +613,9 @@ class Toffoli(Operation):
     def _matrix(cls, *params):
         return cls.matrix
 
+    def adjoint(self, do_queue=False):
+        return Toffoli(wires=self.wires, do_queue=do_queue)
+
 
 class RX(Operation):
     r"""RX(phi, wires)
@@ -607,6 +651,9 @@ class RX(Operation):
 
         return np.array([[c, js], [js, c]])
 
+    def adjoint(self, do_queue=False):
+        return RX(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class RY(Operation):
     r"""RY(phi, wires)
@@ -642,6 +689,9 @@ class RY(Operation):
 
         return np.array([[c, -s], [s, c]])
 
+    def adjoint(self, do_queue=False):
+        return RY(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class RZ(DiagonalOperation):
     r"""RZ(phi, wires)
@@ -683,6 +733,9 @@ class RZ(DiagonalOperation):
 
         return np.array([p, p.conjugate()])
 
+    def adjoint(self, do_queue=False):
+        return RZ(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class PhaseShift(DiagonalOperation):
     r"""PhaseShift(phi, wires)
@@ -725,6 +778,9 @@ class PhaseShift(DiagonalOperation):
         decomp_ops = [RZ(phi, wires=wires)]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return PhaseShift(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class ControlledPhaseShift(DiagonalOperation):
     r"""ControlledPhaseShift(phi, wires)
@@ -777,6 +833,9 @@ class ControlledPhaseShift(DiagonalOperation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return ControlledPhaseShift(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class Rot(Operation):
     r"""Rot(phi, theta, omega, wires)
@@ -831,6 +890,10 @@ class Rot(Operation):
         decomp_ops = [RZ(phi, wires=wires), RY(theta, wires=wires), RZ(omega, wires=wires)]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        phi, theta, omega = self.parameters
+        return Rot(-omega, -theta, -phi, wires=self.wires, do_queue=do_queue)
+
 
 class MultiRZ(DiagonalOperation):
     r"""MultiRZ(theta, wires)
@@ -919,6 +982,9 @@ class MultiRZ(DiagonalOperation):
         for i in range(len(wires) - 1):
             CNOT(wires=[wires[i + 1], wires[i]])
 
+    def adjoint(self, do_queue=False):
+        return MultiRZ(-self.parameters[0], wires=self.wires)
+
 
 class PauliRot(Operation):
     r"""PauliRot(theta, pauli_word, wires)
@@ -1102,6 +1168,11 @@ class PauliRot(Operation):
             elif gate == "Y":
                 RX(-np.pi / 2, wires=[wire])
 
+    def adjoint(self, do_queue=False):
+        return PauliRot(
+            -self.parameters[0], self.parameters[1], wires=self.wires, do_queue=do_queue
+        )
+
 
 # Four term gradient recipe for controlled rotations
 c1 = (np.sqrt(2) - 4 * np.cos(np.pi / 8)) / (4 - 8 * np.cos(np.pi / 8))
@@ -1181,6 +1252,9 @@ class CRX(Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return CRX(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class CRY(Operation):
     r"""CRY(phi, wires)
@@ -1248,6 +1322,9 @@ class CRY(Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return CRY(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class CRZ(DiagonalOperation):
     r"""CRZ(phi, wires)
@@ -1331,6 +1408,9 @@ class CRZ(DiagonalOperation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return CRZ(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class CRot(Operation):
     r"""CRot(phi, theta, omega, wires)
@@ -1406,6 +1486,10 @@ class CRot(Operation):
             ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        phi, theta, omega = self.parameters
+        return CRot(-omega, -theta, -phi, wires=self.wires, do_queue=do_queue)
+
 
 class U1(Operation):
     r"""U1(phi)
@@ -1446,6 +1530,9 @@ class U1(Operation):
     def decomposition(phi, wires):
         return [PhaseShift(phi, wires=wires)]
 
+    def adjoint(self, do_queue=False):
+        return U1(-self.data[0], wires=self.wires, do_queue=do_queue)
+
 
 class U2(Operation):
     r"""U2(phi, lambda, wires)
@@ -1502,6 +1589,10 @@ class U2(Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        # TODO(chase): Replace the `inv()` by instead modifying the parameters.
+        return U2(*self.parameters, wires=self.wires, do_queue=do_queue).inv()
+
 
 class U3(Operation):
     r"""U3(theta, phi, lambda, wires)
@@ -1565,6 +1656,10 @@ class U3(Operation):
         ]
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        # TODO(chase): Replace the `inv()` by instead modifying the parameters.
+        return U3(*self.parameters, wires=self.wires, do_queue=do_queue).inv()
+
 
 # =============================================================================
 # Arbitrary operations
@@ -1602,6 +1697,9 @@ class QubitUnitary(Operation):
 
         return U
 
+    def adjoint(self, do_queue=False):
+        return QubitUnitary(self.data[0].conj().T, wires=self.wires, do_queue=do_queue)
+
 
 class ControlledQubitUnitary(QubitUnitary):
     r"""ControlledQubitUnitary(U, control_wires, wires, control_values)
@@ -1662,7 +1760,6 @@ class ControlledQubitUnitary(QubitUnitary):
         target_dim = 2 ** len(wires)
         if len(U) != target_dim:
             raise ValueError(f"Input unitary must be of shape {(target_dim, target_dim)}")
-
         wires = control_wires + wires
 
         # If control values unspecified, we control on the all-ones string
@@ -1785,6 +1882,9 @@ class DiagonalQubitUnitary(DiagonalOperation):
     def decomposition(D, wires):
         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)
+
 
 class QFT(Operation):
     r"""QFT(wires)
@@ -1873,6 +1973,9 @@ class QFT(Operation):
 
         return decomp_ops
 
+    def adjoint(self, do_queue=False):
+        return QFT(wires=self.wires, do_queue=do_queue).inv()
+
 
 # =============================================================================
 # State preparation
@@ -1910,6 +2013,9 @@ class BasisState(Operation):
     def decomposition(n, wires):
         return BasisStatePreparation(n, wires)
 
+    def adjoint(self, do_queue=False):
+        raise AdjointError("No adjoint exists for BasisState operations.")
+
 
 class QubitStateVector(Operation):
     r"""QubitStateVector(state, wires)
@@ -1941,6 +2047,9 @@ class QubitStateVector(Operation):
     def decomposition(state, wires):
         return MottonenStatePreparation(state, wires)
 
+    def adjoint(self, do_queue=False):
+        raise AdjointError("No adjoint exists for QubitStateVector operations.")
+
 
 # =============================================================================
 # Observables
diff --git a/pennylane/tape/tapes/jacobian_tape.py b/pennylane/tape/tapes/jacobian_tape.py
index 4049fef80..2036f7390 100644
--- a/pennylane/tape/tapes/jacobian_tape.py
+++ b/pennylane/tape/tapes/jacobian_tape.py
@@ -53,6 +53,7 @@ class JacobianTape(QuantumTape):
 
     Args:
         name (str): a name given to the quantum tape
+        do_queue (bool): Whether to queue this tape in a parent tape context.
 
     **Example**
 
@@ -87,8 +88,8 @@ class JacobianTape(QuantumTape):
     [[-0.45478169]]
     """
 
-    def __init__(self, name=None):
-        super().__init__(name=name)
+    def __init__(self, name=None, do_queue=True):
+        super().__init__(name=name, do_queue=do_queue)
         self.jacobian_options = {}
 
     def _grad_method(self, idx, use_graph=True, default_method="F"):
diff --git a/pennylane/tape/tapes/tape.py b/pennylane/tape/tapes/tape.py
index 8799dd08b..3c73029b2 100644
--- a/pennylane/tape/tapes/tape.py
+++ b/pennylane/tape/tapes/tape.py
@@ -161,13 +161,9 @@ def expand_tape(tape, depth=1, stop_at=None, expand_measurements=False):
 class QuantumTape(AnnotatedQueue):
     """A quantum tape recorder, that records, validates and executes variational quantum programs.
 
-    .. note::
-
-        As the quantum tape is a *beta* feature. See :mod:`pennylane.tape`
-        for more details.
-
     Args:
         name (str): a name given to the quantum tape
+        do_queue (bool): Whether to queue this tape in a parent tape context.
 
     **Example**
 
@@ -224,15 +220,33 @@ class QuantumTape(AnnotatedQueue):
     [0.56]
     >>> tape.get_parameters(trainable_only=False)
     [0.56, 0.543, 0.133]
+
+
+    When using a tape with ``do_queue=False``, that tape will not be queueded in a parent tape context.
+
+    .. code-block:: python
+
+    with qml.tape.QuantumTape() as tape1:
+        with qml.tape.QuantumTape(do_queue=False) as tape2:
+            qml.RX(0.123, wires=0)
+
+    Here, tape2 records the RX gate, but tape1 doesn't record tape2.
+
+    >>> tape1.operations
+    []
+    >>> tape2.operations
+    [RX(0.123, wires=[0])]
+
+    This is useful for when you want to transform a tape first before applying it.
     """
 
     _lock = RLock()
     """threading.RLock: Used to synchronize appending to/popping from global QueueingContext."""
 
-    def __init__(self, name=None):
+    def __init__(self, name=None, do_queue=True):
         super().__init__()
         self.name = name
-
+        self.do_queue = do_queue
         self._prep = []
         """list[.Operation]: Tape state preparations."""
 
@@ -280,8 +294,8 @@ class QuantumTape(AnnotatedQueue):
                     for mock in mock_operations():
                         stack.enter_context(mock)
                     self._stack = stack.pop_all()
-
-            QueuingContext.append(self)
+            if self.do_queue:
+                QueuingContext.append(self)
             return super().__enter__()
         except Exception as _:
             QuantumTape._lock.release()
diff --git a/pennylane/tape/transforms/adjoint.py b/pennylane/tape/transforms/adjoint.py
new file mode 100644
index 000000000..980194a0c
--- /dev/null
+++ b/pennylane/tape/transforms/adjoint.py
@@ -0,0 +1,95 @@
+# 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 adjoint transform."""
+
+from functools import wraps
+from pennylane.tape.tapes import QuantumTape
+
+
+def adjoint(fn):
+    """Create a function that applies the adjoint of the provided operation or template.
+
+    This transform can be used to apply the adjoint of an arbitrary sequence of operations.
+
+    Args:
+        fn: Any python function that applies pennylane operations.
+
+    Returns:
+        A new function that will apply the same operations but adjointed and in reverse order.
+
+    **Example**
+
+    .. code-block:: python3
+
+        def my_ops():
+            qml.RX(0.123, wires=0)
+            qml.RY(0.456, wires=0)
+
+        adj_my_ops = qml.adjoint(my_ops)
+        adj_my_ops() # This will queue ops [qml.RY(-0.456), qml.RX(-0.123)]
+
+    .. UsageDetails::
+
+        **Adjoint of a function**
+
+        Here, we apply the ``subroutine`` function, and then apply its inverse.
+        Notice that in addition to adjointing all of the operations, they are also
+        applied in reverse construction order.
+
+        .. code-block:: python3
+
+            def subroutine(wire):
+                qml.RX(0.123, wires=wire)
+                qml.RY(0.456, wires=wire)
+
+            dev = qml.device('default.qubit', wires=1)
+            @qml.qnode(dev)
+            def circuit():
+                subroutine(0)
+                qml.adjoint(subroutine)(0)
+                return qml.expval(qml.PauliZ(0))
+
+        This creates the following circuit:
+
+        >>> print(qml.draw(circuit)())
+        0: --RX(0.123)--RY(0.456)--RY(-0.456)--RX(-0.123)--| <Z>
+
+        **Single operation**
+
+        You can also easily adjoint a single operation just by wrapping it with ``adjoint``:
+
+        .. code-block:: python3
+
+            dev = qml.device('default.qubit', wires=1)
+            @qml.qnode(dev)
+            def circuit():
+                qml.RX(0.123, wires=0)
+                qml.adjoint(qml.RX)(0.123, wires=0)
+                return qml.expval(qml.PauliZ(0))
+
+        This creates the following circuit:
+
+        >>> print(qml.draw(circuit)())
+        0: --RX(0.123)--RX(-0.123)--| <Z>
+
+    """
+
+    @wraps(fn)
+    def wrapper(*args, **kwargs):
+        with QuantumTape(do_queue=False) as tape:
+            fn(*args, **kwargs)
+        for op in reversed(tape.queue):
+            op.adjoint(do_queue=True)
+
+    return wrapper


Test patch:
diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py
index 5105f5ba5..238e3808a 100644
--- a/tests/ops/test_qubit_ops.py
+++ b/tests/ops/test_qubit_ops.py
@@ -330,6 +330,61 @@ class TestOperations:
         res = op.matrix
         assert np.allclose(res, mat, atol=tol, rtol=0)
 
+    @pytest.mark.parametrize(
+        "op", 
+        [
+            qml.RX(0.123, wires=0),
+            qml.RY(1.434, wires=0),
+            qml.RZ(2.774, wires=0),
+            qml.S(wires=0),
+            qml.SX(wires=0),
+            qml.T(wires=0),
+            qml.CNOT(wires=[0, 1]),
+            qml.CZ(wires=[0, 1]),
+            qml.CY(wires=[0, 1]),
+            qml.SWAP(wires=[0, 1]),
+            qml.CSWAP(wires=[0, 1, 2]),
+            qml.PauliRot(0.123, 'Y', wires=0),
+            qml.Rot(0.123, 0.456, 0.789, wires=0),
+            qml.Toffoli(wires=[0, 1, 2]),
+            qml.PhaseShift(2.133, wires=0),
+            qml.ControlledPhaseShift(1.777, wires=[0, 2]),
+            qml.MultiRZ(0.112, wires=[1, 2, 3]),
+            qml.CRX(0.836, wires=[2, 3]),
+            qml.CRY(0.721, wires=[2, 3]),
+            qml.CRZ(0.554, wires=[2, 3]),
+            qml.U1(0.123, wires=0),
+            qml.U2(3.556, 2.134, wires=0),
+            qml.U3(2.009, 1.894, 0.7789, wires=0),
+            qml.Hadamard(wires=0),
+            qml.PauliX(wires=0),
+            qml.PauliZ(wires=0),
+            qml.PauliY(wires=0),
+            qml.CRot(0.123, 0.456, 0.789, wires=[0, 1]),
+            qml.QubitUnitary(np.eye(2) * 1j, wires=0),
+            qml.DiagonalQubitUnitary(np.array([1.0, 1.j]), wires=1),
+            qml.QFT(wires=[1, 2, 3]),
+            qml.ControlledQubitUnitary(np.eye(2) * 1j, wires=[0], control_wires=[2]),
+            qml.MultiControlledX(control_wires=[0, 1], wires=2, control_values='01'),
+        ])
+    def test_adjoint_unitaries(self, op, tol):
+        op_d = op.adjoint()
+        res1 = np.dot(op.matrix, op_d.matrix)
+        res2 = np.dot(op_d.matrix, op.matrix)
+        np.testing.assert_allclose(res1, np.eye(2 ** len(op.wires)), atol=tol)
+        np.testing.assert_allclose(res2, np.eye(2 ** len(op.wires)), atol=tol)
+        assert op.wires == op_d.wires
+
+    @pytest.mark.parametrize(
+        "op", 
+        [
+            qml.BasisState(np.array([0, 1]), wires=0), 
+            qml.QubitStateVector(np.array([1.0, 0.0]), wires=0),
+        ])
+    def test_adjoint_error_exception(self, op, tol):
+        with pytest.raises(qml.ops.qubit.AdjointError):
+            op.adjoint()
+
     @pytest.mark.parametrize("inverse", [True, False])
     def test_QFT(self, inverse):
         """Test if the QFT matrix is equal to a manually-calculated version for 3 qubits"""
diff --git a/tests/tape/transforms/test_adjoint.py b/tests/tape/transforms/test_adjoint.py
new file mode 100644
index 000000000..660d3fc52
--- /dev/null
+++ b/tests/tape/transforms/test_adjoint.py
@@ -0,0 +1,83 @@
+# 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 numpy as np
+from pennylane.tape.transforms.adjoint import adjoint
+import pennylane as qml
+
+def test_adjoint_on_function():
+    """Test that adjoint works when applied to a function"""
+
+    dev = qml.device("default.qubit", wires=1)
+
+    def my_op():
+        qml.RX(0.123, wires=0)
+        qml.RY(2.32, wires=0)
+        qml.RZ(1.95, wires=0)
+
+    @qml.qnode(dev)
+    def my_circuit():
+        qml.PauliX(wires=0)
+        qml.PauliZ(wires=0)
+        my_op()
+        adjoint(my_op)()
+        qml.PauliY(wires=0)
+        return qml.state()
+
+    np.testing.assert_allclose(my_circuit(), np.array([1.0j, 0.0]), atol=1e-6, rtol=1e-6)
+
+def test_adjoint_directly_on_op():
+    """Test that adjoint works when directly applyed to an op"""
+
+    dev = qml.device("default.qubit", wires=1)
+    @qml.qnode(dev)
+    def my_circuit():
+        adjoint(qml.RX)(np.pi/4.0, wires=0)
+        return qml.state()
+
+    np.testing.assert_allclose(my_circuit(), np.array([0.92388, 0.382683j]), atol=1e-6, rtol=1e-6)
+
+def test_nested_adjoint():
+    """Test that adjoint works when nested with other adjoints"""
+    dev = qml.device("default.qubit", wires=1)
+    @qml.qnode(dev)
+    def my_circuit():
+        adjoint(adjoint(qml.RX))(np.pi/4.0, wires=0)
+        return qml.state()
+
+    np.testing.assert_allclose(
+        my_circuit(), 
+        np.array([0.92388, -0.382683j]), 
+        atol=1e-6, rtol=1e-6)
+
+def test_nested_adjoint_on_function():
+    """Test that adjoint works when nested with other adjoints"""
+
+    def my_op():
+        qml.RX(0.123, wires=0)
+        qml.RY(2.32, wires=0)
+        qml.RZ(1.95, wires=0)
+
+    dev = qml.device("default.qubit", wires=1)
+    @qml.qnode(dev)
+    def my_circuit():
+        adjoint(my_op)()
+        qml.Hadamard(wires=0)
+        adjoint(adjoint(my_op))()
+        return qml.state()
+
+    np.testing.assert_allclose(
+        my_circuit(), 
+        np.array([-0.995707,  0.068644+6.209710e-02j]), 
+        atol=1e-6, rtol=1e-6)