Skip to content

fix: preserve physical qubits in reset statements - #325

Merged
TheGupta2012 merged 4 commits into
mainfrom
fix/physical-qubit-reset
Jul 15, 2026
Merged

fix: preserve physical qubits in reset statements#325
TheGupta2012 merged 4 commits into
mainfrom
fix/physical-qubit-reset

Conversation

@ryanhill1

@ryanhill1 ryanhill1 commented Jul 12, 2026

Copy link
Copy Markdown
Member

Summary of changes

reset on a physical qubit rewrote the operand to the internal pulse register: reset $2; unrolled to reset __PYQASM_QUBITS__[2];.

Two consequences:

  1. The unrolled output isn't valid OpenQASM. __PYQASM_QUBITS__ is a register the program never declares, so the unrolled program does not round-trip through dumps()loads().
  2. The qubit is never counted. The early return skipped _register_physical_qubit, so a program whose only operation is reset $3; reports num_qubits == 0.

Gate and measurement operands already branch on _openpulse_grammar_declared and keep $n as-is for plain QASM programs (# Plain QASM program: keep the physical qubit identifier as-is.). Reset was applying the OpenPulse rename unconditionally. It now follows the same rule: keep $n and register the qubit for plain QASM, rename only for OpenPulse programs, where the pulse visitor expects it.

Physical qubits are valid OpenQASM 3 and are what Qiskit emits when a circuit is transpiled against a backend (qasm3.dumps(transpile(circuit, backend))), so this shape shows up in real user programs.

How it was found

Downstream in qbraid-qir, whose QIR visitor could not lower the __PYQASM_QUBITS__[2] identifier. Surfaced from 52 production job failures on qBraid's QIR simulator.

Tests

Three new tests in tests/qasm3/test_reset.py, each failing before the fix:

  • test_reset_physical_qubit_preserves_identifier — unrolled output keeps reset $2;
  • test_reset_physical_qubit_is_countedreset $3; alone gives num_qubits == 4
  • test_reset_physical_qubit_unrolled_output_is_reloadable — the unrolled program reloads and validates

Full suite green with the CI extras (test, cli, pulse): 674 passed, 3 skipped (baseline 671 + these 3). pylint 10/10, mypy and black clean.

Summary by CodeRabbit

  • Bug Fixes

    • Fixed physical-qubit reset operations in plain OpenQASM so operands remain unchanged.
    • Corrected qubit counting for programs containing only a physical-qubit reset.
    • Ensured unrolled reset operations produce valid, reloadable OpenQASM.
    • Preserved expected internal handling for OpenPulse programs.
  • Tests

    • Added coverage for identifier preservation, qubit counting, round-tripping, and validation.

_visit_reset rewrote a physical qubit operand to the internal pulse
register unconditionally, so "reset $2;" unrolled to
"reset __PYQASM_QUBITS__[2];". That names a register the program never
declares, so the unrolled output did not round-trip through
dumps()/loads(), and the qubit was never registered -- a program whose
only operation was "reset $3;" reported num_qubits == 0.

Gate and measurement operands already branch on the OpenPulse grammar
flag and keep "$n" as-is for plain QASM programs. Reset now does the
same, applying the rename only for OpenPulse programs, where the pulse
visitor expects it.
@ryanhill1
ryanhill1 requested a review from TheGupta2012 as a code owner July 12, 2026 14:27
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 8844a279-b972-40ec-b41e-b237907f6ba9

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/physical-qubit-reset

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@codecov-commenter

codecov-commenter commented Jul 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.10345% with 2 lines in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/pyqasm/transformer.py 87.50% 1 Missing ⚠️
src/pyqasm/visitor.py 93.75% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 3

🧹 Nitpick comments (1)
src/pyqasm/visitor.py (1)

711-714: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Add an OpenPulse regression case.

The added tests cover plain-QASM preservation, counting, and reloadability, but not the OpenPulse branch that rewrites $n to __PYQASM_QUBITS__[n]. Add a test asserting that behavior remains valid.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@src/pyqasm/visitor.py` around lines 711 - 714, Add an OpenPulse regression
test covering the branch in the visitor that handles _openpulse_grammar_declared
and rewrites a $n qubit reference to __PYQASM_QUBITS__[n]. Assert the
transformed name is correct and the resulting program remains valid, while
preserving the existing plain-QASM test coverage.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/pyqasm/visitor.py`:
- Around line 698-705: Add an Args entry to _resolve_unindexed_reset_qubit’s
docstring documenting the statement parameter as the QuantumReset being
resolved, while preserving the existing Returns documentation.
- Line 722: Update the internal-register check in the visitor method containing
the __PYQASM_QUBITS__ prefix test so it matches only the generated indexed
register form, not user identifiers with the same prefix; preserve
_get_op_bits() validation and unrolling for all other operands.

In `@tests/qasm3/test_reset.py`:
- Around line 123-148: Update all three newly added test functions in
tests/qasm3/test_reset.py to declare an explicit -> None return annotation and
add a “Returns: None” section to each test’s docstring, including
test_reset_physical_qubit_preserves_identifier and the two adjacent tests.

---

Nitpick comments:
In `@src/pyqasm/visitor.py`:
- Around line 711-714: Add an OpenPulse regression test covering the branch in
the visitor that handles _openpulse_grammar_declared and rewrites a $n qubit
reference to __PYQASM_QUBITS__[n]. Assert the transformed name is correct and
the resulting program remains valid, while preserving the existing plain-QASM
test coverage.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: e2d5b345-2d11-42b2-9175-c4a5cb130a4e

📥 Commits

Reviewing files that changed from the base of the PR and between 13a1b0b and 55fafb7.

📒 Files selected for processing (3)
  • CHANGELOG.md
  • src/pyqasm/visitor.py
  • tests/qasm3/test_reset.py

Comment thread src/pyqasm/visitor.py
Comment thread src/pyqasm/visitor.py Outdated
Comment thread tests/qasm3/test_reset.py
Raised in review. The reset path treated any identifier starting with
"__PYQASM_QUBITS__" as the internal pulse register, which also swallowed a
user register that merely starts with that name:

    qubit[2] __PYQASM_QUBITS__foo;
    reset __PYQASM_QUBITS__foo;      // silently reset nothing

The statement was short-circuited out of unrolling and emitted verbatim,
where a normally-named register expands to one reset per qubit.

Match the exact name, or the name followed by an index or a slice, which are
the forms the transformer actually generates ("__PYQASM_QUBITS__",
"__PYQASM_QUBITS__[2]", "__PYQASM_QUBITS__[0:2]"). Matching only the indexed
form, as first suggested, would silently drop the bare and slice forms.

Also documents the helper's parameter.
Comment thread src/pyqasm/visitor.py Outdated
logger = logging.getLogger(__name__)

# Reserved register that physical qubits are consolidated onto for OpenPulse programs.
_INTERNAL_QUBIT_REGISTER = "__PYQASM_QUBITS__"

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

This seems like a good refactoring opportunity. Can you use this across the file so that this convention is embedded properly across the code base?

@TheGupta2012 TheGupta2012 left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good mostly @ryanhill1 , can you just trickle the _INTERNAL_QUBIT_REGISTER across the file?

The "__PYQASM_QUBITS__" literal was hardcoded across visitor.py, transformer.py
and pulse/utils.py. Move it to elements.py as INTERNAL_QUBIT_REGISTER, next to an
is_internal_qubit_register() helper that becomes the single place the register is
recognized (exact name, or name followed by an index or slice).

Applying the helper to the measurement handler fixes the same lookalike-prefix bug
already fixed for reset: startswith("__PYQASM_QUBITS__") also matched user registers
such as "__PYQASM_QUBITS__foo", short-circuiting them out of unrolling so that
"c = measure __PYQASM_QUBITS__foo;" was emitted verbatim instead of expanded per
qubit. Regression test added.
@ryanhill1
ryanhill1 requested a review from TheGupta2012 July 14, 2026 16:27
@TheGupta2012
TheGupta2012 merged commit 3cb0f2b into main Jul 15, 2026
26 checks passed
TheGupta2012 added a commit to qBraid/qbraid-qir that referenced this pull request Jul 28, 2026
* fix: support physical qubits in QASM3 to QIR conversion

qasm3_to_qir raised a bare AssertionError (empty message) for programs
addressing physical qubits, e.g. "h $0;". These are valid OpenQASM 3 and
are what Qiskit emits for backend-transpiled circuits, but they unroll to
plain Identifier nodes rather than IndexedIdentifier, which _get_op_bits
assumed.

Physical qubits now lower to the QIR qubit of the same index ($3 is qubit
3). The entry point declares enough qubits to cover the highest index
used: pyqasm reports num_qubits == 0 for these programs, and a program
touching only $7 still needs 8 qubits for $7 to be a valid QIR qubit id.
Full-barrier detection has the same problem -- it summed declared register
sizes, which is 0 with no registers, so every barrier looked like an
unsupported subset barrier.

Unsupported operands and target-less measurements now raise
Qasm3ConversionError with a message rather than an empty AssertionError.

Requires pyqasm >= 1.0.4, the first release that preserves physical qubits
in reset statements (qBraid/pyqasm#325).

* refactor: take the physical qubit count from pyqasm instead of re-deriving it

pyqasm already registers physical qubits during unrolling and folds them into
num_qubits (indices are absolute hardware addresses, so a program touching only
"$7" reports 8 qubits). Walking the unrolled AST to recompute the highest index
duplicated semantic processing that belongs in pyqasm, so drop
_HighestPhysicalQubit / _required_qubits and read qasm3_module.num_qubits.

Also extend the physical-qubit tests: assert the single-qubit ops in the
Qiskit-style transpiled program (rz, and the h-s-h that "sx" decomposes to,
since QIR has no native sx) and assert the reset call in the reset test.

* add PR backrefs in CHANGELOG

Co-authored-by: Harshit Gupta <harshit.11235@gmail.com>

* refactor: drop redundant target check in _get_op_bits

_visit_measurement is the sole handler for QuantumMeasurementStatement and
already rejects a missing target before calling _get_op_bits, so the target
validation there was unreachable. Leave the check in _visit_measurement as-is.

---------

Co-authored-by: Harshit Gupta <harshit.11235@gmail.com>
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.

3 participants