Repository: PennyLaneAI/pennylane
Pull request #3340 (2022-11-13 23:32:21)

Qutrit Hadamard
**Context:**

This PR continues the addition of qutrit functionality for PennyLane.

**Description of the Change:**

* Added operation `qml.THadamard`, which is the qutrit Hadamard gate. The operation accepts a `subspace`
  keyword argument which determines which variant of the qutrit Hadamard to use.

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:**


codecov[bot]: # [Codecov](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3340?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
> Merging [#3340](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3340?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (ebb64bf) into [master](https://codecov.io/gh/PennyLaneAI/pennylane/commit/1465ec62a48e97f9f9ae3b3cfd4b11b815290bd2?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) (1465ec6) will **decrease** coverage by `0.00%`.
> The diff coverage is `97.87%`.

> :exclamation: Current head ebb64bf differs from pull request most recent head 1ea08bb. Consider uploading reports for the commit 1ea08bb to get more accurate results

```diff
@@            Coverage Diff             @@
##           master    #3340      +/-   ##
==========================================
- Coverage   99.70%   99.70%   -0.01%     
==========================================
  Files         299      299              
  Lines       26136    26183      +47     
==========================================
+ Hits        26059    26105      +46     
- Misses         77       78       +1     
```


| [Impacted Files](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3340?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/devices/default\_qutrit.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3340/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2RldmljZXMvZGVmYXVsdF9xdXRyaXQucHk=) | `100.00% <ø> (ø)` | |
| [pennylane/ops/qutrit/\_\_init\_\_.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3340/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdXRyaXQvX19pbml0X18ucHk=) | `100.00% <ø> (ø)` | |
| [pennylane/ops/qutrit/non\_parametric\_ops.py](https://codecov.io/gh/PennyLaneAI/pennylane/pull/3340/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9xdXRyaXQvbm9uX3BhcmFtZXRyaWNfb3BzLnB5) | `99.28% <97.87%> (-0.72%)` | :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)


Patch:
diff --git a/doc/introduction/operations.rst b/doc/introduction/operations.rst
index 2ae55d828..b448c02b1 100644
--- a/doc/introduction/operations.rst
+++ b/doc/introduction/operations.rst
@@ -432,6 +432,7 @@ Qutrit non-parametrized gates
 
     ~pennylane.TShift
     ~pennylane.TClock
+    ~pennylane.THadamard
     ~pennylane.TAdd
     ~pennylane.TSWAP
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 700bae45b..990e94dbd 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -279,6 +279,11 @@
   >>> qml.grad(qml.qinfo.purity(circuit, wires=[0]))(param)
   -0.5
   ```
+  
+* Added operation `qml.THadamard`, which is the qutrit Hadamard gate. The operation accepts a `subspace`
+  keyword argument which determines which variant of the qutrit Hadamard to use.
+  [#3340](https://github.com/PennyLaneAI/pennylane/pull/3340)
+
 * New operation `Evolution` defines the exponential of an operator $\hat{O}$ of the form $e^{ix\hat{O}}$, with a single 
   trainable parameter, x. Limiting to a single trainable parameter allows the use of `qml.gradient.param_shift` to 
   find the gradient with respect to the parameter x.
@@ -369,6 +374,10 @@
 
 * Replace (almost) all instances of `with QuantumTape()` with `QuantumScript` construction.
   [(#3454)](https://github.com/PennyLaneAI/pennylane/pull/3454)
+  
+* Added `validate_subspace` static method to `qml.Operator` to check the validity of the subspace of certain
+  qutrit operations.
+  [#3340](https://github.com/PennyLaneAI/pennylane/pull/3340)
 
 * Extended the `qml.equal` function to `Pow`, `SProd`, `Exp` and `Adjoint` objects.
   [(#3471)](https://github.com/PennyLaneAI/pennylane/pull/3471)
@@ -750,6 +759,7 @@ Edward Jiang
 Christina Lee
 Albert Mitjans Coma
 Romain Moyard
+Mudit Pandey
 Matthew Silverman
 Jay Soni
 Antal Száva
diff --git a/pennylane/devices/default_qutrit.py b/pennylane/devices/default_qutrit.py
index 761aff6db..c7f91ccd8 100644
--- a/pennylane/devices/default_qutrit.py
+++ b/pennylane/devices/default_qutrit.py
@@ -60,6 +60,7 @@ class DefaultQutrit(QutritDevice):
         "TClock",
         "TAdd",
         "TSWAP",
+        "THadamard",
     }
 
     # Identity is supported as an observable for qml.state() to work correctly. However, any
diff --git a/pennylane/operation.py b/pennylane/operation.py
index 5bddb1639..5d3c5b911 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -946,6 +946,29 @@ class Operator(abc.ABC):
             return f"{self.name}({params}, wires={self.wires.tolist()})"
         return f"{self.name}(wires={self.wires.tolist()})"
 
+    @staticmethod
+    def validate_subspace(subspace):
+        """Validate the subspace for qutrit operations.
+
+        This method determines whether a given subspace for qutrit operations
+        is defined correctly or not. If not, a `ValueError` is thrown.
+
+        Args:
+            subspace (tuple[int]): Subspace to check for correctness
+        """
+        if not hasattr(subspace, "__iter__") or len(subspace) != 2:
+            raise ValueError(
+                "The subspace must be a sequence with two unique elements from the set {0, 1, 2}."
+            )
+
+        if not all(s in {0, 1, 2} for s in subspace):
+            raise ValueError("Elements of the subspace must be 0, 1, or 2.")
+
+        if subspace[0] == subspace[1]:
+            raise ValueError("Elements of subspace list must be unique.")
+
+        return tuple(sorted(subspace))
+
     @property
     def num_params(self):
         """Number of trainable parameters that the operator depends on.
diff --git a/pennylane/ops/qutrit/__init__.py b/pennylane/ops/qutrit/__init__.py
index c745b4192..55fcd407c 100644
--- a/pennylane/ops/qutrit/__init__.py
+++ b/pennylane/ops/qutrit/__init__.py
@@ -34,6 +34,7 @@ ops = {
     "TClock",
     "TAdd",
     "TSWAP",
+    "THadamard",
 }
 
 obs = {
diff --git a/pennylane/ops/qutrit/non_parametric_ops.py b/pennylane/ops/qutrit/non_parametric_ops.py
index 4cfdcdce5..311235098 100644
--- a/pennylane/ops/qutrit/non_parametric_ops.py
+++ b/pennylane/ops/qutrit/non_parametric_ops.py
@@ -430,3 +430,154 @@ class TSWAP(Operation):
 
     def adjoint(self):
         return TSWAP(wires=self.wires)
+
+
+class THadamard(Operation):
+    r"""THadamard(wires, subspace)
+    The ternary Hadamard operator
+
+    Performs the Hadamard operation on a 2D subspace, if specified. The subspace is
+    given as a keyword argument and determines which two of three single-qutrit basis states the
+    operation applies to. When a subspace is not specified, the generalized Hadamard operation
+    is used.
+
+    The construction of this operator is based on section 2 of
+    `Di et al. (2012) <https://arxiv.org/abs/1105.5485>`_ when the subspace is specified, and
+    definition 4 and equation 5 from `Yeh et al. (2022) <https://arxiv.org/abs/2204.00552>`_
+    when no subspace is specified. The operator definition of the ``subspace=None`` case is
+
+    .. math:: \text{THadamard} = \frac{-i}{\sqrt{3}}\begin{bmatrix}
+                    1 & 1 & 1 \\
+                    1 & \omega & \omega^2 \\
+                    1 & \omega^2 & \omega \\
+                \end{bmatrix}
+
+    where :math:`\omega = \exp(2 \pi i / 3)`.
+
+    **Details:**
+    * Number of wires: 1
+    * Number of parameters: 0
+
+    Args:
+        wires (Sequence[int] or int): the wire the operation acts on
+        subspace (Sequence[int]): the 2D subspace on which to apply the operation. This should be
+            `None` for the generalized Hadamard.
+        do_queue (bool): Indicates whether the operator should be
+            immediately pushed into the Operator queue (optional)
+
+    **Example**
+
+    The specified subspace will determine which basis states the operation actually
+    applies to:
+
+    >>> qml.THadamard(wires=0, subspace=[0, 1]).matrix()
+    array([[ 0.70710678+0.j,  0.70710678+0.j,  0.        +0.j],
+           [ 0.70710678+0.j, -0.70710678+0.j,  0.        +0.j],
+           [ 0.        +0.j,  0.        +0.j,  1.        +0.j]])
+
+    >>> qml.THadamard(wires=0, subspace=[0, 2]).matrix()
+    array([[ 0.70710678+0.j,  0.        +0.j,  0.70710678+0.j],
+           [ 0.        +0.j,  1.        +0.j,  0.        +0.j],
+           [ 0.70710678+0.j,  0.        +0.j, -0.70710678+0.j]])
+
+    >>> qml.THadamard(wires=0, subspace=[1, 2]).matrix()
+    array([[ 1.        +0.j,  0.        +0.j,  0.        +0.j],
+           [ 0.        +0.j,  0.70710678+0.j,  0.70710678+0.j],
+           [ 0.        +0.j,  0.70710678+0.j, -0.70710678+0.j]])
+
+    >>> qml.THadamard(wires=0, subspace=None).matrix()
+    array([[ 0. -0.57735027j,  0. -0.57735027j,  0. -0.57735027j],
+           [ 0. -0.57735027j,  0.5+0.28867513j, -0.5+0.28867513j],
+           [ 0. -0.57735027j, -0.5+0.28867513j,  0.5+0.28867513j]])
+    """
+    num_wires = 1
+    num_params = 0
+    """int: Number of trainable parameters that the operator depends on."""
+
+    def label(self, decimals=None, base_label=None, cache=None):
+        label = base_label or "TH"
+        if self.subspace is None and self.inverse:
+            label += "⁻¹"
+
+        return label
+
+    def __init__(self, wires, subspace=None, do_queue=True):
+        self._subspace = self.validate_subspace(subspace) if subspace is not None else None
+        self._hyperparameters = {
+            "subspace": self.subspace,
+        }
+
+        super().__init__(wires=wires, do_queue=do_queue)
+
+    @property
+    def subspace(self):
+        """The single-qutrit basis states which the operator acts on
+
+        This property returns the 2D subspace on which the operator acts if specified,
+        or None if no subspace is defined. This subspace determines which two single-qutrit
+        basis states the operator acts on. The remaining basis state is not affected by the
+        operator.
+
+        Returns:
+            tuple[int] or None: subspace on which operator acts, if specified, else None
+        """
+        return self._subspace
+
+    @staticmethod
+    def compute_matrix(subspace=None):  # 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:`~.THadamard.matrix`
+
+        Args:
+            subspace (Sequence[int]): the 2D subspace on which to apply operation. This should be
+            `None` for the generalized Hadamard.
+
+        Returns:
+            ndarray: matrix
+
+        **Example**
+
+        >>> print(qml.THadamard.compute_matrix(subspace=[0, 2]))
+        array([[ 0.70710678+0.j,  0.        +0.j,  0.70710678+0.j],
+               [ 0.        +0.j,  1.        +0.j,  0.        +0.j],
+               [ 0.70710678+0.j,  0.        +0.j, -0.70710678+0.j]])
+        """
+
+        if subspace is None:
+            return (-1j / np.sqrt(3)) * np.array(
+                [[1, 1, 1], [1, OMEGA, OMEGA**2], [1, OMEGA**2, OMEGA]]
+            )
+
+        subspace = THadamard.validate_subspace(subspace)
+
+        mat = np.eye(3, dtype=np.complex128)
+
+        unused_ind = list({0, 1, 2}.difference(set(subspace))).pop()
+
+        mat[unused_ind, unused_ind] = np.sqrt(2)
+        mat[subspace[0], subspace[1]] = 1
+        mat[subspace[1], subspace[0]] = 1
+        mat[subspace[1], subspace[1]] = -1
+
+        return mat / np.sqrt(2)
+
+    def adjoint(self):
+        op = THadamard(wires=self.wires, subspace=self.subspace)
+
+        if self.subspace is None:
+            op.inverse = not self.inverse
+
+        return op
+
+    def pow(self, z):
+        if self.subspace is not None:
+            if not isinstance(z, int):
+                return super().pow(z)
+
+            return super().pow(z % 2)
+
+        return super().pow(z)


Test patch:
diff --git a/tests/devices/test_default_qutrit.py b/tests/devices/test_default_qutrit.py
index e47dfa01c..008ce9844 100644
--- a/tests/devices/test_default_qutrit.py
+++ b/tests/devices/test_default_qutrit.py
@@ -80,6 +80,15 @@ class TestApply:
         ),
         (qml.TClock, [1, 0, 0], np.array([1, 0, 0]), None),
         (qml.TClock, [0, 1, 0], np.array([0, OMEGA, 0]), None),
+        (qml.THadamard, [0, 1, 0], np.array([0, 1, 0]), [0, 2]),
+        (
+            qml.THadamard,
+            [1 / np.sqrt(2), 0, 1 / np.sqrt(2)],
+            np.array([1 / np.sqrt(2), 0.5, -0.5]),
+            [1, 2],
+        ),
+        (qml.THadamard, [0, 1, 0], np.array([1, OMEGA, OMEGA**2]) * (-1j / np.sqrt(3)), None),
+        (qml.THadamard, [0, 0, 1], np.array([1, OMEGA**2, OMEGA]) * (-1j / np.sqrt(3)), None),
     ]
 
     test_data_no_parameters_inverses = [
@@ -92,6 +101,25 @@ class TestApply:
         ),
         (qml.TClock, [1, 0, 0], np.array([1, 0, 0]), None),
         (qml.TClock, [0, OMEGA, 0], np.array([0, 1, 0]), None),
+        (qml.THadamard, [0, 1, 0], np.array([0, 1, 0]), [0, 2]),
+        (
+            qml.THadamard,
+            [1 / np.sqrt(2), 0, 1 / np.sqrt(2)],
+            np.array([1 / np.sqrt(2), 0.5, -0.5]),
+            [1, 2],
+        ),
+        (
+            qml.THadamard,
+            np.array([1, OMEGA, OMEGA**2]) * (-1j / np.sqrt(3)),
+            np.array([0, 1, 0]),
+            None,
+        ),
+        (
+            qml.THadamard,
+            np.array([1, OMEGA**2, OMEGA]) * (-1j / np.sqrt(3)),
+            np.array([0, 0, 1]),
+            None,
+        ),
     ]
 
     @pytest.mark.parametrize("operation, input, expected_output, subspace", test_data_no_parameters)
@@ -971,7 +999,6 @@ class TestTensorVar:
         assert np.isclose(res, expected, atol=tol, rtol=0)
 
 
-# TODO: Add tests for tensor non-parametrized observables
 class TestTensorSample:
     """Test tensor samples"""
 
@@ -1233,8 +1260,6 @@ class TestApplyOps:
 class TestApplyOperationUnit:
     """Unit tests for the internal _apply_operation method."""
 
-    # TODO: Add test for testing operations with internal implementations
-
     @pytest.mark.parametrize("inverse", [True, False])
     def test_apply_tensordot_case(self, inverse, monkeypatch):
         """Tests the case when np.tensordot is used to apply an operation in
diff --git a/tests/gate_data.py b/tests/gate_data.py
index e409b979d..e223e8497 100644
--- a/tests/gate_data.py
+++ b/tests/gate_data.py
@@ -103,6 +103,10 @@ TSHIFT = np.array([[0, 0, 1], [1, 0, 0], [0, 1, 0]])  # Qutrit right-shift gate
 
 TCLOCK = np.array([[1, 0, 0], [0, OMEGA, 0], [0, 0, OMEGA**2]])  # Qutrit clock gate
 
+TH = (-1j / np.sqrt(3)) * np.array(
+    [[1, 1, 1], [1, OMEGA, OMEGA**2], [1, OMEGA**2, OMEGA]]
+)  # hadamard gate
+
 TSWAP = np.array(
     [
         [1, 0, 0, 0, 0, 0, 0, 0, 0],
diff --git a/tests/ops/qutrit/test_qutrit_non_parametric_ops.py b/tests/ops/qutrit/test_qutrit_non_parametric_ops.py
index 3ad512136..8e9ccfda3 100644
--- a/tests/ops/qutrit/test_qutrit_non_parametric_ops.py
+++ b/tests/ops/qutrit/test_qutrit_non_parametric_ops.py
@@ -22,13 +22,26 @@ from scipy.stats import unitary_group
 import pennylane as qml
 from pennylane.wires import Wires
 
-from gate_data import TSHIFT, TCLOCK, TADD, TSWAP
+from gate_data import OMEGA, TSHIFT, TCLOCK, TADD, TSWAP, TH
 
 NON_PARAMETRIZED_OPERATIONS = [
-    (qml.TShift, TSHIFT),
-    (qml.TClock, TCLOCK),
-    (qml.TAdd, TADD),
-    (qml.TSWAP, TSWAP),
+    (qml.TShift, TSHIFT, None),
+    (qml.TClock, TCLOCK, None),
+    (qml.TAdd, TADD, None),
+    (qml.TSWAP, TSWAP, None),
+    (qml.THadamard, TH, None),
+    (qml.THadamard, np.array([[1, 1, 0], [1, -1, 0], [0, 0, np.sqrt(2)]]) / np.sqrt(2), [0, 1]),
+    (qml.THadamard, np.array([[1, 0, 1], [0, np.sqrt(2), 0], [1, 0, -1]]) / np.sqrt(2), [0, 2]),
+    (qml.THadamard, np.array([[np.sqrt(2), 0, 0], [0, 1, 1], [0, 1, -1]]) / np.sqrt(2), [1, 2]),
+]
+
+subspace_error_data = [
+    ([1, 1], "Elements of subspace list must be unique."),
+    ([1, 2, 3], "The subspace must be a sequence with"),
+    ([3, 1], "Elements of the subspace must be 0, 1, or 2."),
+    ([3, 3], "Elements of the subspace must be 0, 1, or 2."),
+    ([1], "The subspace must be a sequence with"),
+    (0, "The subspace must be a sequence with two unique"),
 ]
 
 
@@ -36,10 +49,14 @@ NON_PARAMETRIZED_OPERATIONS = [
 
 
 class TestOperations:
-    @pytest.mark.parametrize("op_cls, mat", NON_PARAMETRIZED_OPERATIONS)
-    def test_nonparametrized_op_copy(self, op_cls, mat, tol):
+    @pytest.mark.parametrize("op_cls, mat, subspace", NON_PARAMETRIZED_OPERATIONS)
+    def test_nonparametrized_op_copy(self, op_cls, mat, subspace, tol):
         """Tests that copied nonparametrized ops function as expected"""
-        op = op_cls(wires=range(op_cls.num_wires))
+        op = (
+            op_cls(wires=range(op_cls.num_wires))
+            if subspace is None
+            else op_cls(wires=range(op_cls.num_wires), subspace=subspace)
+        )
         copied_op = copy.copy(op)
         np.testing.assert_allclose(op.matrix(), copied_op.matrix(), atol=tol)
 
@@ -47,15 +64,33 @@ class TestOperations:
         copied_op2 = copy.copy(op)
         np.testing.assert_allclose(op.matrix(), copied_op2.matrix(), atol=tol)
 
-    @pytest.mark.parametrize("ops, mat", NON_PARAMETRIZED_OPERATIONS)
-    def test_matrices(self, ops, mat, tol):
+    @pytest.mark.parametrize("ops, mat, subspace", NON_PARAMETRIZED_OPERATIONS)
+    def test_matrices(self, ops, mat, subspace, tol):
         """Test matrices of non-parametrized operations are correct"""
-        op = ops(wires=range(ops.num_wires))
-        res_static = op.compute_matrix()
+        op = (
+            ops(wires=range(ops.num_wires))
+            if subspace is None
+            else ops(wires=range(ops.num_wires), subspace=subspace)
+        )
+        res_static = (
+            op.compute_matrix() if subspace is None else op.compute_matrix(subspace=subspace)
+        )
         res_dynamic = op.matrix()
         assert np.allclose(res_static, mat, atol=tol, rtol=0)
         assert np.allclose(res_dynamic, mat, atol=tol, rtol=0)
 
+    @pytest.mark.parametrize("subspace, err_msg", subspace_error_data)
+    @pytest.mark.parametrize("op_cls", [qml.THadamard])
+    def test_subspace_op_errors(self, op_cls, subspace, err_msg):
+        """Test that the correct errors are raised when subspace is incorrectly defined"""
+
+        with pytest.raises(ValueError, match=err_msg):
+            op = op_cls(wires=range(op_cls.num_wires), subspace=subspace)
+            op.matrix()
+
+        with pytest.raises(ValueError, match=err_msg):
+            op_cls.compute_matrix(subspace=subspace)
+
 
 class TestEigenval:
     def test_tshift_eigenval(self):
@@ -93,45 +128,68 @@ period_three_ops = [
     qml.TAdd(wires=[0, 1]),
 ]
 
+period_two_ops = [
+    qml.TSWAP(wires=[0, 1]),
+    qml.THadamard(wires=0, subspace=[0, 1]),
+    qml.THadamard(wires=0, subspace=[0, 2]),
+    qml.THadamard(wires=0, subspace=[1, 2]),
+]
 
-class TestPowMethod:
-    @pytest.mark.parametrize("op", period_three_ops)
-    @pytest.mark.parametrize("n", (-5, -2, 1, 4, 7))
-    def test_period_three_1_mod_3(self, op, n):
-        """Tests that ops raised to an integer power == 1 mod 3 are the same as the original op"""
-        assert op.pow(n)[0].__class__ is op.__class__
+no_pow_method_ops = [
+    qml.THadamard(wires=0, subspace=None),
+]
 
-    @pytest.mark.parametrize("op", period_three_ops)
-    @pytest.mark.parametrize("n", (-4, -1, 2, 5, 8))
-    def test_period_three_2_mod_3(self, op, n):
-        """Tests that ops raised to an integer power == 2 mod 3 are the adjoint of the original op"""
-        op_pow = op.pow(n)[0]
-        assert op_pow.__class__ is op.__class__
-        assert np.allclose(op.matrix().conj().T, op_pow.matrix())
-        assert op_pow.inverse == True
 
+class TestPowMethod:
     @pytest.mark.parametrize("op", period_three_ops)
-    @pytest.mark.parametrize("n", (-6, -3, 0, 3, 6))
-    def test_period_three_0_mod_3(self, op, n):
-        """Tests that ops raised to an integer power == 0 mod 3 are empty lists"""
-        assert len(op.pow(n)) == 0
+    @pytest.mark.parametrize("offset", (-6, -3, 0, 3, 6))
+    def test_period_three_pow(self, op, offset):
+        """Tests that ops with period == 3 behave correctly when raised to various
+        integer powers"""
 
-    @pytest.mark.parametrize("op", period_three_ops)
-    def test_period_three_noninteger_power(self, op):
-        """Test that ops with a period of 3 raised to a non-integer power raise an error."""
+        # When raising to power == 0 mod 3
+        assert len(op.pow(0 + offset)) == 0
+
+        # When raising to power == 1 mod 3
+        op_pow_1 = op.pow(1 + offset)[0]
+        assert op_pow_1.__class__ is op.__class__
+        assert np.allclose(op_pow_1.matrix(), op.matrix())
+        assert op_pow_1.inverse == False
+
+        # When raising to power == 2 mod 3
+        op_pow_2 = op.pow(2 + offset)[0]
+        assert op_pow_2.__class__ is op.__class__
+        assert np.allclose(op.matrix().conj().T, op_pow_2.matrix())
+        assert op_pow_2.inverse == True
+
+    @pytest.mark.parametrize("op", period_three_ops + period_two_ops)
+    def test_period_two_three_noninteger_power(self, op):
+        """Test that ops with a period of 2 or 3 raised to a non-integer power raise an error"""
         with pytest.raises(qml.operation.PowUndefinedError):
             op.pow(1.234)
 
     @pytest.mark.parametrize("offset", [0, 2, -2, 4, -4])
-    def test_tswap_pow(self, offset):
-        """Test powers of the TSWAP operator"""
-        op = qml.TSWAP(wires=[0, 1])
+    @pytest.mark.parametrize("op", period_two_ops)
+    def test_period_two_pow(self, offset, op):
+        """Tests that ops with period == 2 behave correctly when raised to various
+        integer powers"""
 
         assert len(op.pow(0 + offset)) == 0
-        assert op.pow(1 + offset)[0].__class__ is qml.TSWAP
+        assert op.pow(1 + offset)[0].__class__ is op.__class__
 
-        with pytest.raises(qml.operation.PowUndefinedError):
-            op.pow(1.234)  # Expect error raised for non-integer power
+    @pytest.mark.parametrize("op", no_pow_method_ops)
+    def test_no_pow_ops(self, op):
+        assert len(op.pow(0)) == 0
+
+        op_pow = op.pow(1)
+        assert len(op_pow) == 1
+        assert op_pow[0].__class__ == op.__class__
+
+        pows = [0.1, 2, -2, -2.5]
+
+        for pow in pows:
+            with pytest.raises(qml.operation.PowUndefinedError):
+                op.pow(pow)
 
 
 label_data = [
@@ -139,6 +197,8 @@ label_data = [
     (qml.TClock(0), "TClock", "TClock⁻¹"),
     (qml.TAdd([0, 1]), "TAdd", "TAdd⁻¹"),
     (qml.TSWAP([0, 1]), "TSWAP", "TSWAP"),
+    (qml.THadamard(0), "TH", "TH⁻¹"),
+    (qml.THadamard(0, subspace=[0, 1]), "TH", "TH"),
 ]
 
 
@@ -156,6 +216,7 @@ control_data = [
     (qml.TClock(0), Wires([])),
     (qml.TAdd([0, 1]), Wires([0])),
     (qml.TSWAP([0, 1]), Wires([])),
+    (qml.THadamard(wires=0), Wires([])),
 ]
 
 
@@ -170,10 +231,14 @@ adjoint_ops = [  # ops that are not their own inverses
     qml.TShift(wires=0),
     qml.TClock(wires=0),
     qml.TAdd(wires=[0, 1]),
+    qml.THadamard(wires=0, subspace=None),
 ]
 
 involution_ops = [  # ops that are their own inverses
     qml.TSWAP(wires=[0, 1]),
+    qml.THadamard(wires=0, subspace=[0, 1]),
+    qml.THadamard(wires=0, subspace=[0, 2]),
+    qml.THadamard(wires=0, subspace=[1, 2]),
 ]