Repository: PennyLaneAI/pennylane
Pull request #1287 (2021-05-07 17:15:03)

Add decomposition for MultiControlledX gate
The `MultiControlledX` gate can now be decomposed, allowing for it to potentially be implemented on hardware or other devices that don't support it.

```python
ctrl_wires = [f"c{i}" for i in range(5)]
work_wires = ["w1"]
target_wires = ["t1"]
all_wires = ctrl_wires + work_wires + target_wires

dev = qml.device("default.qubit", wires=all_wires)

with qml.tape.QuantumTape() as tape:
    qml.MultiControlledX(control_wires=ctrl_wires, wires=target_wires, work_wires=work_wires)
```

```pycon
>>> tape = tape.expand(depth=2)
>>> print(tape.draw(wire_order=Wires(all_wires)))
 c0: ──────────╭C──────────────╭C──────────────────╭C──────────────╭C──────────┤  
 c1: ──────────├C──────────────├C──────────────────├C──────────────├C──────────┤  
 c2: ──────╭C──│───╭C──────╭C──│───╭C──────────╭C──│───╭C──────╭C──│───╭C──────┤  
 c3: ──╭C──│───│───│───╭C──│───│───│───────╭C──│───│───│───╭C──│───│───│───────┤  
 c4: ──│───├C──╰X──├C──│───├C──╰X──├C──╭C──│───├C──╰X──├C──│───├C──╰X──├C──╭C──┤  
 w0: ──├X──│───────│───├X──│───────│───├C──├X──│───────│───├X──│───────│───├C──┤  
 t1: ──╰C──╰X──────╰X──╰C──╰X──────╰X──╰X──╰C──╰X──────╰X──╰C──╰X──────╰X──╰X──┤ 
```

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/1287?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1287](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1287?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (6463973) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/fea709564a84e00f7aab631c767e85b4085a8790?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (fea7095) will **increase** coverage by `0.01%`.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1287/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/1287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

```diff
@@            Coverage Diff             @@
##           master    #1287      +/-   ##
==========================================
+ Coverage   98.14%   98.15%   +0.01%     
==========================================
  Files         147      147              
  Lines       11333    11400      +67     
==========================================
+ Hits        11123    11190      +67     
  Misses        210      210              
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1287?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1287/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC5weQ==) | `98.45% <100.00%> (+0.11%)` | :arrow_up: |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1287?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/1287?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [fea7095...6463973](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1287?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 b4ad09c72..52d8d9510 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -156,6 +156,36 @@ random_mat2 = rng.standard_normal(3, requires_grad=False)
 
 <h3>Improvements</h3>
 
+* The `MultiControlledX` gate now has a decomposition defined. When controlling on three or more wires,
+  an ancilla register of worker wires is required to support the decomposition.
+  [(#1287)](https://github.com/PennyLaneAI/pennylane/pull/1287)
+  
+  ```python
+  ctrl_wires = [f"c{i}" for i in range(5)]
+  work_wires = [f"w{i}" for i in range(3)]
+  target_wires = ["t0"]
+  all_wires = ctrl_wires + work_wires + target_wires
+
+  dev = qml.device("default.qubit", wires=all_wires)
+
+  with qml.tape.QuantumTape() as tape:
+      qml.MultiControlledX(control_wires=ctrl_wires, wires=target_wires, work_wires=work_wires)
+  ```
+  
+  ```pycon
+  >>> tape = tape.expand(depth=2)
+  >>> print(tape.draw(wire_order=Wires(all_wires)))
+   c0: ──────────────╭C──────────────────────╭C──────────┤  
+   c1: ──────────────├C──────────────────────├C──────────┤  
+   c2: ──────────╭C──│───╭C──────────────╭C──│───╭C──────┤  
+   c3: ──────╭C──│───│───│───╭C──────╭C──│───│───│───╭C──┤  
+   c4: ──╭C──│───│───│───│───│───╭C──│───│───│───│───│───┤  
+   w0: ──│───│───├C──╰X──├C──│───│───│───├C──╰X──├C──│───┤  
+   w1: ──│───├C──╰X──────╰X──├C──│───├C──╰X──────╰X──├C──┤  
+   w2: ──├C──╰X──────────────╰X──├C──╰X──────────────╰X──┤  
+   t0: ──╰X──────────────────────╰X──────────────────────┤  
+  ```
+
 * The `qml.SingleExcitation` and `qml.DoubleExcitation` operations now
   have decompositions over elementary gates, and their gradient recipes
   have been updated to use the four-term parameter-shift rules.
diff --git a/pennylane/circuit_drawer/representation_resolver.py b/pennylane/circuit_drawer/representation_resolver.py
index 2065c64e9..b8e7cb5fa 100644
--- a/pennylane/circuit_drawer/representation_resolver.py
+++ b/pennylane/circuit_drawer/representation_resolver.py
@@ -376,6 +376,11 @@ class RepresentationResolver:
                 op, "U", self.unitary_matrix_cache
             )
 
+        elif base_name == "MultiControlledX":
+            if wire in op.control_wires:
+                return self.charset.CONTROL
+            representation = "X"
+
         elif base_name == "Hermitian":
             representation = RepresentationResolver._format_matrix_operation(
                 op, "H", self.hermitian_matrix_cache
diff --git a/pennylane/ops/qubit.py b/pennylane/ops/qubit.py
index 9d981b604..7e742857a 100644
--- a/pennylane/ops/qubit.py
+++ b/pennylane/ops/qubit.py
@@ -1914,7 +1914,7 @@ class QubitUnitary(Operation):
         return U
 
     def adjoint(self):
-        return QubitUnitary(qml.math.T(qml.math.conj(self.data[0])), wires=self.wires)
+        return QubitUnitary(qml.math.T(qml.math.conj(self.matrix)), wires=self.wires)
 
     def _controlled(self, wire):
         ControlledQubitUnitary(*self.parameters, control_wires=wire, wires=self.wires)
@@ -1987,6 +1987,7 @@ class ControlledQubitUnitary(QubitUnitary):
             raise ValueError(f"Input unitary must be of shape {(target_dim, target_dim)}")
 
         # Saving for the circuit drawer
+        self._target_wires = wires
         self.control_wires = control_wires
         self.U = U
 
@@ -1997,6 +1998,7 @@ class ControlledQubitUnitary(QubitUnitary):
             control_values = "1" * len(control_wires)
 
         control_int = self._parse_control_values(control_wires, control_values)
+        self.control_values = control_values
 
         # A multi-controlled operation is a block-diagonal matrix partitioned into
         # blocks where the operation being applied sits in the block positioned at
@@ -2006,15 +2008,19 @@ class ControlledQubitUnitary(QubitUnitary):
         # etc. The positioning of the block is controlled by padding the block diagonal
         # to the left and right with the correct amount of identity blocks.
 
-        padding_left = control_int * len(U)
-        padding_right = 2 ** len(wires) - len(U) - padding_left
+        self._padding_left = control_int * len(U)
+        self._padding_right = 2 ** len(wires) - len(U) - self._padding_left
+        self._CU = None
 
-        CU = block_diag(np.eye(padding_left), U, np.eye(padding_right))
+        super().__init__(*params, wires=wires, do_queue=do_queue)
 
-        params = list(params)
-        params[0] = CU
+    def _matrix(self, *params):
+        if self._CU is None:
+            self._CU = block_diag(np.eye(self._padding_left), self.U, np.eye(self._padding_right))
 
-        super().__init__(*params, wires=wires, do_queue=do_queue)
+        params = list(params)
+        params[0] = self._CU
+        return super()._matrix(*params)
 
     @staticmethod
     def _parse_control_values(control_wires, control_values):
@@ -2034,7 +2040,8 @@ class ControlledQubitUnitary(QubitUnitary):
         return control_int
 
     def _controlled(self, wire):
-        ControlledQubitUnitary(*self.parameters, control_wires=wire, wires=self.wires)
+        ctrl_wires = sorted(self.control_wires + wire)
+        ControlledQubitUnitary(*self.parameters, control_wires=ctrl_wires, wires=self._target_wires)
 
 
 class MultiControlledX(ControlledQubitUnitary):
@@ -2052,6 +2059,31 @@ class MultiControlledX(ControlledQubitUnitary):
         wires (Union[Wires or int]): a single target wire the operation acts on
         control_values (str): a string of bits representing the state of the control
             qubits to control on (default is the all 1s state)
+        work_wires (Union[Wires, Sequence[int], or int]): optional work wires used to decompose
+            the operation into a series of Toffoli gates
+
+    .. note::
+
+        If ``MultiControlledX`` is not supported on the targeted device, PennyLane will decompose
+        the operation into :class:`~.Toffoli` and/or :class:`~.CNOT` gates. When controlling on
+        three or more wires, the Toffoli-based decompositions described in Lemmas 7.2 and 7.3 of
+        `Barenco et al. <https://arxiv.org/abs/quant-ph/9503016>`__ will be used. These methods
+        require at least one work wire.
+
+        The number of work wires provided determines the decomposition method used and the resulting
+        number of Toffoli gates required. When ``MultiControlledX`` is controlling on :math:`n`
+        wires:
+
+        #. If at least :math:`n - 2` work wires are provided, the decomposition in Lemma 7.2 will be
+           applied using the first :math:`n - 2` work wires.
+        #. If fewer than :math:`n - 2` work wires are provided, a combination of Lemmas 7.3 and 7.2
+           will be applied using only the first work wire.
+
+        These methods present a tradeoff between qubit number and depth. The method in point 1
+        requires fewer Toffoli gates but a greater number of qubits.
+
+        Note that the state of the work wires before and after the decomposition takes place is
+        unchanged.
 
     **Example**
 
@@ -2070,10 +2102,25 @@ class MultiControlledX(ControlledQubitUnitary):
     par_domain = "A"
     grad_method = None
 
-    def __init__(self, control_wires=None, wires=None, control_values=None, do_queue=True):
-        if len(Wires(wires)) != 1:
+    # pylint: disable=too-many-arguments
+    def __init__(
+        self, control_wires=None, wires=None, control_values=None, work_wires=None, do_queue=True
+    ):
+        wires = Wires(wires)
+        control_wires = Wires(control_wires)
+        work_wires = Wires([]) if work_wires is None else Wires(work_wires)
+
+        if len(wires) != 1:
             raise ValueError("MultiControlledX accepts a single target wire.")
 
+        if Wires.shared_wires([wires, work_wires]) or Wires.shared_wires(
+            [control_wires, work_wires]
+        ):
+            raise ValueError("The work wires must be different from the control and target wires")
+
+        self._target_wire = wires[0]
+        self._work_wires = work_wires
+
         super().__init__(
             np.array([[0, 1], [1, 0]]),
             control_wires=control_wires,
@@ -2082,6 +2129,113 @@ class MultiControlledX(ControlledQubitUnitary):
             do_queue=do_queue,
         )
 
+    # pylint: disable=unused-argument
+    def decomposition(self, *args, **kwargs):
+
+        if len(self.control_wires) > 2 and len(self._work_wires) == 0:
+            raise ValueError(f"At least one work wire is required to decompose operation: {self}")
+
+        flips1 = [
+            qml.PauliX(self.control_wires[i])
+            for i, val in enumerate(self.control_values)
+            if val == "0"
+        ]
+
+        if len(self.control_wires) == 1:
+            decomp = [qml.CNOT(wires=[self.control_wires[0], self._target_wire])]
+        elif len(self.control_wires) == 2:
+            decomp = [qml.Toffoli(wires=[*self.control_wires, self._target_wire])]
+        else:
+            num_work_wires_needed = len(self.control_wires) - 2
+
+            if len(self._work_wires) >= num_work_wires_needed:
+                decomp = self._decomposition_with_many_workers(
+                    self.control_wires, self._target_wire, self._work_wires
+                )
+            else:
+                work_wire = self._work_wires[0]
+                decomp = self._decomposition_with_one_worker(
+                    self.control_wires, self._target_wire, work_wire
+                )
+
+        flips2 = [
+            qml.PauliX(self.control_wires[i])
+            for i, val in enumerate(self.control_values)
+            if val == "0"
+        ]
+
+        return flips1 + decomp + flips2
+
+    @staticmethod
+    def _decomposition_with_many_workers(control_wires, target_wire, work_wires):
+        """Decomposes the multi-controlled PauliX gate using the approach in Lemma 7.2 of
+        https://arxiv.org/pdf/quant-ph/9503016.pdf, which requires a suitably large register of
+        work wires"""
+        num_work_wires_needed = len(control_wires) - 2
+        work_wires = work_wires[:num_work_wires_needed]
+
+        work_wires_reversed = list(reversed(work_wires))
+        control_wires_reversed = list(reversed(control_wires))
+
+        gates = []
+
+        for i in range(len(work_wires)):
+            ctrl1 = control_wires_reversed[i]
+            ctrl2 = work_wires_reversed[i]
+            t = target_wire if i == 0 else work_wires_reversed[i - 1]
+            gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t]))
+
+        gates.append(qml.Toffoli(wires=[*control_wires[:2], work_wires[0]]))
+
+        for i in reversed(range(len(work_wires))):
+            ctrl1 = control_wires_reversed[i]
+            ctrl2 = work_wires_reversed[i]
+            t = target_wire if i == 0 else work_wires_reversed[i - 1]
+            gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t]))
+
+        for i in range(len(work_wires) - 1):
+            ctrl1 = control_wires_reversed[i + 1]
+            ctrl2 = work_wires_reversed[i + 1]
+            t = work_wires_reversed[i]
+            gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t]))
+
+        gates.append(qml.Toffoli(wires=[*control_wires[:2], work_wires[0]]))
+
+        for i in reversed(range(len(work_wires) - 1)):
+            ctrl1 = control_wires_reversed[i + 1]
+            ctrl2 = work_wires_reversed[i + 1]
+            t = work_wires_reversed[i]
+            gates.append(qml.Toffoli(wires=[ctrl1, ctrl2, t]))
+
+        return gates
+
+    @staticmethod
+    def _decomposition_with_one_worker(control_wires, target_wire, work_wire):
+        """Decomposes the multi-controlled PauliX gate using the approach in Lemma 7.3 of
+        https://arxiv.org/pdf/quant-ph/9503016.pdf, which requires a single work wire"""
+        tot_wires = len(control_wires) + 2
+        partition = int(np.ceil(tot_wires / 2))
+
+        first_part = control_wires[:partition]
+        second_part = control_wires[partition:]
+
+        gates = [
+            MultiControlledX(
+                control_wires=first_part, wires=work_wire, work_wires=second_part + target_wire
+            ),
+            MultiControlledX(
+                control_wires=second_part + work_wire, wires=target_wire, work_wires=first_part
+            ),
+            MultiControlledX(
+                control_wires=first_part, wires=work_wire, work_wires=second_part + target_wire
+            ),
+            MultiControlledX(
+                control_wires=second_part + work_wire, wires=target_wire, work_wires=first_part
+            ),
+        ]
+
+        return gates
+
 
 class DiagonalQubitUnitary(DiagonalOperation):
     r"""DiagonalQubitUnitary(D, wires)


Test patch:
diff --git a/tests/circuit_drawer/test_circuit_drawer.py b/tests/circuit_drawer/test_circuit_drawer.py
index 1246862fe..c18742fc0 100644
--- a/tests/circuit_drawer/test_circuit_drawer.py
+++ b/tests/circuit_drawer/test_circuit_drawer.py
@@ -291,6 +291,7 @@ def parameterized_qubit_qnode():
         qml.CSWAP(wires=[4, 2, 1]).inv()
         qml.QubitUnitary(np.eye(2), wires=[2])
         qml.ControlledQubitUnitary(np.eye(2), control_wires=[0, 1], wires=[2])
+        qml.MultiControlledX(control_wires=[0, 1, 2], wires=[3])
         qml.Toffoli(wires=[0, 2, 1])
         qml.CNOT(wires=[0, 2])
         qml.PauliZ(wires=[1])
@@ -344,11 +345,11 @@ def drawn_parameterized_qubit_circuit_with_variable_names():
 def drawn_parameterized_qubit_circuit_with_values():
     """The rendered circuit representation of the above qubit circuit with variable values."""
     return (
-        " 0: ──RX(0.1)───────────────╭C─────RX(0.4)──────────────────────────────────────╭C───╭C─────╭C───────╭C──╭C────────────╭C──╭SWAP⁻¹───╭SWAP───┤ ⟨Y⟩       \n"
-        + " 1: ──RX(0.2)────Z──────────╰X⁻¹──╭RY(0.2)──RX(2)────╭RY(0.359)⁻¹──╭SWAP⁻¹──────├C───├X──Z──│───Z⁻¹──╰Z──│─────╭X──╭C──│───│─────────├SWAP───┤ Var[H]    \n"
-        + " 2: ──Rϕ(0.567)──RX(0.6)⁻¹────────│──────────────────│─────────────├SWAP⁻¹──U0──╰U0──╰C─────╰X───────────╰Z⁻¹──╰C──│───╰X──╰SWAP⁻¹───│───────┤ Sample[X] \n"
-        + " 3: ──────────────────────────────╰C────────RZ(0.2)──╰C────────────│───────────────────────────────────────────────╰X───────RZ(0.2)──│──────╭┤ ⟨H0⟩      \n"
-        + " 4: ───────────────────────────────────────────────────────────────╰C────────────────────────────────────────────────────────────────╰C─────╰┤ ⟨H0⟩      \n"
+        " 0: ──RX(0.1)───────────────╭C─────RX(0.4)──────────────────────────────────────╭C───╭C──╭C─────╭C───────╭C──╭C────────────╭C──╭SWAP⁻¹───╭SWAP───┤ ⟨Y⟩       \n"
+        + " 1: ──RX(0.2)────Z──────────╰X⁻¹──╭RY(0.2)──RX(2)────╭RY(0.359)⁻¹──╭SWAP⁻¹──────├C───├C──├X──Z──│───Z⁻¹──╰Z──│─────╭X──╭C──│───│─────────├SWAP───┤ Var[H]    \n"
+        + " 2: ──Rϕ(0.567)──RX(0.6)⁻¹────────│──────────────────│─────────────├SWAP⁻¹──U0──╰U0──├C──╰C─────╰X───────────╰Z⁻¹──╰C──│───╰X──╰SWAP⁻¹───│───────┤ Sample[X] \n"
+        + " 3: ──────────────────────────────╰C────────RZ(0.2)──╰C────────────│─────────────────╰X────────────────────────────────╰X───────RZ(0.2)──│──────╭┤ ⟨H0⟩      \n"
+        + " 4: ───────────────────────────────────────────────────────────────╰C────────────────────────────────────────────────────────────────────╰C─────╰┤ ⟨H0⟩      \n"
         + "U0 =\n"
         + "[[1. 0.]\n"
         + " [0. 1.]]\n"
diff --git a/tests/ops/test_qubit_ops.py b/tests/ops/test_qubit_ops.py
index f7d017b97..eedcd0498 100644
--- a/tests/ops/test_qubit_ops.py
+++ b/tests/ops/test_qubit_ops.py
@@ -14,6 +14,8 @@
 """
 Unit tests for the available built-in discrete-variable quantum operations.
 """
+import itertools
+import re
 import pytest
 import functools
 import numpy as np
@@ -2234,6 +2236,190 @@ class TestMultiControlledX:
 
         assert np.allclose(mpmct_state, pauli_x_state)
 
+    @pytest.mark.parametrize("n_ctrl_wires", range(3, 6))
+    def test_decomposition_with_many_workers(self, n_ctrl_wires):
+        """Test that the decomposed MultiControlledX gate performs the same unitary as the
+        matrix-based version by checking if U^dagger U applies the identity to each basis
+        state. This test focuses on the case where there are many work wires."""
+        control_wires = range(n_ctrl_wires)
+        target_wire = n_ctrl_wires
+        work_wires = range(n_ctrl_wires + 1, 2 * n_ctrl_wires + 1)
+
+        dev = qml.device("default.qubit", wires=2 * n_ctrl_wires + 1)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.MultiControlledX._decomposition_with_many_workers(
+                control_wires, target_wire, work_wires
+            )
+        assert all(isinstance(op, qml.Toffoli) for op in tape.operations)
+
+        @qml.qnode(dev)
+        def f(bitstring):
+            qml.BasisState(bitstring, wires=range(n_ctrl_wires + 1))
+            qml.MultiControlledX(control_wires=control_wires, wires=target_wire).inv()
+            for op in tape.operations:
+                op.queue()
+            return qml.probs(wires=range(n_ctrl_wires + 1))
+
+        u = np.array([f(b) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)]).T
+        assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1)))
+
+    @pytest.mark.parametrize("n_ctrl_wires", range(3, 6))
+    def test_decomposition_with_one_worker(self, n_ctrl_wires):
+        """Test that the decomposed MultiControlledX gate performs the same unitary as the
+        matrix-based version by checking if U^dagger U applies the identity to each basis
+        state. This test focuses on the case where there is one work wire."""
+        control_wires = Wires(range(n_ctrl_wires))
+        target_wire = n_ctrl_wires
+        work_wires = n_ctrl_wires + 1
+
+        dev = qml.device("default.qubit", wires=n_ctrl_wires + 2)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.MultiControlledX._decomposition_with_one_worker(
+                control_wires, target_wire, work_wires
+            )
+        tape = tape.expand(depth=1)
+        assert all(
+            isinstance(op, qml.Toffoli) or isinstance(op, qml.CNOT) for op in tape.operations
+        )
+
+        @qml.qnode(dev)
+        def f(bitstring):
+            qml.BasisState(bitstring, wires=range(n_ctrl_wires + 1))
+            qml.MultiControlledX(control_wires=control_wires, wires=target_wire).inv()
+            for op in tape.operations:
+                op.queue()
+            return qml.probs(wires=range(n_ctrl_wires + 1))
+
+        u = np.array([f(b) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)]).T
+        assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1)))
+
+    def test_not_enough_workers(self):
+        """Test that a ValueError is raised when more than 2 control wires are to be decomposed with
+        no work wires supplied"""
+        control_wires = range(3)
+        target_wire = 4
+        op = qml.MultiControlledX(control_wires=control_wires, wires=target_wire)
+
+        match = (
+            f"At least one work wire is required to decompose operation: {re.escape(op.__repr__())}"
+        )
+        with pytest.raises(ValueError, match=match):
+            op.decomposition()
+
+    def test_not_unique_wires(self):
+        """Test that a ValueError is raised when work_wires is not complementary to control_wires"""
+        control_wires = range(3)
+        target_wire = 4
+        work_wires = range(2)
+        with pytest.raises(ValueError, match="The work wires must be different from the control"):
+            qml.MultiControlledX(
+                control_wires=control_wires, wires=target_wire, work_wires=work_wires
+            )
+
+    @pytest.mark.parametrize("control_val", ["0", "1"])
+    @pytest.mark.parametrize("n_ctrl_wires", range(1, 6))
+    def test_decomposition_with_flips(self, n_ctrl_wires, control_val, mocker):
+        """Test that the decomposed MultiControlledX gate performs the same unitary as the
+        matrix-based version by checking if U^dagger U applies the identity to each basis
+        state. This test focuses on varying the control values."""
+        control_values = control_val * n_ctrl_wires
+        control_wires = range(n_ctrl_wires)
+        target_wire = n_ctrl_wires
+        work_wires = range(n_ctrl_wires + 1, 2 * n_ctrl_wires + 1)
+
+        spy = mocker.spy(qml.MultiControlledX, "decomposition")
+        dev = qml.device("default.qubit", wires=2 * n_ctrl_wires + 1)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.MultiControlledX(
+                control_wires=control_wires,
+                wires=target_wire,
+                work_wires=work_wires,
+                control_values=control_values,
+            )
+        tape = tape.expand(depth=2)
+        assert all(not isinstance(op, qml.MultiControlledX) for op in tape.operations)
+
+        @qml.qnode(dev)
+        def f(bitstring):
+            qml.BasisState(bitstring, wires=range(n_ctrl_wires + 1))
+            qml.MultiControlledX(
+                control_wires=control_wires, wires=target_wire, control_values=control_values
+            ).inv()
+            for op in tape.operations:
+                op.queue()
+            return qml.probs(wires=range(n_ctrl_wires + 1))
+
+        u = np.array([f(b) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)]).T
+        spy.assert_called()
+        assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1)))
+
+    def test_decomposition_with_custom_wire_labels(self, mocker):
+        """Test that the decomposed MultiControlledX gate performs the same unitary as the
+        matrix-based version by checking if U^dagger U applies the identity to each basis
+        state. This test focuses on using custom wire labels."""
+        n_ctrl_wires = 4
+        control_wires = [-1, "alice", 42, 3.14]
+        target_wire = ["bob"]
+        work_wires = ["charlie"]
+        all_wires = control_wires + target_wire + work_wires
+
+        spy = mocker.spy(qml.MultiControlledX, "decomposition")
+        dev = qml.device("default.qubit", wires=all_wires)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.MultiControlledX(
+                control_wires=control_wires, wires=target_wire, work_wires=work_wires
+            )
+        tape = tape.expand(depth=2)
+        assert all(not isinstance(op, qml.MultiControlledX) for op in tape.operations)
+
+        @qml.qnode(dev)
+        def f(bitstring):
+            qml.BasisState(bitstring, wires=control_wires + target_wire)
+            qml.MultiControlledX(control_wires=control_wires, wires=target_wire).inv()
+            for op in tape.operations:
+                op.queue()
+            return qml.probs(wires=control_wires + target_wire)
+
+        u = np.array([f(b) for b in itertools.product(range(2), repeat=n_ctrl_wires + 1)]).T
+        spy.assert_called()
+        assert np.allclose(u, np.eye(2 ** (n_ctrl_wires + 1)))
+
+    def test_worker_state_unperturbed(self, mocker):
+        """Test that the state of the worker wires is unperturbed after the decomposition has used
+        them. To do this, a random state over all the qubits (control, target and workers) is
+        loaded and U^dagger U(decomposed) is applied. If the workers are uncomputed, the output
+        state will be the same as the input."""
+        control_wires = range(4)
+        target_wire = 4
+        worker_wires = [5, 6]
+        n_all_wires = 7
+
+        rnd_state = unitary_group.rvs(2 ** n_all_wires, random_state=1)[0]
+        spy = mocker.spy(qml.MultiControlledX, "decomposition")
+        dev = qml.device("default.qubit", wires=n_all_wires)
+
+        with qml.tape.QuantumTape() as tape:
+            qml.MultiControlledX(
+                control_wires=control_wires, wires=target_wire, work_wires=worker_wires
+            )
+        tape = tape.expand(depth=1)
+        assert all(not isinstance(op, qml.MultiControlledX) for op in tape.operations)
+
+        @qml.qnode(dev)
+        def f():
+            qml.QubitStateVector(rnd_state, wires=range(n_all_wires))
+            qml.MultiControlledX(control_wires=control_wires, wires=target_wire).inv()
+            for op in tape.operations:
+                op.queue()
+            return qml.state()
+
+        assert np.allclose(f(), rnd_state)
+        spy.assert_called()
+
 
 class TestArithmetic:
     """Tests the arithmetic operations."""
diff --git a/tests/test_operation.py b/tests/test_operation.py
index 29ed19170..bb163a363 100644
--- a/tests/test_operation.py
+++ b/tests/test_operation.py
@@ -176,10 +176,11 @@ class TestOperation:
         U = qml.CRX._matrix(0.4)
 
         op = qml.ControlledQubitUnitary(U, control_wires=control_wires, wires=target_wires)
-        target_data = [np.block([[np.eye(12), np.zeros((12, 4))], [np.zeros((4, 12)), U]])]
+        target_matrix = np.block([[np.eye(12), np.zeros((12, 4))], [np.zeros((4, 12)), U]])
 
         assert op.name == qml.ControlledQubitUnitary.__name__
-        assert np.allclose(target_data, op.data)
+        assert np.allclose([U], op.data)
+        assert np.allclose(op.matrix, target_matrix)
         assert op._wires == Wires(control_wires) + Wires(target_wires)
 
     @pytest.fixture(scope="function")