Skip to content

feat: Classical control flow with mid circuit measurements#347

Open
speller26 wants to merge 43 commits intomainfrom
mcm-experimental
Open

feat: Classical control flow with mid circuit measurements#347
speller26 wants to merge 43 commits intomainfrom
mcm-experimental

Conversation

@speller26
Copy link
Copy Markdown
Member

Issue #, if available:

Description of changes:

Testing done:

Merge Checklist

Put an x in the boxes that apply. You can also fill these out after creating the PR. If you're unsure about any of them, don't hesitate to ask. We're here to help! This is simply a reminder of what we are going to look for before merging your pull request.

General

Tests

  • I have added tests that prove my fix is effective or that my feature works (if appropriate)
  • I have checked that my tests are not configured for a specific region or account (if appropriate)

By submitting this pull request, I confirm that my contribution is made under the terms of the Apache 2.0 license.

@yitchen-tim yitchen-tim self-requested a review February 19, 2026 22:02
* Squeeze single-element density matrix before computing expectation
* Pinned setuptools to fix doc build
* Fixed all linter errors
@speller26 speller26 changed the base branch from main to fix February 21, 2026 00:33
Base automatically changed from fix to main February 23, 2026 17:29
@codecov
Copy link
Copy Markdown

codecov bot commented Feb 23, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (aed65bd) to head (7ae10d1).

Additional details and impacted files
@@            Coverage Diff             @@
##              main      #347    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files           48        49     +1     
  Lines         4142      4752   +610     
  Branches       426       533   +107     
==========================================
+ Hits          4142      4752   +610     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@yitchen-tim
Copy link
Copy Markdown
Contributor

yitchen-tim commented Mar 2, 2026

Some product-level UX questions:

  • Several new feature for MCM triggers _uses_advanced_language_features=True. This causes warning "This program uses OpenQASM language features that may not be supported on QPUs or on-demand simulators." This is somewhat confusing to users, as if there is something wrong in their program/execution and they cannot trust the simulation results. Given that we are pushing for official support of MCM. Should we remove these warnings for MCM, and reserve _uses_advanced_language_features=True for something truly experimental?
  • We are lacking result schema and result class that supports MCM and general variable results. Right now, variables play no role in the result object. Result object always returns the end-of-circuit measurement outcome for all used qubits regardless whether there is an measurement instruction. Is it intentional and expected? Have you thought about the UX around the fact that we are not outputting any variables?

Copy link
Copy Markdown
Contributor

@laurencap laurencap left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Reviewed 1.5 files today. Should have more time tomorrow!

condition = cast_to(BooleanLiteral, self.visit(node.condition))
for statement in node.if_block if condition.value else node.else_block:
self.visit(statement)
self.context.handle_branching_statement(node, self.visit)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious about this. Doesn't the visitor usually extract all the important fields, and then call context methods on extracted fields? Why not call visit from this visitor?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really annoying, and I'm totally open to better ways to do this. The problem is, the node itself can contain MCM conditionals, so calling visit right away will trigger the branch hander; the recursive nature of these calls means evaluation is completed "bottom-up," while the branch simulations have to be done "top-down."

measurement_target (Identifier | IndexedIdentifier | None): The AST node
for the classical variable being assigned, e.g. ``b`` in
``b = measure q[0]``. Used by the branched MCM path to update
per-path classical variables. None for end-of-circuit measurements.
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

b[0] = measure q[0] is a valid circuit-final measurement right? In those cases, this is None. Is there no reason to track b[0] for circuit-final measurements?

I guess, because we return results by qubit index order at the end, it's not needed for anything?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Renamed to classical_destination and clarified documentation; None guarantees that it's terminal because the result can't be used (it isn't assigned to anything); however, assignment to a target may be either terminal or feedforward, but we don't know until the result is actually used.

def add_verbatim_marker(self, marker) -> None:
"""Add verbatim markers"""

def handle_branching_statement(self, node: BranchingStatement, visit_block: Callable) -> None:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cross commenting from the last file -- visit_block callback feels odd to me. Not that important though. Nice documentation here

with self.enter_scope():
self.declare_variable(node.identifier.name, node.type, i)
visit_block(deepcopy(node.block))
except _BreakSignal:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, maybe these were moved for ease of integration with _Continue/BreakSignal? Is that basically what changed from interpreter.py, support for these signals?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, break and continue actually weren't supported at all before, even for static circuits

but no control flow depended on them) are registered in the circuit
as normal end-of-circuit measurements.
"""
if not self._is_branched and self._pending_mcm_targets:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

is_branched and circuit properties won't be called until interpretation is complete?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

_is_branched ia called frequently, but _pending_measurements will always be empty once the simulation is in branched mode (although that can be optimized).

@speller26 speller26 requested a review from aniksd-braket March 4, 2026 04:41
@speller26 speller26 marked this pull request as ready for review March 5, 2026 22:47
@speller26 speller26 requested a review from a team as a code owner March 5, 2026 22:47
zero_index = i & ~mask

# Transfer the amplitude (with proper scaling)
state[zero_index] += state[i]
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

do you want to copy state into a separate array first, rather than mutating the input array in place? (for consistency with how the Measure operation works)

MCM tests now all start from an OpenQASM program.
for statement in node.if_block if condition.value else node.else_block:
self.visit(statement)
if self.context.supports_midcircuit_measurement:
self.context.handle_branching_statement(node)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is only handled under BranchingStatement would cause the following use case to fail:

OPENQASM 3.0;
qubit[3] __qubits__;
bit[1] mcm;
bit __bit_1__;
__bit_1__ = measure __qubits__[1];
mcm[0] = __bit_1__;

when mcm[0] = __bit_1__; is called, __bit_1__ is not yet initialized because it's not a branch statement. It throws NameError: Identifier '__bit_1__' is not initialized.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed!

@yitchen-tim yitchen-tim closed this Mar 9, 2026
@yitchen-tim yitchen-tim deleted the mcm-experimental branch March 9, 2026 14:50
@yitchen-tim yitchen-tim restored the mcm-experimental branch March 9, 2026 14:51
@yitchen-tim yitchen-tim reopened this Mar 9, 2026
@yitchen-tim
Copy link
Copy Markdown
Contributor

Opened #351 to fix and allow re-measuring qubits.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants