Repository: PennyLaneAI/pennylane (python)

Issue:
[BUG] `qml.map_wires` does not work correctly with `qml.cond`
### Expected behavior

The mid-circuit measurements that a `qml.cond` depends on are mapped to the new wires correctly.

### Actual behavior

The wires of the measurements used to control the `qml.cond` are not mapped.

### Additional information

_No response_

### Source code

```shell
with qml.queuing.AnnotatedQueue() as q:
    m = qml.measure("a")
    qml.cond(m, qml.PauliZ)("b")

qs = qml.tape.QuantumScript.from_queue(q)
[tape], _ = qml.map_wires(qs, {"a": 0, "b": 1})
tape.operations[1].meas_val.measurements
```


### Tracebacks

```shell
[measure(wires=['a'])]
```


### System information

```shell
Name: PennyLane
Version: 0.34.0.dev0
Summary: PennyLane is a Python quantum machine learning library by Xanadu Inc.
Home-page: https://github.com/PennyLaneAI/pennylane
Author: 
Author-email: 
License: Apache License 2.0
Location: /Users/mudit.pandey/repos/pennylane
Requires: appdirs, autograd, autoray, cachetools, networkx, numpy, pennylane-lightning, requests, rustworkx, scipy, semantic-version, toml, typing_extensions
Required-by: PennyLane-Lightning

Platform info:           macOS-13.6.1-arm64-arm-64bit
Python version:          3.10.12
Numpy version:           1.26.1
Scipy version:           1.11.1
Installed devices:
- default.gaussian (PennyLane-0.34.0.dev0)
- default.mixed (PennyLane-0.34.0.dev0)
- default.qubit (PennyLane-0.34.0.dev0)
- default.qubit.autograd (PennyLane-0.34.0.dev0)
- default.qubit.jax (PennyLane-0.34.0.dev0)
- default.qubit.legacy (PennyLane-0.34.0.dev0)
- default.qubit.tf (PennyLane-0.34.0.dev0)
- default.qubit.torch (PennyLane-0.34.0.dev0)
- default.qutrit (PennyLane-0.34.0.dev0)
- null.qubit (PennyLane-0.34.0.dev0)
- lightning.qubit (PennyLane-Lightning-0.33.1)
```


### Existing GitHub issues

- [X] I have searched existing GitHub issues to make sure the issue does not already exist.

Pull request:
implement map_wires for cond and measurementValue
**Context:**
While updating drawing for `qml.cond`, it was revealed that `Conditional.map_wires` does not work as expected. I figured it would be easiest to fix this in a follow-up PR, so here it is. A consequence of this is that the [teleportation tutorial broke](https://github.com/PennyLaneAI/qml/actions/runs/6992158970/job/19023455341), but I double-checked locally that it works as expected with this branch.

**Description of the Change:**
Implement map_wires for `MeasurementValue` and `Conditional`

**Benefits:**
`map_wires` exists for these objects, allowing the drawer to work as expected, and fixing the qml dev builds

**Possible Drawbacks:**
mapping a `Conditional` maps its `meas_val`, and that maps each measurement in its `measurements` property. This will preserve the `id`, as it should. But it might be a little strange. Example:

```python
with qml.queuing.AnnotatedQueue() as q:
    m_0 = qml.measure(0)
    qml.cond(m_0, qml.PauliX)(1)

m_0, cond_op = q.queue
mapped_cond = cond_op.map_wires({0: "a", 1: "b"})
[m_a] = mapped_cond.meas_val.measurements
print(m_0.wires, m_a.wires, m_0.id == m_a.id)
# <Wires = [0]> <Wires = ['a']> True
```

**Related GitHub Issues:**
Fixes #4883 [sc-50825]

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index ce0646a98..b4215f694 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -214,6 +214,9 @@
   style, but with sketch-style lines.
   [(#4880)](https://github.com/PennyLaneAI/pennylane/pull/4880)
 
+* `Conditional` and `MeasurementValue` objects now implement `map_wires`.
+  [(#4884)](https://github.com/PennyLaneAI/pennylane/pull/4884)
+
 <h3>Breaking changes 💔</h3>
 
 * The transforms submodule `qml.transforms.qcut` becomes its own module `qml.qcut`.
diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py
index a8f1a6358..721611a40 100644
--- a/pennylane/measurements/measurements.py
+++ b/pennylane/measurements/measurements.py
@@ -127,6 +127,8 @@ class MeasurementProcess(ABC):
             where the instance has to be identified
     """
 
+    # pylint:disable=too-many-instance-attributes
+
     def __init_subclass__(cls, **_):
         register_pytree(cls, cls._flatten, cls._unflatten)
 
@@ -469,10 +471,8 @@ class MeasurementProcess(ABC):
         """
         new_measurement = copy.copy(self)
         if self.mv is not None:
-            mv = copy.copy(self.mv)
-            mv.measurements = [m.map_wires(wire_map=wire_map) for m in mv.measurements]
-            new_measurement.mv = mv
-        if self.obs is not None:
+            new_measurement.mv = self.mv.map_wires(wire_map=wire_map)
+        elif self.obs is not None:
             new_measurement.obs = self.obs.map_wires(wire_map=wire_map)
         elif self._wires is not None:
             new_measurement._wires = Wires([wire_map.get(wire, wire) for wire in self.wires])
diff --git a/pennylane/measurements/mid_measure.py b/pennylane/measurements/mid_measure.py
index ae5734677..9becfac98 100644
--- a/pennylane/measurements/mid_measure.py
+++ b/pennylane/measurements/mid_measure.py
@@ -305,6 +305,19 @@ class MeasurementValue(Generic[T]):
             ret_dict[branch] = self.processing_fn(*branch)
         return ret_dict
 
+    def map_wires(self, wire_map):
+        """Returns a copy of the current ``MeasurementValue`` with the wires of each measurement changed
+        according to the given wire map.
+
+        Args:
+            wire_map (dict): dictionary containing the old wires as keys and the new wires as values
+
+        Returns:
+            MeasurementValue: new ``MeasurementValue`` instance with measurement wires mapped
+        """
+        mapped_measurements = [m.map_wires(wire_map) for m in self.measurements]
+        return MeasurementValue(mapped_measurements, self.processing_fn)
+
     def _transform_bin_op(self, base_bin, other):
         """Helper function for defining dunder binary operations."""
         if isinstance(other, MeasurementValue):
diff --git a/pennylane/ops/op_math/condition.py b/pennylane/ops/op_math/condition.py
index a18dc7a15..a1b10a961 100644
--- a/pennylane/ops/op_math/condition.py
+++ b/pennylane/ops/op_math/condition.py
@@ -55,6 +55,11 @@ class Conditional(Operation):
     def label(self, decimals=None, base_label=None, cache=None):
         return self.then_op.label(decimals=decimals, base_label=base_label, cache=cache)
 
+    def map_wires(self, wire_map):
+        meas_val = self.meas_val.map_wires(wire_map)
+        then_op = self.then_op.map_wires(wire_map)
+        return Conditional(meas_val, then_op=then_op)
+
 
 def cond(condition, true_fn, false_fn=None):
     """Condition a quantum operation on the results of mid-circuit qubit measurements.


Test patch:
diff --git a/tests/drawer/test_drawable_layers.py b/tests/drawer/test_drawable_layers.py
index d56149074..113f0130c 100644
--- a/tests/drawer/test_drawable_layers.py
+++ b/tests/drawer/test_drawable_layers.py
@@ -138,6 +138,26 @@ class TestDrawableLayers:
 
         assert layers == [[ops[0]], [ops[1]], [ops[2]]]
 
+    def test_mid_measure_custom_wires(self):
+        """Test that custom wires do not break the drawing of mid-circuit measurements."""
+
+        def teleport(state):
+            qml.QubitStateVector(state, wires=["A"])
+            qml.Hadamard(wires="a")
+            qml.CNOT(wires=["a", "B"])
+            qml.CNOT(wires=["A", "a"])
+            qml.Hadamard(wires="A")
+            m0 = qml.measure("A")
+            m1 = qml.measure("a")
+            qml.cond(m1, qml.PauliX)("B")
+            qml.cond(m0, qml.PauliZ)("B")
+
+        tape_custom = qml.tape.make_qscript(teleport)([0, 1])
+        [tape_standard], _ = qml.map_wires(tape_custom, {"A": 0, "a": 1, "B": 2})
+        ops = tape_standard.operations
+        layers = drawable_layers(ops)
+        assert layers == [ops[:2]] + [[op] for op in ops[2:]]
+
 
 class TestMidMeasure:
     """Tests the various changes from mid-circuit measurements."""
diff --git a/tests/measurements/test_mid_measure.py b/tests/measurements/test_mid_measure.py
index de192de51..935fba003 100644
--- a/tests/measurements/test_mid_measure.py
+++ b/tests/measurements/test_mid_measure.py
@@ -388,6 +388,14 @@ if m0=1,m1=0 => 1
 if m0=1,m1=1 => 2"""
         )
 
+    def test_map_wires(self):
+        """Test that map_wires works as expected."""
+        a = MeasurementValue([mp1], lambda v: v)
+        b = a.map_wires({0: "b"})
+        [new_meas] = b.measurements
+        assert new_meas.wires == Wires(["b"])
+        assert new_meas.id == mp1.id
+
 
 unary_dunders = ["__invert__"]
 
diff --git a/tests/ops/op_math/test_condition.py b/tests/ops/op_math/test_condition.py
index 020c51655..d8a88da63 100644
--- a/tests/ops/op_math/test_condition.py
+++ b/tests/ops/op_math/test_condition.py
@@ -190,10 +190,13 @@ class TestCond:
             m_0 = qml.measure(1)
             qml.cond(m_0, g, f)()  # Check that the same error is raised when f and g are swapped
 
+
+class TestAdditionalCond:
+    """Test additional/misc functionality relating to qml.cond"""
+
     @pytest.mark.parametrize("inp", [1, "string", qml.PauliZ(0)])
-    def test_cond_error_unrecognized_input(self, inp, terminal_measurement):
+    def test_cond_error_unrecognized_input(self, inp):
         """Test that an error is raised when the input is not recognized."""
-        # pylint: disable=unused-argument
 
         with pytest.raises(
             ConditionalTransformError,
@@ -202,6 +205,17 @@ class TestCond:
             m_0 = qml.measure(1)
             qml.cond(m_0, inp)()
 
+    def test_map_wires(self):
+        """Tests the cond.map_wires function."""
+        with qml.queuing.AnnotatedQueue() as q:
+            m_0 = qml.measure(0)
+            qml.cond(m_0, qml.PauliX)(1)
+
+        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")
+
 
 @pytest.mark.parametrize("op_class", [qml.PauliY, qml.Toffoli, qml.Hadamard, qml.CZ])
 def test_conditional_label(op_class):