Repository: PennyLaneAI/pennylane
Pull request #7027 (2025-02-28 20:26:46)

Raise an error for MCMs nested in Conditionals
**Context:**
The current methods for of mid-circuit measurement processing don't support applying a mid-circuit measurement conditionally. This was not within the scope of the implementation for any of these methods, so we don't expect it to work - however, we would prefer to return clear error messages, and we don't want to be returning incorrect results.

Currently, `qml.cond(m, qml.measure)(wire)` either raises a difficult to parse error (in the case of deferred measurements and analytical execution), or returns incorrect results (in the case of `tree-traversal`, `one-shot`, and deferred measurements with `shots`).

**Description of the Change:**

We add validation in the `midcircuit_measurements` transform, checking for conditional mcms and raising an error. Specific `mcm_method`s can be made to pass this validation check in the future if support for conditional mcms are added to the method; for now, we disallow it completely.

**Benefits:**
We don't return incorrect results or raise cryptic errors.

**Possible Drawbacks:**
It adds one more iteration over the operations for validation 🤷‍♀️ 

**Related GitHub Issues:**
None, but sc stories [sc-84177] and [sc-84178]

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/7027?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 99.60%. Comparing base [(`9b45754`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/9b4575432585f73ed2ed5a3749f2b2683d84ed01?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI) to head [(`d67b42b`)](https://app.codecov.io/gh/PennyLaneAI/pennylane/commit/d67b42bf8e4699bdfb28e0f39939a249378636c0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=PennyLaneAI).
> Report is 1 commits behind head on master.

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


```diff
@@           Coverage Diff           @@
##           master    #7027   +/-   ##
=======================================
  Coverage   99.60%   99.60%           
=======================================
  Files         484      484           
  Lines       46305    46312    +7     
=======================================
+ Hits        46122    46129    +7     
  Misses        183      183           
```

</details>

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

lillian542: > What if the measure is in a cond branch that has other operators around it, will this still detect it?

@andrijapau yes! On the tape we have a series of `Conditional` ops, each one with a single base.

Patch:
diff --git a/doc/releases/changelog-dev.md b/doc/releases/changelog-dev.md
index 4f3cd7aea..da08c8a68 100644
--- a/doc/releases/changelog-dev.md
+++ b/doc/releases/changelog-dev.md
@@ -542,6 +542,11 @@
 * The `QROM` template is upgraded to decompose more efficiently when `work_wires` are not used.
   [#6967)](https://github.com/PennyLaneAI/pennylane/pull/6967)
 
+* Processing mid-circuit measurements inside conditionals is not supported and previously resulted in 
+  unclear error messages or incorrect results. It is now explicitly not allowed, and raises an error when 
+  processing the tape.
+  [(#7027)](https://github.com/PennyLaneAI/pennylane/pull/7027)
+
 <h3>Contributors ✍️</h3>
 
 This release contains contributions from (in alphabetical order):
diff --git a/pennylane/devices/preprocess.py b/pennylane/devices/preprocess.py
index 3bf023fe6..18794a118 100644
--- a/pennylane/devices/preprocess.py
+++ b/pennylane/devices/preprocess.py
@@ -20,6 +20,7 @@ import os
 import warnings
 from collections.abc import Callable, Generator, Sequence
 from copy import copy
+from functools import lru_cache
 from itertools import chain
 from typing import Optional, Type
 
@@ -41,7 +42,7 @@ def null_postprocessing(results):
     return results[0]
 
 
-def _operator_decomposition_gen(
+def _operator_decomposition_gen(  # pylint: disable = too-many-positional-arguments
     op: qml.operation.Operator,
     acceptance_function: Callable[[qml.operation.Operator], bool],
     decomposer: Callable[[qml.operation.Operator], Sequence[qml.operation.Operator]],
@@ -196,6 +197,11 @@ def mid_circuit_measurements(
     if mcm_method is None:
         mcm_method = "one-shot" if tape.shots else "deferred"
 
+    if _includes_conditional_mcms(tape):
+        raise NotImplementedError(
+            "Conditionally applied mid-circuit measurements are not supported"
+        )
+
     if mcm_method == "one-shot":
         return qml.dynamic_one_shot(tape, postselect_mode=mcm_config.postselect_mode)
     if mcm_method == "tree-traversal":
@@ -205,6 +211,20 @@ def mid_circuit_measurements(
     )
 
 
+def _is_conditional_mcm(operation: qml.operation.Operator) -> bool:
+    """Returns True if the operation is a mid-circuit measurement nested inside a Conditional,
+    and False otherwise."""
+    if isinstance(operation, qml.ops.Conditional):
+        if isinstance(operation.base, qml.measurements.MidMeasureMP):
+            return True
+    return False
+
+
+@lru_cache(maxsize=2)
+def _includes_conditional_mcms(tape: QuantumScript) -> bool:
+    return any(_is_conditional_mcm(op) for op in tape.operations)
+
+
 @transform
 def validate_multiprocessing_workers(
     tape: QuantumScript, max_workers: int, device
@@ -294,7 +314,7 @@ def validate_adjoint_trainable_params(
 
 
 @transform
-def decompose(
+def decompose(  # pylint: disable = too-many-positional-arguments
     tape: QuantumScript,
     stopping_condition: Callable[[qml.operation.Operator], bool],
     stopping_condition_shots: Callable[[qml.operation.Operator], bool] = None,


Test patch:
diff --git a/tests/devices/test_preprocess.py b/tests/devices/test_preprocess.py
index fba3deeff..2034c4e69 100644
--- a/tests/devices/test_preprocess.py
+++ b/tests/devices/test_preprocess.py
@@ -495,6 +495,22 @@ class TestMidCircuitMeasurements:
         ):
             _, _ = mid_circuit_measurements(tape, dev, mcm_config)
 
+    @pytest.mark.parametrize("mcm_method", ["tree-traversal", "one-shot", "deferred"])
+    def test_conditional_mcms_raise_error(self, mcm_method):
+        """Test that an error is raised if a tape contains mid-circuit measurements inside
+        a Conditional"""
+        dev = qml.device("default.qubit", shots=10)
+        mcm_config = {"postselect_mode": None, "mcm_method": mcm_method}
+
+        m = qml.measure(0)
+        tape = QuantumScript([qml.ops.Conditional(m, qml.measurements.MidMeasureMP(0))], [])
+
+        with pytest.raises(
+            NotImplementedError,
+            match="Conditionally applied mid-circuit measurements are not supported",
+        ):
+            _, _ = mid_circuit_measurements(tape, dev, mcm_config)
+
 
 def test_validate_multiprocessing_workers_None():
     """Test that validation does not fail when max_workers is None"""