Repository: PennyLaneAI/pennylane
Pull request #5674 (2024-05-09 05:30:52)

Custom boolean functions for supporting noise models
**Context:** We aim to have a custom boolean function that could serve as a building block for creating noise models.

**Description of the Change:** 
1. Generalizes the existing `BooleanFn` to work with arbitrary `args` and `kwargs`.
2. Adds `NoiseConditionals` as a class that helps build `BooleanFn` required for creating noise models.
3. Adds methods like `wire/op_eq/in` to enhance the building experience for the above `BooleanFn`.  

**Benefits:**

**Possible Drawbacks:**

**Related GitHub Issues:** [sc-63080]


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/5674?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 `9.37500%` with `58 lines` in your changes are missing coverage. Please review.
> Project coverage is 99.53%. Comparing base [(`9f7e8ba`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/9f7e8ba8f41580fc3efe88bf88f0cc05c04a66ab?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`4b6c6b4`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?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/noise/conditionals.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?src=pr&el=tree&filepath=pennylane%2Fnoise%2Fconditionals.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL25vaXNlL2NvbmRpdGlvbmFscy5weQ==) | 0.00% | [54 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/noise/\_\_init\_\_.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?src=pr&el=tree&filepath=pennylane%2Fnoise%2F__init__.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL25vaXNlL19faW5pdF9fLnB5) | 0.00% | [3 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/boolean\_fn.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?src=pr&el=tree&filepath=pennylane%2Fboolean_fn.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL2Jvb2xlYW5fZm4ucHk=) | 85.71% | [1 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?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    #5674      +/-   ##
==========================================
- Coverage   99.68%   99.53%   -0.16%     
==========================================
  Files         412      414       +2     
  Lines       38645    38413     -232     
==========================================
- Hits        38524    38233     -291     
- Misses        121      180      +59     
```



</details>

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

trbromley: Thanks @obliviateandsurrender! Would you mind linking the corresponding SC story?
codecov-commenter: ## [Codecov](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/5674?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.67%. Comparing base [(`fdca352`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/fdca35219255946ca8dd5dcd8a15d9fedbc5d034?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`dd45dcd`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/dd45dcd03c420d77bc717fc7e63c119e12f711b8?dropdown=coverage&el=desc&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    #5674      +/-   ##
==========================================
- Coverage   99.68%   99.67%   -0.01%     
==========================================
  Files         416      418       +2     
  Lines       39053    38903     -150     
==========================================
- Hits        38929    38778     -151     
- Misses        124      125       +1     
```



</details>

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

obliviateandsurrender: > The only use of the bitwise and condition properties so far is to determine what the repr of the BooleanFunction is going to be.

Not really. I do use them for that, but that is not the intended purpose for them. These properties check the existence of underlying respective attributes that store expressions that BooleanFn uses for evaluation. For the `repr,` I do have my implementation based on your second suggestion. 

> Regarding OpEq, it doesn't seem to me that we need it? 

Yes, it is a special case of `OpIn`, and similar to us having similar conditionals for wires - `WiresEq` and `WiresIn`. This conditional checks for equivalence based on `type` and not `attributes` (like parameters and wires). You may combine it with a wire conditional to include checking `wires` as well as `wires_eq(0) & op_eq(qml.RX)`. Otherwise, for actually checking attributes, they can rely on using`qml.equal` as I note above in one of the comments - 
```
cond_equal = qml.BooleanFn(lambda ops, **kwargs: (lambda op: qml.equal(op, ops, **kwargs)))
```


Patch:
diff --git a/doc/code/qml_noise.rst b/doc/code/qml_noise.rst
new file mode 100644
index 000000000..e07cec2ad
--- /dev/null
+++ b/doc/code/qml_noise.rst
@@ -0,0 +1,187 @@
+qml.noise
+=========
+
+This module contains the functionality for building and manipulating insertion-based noise models,
+where noisy gates and channels are inserted based on the target operations.
+
+Overview
+--------
+
+Insertion-based noise models in PennyLane are defined via a mapping from conditionals, specified
+as :class:`~.BooleanFn` objects, to :ref:`quantum function <intro_vcirc_qfunc>`-like callables
+that contain the noisy operations to be applied, but without any return statements. Additional
+noise-related metadata can also be supplied to construct a noise model.
+
+::
+
+    NoiseModel: ({Conditional --> Callables},  metadata)
+
+Each ``Conditional`` evaluates the gate operations in the quantum circuit based on some
+condition of its attributes (e.g., type, parameters, wires, etc.) and use the corresponding
+``Callable`` to apply the noise operations, using the user-provided metadata (e.g., hardware
+topologies or relaxation times), whenever the condition results true.
+
+Boolean functions
+^^^^^^^^^^^^^^^^^
+
+Each :class:`~.BooleanFn` in the noise model is evaluated on the operations of a given
+quantum circuit. One can construct standard Boolean functions using the following helpers:
+
+.. currentmodule:: pennylane.noise
+
+.. autosummary::
+    :toctree: api
+
+    ~op_eq
+    ~op_in
+    ~wires_eq
+    ~wires_in
+
+For example, a Boolean function that checks of an operation is on wire ``0`` can be created
+like so:
+
+>>> fn = wires_eq(0)
+>>> op1, op2 = qml.PauliX(0), qml.PauliX(1)
+>>> fn(op1)
+False
+>>> fn(op2)
+True
+
+Arbitrary Boolean functions can also be defined by wrapping the functional form
+of custom conditions with the following decorator:
+
+.. currentmodule:: pennylane
+
+.. autosummary::
+    :toctree: api
+ 
+    ~BooleanFn
+
+For example, a Boolean function can be created to identify an :class:`~.RX` gate
+with a maximum parameter value:
+
+.. code-block:: python
+
+    @qml.BooleanFn
+    def rx_condition(op, **metadata):
+        return isinstance(op, qml.RX) and op.parameters[0] < 1.0
+
+Boolean functions can be combined using standard bit-wise operators, such as
+``&``, ``|``, ``^``, or ``~``. The result will be another Boolean function. It
+is important to note that as Python will evaluate the expression in the order
+of their combination, i.e., left to right, the order of composition could matter,
+even though bitwise operations are symmetric by definition.
+
+Noisy quantum functions
+^^^^^^^^^^^^^^^^^^^^^^^
+
+If a Boolean function evaluates to ``True`` on a given operation in the quantum circuit,
+the corresponding quantum function is evaluated that inserts the noise directly after
+the operation. The quantum function should have signature ``fn(op, **metadata)``,
+allowing for dependency on both the preceding operation and metadata specified in
+the noise model. For example, the following noise model adds an over-rotation
+to the ``RX`` gate:
+
+.. code-block:: python
+
+    def noisy_rx(op, **metadata):
+        qml.RX(op.parameters[0] * 0.05, op.wires)
+
+    noise_model = qml.NoiseModel({rx_condition: noisy_rx})
+
+A common use case is to have a single-operation :ref:`noise channel <intro_ref_ops_channels>`
+whose wires are the same as the preceding operation. This can be constructed using:
+
+.. currentmodule:: pennylane.noise
+
+.. autosummary::
+    :toctree: api
+
+    ~partial_wires
+
+For example, a constant-valued over-rotation can be created using:
+
+>>> rx_constant = qml.noise.partial_wires(qml.RX(0.1, wires=[0]))
+>>> rx_constant(2)
+RX(0.1, 2)
+>>> qml.NoiseModel({rx_condition: rx_constant})
+NoiseModel({
+    BooleanFn(rx_condition): RX(phi=0.1)
+})
+
+Example noise model
+^^^^^^^^^^^^^^^^^^^
+
+The following example shows how to set up an artificial noise model in PennyLane:
+
+.. code-block:: python
+
+  # Set up the conditions
+  c0 = qml.noise.op_eq(qml.PauliX) | qml.noise.op_eq(qml.PauliY)
+  c1 = qml.noise.op_eq(qml.Hadamard) & qml.noise.wires_in([0, 1])
+  c2 = qml.noise.op_eq(qml.RX)
+
+  @qml.BooleanFn
+  def c3(op, **metadata):
+      return isinstance(op, qml.RY) and op.parameters[0] >= 0.5
+
+  # Set up noisy ops
+  n0 = qml.noise.partial_wires(qml.AmplitudeDamping, 0.4)
+
+  def n1(op, **metadata):
+      ThermalRelaxationError(0.4, metadata["t1"], 0.2, 0.6, op.wires)
+
+  def n2(op, **metadata):
+      qml.RX(op.parameters[0] * 0.05, op.wires)
+
+  n3 = qml.noise.partial_wires(qml.PhaseDamping, 0.9)
+  
+  # Set up noise model
+  noise_model = qml.NoiseModel({c0: n0, c1: n1, c2: n2}, t1=0.04)
+  noise_model += {c3: n3}  # One-at-a-time construction
+
+>>> noise_model
+NoiseModel({
+    OpEq(PauliX) | OpEq(PauliY): AmplitudeDamping(gamma=0.4)
+    OpEq(Hadamard) & WiresIn([0, 1]): n1
+    OpEq(RX): n2
+    BooleanFn(c3): PhaseDamping(gamma=0.9)
+}, t1 = 0.04)
+
+API overview
+^^^^^^^^^^^^
+
+The following are the ``BooleanFn`` objects created by calling the helper functions
+above, such as :func:`~.op_eq`. These objects do not need to be instantiated directly
+
+.. currentmodule:: pennylane.noise.conditionals
+
+.. autosummary::
+    :toctree: api
+
+    ~OpEq
+    ~OpIn
+    ~WiresEq
+    ~WiresIn
+
+Bitwise operations like `And` and `Or` are represented with the following classes in the
+:mod:`boolean_fn` module:
+
+.. currentmodule:: pennylane.boolean_fn
+
+.. autosummary::
+    :toctree: api
+
+    ~And
+    ~Or
+    ~Xor
+    ~Not
+
+Class Inheritence Diagram
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. inheritance-diagram:: pennylane.noise.conditionals
+    :parts: 1
+
+.. inheritance-diagram:: pennylane.boolean_fn
+    :parts: 1
diff --git a/doc/index.rst b/doc/index.rst
index 091faa994..d99491730 100644
--- a/doc/index.rst
+++ b/doc/index.rst
@@ -201,6 +201,7 @@ PennyLane is **free** and **open source**, released under the Apache License, Ve
    code/qml_kernels
    code/qml_logging
    code/qml_math
+   code/qml_noise
    code/qml_numpy
    code/qml_ops_op_math
    code/qml_pauli
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 8c23238f7..0a376848a 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -47,6 +47,8 @@
 * The `default.tensor` device is introduced to perform tensor network simulation of a quantum circuit.
   [(#5699)](https://github.com/PennyLaneAI/pennylane/pull/5699)
 
+* A new `qml.noise` module which contains utililty functions for building `NoiseModels`.
+  [(#5674)](https://github.com/PennyLaneAI/pennylane/pull/5674)
 
 <h3>Improvements 🛠</h3>
 
@@ -378,6 +380,7 @@ This release contains contributions from (in alphabetical order):
 
 Tarun Kumar Allamsetty,
 Guillermo Alonso-Linaje,
+Utkarsh Azad,
 Lillian M. A. Frederiksen,
 Gabriel Bottrill,
 Astral Cai,
diff --git a/pennylane/__init__.py b/pennylane/__init__.py
index 9e92671d2..1138721b7 100644
--- a/pennylane/__init__.py
+++ b/pennylane/__init__.py
@@ -140,6 +140,7 @@ from pennylane.compiler import qjit, while_loop, for_loop
 import pennylane.compiler
 
 import pennylane.data
+import pennylane.noise
 
 # Look for an existing configuration file
 default_config = Configuration("config.toml")
diff --git a/pennylane/boolean_fn.py b/pennylane/boolean_fn.py
index 2ca4e2954..45b793b42 100644
--- a/pennylane/boolean_fn.py
+++ b/pennylane/boolean_fn.py
@@ -1,4 +1,4 @@
-# Copyright 2018-2021 Xanadu Quantum Technologies Inc.
+# Copyright 2018-2024 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.
@@ -19,13 +19,14 @@ of functions with boolean output.
 import functools
 
 
+# pylint: disable=unnecessary-lambda
 class BooleanFn:
-    r"""Wrapper for simple callables with boolean output that can be
+    r"""Wrapper for simple callables with Boolean output that can be
     manipulated and combined with bit-wise operators.
 
     Args:
-        fn (callable): Function to be wrapped. It must accept a single
-            argument, and must return a boolean.
+        fn (callable): Function to be wrapped. It can accept any number
+            of arguments, and must return a Boolean.
 
     **Example**
 
@@ -77,18 +78,132 @@ class BooleanFn:
 
     """
 
-    def __init__(self, fn):
+    def __init__(self, fn, name=None):
         self.fn = fn
+        self.name = name or self.fn.__name__
         functools.update_wrapper(self, fn)
 
     def __and__(self, other):
-        return BooleanFn(lambda obj: self.fn(obj) and other.fn(obj))
+        return And(self, other)
 
     def __or__(self, other):
-        return BooleanFn(lambda obj: self.fn(obj) or other.fn(obj))
+        return Or(self, other)
+
+    def __xor__(self, other):
+        return Xor(self, other)
 
     def __invert__(self):
-        return BooleanFn(lambda obj: not self.fn(obj))
+        return Not(self)
+
+    def __call__(self, *args, **kwargs):
+        return self.fn(*args, **kwargs)
+
+    def __repr__(self):
+        return f"BooleanFn({self.name})" if not (self.bitwise or self.conditional) else self.name
+
+    @property
+    def bitwise(self):
+        """Determine whether wrapped callable performs a bit-wise operation or not.
+        This checks for the ``operands`` attribute that should be defined by it."""
+        return bool(getattr(self, "operands", tuple()))
+
+    @property
+    def conditional(self):
+        """Determine whether wrapped callable is for a conditional or not.
+        This checks for the ``condition`` attribute that should be defined by it."""
+        return bool(getattr(self, "condition", None))
+
+
+class And(BooleanFn):
+    """Developer facing class for implemeting bit-wise ``AND`` for callables
+    wrapped up with :class:`BooleanFn <pennylane.BooleanFn>`.
+
+    Args:
+        left (~.BooleanFn): Left operand in the bit-wise expression.
+        right (~.BooleanFn): Right operand in the bit-wise expression.
+    """
+
+    def __init__(self, left, right):
+        self.operands = (left, right)
+
+        if any(getattr(opr, "condition", None) for opr in self.operands):
+            self.condition = tuple(getattr(opr, "condition", ()) for opr in self.operands)
+
+        super().__init__(
+            lambda *args, **kwargs: left(*args, **kwargs) and right(*args, **kwargs),
+            f"And({left.name}, {right.name})",
+        )
+
+    def __str__(self):
+        return f"{self.operands[0].name} & {self.operands[1].name}"
+
+
+class Or(BooleanFn):
+    """Developer facing class for implemeting bit-wise ``OR`` for callables
+    wrapped up with :class:`BooleanFn <pennylane.BooleanFn>`.
+
+    Args:
+        left (~.BooleanFn): Left operand in the bit-wise expression.
+        right (~.BooleanFn): Right operand in the bit-wise expression.
+    """
+
+    def __init__(self, left, right):
+        self.operands = (left, right)
+
+        if any(getattr(opr, "condition", None) for opr in self.operands):
+            self.condition = tuple(getattr(opr, "condition", ()) for opr in self.operands)
+
+        super().__init__(
+            lambda *args, **kwargs: left(*args, **kwargs) or right(*args, **kwargs),
+            f"Or({left.name}, {right.name})",
+        )
+
+    def __str__(self):
+        return f"{self.operands[0].name} | {self.operands[1].name}"
+
+
+class Xor(BooleanFn):
+    """Developer facing class for implemeting bit-wise ``XOR`` for callables
+    wrapped up with :class:`BooleanFn <pennylane.BooleanFn>`.
+
+    Args:
+        left (~.BooleanFn): Left operand in the bit-wise expression.
+        right (~.BooleanFn): Right operand in the bit-wise expression.
+    """
+
+    def __init__(self, left, right):
+        self.operands = (left, right)
+
+        if any(getattr(opr, "condition", None) for opr in self.operands):
+            self.condition = tuple(getattr(opr, "condition", ()) for opr in self.operands)
+
+        super().__init__(
+            lambda *args, **kwargs: left(*args, **kwargs) ^ right(*args, **kwargs),
+            f"Xor({left.name}, {right.name})",
+        )
+
+    def __str__(self):
+        return f"{self.operands[0].name} ^ {self.operands[1].name}"
+
+
+class Not(BooleanFn):
+    """Developer facing class for implemeting bit-wise ``NOT`` for callables
+    wrapped up with :class:`BooleanFn <pennylane.BooleanFn>`.
+
+    Args:
+        left (~.BooleanFn): Left operand in the bit-wise expression.
+    """
+
+    def __init__(self, left):
+        self.operands = (left,)
+
+        if any(getattr(opr, "condition", None) for opr in self.operands):
+            self.condition = tuple(getattr(opr, "condition", ()) for opr in self.operands)
+
+        super().__init__(
+            lambda *args, **kwargs: not left(*args, **kwargs),
+            f"Not({left.name})",
+        )
 
-    def __call__(self, obj):
-        return self.fn(obj)
+    def __str__(self):
+        return f"~{self.operands[0].name}"
diff --git a/pennylane/noise/__init__.py b/pennylane/noise/__init__.py
new file mode 100644
index 000000000..5458fc874
--- /dev/null
+++ b/pennylane/noise/__init__.py
@@ -0,0 +1,16 @@
+# Copyright 2018-2024 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 functionality to work with noise models in PennyLane."""
+
+from .conditionals import wires_in, wires_eq, op_in, op_eq, partial_wires
diff --git a/pennylane/noise/conditionals.py b/pennylane/noise/conditionals.py
new file mode 100644
index 000000000..bc9603be6
--- /dev/null
+++ b/pennylane/noise/conditionals.py
@@ -0,0 +1,483 @@
+# Copyright 2018-2024 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.
+"""Contains utility functions for building boolean conditionals for noise models
+
+Developer note: Conditionals inherit from BooleanFn and store the condition they
+utilize in the ``condition`` attribute.
+"""
+
+from functools import partial
+from inspect import isclass, signature
+
+import pennylane as qml
+from pennylane.boolean_fn import BooleanFn
+from pennylane.ops import Controlled
+from pennylane.templates import ControlledSequence
+from pennylane.wires import WireError, Wires
+
+# pylint: disable = unnecessary-lambda, too-few-public-methods
+
+
+class WiresIn(BooleanFn):
+    """A conditional for evaluating if the wires of an operation exist in a specified set of wires.
+
+    Args:
+        wires (Union[Iterable[int, str], Wires]): sequence of wires for building the wire set.
+
+    .. seealso:: Users are advised to use :func:`~.wires_in` for a functional construction.
+    """
+
+    def __init__(self, wires):
+        self._cond = set(wires)
+        self.condition = self._cond
+        super().__init__(
+            lambda wire: _get_wires(wire).issubset(self._cond), f"WiresIn({list(wires)})"
+        )
+
+
+class WiresEq(BooleanFn):
+    """A conditional for evaluating if a given wire is equal to a specified set of wires.
+
+    Args:
+        wires (Union[Iterable[int, str], Wires]): sequence of wires for building the wire set.
+
+    .. seealso:: Users are advised to use :func:`~.wires_eq` for a functional construction.
+    """
+
+    def __init__(self, wires):
+        self._cond = set(wires)
+        self.condition = self._cond
+        super().__init__(
+            lambda wire: _get_wires(wire) == self._cond,
+            f"WiresEq({list(wires) if len(wires) > 1 else list(wires)[0]})",
+        )
+
+
+def _get_wires(val):
+    """Extract wires as a set from an integer, string, Iterable, Wires or Operation instance.
+
+    Args:
+        val (Union[int, str, Iterable, ~.wires.Wires, ~.operation.Operation]): object to be used
+            for building the wire set.
+
+    Returns:
+        set[Union[int, str]]: computed wire set
+
+    Raises:
+        ValueError: if the wire set cannot be computed for ``val``.
+    """
+    iters = val if isinstance(val, (list, tuple, set, Wires)) else getattr(val, "wires", [val])
+    try:
+        wires = set().union(*((getattr(w, "wires", None) or Wires(w)).tolist() for w in iters))
+    except (TypeError, WireError):
+        raise ValueError(f"Wires cannot be computed for {val}") from None
+    return wires
+
+
+def wires_in(wires):
+    """Builds a conditional as a :class:`~.BooleanFn` for evaluating
+    if the wires of an input operation are within the specified set of wires.
+
+    Args:
+        wires (Union(Iterable[int, str], Wires, Operation, int, str)): object to be used
+            for building the wire set.
+
+    Returns:
+        :class:`WiresIn <pennylane.noise.conditionals.WiresIn>`: a callable object with
+        signature ``Union(Iterable[int, str], Wires, Operation, int, str)``. It evaluates
+        to ``True`` if the wire set constructed from the input to the callable is a
+        subset of the one built from the specified ``wires`` set.
+
+    Raises:
+        ValueError: if the wire set cannot be computed from ``wires``.
+
+    **Example**
+
+    One may use ``wires_in`` with a given sequence of wires which are used as a wire set:
+
+    >>> cond_func = qml.noise.wires_in([0, 1])
+    >>> cond_func(qml.X(0))
+    True
+    >>> cond_func(qml.X(3))
+    False
+
+    Additionally, if an :class:`Operation <pennylane.operation.Operation>` is provided,
+    its ``wires`` are extracted and used to build the wire set:
+
+    >>> cond_func = qml.noise.wires_in(qml.CNOT(["alice", "bob"]))
+    >>> cond_func("alice")
+    True
+    >>> cond_func("eve")
+    False
+    """
+    return WiresIn(_get_wires(wires))
+
+
+def wires_eq(wires):
+    """Builds a conditional as a :class:`~.BooleanFn` for evaluating
+    if a given wire is equal to specified set of wires.
+
+    Args:
+        wires (Union(Iterable[int, str], Wires, Operation, int, str)): object to be used
+            for building the wire set.
+
+    Returns:
+        :class:`WiresEq <pennylane.noise.conditionals.WiresEq>`: a callable object with
+        signature ``Union(Iterable[int, str], Wires, Operation, int, str)``. It evaluates
+        to ``True`` if the wire set constructed from the input to the callable is equal
+        to the one built from the specified ``wires`` set.
+
+    Raises:
+        ValueError: if the wire set cannot be computed from ``wires``.
+
+    **Example**
+
+    One may use ``wires_eq`` with a given sequence of wires which are used as a wire set:
+
+    >>> cond_func = qml.noise.wires_eq(0)
+    >>> cond_func(qml.X(0))
+    True
+    >>> cond_func(qml.RY(1.23, wires=[3]))
+    False
+
+    Additionally, if an :class:`Operation <pennylane.operation.Operation>` is provided,
+    its ``wires`` are extracted and used to build the wire set:
+
+    >>> cond_func = qml.noise.wires_eq(qml.RX(1.0, "dino"))
+    >>> cond_func(qml.RZ(1.23, wires="dino"))
+    True
+    >>> cond_func("eve")
+    False
+    """
+    return WiresEq(_get_wires(wires))
+
+
+class OpIn(BooleanFn):
+    """A conditional for evaluating if a given operation exist in a specified set of operations.
+
+    Args:
+        ops (Union[str, class, Operation, list[str, class, Operation]]): sequence of operation
+            instances, string representations or classes to build the operation set.
+
+    .. seealso:: Users are advised to use :func:`~.op_in` for a functional construction.
+    """
+
+    def __init__(self, ops):
+        self._cond = ops
+        self._cops = _get_ops(ops)
+        self.condition = self._cops
+        super().__init__(
+            self._check_in_ops, f"OpIn({[getattr(op, '__name__') for op in self._cops]})"
+        )
+
+    def _check_in_ops(self, operation):
+        xs = operation if isinstance(operation, (list, tuple, set)) else [operation]
+        cs = _get_ops(xs)
+
+        try:
+            return all(
+                (
+                    c in self._cops
+                    if isclass(x) or not getattr(x, "arithmetic_depth", 0)
+                    else any(
+                        (
+                            _check_arithmetic_ops(op, x)
+                            if isinstance(op, cp) and getattr(op, "arithmetic_depth", 0)
+                            else cp == _get_ops(x)[0]
+                        )
+                        for op, cp in zip(self._cond, self._cops)
+                    )
+                )
+                for x, c in zip(xs, cs)
+            )
+        except:  # pylint: disable = bare-except # pragma: no cover
+            raise ValueError(
+                "OpIn does not support arithmetic operations "
+                "that cannot be converted to a linear combination"
+            ) from None
+
+
+class OpEq(BooleanFn):
+    """A conditional for evaluating if a given operation is equal to the specified operation.
+
+    Args:
+        ops (Union[str, class, Operation]): An operation instance, string representation or
+            class to build the operation set.
+
+    .. seealso:: Users are advised to use :func:`~.op_eq` for a functional construction.
+    """
+
+    def __init__(self, ops):
+        self._cond = [ops] if not isinstance(ops, (list, tuple, set)) else ops
+        self._cops = _get_ops(ops)
+        self.condition = self._cops
+        cops_names = list(getattr(op, "__name__") for op in self._cops)
+        super().__init__(
+            self._check_eq_ops,
+            f"OpEq({cops_names if len(cops_names) > 1 else cops_names[0]})",
+        )
+
+    def _check_eq_ops(self, operation):
+        if all(isclass(op) or not getattr(op, "arithmetic_depth", 0) for op in self._cond):
+            return _get_ops(operation) == self._cops
+
+        try:
+            xs = operation if isinstance(operation, (list, tuple, set)) else [operation]
+            return (
+                len(xs) == len(self._cond)
+                and _get_ops(xs) == self._cops
+                and all(
+                    _check_arithmetic_ops(op, x)
+                    for (op, x) in zip(self._cond, xs)
+                    if not isclass(x) and getattr(x, "arithmetic_depth", 0)
+                )
+            )
+        except:  # pylint: disable = bare-except # pragma: no cover
+            raise ValueError(
+                "OpEq does not support arithmetic operations "
+                "that cannot be converted to a linear combination"
+            ) from None
+
+
+def _get_ops(val):
+    """Computes the class for a given argument from its string name, instance,
+    or a sequence of them.
+
+    Args:
+        val (Union[str, Operation, Iterable]): object to be used
+            for building the operation set.
+
+    Returns:
+        tuple[class]: tuple of :class:`Operation <pennylane.operation.Operation>`
+        classes corresponding to val.
+    """
+    vals = val if isinstance(val, (list, tuple, set)) else [val]
+    return tuple(
+        (
+            getattr(qml.ops, val, None) or getattr(qml, val)
+            if isinstance(val, str)
+            else (val if isclass(val) else getattr(val, "__class__"))
+        )
+        for val in vals
+    )
+
+
+def _check_arithmetic_ops(op1, op2):
+    """Helper method for comparing two arithmetic operators based on type check of the bases"""
+    # pylint: disable = unnecessary-lambda-assignment
+
+    if isinstance(op1, (Controlled, ControlledSequence)) or isinstance(
+        op2, (Controlled, ControlledSequence)
+    ):
+        return (
+            isinstance(op1, type(op2))
+            and op1.arithmetic_depth == op2.arithmetic_depth
+            and _get_ops(op1.base) == _get_ops(op2.base)
+        )
+
+    lc_cop = lambda op: qml.ops.LinearCombination(*op.terms())
+
+    if isinstance(op1, qml.ops.Exp) or isinstance(op2, qml.ops.Exp):
+        if (
+            not isinstance(op1, type(op2))
+            or (op1.base.arithmetic_depth != op2.base.arithmetic_depth)
+            or not qml.math.allclose(op1.coeff, op2.coeff)
+            or (op1.num_steps != op2.num_steps)
+        ):
+            return False
+        if op1.base.arithmetic_depth:
+            return _check_arithmetic_ops(op1.base, op2.base)
+        return _get_ops(op1.base) == _get_ops(op2.base)
+
+    op1, op2 = qml.simplify(op1), qml.simplify(op2)
+    if op1.arithmetic_depth != op2.arithmetic_depth:
+        return False
+
+    coeffs, op_terms = lc_cop(op1).terms()
+    sprods = [_get_ops(getattr(op_term, "operands", op_term)) for op_term in op_terms]
+
+    def _lc_op(x):
+        coeffs2, op_terms2 = lc_cop(x).terms()
+        sprods2 = [_get_ops(getattr(op_term, "operands", op_term)) for op_term in op_terms2]
+        for coeff, sprod in zip(coeffs2, sprods2):
+            present, p_index = False, -1
+            while sprod in sprods[p_index + 1 :]:
+                p_index = sprods[p_index + 1 :].index(sprod) + (p_index + 1)
+                if qml.math.allclose(coeff, coeffs[p_index]):
+                    coeffs.pop(p_index)
+                    sprods.pop(p_index)
+                    present = True
+                    break
+
+            if not present:
+                break
+
+        return present
+
+    return _lc_op(op2)
+
+
+def op_in(ops):
+    """Builds a conditional as a :class:`~.BooleanFn` for evaluating
+    if a given operation exist in a specified set of operations.
+
+    Args:
+        ops (str, class, Operation, list(Union[str, class, Operation])): sequence of string
+            representations, instances or classes of the operation(s).
+
+    Returns:
+        :class:`OpIn <pennylane.noise.conditionals.OpIn>`: A callable object that accepts
+        an :class:`~.Operation` and gives a boolean output. It accepts any input from:
+        ``Union(str, class, Operation, list(Union[str, class, Operation]))`` and evaluates
+        to ``True``, if input operation(s) exist in the set of operation(s) specified by
+        ``ops``, based on a comparison of the operation type, irrespective of wires.
+
+    **Example**
+
+    One may use ``op_in`` with a string representation of the name of the operation:
+
+    >>> cond_func = qml.noise.op_in(["RX", "RY"])
+    >>> cond_func(qml.RX(1.23, wires=[0]))
+    True
+    >>> cond_func(qml.RZ(1.23, wires=[3]))
+    False
+    >>> cond_func([qml.RX(1.23, wires=[1]), qml.RY(4.56, wires=[2])])
+    True
+
+    Additionally, an instance of :class:`Operation <pennylane.operation.Operation>`
+    can also be provided:
+
+    >>> cond_func = qml.noise.op_in([qml.RX(1.0, "dino"), qml.RY(2.0, "rhino")])
+    >>> cond_func(qml.RX(1.23, wires=["eve"]))
+    True
+    >>> cond_func(qml.RY(1.23, wires=["dino"]))
+    True
+    >>> cond_func([qml.RX(1.23, wires=[1]), qml.RZ(4.56, wires=[2])])
+    False
+    """
+    ops = [ops] if not isinstance(ops, (list, tuple, set)) else ops
+    return OpIn(ops)
+
+
+def op_eq(ops):
+    """Builds a conditional as a :class:`~.BooleanFn` for evaluating
+    if a given operation is equal to the specified operation.
+
+    Args:
+        ops (str, class, Operation): string representation, an instance or class of the operation.
+
+    Returns:
+        :class:`OpEq <pennylane.noise.conditionals.OpEq>`: A callable object that accepts
+        an :class:`~.Operation` and gives a boolean output. It accepts any input from:
+        ``Union(str, class, Operation)`` and evaluates to ``True``, if input operation(s)
+        is equal to the set of operation(s) specified by ``ops``, based on a comparison of
+        the operation type, irrespective of wires.
+
+    **Example**
+
+    One may use ``op_eq`` with a string representation of the name of the operation:
+
+    >>> cond_func = qml.noise.op_eq("RX")
+    >>> cond_func(qml.RX(1.23, wires=[0]))
+    True
+    >>> cond_func(qml.RZ(1.23, wires=[3]))
+    False
+    >>> cond_func("CNOT")
+    False
+
+    Additionally, an instance of :class:`Operation <pennylane.operation.Operation>`
+    can also be provided:
+
+    >>> cond_func = qml.noise.op_eq(qml.RX(1.0, "dino"))
+    >>> cond_func(qml.RX(1.23, wires=["eve"]))
+    True
+    >>> cond_func(qml.RY(1.23, wires=["dino"]))
+    False
+    """
+    return OpEq(ops)
+
+
+def partial_wires(operation, *args, **kwargs):
+    """Builds a partial function based on the given operation with
+    all argument frozen except ``wires``.
+
+    Args:
+        operation (Operation, class): instance of an operation or the class
+            corresponding to the operation.
+        *args: positional arguments provided in the case where the keyword argument
+            ``operation`` is a class for building the partially evaluated instance.
+        **kwargs: keyword arguments for the building the partially evaluated instance.
+            These will override any arguments present in the operation instance or ``args``.
+
+    Returns:
+        callable: a wrapper function that accepts a sequence of wires as an argument or
+        any object with a ``wires`` property.
+
+    Raises:
+        ValueError: if ``args`` are provided when the given ``operation`` is an instance.
+
+    **Example**
+
+    One may give an instance of :class:`Operation <pennylane.operation.Operation>`
+    for the ``operation`` argument:
+
+    >>> func = qml.noise.partial_wires(qml.RX(1.2, [12]))
+    >>> func(2)
+    qml.RX(1.2, wires=[2])
+    >>> func(qml.RY(1.0, ["wires"]))
+    qml.RX(1.2, wires=["wires"])
+
+    Additionally, an :class:`Operation <pennylane.operation.Operation>` class can
+    also be provided, while providing required positional arguments via ``args``:
+
+    >>> func = qml.noise.partial_wires(qml.RX, 3.2, [20])
+    >>> func(qml.RY(1.0, [0]))
+    qml.RX(3.2, wires=[0])
+
+    Finally, one can also use ``kwargs`` instead of positional arguments:
+
+    >>> func = qml.noise.partial_wires(qml.RX, phi=1.2)
+    >>> func(qml.RY(1.0, [2]))
+    qml.RX(1.2, wires=[2])
+    >>> rfunc = qml.noise.partial_wires(qml.RX(1.2, [12]), phi=2.3)
+    >>> rfunc(qml.RY(1.0, ["light"]))
+    qml.RX(2.3, wires=["light"])
+    """
+
+    def _partial_op(x):
+        """Wrapper function for partial_wires"""
+        wires = getattr(x, "wires", None) or ([x] if isinstance(x, (int, str)) else list(x))
+        return op(wires=wires)
+
+    if not callable(operation):
+        if args:
+            raise ValueError(
+                "Args cannot be provided when operation is an instance, "
+                f"got operation = {operation} and args = {args}."
+            )
+        args, metadata = getattr(operation, "_flatten")()
+        if len(metadata) > 1:
+            kwargs = {**dict(metadata[1]), **kwargs}
+        operation = type(operation)
+
+    fsignature = signature(getattr(operation, "__init__", operation)).parameters
+    parameters = list(fsignature)[int("self" in fsignature) :]
+    arg_params = {**dict(zip(parameters, args)), **kwargs}
+
+    if "wires" in arg_params:  # Ensure we don't include wires arg
+        arg_params.pop("wires")
+
+    op = partial(operation, **{**arg_params, **kwargs})
+
+    return _partial_op


Test patch:
diff --git a/tests/noise/test_conditionals.py b/tests/noise/test_conditionals.py
new file mode 100644
index 000000000..334d0d945
--- /dev/null
+++ b/tests/noise/test_conditionals.py
@@ -0,0 +1,283 @@
+# Copyright 2018-2024 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.
+"""
+Unit tests for the available conditional utitlities for noise models.
+"""
+
+# pylint: disable = too-few-public-methods
+import pytest
+
+import pennylane as qml
+from pennylane.boolean_fn import And, Not, Or, Xor
+from pennylane.noise.conditionals import _get_ops, _get_wires
+
+
+class TestNoiseConditionals:
+    """Test for the Conditional classes"""
+
+    def test_noise_conditional_lambda(self):
+        """Test for BooleanFn builds correct objects"""
+
+        func = qml.BooleanFn(lambda x: x < 10, "less_than_ten")
+
+        assert isinstance(func, qml.BooleanFn)
+        assert func(2) and not func(42)
+        assert repr(func) == "BooleanFn(less_than_ten)"
+
+    def test_noise_conditional_def(self):
+        """Test for BooleanFn builds correct objects"""
+
+        def greater_than_five(x):
+            return x > 5
+
+        func = qml.BooleanFn(greater_than_five)
+
+        assert isinstance(func, qml.BooleanFn)
+        assert not func(3) and func(7)
+        assert repr(func) == "BooleanFn(greater_than_five)"
+
+    def test_and_conditionals(self):
+        """Test for BooleanFn supports bitwise AND"""
+
+        @qml.BooleanFn
+        def is_int(x):
+            return isinstance(x, int)
+
+        @qml.BooleanFn
+        def has_bit_length_3(x):
+            return x.bit_length() == 3
+
+        func = is_int & has_bit_length_3
+
+        assert isinstance(func, And)
+        assert func(4) and not func(2.3)
+        assert repr(func) == "And(is_int, has_bit_length_3)"
+        assert str(func) == "is_int & has_bit_length_3"
+        assert func.bitwise
+
+    def test_or_conditionals(self):
+        """Test for BooleanFn supports bitwise OR"""
+
+        @qml.BooleanFn
+        def is_int(x):
+            return isinstance(x, int)
+
+        @qml.BooleanFn
+        def less_than_five(x):
+            return x < 5
+
+        func = is_int | less_than_five
+
+        assert isinstance(func, Or)
+        assert func(4) and not func(7.5)
+        assert repr(func) == "Or(is_int, less_than_five)"
+        assert str(func) == "is_int | less_than_five"
+        assert func.bitwise
+
+    def test_xor_conditionals(self):
+        """Test for BooleanFn supports bitwise XOR"""
+
+        @qml.BooleanFn
+        def is_int(x):
+            return isinstance(x, int)
+
+        @qml.BooleanFn
+        def has_bit_length_3(x):
+            return x.bit_length() == 3
+
+        func = is_int ^ has_bit_length_3
+
+        assert isinstance(func, Xor)
+        assert not func(4) and func(11)
+        assert repr(func) == "Xor(is_int, has_bit_length_3)"
+        assert str(func) == "is_int ^ has_bit_length_3"
+        assert func.bitwise
+
+    def test_not_conditionals(self):
+        """Test for BooleanFn supports bitwise NOT"""
+
+        def is_int(x):
+            return isinstance(x, int)
+
+        func = ~qml.BooleanFn(is_int)
+
+        assert isinstance(func, Not)
+        assert not func(4) and func(7.5)
+        assert repr(func) == "Not(is_int)"
+        assert str(func) == "~is_int"
+        assert func.bitwise
+
+
+class TestNoiseFunctions:
+    """Test for the Conditional methods"""
+
+    @pytest.mark.parametrize(
+        ("obj", "wires", "result"),
+        [
+            (0, 0, True),
+            ([1, 2, 3], 0, False),
+            (qml.wires.Wires(["aurora", "borealis"]), "borealis", True),
+            (qml.wires.Wires(1), [0], False),
+            (qml.Y(2), 2, True),
+            (qml.CNOT(["a", "c"]), "b", False),
+            (qml.CZ(["a", "c"]), "c", True),
+            (qml.DoubleExcitation(1.2, ["alpha", "beta", "gamma", "delta"]), "alpha", True),
+            (qml.TrotterProduct(qml.Z(0) + qml.Z(1), -3j), 2, False),
+            (qml.TrotterProduct(qml.Z("a") + qml.Z("b"), -3j), "b", True),
+        ],
+    )
+    def test_wires_in(self, obj, wires, result):
+        """Test for checking WiresIn work as expected for checking if a wire exist in a set of specified wires"""
+
+        func = qml.noise.wires_in(obj)
+
+        assert isinstance(func, qml.BooleanFn)
+        assert str(func) == f"WiresIn({list(_get_wires(obj))})"
+        assert func(wires) == result
+
+    @pytest.mark.parametrize(
+        ("obj", "wires", "result"),
+        [
+            (0, 0, True),
+            ([1, 2], [1, 2], True),
+            (qml.wires.Wires(["street", "fighter"]), "fighter", False),
+            (qml.wires.Wires(1), [1], True),
+            (qml.Y(2), 2, True),
+            (qml.CNOT(["a", "c"]), "b", False),
+            (qml.CNOT(["c", "d"]), ["c", "d"], True),
+            (qml.TrotterProduct(qml.Z(0) + qml.Z(1), -3j), 2, False),
+            (qml.TrotterProduct(qml.Z("b") + qml.Z("a"), -3j), ["b", "a"], True),
+        ],
+    )
+    def test_wires_eq(self, obj, wires, result):
+        """Test for checking WiresEq work as expected for checking if a set of wires is equal to specified wires"""
+
+        func = qml.noise.wires_eq(obj)
+        _wires = list(_get_wires(obj))
+        assert isinstance(func, qml.BooleanFn)
+        assert str(func) == f"WiresEq({_wires if len(_wires) > 1 else _wires[0]})"
+        assert func(wires) == result
+
+    def test_get_wires_error(self):
+        """Test for checking _get_wires method raise correct error"""
+
+        with pytest.raises(ValueError, match="Wires cannot be computed for"):
+            _get_wires(qml.RX)
+
+    @pytest.mark.parametrize(
+        ("obj", "op", "result"),
+        [
+            ([qml.RX, qml.RY], qml.RY(1.0, 1), True),
+            ([qml.RX, qml.RY], qml.RX, True),
+            (qml.RX(0, 1), qml.RY(1.0, 1), False),
+            ("RX", qml.RX(0, 1), True),
+            (["CZ", "RY", "CNOT"], qml.CNOT([0, 1]), True),
+            (qml.Y(1), qml.RY(1.0, 1), False),
+            (qml.CNOT(["a", "c"]), qml.CNOT([0, 1]), True),
+            ([qml.CZ(["a", "c"]), qml.Y(1)], qml.CZ([0, 1]), True),
+            ([qml.RZ(1.9, 0), qml.Z(0) @ qml.Z(1)], qml.Z("b") @ qml.Z("a"), True),
+            ([qml.Z(0) + qml.Z(1), qml.Z(2)], qml.Z("b") + qml.Z("a"), True),
+            ([qml.Z(0), qml.Z(0) + 1.2 * qml.Z(1)], qml.Y("b") + qml.Y("a"), False),
+        ],
+    )
+    def test_op_in(self, obj, op, result):
+        """Test for checking OpIn work as expected for checking if a operation exist in a set of specified operation"""
+
+        func = qml.noise.op_in(obj)
+
+        assert isinstance(func, qml.BooleanFn)
+
+        op_repr = list(getattr(op, "__name__") for op in _get_ops(obj))
+        assert str(func) == f"OpIn({op_repr})"
+        assert func(op) == result
+
+    @pytest.mark.parametrize(
+        ("obj", "op", "result"),
+        [
+            ("I", qml.I(wires=[0, 1]), True),
+            (qml.Y(1), qml.RY(1.0, 1), False),
+            (qml.CNOT(["a", "c"]), qml.CNOT([0, 1]), True),
+            (qml.RX(0, 1), qml.RY(1.0, 1), False),
+            ("RX", qml.RX(0, 1), True),
+            ([qml.RX, qml.RY], qml.RX, False),
+            ([qml.RX, qml.RY], [qml.RX(1.0, 1), qml.RY(2.0, 2)], True),
+            (["CZ", "RY"], [qml.CZ([0, 1]), qml.RY(1.0, [1])], True),
+            (qml.Z(0) @ qml.Z(1), qml.Z("b") @ qml.Z("a"), True),
+            (qml.Z(0) + qml.Z(1), qml.Z("b") + qml.Z("a"), True),
+            (qml.Z(0) + 1.2 * qml.Z(1), 2.4 * qml.Z("b") + qml.Z("a"), False),
+            (qml.Z(0) + 1.2 * qml.Z(1), qml.Z("b") + qml.Z("a"), False),
+            (qml.exp(qml.RX(1.2, 0), 1.2, 1), qml.exp(qml.RX(2.3, "a"), 1.2, 1), True),
+            (qml.exp(qml.Z(0) + qml.Z(1), 1.2, 2), qml.exp(qml.Z("b") + qml.Z("a"), 1.2, 2), True),
+            (qml.exp(qml.Z(0) @ qml.Z(1), 2j), qml.exp(qml.Z("b") @ qml.Z("a"), 1j), False),
+        ],
+    )
+    def test_op_eq(self, obj, op, result):
+        """Test for checking OpEq work as expected for checking if an operation is equal to specified operation"""
+
+        func = qml.noise.op_eq(obj)
+
+        assert isinstance(func, qml.BooleanFn)
+
+        op_repr = [getattr(op, "__name__") for op in _get_ops(obj)]
+        assert str(func) == f"OpEq({op_repr if len(op_repr) > 1 else op_repr[0]})"
+        assert func(op) == result
+
+    def test_conditional_bitwise(self):
+        """Test that conditionals can be operated with bitwise operations"""
+
+        cond1 = qml.noise.wires_eq(0)
+        cond2 = qml.noise.op_eq(qml.X)
+        conds = (cond1.condition, cond2.condition)
+
+        and_cond = cond1 & cond2
+        assert and_cond(qml.X(0)) and not and_cond(qml.X(1))
+        assert and_cond.condition == conds
+
+        or_cond = cond1 | cond2
+        assert or_cond(qml.X(1)) and not or_cond(qml.Y(1))
+        assert or_cond.condition == conds
+
+        xor_cond = cond1 ^ cond2
+        assert xor_cond(qml.X(1)) and not xor_cond(qml.Y(1))
+        assert xor_cond.condition == conds
+
+        not_cond = ~cond1
+        assert not_cond(qml.X(1))
+        assert not_cond.condition == conds[:1]
+
+    def test_partial_wires(self):
+        """Test for checking partial_wires work as expected for building callables to make op with correct wires"""
+
+        op = qml.noise.partial_wires(qml.RX(1.2, [12]))(qml.RY(1.0, ["wires"]))
+        assert qml.equal(op, qml.RX(1.2, wires=["wires"]))
+
+        op = qml.noise.partial_wires(qml.RX, 3.2, [20])(qml.RY(1.0, [0]))
+        assert qml.equal(op, qml.RX(3.2, wires=[0]))
+
+        op = qml.noise.partial_wires(qml.RX, phi=1.2)(qml.RY(1.0, [2]))
+        assert qml.equal(op, qml.RX(1.2, wires=[2]))
+
+        op = qml.noise.partial_wires(qml.PauliRot(1.2, "XY", wires=(0, 1)))(qml.CNOT([2, 1]))
+        assert qml.equal(op, qml.PauliRot(1.2, "XY", wires=(2, 1)))
+
+        op = qml.noise.partial_wires(qml.RX(1.2, [12]), phi=2.3)(qml.RY(1.0, ["light"]))
+        assert qml.equal(op, qml.RX(2.3, wires=["light"]))
+
+    def test_partial_wires_error(self):
+        """Test for checking partial_wires raise correct error when args are given"""
+
+        with pytest.raises(
+            ValueError, match="Args cannot be provided when operation is an instance"
+        ):
+            qml.noise.partial_wires(qml.RX(1.2, [12]), 1.2)(qml.RY(1.0, ["wires"]))
diff --git a/tests/test_boolean_fn.py b/tests/test_boolean_fn.py
index 67422e179..aa6324c20 100644
--- a/tests/test_boolean_fn.py
+++ b/tests/test_boolean_fn.py
@@ -58,3 +58,13 @@ class TestBooleanFn:
         assert crit(-2)
         assert not crit(6)
         assert crit(10)
+
+    def test_repr(self):
+        """Test the repr is right."""
+
+        def greater_than_five(x):
+            return x > 5
+
+        func = qml.BooleanFn(greater_than_five)
+
+        assert repr(func) == "BooleanFn(greater_than_five)"