Repository: PennyLaneAI/pennylane
Pull request #4764 (2023-10-31 13:47:34)

`assert_valid` function for testing operators
This function `qml.ops.functions.assert_valid` is a tool for any developer creating their own `Operator` subclass.  It checks multiple details of the class, such as:

* dunder methods
* property and method return types
* interactions between different methods and properties
* interaction with other packages like pickle and jax

This method will also be extensible as we find more mistakes and misconceptions that we could add.


[sc-43674]

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4764?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: `107 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`099bc38`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/099bc38c6acf195a3b5ec959fdfce03fc2c98bfc?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.64% compared to head [(`55a616d`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4764?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.33%.

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master    #4764      +/-   ##
==========================================
- Coverage   99.64%   99.33%   -0.32%     
==========================================
  Files         381      382       +1     
  Lines       34316    34181     -135     
==========================================
- Hits        34195    33952     -243     
- Misses        121      229     +108     
```


| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4764?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Coverage Δ | |
|---|---|---|
| [pennylane/ops/functions/\_\_init\_\_.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4764?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvX19pbml0X18ucHk=) | `100.00% <100.00%> (ø)` | |
| [pennylane/ops/functions/assert\_valid.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4764?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9mdW5jdGlvbnMvYXNzZXJ0X3ZhbGlkLnB5) | `13.70% <13.70%> (ø)` | |

... and [42 files with indirect coverage changes](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4764/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI)


</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4764?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

mudit2812: Approved by accident, meant to select "Comment"

Patch:
diff --git a/doc/development/adding_operators.rst b/doc/development/adding_operators.rst
index 1be3872a7..011d375a5 100644
--- a/doc/development/adding_operators.rst
+++ b/doc/development/adding_operators.rst
@@ -199,6 +199,18 @@ knows a native implementation for ``FlipAndRotate``). It also defines an adjoint
             # the adjoint operator of this gate simply negates the angle
             return FlipAndRotate(-self.parameters[0], self.wires[0], self.wires[1], do_flip=self.hyperparameters["do_flip"])
 
+        @classmethod
+        def _unflatten(cls, data, metadata):
+            # as the class differs from the standard `__init__` call signature of
+            # (*data, wires=wires, **hyperparameters), the _unflatten method that
+            # must be defined as well
+            # _unflatten recreates a opeartion from the serialized data and metadata of ``Operator._flatten``
+            # copied_op = type(op)._unflatten(*op._flatten())
+            wires = metadata[0]
+            hyperparams = dict(metadata[1])
+            return cls(data[0], wire_rot=wires[0], wire_flip=wires[1], do_flip=hyperparams['do_flip'])
+
+
 The new gate can now be created as follows:
 
 >>> op = FlipAndRotate(0.1, wire_rot="q3", wire_flip="q1", do_flip=True)
@@ -209,6 +221,27 @@ FlipAndRotate(0.1, wires=['q3', 'q1'])
 >>> op.adjoint()
 FlipAndRotate(-0.1, wires=['q3', 'q1'])
 
+Once the class has been created, you can run a suite of validation checks using :func:`.ops.functions.assert_valid`.
+This function will warn you of some common errors in custom operators.
+
+>>> qml.ops.functions.assert_valid(op)
+
+If the above operator omitted the ``_unflatten`` custom definition, it would raise:
+
+.. code-block::
+
+
+    TypeError: FlipAndRotate.__init__() got an unexpected keyword argument 'wires'
+
+
+    The above exception was the direct cause of the following exception:
+
+    AssertionError: FlipAndRotate._unflatten must be able to reproduce the original operation
+    from (0.1,) and (<Wires = ['q3', 'q1']>, (('do_flip', True),)). You may need to override
+    either the _unflatten or _flatten method. 
+    For local testing, try type(op)._unflatten(*op._flatten())
+
+
 The new gate can be used with PennyLane devices. Device support for an operation can be checked via
 ``dev.stopping_condition(op)``.  If ``True``, then the device supports the operation.
 
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index fe45baadf..777988ddb 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -33,6 +33,9 @@
 * `qml.draw` and `qml.draw_mpl` now render operator ids.
   [(#4749)](https://github.com/PennyLaneAI/pennylane/pull/4749)
 
+* Added `ops.functions.assert_valid` for checking if an `Operator` class is defined correctly.
+  [(#4764)](https://github.com/PennyLaneAI/pennylane/pull/4764)
+
 <h3>Breaking changes 💔</h3>
 
 * The `prep` keyword argument has been removed from `QuantumScript` and `QuantumTape`.
diff --git a/pennylane/operation.py b/pennylane/operation.py
index a18e06ceb..34149a884 100644
--- a/pennylane/operation.py
+++ b/pennylane/operation.py
@@ -205,6 +205,15 @@ Other
     ~AllWires
     ~AnyWires
 
+.. currentmodule:: pennylane
+
+PennyLane also provides a function for checking the consistency and correctness of an operator instance.
+
+.. autosummary::
+    :toctree: api
+
+    ~ops.functions.assert_valid
+
 Operation attributes
 ~~~~~~~~~~~~~~~~~~~~
 
diff --git a/pennylane/ops/functions/__init__.py b/pennylane/ops/functions/__init__.py
index 557e0675a..292e338ea 100644
--- a/pennylane/ops/functions/__init__.py
+++ b/pennylane/ops/functions/__init__.py
@@ -20,6 +20,7 @@ This module contains functions that act on operators and tapes.
     :toctree: api
 
     ~ops.functions.bind_new_parameters
+    ~ops.functions.assert_valid
     ~dot
     ~eigvals
     ~equal
@@ -45,3 +46,4 @@ from .is_unitary import is_unitary
 from .map_wires import map_wires
 from .matrix import matrix
 from .simplify import simplify
+from .assert_valid import assert_valid
diff --git a/pennylane/ops/functions/assert_valid.py b/pennylane/ops/functions/assert_valid.py
new file mode 100644
index 000000000..7006069bf
--- /dev/null
+++ b/pennylane/ops/functions/assert_valid.py
@@ -0,0 +1,290 @@
+# Copyright 2018-2023 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.
+"""
+This module contains the qml.ops.functions.check_validity function for determining whether or not an
+Operator class is correctly defined.
+"""
+
+from string import ascii_lowercase
+import copy
+import pickle
+
+import numpy as np
+
+import pennylane as qml
+from pennylane.operation import EigvalsUndefinedError
+
+
+def _assert_error_raised(func, error, failure_comment):
+    def inner_func(*args, **kwargs):
+        error_raised = False
+        try:
+            func(*args, **kwargs)
+        except error:
+            error_raised = True
+        assert error_raised, failure_comment
+
+    return inner_func
+
+
+def _check_decomposition(op):
+    """Checks involving the decomposition."""
+    if op.has_decomposition:
+        decomp = op.decomposition()
+        try:
+            compute_decomp = type(op).compute_decomposition(
+                *op.data, wires=op.wires, **op.hyperparameters
+            )
+        except (qml.operation.DecompositionUndefinedError, TypeError):
+            # sometimes decomposition is defined but not compute_decomposition
+            # Also  sometimes compute_decomposition can have a different signature
+            compute_decomp = decomp
+        with qml.queuing.AnnotatedQueue() as queued_decomp:
+            op.decomposition()
+        processed_queue = qml.tape.QuantumTape.from_queue(queued_decomp)
+        expand = op.expand()
+
+        assert isinstance(decomp, list), "decomposition must be a list"
+        assert isinstance(compute_decomp, list), "decomposition must be a list"
+        assert isinstance(expand, qml.tape.QuantumScript), "expand must return a QuantumScript"
+
+        for o1, o2, o3, o4 in zip(decomp, compute_decomp, processed_queue, expand):
+            assert o1 == o2, "decomposition must match compute_decomposition"
+            assert o1 == o3, "decomposition must match queued operations"
+            assert o1 == o4, "decomposition must match expansion"
+            assert isinstance(o1, qml.operation.Operator), "decomposition must contain operators"
+    else:
+        failure_comment = "If has_decomposition is False, then decomposition must raise a ``DecompositionUndefinedError``."
+        _assert_error_raised(
+            op.decomposition,
+            qml.operation.DecompositionUndefinedError,
+            failure_comment=failure_comment,
+        )()
+        _assert_error_raised(
+            op.expand, qml.operation.DecompositionUndefinedError, failure_comment=failure_comment
+        )()
+        _assert_error_raised(
+            op.compute_decomposition,
+            qml.operation.DecompositionUndefinedError,
+            failure_comment=failure_comment,
+        )(*op.data, wires=op.wires, **op.hyperparameters)
+
+
+def _check_matrix(op):
+    """Check that if the operation says it has a matrix, it does. Otherwise a ``MatrixUndefinedError`` should be raised."""
+    if op.has_matrix:
+        mat = op.matrix()
+        assert isinstance(mat, qml.typing.TensorLike), "matrix must be a TensorLike"
+        l = 2 ** len(op.wires)
+        failure_comment = f"matrix must be two dimensional with shape ({l}, {l})"
+        assert qml.math.shape(mat) == (l, l), failure_comment
+    else:
+        failure_comment = (
+            "If has_matrix is False, the matrix method must raise a ``MatrixUndefinedError``."
+        )
+        _assert_error_raised(
+            op.matrix, qml.operation.MatrixUndefinedError, failure_comment=failure_comment
+        )()
+
+
+def _check_matrix_matches_decomp(op):
+    """Check that if both the matrix and decomposition are defined, they match."""
+    if op.has_matrix and op.has_decomposition:
+        mat = op.matrix()
+        decomp_mat = qml.matrix(op.decomposition, wire_order=op.wires)()
+        failure_comment = (
+            f"matrix and matrix from decomposition must match. Got \n{mat}\n\n {decomp_mat}"
+        )
+        assert qml.math.allclose(mat, decomp_mat), failure_comment
+
+
+def _check_eigendecomposition(op):
+    """Checks involving diagonalizing gates and eigenvalues."""
+    if op.has_diagonalizing_gates:
+        dg = op.diagonalizing_gates()
+        try:
+            compute_dg = type(op).compute_diagonalizing_gates(
+                *op.data, wires=op.wires, **op.hyperparameters
+            )
+        except (qml.operation.DiagGatesUndefinedError, TypeError):
+            # sometimes diagonalizing gates is defined but not compute_diagonalizing_gates
+            # compute_diagonalizing_gates might also have a different call signature
+            compute_dg = dg
+
+        for op1, op2 in zip(dg, compute_dg):
+            assert op1 == op2, "diagonalizing_gates and compute_diagonalizing_gates must match"
+    else:
+        failure_comment = "If has_diagonalizing_gates is False, diagonalizing_gates must raise a DiagGatesUndefinedError"
+        _assert_error_raised(
+            op.diagonalizing_gates, qml.operation.DiagGatesUndefinedError, failure_comment
+        )()
+
+    try:
+        eg = op.eigvals()
+    except EigvalsUndefinedError:
+        eg = None
+
+    has_eigvals = True
+    try:
+        compute_eg = type(op).compute_eigvals(*op.data, **op.hyperparameters)
+    except EigvalsUndefinedError:
+        compute_eg = eg
+        has_eigvals = False
+
+    if has_eigvals:
+        assert qml.math.allclose(eg, compute_eg), "eigvals and compute_eigvals must match"
+
+    if has_eigvals and op.has_diagonalizing_gates:
+        dg = qml.prod(*dg) if len(dg) > 0 else qml.Identity(op.wires)
+        eg = qml.QubitUnitary(np.diag(eg), wires=op.wires)
+        decomp = qml.prod(qml.adjoint(dg), eg, dg)
+        decomp_mat = qml.matrix(decomp)
+        original_mat = qml.matrix(op)
+        failure_comment = f"eigenvalues and diagonalizing gates must be able to reproduce the original operator. Got \n{decomp_mat}\n\n{original_mat}"
+        assert qml.math.allclose(decomp_mat, original_mat), failure_comment
+
+
+def _check_copy(op):
+    """Check that copies and deep copies give identical objects."""
+    copied_op = copy.copy(op)
+    assert qml.equal(copied_op, op), "copied op must be equal with qml.equal"
+    assert copied_op == op, "copied op must be equivalent to original operation"
+    assert copied_op is not op, "copied op must be a separate instance from original operaiton"
+    assert qml.equal(copy.deepcopy(op), op), "deep copied op must also be equal"
+
+
+# pylint: disable=import-outside-toplevel, protected-access
+def _check_pytree(op):
+    """Check that the operator is a pytree."""
+    data, metadata = op._flatten()
+    try:
+        assert hash(metadata), "metadata must be hashable"
+    except Exception as e:
+        raise AssertionError(
+            f"metadata output from _flatten must be hashable. Got metadata {metadata}"
+        ) from e
+    try:
+        new_op = type(op)._unflatten(data, metadata)
+    except Exception as e:
+        message = (
+            f"{type(op).__name__}._unflatten must be able to reproduce the original operation from "
+            f"{data} and {metadata}. You may need to override either the _unflatten or _flatten method. "
+            f"\nFor local testing, try type(op)._unflatten(*op._flatten())"
+        )
+        raise AssertionError(message) from e
+    assert op == new_op, "metadata and data must be able to reproduce the original operation"
+
+    try:
+        import jax
+    except ImportError:
+        return
+    leaves, struct = jax.tree_util.tree_flatten(op)
+    unflattened_op = jax.tree_util.tree_unflatten(struct, leaves)
+    assert unflattened_op == op, f"op must be a valid pytree. Got {unflattened_op} instead of {op}."
+
+    for d1, d2 in zip(op.data, leaves):
+        assert qml.math.allclose(
+            d1, d2
+        ), f"data must be the terminal leaves of the pytree. Got {d1}, {d2}"
+
+
+def _check_pickle(op):
+    """Check that an operation can be dumped and reloaded with pickle."""
+    pickled = pickle.dumps(op)
+    unpickled = pickle.loads(pickled)
+    assert unpickled == op, "operation must be able to be pickled and unpickled"
+
+
+# pylint: disable=no-member
+def _check_bind_new_parameters(op):
+    """Check that bind new parameters can create a new op with different data."""
+    new_data = [d * 0.0 for d in op.data]
+    new_data_op = qml.ops.functions.bind_new_parameters(op, new_data)
+    failure_comment = "bind_new_parameters must be able to update the operator with new data."
+    for d1, d2 in zip(new_data_op.data, new_data):
+        assert qml.math.allclose(d1, d2), failure_comment
+
+
+def _check_wires(op):
+    """Check that wires are a ``Wires`` class and can be mapped."""
+    assert isinstance(op.wires, qml.wires.Wires), "wires must be a wires instance"
+
+    wire_map = {w: ascii_lowercase[i] for i, w in enumerate(op.wires)}
+    mapped_op = op.map_wires(wire_map)
+    new_wires = qml.wires.Wires(list(ascii_lowercase[: len(op.wires)]))
+    assert mapped_op.wires == new_wires, "wires must be mappable with map_wires"
+
+
+def assert_valid(op: qml.operation.Operator, skip_pickle=False) -> None:
+    """Runs basic validation checks on an :class:`~.operation.Operator` to make
+    sure it has been correctly defined.
+
+    Args:
+        op (.Operator): an operator instance to validate
+
+    Keyword Args:
+        skip_pickle=False : If ``True``, pickling tests are not run. Set to ``True`` when
+            testing a locally defined operator, as pickle cannot handle local objects
+
+    **Examples:**
+
+    .. code-block:: python
+
+        class MyOp(qml.operation.Operator):
+
+            def __init__(self, data, wires):
+                self.data = data
+                super().__init__(wires=wires)
+
+        op = MyOp(qml.numpy.array(0.5), wires=0)
+
+    .. code-block::
+
+        >>> assert_valid(op)
+        AssertionError: op.data must be a tuple
+
+    .. code-block:: python
+
+        class MyOp(qml.operation.Operator):
+
+            def __init__(self, wires):
+                self.hyperparameters["unhashable_list"] = []
+                super().__init__(wires=wires)
+
+        op = MyOp(wires = 0)
+        assert_valid(op)
+
+    .. code-block::
+
+        ValueError: metadata output from _flatten must be hashable. This also applies to hyperparameters
+
+    """
+
+    assert isinstance(op.data, tuple), "op.data must be a tuple"
+    assert isinstance(op.parameters, list), "op.parameters must be a list"
+    for d, p in zip(op.data, op.parameters):
+        assert isinstance(d, qml.typing.TensorLike), "each data element must be tensorlike"
+        assert qml.math.allclose(d, p), "data and parameters must match."
+
+    _check_wires(op)
+    _check_copy(op)
+    _check_pytree(op)
+    if not skip_pickle:
+        _check_pickle(op)
+    _check_bind_new_parameters(op)
+
+    _check_decomposition(op)
+    _check_matrix(op)
+    _check_matrix_matches_decomp(op)
+    _check_eigendecomposition(op)
diff --git a/pennylane/ops/functions/bind_new_parameters.py b/pennylane/ops/functions/bind_new_parameters.py
index 1d7879305..d28ac9c6a 100644
--- a/pennylane/ops/functions/bind_new_parameters.py
+++ b/pennylane/ops/functions/bind_new_parameters.py
@@ -120,6 +120,14 @@ def bind_new_parameters_symbolic_op(op: SymbolicOp, params: Sequence[TensorLike]
     return op.__class__(new_base, **new_hyperparameters)
 
 
+@bind_new_parameters.register
+def bind_new_parameters_controlled_sequence(
+    op: qml.ControlledSequence, params: Sequence[TensorLike]
+):
+    new_base = bind_new_parameters(op.base, params)
+    return op.__class__(new_base, control=op.control)
+
+
 @bind_new_parameters.register
 def bind_new_parameters_adjoint(op: Adjoint, params: Sequence[TensorLike]):
     # Need a separate dispatch for `Adjoint` because using a more general class
diff --git a/pennylane/ops/qubit/observables.py b/pennylane/ops/qubit/observables.py
index 7d7958240..03f1d1ebb 100644
--- a/pennylane/ops/qubit/observables.py
+++ b/pennylane/ops/qubit/observables.py
@@ -427,7 +427,7 @@ class BasisStateProjector(Projector, Operation):
 
         super().__init__(state, wires=wires, id=id)
 
-    def __new__(cls):  # pylint: disable=arguments-differ
+    def __new__(cls, *_, **__):  # pylint: disable=arguments-differ
         return object.__new__(cls)
 
     def label(self, decimals=None, base_label=None, cache=None):
@@ -555,7 +555,7 @@ class StateVectorProjector(Projector):
 
         super().__init__(state, wires=wires, id=id)
 
-    def __new__(cls):  # pylint: disable=arguments-differ
+    def __new__(cls, *_, **__):  # pylint: disable=arguments-differ
         return object.__new__(cls)
 
     def label(self, decimals=None, base_label=None, cache=None):
diff --git a/pennylane/templates/embeddings/iqp.py b/pennylane/templates/embeddings/iqp.py
index f3ce507c6..809bfcafb 100644
--- a/pennylane/templates/embeddings/iqp.py
+++ b/pennylane/templates/embeddings/iqp.py
@@ -181,8 +181,7 @@ class IQPEmbedding(Operation):
 
         if pattern is None:
             # default is an all-to-all pattern
-            pattern = combinations(wires, 2)
-
+            pattern = tuple(combinations(wires, 2))
         self._hyperparameters = {"pattern": pattern, "n_repeats": n_repeats}
 
         super().__init__(features, wires=wires, id=id)
diff --git a/pennylane/templates/layers/gate_fabric.py b/pennylane/templates/layers/gate_fabric.py
index 4ab1979e8..ecdafb091 100644
--- a/pennylane/templates/layers/gate_fabric.py
+++ b/pennylane/templates/layers/gate_fabric.py
@@ -200,7 +200,7 @@ class GateFabric(Operation):
             )
 
         self._hyperparameters = {
-            "init_state": qml.math.toarray(init_state),
+            "init_state": tuple(init_state),
             "include_pi": include_pi,
         }
 
diff --git a/pennylane/templates/layers/particle_conserving_u1.py b/pennylane/templates/layers/particle_conserving_u1.py
index 24928ce0d..fc0787ec4 100644
--- a/pennylane/templates/layers/particle_conserving_u1.py
+++ b/pennylane/templates/layers/particle_conserving_u1.py
@@ -263,7 +263,7 @@ class ParticleConservingU1(Operation):
                 f"Weights tensor must have third dimension of length 2; got {shape[2]}"
             )
 
-        self._hyperparameters = {"init_state": qml.math.toarray(init_state)}
+        self._hyperparameters = {"init_state": tuple(init_state)}
 
         super().__init__(weights, wires=wires, id=id)
 
diff --git a/pennylane/templates/layers/particle_conserving_u2.py b/pennylane/templates/layers/particle_conserving_u2.py
index d1a2b8d2c..8518b6dcd 100644
--- a/pennylane/templates/layers/particle_conserving_u2.py
+++ b/pennylane/templates/layers/particle_conserving_u2.py
@@ -169,7 +169,7 @@ class ParticleConservingU2(Operation):
                 f"Weights tensor must have a second dimension of length {2 * len(wires) - 1}; got {shape[1]}"
             )
 
-        self._hyperparameters = {"init_state": qml.math.toarray(init_state)}
+        self._hyperparameters = {"init_state": tuple(init_state)}
 
         super().__init__(weights, wires=wires, id=id)
 
diff --git a/pennylane/templates/layers/strongly_entangling.py b/pennylane/templates/layers/strongly_entangling.py
index e7c8eb8ba..017b3c74b 100644
--- a/pennylane/templates/layers/strongly_entangling.py
+++ b/pennylane/templates/layers/strongly_entangling.py
@@ -148,10 +148,11 @@ class StronglyEntanglingLayers(Operation):
         if ranges is None:
             if len(wires) > 1:
                 # tile ranges with iterations of range(1, n_wires)
-                ranges = [(l % (len(wires) - 1)) + 1 for l in range(shape[0])]
+                ranges = tuple((l % (len(wires) - 1)) + 1 for l in range(shape[0]))
             else:
-                ranges = [0] * shape[0]
+                ranges = (0,) * shape[0]
         else:
+            ranges = tuple(ranges)
             if len(ranges) != shape[0]:
                 raise ValueError(f"Range sequence must be of length {shape[0]}; got {len(ranges)}")
             for r in ranges:
diff --git a/pennylane/templates/subroutines/all_singles_doubles.py b/pennylane/templates/subroutines/all_singles_doubles.py
index cce3759ee..3e52d7752 100644
--- a/pennylane/templates/subroutines/all_singles_doubles.py
+++ b/pennylane/templates/subroutines/all_singles_doubles.py
@@ -140,15 +140,18 @@ class AllSinglesDoubles(Operation):
         if weights_shape != exp_shape:
             raise ValueError(f"'weights' tensor must be of shape {exp_shape}; got {weights_shape}.")
 
+        if hf_state[0].dtype != np.dtype("int"):
+            raise ValueError(f"Elements of 'hf_state' must be integers; got {hf_state.dtype}")
+
+        singles = tuple(tuple(s) for s in singles)
+        doubles = tuple(tuple(d) for d in doubles)
+
         self._hyperparameters = {
-            "hf_state": qml.math.toarray(hf_state),
+            "hf_state": tuple(hf_state),
             "singles": singles,
             "doubles": doubles,
         }
 
-        if hf_state.dtype != np.dtype("int"):
-            raise ValueError(f"Elements of 'hf_state' must be integers; got {hf_state.dtype}")
-
         super().__init__(weights, wires=wires, id=id)
 
     @property
diff --git a/pennylane/templates/subroutines/commuting_evolution.py b/pennylane/templates/subroutines/commuting_evolution.py
index ba4fd76e7..bb1e1f2b3 100644
--- a/pennylane/templates/subroutines/commuting_evolution.py
+++ b/pennylane/templates/subroutines/commuting_evolution.py
@@ -108,12 +108,12 @@ class CommutingEvolution(Operation):
 
     def _flatten(self):
         h = self.hyperparameters["hamiltonian"]
-        data = (h, self.data[0])
+        data = (self.data[0], h)
         return data, (self.hyperparameters["frequencies"], self.hyperparameters["shifts"])
 
     @classmethod
     def _unflatten(cls, data, metadata) -> "CommutingEvolution":
-        return cls(data[0], data[1], frequencies=metadata[0], shifts=metadata[1])
+        return cls(data[1], data[0], frequencies=metadata[0], shifts=metadata[1])
 
     def __init__(self, hamiltonian, time, frequencies=None, shifts=None, id=None):
         # pylint: disable=import-outside-toplevel
diff --git a/pennylane/templates/subroutines/flip_sign.py b/pennylane/templates/subroutines/flip_sign.py
index fa1fdfb62..745ffc315 100644
--- a/pennylane/templates/subroutines/flip_sign.py
+++ b/pennylane/templates/subroutines/flip_sign.py
@@ -88,6 +88,7 @@ class FlipSign(Operation):
                 raise ValueError(
                     "expected an integer equal or greater than zero for basic flipping state"
                 )
+        n = tuple(n)
 
         if len(wires) != len(n):
             raise ValueError(
diff --git a/pennylane/templates/subroutines/permute.py b/pennylane/templates/subroutines/permute.py
index 16444affc..394fa2f64 100644
--- a/pennylane/templates/subroutines/permute.py
+++ b/pennylane/templates/subroutines/permute.py
@@ -23,7 +23,7 @@ class Permute(Operation):
     r"""Applies a permutation to a set of wires.
 
     Args:
-        permutation (list): A list of wire labels that represents the new ordering of wires
+        permutation (Sequence): A list of wire labels that represents the new ordering of wires
             after the permutation. The list may consist of integers or strings, so long as
             they match the labels of ``wires``.
         wires (Iterable or Wires): Wires that the permutation acts on. Accepts an iterable
@@ -158,7 +158,7 @@ class Permute(Operation):
             if label not in wires:
                 raise ValueError(f"Cannot permute wire {label} not present in wire set.")
 
-        self._hyperparameters = {"permutation": permutation}
+        self._hyperparameters = {"permutation": tuple(permutation)}
         super().__init__(wires=wires, id=id)
 
     @property
diff --git a/pennylane/templates/subroutines/qdrift.py b/pennylane/templates/subroutines/qdrift.py
index de7ba8f0f..d1b52f9ea 100644
--- a/pennylane/templates/subroutines/qdrift.py
+++ b/pennylane/templates/subroutines/qdrift.py
@@ -41,7 +41,7 @@ def _sample_decomposition(coeffs, ops, time, n=1, seed=None):
     ]
 
     choice_rng = qml.math.random.default_rng(seed)
-    return choice_rng.choice(exps, p=probs, size=n, replace=True)
+    return tuple(choice_rng.choice(exps, p=probs, size=n, replace=True))
 
 
 class QDrift(Operation):
@@ -246,7 +246,7 @@ class QDrift(Operation):
             for op in decomp:
                 qml.apply(op)
 
-        return decomp
+        return list(decomp)
 
     @staticmethod
     def error(hamiltonian, time, n=1):
diff --git a/pennylane/templates/subroutines/qft.py b/pennylane/templates/subroutines/qft.py
index a0b547fa0..a239b436c 100644
--- a/pennylane/templates/subroutines/qft.py
+++ b/pennylane/templates/subroutines/qft.py
@@ -71,6 +71,9 @@ class QFT(Operation):
         self.hyperparameters["n_wires"] = len(wires)
         super().__init__(wires=wires, id=id)
 
+    def _flatten(self):
+        return tuple(), (self.wires, tuple())
+
     @property
     def num_params(self):
         return 0
diff --git a/pennylane/templates/subroutines/select.py b/pennylane/templates/subroutines/select.py
index 48ece88e5..8fa4c0fd6 100644
--- a/pennylane/templates/subroutines/select.py
+++ b/pennylane/templates/subroutines/select.py
@@ -72,9 +72,12 @@ class Select(Operation):
     def _unflatten(cls, data, metadata) -> "Select":
         return cls(data, metadata)
 
+    def __repr__(self):
+        return f"Select(ops={self.ops}, control={self.control})"
+
     def __init__(self, ops, control, id=None):
         control = qml.wires.Wires(control)
-        self.hyperparameters["ops"] = ops
+        self.hyperparameters["ops"] = tuple(ops)
         self.hyperparameters["control"] = control
 
         if 2 ** len(control) < len(ops):
@@ -99,6 +102,11 @@ class Select(Operation):
         all_wires = target_wires + control
         super().__init__(*self.data, wires=all_wires, id=id)
 
+    def map_wires(self, wire_map: dict) -> "Select":
+        new_ops = [o.map_wires(wire_map) for o in self.hyperparameters["ops"]]
+        new_control = [wire_map.get(wire, wire) for wire in self.hyperparameters["control"]]
+        return Select(new_ops, new_control)
+
     def __copy__(self):
         """Copy this op"""
         cls = self.__class__


Test patch:
diff --git a/tests/ops/functions/test_assert_valid.py b/tests/ops/functions/test_assert_valid.py
new file mode 100644
index 000000000..3b4f561d3
--- /dev/null
+++ b/tests/ops/functions/test_assert_valid.py
@@ -0,0 +1,317 @@
+# Copyright 2018-2023 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.
+"""
+This module contains unit tests for ``qml.ops.functions.assert_valid``.
+"""
+# pylint: disable=too-few-public-methods, unused-argument
+import pytest
+
+import numpy as np
+
+import pennylane as qml
+from pennylane.operation import Operator
+from pennylane.ops.functions import assert_valid
+
+
+class TestDecompositionErrors:
+    """Test assertions involving decompositions."""
+
+    def test_bad_decomposition_output(self):
+        """Test decomposition output must be a list of operators."""
+
+        class BadDecomp(Operator):
+            @staticmethod
+            def compute_decomposition(wires):
+                qml.RX(1.2, wires=0)
+
+        with pytest.raises(AssertionError, match=r"decomposition must be a list"):
+            assert_valid(BadDecomp(wires=0), skip_pickle=True)
+
+    def test_bad_decomposition_queuing(self):
+        """Test that output must match queued contents."""
+
+        class BadDecomp(Operator):
+            @staticmethod
+            def compute_decomposition(wires):
+                qml.RX(1.2, wires=0)
+                return [qml.RY(2.3, 0)]
+
+        with pytest.raises(AssertionError, match="decomposition must match queued operations"):
+            assert_valid(BadDecomp(wires=0), skip_pickle=True)
+
+    def test_expand_must_be_qscript(self):
+        """Test that an error is raised if expand does not return a QuantumScript"""
+
+        class BadDecomp(Operator):
+            @staticmethod
+            def compute_decomposition(wires):
+                return [qml.RY(2.3, 0)]
+
+            def expand(self):
+                return [qml.S(0)]
+
+        with pytest.raises(AssertionError, match=r"expand must return a QuantumScript"):
+            assert_valid(BadDecomp(wires=0), skip_pickle=True)
+
+    def test_decomposition_must_match_expand(self):
+        """Test that decomposition and expand must match."""
+
+        class BadDecomp(Operator):
+            @staticmethod
+            def compute_decomposition(wires):
+                return [qml.RY(2.3, 0)]
+
+            def expand(self):
+                return qml.tape.QuantumScript([qml.S(0)])
+
+        with pytest.raises(AssertionError, match="decomposition must match expansion"):
+            assert_valid(BadDecomp(wires=0), skip_pickle=True)
+
+    def test_error_not_raised(self):
+        """Test if has_decomposition is False but decomposition defined."""
+
+        class BadDecomp(Operator):
+            @staticmethod
+            def compute_decomposition(wires):
+                return [qml.RY(2.3, 0)]
+
+            has_decomposition = False
+
+        with pytest.raises(AssertionError, match="If has_decomposition is False"):
+            assert_valid(BadDecomp(wires=0), skip_pickle=True)
+
+
+class TestBadMatrix:
+    """Tests involving matrix validation."""
+
+    def test_error_not_raised(self):
+        """Test that if has_matrix if False, then an error must be raised."""
+
+        class BadMat(Operator):
+            has_matrix = False
+
+            def matrix(self):
+                return np.eye(2)
+
+        with pytest.raises(
+            AssertionError, match="If has_matrix is False, the matrix method must raise"
+        ):
+            assert_valid(BadMat(wires=0), skip_pickle=True)
+
+    def test_bad_matrix_shape(self):
+        """Test an error if the matrix is of the wrong shape."""
+
+        class BadMat(Operator):
+            @staticmethod
+            def compute_matrix():
+                return np.eye(2)
+
+        with pytest.raises(
+            AssertionError, match=r"matrix must be two dimensional with shape \(4, 4\)"
+        ):
+            assert_valid(BadMat(wires=(0, 1)), skip_pickle=True)
+
+    def test_matrix_not_tensorlike(self):
+        """Test an error is raised if the matrix is not a TensorLike"""
+
+        class BadMat(Operator):
+            @staticmethod
+            def compute_matrix():
+                return "a"
+
+        with pytest.raises(AssertionError, match=r"matrix must be a TensorLike"):
+            assert_valid(BadMat(0), skip_pickle=True)
+
+
+class TestBadCopyComparison:
+    """Check errors invovling copy, deepcopy, and comparison."""
+
+    def test_bad_comparison(self):
+        """Test an operator that cannot be compared with standard qml.equal."""
+
+        class BadComparison(Operator):
+            def __init__(self, wires, val):
+                self.hyperparameters["val"] = val
+                super().__init__(wires)
+
+        with pytest.raises(ValueError, match=r"The truth value of an array with more than one"):
+            assert_valid(BadComparison(0, val=np.eye(2)))
+
+
+def test_mismatched_mat_decomp():
+    """Test that an error is raised if the matrix does not match the decomposition if both are defined."""
+
+    class MisMatchedMatDecomp(Operator):
+        @staticmethod
+        def compute_matrix():
+            return np.eye(2)
+
+        def decomposition(self):
+            return [qml.PauliX(0)]
+
+    with pytest.raises(AssertionError, match=r"matrix and matrix from decomposition must match"):
+        assert_valid(MisMatchedMatDecomp(0), skip_pickle=True)
+
+
+def test_bad_eigenvalues_order():
+    """Test that an error is raised if the order of eigenvalues does not match the diagonalizing gates."""
+
+    class BadEigenDecomp(qml.PauliX):
+        @staticmethod
+        def compute_eigvals():
+            return [-1, 1]
+
+    with pytest.raises(
+        AssertionError, match=r"eigenvalues and diagonalizing gates must be able to"
+    ):
+        assert_valid(BadEigenDecomp(0), skip_pickle=True)
+
+
+class BadPickling0(Operator):
+    def __init__(self, f, wires):
+        super().__init__(wires)
+
+        self.hyperparameters["f"] = f
+
+
+def test_bad_pickling():
+    """Test an error is raised in an operator cant be pickled."""
+
+    with pytest.raises(AttributeError, match="Can't pickle local object"):
+        assert_valid(BadPickling0(lambda x: x, wires=0))
+
+
+def test_bad_bind_new_parameters():
+    """Test a function that is not working with bind_new_parameters."""
+
+    class NoBindNewParameters(Operator):
+        num_params = 1
+
+        def __init__(self, x, wires):
+            super().__init__(x, wires)
+            self.data = (1.0,)  # different x will not change data attribute
+
+    with pytest.raises(
+        AssertionError, match=r"bind_new_parameters must be able to update the operator"
+    ):
+        assert_valid(NoBindNewParameters(2.0, wires=0), skip_pickle=True)
+
+
+def test_bad_wire_mapping():
+    """Test that an error is raised if the wires cant be mapped with map_wires."""
+
+    class BadWireMap(Operator):
+        def __init__(self, op1):
+            self.hyperparameters["op1"] = op1
+            super().__init__(wires=op1.wires)
+
+        @property
+        def wires(self):
+            return self.hyperparameters["op1"].wires
+
+    with pytest.raises(AssertionError, match=r"wires must be mappable"):
+        assert_valid(BadWireMap(qml.PauliX(0)), skip_pickle=True)
+
+
+class TestPytree:
+    """Pytree related checks."""
+
+    def test_not_hashable_metadata(self):
+        """Assert that an error is raised if metadata is not hashable."""
+
+        class BadMetadata(Operator):
+            def __init__(self, wires):
+                super().__init__(wires)
+                self.hyperparameters["test"] = ["a"]
+
+        op = BadMetadata(0)
+        with pytest.raises(AssertionError, match=r"metadata output from _flatten must be hashable"):
+            assert_valid(op, skip_pickle=True)
+
+    def test_bad_pytree(self):
+        """Check an operation that errors out of the _unflatten call."""
+
+        class BadPytree(qml.operation.Operator):
+            def __init__(self, wires1, wires2):
+                super().__init__(wires=wires1 + wires2)
+
+        op = BadPytree([0], [1])
+
+        with pytest.raises(AssertionError, match=r"BadPytree._unflatten must be able to reproduce"):
+            assert_valid(op, skip_pickle=True)
+
+    def test_badpytree_incomplete_info(self):
+        """Assert that an unpacking and repacking a pytree reproduces the original operation."""
+
+        class BadPytree(Operator):
+            def _flatten(self):
+                return tuple(), (self.wires, tuple())
+
+            def __init__(self, wires, val="a"):
+                super().__init__(wires)
+                self.hyperparameters["val"] = val
+
+        op = BadPytree(wires=0, val="b")
+        with pytest.raises(
+            AssertionError,
+            match=r"metadata and data must be able to reproduce the original operation",
+        ):
+            assert_valid(op, skip_pickle=True)
+
+    @pytest.mark.jax
+    def test_nested_bad_pytree(self):
+        """Test that an operator with a bad leaf will raise an error."""
+
+        class BadPytree(Operator):
+            def _flatten(self):
+                return tuple(), (self.wires, tuple())
+
+            def __init__(self, wires, val="a"):
+                super().__init__(wires)
+                self.hyperparameters["val"] = val
+
+        op = qml.adjoint(BadPytree(wires=0, val="b"))
+        with pytest.raises(AssertionError, match=r"op must be a valid pytree."):
+            assert_valid(op, skip_pickle=True)
+
+    @pytest.mark.jax
+    def test_bad_leaves_ordering(self):
+        """Test an error is raised if data and pytree leaves have a different ordering convention."""
+
+        class BadLeavesOrdering(qml.ops.op_math.SProd):
+            def _flatten(self):
+                return (self.base, self.scalar), tuple()
+
+            @classmethod
+            def _unflatten(cls, data, _):
+                return cls(data[1], data[0])
+
+        op = BadLeavesOrdering(2.0, qml.RX(1.2, wires=0))
+
+        with pytest.raises(AssertionError, match=r"data must be the terminal leaves of the pytree"):
+            assert_valid(op, skip_pickle=True)
+
+
+def test_data_is_tuple():
+    """Check that the data property is a tuple."""
+
+    class BadData(Operator):
+        num_params = 1
+
+        def __init__(self, x, wires):
+            super().__init__(x, wires)
+            self.data = [x]
+
+    with pytest.raises(AssertionError, match=r"op.data must be a tuple"):
+        assert_valid(BadData(2.0, wires=0))
diff --git a/tests/ops/functions/test_bind_new_parameters.py b/tests/ops/functions/test_bind_new_parameters.py
index 4c4f24418..3271660de 100644
--- a/tests/ops/functions/test_bind_new_parameters.py
+++ b/tests/ops/functions/test_bind_new_parameters.py
@@ -176,6 +176,14 @@ def test_symbolic_ops(op, new_params, expected_op):
     assert new_op.base is not op.base
 
 
+def test_controlled_sequence():
+    """Test integration of controlled sequence with bind_new_parameters."""
+    op = qml.ControlledSequence(qml.RX(0.25, wires=3), control=[0, 1, 2])
+    new_op = bind_new_parameters(op, (0.5,))
+    assert qml.math.allclose(new_op.data[0], 0.5)
+    assert qml.equal(new_op.base, qml.RX(0.5, wires=3))
+
+
 @pytest.mark.parametrize(
     "H, new_coeffs, expected_H",
     [
diff --git a/tests/ops/op_math/test_adjoint.py b/tests/ops/op_math/test_adjoint.py
index 4a7278d6a..da39057db 100644
--- a/tests/ops/op_math/test_adjoint.py
+++ b/tests/ops/op_math/test_adjoint.py
@@ -28,6 +28,13 @@ class PlainOperator(qml.operation.Operator):
     """just an operator."""
 
 
+@pytest.mark.parametrize("target", (qml.PauliZ(0), qml.Rot(1.2, 2.3, 3.4, wires=0)))
+def test_basic_validity(target):
+    """Run basic operator validity fucntions."""
+    op = qml.adjoint(target)
+    qml.ops.functions.assert_valid(op)
+
+
 class TestInheritanceMixins:
     """Test inheritance structure and mixin addition through dynamic __new__ method."""
 
diff --git a/tests/ops/op_math/test_evolution.py b/tests/ops/op_math/test_evolution.py
index f707be56f..0044b3ada 100644
--- a/tests/ops/op_math/test_evolution.py
+++ b/tests/ops/op_math/test_evolution.py
@@ -19,6 +19,13 @@ from pennylane import numpy as np
 from pennylane.ops.op_math import Evolution, Exp
 
 
+def test_basic_validity():
+    """Assert the basic validity of an evolution op."""
+    base = qml.prod(qml.PauliX(0), qml.PauliY(1))
+    op = Evolution(base, 5.2)
+    qml.ops.functions.assert_valid(op)
+
+
 class TestEvolution:
     """Test Evolution(Exp) class that takes a parameter x and a generator G and defines an evolution exp(ixG)"""
 
diff --git a/tests/ops/op_math/test_pow_op.py b/tests/ops/op_math/test_pow_op.py
index 1cd473fa8..af7f45508 100644
--- a/tests/ops/op_math/test_pow_op.py
+++ b/tests/ops/op_math/test_pow_op.py
@@ -39,6 +39,13 @@ def pow_using_dunder_method(base, z, id=None):
     return base**z
 
 
+@pytest.mark.xfail  # TODO: remove xfail as part of story 49618
+def test_basic_validity():
+    """Run basic operator validity checks."""
+    op = qml.pow(qml.RX(1.2, wires=0), 3)
+    qml.ops.functions.assert_valid(op)
+
+
 class TestConstructor:
     def test_lazy_mode(self):
         """Test that by default, the operator is simply wrapped in `Pow`, even if a simplification exists."""
diff --git a/tests/ops/op_math/test_prod.py b/tests/ops/op_math/test_prod.py
index 48c6a558f..074ec74fc 100644
--- a/tests/ops/op_math/test_prod.py
+++ b/tests/ops/op_math/test_prod.py
@@ -87,6 +87,17 @@ ops_hermitian_status = (  # computed manually
 )
 
 
+# currently failing due to has_diagonalizing_gates logic
+@pytest.mark.xfail  # TODO: fix with story 49608
+def test_basic_validity():
+    """Run basic validity checks on a prod operator."""
+    op1 = qml.PauliZ(0)
+    op2 = qml.Rot(1.2, 2.3, 3.4, wires=0)
+    op3 = qml.IsingZZ(4.32, wires=("a", "b"))
+    op = qml.prod(op1, op2, op3)
+    qml.ops.functions.assert_valid(op)
+
+
 def compare_and_expand_mat(mat1, mat2):
     """Helper function which takes two square matrices (of potentially different sizes)
     and expands the smaller matrix until their shapes match."""
@@ -854,6 +865,7 @@ class TestProperties:
         assert np.allclose(eig_vals, cached_vals)
         assert np.allclose(eig_vecs, cached_vecs)
 
+    # pylint: disable=use-implicit-booleaness-not-comparison
     def test_diagonalizing_gates(self):
         """Test that the diagonalizing gates are correct."""
         diag_prod_op = Prod(qml.PauliZ(wires=0), qml.PauliZ(wires=1))
diff --git a/tests/ops/qubit/test_observables.py b/tests/ops/qubit/test_observables.py
index 721bcf4c7..525567a91 100644
--- a/tests/ops/qubit/test_observables.py
+++ b/tests/ops/qubit/test_observables.py
@@ -470,6 +470,8 @@ class TestProjector:
         second_projector = qml.Projector(basis_state, wires)
         assert qml.equal(second_projector, basis_state_projector)
 
+        qml.ops.functions.assert_valid(basis_state_projector)
+
     def test_statevector_projector(self):
         """Test that we obtain a _StateVectorProjector when input is a state vector."""
         state_vector = np.array([1, 1, 1, 1]) / 2
@@ -480,6 +482,8 @@ class TestProjector:
         second_projector = qml.Projector(state_vector, wires)
         assert qml.equal(second_projector, state_vector_projector)
 
+        qml.ops.functions.assert_valid(state_vector_projector)
+
     def test_pow_zero(self):
         """Assert that the projector raised to zero is an empty list."""
         # Basis state projector
diff --git a/tests/pulse/test_parametrized_evolution.py b/tests/pulse/test_parametrized_evolution.py
index f2f2fd083..397f57a56 100644
--- a/tests/pulse/test_parametrized_evolution.py
+++ b/tests/pulse/test_parametrized_evolution.py
@@ -36,6 +36,21 @@ class MyOp(qml.RX):  # pylint: disable=too-few-public-methods
     has_diagonalizing_gates = False
 
 
+@pytest.mark.jax
+@pytest.mark.xfail  # TODO: fix by story 50356
+def test_standard_validity():
+    """Run standard validity checks on the parametrized evolution."""
+    from jax import numpy as jnp
+
+    def f1(p, t):
+        return jnp.sin(p * t)
+
+    H = f1 * qml.PauliY(0)
+
+    ev = qml.evolve(H)
+    qml.ops.functions.assert_valid(ev)
+
+
 def time_independent_hamiltonian():
     """Create a time-independent Hamiltonian on two qubits."""
     ops = [qml.PauliX(0), qml.PauliZ(1), qml.PauliY(0), qml.PauliX(1)]
diff --git a/tests/templates/test_embeddings/test_amplitude.py b/tests/templates/test_embeddings/test_amplitude.py
index 4fb1296e6..f538bdb4a 100644
--- a/tests/templates/test_embeddings/test_amplitude.py
+++ b/tests/templates/test_embeddings/test_amplitude.py
@@ -44,6 +44,13 @@ TOO_MANY_FEATURES = [
 TOO_MANY_BROADCASTED_FEATURES = [np.eye(6)[:3, :5], np.ones((3, 8)) / np.sqrt(8)]
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    op = qml.AmplitudeEmbedding(features=FEATURES[0], wires=range(2))
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_embeddings/test_angle.py b/tests/templates/test_embeddings/test_angle.py
index 2cb367726..12b9f96b1 100644
--- a/tests/templates/test_embeddings/test_angle.py
+++ b/tests/templates/test_embeddings/test_angle.py
@@ -20,6 +20,12 @@ from pennylane import numpy as pnp
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    op = qml.AngleEmbedding(features=[1, 2, 3], wires=range(3), rotation="Z")
+    qml.ops.functions.assert_valid(op)
+
+
 def test_repr():
     """Test the custom repr for angle embedding."""
     op = qml.AngleEmbedding(features=[1, 2, 3], wires=range(3), rotation="Z")
diff --git a/tests/templates/test_embeddings/test_basis.py b/tests/templates/test_embeddings/test_basis.py
index 158448338..b901a0c7d 100644
--- a/tests/templates/test_embeddings/test_basis.py
+++ b/tests/templates/test_embeddings/test_basis.py
@@ -20,6 +20,13 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    wires = qml.wires.Wires((0, 1, 2))
+    op = qml.BasisEmbedding(features=np.array([1, 1, 1]), wires=wires)
+    qml.ops.functions.assert_valid(op)
+
+
 # pylint: disable=protected-access
 def test_flatten_unflatten():
     """Test the _flatten and _unflatten methods."""
diff --git a/tests/templates/test_embeddings/test_displacement_emb.py b/tests/templates/test_embeddings/test_displacement_emb.py
index 4d0074965..a942b9823 100644
--- a/tests/templates/test_embeddings/test_displacement_emb.py
+++ b/tests/templates/test_embeddings/test_displacement_emb.py
@@ -21,6 +21,13 @@ from pennylane import numpy as pnp
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    feature_vector = [1, 2, 3]
+    op = qml.DisplacementEmbedding(features=feature_vector, wires=range(3), method="phase", c=0.5)
+    qml.ops.functions.assert_valid(op)
+
+
 def test_flatten_unflatten_methods():
     """Test the _flatten and _unflatten methods."""
     feature_vector = [1, 2, 3]
diff --git a/tests/templates/test_embeddings/test_iqp_emb.py b/tests/templates/test_embeddings/test_iqp_emb.py
index b0cc165f0..23a52ca16 100644
--- a/tests/templates/test_embeddings/test_iqp_emb.py
+++ b/tests/templates/test_embeddings/test_iqp_emb.py
@@ -20,6 +20,14 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    features = (0, 1, 2)
+
+    op = qml.IQPEmbedding(features, wires=(0, 1, 2))
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_embeddings/test_qaoa_emb.py b/tests/templates/test_embeddings/test_qaoa_emb.py
index a33dbac9c..9bf92ff86 100644
--- a/tests/templates/test_embeddings/test_qaoa_emb.py
+++ b/tests/templates/test_embeddings/test_qaoa_emb.py
@@ -21,6 +21,17 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    features = [1.0, 2.0]
+    layer1 = [0.1, -0.3, 1.5]
+    layer2 = [3.1, 0.2, -2.8]
+    weights = [layer1, layer2]
+
+    op = qml.QAOAEmbedding(features=features, wires=(0, 1), weights=weights)
+    qml.ops.functions.assert_valid(op)
+
+
 # pylint: disable=protected-access
 def test_flatten_unflatten():
     """Test _flatten and _unflatten methods."""
diff --git a/tests/templates/test_embeddings/test_squeezing_emb.py b/tests/templates/test_embeddings/test_squeezing_emb.py
index f50cc6ff0..b28c31020 100644
--- a/tests/templates/test_embeddings/test_squeezing_emb.py
+++ b/tests/templates/test_embeddings/test_squeezing_emb.py
@@ -21,6 +21,13 @@ from pennylane import numpy as pnp
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    feature_vector = [1, 2, 3]
+    op = qml.SqueezingEmbedding(features=feature_vector, wires=range(3), method="phase", c=0.5)
+    qml.ops.functions.assert_valid(op)
+
+
 def test_flatten_unflatten_methods():
     """Test the _flatten and _unflatten methods."""
     feature_vector = [1, 2, 3]
diff --git a/tests/templates/test_layers/test_basic_entangler.py b/tests/templates/test_layers/test_basic_entangler.py
index 1e2170333..276258ec6 100644
--- a/tests/templates/test_layers/test_basic_entangler.py
+++ b/tests/templates/test_layers/test_basic_entangler.py
@@ -21,6 +21,15 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    weights = np.random.random((1, 3))
+
+    op = qml.BasicEntanglerLayers(weights, wires=range(3))
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_layers/test_gate_fabric.py b/tests/templates/test_layers/test_gate_fabric.py
index fedad87a0..02fd264ac 100644
--- a/tests/templates/test_layers/test_gate_fabric.py
+++ b/tests/templates/test_layers/test_gate_fabric.py
@@ -21,6 +21,21 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+@pytest.mark.parametrize("include_pi", (True, False))
+def test_standard_validity(include_pi):
+    """Check the operation using the assert_valid function."""
+
+    layers = 2
+    qubits = 6
+    init_state = qml.math.array([1, 1, 0, 0, 0, 0])
+
+    weights = np.random.normal(0, 2 * np.pi, (layers, qubits // 2 - 1, 2))
+
+    op = qml.GateFabric(weights, wires=range(qubits), init_state=init_state, include_pi=include_pi)
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_layers/test_particle_conserving_u1.py b/tests/templates/test_layers/test_particle_conserving_u1.py
index 9a5915ca8..facc6c242 100644
--- a/tests/templates/test_layers/test_particle_conserving_u1.py
+++ b/tests/templates/test_layers/test_particle_conserving_u1.py
@@ -20,6 +20,16 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    weights = np.random.random(size=(1, 2, 2))
+    init_state = np.array([1, 1, 0])
+    op = qml.ParticleConservingU1(weights, wires=range(3), init_state=init_state)
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_layers/test_particle_conserving_u2.py b/tests/templates/test_layers/test_particle_conserving_u2.py
index 55ffd0a27..baa40438b 100644
--- a/tests/templates/test_layers/test_particle_conserving_u2.py
+++ b/tests/templates/test_layers/test_particle_conserving_u2.py
@@ -20,6 +20,18 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Run standard checks with the assert_valid function."""
+
+    layers = 2
+    qubits = 4
+
+    weights = np.random.normal(0, 2 * np.pi, (layers, 2 * qubits - 1))
+    init_state = np.array([1, 1, 0, 0])
+    op = qml.ParticleConservingU2(weights, wires=range(qubits), init_state=init_state)
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_layers/test_random.py b/tests/templates/test_layers/test_random.py
index 370141857..3241fb2c8 100644
--- a/tests/templates/test_layers/test_random.py
+++ b/tests/templates/test_layers/test_random.py
@@ -21,6 +21,14 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Run standard checks with the assert_valid function."""
+
+    weights = np.array([[0.1, -2.1, 1.4]])
+    op = qml.RandomLayers(weights, wires=(0, 1))
+    qml.ops.functions.assert_valid(op)
+
+
 def test_hyperparameters():
     """Test that the hyperparmaeters are set as expected."""
     weights = np.array([[0.1, -2.1, 1.4]])
diff --git a/tests/templates/test_layers/test_simplified_twodesign.py b/tests/templates/test_layers/test_simplified_twodesign.py
index 860d951f8..6e0ff55e6 100644
--- a/tests/templates/test_layers/test_simplified_twodesign.py
+++ b/tests/templates/test_layers/test_simplified_twodesign.py
@@ -21,6 +21,19 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Run standard checks with the assert_valid function."""
+
+    n_wires = 2
+    weight_shape = (2, 1, 2)
+
+    weights = np.random.random(size=weight_shape)
+    initial_layer = np.random.randn(n_wires)
+
+    op = qml.SimplifiedTwoDesign(initial_layer, weights, wires=range(n_wires))
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_layers/test_strongly_entangling.py b/tests/templates/test_layers/test_strongly_entangling.py
index e75a203de..4be9ef031 100644
--- a/tests/templates/test_layers/test_strongly_entangling.py
+++ b/tests/templates/test_layers/test_strongly_entangling.py
@@ -21,6 +21,19 @@ import pennylane as qml
 from pennylane import numpy as pnp
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    n_wires = 2
+    weight_shape = (1, 2, 3)
+
+    weights = np.random.random(size=weight_shape)
+
+    op = qml.StronglyEntanglingLayers(weights, wires=range(n_wires))
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_state_preparations/test_arbitrary_state_prep.py b/tests/templates/test_state_preparations/test_arbitrary_state_prep.py
index ba3a88ef3..f6042a632 100644
--- a/tests/templates/test_state_preparations/test_arbitrary_state_prep.py
+++ b/tests/templates/test_state_preparations/test_arbitrary_state_prep.py
@@ -59,6 +59,16 @@ class TestHelpers:
             assert expected_pauli_words[idx] == pauli_word
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    weights = np.array([0, 1], dtype=float)
+
+    op = qml.ArbitraryStatePreparation(weights, wires=[0])
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
@@ -155,7 +165,7 @@ class TestDecomposition:
 
     def test_custom_wire_labels(self, tol):
         """Test that template can deal with non-numeric, nonconsecutive wire labels."""
-        weights = np.random.random(size=(2**4 - 2))
+        weights = np.random.random(size=2**4 - 2)
 
         dev = qml.device("default.qubit", wires=3)
         dev2 = qml.device("default.qubit", wires=["z", "a", "k"])
@@ -197,7 +207,7 @@ class TestInputs:
     def test_id(self):
         """Tests that the id attribute can be set."""
         template = qml.ArbitraryStatePreparation(
-            np.random.random(size=(2**4 - 2)), wires=[0, 1, 2], id="a"
+            np.random.random(size=2**4 - 2), wires=[0, 1, 2], id="a"
         )
         assert template.id == "a"
 
diff --git a/tests/templates/test_state_preparations/test_basis_state_prep.py b/tests/templates/test_state_preparations/test_basis_state_prep.py
index 24b58e906..0afca7b03 100644
--- a/tests/templates/test_state_preparations/test_basis_state_prep.py
+++ b/tests/templates/test_state_preparations/test_basis_state_prep.py
@@ -20,6 +20,17 @@ import numpy as np
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    basis_state = [0, 1]
+    wires = [0, 1]
+
+    op = qml.BasisStatePreparation(basis_state, wires)
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_state_preparations/test_cosine_window.py b/tests/templates/test_state_preparations/test_cosine_window.py
index 40858853f..7fb713697 100644
--- a/tests/templates/test_state_preparations/test_cosine_window.py
+++ b/tests/templates/test_state_preparations/test_cosine_window.py
@@ -21,6 +21,14 @@ import pennylane as qml
 from pennylane.wires import WireError
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    op = qml.CosineWindow(wires=[0, 1])
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_state_preparations/test_mottonen_state_prep.py b/tests/templates/test_state_preparations/test_mottonen_state_prep.py
index e27977c10..57dfebffe 100644
--- a/tests/templates/test_state_preparations/test_mottonen_state_prep.py
+++ b/tests/templates/test_state_preparations/test_mottonen_state_prep.py
@@ -22,6 +22,17 @@ from pennylane import numpy as pnp
 from pennylane.templates.state_preparations.mottonen import gray_code, _get_alpha_y
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    state = np.array([1, 2j, 3, 4j, 5, 6j, 7, 8j])
+    state = state / np.linalg.norm(state)
+
+    op = qml.MottonenStatePreparation(state_vector=state, wires=range(3))
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestHelpers:
     """Tests the helper functions for classical pre-processsing."""
 
diff --git a/tests/templates/test_state_preparations/test_qutrit_basis_state_prep.py b/tests/templates/test_state_preparations/test_qutrit_basis_state_prep.py
index 049cb4208..966c98c2b 100644
--- a/tests/templates/test_state_preparations/test_qutrit_basis_state_prep.py
+++ b/tests/templates/test_state_preparations/test_qutrit_basis_state_prep.py
@@ -20,6 +20,16 @@ import numpy as np
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+
+    basis_state = [2, 1, 0, 2]
+    wires = [1, 2, 6, 8]
+    op = qml.QutritBasisStatePreparation(basis_state, wires)
+
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_subroutines/test_all_singles_doubles.py b/tests/templates/test_subroutines/test_all_singles_doubles.py
index a0e5413f5..69a24fa34 100644
--- a/tests/templates/test_subroutines/test_all_singles_doubles.py
+++ b/tests/templates/test_subroutines/test_all_singles_doubles.py
@@ -21,6 +21,18 @@ from pennylane import numpy as pnp
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Run standard tests of operation validity."""
+    op = qml.AllSinglesDoubles(
+        [1, 2],
+        wires=range(4),
+        hf_state=np.array([1, 1, 0, 0]),
+        singles=[[0, 1]],
+        doubles=[[0, 1, 2, 3]],
+    )
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_subroutines/test_approx_time_evolution.py b/tests/templates/test_subroutines/test_approx_time_evolution.py
index 93337c557..42587c537 100644
--- a/tests/templates/test_subroutines/test_approx_time_evolution.py
+++ b/tests/templates/test_subroutines/test_approx_time_evolution.py
@@ -20,6 +20,14 @@ from pennylane import numpy as pnp
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Run standard tests of operation validity."""
+    H = 2.0 * qml.PauliX(0) + 3.0 * qml.PauliY(0)
+    t = 0.1
+    op = qml.ApproxTimeEvolution(H, t, n=20)
+    qml.ops.functions.assert_valid(op)
+
+
 # pylint: disable=protected-access
 def test_flatten_unflatten():
     """Tests the _flatten and _unflatten methods."""
diff --git a/tests/templates/test_subroutines/test_aqft.py b/tests/templates/test_subroutines/test_aqft.py
index 1d27a4d7b..26219da94 100644
--- a/tests/templates/test_subroutines/test_aqft.py
+++ b/tests/templates/test_subroutines/test_aqft.py
@@ -20,6 +20,12 @@ import numpy as np
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    op = qml.AQFT(order=2, wires=(0, 1, 2))
+    qml.ops.functions.assert_valid(op)
+
+
 class TestAQFT:
     """Tests for the aqft operations"""
 
diff --git a/tests/templates/test_subroutines/test_arbitrary_unitary.py b/tests/templates/test_subroutines/test_arbitrary_unitary.py
index 28d1e9a72..b6f4bc1a3 100644
--- a/tests/templates/test_subroutines/test_arbitrary_unitary.py
+++ b/tests/templates/test_subroutines/test_arbitrary_unitary.py
@@ -25,6 +25,15 @@ from pennylane.templates.subroutines.arbitrary_unitary import (
     _n_k_gray_code,
 )
 
+
+def test_standard_validity():
+    """Run standard tests of operation validity."""
+    shape = (3,)
+    weights = np.arange(np.prod(shape), dtype=float).reshape(shape)
+    op = qml.ArbitraryUnitary(weights, wires=[0])
+    qml.ops.functions.assert_valid(op)
+
+
 # fmt: off
 PAULI_WORD_TEST_DATA = [
     (1, ["X", "Y", "Z"]),
diff --git a/tests/templates/test_subroutines/test_basis_rotation.py b/tests/templates/test_subroutines/test_basis_rotation.py
index f969e001b..20b334727 100644
--- a/tests/templates/test_subroutines/test_basis_rotation.py
+++ b/tests/templates/test_subroutines/test_basis_rotation.py
@@ -20,6 +20,19 @@ import numpy as np
 import pennylane as qml
 
 
+@pytest.mark.xfail  # to be fixed by shortcut story 49160
+def test_standard_validity():
+    """Run standard tests of operation validity."""
+    weights = np.array(
+        [
+            [-0.618452, -0.68369054 - 0.38740723j],
+            [-0.78582258, 0.53807284 + 0.30489424j],
+        ]
+    )
+    op = qml.BasisRotation(wires=range(2), unitary_matrix=weights)
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Test that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_subroutines/test_commuting_evolution.py b/tests/templates/test_subroutines/test_commuting_evolution.py
index e36ad81c7..137ed740c 100644
--- a/tests/templates/test_subroutines/test_commuting_evolution.py
+++ b/tests/templates/test_subroutines/test_commuting_evolution.py
@@ -21,6 +21,16 @@ import pennylane as qml
 from pennylane import numpy as np
 
 
+def test_standard_validity():
+    """Run standard tests of operation validity."""
+    H = 2.0 * qml.PauliX(0) @ qml.PauliY(1) + 3.0 * qml.PauliY(0) @ qml.PauliZ(1)
+    time = 0.5
+    frequencies = (2, 4)
+    shifts = (1, 0.5)
+    op = qml.CommutingEvolution(H, time, frequencies=frequencies, shifts=shifts)
+    qml.ops.functions.assert_valid(op)
+
+
 # pylint: disable=protected-access
 def test_flatten_unflatten():
     """Unit tests for the flatten and unflatten methods."""
@@ -34,8 +44,8 @@ def test_flatten_unflatten():
     assert hash(metadata)
 
     assert len(data) == 2
-    assert data[0] is H
-    assert data[1] == time
+    assert data[1] is H
+    assert data[0] == time
     assert metadata == (frequencies, shifts)
 
     new_op = type(op)._unflatten(*op._flatten())
diff --git a/tests/templates/test_subroutines/test_controlled_sequence.py b/tests/templates/test_subroutines/test_controlled_sequence.py
index c714fe610..4355d85ba 100644
--- a/tests/templates/test_subroutines/test_controlled_sequence.py
+++ b/tests/templates/test_subroutines/test_controlled_sequence.py
@@ -23,6 +23,13 @@ from pennylane.wires import Wires
 # pylint: disable=unidiomatic-typecheck, cell-var-from-loop
 
 
+@pytest.mark.xfail  # to be fixed by shortcut #49175
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    op = qml.ControlledSequence(qml.RX(0.25, wires=3), control=[0, 1, 2])
+    qml.ops.functions.assert_valid(op)
+
+
 class TestInitialization:
     def test_id(self):
         """Tests that the id attribute can be set."""
diff --git a/tests/templates/test_subroutines/test_double_excitation.py b/tests/templates/test_subroutines/test_double_excitation.py
index 39b7c3174..e05d8470a 100644
--- a/tests/templates/test_subroutines/test_double_excitation.py
+++ b/tests/templates/test_subroutines/test_double_excitation.py
@@ -20,6 +20,15 @@ from pennylane import numpy as pnp
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Run standard tests of operation validity."""
+    weight = 0.5
+    wires1 = qml.wires.Wires((0, 1))
+    wires2 = qml.wires.Wires((2, 3, 4))
+    op = qml.FermionicDoubleExcitation(weight, wires1=wires1, wires2=wires2)
+    qml.ops.functions.assert_valid(op)
+
+
 # pylint: disable=protected-access
 def test_flatten_unflatten():
     """Test the _flatten and _unflatten methods."""
diff --git a/tests/templates/test_subroutines/test_flip_sign.py b/tests/templates/test_subroutines/test_flip_sign.py
index b81701679..7c5c4e79a 100644
--- a/tests/templates/test_subroutines/test_flip_sign.py
+++ b/tests/templates/test_subroutines/test_flip_sign.py
@@ -19,10 +19,16 @@ from pennylane import numpy as np
 import pennylane as qml
 
 
+def test_standarad_checks():
+    """Run standard checks with the assert_valid function."""
+    op = qml.FlipSign([0, 1], wires=("a", "b"))
+    qml.ops.functions.assert_valid(op)
+
+
 def test_repr():
     """Test the repr for a flip sign operator."""
     op = qml.FlipSign([0, 1], wires=("a", "b"))
-    expected = "FlipSign([0, 1], wires=['a', 'b'])"
+    expected = "FlipSign((0, 1), wires=['a', 'b'])"
     assert repr(op) == expected
 
 
diff --git a/tests/templates/test_subroutines/test_hilbert_schmidt.py b/tests/templates/test_subroutines/test_hilbert_schmidt.py
index 9d3975f68..dc9ef4f7f 100644
--- a/tests/templates/test_subroutines/test_hilbert_schmidt.py
+++ b/tests/templates/test_subroutines/test_hilbert_schmidt.py
@@ -19,22 +19,26 @@ import pytest
 import pennylane as qml
 
 
+def global_v_circuit(params):
+    qml.RZ(params, wires=1)
+
+
 # pylint: disable=protected-access
+@pytest.mark.xfail  # to be fixed by shortcut story 49174
 @pytest.mark.parametrize("op_type", (qml.HilbertSchmidt, qml.LocalHilbertSchmidt))
-def test_flatten_unflatten(op_type):
+def test_flatten_unflatten_standard_checks(op_type):
     """Test the flatten and unflatten methods."""
     u_tape = qml.tape.QuantumScript([qml.Hadamard("a"), qml.Identity("b")])
 
-    def v_circuit(params):
-        qml.RZ(params, wires=1)
-
     v_wires = qml.wires.Wires((0, 1))
-    op = op_type([0.1], v_function=v_circuit, v_wires=v_wires, u_tape=u_tape)
+    op = op_type([0.1], v_function=global_v_circuit, v_wires=v_wires, u_tape=u_tape)
+    qml.ops.functions.assert_valid(op)
+
     data, metadata = op._flatten()
 
     assert data == (0.1,)
     assert metadata == (
-        ("v_function", v_circuit),
+        ("v_function", global_v_circuit),
         ("v_wires", v_wires),
         ("u_tape", u_tape),
     )
diff --git a/tests/templates/test_subroutines/test_permute.py b/tests/templates/test_subroutines/test_permute.py
index 08f240d82..6eefab228 100644
--- a/tests/templates/test_subroutines/test_permute.py
+++ b/tests/templates/test_subroutines/test_permute.py
@@ -20,6 +20,12 @@ import numpy as np
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    op = qml.Permute([0, 1, 2, 3], wires=(3, 2, 1, 0))
+    qml.ops.functions.assert_valid(op)
+
+
 class TestDecomposition:
     """Tests that the template defines the correct decomposition."""
 
diff --git a/tests/templates/test_subroutines/test_qdrift.py b/tests/templates/test_subroutines/test_qdrift.py
index 7a88a9fe9..4e7b5a808 100644
--- a/tests/templates/test_subroutines/test_qdrift.py
+++ b/tests/templates/test_subroutines/test_qdrift.py
@@ -53,6 +53,8 @@ class TestInitialization:
         h = qml.dot(coeffs, ops)
         op = qml.QDrift(h, time, n=n, seed=seed)
 
+        qml.ops.functions.assert_valid(op)
+
         assert op.wires == h.wires
         assert op.parameters == [time]
         assert op.data == (time,)
@@ -137,7 +139,7 @@ class TestInitialization:
             "n": n,
             "seed": seed,
             "base": hamiltonian,
-            "decomposition": decomp,
+            "decomposition": tuple(decomp),
         }
 
         new_op = type(op)._unflatten(data, metadata)  # pylint: disable=protected-access
@@ -192,8 +194,8 @@ class TestDecomposition:
         with qml.tape.QuantumTape() as tape:
             decomp = op.compute_decomposition(*op.parameters, **op.hyperparameters)
 
-        assert all(decomp == tape.operations)  # queue matches decomp with circuit ordering
-        assert all(decomp == expected_decomp)  # sample the same ops
+        assert decomp == tape.operations  # queue matches decomp with circuit ordering
+        assert decomp == list(expected_decomp)  # sample the same ops
 
 
 class TestIntegration:
diff --git a/tests/templates/test_subroutines/test_qft.py b/tests/templates/test_subroutines/test_qft.py
index 9e17377f2..3e587cb77 100644
--- a/tests/templates/test_subroutines/test_qft.py
+++ b/tests/templates/test_subroutines/test_qft.py
@@ -22,6 +22,12 @@ from gate_data import QFT
 import pennylane as qml
 
 
+def test_standard_validity():
+    """Check the operation using the assert_valid function."""
+    op = qml.QFT(wires=(0, 1, 2))
+    qml.ops.functions.assert_valid(op)
+
+
 class TestQFT:
     """Tests for the qft operations"""
 
diff --git a/tests/templates/test_subroutines/test_select.py b/tests/templates/test_subroutines/test_select.py
index db3c7b850..4e58187d6 100644
--- a/tests/templates/test_subroutines/test_select.py
+++ b/tests/templates/test_subroutines/test_select.py
@@ -22,6 +22,24 @@ from pennylane import numpy as pnp
 import pennylane as qml
 
 
+def test_standard_checks():
+    """Run standard validity tests."""
+    ops = [qml.PauliX(0), qml.PauliY(0)]
+    control = [1]
+
+    op = qml.Select(ops, control)
+    qml.ops.functions.assert_valid(op)
+
+
+def test_repr():
+    """Test the repr method."""
+    ops = [qml.PauliX(0), qml.PauliY(0)]
+    control = [1]
+
+    op = qml.Select(ops, control)
+    assert repr(op) == "Select(ops=(PauliX(wires=[0]), PauliY(wires=[0])), control=<Wires = [1]>)"
+
+
 class TestSelect:
     """Tests that the template defines the correct decomposition."""