Skip to content

fix: Record measurements into named classical registers - #409

Closed
yitchen-tim wants to merge 5 commits into
mainfrom
classical-registers
Closed

yitchen-tim wants to merge 5 commits into
mainfrom
classical-registers

Conversation

@yitchen-tim

@yitchen-tim yitchen-tim commented Sep 30, 2026 •

Copy link
Copy Markdown
Contributor

Issue #, if available: Simplifies #399 (output variables, estimated removing 600 LOC) and future PRs related to classical control

Description of changes:

Replace the flat, anonymous classical bit index space in the simulator's circuit IR with named classical registers. A measurement is now recorded as "qubit q into element e of register r" rather than into a global classical index. Existing single register programs produce identical results. Six result bugs in the measurement path are fixed, and programs whose measurements span more than one register are rejected until per register results can be reported through the outputs field.

Background

Circuit kept two parallel lists, measured_qubits and target_classical_indices, and the simulator sorted the indices to get column order. The classical index was inherited from JAQCD, where a program has exactly one classical register, but OpenQASM programs declare many and the interpreter already resolves them through scoped symbol tables. Dropping that identity at the circuit boundary caused several bugs:

  • c[1] = measure q[0]; d[1] = measure q[1]; collided on index 1 and produced one column.
  • A scalar bit b destination was never recorded in the circuit when the program branched, so measurements fell back to every qubit and reported unmeasured qubits. bit b and bit[1] b disagreed on column count.
  • Explicit indices were global rather than register relative (bit[4] c; c[3] = measure q; sorted against unrelated registers).
  • A deferred measurement left pending after a partial transition to branched mode was silently dropped from the results.
  • Control flow on a measured bit with shots=0 crashed.
  • Applying a deferred measurement after its block scoped destination went out of scope crashed.

The branched flow also stored each mid circuit outcome three times under two addressing schemes (per path variable, circuit.target_classical_indices, path._mcm_outcomes by flat index), reconciled by hand. The output feature in #399 had to rebuild the variable to column mapping after the fact with name keyed maps, which reintroduced scope shadowing problems.

Changes

Key changes are:

  • Circuit records measurements in ClassicalRegister objects with per element qubit sources, replacing the two parallel lists measured_qubits and target_classical_indices that addressed every bit by a single flat index.
  • A scoped RegisterTable on the context maps each bit declaration to its register handle, so destinations are resolved by scope once and no name string or flat index reaches the circuit.

Other changes are:

  • measurement_slots defines the result columns as registers in declaration order and elements in index order, and run_openqasm and _run_branched build measurements from it instead of sorting classical indices.
  • The three per path update methods collapse into _record_measurement_outcomes, and per path outcomes are keyed by (register, element) rather than flat index.
  • A classical write to a measured register element releases the measurement binding, so a bound element always means last written by a measurement, which is what the upcoming output feature needs.
  • Scalar bit and bit[1] are both size one registers, so a scalar destination in branched mode now produces one column instead of falling back to every qubit.
  • Programs that measure into more than one register are rejected with a clear message until per register results can be reported through outputs, while the internals stay multi register throughout.

Useful concepts and terms for reviewing

  • Binding: recording which qubit a register element reports, needed because the simulator samples every qubit at the end of the circuit and the result has to know which sample each bit of the program's register takes.
  • Measurement slot: one (register, element, qubit) triple per bound element, listed in declaration order then element order; measurement_slots is the definition of the columns in measurements.
  • Measured qubits: It annotates what qubits correspond each column of the result bitstrings. It is reported in the result as measuredQubits. Crucially, this is not the same as "qubits that are measured" because there could be measured qubits that are not associated with a bitstring.
  • Register table: a scoped table on the context mapping a bit variable name to its ClassicalRegister handle, so a shadowing bit b in an inner scope resolves to a different register than the outer one.
  • Releasing a measurement: before branching, a classical assignment to a bound element clears its source so it is no longer a column, keeping the rule that a bound element was last written by a measurement.

LOC summary

Added Deleted Net
Source (4 files) 244 180 +64
Tests (4 files) 741 326 +415
Total 985 506 +479

Replace the flat, anonymous classical bit index space in the simulator
circuit IR with first-class registers. A measurement is now recorded as
"qubit q into element e of register r" instead of a global classical
index, and the interpreter carries register identity from its scoped
symbol tables down to the circuit.
@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (c881cfd) to head (63a1f03).

Additional details and impacted files
@@            Coverage Diff            @@
##              main      #409   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           49        49           
  Lines         4964      5030   +66     
  Branches       580       580           
=========================================
+ Hits          4964      5030   +66     

☔ View full report in Codecov by Harness.
📢 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 yitchen-tim mentioned this pull request Oct 1, 2026
5 tasks done
@@ -2597,13 +2597,14 @@ def test_simple_conditional_feedforward(self, simulator):
"""MCM with conditional: if measured 1, flip second qubit."""
qasm = """

@yitchen-tim yitchen-tim Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

In this test file, many tests have measurements become fully specified. The partial measurements is covered in test_scalar_bit_mcm_reports_only_the_measured_qubit.

@yitchen-tim yitchen-tim Oct 1, 2026 •

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also, storing measurements into multiple registers becomes not allowed until the output is supported (we have PR opened: #399). Users must store measurement into a single register for now. I have changed few tests so that there are only one register for measurements.

def _apply_pending_mcm_targets(self) -> None:
"""Apply every pending measurement now, in program order.

Called when something in the program needs a deferred measurement to have

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I keep these docstring to give context about where and when these private functions are called. I feel they can be tricky to understand.

@yitchen-tim
yitchen-tim marked this pull request as ready for review October 1, 2026 18:36
@yitchen-tim
yitchen-tim requested a review from a team as a code owner October 1, 2026 18:36
@yitchen-tim

Copy link
Copy Markdown
Contributor Author

@speller26 This is ready for review. This PR

  • Adds classical registers to the internal IR of simulator.
  • Reduces complexity of future control flow IR. (e.g., estimated ~500 less LOC for the output PR)
  • Fixes several bugs regarding remeasuring, reusing qubits, reusing bits. Those are caused by edge cases in index management between the multiple registers in the frontend and the single flattened internal register.

@yitchen-tim
yitchen-tim requested a review from speller26 October 1, 2026 18:53
@yitchen-tim
yitchen-tim marked this pull request as draft October 5, 2026 15:03
@yitchen-tim
yitchen-tim removed the request for review from speller26 October 5, 2026 15:28
@yitchen-tim

Copy link
Copy Markdown
Contributor Author

Breaking this PR apart into #410 and #411

@yitchen-tim yitchen-tim closed this Oct 5, 2026
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.

1 participant