Repository: PennyLaneAI/pennylane
Pull request #5772 (2024-05-30 19:12:01)

Make `Conditional` a `SymbolicOp` 
**Context:**

The `Conditional` class currently inherits from `Operator`, such that many attributes and methods are missing. 

**Description of the Change:**

By switching to inherit from `SymbolicOp`, the `Conditional` class  now includes some common default behaviour, such as deferring properties to the base class, copying the base class during a shallow copy, and updating the metadata of the base operator during queueing. 

In addition, other relevant representations have been added and tested, e.g.: `matrix`, `adjoint`, `diagonalizing_gates`, and `eigvals`.

As part of the work needed for this PR, new dispatches for `qml.Conditional` and `qml.MeasurementValue` have been added to the `qml.equal()` function.

**Benefits:**
Seamlessly inheritance of properties and basic methods  implemented by `SymbolicOp`, including differing of properties.

**Possible Drawbacks:**
Have to make sure that is not a breaking change as some attributes/methods of `Conditional` are changed.
Need to rethink approach for decomposition of `Conditional` operators.

[sc-61356]


github-actions[bot]: Hello. You may have forgotten to update the changelog!
Please edit [`doc/releases/changelog-dev.md`](/PennyLaneAI/pennylane/blob/master/doc/releases/changelog-dev.md) with:
* A one-to-two sentence description of the change. You may include a small working example for new features.
* A link back to this PR.
* Your name (or GitHub username) in the contributors section.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5772?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: Patch coverage is `84.21053%` with `6 lines` in your changes missing coverage. Please review.
> Project coverage is 99.65%. Comparing base [(`b797b1d`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/b797b1d3f3af6b8f134c12c8704b5ceb6ab55fd8?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`e876105`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/e876105d439d7f7f8500c2afad0e3ba501560ece?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 5 commits behind head on master.

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5772?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/ops/op\_math/condition.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5772?src=pr&el=tree&filepath=pennylane%2Fops%2Fop_math%2Fcondition.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL29wcy9vcF9tYXRoL2NvbmRpdGlvbi5weQ==) | 76.92% | [6 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5772?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |

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


```diff
@@            Coverage Diff             @@
##           master    #5772      +/-   ##
==========================================
- Coverage   99.67%   99.65%   -0.02%     
==========================================
  Files         414      414              
  Lines       39310    39191     -119     
==========================================
- Hits        39181    39056     -125     
- Misses        129      135       +6     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5772?dropdown=coverage&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).


Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 16d0483df..2e1c1b946 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -150,6 +150,12 @@
   `par_info`, `obs_sharing_wires`, and `obs_sharing_wires_id` are now public attributes.
   [(#5696)](https://github.com/PennyLaneAI/pennylane/pull/5696)
 
+* `qml.ops.Conditional` now inherits from `qml.ops.SymbolicOp`, thus it inherits several useful common functionalities. Other properties such as adjoint and diagonalizing gates have been added using the `base` properties.
+  [(##5772)](https://github.com/PennyLaneAI/pennylane/pull/5772)
+
+* New dispatches for `qml.ops.Conditional` and `qml.MeasurementValue` have been added to `qml.equal`.
+  [(##5772)](https://github.com/PennyLaneAI/pennylane/pull/5772)
+
 * The `qml.qchem.Molecule` object is now the central object used by all qchem functions.
   [(#5571)](https://github.com/PennyLaneAI/pennylane/pull/5571)
 
diff --git a/pennylane/devices/qubit/apply_operation.py b/pennylane/devices/qubit/apply_operation.py
index e5a3ed546..4d7bbbe80 100644
--- a/pennylane/devices/qubit/apply_operation.py
+++ b/pennylane/devices/qubit/apply_operation.py
@@ -264,7 +264,7 @@ def apply_conditional(
         return cond(
             op.meas_val.concretize(mid_measurements),
             lambda x: apply_operation(
-                op.then_op,
+                op.base,
                 x,
                 is_state_batched=is_state_batched,
                 debugger=debugger,
@@ -277,7 +277,7 @@ def apply_conditional(
         )
     if op.meas_val.concretize(mid_measurements):
         return apply_operation(
-            op.then_op,
+            op.base,
             state,
             is_state_batched=is_state_batched,
             debugger=debugger,
diff --git a/pennylane/drawer/drawable_layers.py b/pennylane/drawer/drawable_layers.py
index f57f8d592..a2d78e772 100644
--- a/pennylane/drawer/drawable_layers.py
+++ b/pennylane/drawer/drawable_layers.py
@@ -95,7 +95,7 @@ def _get_op_occupied_wires(op, wire_map, bit_map):
         return {mapped_wire}
 
     if isinstance(op, Conditional):
-        mapped_wires = [wire_map[wire] for wire in op.then_op.wires]
+        mapped_wires = [wire_map[wire] for wire in op.base.wires]
         min_wire = min(mapped_wires)
         max_wire = max(wire_map.values())
         return set(range(min_wire, max_wire + 1))
diff --git a/pennylane/drawer/tape_mpl.py b/pennylane/drawer/tape_mpl.py
index e113805d5..d73e4bcc6 100644
--- a/pennylane/drawer/tape_mpl.py
+++ b/pennylane/drawer/tape_mpl.py
@@ -167,7 +167,7 @@ def _(op: qml.ops.op_math.Conditional, drawer, layer, config) -> None:
     drawer.box_gate(
         layer,
         list(op.wires),
-        op.then_op.label(decimals=config.decimals),
+        op.base.label(decimals=config.decimals),
         box_options={"zorder": 4},
         text_options={"zorder": 5},
     )
diff --git a/pennylane/drawer/tape_text.py b/pennylane/drawer/tape_text.py
index 4599f5dd7..9cc4c0d9e 100644
--- a/pennylane/drawer/tape_text.py
+++ b/pennylane/drawer/tape_text.py
@@ -131,7 +131,7 @@ def _add_op(op, layer_str, config):
     """Updates ``layer_str`` with ``op`` operation."""
     if isinstance(op, qml.ops.Conditional):  # pylint: disable=no-member
         layer_str = _add_cond_grouping_symbols(op, layer_str, config)
-        return _add_op(op.then_op, layer_str, config)
+        return _add_op(op.base, layer_str, config)
 
     if isinstance(op, MidMeasureMP):
         return _add_mid_measure_op(op, layer_str, config)
diff --git a/pennylane/ops/functions/bind_new_parameters.py b/pennylane/ops/functions/bind_new_parameters.py
index 5ca4a1174..c80456ab5 100644
--- a/pennylane/ops/functions/bind_new_parameters.py
+++ b/pennylane/ops/functions/bind_new_parameters.py
@@ -254,7 +254,7 @@ def bind_new_parameters_tensor(op: Tensor, params: Sequence[TensorLike]):
 
 @bind_new_parameters.register
 def bind_new_parameters_conditional(op: qml.ops.Conditional, params: Sequence[TensorLike]):
-    then_op = bind_new_parameters(op.then_op, params)
+    then_op = bind_new_parameters(op.base, params)
     mv = copy.deepcopy(op.meas_val)
 
     return qml.ops.Conditional(mv, then_op)
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index 44cfd42fe..dd8fa58e3 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -30,6 +30,7 @@ from pennylane.operation import Observable, Operator, Tensor
 from pennylane.ops import (
     Adjoint,
     CompositeOp,
+    Conditional,
     Controlled,
     Exp,
     Hamiltonian,
@@ -440,6 +441,22 @@ def _equal_adjoint(op1: Adjoint, op2: Adjoint, **kwargs):
     return qml.equal(op1.base, op2.base, **kwargs)
 
 
+@_equal.register
+def _equal_conditional(op1: Conditional, op2: Conditional, **kwargs):
+    """Determine whether two Conditional objects are equal"""
+    # first line of top-level equal function already confirms both are Conditionaly - only need to compare bases and meas_val
+    return qml.equal(op1.base, op2.base, **kwargs) and qml.equal(
+        op1.meas_val, op2.meas_val, **kwargs
+    )
+
+
+@_equal.register
+# pylint: disable=unused-argument
+def _equal_measurement_value(op1: MeasurementValue, op2: MeasurementValue, **kwargs):
+    """Determine whether two MeasurementValue objects are equal"""
+    return op1.measurements == op2.measurements
+
+
 @_equal.register
 # pylint: disable=unused-argument
 def _equal_exp(op1: Exp, op2: Exp, **kwargs):
@@ -563,9 +580,8 @@ def _equal_measurements(
         )
 
     if op1.mv is not None and op2.mv is not None:
-        # qml.equal doesn't check if the MeasurementValues have the same processing functions
         if isinstance(op1.mv, MeasurementValue) and isinstance(op2.mv, MeasurementValue):
-            return op1.mv.measurements == op2.mv.measurements
+            return qml.equal(op1.mv, op2.mv)
 
         if isinstance(op1.mv, Iterable) and isinstance(op2.mv, Iterable):
             if len(op1.mv) == len(op2.mv):
diff --git a/pennylane/ops/op_math/condition.py b/pennylane/ops/op_math/condition.py
index 611b306ed..70cdf6fca 100644
--- a/pennylane/ops/op_math/condition.py
+++ b/pennylane/ops/op_math/condition.py
@@ -20,6 +20,7 @@ from typing import Type
 from pennylane import QueuingManager
 from pennylane.compiler import compiler
 from pennylane.operation import AnyWires, Operation, Operator
+from pennylane.ops.op_math.symbolicop import SymbolicOp
 from pennylane.tape import make_qscript
 
 
@@ -27,7 +28,7 @@ class ConditionalTransformError(ValueError):
     """Error for using qml.cond incorrectly"""
 
 
-class Conditional(Operation):
+class Conditional(SymbolicOp):
     """A Conditional Operation.
 
     Unless you are a Pennylane plugin developer, **you should NOT directly use this class**,
@@ -50,26 +51,52 @@ class Conditional(Operation):
     num_wires = AnyWires
 
     def __init__(self, expr, then_op: Type[Operation], id=None):
-        self.meas_val = expr
-        self.then_op = then_op
-        super().__init__(*then_op.data, wires=then_op.wires, id=id)
+        self.hyperparameters["meas_val"] = expr
+        self._name = f"Conditional({then_op.name})"
+        super().__init__(then_op, id=id)
 
     def label(self, decimals=None, base_label=None, cache=None):
-        return self.then_op.label(decimals=decimals, base_label=base_label, cache=cache)
+        return self.base.label(decimals=decimals, base_label=base_label, cache=cache)
+
+    @property
+    def meas_val(self):
+        "the measurement outcome value to consider from `expr` argument"
+        return self.hyperparameters["meas_val"]
 
     @property
     def num_params(self):
-        return self.then_op.num_params
+        return self.base.num_params
 
     @property
     def ndim_params(self):
-        return self.then_op.ndim_params
+        return self.base.ndim_params
 
     def map_wires(self, wire_map):
         meas_val = self.meas_val.map_wires(wire_map)
-        then_op = self.then_op.map_wires(wire_map)
+        then_op = self.base.map_wires(wire_map)
         return Conditional(meas_val, then_op=then_op)
 
+    def matrix(self, wire_order=None):
+        return self.base.matrix(wire_order=wire_order)
+
+    # pylint: disable=arguments-renamed, invalid-overridden-method
+    @property
+    def has_diagonalizing_gates(self):
+        return self.base.has_diagonalizing_gates
+
+    def diagonalizing_gates(self):
+        return self.base.diagonalizing_gates()
+
+    def eigvals(self):
+        return self.base.eigvals()
+
+    @property
+    def has_adjoint(self):
+        return self.base.has_adjoint
+
+    def adjoint(self):
+        return Conditional(self.meas_val, self.base.adjoint())
+
 
 def cond(condition, true_fn, false_fn=None, elifs=()):
     """Quantum-compatible if-else conditionals --- condition quantum operations
diff --git a/pennylane/transforms/defer_measurements.py b/pennylane/transforms/defer_measurements.py
index 69de5f88a..bf1bc5f56 100644
--- a/pennylane/transforms/defer_measurements.py
+++ b/pennylane/transforms/defer_measurements.py
@@ -406,11 +406,11 @@ def _add_control_gate(op, control_wires, reduce_postselected):
         if value:
             # Empty sampling branches can occur when using _postselected_items
             if branch == ():
-                new_ops.append(op.then_op)
+                new_ops.append(op.base)
                 continue
             qscript = qml.tape.make_qscript(
                 ctrl(
-                    lambda: qml.apply(op.then_op),  # pylint: disable=cell-var-from-loop
+                    lambda: qml.apply(op.base),  # pylint: disable=cell-var-from-loop
                     control=Wires(control),
                     control_values=branch,
                 )


Test patch:
diff --git a/tests/ops/functions/test_bind_new_parameters.py b/tests/ops/functions/test_bind_new_parameters.py
index 19f9e7c1e..f12899f60 100644
--- a/tests/ops/functions/test_bind_new_parameters.py
+++ b/tests/ops/functions/test_bind_new_parameters.py
@@ -498,7 +498,7 @@ def test_conditional_ops(op, new_params, expected_op):
     new_op = bind_new_parameters(cond_op, new_params)
 
     assert isinstance(new_op, qml.ops.Conditional)
-    assert new_op.then_op == expected_op
+    assert new_op.base == expected_op
     assert new_op.meas_val.measurements == [mp0]
 
 
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index 0cc8e6337..be7d522a5 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -27,6 +27,7 @@ import pennylane as qml
 from pennylane import numpy as npp
 from pennylane.measurements import ExpectationMP
 from pennylane.measurements.probs import ProbabilityMP
+from pennylane.ops import Conditional
 from pennylane.ops.functions.equal import _equal, assert_equal
 from pennylane.ops.op_math import Controlled, SymbolicOp
 from pennylane.templates.subroutines import ControlledSequence
@@ -1286,6 +1287,35 @@ class TestMeasurementsEqual:
             qml.measurements.MidMeasureMP(wires=qml.wires.Wires([0, 1]), reset=True, id="test_id"),
         )
 
+    def test_equal_measurement_value(self):
+        """Test that MeasurementValue's are equal when their measurements are the same."""
+        mv1 = qml.measure(0)
+        mv2 = qml.measure(0)
+        # qml.equal of MidMeasureMP checks the id
+        mv2.measurements[0].id = mv1.measurements[0].id
+
+        assert qml.equal(mv1, mv1)
+        assert qml.equal(mv1, mv2)
+
+    def test_different_measurement_value(self):
+        """Test that MeasurementValue's are different when their measurements are not the same."""
+        mv1 = qml.measure(0)
+        mv2 = qml.measure(1)
+        assert not qml.equal(mv1, mv2)
+
+    def test_composed_measurement_value(self):
+        """Test that composition of MeasurementValue's are checked correctly."""
+        mv1 = qml.measure(0)
+        mv2 = qml.measure(1)
+        mv3 = qml.measure(0)
+        # qml.equal of MidMeasureMP checks the id
+        mv3.measurements[0].id = mv1.measurements[0].id
+
+        assert qml.equal(mv1 * mv2, mv2 * mv1)
+        assert qml.equal(mv1 + mv2, mv3 + mv2)
+        # NOTE: we are deliberatily just checking for measurements and not for processing_fn, such that two MeasurementValue objects composed from the same operators will be qml.equal
+        assert qml.equal(3 * mv1 + 1, 4 * mv3 + 2)
+
     @pytest.mark.parametrize("mp_fn", [qml.probs, qml.sample, qml.counts])
     def test_mv_list_as_op(self, mp_fn):
         """Test that MeasurementProcesses that measure a list of MeasurementValues check for equality
@@ -1648,6 +1678,73 @@ class TestSymbolicOpComparison:
         assert qml.equal(op1, op2, check_interface=False, check_trainability=False)
         assert not qml.equal(op1, op2, check_interface=False, check_trainability=True)
 
+    @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
+    def test_conditional_base_operator_wire_comparison(self, wire1, wire2, res):
+        """Test that equal compares operator wires for Conditional operators"""
+        m = qml.measure(0)
+        base1 = qml.PauliX(wire1)
+        base2 = qml.PauliX(wire2)
+        op1 = Conditional(m, base1)
+        op2 = Conditional(m, base2)
+        assert qml.equal(op1, op2) == res
+
+    @pytest.mark.parametrize(("wire1", "wire2", "res"), WIRES)
+    def test_conditional_measurement_value_wire_comparison(self, wire1, wire2, res):
+        """Test that equal compares operator wires for Conditional operators"""
+        m1 = qml.measure(wire1)
+        m2 = qml.measure(wire2)
+        if wire1 == wire2:
+            # qml.equal checks id for MidMeasureMP, but here we only care about them acting on the same wire
+            m2.measurements[0].id = m1.measurements[0].id
+        base = qml.PauliX(wire2)
+        op1 = Conditional(m1, base)
+        op2 = Conditional(m2, base)
+        assert qml.equal(op1, op2) == res
+
+    @pytest.mark.parametrize(("base1", "base2", "res"), BASES)
+    def test_conditional_base_operator_comparison(self, base1, base2, res):
+        """Test that equal compares base operators for Conditional operators"""
+        m = qml.measure(0)
+        op1 = Conditional(m, base1)
+        op2 = Conditional(m, base2)
+        assert qml.equal(op1, op2) == res
+
+    def test_conditional_comparison_with_tolerance(self):
+        """Test that equal compares the parameters within a provided tolerance of the Conditional class."""
+        m = qml.measure(0)
+        base1 = qml.RX(1.2, wires=0)
+        base2 = qml.RX(1.2 + 1e-4, wires=0)
+        op1 = Conditional(m, base1)
+        op2 = Conditional(m, base2)
+
+        assert qml.equal(op1, op2, atol=1e-3, rtol=0)
+        assert not qml.equal(op1, op2, atol=1e-5, rtol=0)
+        assert qml.equal(op1, op2, atol=0, rtol=1e-3)
+        assert not qml.equal(op1, op2, atol=0, rtol=1e-5)
+
+    def test_conditional_base_op_comparison_with_interface(self):
+        """Test that equal compares the parameters within a provided interface of the base operator of Conditional class."""
+        m = qml.measure(0)
+        base1 = qml.RX(1.2, wires=0)
+        base2 = qml.RX(npp.array(1.2), wires=0)
+        op1 = Conditional(m, base1)
+        op2 = Conditional(m, base2)
+
+        assert qml.equal(op1, op2, check_interface=False, check_trainability=False)
+        assert not qml.equal(op1, op2, check_interface=True, check_trainability=False)
+
+    def test_conditional_base_op_comparison_with_trainability(self):
+        """Test that equal compares the parameters within a provided trainability of the base operator of Conditional class."""
+
+        m = qml.measure(0)
+        base1 = qml.RX(npp.array(1.2, requires_grad=False), wires=0)
+        base2 = qml.RX(npp.array(1.2, requires_grad=True), wires=0)
+        op1 = Conditional(m, base1)
+        op2 = Conditional(m, base2)
+
+        assert qml.equal(op1, op2, check_interface=False, check_trainability=False)
+        assert not qml.equal(op1, op2, check_interface=False, check_trainability=True)
+
     @pytest.mark.parametrize("bases_bases_match", BASES)
     @pytest.mark.parametrize("params_params_match", PARAMS)
     def test_pow_comparison(self, bases_bases_match, params_params_match):
diff --git a/tests/ops/op_math/test_condition.py b/tests/ops/op_math/test_condition.py
index 275e722ed..720989ae0 100644
--- a/tests/ops/op_math/test_condition.py
+++ b/tests/ops/op_math/test_condition.py
@@ -23,10 +23,12 @@ are located in the:
 files.
 """
 
+import numpy as np
 import pytest
 
 import pennylane as qml
-from pennylane.ops.op_math.condition import ConditionalTransformError
+from pennylane.operation import Operator
+from pennylane.ops.op_math.condition import Conditional, ConditionalTransformError
 
 terminal_meas = [
     qml.probs(wires=[1, 0]),
@@ -41,7 +43,7 @@ terminal_meas = [
 
 @pytest.mark.parametrize("terminal_measurement", terminal_meas)
 class TestCond:
-    """Tests that verify that the cond transform works as expect."""
+    """Tests that the cond transform works as expect."""
 
     def test_cond_ops(self, terminal_measurement):
         """Test that qml.cond creates conditional operations as expected."""
@@ -65,17 +67,17 @@ class TestCond:
         assert ops[0].return_type == qml.measurements.MidMeasure
 
         assert isinstance(ops[1], qml.ops.Conditional)
-        assert isinstance(ops[1].then_op, qml.PauliX)
-        assert ops[1].then_op.wires == target_wire
+        assert isinstance(ops[1].base, qml.PauliX)
+        assert ops[1].base.wires == target_wire
 
         assert isinstance(ops[2], qml.ops.Conditional)
-        assert isinstance(ops[2].then_op, qml.RY)
-        assert ops[2].then_op.wires == target_wire
-        assert ops[2].then_op.data == (r,)
+        assert isinstance(ops[2].base, qml.RY)
+        assert ops[2].base.wires == target_wire
+        assert ops[2].base.data == (r,)
 
         assert isinstance(ops[3], qml.ops.Conditional)
-        assert isinstance(ops[3].then_op, qml.PauliZ)
-        assert ops[3].then_op.wires == target_wire
+        assert isinstance(ops[3].base, qml.PauliZ)
+        assert ops[3].base.wires == target_wire
 
         assert len(tape.measurements) == 1
         assert tape.measurements[0] is terminal_measurement
@@ -130,21 +132,21 @@ class TestCond:
         assert ops[0].return_type == qml.measurements.MidMeasure
 
         assert isinstance(ops[1], qml.ops.Conditional)
-        assert isinstance(ops[1].then_op, qml.PauliX)
-        assert ops[1].then_op.wires == target_wire
+        assert isinstance(ops[1].base, qml.PauliX)
+        assert ops[1].base.wires == target_wire
 
         assert isinstance(ops[2], qml.ops.Conditional)
-        assert isinstance(ops[2].then_op, qml.RY)
-        assert ops[2].then_op.wires == target_wire
-        assert ops[2].then_op.data == (r,)
+        assert isinstance(ops[2].base, qml.RY)
+        assert ops[2].base.wires == target_wire
+        assert ops[2].base.data == (r,)
 
         assert isinstance(ops[3], qml.ops.Conditional)
-        assert isinstance(ops[3].then_op, qml.PauliZ)
-        assert ops[3].then_op.wires == target_wire
+        assert isinstance(ops[3].base, qml.PauliZ)
+        assert ops[3].base.wires == target_wire
 
         assert isinstance(ops[4], qml.ops.Conditional)
-        assert isinstance(ops[4].then_op, qml.PauliY)
-        assert ops[4].then_op.wires == target_wire
+        assert isinstance(ops[4].base, qml.PauliY)
+        assert ops[4].base.wires == target_wire
 
         # Check that: the measurement value is the same for true_fn conditional
         # ops
@@ -214,7 +216,7 @@ class TestAdditionalCond:
         meas, cond_op = q.queue
         mapped_cond = cond_op.map_wires({0: "a", 1: "b"})
         assert mapped_cond.meas_val.measurements == [meas.map_wires({0: "a"})]
-        assert mapped_cond.then_op == qml.PauliX("b")
+        assert mapped_cond.base == qml.PauliX("b")
 
     @pytest.mark.parametrize(
         "op", [qml.RX(0.123, 0), qml.RX([0.123, 4.56], 0), qml.Rot(1.23, 4.56, 7.89, 0)]
@@ -265,14 +267,14 @@ class TestOtherTransforms:
         assert ops[0].return_type == qml.measurements.MidMeasure
 
         assert isinstance(ops[1], qml.ops.Conditional)
-        assert isinstance(ops[1].then_op, qml.ops.op_math.Adjoint)
-        assert isinstance(ops[1].then_op.base, qml.RX)
-        assert ops[1].then_op.wires == target_wire
+        assert isinstance(ops[1].base, qml.ops.op_math.Adjoint)
+        assert isinstance(ops[1].base.base, qml.RX)
+        assert ops[1].base.wires == target_wire
 
         assert isinstance(ops[2], qml.ops.Conditional)
-        assert isinstance(ops[2].then_op, qml.RX)
-        assert ops[2].then_op.data == (r,)
-        assert ops[2].then_op.wires == target_wire
+        assert isinstance(ops[2].base, qml.RX)
+        assert ops[2].base.data == (r,)
+        assert ops[2].base.wires == target_wire
 
         assert len(tape.measurements) == 1
         assert tape.measurements[0] is terminal_measurement
@@ -294,12 +296,12 @@ class TestOtherTransforms:
         assert ops[0].return_type == qml.measurements.MidMeasure
 
         assert isinstance(ops[1], qml.ops.Conditional)
-        assert isinstance(ops[1].then_op, qml.ops.op_math.Controlled)
-        assert qml.equal(ops[1].then_op.base, qml.RX(r, wires=2))
+        assert isinstance(ops[1].base, qml.ops.op_math.Controlled)
+        assert qml.equal(ops[1].base.base, qml.RX(r, wires=2))
 
         assert isinstance(ops[2], qml.ops.Conditional)
-        assert isinstance(ops[2].then_op, qml.ops.op_math.Controlled)
-        assert qml.equal(ops[2].then_op.base, qml.RY(r, wires=2))
+        assert isinstance(ops[2].base, qml.ops.op_math.Controlled)
+        assert qml.equal(ops[2].base.base, qml.RY(r, wires=2))
 
         assert len(tape.measurements) == 1
         assert tape.measurements[0] is terminal_measurement
@@ -322,11 +324,11 @@ class TestOtherTransforms:
 
         assert isinstance(ops[1], qml.ops.op_math.Controlled)
         assert isinstance(ops[1].base, qml.ops.Conditional)
-        assert qml.equal(ops[1].base.then_op, qml.RX(1.234, wires=0))
+        assert qml.equal(ops[1].base.base, qml.RX(1.234, wires=0))
 
         assert isinstance(ops[2], qml.ops.op_math.Controlled)
         assert isinstance(ops[2].base, qml.ops.Conditional)
-        assert qml.equal(ops[2].base.then_op, qml.RY(r, wires=0))
+        assert qml.equal(ops[2].base.base, qml.RY(r, wires=0))
 
         assert len(tape.measurements) == 1
         assert tape.measurements[0] is terminal_measurement
@@ -362,3 +364,134 @@ class TestOtherTransforms:
         assert tape[1] == mp
         assert isinstance(tape[2], qml.ops.Conditional)
         assert tape[3] == terminal_measurement
+
+
+class TestProperties:
+    """Test Conditional properties."""
+
+    BASE_OP = [qml.RX(1.23, 0), qml.Rot(1.2, 2.3, 3.4, 0), qml.QubitUnitary([[0, 1], [1, 0]], 0)]
+
+    def test_data(self):
+        """Test base data can be get and set through Conditional class."""
+        x = np.array(1.234)
+        m = qml.measure("a")
+        base = qml.RX(x, wires="a")
+        cond_op = Conditional(m, base)
+
+        assert cond_op.data == (x,)
+
+        # update parameters through Conditional
+        x_new = np.array(2.3456)
+        cond_op.data = (x_new,)
+        assert base.data == (x_new,)
+        assert cond_op.data == (x_new,)
+
+        # update base data updates Conditional data
+        x_new2 = np.array(3.456)
+        base.data = (x_new2,)
+        assert cond_op.data == (x_new2,)
+
+    @pytest.mark.parametrize("value", (True, False))
+    def test_has_matrix(self, value):
+        """Test that Conditional defers has_matrix to base operator."""
+
+        # pylint:disable=too-few-public-methods
+        class DummyOp(Operator):
+            num_wires = 1
+            has_matrix = value
+
+        m = qml.measure(0)
+        cond_op = Conditional(m, DummyOp(1))
+
+        assert cond_op.has_matrix is value
+
+    @pytest.mark.parametrize("value", (True, False))
+    def test_has_adjoint(self, value):
+        """Test that Conditional defers has_adjoint to base operator."""
+
+        # pylint:disable=too-few-public-methods
+        class DummyOp(Operator):
+            num_wires = 1
+            has_adjoint = value
+
+        m = qml.measure(0)
+        cond_op = Conditional(m, DummyOp(1))
+
+        assert cond_op.has_adjoint is value
+
+    @pytest.mark.parametrize("value", (True, False))
+    def test_has_diagonalizing_gates(self, value):
+        """Test that Conditional defers has_adjoint to base operator."""
+
+        # pylint:disable=too-few-public-methods
+        class DummyOp(Operator):
+            num_wires = 1
+            has_diagonalizing_gates = value
+
+        m = qml.measure(0)
+        cond_op = Conditional(m, DummyOp(1))
+
+        assert cond_op.has_diagonalizing_gates is value
+
+    @pytest.mark.parametrize("base", BASE_OP)
+    def test_ndim_params(self, base):
+        """Test that Conditional defers to base ndim_params."""
+        m = qml.measure(0)
+        op = Conditional(m, base)
+        assert op.ndim_params == base.ndim_params
+
+    @pytest.mark.parametrize("base", BASE_OP)
+    def test_num_params(self, base):
+        """Test that Conditional defers to base num_params."""
+        m = qml.measure(0)
+        op = Conditional(m, base)
+        assert op.num_params == base.num_params
+
+
+class TestMethods:
+    """Test Conditional methods."""
+
+    def test_diagonalizing_gates(self):
+        """Test that Conditional defers to base diagonalizing_gates."""
+        base = qml.PauliX(0)
+        m = qml.measure(0)
+        op = Conditional(m, base)
+
+        assert op.diagonalizing_gates() == base.diagonalizing_gates()
+
+    def test_eigvals(self):
+        """Test that Conditional defers to base eigvals."""
+        base = qml.PauliX(0)
+        m = qml.measure(0)
+        op = Conditional(m, base)
+
+        assert qml.math.allclose(op.eigvals(), base.eigvals())
+
+    def test_matrix_value(self):
+        """Test that Conditional defers to base matrix."""
+        base = qml.PauliX(0)
+        m = qml.measure(0)
+        op = Conditional(m, base)
+        assert qml.math.allclose(op.matrix(), op.base.matrix())
+
+    def test_matrix_wire_oder(self):
+        """Test that `wire_order` in `matrix` method behaves as expected."""
+        m = qml.measure(0)
+        base = qml.RX(-4.432, wires=1)
+        op = Conditional(m, base)
+
+        method_order = op.matrix(wire_order=(1, 0))
+        function_order = qml.math.expand_matrix(op.matrix(), op.wires, (1, 0))
+
+        assert qml.math.allclose(method_order, function_order)
+
+    def test_adjoint(self):
+        """Test adjoint method for Conditional."""
+        base = qml.RX(np.pi / 2, 0)
+        m = qml.measure(0)
+        op = Conditional(m, base)
+        adj_op = op.adjoint()
+
+        assert isinstance(adj_op, Conditional)
+        assert adj_op.meas_val == op.meas_val
+        assert adj_op.base == base.adjoint()
diff --git a/tests/tape/test_tape.py b/tests/tape/test_tape.py
index c7805c396..ab832a55e 100644
--- a/tests/tape/test_tape.py
+++ b/tests/tape/test_tape.py
@@ -297,17 +297,17 @@ class TestConstruction:
         assert tape.circuit[0].return_type == qml.measurements.MidMeasure
 
         assert isinstance(tape.circuit[1], qml.ops.Conditional)
-        assert isinstance(tape.circuit[1].then_op, qml.PauliX)
-        assert tape.circuit[1].then_op.wires == target_wire
+        assert isinstance(tape.circuit[1].base, qml.PauliX)
+        assert tape.circuit[1].base.wires == target_wire
 
         assert isinstance(tape.circuit[2], qml.ops.Conditional)
-        assert isinstance(tape.circuit[2].then_op, qml.RY)
-        assert tape.circuit[2].then_op.wires == target_wire
-        assert tape.circuit[2].then_op.data == (r,)
+        assert isinstance(tape.circuit[2].base, qml.RY)
+        assert tape.circuit[2].base.wires == target_wire
+        assert tape.circuit[2].base.data == (r,)
 
         assert isinstance(tape.circuit[3], qml.ops.Conditional)
-        assert isinstance(tape.circuit[3].then_op, qml.PauliZ)
-        assert tape.circuit[3].then_op.wires == target_wire
+        assert isinstance(tape.circuit[3].base, qml.PauliZ)
+        assert tape.circuit[3].base.wires == target_wire
 
         assert tape.circuit[4] is terminal_measurement
 
diff --git a/tests/test_device.py b/tests/test_device.py
index 46845df78..a9337da88 100644
--- a/tests/test_device.py
+++ b/tests/test_device.py
@@ -587,7 +587,7 @@ class TestInternalFunctions:  # pylint:disable=too-many-public-methods
         tape = qml.tape.QuantumScript.from_queue(q)
         # Raises an error for device that doesn't support conditional
         # operations natively
-        with pytest.raises(DeviceError, match="Gate Conditional not supported on device"):
+        with pytest.raises(DeviceError, match="Gate Conditional\\(RY\\) not supported on device"):
             dev.check_validity(tape.operations, tape.observables)
 
     @pytest.mark.parametrize(