Repository: PennyLaneAI/pennylane
Pull request #3408 (2022-11-22 18:20:32)

Add control Hadamard gate: CH
**Description of the Change:**

Add the control Hadamard gate CH:

```
  >>> ch = qml.CH(wires=[0, 1])
  >>> matrix = ch.compute_matrix()
[[ 1.          0.          0.          0.        ]
 [ 0.          1.          0.          0.        ]
 [ 0.          0.          0.70710678  0.70710678]
 [ 0.          0.          0.70710678 -0.70710678]]
```

github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.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/3408?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3408](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3408?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (7eaf56b) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/6881ca8076983f4f324492cfeb8edb10ec06a5be?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (6881ca8) will **decrease** coverage by `0.00%`.
> The diff coverage is `96.15%`.

```diff
@@            Coverage Diff             @@
##           master    #3408      +/-   ##
==========================================
- Coverage   99.70%   99.69%   -0.01%     
==========================================
  Files         298      298              
  Lines       26220    26246      +26     
==========================================
+ Hits        26142    26167      +25     
- Misses         78       79       +1     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3408?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/devices/default\_mixed.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3408/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9taXhlZC5weQ==) | `100.00% <ø> (ø)` | |
| [pennylane/devices/default\_qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3408/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% <ø> (ø)` | |
| [pennylane/devices/null\_qubit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3408/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvbnVsbF9xdWJpdC5weQ==) | `100.00% <ø> (ø)` | |
| [pennylane/ops/qubit/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3408/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9fX2luaXRfXy5weQ==) | `100.00% <ø> (ø)` | |
| [pennylane/ops/qubit/non\_parametric\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3408/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdWJpdC9ub25fcGFyYW1ldHJpY19vcHMucHk=) | `99.83% <96.15%> (-0.17%)` | :arrow_down: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)

AlbertMitjans: Why not using `qml.ctrl(qml.Hadamard(0), 1)`? I believe the idea of the `Controlled` class was to avoid having to create a class for each controlled gate.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 4532e2e78..fa316583a 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -4,6 +4,17 @@
 
 <h3>New features since last release</h3>
 
+* Add the controlled Hadamard gate.
+  ```pycon
+  >>> ch = qml.CH(wires=[0, 1])
+  >>> matrix = ch.compute_matrix()
+  [[ 1.          0.          0.          0.        ]
+   [ 0.          1.          0.          0.        ]
+   [ 0.          0.          0.70710678  0.70710678]
+   [ 0.          0.          0.70710678 -0.70710678]]
+  ```
+  [#3408](https://github.com/PennyLaneAI/pennylane/pull/3408)
+  
 * Support custom measurement processes:
   * `SampleMeasurement` and `StateMeasurement` classes have been added. They contain an abstract
     method to process samples/quantum state.
diff --git a/pennylane/devices/default_mixed.py b/pennylane/devices/default_mixed.py
index 16b7876a6..98f8f1e77 100644
--- a/pennylane/devices/default_mixed.py
+++ b/pennylane/devices/default_mixed.py
@@ -88,7 +88,9 @@ class DefaultMixed(QubitDevice):
         "SWAP",
         "CSWAP",
         "Toffoli",
+        "CY",
         "CZ",
+        "CH",
         "PhaseShift",
         "ControlledPhaseShift",
         "CPhaseShift00",
diff --git a/pennylane/devices/default_qubit.py b/pennylane/devices/default_qubit.py
index 8b01c0d55..e468bf0dd 100644
--- a/pennylane/devices/default_qubit.py
+++ b/pennylane/devices/default_qubit.py
@@ -120,6 +120,7 @@ class DefaultQubit(QubitDevice):
         "Toffoli",
         "CY",
         "CZ",
+        "CH",
         "PhaseShift",
         "ControlledPhaseShift",
         "CPhaseShift00",
diff --git a/pennylane/devices/null_qubit.py b/pennylane/devices/null_qubit.py
index aac05f112..ecdaa18e1 100644
--- a/pennylane/devices/null_qubit.py
+++ b/pennylane/devices/null_qubit.py
@@ -71,6 +71,7 @@ class NullQubit(QubitDevice):
         "Toffoli",
         "CY",
         "CZ",
+        "CH",
         "PhaseShift",
         "ControlledPhaseShift",
         "CPhase",
diff --git a/pennylane/ops/functions/is_commuting.py b/pennylane/ops/functions/is_commuting.py
index bec93ad88..6ab8708c0 100644
--- a/pennylane/ops/functions/is_commuting.py
+++ b/pennylane/ops/functions/is_commuting.py
@@ -70,6 +70,7 @@ def _get_target_name(op):
         "CNOT": "PauliX",
         "CZ": "PauliZ",
         "CY": "PauliY",
+        "CH": "Hadamard",
         "CSWAP": "SWAP",
         "Toffoli": "PauliX",
         "ControlledPhaseShift": "PhaseShift",
diff --git a/pennylane/ops/qubit/__init__.py b/pennylane/ops/qubit/__init__.py
index f90203f05..f3938aa91 100644
--- a/pennylane/ops/qubit/__init__.py
+++ b/pennylane/ops/qubit/__init__.py
@@ -54,6 +54,7 @@ ops = {
     "CNOT",
     "CZ",
     "CY",
+    "CH",
     "SWAP",
     "ISWAP",
     "SISWAP",
diff --git a/pennylane/ops/qubit/attributes.py b/pennylane/ops/qubit/attributes.py
index b4eade08b..f6021793f 100644
--- a/pennylane/ops/qubit/attributes.py
+++ b/pennylane/ops/qubit/attributes.py
@@ -154,7 +154,7 @@ approximation.
 """
 
 self_inverses = Attribute(
-    ["Hadamard", "PauliX", "PauliY", "PauliZ", "CNOT", "CZ", "CY", "SWAP", "Toffoli"]
+    ["Hadamard", "PauliX", "PauliY", "PauliZ", "CNOT", "CZ", "CH", "CY", "SWAP", "Toffoli"]
 )
 """Attribute: Operations that are their own inverses."""
 
diff --git a/pennylane/ops/qubit/non_parametric_ops.py b/pennylane/ops/qubit/non_parametric_ops.py
index 71a611579..1cf6c7965 100644
--- a/pennylane/ops/qubit/non_parametric_ops.py
+++ b/pennylane/ops/qubit/non_parametric_ops.py
@@ -164,6 +164,9 @@ class Hadamard(Observable, Operation):
         ]
         return decomp_ops
 
+    def _controlled(self, wire):
+        return CH(wires=Wires(wire) + self.wires)
+
     def adjoint(self):
         return Hadamard(wires=self.wires)
 
@@ -1222,6 +1225,110 @@ class CY(Operation):
         return True
 
 
+class CH(Operation):
+    r"""CH(wires)
+    The controlled-Hadamard operator
+
+    .. math:: CY = \begin{bmatrix}
+            1 & 0 & 0 & 0 \\
+            0 & 1 & 0 & 0 \\
+            0 & 0 & \frac{1}{\sqrt{2}} & \frac{1}{\sqrt{2}} \\
+            0 & 0 & \frac{1}{\sqrt{2}} & -\frac{1}{\sqrt{2}}
+        \end{bmatrix}.
+
+    .. note:: The first wire provided corresponds to the **control qubit**.
+
+    **Details:**
+
+    * Number of wires: 2
+    * Number of parameters: 0
+
+    Args:
+        wires (Sequence[int]): the wires the operation acts on
+    """
+    num_wires = 2
+    num_params = 0
+    """int: Number of trainable parameters that the operator depends on."""
+
+    basis = "Hadamard"
+
+    def label(self, decimals=None, base_label=None, cache=None):
+        return base_label or "H"
+
+    @staticmethod
+    @lru_cache()
+    def compute_matrix():  # pylint: disable=arguments-differ
+        r"""Representation of the operator as a canonical matrix in the computational basis (static method).
+
+        The canonical matrix is the textbook matrix representation that does not consider wires.
+        Implicitly, this assumes that the wires of the operator correspond to the global wire order.
+
+        .. seealso:: :meth:`~.CH.matrix`
+
+
+        Returns:
+            ndarray: matrix
+
+        **Example**
+
+        >>> print(qml.CH.compute_matrix())
+        [[ 1.          0.          0.          0.        ]
+         [ 0.          1.          0.          0.        ]
+         [ 0.          0.          0.70710678  0.70710678]
+         [ 0.          0.          0.70710678 -0.70710678]]
+        """
+        return np.array(
+            [
+                [1, 0, 0, 0],
+                [0, 1, 0, 0],
+                [0, 0, INV_SQRT2, INV_SQRT2],
+                [0, 0, INV_SQRT2, -INV_SQRT2],
+            ]
+        )
+
+    @staticmethod
+    def compute_decomposition(wires):
+        r"""Representation of the operator as a product of other operators (static method).
+
+
+        .. math:: O = O_1 O_2 \dots O_n.
+
+
+        .. seealso:: :meth:`~.CH.decomposition`.
+
+        Args:
+            wires (Iterable, Wires): wires that the operator acts on
+
+        Returns:
+            list[Operator]: decomposition into lower level operations
+
+        **Example:**
+
+        >>> print(qml.CH.compute_decomposition([0, 1]))
+        [qml.RY(-0.78539816339, wires=[1]), CZ(wires=[0, 1]), qml.RY(0.78539816339, wires=[1])]
+
+        """
+        return [
+            qml.RY(-np.pi / 4, wires=wires[1]),
+            qml.CZ(wires=wires),
+            qml.RY(+np.pi / 4, wires=wires[1]),
+        ]
+
+    def adjoint(self):
+        return CH(wires=self.wires)
+
+    def pow(self, z):
+        return super().pow(z % 2)
+
+    @property
+    def control_wires(self):
+        return Wires(self.wires[0])
+
+    @property
+    def is_hermitian(self):
+        return True
+
+
 class SWAP(Operation):
     r"""SWAP(wires)
     The swap operator
diff --git a/pennylane/transforms/optimization/pattern_matching.py b/pennylane/transforms/optimization/pattern_matching.py
index 1dd45607c..d1d50358c 100644
--- a/pennylane/transforms/optimization/pattern_matching.py
+++ b/pennylane/transforms/optimization/pattern_matching.py
@@ -443,6 +443,7 @@ def _first_match_qubits(node_c, node_p, n_qubits_p):
         "CNOT": "PauliX",
         "CZ": "PauliZ",
         "CY": "PauliY",
+        "CH": "Hadamard",
         "CSWAP": "SWAP",
         "Toffoli": "PauliX",
         "ControlledPhaseShift": "PhaseShift",


Test patch:
diff --git a/pennylane/devices/tests/test_gates.py b/pennylane/devices/tests/test_gates.py
index c87588fb5..4bcfbc1fe 100755
--- a/pennylane/devices/tests/test_gates.py
+++ b/pennylane/devices/tests/test_gates.py
@@ -48,6 +48,7 @@ ops = {
     "CSWAP": qml.CSWAP(wires=[0, 1, 2]),
     "CZ": qml.CZ(wires=[0, 1]),
     "CY": qml.CY(wires=[0, 1]),
+    "CH": qml.CH(wires=[0, 1]),
     "DiagonalQubitUnitary": qml.DiagonalQubitUnitary(np.array([1, 1]), wires=[0]),
     "Hadamard": qml.Hadamard(wires=[0]),
     "MultiRZ": qml.MultiRZ(0, wires=[0]),
@@ -145,6 +146,14 @@ ECR = np.array(
 CNOT = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, 1], [0, 0, 1, 0]])
 CZ = np.diag([1, 1, 1, -1])
 CY = np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 0, -1j], [0, 0, 1j, 0]])
+CH = np.array(
+    [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1 / sqrt(2), 1 / sqrt(2)],
+        [0, 0, 1 / sqrt(2), -1 / sqrt(2)],
+    ]
+)
 toffoli = np.diag([1 for i in range(8)])
 toffoli[6:8, 6:8] = np.array([[0, 1], [1, 0]])
 CSWAP = block_diag(I, I, SWAP)
@@ -281,6 +290,7 @@ two_qubit = [
     (qml.ECR, ECR),
     (qml.CZ, CZ),
     (qml.CY, CY),
+    (qml.CH, CH),
     adjoint_tuple(qml.ISWAP, ISWAP),
 ]
 # list of all parametrized two-qubit gates
diff --git a/tests/devices/test_default_qubit_torch.py b/tests/devices/test_default_qubit_torch.py
index 68ec00637..568757aeb 100644
--- a/tests/devices/test_default_qubit_torch.py
+++ b/tests/devices/test_default_qubit_torch.py
@@ -33,6 +33,7 @@ from gate_data import (
     CNOT,
     CSWAP,
     CZ,
+    CH,
     SWAP,
     ControlledPhaseShift,
     CRot3,
@@ -108,7 +109,7 @@ single_qubit_param = [
     (qml.RZ, Rotz),
     (qml.MultiRZ, MultiRZ1),
 ]
-two_qubit = [(qml.CZ, CZ), (qml.CNOT, CNOT), (qml.SWAP, SWAP)]
+two_qubit = [(qml.CZ, CZ), (qml.CNOT, CNOT), (qml.SWAP, SWAP), (qml.CH, CH)]
 two_qubit_param = [
     (qml.CRX, CRotx),
     (qml.CRY, CRoty),
diff --git a/tests/gate_data.py b/tests/gate_data.py
index 85a546cfd..4a07e1daa 100644
--- a/tests/gate_data.py
+++ b/tests/gate_data.py
@@ -54,6 +54,14 @@ ECR = math.array(
         [-1j * 1 / math.sqrt(2), 1 / math.sqrt(2), 0, 0],
     ]
 )
+CH = math.array(
+    [
+        [1, 0, 0, 0],
+        [0, 1, 0, 0],
+        [0, 0, 1 / math.sqrt(2), 1 / math.sqrt(2)],
+        [0, 0, 1 / math.sqrt(2), -1 / math.sqrt(2)],
+    ]
+)  # CH gate
 
 # Three qubit gates
 CSWAP = math.array(
diff --git a/tests/ops/functions/test_is_hermitian.py b/tests/ops/functions/test_is_hermitian.py
index c62fc1e11..ee78b4ca0 100644
--- a/tests/ops/functions/test_is_hermitian.py
+++ b/tests/ops/functions/test_is_hermitian.py
@@ -30,6 +30,7 @@ hermitian_ops = (
     qml.CNOT([0, 1]),
     qml.CZ([0, 1]),
     qml.CY([0, 1]),
+    qml.CH([0, 1]),
     qml.SWAP([0, 1]),
     qml.CSWAP([0, 1, 2]),
     qml.Toffoli([0, 1, 2]),
diff --git a/tests/ops/op_math/test_control.py b/tests/ops/op_math/test_control.py
index 33b098a96..51113fe28 100644
--- a/tests/ops/op_math/test_control.py
+++ b/tests/ops/op_math/test_control.py
@@ -199,10 +199,10 @@ def test_no_control_defined():
     with QuantumTape() as tape:
         ctrl(qml.templates.QFT, 2)(wires=[0, 1])
     tape = tape.expand(depth=3, stop_at=lambda op: not isinstance(op, Controlled))
-    assert len(tape.operations) == 12
+    assert len(tape.operations) == 8
     # 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}
+        assert type(op) in {qml.ControlledPhaseShift, qml.Toffoli, qml.CRX, qml.CSWAP, qml.CH}
 
 
 def test_no_decomposition_defined():
diff --git a/tests/ops/qubit/test_non_parametric_ops.py b/tests/ops/qubit/test_non_parametric_ops.py
index c887e888f..98575dab6 100644
--- a/tests/ops/qubit/test_non_parametric_ops.py
+++ b/tests/ops/qubit/test_non_parametric_ops.py
@@ -19,7 +19,7 @@ import itertools
 
 import numpy as np
 import pytest
-from gate_data import CNOT, CSWAP, CZ, ECR, ISWAP, SISWAP, SWAP, H, I, S, T, Toffoli, X, Y, Z
+from gate_data import CNOT, CSWAP, CZ, ECR, ISWAP, SISWAP, SWAP, H, I, S, T, Toffoli, X, Y, Z, CH
 from scipy.sparse import csr_matrix
 from scipy.stats import unitary_group
 
@@ -41,6 +41,7 @@ NON_PARAMETRIZED_OPERATIONS = [
     (qml.CSWAP, CSWAP),
     (qml.Toffoli, Toffoli),
     (qml.ECR, ECR),
+    (qml.CH, CH),
 ]
 
 SPARSE_MATRIX_SUPPORTED_OPERATIONS = (
@@ -226,6 +227,24 @@ class TestDecompositions:
         decomposed_matrix = np.linalg.multi_dot(mats)
         assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0)
 
+    def test_CH_decomposition(self, tol):
+        """Tests that the decomposition of the CH gate is correct"""
+        op = qml.CH(wires=[0, 1])
+        res = op.decomposition()
+
+        mats = []
+        for i in reversed(res):
+            if i.wires == Wires([1]):
+                mats.append(np.kron(np.eye(2), i.matrix()))
+            elif i.wires == Wires([0, 1]) and i.name == "CZ":
+                mats.append(np.array([[1, 0, 0, 0], [0, 1, 0, 0], [0, 0, 1, 0], [0, 0, 0, -1]]))
+            else:
+                raise Exception("Unexpected gate in decomposition.")
+
+        decomposed_matrix = np.linalg.multi_dot(mats)
+
+        assert np.allclose(decomposed_matrix, op.matrix(), atol=tol, rtol=0)
+
     def test_ISWAP_decomposition(self, tol):
         """Tests that the decomposition of the ISWAP gate is correct"""
         op = qml.ISWAP(wires=[0, 1])
@@ -1067,6 +1086,7 @@ period_two_ops = (
     qml.CNOT(wires=(0, 1)),
     qml.CZ(wires=(0, 1)),
     qml.CY(wires=(0, 1)),
+    qml.CH(wires=(0, 1)),
     qml.SWAP(wires=(0, 1)),
     qml.ISWAP(wires=(0, 1)),
     qml.ECR(wires=(0, 1)),
@@ -1234,6 +1254,11 @@ class TestControlledMethod:
         out = qml.PauliZ(0)._controlled("a")
         assert qml.equal(out, qml.CZ(("a", 0)))
 
+    def test_Hadamard(self):
+        """Test the PauliZ _controlled method."""
+        out = qml.Hadamard(0)._controlled("a")
+        assert qml.equal(out, qml.CH(("a", 0)))
+
     def test_CNOT(self):
         """Test the CNOT _controlled method"""
         out = qml.CNOT((0, 1))._controlled("a")
@@ -1274,6 +1299,7 @@ label_data = [
     (qml.CNOT(wires=(0, 1)), "X", "X"),
     (qml.CZ(wires=(0, 1)), "Z", "Z"),
     (qml.CY(wires=(0, 1)), "Y", "Y"),
+    (qml.CH(wires=(0, 1)), "H", "H"),
     (qml.SWAP(wires=(0, 1)), "SWAP", "SWAP⁻¹"),
     (qml.ISWAP(wires=(0, 1)), "ISWAP", "ISWAP⁻¹"),
     (qml.ECR(wires=(0, 1)), "ECR", "ECR⁻¹"),
@@ -1311,6 +1337,7 @@ control_data = [
     (qml.CNOT(wires=(0, 1)), Wires(0)),
     (qml.CZ(wires=(0, 1)), Wires(0)),
     (qml.CY(wires=(0, 1)), Wires(0)),
+    (qml.CH(wires=(0, 1)), Wires(0)),
     (qml.CSWAP(wires=(0, 1, 2)), Wires([0])),
     (qml.Toffoli(wires=(0, 1, 2)), Wires([0, 1])),
     (qml.MultiControlledX(wires=[0, 1, 2, 3, 4]), Wires([0, 1, 2, 3])),
@@ -1333,6 +1360,7 @@ involution_ops = [  # ops who are their own inverses
     qml.CNOT((0, 1)),
     qml.CZ((0, 1)),
     qml.CY((0, 1)),
+    qml.CH((0, 1)),
     qml.SWAP((0, 1)),
     qml.ECR((0, 1)),
     qml.CSWAP((0, 1, 2)),