Repository: PennyLaneAI/pennylane
Pull request #4888 (2023-11-27 23:08:52)

Support joint measurement statistics for mid-circuit measurements
**Context:**
Adding support for advanced statistics for mid-circuit measurements. Users can now collect statistics on multiple measurement values. This can be done in two ways:
1. Using arithmetic operators to collect measurement values into a composite value. Eg: `m0 * m1 - 2 * m3` or `m1 > m0` or `~m0 & m1`, etc. Available for `qml.expval`, `qml.var`, `qml.sample`, `qml.counts`.
2. Putting values in a list. This will give wire-like results. Eg: `[m0, m1, m2]`. Available for `qml.sample`, `qml.probs`, `qml.counts`.

**Composite vs single `MeasurementValue`s**:
Composite `MeasurementValue`s refers to `MeasurementValue`s created by applying arithmetic/boolean operators to `MeasurementValue`s as shown in the first point above. A single `MeasurementValue` is the output of `qml.measure`. Applying arithmetic/boolean operators to `MeasurementValue`s also returns `MeasurementValue`s.

**Description of the Change:**
* Add `__repr__` to `MeasurementValue` and update `MeasurementProcess.__repr__` appropriately.
* Update `MeasurementProcess.eigvals` to return outcomes of composed `MeasurementValue`s when in use. If a list of `MeasurementValue`s is given, the eigenvalues are just `range(2**num_wires)`.
* Update `QubitDevice` to use the correct eigenvalues for composite `MeasurementValue`s, and treat a list of `MeasurementValue`s as wires.
* Update `qml.expval` and `qml.var` to disallow using lists of `MeasurementValue`s, and allow composite MeasurementValue`s.
* Update `qml.sample` and `qml.counts` to allow both lists of `MeasurementValue`s as well as composite `MeasurementValue`s.
* Update `qml.defer_measurements` to disallow using `qml.probs` with composite `MeasurementValue`s. This is done because the wire order of the composite value is not deterministic as the sorting is determined by the ID of the mid-circuit measurements, which are generated randomly. There isn't really a way to go around this at the moment, so I just blocked users from being able to do it. Besides, the results should be the same as when they use a list of `MeasurementValue`s.

**TODO**: Add docs.

**Benefits:**
* Advanced mid-circuit statistics are now supported! And in multiple ways 😄 

**Possible Drawbacks:**

**Related GitHub Issues:**


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/4888?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
Attention: `22 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`b77f674`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/b77f67447541f2a816fe38e383536d3edb84edd7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.65% compared to head [(`caecd37`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) 99.58%.

| [Files](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) | Patch % | Lines |
|---|---|---|
| [pennylane/measurements/counts.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9jb3VudHMucHk=) | 33.33% | [4 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/measurements/expval.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9leHB2YWwucHk=) | 42.85% | [4 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/measurements/probs.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9wcm9icy5weQ==) | 20.00% | [4 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/measurements/sample.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9zYW1wbGUucHk=) | 20.00% | [4 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/measurements/var.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy92YXIucHk=) | 42.85% | [4 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) |
| [pennylane/measurements/mid\_measure.py](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI#diff-cGVubnlsYW5lL21lYXN1cmVtZW50cy9taWRfbWVhc3VyZS5weQ==) | 60.00% | [2 Missing :warning: ](https://app.codecov.io/gh/PennyLaneAI/pennylane/pull/4888?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    #4888      +/-   ##
==========================================
- Coverage   99.65%   99.58%   -0.08%     
==========================================
  Files         385      385              
  Lines       34896    34663     -233     
==========================================
- Hits        34776    34518     -258     
- Misses        120      145      +25     
```



</details>

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

mudit2812: [sc-48303]
mudit2812: Smol breaking change: `qml.counts` with a single `MeasurementValue` used to return strings for the keys, but now it returns integers. This is because of potentially having to process measurement values if unary operators are applied to it (eg. `~m0`). I'm not very worried about this though since mid-circuit measurement support for `qml.counts` has only been out for 1 release.

Patch:
diff --git a/doc/introduction/measurements.rst b/doc/introduction/measurements.rst
index 51e52f9c1..fc78a58d5 100644
--- a/doc/introduction/measurements.rst
+++ b/doc/introduction/measurements.rst
@@ -402,17 +402,71 @@ statistics are :class:`~.pennylane.devices.DefaultQubit`, :class:`~.pennylane.de
         qml.cond(m0, qml.RY)(y, wires=1)
         return qml.probs(wires=1), qml.probs(op=m0)
 
-Executing this QNode:
+Executing this ``QNode``:
 
 >>> func(np.pi / 2, np.pi / 4)
 (tensor([0.9267767, 0.0732233], requires_grad=True),
  tensor([0.5, 0.5], requires_grad=True))
 
+Users can also collect statistics on mid-circuit measurements manipulated using arithmetic/boolean operators.
+This works for both unary and binary operators. To see a full list of supported operators, refer to the
+:func:`~.pennylane.measure` documentation. An example for collecting such statistics is shown below:
+
+.. code-block:: python3
+
+    import pennylane as qml
+
+    dev = qml.device("default.qubit")
+
+    @qml.qnode(dev)
+    def circuit(phi, theta):
+        qml.RX(phi, wires=0)
+        m0 = qml.measure(wires=0)
+        qml.RY(theta, wires=1)
+        m1 = qml.measure(wires=1)
+        return qml.sample(~m0 - 2 * m1)
+
+Executing this ``QNode``:
+
+>>> circuit(1.23, 4.56, shots=5)
+array([-1, -2,  1, -1,  1])
+
+Collecting statistics for mid-circuit measurements manipulated using arithmetic/boolean operators is supported
+with ``qml.expval``, ``qml.var``, ``qml.sample``, and ``qml.counts``.
+
+Moreover, statistics for multiple mid-circuit measurements can be collected by passing lists of mid-circuit
+measurement values to the measurement process:
+
+.. code-block:: python3
+
+    import pennylane as qml
+
+    dev = qml.device("default.qubit")
+
+    @qml.qnode(dev)
+    def circuit(phi, theta):
+        qml.RX(phi, wires=0)
+        m0 = qml.measure(wires=0)
+        qml.RY(theta, wires=1)
+        m1 = qml.measure(wires=1)
+        return qml.sample([m0, m1])
+
+Executing this ``QNode``:
+
+>>> circuit(1.23, 4.56, shots=5)
+array([[0, 1],
+       [1, 1],
+       [0, 1],
+       [0, 0],
+       [1, 1]])
+
+Collecting statistics for sequences of mid-circuit measurements is supported with ``qml.sample``,
+``qml.probs``, and ``qml.counts``.
+
 .. warning::
 
-    Currently, statistics can only be collected for single mid-circuit measurement values. Moreover, any
-    measurement values manipulated using boolean or arithmetic operators cannot be used. These can lead to
-    unexpected/incorrect behaviour.
+    When collecting statistics for a list of mid-circuit measurements, values manipulated using
+    arithmetic operators should not be used as this behaviour is not supported.
 
 Changing the number of shots
 ----------------------------
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 1ad4a167a..ef71f1242 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -49,6 +49,62 @@
   
   <img src="https://docs.pennylane.ai/en/latest/_images/mid-circuit-measurement.png" width=70%/>
 
+* Users can now return statistics for multiple mid-circuit measurements.
+  [(#4888)](https://github.com/PennyLaneAI/pennylane/pull/4888)
+
+  There are two ways in which mid-circuit measurement statistics can be collected:
+
+  * By using arithmetic/binary operators. This can be through unary or binary operators as such:
+
+    ```python
+    import pennylane as qml
+
+    dev = qml.device("default.qubit")
+
+    @qml.qnode(dev)
+    def circuit(phi, theta):
+        qml.RX(phi, wires=0)
+        m0 = qml.measure(wires=0)
+        qml.RY(theta, wires=1)
+        m1 = qml.measure(wires=1)
+        return qml.expval(~m0 + m1)
+
+    print(circuit(1.23, 4.56))
+    ```
+    ```
+    1.2430187928114291
+    ```
+
+  * By using a list of mid-circuit measurement values:
+
+    ```python
+    import pennylane as qml
+
+    dev = qml.device("default.qubit")
+
+    @qml.qnode(dev)
+    def circuit(phi, theta):
+        qml.RX(phi, wires=0)
+        m0 = qml.measure(wires=0)
+        qml.RY(theta, wires=1)
+        m1 = qml.measure(wires=1)
+        return qml.sample([m0, m1])
+
+    print(circuit(1.23, 4.56, shots=5))
+    ```
+    ```
+    [[0 1]
+     [0 1]
+     [0 0]
+     [1 0]
+     [0 1]]
+    ```
+
+  This feature is supported on `default.qubit`, `default.qubit.legacy`, and `default.mixed`. To
+  learn more about which measurements and arithmetic operators are supported, refer to the
+  [Measurements](https://docs.pennylane.ai/en/stable/introduction/measurements.html) page and the
+  documentation for [`qml.measure`](https://docs.pennylane.ai/en/stable/code/api/pennylane.measure.html).
+
 <h4>Catalyst is seamlessly integrated with PennyLane ⚗️</h4>
 
 * Catalyst, our next-generation compilation framework, is now accessible within PennyLane,
diff --git a/pennylane/_qubit_device.py b/pennylane/_qubit_device.py
index a74bd8ae7..dcb1957c4 100644
--- a/pennylane/_qubit_device.py
+++ b/pennylane/_qubit_device.py
@@ -613,7 +613,12 @@ class QubitDevice(Device):
 
         for m in measurements:
             # TODO: Remove this when all overriden measurements support the `MeasurementProcess` class
-            obs = m.obs or m.mv or m
+            if isinstance(m.mv, list):
+                # MeasurementProcess stores information needed for processing if terminal measurement
+                # uses a list of mid-circuit measurement values
+                obs = m
+            else:
+                obs = m.obs or m.mv or m
             # Check if there is an overriden version of the measurement process
             if method := getattr(self, self.measurement_map[type(m)], False):
                 if isinstance(m, MeasurementTransform):
@@ -636,7 +641,7 @@ class QubitDevice(Device):
                 result = self.sample(m, shot_range=shot_range, bin_size=bin_size, counts=True)
 
             elif isinstance(m, ProbabilityMP):
-                result = self.probability(wires=obs.wires, shot_range=shot_range, bin_size=bin_size)
+                result = self.probability(wires=m.wires, shot_range=shot_range, bin_size=bin_size)
 
             elif isinstance(m, StateMP):
                 if len(measurements) > 1:
@@ -1314,9 +1319,10 @@ class QubitDevice(Device):
         if self.shots is None:
             try:
                 eigvals = self._asarray(
-                    observable.eigvals()
-                    if not isinstance(observable, MeasurementValue)
-                    else np.arange(2 ** len(observable.wires)),
+                    observable.eigvals() if not isinstance(observable, MeasurementValue)
+                    # Indexing a MeasurementValue gives the output of the processing function
+                    # for that index as a binary number.
+                    else [observable[i] for i in range(2 ** len(observable.measurements))],
                     dtype=self.R_DTYPE,
                 )
             except qml.operation.EigvalsUndefinedError as e:
@@ -1349,9 +1355,10 @@ class QubitDevice(Device):
         if self.shots is None:
             try:
                 eigvals = self._asarray(
-                    observable.eigvals()
-                    if not isinstance(observable, MeasurementValue)
-                    else np.arange(2 ** len(observable.wires)),
+                    observable.eigvals() if not isinstance(observable, MeasurementValue)
+                    # Indexing a MeasurementValue gives the output of the processing function
+                    # for that index as a binary number.
+                    else [observable[i] for i in range(2 ** len(observable.measurements))],
                     dtype=self.R_DTYPE,
                 )
             except qml.operation.EigvalsUndefinedError as e:
@@ -1425,7 +1432,7 @@ class QubitDevice(Device):
         batched_ndims = 2
         shape = samples.shape
 
-        if mp.obs is None and mp.mv is None:
+        if mp.obs is None and not isinstance(mp.mv, MeasurementValue):
             # convert samples and outcomes (if using) from arrays to str for dict keys
             samples = np.array([sample for sample in samples if not np.any(np.isnan(sample))])
             samples = qml.math.cast_like(samples, qml.math.int8(0))
@@ -1434,11 +1441,7 @@ class QubitDevice(Device):
             if mp.all_outcomes:
                 outcomes = list(map(_sample_to_str, self.generate_basis_states(num_wires)))
         elif mp.all_outcomes:
-            outcomes = (
-                qml.eigvals(mp.obs)
-                if mp.obs is not None
-                else np.arange(0, 2 ** len(mp.mv.wires), 1)
-            )
+            outcomes = mp.eigvals()
 
         batched = len(shape) == batched_ndims
         if not batched:
@@ -1481,12 +1484,13 @@ class QubitDevice(Device):
         if isinstance(mp, MeasurementProcess):
             if mp.obs is not None:
                 observable = mp.obs
-            elif mp.mv is not None:
+            elif mp.mv is not None and isinstance(mp.mv, MeasurementValue):
                 observable = mp.mv
             else:
                 no_observable_provided = True
 
-        # translate to wire labels used by device
+        # translate to wire labels used by device. observable is list when measuring sequence
+        # of multiple MeasurementValues
         device_wires = self.map_wires(observable.wires)
         name = observable.name
         # Select the samples from self._samples that correspond to ``shot_range`` if provided
@@ -1518,7 +1522,11 @@ class QubitDevice(Device):
             indices = samples @ powers_of_two
             indices = np.array(indices)  # Add np.array here for Jax support.
             if isinstance(observable, MeasurementValue):
-                samples = np.arange(0, 2 ** len(observable.wires), 1)[indices]
+                eigvals = self._asarray(
+                    [observable[i] for i in range(2 ** len(observable.measurements))],
+                    dtype=self.R_DTYPE,
+                )
+                samples = eigvals[indices]
             else:
                 try:
                     samples = observable.eigvals()[indices]
diff --git a/pennylane/measurements/counts.py b/pennylane/measurements/counts.py
index 6c7b0de36..66720bff7 100644
--- a/pennylane/measurements/counts.py
+++ b/pennylane/measurements/counts.py
@@ -136,6 +136,16 @@ def counts(op=None, wires=None, all_outcomes=False) -> "CountsMP":
     if isinstance(op, MeasurementValue):
         return CountsMP(obs=op, all_outcomes=all_outcomes)
 
+    if isinstance(op, Sequence):
+        if not all(isinstance(o, MeasurementValue) and len(o.measurements) == 1 for o in op):
+            raise qml.QuantumFunctionError(
+                "Only sequences of single MeasurementValues can be passed with the op argument. "
+                "MeasurementValues manipulated using arithmetic operators cannot be used when "
+                "collecting statistics for a sequence of mid-circuit measurements."
+            )
+
+        return CountsMP(obs=op, all_outcomes=all_outcomes)
+
     if op is not None and not op.is_hermitian:  # None type is also allowed for op
         warnings.warn(f"{op.name} might not be hermitian.")
 
@@ -187,12 +197,14 @@ class CountsMP(SampleMeasurement):
         return (self.obs or self.mv, self._eigvals), metadata
 
     def __repr__(self):
-        if self.obs is None:
-            if self._eigvals is None:
-                return f"CountsMP(wires={self.wires.tolist()}, all_outcomes={self.all_outcomes})"
+        if self.mv:
+            return f"CountsMP({repr(self.mv)}, all_outcomes={self.all_outcomes})"
+        if self.obs:
+            return f"CountsMP({self.obs}, all_outcomes={self.all_outcomes})"
+        if self._eigvals is not None:
             return f"CountsMP(eigvals={self._eigvals}, wires={self.wires.tolist()}, all_outcomes={self.all_outcomes})"
 
-        return f"CountsMP({self.obs}, all_outcomes={self.all_outcomes})"
+        return f"CountsMP(wires={self.wires.tolist()}, all_outcomes={self.all_outcomes})"
 
     @property
     def hash(self):
@@ -219,14 +231,9 @@ class CountsMP(SampleMeasurement):
         bin_size: int = None,
     ):
         with qml.queuing.QueuingManager.stop_recording():
-            if self.mv is not None:
-                samples = qml.sample(wires=self.mv.wires).process_samples(
-                    samples, wire_order, shot_range, bin_size
-                )
-            else:
-                samples = qml.sample(op=self.obs, wires=self._wires).process_samples(
-                    samples, wire_order, shot_range, bin_size
-                )
+            samples = qml.sample(op=self.obs or self.mv, wires=self._wires).process_samples(
+                samples, wire_order, shot_range, bin_size
+            )
 
         if bin_size is None:
             return self._samples_to_counts(samples)
@@ -234,7 +241,7 @@ class CountsMP(SampleMeasurement):
         num_wires = len(self.wires) if self.wires else len(wire_order)
         samples = (
             samples.reshape((num_wires, -1)).T.reshape(-1, bin_size, num_wires)
-            if self.obs is None
+            if self.obs is None and not isinstance(self.mv, MeasurementValue)
             else samples.reshape((-1, bin_size))
         )
 
@@ -292,7 +299,7 @@ class CountsMP(SampleMeasurement):
         batched_ndims = 2
         shape = qml.math.shape(samples)
 
-        if self.obs is None:
+        if self.obs is None and not isinstance(self.mv, MeasurementValue):
             # convert samples and outcomes (if using) from arrays to str for dict keys
             samples = qml.math.array(
                 [sample for sample in samples if not qml.math.any(qml.math.isnan(sample))]
@@ -306,7 +313,9 @@ class CountsMP(SampleMeasurement):
                     map(_sample_to_str, qml.QubitDevice.generate_basis_states(num_wires))
                 )
         elif self.all_outcomes:
-            outcomes = qml.eigvals(self.obs)
+            # This also covers statistics for mid-circuit measurements manipulated using
+            # arithmetic operators
+            outcomes = self.eigvals()
 
         batched = len(shape) == batched_ndims
         if not batched:
diff --git a/pennylane/measurements/expval.py b/pennylane/measurements/expval.py
index 7a31c3248..3f935dc1a 100644
--- a/pennylane/measurements/expval.py
+++ b/pennylane/measurements/expval.py
@@ -58,6 +58,11 @@ def expval(op: Union[Operator, MeasurementValue]):
     if isinstance(op, MeasurementValue):
         return ExpectationMP(obs=op)
 
+    if isinstance(op, Sequence):
+        raise ValueError(
+            "qml.expval does not support measuring sequences of measurements or observables"
+        )
+
     if not op.is_hermitian:
         warnings.warn(f"{op.name} might not be hermitian.")
 
@@ -133,7 +138,11 @@ class ExpectationMP(SampleMeasurement, StateMeasurement):
                     state=state, wire_order=wire_order
                 )
             return probs[idx]
+
+        # This also covers statistics for mid-circuit measurements manipulated using
+        # arithmetic operators
         eigvals = qml.math.asarray(self.eigvals(), dtype="float64")
+
         # we use ``self.wires`` instead of ``self.obs`` because the observable was
         # already applied to the state
         with qml.queuing.QueuingManager.stop_recording():
diff --git a/pennylane/measurements/measurements.py b/pennylane/measurements/measurements.py
index d6fb8d755..1fe2699b3 100644
--- a/pennylane/measurements/measurements.py
+++ b/pennylane/measurements/measurements.py
@@ -20,10 +20,10 @@ import copy
 import functools
 from abc import ABC, abstractmethod
 from enum import Enum
-from typing import Sequence, Tuple, Optional
+from typing import Sequence, Tuple, Optional, Union
 
 import pennylane as qml
-from pennylane.operation import Operator
+from pennylane.operation import Operator, DecompositionUndefinedError, EigvalsUndefinedError
 from pennylane.pytrees import register_pytree
 from pennylane.typing import TensorLike
 from pennylane.wires import Wires
@@ -116,9 +116,9 @@ class MeasurementProcess(ABC):
     quantum variational circuit.
 
     Args:
-        obs (Union[.Operator, .MeasurementValue]): The observable that is to be measured as part of the
-            measurement process. Not all measurement processes require observables (for
-            example ``Probability``); this argument is optional.
+        obs (Union[.Operator, .MeasurementValue, Sequence[.MeasurementValue]]): The observable that
+            is to be measured as part of the measurement process. Not all measurement processes
+            require observables (for example ``Probability``); this argument is optional.
         wires (.Wires): The wires the measurement process applies to.
             This can only be specified if an observable was not provided.
         eigvals (array): A flat array representing the eigenvalues of the measurement.
@@ -147,13 +147,20 @@ class MeasurementProcess(ABC):
     # pylint: disable=too-many-arguments
     def __init__(
         self,
-        obs: Optional[Operator] = None,
+        obs: Optional[
+            Union[
+                Operator,
+                "qml.measurements.MeasurementValue",
+                Sequence["qml.measurements.MeasurementValue"],
+            ]
+        ] = None,
         wires: Optional[Wires] = None,
         eigvals: Optional[TensorLike] = None,
         id: Optional[str] = None,
     ):
-        if obs is not None and obs.name == "MeasurementValue":
-            self.mv = obs
+        if getattr(obs, "name", None) == "MeasurementValue" or isinstance(obs, Sequence):
+            # Cast sequence of measurement values to list
+            self.mv = obs if getattr(obs, "name", None) == "MeasurementValue" else list(obs)
             self.obs = None
         else:
             self.obs = obs
@@ -287,13 +294,15 @@ class MeasurementProcess(ABC):
 
     def __repr__(self):
         """Representation of this class."""
-        if self.obs is None:
-            if self._eigvals is None:
-                return f"{self.return_type.value}(wires={self.wires.tolist()})"
+        if self.mv:
+            return f"{self.return_type.value}({repr(self.mv)})"
+        if self.obs:
+            return f"{self.return_type.value}({self.obs})"
+        if self._eigvals is not None:
             return f"{self.return_type.value}(eigvals={self._eigvals}, wires={self.wires.tolist()})"
 
         # Todo: when tape is core the return type will always be taken from the MeasurementProcess
-        return f"{self.return_type.value}({self.obs})"
+        return f"{self.return_type.value}(wires={self.wires.tolist()})"
 
     def __copy__(self):
         cls = self.__class__
@@ -314,6 +323,8 @@ class MeasurementProcess(ABC):
         This is the union of all the Wires objects of the measurement.
         """
         if self.mv is not None:
+            if isinstance(self.mv, list):
+                return qml.wires.Wires.all_wires([m.wires for m in self.mv])
             return self.mv.wires
 
         if self.obs is not None:
@@ -356,10 +367,17 @@ class MeasurementProcess(ABC):
             array: eigvals representation
         """
         if self.mv is not None:
+            if getattr(self.mv, "name", None) == "MeasurementValue":
+                # Indexing a MeasurementValue gives the output of the processing function
+                # for the binary number corresponding to the index.
+                return qml.math.asarray([self.mv[i] for i in range(2 ** len(self.wires))])
             return qml.math.arange(0, 2 ** len(self.wires), 1)
 
         if self.obs is not None:
-            return self.obs.eigvals()
+            try:
+                return qml.eigvals(self.obs)
+            except DecompositionUndefinedError as e:
+                raise EigvalsUndefinedError from e
         return self._eigvals
 
     @property
@@ -471,7 +489,11 @@ class MeasurementProcess(ABC):
         """
         new_measurement = copy.copy(self)
         if self.mv is not None:
-            new_measurement.mv = self.mv.map_wires(wire_map=wire_map)
+            new_measurement.mv = (
+                self.mv.map_wires(wire_map=wire_map)
+                if getattr(self.mv, "name", None) == "MeasurementValue"
+                else [m.map_wires(wire_map=wire_map) for m in self.mv]
+            )
         elif self.obs is not None:
             new_measurement.obs = self.obs.map_wires(wire_map=wire_map)
         elif self._wires is not None:
diff --git a/pennylane/measurements/mid_measure.py b/pennylane/measurements/mid_measure.py
index f272bb583..1006ad40b 100644
--- a/pennylane/measurements/mid_measure.py
+++ b/pennylane/measurements/mid_measure.py
@@ -300,7 +300,7 @@ class MeasurementValue(Generic[T]):
     @property
     def wires(self):
         """Returns a list of wires corresponding to the mid-circuit measurements."""
-        return Wires([m.wires[0] for m in self.measurements])
+        return Wires.all_wires([m.wires for m in self.measurements])
 
     @property
     def branches(self):
@@ -423,3 +423,6 @@ class MeasurementValue(Generic[T]):
                 "if " + ",".join(id_branch_mapping) + " => " + str(self.processing_fn(*branch))
             )
         return "\n".join(lines)
+
+    def __repr__(self):
+        return f"MeasurementValue(wires={self.wires.tolist()})"
diff --git a/pennylane/measurements/probs.py b/pennylane/measurements/probs.py
index ae1dc5315..20ef2a02f 100644
--- a/pennylane/measurements/probs.py
+++ b/pennylane/measurements/probs.py
@@ -93,6 +93,22 @@ def probs(wires=None, op=None) -> "ProbabilityMP":
     the device simulates qubit or continuous variable quantum systems.
     """
     if isinstance(op, MeasurementValue):
+        if len(op.measurements) > 1:
+            raise ValueError(
+                "Cannot use qml.probs() when measuring multiple mid-circuit measurements collected "
+                "using arithmetic operators. To collect probabilities for multiple mid-circuit "
+                "measurements, use a list of mid-circuit measurements with qml.probs()."
+            )
+        return ProbabilityMP(obs=op)
+
+    if isinstance(op, Sequence):
+        if not all(isinstance(o, MeasurementValue) and len(o.measurements) == 1 for o in op):
+            raise qml.QuantumFunctionError(
+                "Only sequences of single MeasurementValues can be passed with the op argument. "
+                "MeasurementValues manipulated using arithmetic operators cannot be used when "
+                "collecting statistics for a sequence of mid-circuit measurements."
+            )
+
         return ProbabilityMP(obs=op)
 
     if isinstance(op, qml.Hamiltonian):
diff --git a/pennylane/measurements/sample.py b/pennylane/measurements/sample.py
index 048ba97e2..0e7b02645 100644
--- a/pennylane/measurements/sample.py
+++ b/pennylane/measurements/sample.py
@@ -106,6 +106,16 @@ def sample(op: Optional[Union[Operator, MeasurementValue]] = None, wires=None) -
     if isinstance(op, MeasurementValue):
         return SampleMP(obs=op)
 
+    if isinstance(op, Sequence):
+        if not all(isinstance(o, MeasurementValue) and len(o.measurements) == 1 for o in op):
+            raise qml.QuantumFunctionError(
+                "Only sequences of single MeasurementValues can be passed with the op argument. "
+                "MeasurementValues manipulated using arithmetic operators cannot be used when "
+                "collecting statistics for a sequence of mid-circuit measurements."
+            )
+
+        return SampleMP(obs=op)
+
     if op is not None and not op.is_hermitian:  # None type is also allowed for op
         warnings.warn(f"{op.name} might not be hermitian.")
 
@@ -205,10 +215,12 @@ class SampleMP(SampleMeasurement):
 
         num_wires = samples.shape[-1]  # wires is the last dimension
 
-        if self.obs is None and self.mv is None:
+        # If we're sampling wires or a list of mid-circuit measurements
+        if self.obs is None and not isinstance(self.mv, MeasurementValue):
             # if no observable was provided then return the raw samples
             return samples if bin_size is None else samples.T.reshape(num_wires, bin_size, -1)
 
+        # If we're sampling observables
         if str(name) in {"PauliX", "PauliY", "PauliZ", "Hadamard"}:
             # Process samples for observables with eigenvalues {1, -1}
             samples = 1 - 2 * qml.math.squeeze(samples, axis=-1)
@@ -219,6 +231,8 @@ class SampleMP(SampleMeasurement):
             indices = samples @ powers_of_two
             indices = qml.math.array(indices)  # Add np.array here for Jax support.
             try:
+                # This also covers statistics for mid-circuit measurements manipulated using
+                # arithmetic operators
                 samples = self.eigvals()[indices]
             except qml.operation.EigvalsUndefinedError as e:
                 # if observable has no info on eigenvalues, we cannot return this measurement
diff --git a/pennylane/measurements/var.py b/pennylane/measurements/var.py
index 8597e436a..131e153ab 100644
--- a/pennylane/measurements/var.py
+++ b/pennylane/measurements/var.py
@@ -59,6 +59,11 @@ def var(op: Union[Operator, MeasurementValue]) -> "VarianceMP":
     if isinstance(op, MeasurementValue):
         return VarianceMP(obs=op)
 
+    if isinstance(op, Sequence):
+        raise ValueError(
+            "qml.var does not support measuring sequences of measurements or observables"
+        )
+
     if not op.is_hermitian:
         warnings.warn(f"{op.name} might not be hermitian.")
     return VarianceMP(obs=op)
@@ -137,6 +142,8 @@ class VarianceMP(SampleMeasurement, StateMeasurement):
                 )
             return probs[idx] - probs[idx] ** 2
 
+        # This also covers statistics for mid-circuit measurements manipulated using
+        # arithmetic operators
         eigvals = qml.math.asarray(self.eigvals(), dtype="float64")
 
         # we use ``wires`` instead of ``op`` because the observable was
diff --git a/pennylane/ops/functions/equal.py b/pennylane/ops/functions/equal.py
index 34a1764e0..3fe4ceced 100644
--- a/pennylane/ops/functions/equal.py
+++ b/pennylane/ops/functions/equal.py
@@ -22,7 +22,7 @@ from typing import Union
 import pennylane as qml
 from pennylane.measurements import MeasurementProcess
 from pennylane.measurements.classical_shadow import ShadowExpvalMP
-from pennylane.measurements.mid_measure import MidMeasureMP
+from pennylane.measurements.mid_measure import MidMeasureMP, MeasurementValue
 from pennylane.measurements.mutual_info import MutualInfoMP
 from pennylane.measurements.vn_entropy import VnEntropyMP
 from pennylane.measurements.counts import CountsMP
@@ -418,6 +418,17 @@ def _equal_measurements(
             atol=atol,
         )
 
+    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
+
+        if isinstance(op1.mv, Iterable) and isinstance(op2.mv, Iterable):
+            if len(op1.mv) == len(op2.mv):
+                return all(mv1.measurements == mv2.measurements for mv1, mv2 in zip(op1.mv, op2.mv))
+
+        return False
+
     if op1.wires != op2.wires:
         return False
 
@@ -433,15 +444,7 @@ def _equal_measurements(
 
 
 @_equal.register
-# pylint: disable=unused-argument
-def _equal_mid_measure(
-    op1: MidMeasureMP,
-    op2: MidMeasureMP,
-    check_interface=True,
-    check_trainability=True,
-    rtol=1e-5,
-    atol=1e-9,
-):
+def _equal_mid_measure(op1: MidMeasureMP, op2: MidMeasureMP, **_):
     return (
         op1.wires == op2.wires
         and op1.id == op2.id
diff --git a/pennylane/transforms/defer_measurements.py b/pennylane/transforms/defer_measurements.py
index aebe49ccc..068c7afbd 100644
--- a/pennylane/transforms/defer_measurements.py
+++ b/pennylane/transforms/defer_measurements.py
@@ -14,7 +14,7 @@
 """Code for the tape transform implementing the deferred measurement principle."""
 from typing import Sequence, Callable
 import pennylane as qml
-from pennylane.measurements import MidMeasureMP, ProbabilityMP, SampleMP, CountsMP
+from pennylane.measurements import MidMeasureMP, ProbabilityMP, SampleMP, CountsMP, MeasurementValue
 from pennylane.ops.op_math import ctrl
 
 from pennylane.tape import QuantumTape
@@ -143,7 +143,8 @@ def defer_measurements(tape: QuantumTape, **kwargs) -> (Sequence[QuantumTape], C
         tape (QNode or QuantumTape or Callable): a quantum circuit.
 
     Returns:
-        qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The transformed circuit as described in :func:`qml.transform <pennylane.transform>`.
+        qnode (QNode) or quantum function (Callable) or tuple[List[QuantumTape], function]: The
+        transformed circuit as described in :func:`qml.transform <pennylane.transform>`.
 
     **Example**
 
@@ -248,8 +249,9 @@ def defer_measurements(tape: QuantumTape, **kwargs) -> (Sequence[QuantumTape], C
                         if op.postselect is None:
                             new_operations.append(qml.CNOT([cur_wire, op.wires[0]]))
                         elif op.postselect == 1:
-                            # We know that the measured wire will be in the |1> state if postselected
-                            # |1>. So we can just apply a PauliX instead of a CNOT to reset
+                            # We know that the measured wire will be in the |1> state if
+                            # postselected |1>. So we can just apply a PauliX instead of
+                            # a CNOT to reset
                             new_operations.append(qml.PauliX(op.wires[0]))
 
                 cur_wire += 1
@@ -266,10 +268,33 @@ def defer_measurements(tape: QuantumTape, **kwargs) -> (Sequence[QuantumTape], C
 
     for mp in tape.measurements:
         if mp.mv is not None:
-            # Update measurement value wires
-            wire_map = {m.wires[0]: control_wires[m.id] for m in mp.mv.measurements}
-            mp = qml.map_wires(mp, wire_map=wire_map)
-        new_measurements.append(mp)
+            # Update measurement value wires. We can't use `qml.map_wires` because the same
+            # wire can map to different control wires when multiple mid-circuit measurements
+            # are made on the same wire. This mapping is determined by the id of the
+            # MidMeasureMPs. Thus, we need to manually map wires for each MidMeasureMP.
+            if isinstance(mp.mv, MeasurementValue):
+                new_ms = [
+                    qml.map_wires(m, {m.wires[0]: control_wires[m.id]}) for m in mp.mv.measurements
+                ]
+                new_m = MeasurementValue(new_ms, mp.mv.processing_fn)
+            else:
+                new_m = []
+                for val in mp.mv:
+                    new_ms = [
+                        qml.map_wires(m, {m.wires[0]: control_wires[m.id]})
+                        for m in val.measurements
+                    ]
+                    new_m.append(MeasurementValue(new_ms, val.processing_fn))
+
+            with QueuingManager.stop_recording():
+                new_mp = (
+                    type(mp)(obs=new_m)
+                    if not isinstance(mp, CountsMP)
+                    else CountsMP(obs=new_m, all_outcomes=mp.all_outcomes)
+                )
+        else:
+            new_mp = mp
+        new_measurements.append(new_mp)
 
     new_tape = type(tape)(new_operations, new_measurements, shots=tape.shots)
 


Test patch:
diff --git a/tests/measurements/legacy/test_counts_legacy.py b/tests/measurements/legacy/test_counts_legacy.py
index df07aa4fa..106e6c6df 100644
--- a/tests/measurements/legacy/test_counts_legacy.py
+++ b/tests/measurements/legacy/test_counts_legacy.py
@@ -173,10 +173,11 @@ class TestCountsIntegration:
 
     @pytest.mark.parametrize("shots", [1000, [1000, 1000]])
     @pytest.mark.parametrize("phi", np.arange(np.pi / 4, 2 * np.pi, np.pi / 2))
-    def test_observable_is_measurement_value(self, shots, phi):
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_measurement_value(self, shots, phi, device_name):
         """Test that counts for mid-circuit measurement values
         are correct for a single measurement value."""
-        dev = qml.device("default.qubit.legacy", wires=2, shots=shots)
+        dev = qml.device(device_name, wires=2, shots=shots)
 
         @qml.qnode(dev)
         def circuit(phi):
@@ -199,11 +200,73 @@ class TestCountsIntegration:
             assert len(res) == 2
             assert set(res.keys()) == {0, 1}
 
+    @pytest.mark.parametrize("shots", [1000, [1000, 1000]])
+    @pytest.mark.parametrize("phi", np.arange(np.pi / 4, 2 * np.pi, np.pi / 2))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_composite_measurement_value(self, shots, phi, device_name):
+        """Test that counts for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        dev = qml.device(device_name, wires=4, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(0.5 * phi, 1)
+            m1 = qml.measure(1)
+            return qml.counts(m0 + m1)
+
+        res = circuit(phi)
+        if isinstance(shots, list):
+            assert isinstance(res, tuple)
+            assert len(res) == 2
+
+            for r in res:
+                assert isinstance(r, dict)
+                assert len(r) == 3
+                assert set(r.keys()) == {0, 1, 2}
+
+        else:
+            assert isinstance(res, dict)
+            assert len(res) == 3
+            assert set(res.keys()) == {0, 1, 2}
+
+    @pytest.mark.parametrize("shots", [1000, [1000, 1000]])
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_measurement_value_list(self, shots, device_name):
+        """Test that counts for mid-circuit measurement values
+        are correct for a list of measurement values."""
+        dev = qml.device(device_name, wires=4, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit():
+            qml.Hadamard(0)
+            m0 = qml.measure(0)
+            qml.Hadamard(1)
+            m1 = qml.measure(1)
+            return qml.counts([m0, m1])
+
+        res = circuit()
+        if isinstance(shots, list):
+            assert isinstance(res, tuple)
+            assert len(res) == 2
+
+            for r in res:
+                assert isinstance(r, dict)
+                assert len(r) == 4
+                assert set(r.keys()) == {"00", "01", "10", "11"}
+
+        else:
+            assert isinstance(res, dict)
+            assert len(res) == 4
+            assert set(res.keys()) == {"00", "01", "10", "11"}
+
     @pytest.mark.parametrize("shots", [5, [5, 5]])
-    def test_observable_is_measurement_value_all_outcomes(self, shots):
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_measurement_value_all_outcomes(self, shots, device_name):
         """Test that counts for mid-circuit measurement values
         are correct for a single measurement value."""
-        dev = qml.device("default.qubit.legacy", wires=2, shots=shots)
+        dev = qml.device(device_name, wires=2, shots=shots)
 
         @qml.qnode(dev)
         def circuit():
@@ -212,7 +275,6 @@ class TestCountsIntegration:
             return qml.counts(m0, all_outcomes=True)
 
         res = circuit()
-        print(res)
         if isinstance(shots, list):
             assert isinstance(res, tuple)
             assert len(res) == 2
@@ -231,6 +293,80 @@ class TestCountsIntegration:
             assert res[0] == 0
             assert res[1] == 5
 
+    @pytest.mark.parametrize("shots", [5, [5, 5]])
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_composite_measurement_value_all_outcomes(self, shots, device_name):
+        """Test that counts for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        dev = qml.device(device_name, wires=4, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit():
+            qml.PauliX(0)
+            m0 = qml.measure(0)
+            qml.PauliX(1)
+            m1 = qml.measure(1)
+            return qml.counts(m0 + m1, all_outcomes=True)
+
+        res = circuit()
+        if isinstance(shots, list):
+            assert isinstance(res, tuple)
+            assert len(res) == 2
+
+            for r in res:
+                assert isinstance(r, dict)
+                assert len(r) == 3
+                assert set(r.keys()) == {0, 1, 2}
+                assert r[0] == 0
+                assert r[1] == 0
+                assert r[2] == 5
+
+        else:
+            assert isinstance(res, dict)
+            assert len(res) == 3
+            assert set(res.keys()) == {0, 1, 2}
+            assert res[0] == 0
+            assert res[1] == 0
+            assert res[2] == 5
+
+    @pytest.mark.parametrize("shots", [5, [5, 5]])
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_measurement_value_list_all_outcomes(self, shots, device_name):
+        """Test that counts for mid-circuit measurement values
+        are correct for a list of measurement values."""
+        dev = qml.device(device_name, wires=4, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit():
+            qml.PauliX(0)
+            m0 = qml.measure(0)
+            qml.PauliX(1)
+            m1 = qml.measure(1)
+            return qml.counts([m0, m1], all_outcomes=True)
+
+        res = circuit()
+        if isinstance(shots, list):
+            assert isinstance(res, tuple)
+            assert len(res) == 2
+
+            for r in res:
+                assert isinstance(r, dict)
+                assert len(r) == 4
+                assert set(r.keys()) == {"00", "01", "10", "11"}
+                assert r["00"] == 0
+                assert r["01"] == 0
+                assert r["10"] == 0
+                assert r["11"] == 5
+
+        else:
+            assert isinstance(res, dict)
+            assert len(res) == 4
+            assert set(res.keys()) == {"00", "01", "10", "11"}
+            assert res["00"] == 0
+            assert res["01"] == 0
+            assert res["10"] == 0
+            assert res["11"] == 5
+
     def test_providing_no_observable_and_no_wires_counts(self, mocker):
         """Test that we can provide no observable and no wires to sample function"""
         dev = qml.device("default.qubit.legacy", wires=2, shots=1000)
diff --git a/tests/measurements/legacy/test_expval_legacy.py b/tests/measurements/legacy/test_expval_legacy.py
index 08a5610d3..995551c55 100644
--- a/tests/measurements/legacy/test_expval_legacy.py
+++ b/tests/measurements/legacy/test_expval_legacy.py
@@ -117,12 +117,14 @@ class TestExpval:
 
     @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
     @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
     def test_observable_is_measurement_value(
-        self, shots, phi, mocker, tol, tol_stochastic
+        self, shots, phi, mocker, tol, tol_stochastic, device_name
     ):  # pylint: disable=too-many-arguments
         """Test that expectation values for mid-circuit measurement values
         are correct for a single measurement value."""
-        dev = qml.device("default.qubit.legacy", wires=2, shots=shots)
+        np.random.seed(42)
+        dev = qml.device(device_name, wires=2, shots=shots)
 
         @qml.qnode(dev)
         def circuit(phi):
@@ -137,7 +139,55 @@ class TestExpval:
 
         atol = tol if shots is None else tol_stochastic
         assert np.allclose(np.array(res), np.sin(phi / 2) ** 2, atol=atol, rtol=0)
-        custom_measurement_process(new_dev, spy)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
+
+    @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_composite_measurement_value(
+        self, shots, phi, mocker, tol, tol_stochastic, device_name
+    ):  # pylint: disable=too-many-arguments
+        """Test that expectation values for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        np.random.seed(42)
+        dev = qml.device(device_name, wires=6, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(0.5 * phi, 1)
+            m1 = qml.measure(1)
+            qml.RX(2 * phi, 2)
+            m2 = qml.measure(2)
+            return qml.expval(m0 * m1 + m2)
+
+        new_dev = circuit.device
+        spy = mocker.spy(qml.QubitDevice, "expval")
+
+        res = circuit(phi, shots=shots)
+
+        @qml.qnode(dev)
+        def expected_circuit(phi):
+            qml.RX(phi, 0)
+            qml.RX(0.5 * phi, 1)
+            qml.RX(2 * phi, 2)
+            return (
+                qml.expval(qml.Projector([1], 0)),
+                qml.expval(qml.Projector([1], 1)),
+                qml.expval(qml.Projector([1], 2)),
+            )
+
+        evals = expected_circuit(phi, shots=None)
+        expected = evals[0] * evals[1] + evals[2]
+
+        atol = tol if shots is None else tol_stochastic
+        assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
 
     @pytest.mark.parametrize(
         "obs",
diff --git a/tests/measurements/legacy/test_probs_legacy.py b/tests/measurements/legacy/test_probs_legacy.py
index 8742a737c..ca67aaff8 100644
--- a/tests/measurements/legacy/test_probs_legacy.py
+++ b/tests/measurements/legacy/test_probs_legacy.py
@@ -175,12 +175,14 @@ class TestProbs:
 
     @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
     @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
     def test_observable_is_measurement_value(
-        self, shots, phi, mocker, tol, tol_stochastic
+        self, shots, phi, mocker, tol, tol_stochastic, device_name
     ):  # pylint: disable=too-many-arguments
         """Test that probs for mid-circuit measurement values
         are correct for a single measurement value."""
-        dev = qml.device("default.qubit.legacy", wires=2, shots=shots)
+        np.random.seed(42)
+        dev = qml.device(device_name, wires=2, shots=shots)
 
         @qml.qnode(dev)
         def circuit(phi):
@@ -202,7 +204,54 @@ class TestProbs:
             for r in res:  # pylint: disable=not-an-iterable
                 assert np.allclose(r, expected, atol=atol, rtol=0)
 
-        custom_measurement_process(new_dev, spy)
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
+
+    @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_measurement_value_list(
+        self, shots, phi, mocker, tol, tol_stochastic, device_name
+    ):  # pylint: disable=too-many-arguments
+        """Test that probs for mid-circuit measurement values
+        are correct for a list of measurement value."""
+        np.random.seed(42)
+        dev = qml.device(device_name, wires=6, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(0.5 * phi, 1)
+            m1 = qml.measure(1)
+            qml.RX(2.0 * phi, 2)
+            m2 = qml.measure(2)
+            return qml.probs(op=[m0, m1, m2])
+
+        new_dev = circuit.device
+        spy = mocker.spy(qml.QubitDevice, "probability")
+
+        res = circuit(phi, shots=shots)
+
+        @qml.qnode(dev)
+        def expected_circuit(phi):
+            qml.RX(phi, 0)
+            qml.RX(0.5 * phi, 1)
+            qml.RX(2.0 * phi, 2)
+            return qml.probs(wires=[0, 1, 2])
+
+        expected = expected_circuit(phi)
+
+        atol = tol if shots is None else tol_stochastic
+
+        if not isinstance(shots, list):
+            assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
+        else:
+            for r in res:  # pylint: disable=not-an-iterable
+                assert np.allclose(r, expected, atol=atol, rtol=0)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
 
     def test_integration(self, tol, mocker):
         """Test the probability is correct for a known state preparation."""
diff --git a/tests/measurements/legacy/test_sample_legacy.py b/tests/measurements/legacy/test_sample_legacy.py
index baea22162..495fdbaa9 100644
--- a/tests/measurements/legacy/test_sample_legacy.py
+++ b/tests/measurements/legacy/test_sample_legacy.py
@@ -97,10 +97,11 @@ class TestSample:
 
     @pytest.mark.parametrize("shots", [5, [5, 5]])
     @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 2))
-    def test_observable_is_measurement_value(self, shots, phi, mocker):
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_measurement_value(self, shots, phi, mocker, device_name):
         """Test that samples for mid-circuit measurement values
         are correct for a single measurement value."""
-        dev = qml.device("default.qubit.legacy", wires=2, shots=shots)
+        dev = qml.device(device_name, wires=2, shots=shots)
 
         @qml.qnode(dev)
         def circuit(phi):
@@ -118,7 +119,68 @@ class TestSample:
             assert all(r.shape == (s,) for r, s in zip(res, shots))
         else:
             assert res.shape == (shots,)
-        custom_measurement_process(new_dev, spy)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
+
+    @pytest.mark.parametrize("shots", [5, [5, 5]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 2))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_composite_measurement_value(self, shots, phi, mocker, device_name):
+        """Test that samples for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        dev = qml.device(device_name, wires=4, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(phi, 1)
+            m1 = qml.measure(1)
+            return qml.sample(op=m0 + m1)
+
+        new_dev = circuit.device
+        spy = mocker.spy(qml.QubitDevice, "sample")
+
+        res = circuit(phi)
+
+        if isinstance(shots, list):
+            assert len(res) == len(shots)
+            assert all(r.shape == (s,) for r, s in zip(res, shots))
+        else:
+            assert res.shape == (shots,)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
+
+    @pytest.mark.parametrize("shots", [5, [5, 5]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 2))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_measurement_value_list(self, shots, phi, mocker, device_name):
+        """Test that samples for mid-circuit measurement values
+        are correct for a measurement value list."""
+        dev = qml.device(device_name, wires=4, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            m1 = qml.measure(1)
+            return qml.sample(op=[m0, m1])
+
+        new_dev = circuit.device
+        spy = mocker.spy(qml.QubitDevice, "sample")
+
+        res = circuit(phi)
+
+        if isinstance(shots, list):
+            assert len(res) == len(shots)
+            assert all(r.shape == (s, 2) for r, s in zip(res, shots))
+        else:
+            assert res.shape == (shots, 2)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
 
     def test_single_wire_sample(self, mocker):
         """Test the return type and shape of sampling a single wire"""
diff --git a/tests/measurements/legacy/test_var_legacy.py b/tests/measurements/legacy/test_var_legacy.py
index 6245e4585..ef84166ba 100644
--- a/tests/measurements/legacy/test_var_legacy.py
+++ b/tests/measurements/legacy/test_var_legacy.py
@@ -110,12 +110,14 @@ class TestVar:
 
     @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
     @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
     def test_observable_is_measurement_value(
-        self, shots, phi, mocker, tol, tol_stochastic
+        self, shots, phi, mocker, tol, tol_stochastic, device_name
     ):  # pylint: disable=too-many-arguments
         """Test that variances for mid-circuit measurement values
         are correct for a single measurement value."""
-        dev = qml.device("default.qubit.legacy", wires=2, shots=shots)
+        np.random.seed(42)
+        dev = qml.device(device_name, wires=2, shots=shots)
 
         @qml.qnode(dev)
         def circuit(phi):
@@ -131,7 +133,56 @@ class TestVar:
         atol = tol if shots is None else tol_stochastic
         expected = np.sin(phi / 2) ** 2 - np.sin(phi / 2) ** 4
         assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
-        custom_measurement_process(new_dev, spy)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
+
+    @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    @pytest.mark.parametrize("device_name", ["default.qubit.legacy", "default.mixed"])
+    def test_observable_is_composite_measurement_value(
+        self, shots, phi, mocker, tol, tol_stochastic, device_name
+    ):  # pylint: disable=too-many-arguments
+        """Test that variances for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        np.random.seed(42)
+        dev = qml.device(device_name, wires=6, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(0.5 * phi, 1)
+            m1 = qml.measure(1)
+            qml.RX(2 * phi, 2)
+            m2 = qml.measure(2)
+            return qml.var(m0 - 2 * m1 + m2)
+
+        new_dev = circuit.device
+        spy = mocker.spy(qml.QubitDevice, "var")
+
+        res = circuit(phi, shots=shots)
+
+        @qml.qnode(dev)
+        def expected_circuit(phi):
+            qml.RX(phi, 0)
+            qml.RX(0.5 * phi, 1)
+            qml.RX(2 * phi, 2)
+            return qml.probs(wires=[0, 1, 2])
+
+        probs = expected_circuit(phi, shots=None)
+        # List of possible outcomes by applying the formula to the binary repr of the indices
+        outcomes = np.array([0.0, 1.0, -2.0, -1.0, 1.0, 2.0, -1.0, 0.0])
+        expected = (
+            sum(probs[i] * outcomes[i] ** 2 for i in range(len(probs)))
+            - sum(probs[i] * outcomes[i] for i in range(len(probs))) ** 2
+        )
+
+        atol = tol if shots is None else tol_stochastic
+        assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
+
+        if device_name != "default.mixed":
+            custom_measurement_process(new_dev, spy)
 
     @pytest.mark.parametrize(
         "obs",
diff --git a/tests/measurements/test_counts.py b/tests/measurements/test_counts.py
index d2994e6e7..c1cf04c59 100644
--- a/tests/measurements/test_counts.py
+++ b/tests/measurements/test_counts.py
@@ -89,6 +89,10 @@ class TestCounts:
         m3 = CountsMP(eigvals=(-1, 1), all_outcomes=False)
         assert repr(m3) == "CountsMP(eigvals=[-1  1], wires=[], all_outcomes=False)"
 
+        mv = qml.measure(0)
+        m4 = CountsMP(obs=mv, all_outcomes=False)
+        assert repr(m4) == "CountsMP(MeasurementValue(wires=[0]), all_outcomes=False)"
+
 
 class TestProcessSamples:
     """Unit tests for the counts.process_samples method"""
@@ -170,9 +174,65 @@ class TestProcessSamples:
         result = qml.counts(mv).process_samples(samples, wire_order=[0])
 
         assert len(result) == 2
-        assert set(result.keys()) == {"0", "1"}
-        assert result["0"] == np.count_nonzero(samples[:, 0] == 0)
-        assert result["1"] == np.count_nonzero(samples[:, 0] == 1)
+        assert set(result.keys()) == {0, 1}
+        assert result[0] == np.count_nonzero(samples[:, 0] == 0)
+        assert result[1] == np.count_nonzero(samples[:, 0] == 1)
+
+    def test_counts_shape_composite_measurement_value(self):
+        """Test that the counts output is correct for composite mid-circuit measurement
+        values."""
+        shots = 1000
+        samples = np.random.choice([0, 1], size=(shots, 2)).astype(np.int64)
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+
+        result = qml.counts(op=m0 | m1).process_samples(samples, wire_order=[0, 1])
+
+        assert len(result) == 2
+        assert set(result.keys()) == {0, 1}
+        samples = np.apply_along_axis((lambda x: x[0] | x[1]), axis=1, arr=samples)
+        assert result[0] == np.count_nonzero(samples == 0)
+        assert result[1] == np.count_nonzero(samples == 1)
+
+    def test_counts_shape_measurement_value_list(self):
+        """Test that the counts output is correct for list mid-circuit measurement
+        values."""
+        shots = 1000
+        samples = np.random.choice([0, 1], size=(shots, 2)).astype(np.int64)
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+
+        result = qml.counts(op=[m0, m1]).process_samples(samples, wire_order=[0, 1])
+
+        assert len(result) == 4
+        assert set(result.keys()) == {"00", "01", "10", "11"}
+        assert result["00"] == np.count_nonzero([np.allclose(s, [0, 0]) for s in samples])
+        assert result["01"] == np.count_nonzero([np.allclose(s, [0, 1]) for s in samples])
+        assert result["10"] == np.count_nonzero([np.allclose(s, [1, 0]) for s in samples])
+        assert result["11"] == np.count_nonzero([np.allclose(s, [1, 1]) for s in samples])
+
+    def test_mixed_lists_as_op_not_allowed(self):
+        """Test that passing a list not containing only measurement values raises an error."""
+        m0 = qml.measure(0)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="Only sequences of single MeasurementValues can be passed with the op argument",
+        ):
+            _ = qml.counts(op=[m0, qml.PauliZ(0)])
+
+    def test_composed_measurement_value_lists_not_allowed(self):
+        """Test that passing a list containing measurement values composed with arithmetic
+        raises an error."""
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+        m2 = qml.measure(2)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="Only sequences of single MeasurementValues can be passed with the op argument",
+        ):
+            _ = qml.counts(op=[m0 + m1, m2])
 
     def test_counts_all_outcomes_wires(self):
         """Test that the counts output is correct when all_outcomes is passed"""
@@ -224,15 +284,65 @@ class TestProcessSamples:
         result1 = qml.counts(mv, all_outcomes=False).process_samples(samples, wire_order=[0])
 
         assert len(result1) == 1
-        assert set(result1.keys()) == {"0"}
-        assert result1["0"] == shots
+        assert set(result1.keys()) == {0}
+        assert result1[0] == shots
 
         result2 = qml.counts(mv, all_outcomes=True).process_samples(samples, wire_order=[0])
 
         assert len(result2) == 2
-        assert set(result2.keys()) == {"0", "1"}
-        assert result2["0"] == shots
-        assert result2["1"] == 0
+        assert set(result2.keys()) == {0, 1}
+        assert result2[0] == shots
+        assert result2[1] == 0
+
+    def test_counts_all_outcomes_composite_measurement_value(self):
+        """Test that the counts output is correct when all_outcomes is passed
+        for composite mid-circuit measurement values."""
+        shots = 1000
+        samples = np.zeros((shots, 2)).astype(np.int64)
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+        mv = (m0 - 1) * 2 * (m1 + 1) - 2  # 00 equates to -4
+
+        result1 = qml.counts(mv, all_outcomes=False).process_samples(samples, wire_order=[0, 1])
+
+        assert len(result1) == 1
+        assert set(result1.keys()) == {-4}
+        assert result1[-4] == shots
+
+        result2 = qml.counts(mv, all_outcomes=True).process_samples(samples, wire_order=[0, 1])
+
+        # Possible outcomes are -4, -6, -2
+        assert len(result2) == 3
+        assert set(result2.keys()) == {-6, -4, -2}
+        assert result2[-4] == shots
+        assert result2[-6] == result2[-2] == 0
+
+    def test_counts_all_outcomes_measurement_value_list(self):
+        """Test that the counts output is correct when all_outcomes is passed
+        for a list of mid-circuit measurement values."""
+        shots = 1000
+        samples = np.zeros((shots, 2)).astype(np.int64)
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+
+        result1 = qml.counts([m0, m1], all_outcomes=False).process_samples(
+            samples, wire_order=[0, 1]
+        )
+
+        assert len(result1) == 1
+        assert set(result1.keys()) == {"00"}
+        assert result1["00"] == shots
+
+        result2 = qml.counts([m0, m1], all_outcomes=True).process_samples(
+            samples, wire_order=[0, 1]
+        )
+
+        assert len(result2) == 4
+        assert set(result2.keys()) == {"00", "01", "10", "11"}
+        assert result2["00"] == shots
+        assert result2["01"] == 0
+        assert result2["10"] == 0
+        assert result2["11"] == 0
 
 
 class TestCountsIntegration:
diff --git a/tests/measurements/test_expval.py b/tests/measurements/test_expval.py
index 2acfad8e1..135cfaa7b 100644
--- a/tests/measurements/test_expval.py
+++ b/tests/measurements/test_expval.py
@@ -94,6 +94,54 @@ class TestExpval:
         atol = tol if shots is None else tol_stochastic
         assert np.allclose(np.array(res), np.sin(phi / 2) ** 2, atol=atol, rtol=0)
 
+    @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    def test_observable_is_composite_measurement_value(
+        self, shots, phi, tol, tol_stochastic
+    ):  # pylint: disable=too-many-arguments
+        """Test that expectation values for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        dev = qml.device("default.qubit")
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(0.5 * phi, 1)
+            m1 = qml.measure(1)
+            qml.RX(2 * phi, 2)
+            m2 = qml.measure(2)
+            return qml.expval(m0 * m1 + m2)
+
+        res = circuit(phi, shots=shots)
+
+        @qml.qnode(dev)
+        def expected_circuit(phi):
+            qml.RX(phi, 0)
+            qml.RX(0.5 * phi, 1)
+            qml.RX(2 * phi, 2)
+            return (
+                qml.expval(qml.Projector([1], 0)),
+                qml.expval(qml.Projector([1], 1)),
+                qml.expval(qml.Projector([1], 2)),
+            )
+
+        evals = expected_circuit(phi)
+        expected = evals[0] * evals[1] + evals[2]
+
+        atol = tol if shots is None else tol_stochastic
+        assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
+
+    def test_measurement_value_list_not_allowed(self):
+        """Test that measuring a list of measurement values raises an error."""
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+
+        with pytest.raises(
+            ValueError, match="qml.expval does not support measuring sequences of measurements"
+        ):
+            _ = qml.expval([m0, m1])
+
     @pytest.mark.parametrize(
         "obs",
         [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
diff --git a/tests/measurements/test_measurements.py b/tests/measurements/test_measurements.py
index 80249a4f8..26fc40305 100644
--- a/tests/measurements/test_measurements.py
+++ b/tests/measurements/test_measurements.py
@@ -24,6 +24,7 @@ from pennylane.measurements import (
     ExpectationMP,
     MeasurementProcess,
     MeasurementTransform,
+    MeasurementValue,
     MidMeasure,
     MidMeasureMP,
     MutualInfoMP,
@@ -337,9 +338,15 @@ class TestProperties:
         are correct if the internal observable is a
         MeasurementValue."""
         m0 = qml.measure(0)
+        m0.measurements[0].id = "abc"
+        m1 = qml.measure(1)
+        m1.measurements[0].id = "def"
 
-        m = qml.expval(m0)
-        assert np.all(m.eigvals() == [0, 1])
+        mp1 = qml.sample(op=[m0, m1])
+        assert np.all(mp1.eigvals() == [0, 1, 2, 3])
+
+        mp2 = qml.sample(op=3 * m0 - m1 / 2)
+        assert np.all(mp2.eigvals() == [0, -0.5, 3, 2.5])
 
     def test_error_obs_and_eigvals(self):
         """Test that providing both eigenvalues and an observable
@@ -378,6 +385,35 @@ class TestProperties:
         m = ProbabilityMP(eigvals=(1, 0), wires=qml.wires.Wires(0))
         assert repr(m) == "probs(eigvals=[1 0], wires=[0])"
 
+        m0 = MeasurementValue([MidMeasureMP(Wires(0), id="0")], lambda v: v)
+        m1 = MeasurementValue([MidMeasureMP(Wires(1), id="1")], lambda v: v)
+        m = ProbabilityMP(obs=[m0, m1])
+        expected = "probs([MeasurementValue(wires=[0]), MeasurementValue(wires=[1])])"
+        assert repr(m) == expected
+
+        m = ProbabilityMP(obs=m0 * m1)
+        expected = "probs(MeasurementValue(wires=[0, 1]))"
+
+    def test_measurement_value_map_wires(self):
+        """Test that MeasurementProcess.map_wires works correctly when mp.mv
+        is not None."""
+        m0 = qml.measure("a")
+        m1 = qml.measure("b")
+        m2 = qml.measure(0)
+        m3 = qml.measure(1)
+        m2.measurements[0].id = m0.measurements[0].id
+        m3.measurements[0].id = m1.measurements[0].id
+
+        wire_map = {"a": 0, "b": 1}
+
+        mp1 = qml.sample(op=[m0, m1])
+        mapped_mp1 = mp1.map_wires(wire_map)
+        assert qml.equal(mapped_mp1, qml.sample(op=[m2, m3]))
+
+        mp2 = qml.sample(op=m0 * m1)
+        mapped_mp2 = mp2.map_wires(wire_map)
+        assert qml.equal(mapped_mp2, qml.sample(op=m2 * m3))
+
 
 class TestExpansion:
     """Test for measurement expansion"""
@@ -521,7 +557,7 @@ class TestDiagonalizingGates:
         """Test a measurement that has no expansion"""
         m = qml.sample()
 
-        assert m.diagonalizing_gates() == []
+        assert len(m.diagonalizing_gates()) == 0
 
     def test_obs_diagonalizing_gates(self):
         """Test diagonalizing_gates method with and observable."""
diff --git a/tests/measurements/test_mid_measure.py b/tests/measurements/test_mid_measure.py
index 935fba003..1b459de5a 100644
--- a/tests/measurements/test_mid_measure.py
+++ b/tests/measurements/test_mid_measure.py
@@ -380,14 +380,22 @@ class TestMeasurementValueManipulation:
         w.r.t a more complicated MeasurementValue"""
         a = MeasurementValue([mp1], lambda v: v)
         b = MeasurementValue([mp2], lambda v: v)
-        assert (
-            str(a + b)
-            == """if m0=0,m1=0 => 0
-if m0=0,m1=1 => 1
-if m0=1,m1=0 => 1
-if m0=1,m1=1 => 2"""
+        assert str(a + b) == (
+            "if m0=0,m1=0 => 0\nif m0=0,m1=1 => 1\nif m0=1,m1=0 => 1\nif m0=1,m1=1 => 2"
         )
 
+    def test_repr(self):
+        """Test that the output of the __repr__ dubder method is as expected."""
+        m = qml.measure(0)
+        assert repr(m) == "MeasurementValue(wires=[0])"
+
+    def test_complex_repr(self):
+        """Test that the output of the __repr__ dunder method is as expected
+        w.r.t a more complicated MeasurementValue"""
+        a = MeasurementValue([mp1], lambda v: v)
+        b = MeasurementValue([mp2], lambda v: v)
+        assert repr(a + b) == "MeasurementValue(wires=[0, 1])"
+
     def test_map_wires(self):
         """Test that map_wires works as expected."""
         a = MeasurementValue([mp1], lambda v: v)
diff --git a/tests/measurements/test_probs.py b/tests/measurements/test_probs.py
index 94090002c..37ae5251d 100644
--- a/tests/measurements/test_probs.py
+++ b/tests/measurements/test_probs.py
@@ -273,6 +273,76 @@ class TestProbs:
             for r in res:  # pylint: disable=not-an-iterable
                 assert np.allclose(r, expected, atol=atol, rtol=0)
 
+    @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    def test_observable_is_measurement_value_list(
+        self, shots, phi, tol, tol_stochastic
+    ):  # pylint: disable=too-many-arguments
+        """Test that probs for mid-circuit measurement values
+        are correct for a measurement value list."""
+        dev = qml.device("default.qubit")
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(0.5 * phi, 1)
+            m1 = qml.measure(1)
+            qml.RX(2.0 * phi, 2)
+            m2 = qml.measure(2)
+            return qml.probs(op=[m0, m1, m2])
+
+        res = circuit(phi, shots=shots)
+
+        @qml.qnode(dev)
+        def expected_circuit(phi):
+            qml.RX(phi, 0)
+            qml.RX(0.5 * phi, 1)
+            qml.RX(2.0 * phi, 2)
+            return qml.probs(wires=[0, 1, 2])
+
+        expected = expected_circuit(phi)
+
+        atol = tol if shots is None else tol_stochastic
+
+        if not isinstance(shots, list):
+            assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
+        else:
+            for r in res:  # pylint: disable=not-an-iterable
+                assert np.allclose(r, expected, atol=atol, rtol=0)
+
+    def test_composite_measurement_value_not_allowed(self):
+        """Test that measuring composite mid-circuit measurement values raises
+        an error."""
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+
+        with pytest.raises(ValueError, match=r"Cannot use qml.probs\(\) when measuring multiple"):
+            _ = qml.probs(op=m0 + m1)
+
+    def test_mixed_lists_as_op_not_allowed(self):
+        """Test that passing a list not containing only measurement values raises an error."""
+        m0 = qml.measure(0)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="Only sequences of single MeasurementValues can be passed with the op argument",
+        ):
+            _ = qml.probs(op=[m0, qml.PauliZ(0)])
+
+    def test_composed_measurement_value_lists_not_allowed(self):
+        """Test that passing a list containing measurement values composed with arithmetic
+        raises an error."""
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+        m2 = qml.measure(2)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="Only sequences of single MeasurementValues can be passed with the op argument",
+        ):
+            _ = qml.probs(op=[m0 + m1, m2])
+
     @pytest.mark.parametrize("shots", [None, 100])
     def test_batch_size(self, shots):
         """Test the probability is correct for a batched input."""
diff --git a/tests/measurements/test_sample.py b/tests/measurements/test_sample.py
index 0aabe2675..e510a556a 100644
--- a/tests/measurements/test_sample.py
+++ b/tests/measurements/test_sample.py
@@ -173,6 +173,74 @@ class TestSample:
         else:
             assert res.shape == (shots,)
 
+    @pytest.mark.parametrize("shots", [5, [5, 5]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 2))
+    def test_observable_is_composite_measurement_value(self, shots, phi):
+        """Test that samples for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        dev = qml.device("default.qubit", wires=2, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(phi, 1)
+            m1 = qml.measure(1)
+            return qml.sample(op=m0 + m1)
+
+        res = circuit(phi)
+
+        if isinstance(shots, list):
+            assert len(res) == len(shots)
+            assert all(r.shape == (s,) for r, s in zip(res, shots))
+        else:
+            assert res.shape == (shots,)
+
+    @pytest.mark.parametrize("shots", [5, [5, 5]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 2))
+    def test_observable_is_measurement_value_list(self, shots, phi):
+        """Test that samples for mid-circuit measurement values
+        are correct for a measurement value list."""
+        dev = qml.device("default.qubit", wires=2, shots=shots)
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            m1 = qml.measure(1)
+            return qml.sample(op=[m0, m1])
+
+        res = circuit(phi)
+
+        if isinstance(shots, list):
+            assert len(res) == len(shots)
+            assert all(r.shape == (s, 2) for r, s in zip(res, shots))
+        else:
+            assert res.shape == (shots, 2)
+
+    def test_mixed_lists_as_op_not_allowed(self):
+        """Test that passing a list not containing only measurement values raises an error."""
+        m0 = qml.measure(0)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="Only sequences of single MeasurementValues can be passed with the op argument",
+        ):
+            _ = qml.sample(op=[m0, qml.PauliZ(0)])
+
+    def test_composed_measurement_value_lists_not_allowed(self):
+        """Test that passing a list containing measurement values composed with arithmetic
+        raises an error."""
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+        m2 = qml.measure(2)
+
+        with pytest.raises(
+            qml.QuantumFunctionError,
+            match="Only sequences of single MeasurementValues can be passed with the op argument",
+        ):
+            _ = qml.sample(op=[m0 + m1, m2])
+
     def test_providing_observable_and_wires(self):
         """Test that a ValueError is raised if both an observable is provided and wires are specified"""
         dev = qml.device("default.qubit", wires=2)
diff --git a/tests/measurements/test_var.py b/tests/measurements/test_var.py
index ef19d1ba4..f94f4299e 100644
--- a/tests/measurements/test_var.py
+++ b/tests/measurements/test_var.py
@@ -92,6 +92,55 @@ class TestVar:
         expected = np.sin(phi / 2) ** 2 - np.sin(phi / 2) ** 4
         assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
 
+    @pytest.mark.parametrize("shots", [None, 10000, [10000, 10000]])
+    @pytest.mark.parametrize("phi", np.arange(0, 2 * np.pi, np.pi / 3))
+    def test_observable_is_composite_measurement_value(
+        self, shots, phi, tol, tol_stochastic
+    ):  # pylint: disable=too-many-arguments
+        """Test that expectation values for mid-circuit measurement values
+        are correct for a composite measurement value."""
+        dev = qml.device("default.qubit")
+
+        @qml.qnode(dev)
+        def circuit(phi):
+            qml.RX(phi, 0)
+            m0 = qml.measure(0)
+            qml.RX(0.5 * phi, 1)
+            m1 = qml.measure(1)
+            qml.RX(2 * phi, 2)
+            m2 = qml.measure(2)
+            return qml.var(m0 - 2 * m1 + m2)
+
+        res = circuit(phi, shots=shots)
+
+        @qml.qnode(dev)
+        def expected_circuit(phi):
+            qml.RX(phi, 0)
+            qml.RX(0.5 * phi, 1)
+            qml.RX(2 * phi, 2)
+            return qml.probs()
+
+        probs = expected_circuit(phi)
+        # List of possible outcomes by applying the formula to the binary repr of the indices
+        outcomes = np.array([0.0, 1.0, -2.0, -1.0, 1.0, 2.0, -1.0, 0.0])
+        expected = (
+            sum(probs[i] * outcomes[i] ** 2 for i in range(len(probs)))
+            - sum(probs[i] * outcomes[i] for i in range(len(probs))) ** 2
+        )
+
+        atol = tol if shots is None else tol_stochastic
+        assert np.allclose(np.array(res), expected, atol=atol, rtol=0)
+
+    def test_measurement_value_list_not_allowed(self):
+        """Test that measuring a list of measurement values raises an error."""
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+
+        with pytest.raises(
+            ValueError, match="qml.var does not support measuring sequences of measurements"
+        ):
+            _ = qml.var([m0, m1])
+
     @pytest.mark.parametrize(
         "obs",
         [qml.PauliZ(0), qml.Hermitian(np.diag([1, 2]), 0), qml.Hermitian(np.diag([1.0, 2.0]), 0)],
diff --git a/tests/ops/functions/test_equal.py b/tests/ops/functions/test_equal.py
index e765f5af7..a25c20224 100644
--- a/tests/ops/functions/test_equal.py
+++ b/tests/ops/functions/test_equal.py
@@ -1259,6 +1259,56 @@ class TestMeasurementsEqual:
             qml.measurements.MidMeasureMP(wires=qml.wires.Wires([0, 1]), reset=True, id="test_id"),
         )
 
+    @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
+        correctly."""
+        mv1 = qml.measure(0)
+        mv2 = qml.measure(1)
+        mv3 = qml.measure(1)
+        mv4 = qml.measure(0)
+        mv4.measurements[0].id = mv1.measurements[0].id
+
+        mp1 = mp_fn(op=[mv1, mv2])
+        mp2 = mp_fn(op=[mv4, mv2])
+        mp3 = mp_fn(op=[mv1, mv3])
+        mp4 = mp_fn(op=[mv2, mv1])
+
+        assert qml.equal(mp1, mp1)
+        assert qml.equal(mp1, mp2)
+        assert not qml.equal(mp1, mp3)
+        assert not qml.equal(mp1, mp4)
+
+    def test_mv_list_and_arithmetic_as_op(self):
+        """Test that comparing measurements using composite measurement values and
+        a list of measurement values fails."""
+        m0 = qml.measure(0)
+        m1 = qml.measure(1)
+        mp1 = qml.sample(op=m0 * m1)
+        mp2 = qml.sample(op=[m0, m1])
+
+        assert not qml.equal(mp1, mp2)
+
+    @pytest.mark.parametrize("mp_fn", [qml.expval, qml.var, qml.sample, qml.counts])
+    def test_mv_arithmetic_as_op(self, mp_fn):
+        """Test that MeasurementProcesses that measure a list of MeasurementValues check for equality
+        correctly."""
+        mv1 = qml.measure(0)
+        mv2 = qml.measure(1)
+        mv3 = qml.measure(1)
+        mv4 = qml.measure(0)
+        mv4.measurements[0].id = mv1.measurements[0].id
+
+        mp1 = mp_fn(op=mv1 * mv2)
+        mp2 = mp_fn(op=mv4 * mv2)
+        mp3 = mp_fn(op=mv2 * mv1)
+        mp4 = mp_fn(op=mv1 * mv3)
+
+        assert qml.equal(mp1, mp1)
+        assert qml.equal(mp1, mp2)
+        assert qml.equal(mp1, mp3)
+        assert not qml.equal(mp1, mp4)
+
 
 class TestObservablesComparisons:
     """Tests comparisons between Hamiltonians, Tensors and PauliX/Y/Z operators"""
diff --git a/tests/transforms/test_defer_measurements.py b/tests/transforms/test_defer_measurements.py
index c86fa4b33..5b533e3cf 100644
--- a/tests/transforms/test_defer_measurements.py
+++ b/tests/transforms/test_defer_measurements.py
@@ -97,6 +97,40 @@ def test_unsupported_measurements(mp, err_msg):
         _, _ = qml.defer_measurements(tape)
 
 
+@pytest.mark.parametrize(
+    "mp, compose_mv",
+    [
+        (qml.expval, True),
+        (qml.var, True),
+        (qml.probs, False),
+        (qml.sample, True),
+        (qml.sample, False),
+        (qml.counts, True),
+        (qml.counts, False),
+    ],
+)
+def test_multi_mcm_stats_same_wire(mp, compose_mv):
+    """Test that a tape collecting statistics on multiple mid-circuit measurements when
+    they measure the same wire is transformed correctly."""
+    mp1 = MidMeasureMP(0, id="foo")
+    mp2 = MidMeasureMP(0, id="bar")
+    mv1 = MeasurementValue([mp1], None)
+    mv2 = MeasurementValue([mp2], None)
+
+    mv = mv1 * mv2 if compose_mv else [mv1, mv2]
+    tape = qml.tape.QuantumScript([qml.PauliX(0), mp1, mp2], [mp(op=mv)], shots=10)
+    [deferred_tape], _ = qml.defer_measurements(tape)
+
+    emp1 = MidMeasureMP(1, id="foo")
+    emp2 = MidMeasureMP(2, id="bar")
+    emv1 = MeasurementValue([emp1], None)
+    emv2 = MeasurementValue([emp2], None)
+    emv = emv1 * emv2 if compose_mv else [emv1, emv2]
+
+    assert deferred_tape.operations == [qml.PauliX(0), qml.CNOT([0, 1]), qml.CNOT([0, 2])]
+    assert deferred_tape.measurements == [mp(op=emv)]
+
+
 class TestQNode:
     """Test that the transform integrates well with QNodes."""