Repository: PennyLaneAI/pennylane
Pull request #1249 (2021-04-22 13:50:55)

Added a custom gate application for Toffoli in default.qubit
**Context:**

Custom gate definition for the Toffoli gate

**Description of the Change:**

- In the `pennylane/devices/default_qubit.py` file, extended the `_apply_ops` dictionary to contain the `"Toffoli": self._apply_toffoli` entry
- Defined the `_apply_toffoli` method for `DefaultQubit` in the same file
- Added test cases for applying the Toffoli gate in `devices/test_default_qubit.py`.

**Benefits:**

Custom gate definition for the Toffoli gate

**Possible Drawbacks:**

N/A

**Related GitHub Issues:**

#1215 


antalszava: Hi @ryanhill1, thanks so much for going for this! :blush: Let us know if anything questions come up in the process.
co9olguy: Hi @ryanhill1, thanks for the contribution! :tada: 

I see you've added code, docstrings, tests, and updated the changelog. Would you say it's ready for code review?
ryanhill1: @co9olguy yep! should be ready
ryanhill1: Thank you everyone for all of your feedback!  I accidentally pushed some working versions the other day that were super messy, so if you can ignore those and just look at my most recent commit, I think it should be ready for code review! The toffoli function should now work for any number of wires, and any ordering of control and target wires. Plus it is a much simpler implementation than I was had before. I added three test cases that account for both controls being greater than the target, both smaller, and one smaller one greater. I also slightly modified the one and two-qubit test cases to allow us to use the same 4 qubit state across all of them, instead of defining different states for different numbers of wires, which got kind of messy. Let me know any thoughts and/or revisions, I'm happy to make edits as needed. 
codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1249?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#1249](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1249?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (835f5c3) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/ef87a8cf35accfbb556fab13210288f9c46b358b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (ef87a8c) will **increase** coverage by `0.00%`.
> The diff coverage is `100.00%`.

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

```diff
@@           Coverage Diff           @@
##           master    #1249   +/-   ##
=======================================
  Coverage   98.11%   98.12%           
=======================================
  Files         146      146           
  Lines       11050    11064   +14     
=======================================
+ Hits        10842    10856   +14     
  Misses        208      208           
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1249?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/devices/default\_qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1249/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdWJpdC5weQ==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1249?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/1249?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Last update [ef87a8c...835f5c3](https://codecov.io/gh/PennyLaneAI/pennylane/pull/1249?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).

co9olguy: Hi @ryanhill1, looks like all checks are passing :tada: 

Let us know when this is ready for re-review :slightly_smiling_face: 
ryanhill1: @co9olguy great! In that case it is ready for review 

Patch:
diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md
index bfc10f753..cd9dcb07c 100644
--- a/.github/CHANGELOG.md
+++ b/.github/CHANGELOG.md
@@ -37,6 +37,9 @@
 
 <h3>Improvements</h3>
 
+* Added custom gate application for Toffoli in `default.qubit`.
+  [(#1249)](https://github.com/PennyLaneAI/pennylane/pull/1249)
+
 * The device test suite now provides test cases for checking gates by comparing
   expectation values.
   [(#1212)](https://github.com/PennyLaneAI/pennylane/pull/1212)
@@ -95,6 +98,7 @@ Thomas Bromley, Olivia Di Matteo, Diego Guala, Anthony Hayes, Josh Izaac, Antal
 
 This release contains contributions from (in alphabetical order):
 
+Thomas Bromley, Olivia Di Matteo, Diego Guala, Anthony Hayes, Ryan Hill,
 Josh Izaac, Maria Schuld, Antal Száva.
 
 # Release 0.15.0
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index 0c070a215..45e8c8684 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -151,6 +151,7 @@ class DefaultQubit(QubitDevice):
             "CNOT": self._apply_cnot,
             "SWAP": self._apply_swap,
             "CZ": self._apply_cz,
+            "Toffoli": self._apply_toffoli,
         }
 
     def map_wires(self, wires):
@@ -336,6 +337,44 @@ class DefaultQubit(QubitDevice):
         state_x = self._apply_x(state[sl_1], axes=target_axes)
         return self._stack([state[sl_0], state_x], axis=axes[0])
 
+    def _apply_toffoli(self, state, axes, **kwargs):
+        """Applies a Toffoli gate by slicing along the axis of the greater control qubit, slicing
+        each of the resulting sub-arrays along the axis of the smaller control qubit, and then applying
+        an X transformation along the axis of the target qubit of the fourth sub-sub-array.
+
+        By performing two consecutive slices in this way, we are able to select all of the amplitudes with
+        a corresponding :math:`|11\rangle` for the two control qubits. This means we then just need to apply
+        a :class:`~.PauliX` (NOT) gate to the result.
+
+        Args:
+            state (array[complex]): input state
+            axes (List[int]): target axes to apply transformation
+
+        Returns:
+            array[complex]: output state
+        """
+        cntrl_max = np.argmax(axes[:2])
+        cntrl_min = cntrl_max ^ 1
+        sl_a0 = _get_slice(0, axes[cntrl_max], self.num_wires)
+        sl_a1 = _get_slice(1, axes[cntrl_max], self.num_wires)
+        sl_b0 = _get_slice(0, axes[cntrl_min], self.num_wires - 1)
+        sl_b1 = _get_slice(1, axes[cntrl_min], self.num_wires - 1)
+
+        # If both controls are smaller than the target, shift the target axis down by two. If one
+        # control is greater and one control is smaller than the target, shift the target axis
+        # down by one. If both controls are greater than the target, leave the target axis as-is.
+        if axes[cntrl_min] > axes[2]:
+            target_axes = [axes[2]]
+        elif axes[cntrl_max] > axes[2]:
+            target_axes = [axes[2] - 1]
+        else:
+            target_axes = [axes[2] - 2]
+
+        # state[sl_a1][sl_b1] gives us all of the amplitudes with a |11> for the two control qubits.
+        state_x = self._apply_x(state[sl_a1][sl_b1], axes=target_axes)
+        state_stacked_a1 = self._stack([state[sl_a1][sl_b0], state_x], axis=axes[cntrl_min])
+        return self._stack([state[sl_a0], state_stacked_a1], axis=axes[cntrl_max])
+
     def _apply_swap(self, state, axes, **kwargs):
         """Applies a SWAP gate by performing a partial transposition along the specified axes.
 


Test patch:
diff --git a/tests/devices/test_default_qubit.py b/tests/devices/test_default_qubit.py
index 6db6fd818..74ae91f81 100644
--- a/tests/devices/test_default_qubit.py
+++ b/tests/devices/test_default_qubit.py
@@ -1909,8 +1909,8 @@ class TestApplyOps:
     """Tests for special methods listed in _apply_ops that use array manipulation tricks to apply
     gates in DefaultQubit."""
 
-    state = np.arange(2 ** 3).reshape((2, 2, 2))
-    dev = qml.device("default.qubit", wires=3)
+    state = np.arange(2 ** 4).reshape((2, 2, 2, 2))
+    dev = qml.device("default.qubit", wires=4)
     single_qubit_ops = [
         (qml.PauliX, dev._apply_x),
         (qml.PauliY, dev._apply_y),
@@ -1925,6 +1925,9 @@ class TestApplyOps:
         (qml.SWAP, dev._apply_swap),
         (qml.CZ, dev._apply_cz),
     ]
+    three_qubit_ops = [
+        (qml.Toffoli, dev._apply_toffoli),
+    ]
 
     @pytest.mark.parametrize("op, method", single_qubit_ops)
     def test_apply_single_qubit_op(self, op, method, inverse):
@@ -1932,7 +1935,7 @@ class TestApplyOps:
         state_out = method(self.state, axes=[1], inverse=inverse)
         op = op(wires=[1])
         matrix = op.inv().matrix if inverse else op.matrix
-        state_out_einsum = np.einsum("ab,ibk->iak", matrix, self.state)
+        state_out_einsum = np.einsum("ab,ibjk->iajk", matrix, self.state)
         assert np.allclose(state_out, state_out_einsum)
 
     @pytest.mark.parametrize("op, method", two_qubit_ops)
@@ -1942,7 +1945,7 @@ class TestApplyOps:
         op = op(wires=[0, 1])
         matrix = op.inv().matrix if inverse else op.matrix
         matrix = matrix.reshape((2, 2, 2, 2))
-        state_out_einsum = np.einsum("abcd,cdk->abk", matrix, self.state)
+        state_out_einsum = np.einsum("abcd,cdjk->abjk", matrix, self.state)
         assert np.allclose(state_out, state_out_einsum)
 
     @pytest.mark.parametrize("op, method", two_qubit_ops)
@@ -1953,7 +1956,40 @@ class TestApplyOps:
         op = op(wires=[2, 1])
         matrix = op.inv().matrix if inverse else op.matrix
         matrix = matrix.reshape((2, 2, 2, 2))
-        state_out_einsum = np.einsum("abcd,idc->iba", matrix, self.state)
+        state_out_einsum = np.einsum("abcd,idck->ibak", matrix, self.state)
+        assert np.allclose(state_out, state_out_einsum)
+
+    @pytest.mark.parametrize("op, method", three_qubit_ops)
+    def test_apply_three_qubit_op_controls_smaller(self, op, method, inverse):
+        """Test if the application of three qubit operations is correct when both control wires are
+        smaller than the target wire."""
+        state_out = method(self.state, axes=[0, 2, 3])
+        op = op(wires=[0, 2, 3])
+        matrix = op.inv().matrix if inverse else op.matrix
+        matrix = matrix.reshape((2, 2) * 3)
+        state_out_einsum = np.einsum("abcdef,dkef->akbc", matrix, self.state)
+        assert np.allclose(state_out, state_out_einsum)
+
+    @pytest.mark.parametrize("op, method", three_qubit_ops)
+    def test_apply_three_qubit_op_controls_greater(self, op, method, inverse):
+        """Test if the application of three qubit operations is correct when both control wires are
+        greater than the target wire."""
+        state_out = method(self.state, axes=[2, 1, 0])
+        op = op(wires=[2, 1, 0])
+        matrix = op.inv().matrix if inverse else op.matrix
+        matrix = matrix.reshape((2, 2) * 3)
+        state_out_einsum = np.einsum("abcdef,fedk->cbak", matrix, self.state)
+        assert np.allclose(state_out, state_out_einsum)
+
+    @pytest.mark.parametrize("op, method", three_qubit_ops)
+    def test_apply_three_qubit_op_controls_split(self, op, method, inverse):
+        """Test if the application of three qubit operations is correct when one control wire is smaller
+        and one control wire is greater than the target wire."""
+        state_out = method(self.state, axes=[3, 1, 2])
+        op = op(wires=[3, 1, 2])
+        matrix = op.inv().matrix if inverse else op.matrix
+        matrix = matrix.reshape((2, 2) * 3)
+        state_out_einsum = np.einsum("abcdef,kdfe->kacb", matrix, self.state)
         assert np.allclose(state_out, state_out_einsum)