Skip to content

fix: support physical qubits in QASM3 to QIR conversion#290

Open
ryanhill1 wants to merge 4 commits into
mainfrom
fix/qasm3-physical-qubits
Open

fix: support physical qubits in QASM3 to QIR conversion#290
ryanhill1 wants to merge 4 commits into
mainfrom
fix/qasm3-physical-qubits

Conversation

@ryanhill1

@ryanhill1 ryanhill1 commented Jul 12, 2026

Copy link
Copy Markdown
Member

Summary of changes

qasm3_to_qir raised a bare AssertionError with an empty message for any program addressing physical qubits, e.g. h $0;:

qbraid_qir/qasm3/visitor.py:218
    assert isinstance(bit, qasm3_ast.IndexedIdentifier)   -->  AssertionError('')

Physical qubits are valid OpenQASM 3, and are exactly what Qiskit emits when a circuit is transpiled against a backend (qasm3.dumps(transpile(circuit, backend))). pyqasm parses, validates, and unrolls them fine — but they survive unrolling as plain Identifier nodes rather than IndexedIdentifier, which _get_op_bits assumed of every operand.

The fix

  • Operand lowering: a physical qubit lowers to the QIR qubit of the same index — $3 is qubit 3 — for gates, barriers, measurements, and reset.
  • Entry point qubit count: pyqasm reports num_qubits == 0 after unrolling these programs, so the visitor now derives the count itself. Physical indices are absolute hardware addresses, so a program touching only $7 still needs 8 qubits for $7 to be a valid QIR qubit id.
  • Full-barrier detection: _barrier_applicable summed declared register sizes, which is 0 when there are no registers, so every barrier on a physical-qubit program looked like an unsupported "subset barrier" and raised NotImplementedError.
  • No more bare asserts on operands: unsupported operands and target-less measurements (measure q;) now raise Qasm3ConversionError with an actual message.

Impact

This was found via 52 production job failures on qBraid's QIR simulator, where the empty exception message surfaced to users as an opaque "Internal server error". All 52 of those programs convert correctly with this change.

Tests

Six new tests in tests/qasm3_qir/converter/test_physical_qubits.py, all failing before the fix:

  • gates + measurement on physical qubits lower to QIR qubits 0/1
  • $3/$7 keep their indices and the entry point declares 8 qubits
  • a Qiskit-style transpiled program (physical qubits, barrier, register-indexed measurements)
  • reset on a physical qubit
  • measure q; with no target raises Qasm3ConversionError
  • an unsupported operand raises Qasm3ConversionError

Full suite green against both pyqir versions CI covers (0.11.x and 0.12+): 325 passed, 7 skipped. pylint 10/10, black clean.

Summary by CodeRabbit

  • Bug Fixes

    • Improved QASM3-to-QIR conversion for physical qubits, preserving hardware indices across gates, measurements, barriers, and resets.
    • Ensured generated entry points declare enough qubits for the highest referenced physical index.
    • Replaced unclear assertion failures with descriptive conversion errors for unsupported inputs and incomplete measurements.
  • Dependency Updates

    • Updated the QASM3 optional dependency requirement to ensure physical qubits are preserved in reset statements.

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).
@coderabbitai

coderabbitai Bot commented Jul 12, 2026

Copy link
Copy Markdown

Review Change Stack

Walkthrough

QASM3-to-QIR conversion now supports physical qubit identifiers, sizes entry points for their highest index, preserves reset mappings, validates unsupported operands and missing measurement targets, updates the pyqasm constraint, and adds regression tests.

Changes

Physical qubit conversion

Layer / File(s) Summary
Physical qubit lowering and sizing
qbraid_qir/qasm3/visitor.py, tests/qasm3_qir/converter/test_physical_qubits.py, pyproject.toml, CHANGELOG.md
Physical identifiers map to matching QIR qubits, entry points account for the highest index, resets preserve hardware indices, and tests cover gates, measurements, and dependency updates.
Conversion validation and barrier coverage
qbraid_qir/qasm3/visitor.py, tests/qasm3_qir/converter/test_physical_qubits.py, CHANGELOG.md
Missing measurement targets and unsupported operands raise Qasm3ConversionError, barriers cover physically required qubits, and the bug fix is documented.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: adding physical qubit support in QASM3 to QIR conversion.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/qasm3-physical-qubits

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.

include "stdgates.inc";
bit[3] meas;
rz(pi / 2) $0;
sx $0;

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.

should add the check for the single qubit op as well in the final qasm

include "stdgates.inc";
bit[1] meas;
h $2;
reset $2;

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.

add check for reset as well

Comment thread qbraid_qir/qasm3/visitor.py Outdated
touching only "$7" still needs 8 qubits for "$7" to be a valid QIR qubit id.
"""
finder = _HighestPhysicalQubit()
for statement in module.qasm_program.unrolled_ast.statements:

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 will mean that we are going over the complete AST just for qubit count calculation which seems wasteful. Is it possible to derive this count from the qasm module object? Or keep a property max_physical_qubit_addr in the qasm module object? Should eliminate the need for a recalculation here

_PHYSICAL_QUBIT_RE = re.compile(r"^\$(\d+)$")


def _physical_qubit_index(node: Any) -> Optional[int]:

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.

As mentioned in the comment below, better to move the parsing semantics to pyqasm

@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.

Thanks for catching this @ryanhill1 ! The logic seems correct but I think we would be better off keeping the attribute denoting the largest physical qubit in pyqasm.

It might be possible that physical qubit addressing related logic is needed in qBraid SDK or other dependencies so it is better to make sure that pyqasm remains the single place for semantic processing

…iving 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.
@TheGupta2012
TheGupta2012 marked this pull request as ready for review July 15, 2026 14:19
@TheGupta2012
TheGupta2012 self-requested a review July 15, 2026 14:20

@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.

Thanks for the changes @ryanhill1 ! I'll wait for pyqasm 1.0.4 to be published before approval and merge

@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: 1

🤖 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 `@qbraid_qir/qasm3/visitor.py`:
- Around line 236-241: Remove the redundant operation.target validation block
from the surrounding visitor logic, since _visit_measurement is the sole handler
for QuantumMeasurementStatement and already performs this check. Leave the
validation and error behavior in _visit_measurement unchanged.
🪄 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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: cdf9cd44-e467-464f-91d6-b23229b28707

📥 Commits

Reviewing files that changed from the base of the PR and between e3cc954 and d7ac111.

📒 Files selected for processing (4)
  • CHANGELOG.md
  • pyproject.toml
  • qbraid_qir/qasm3/visitor.py
  • tests/qasm3_qir/converter/test_physical_qubits.py

Comment on lines +236 to +241
if operation.target is None:
raise_qasm3_error(
"Measurement result must be assigned to a classical bit, "
"e.g. 'c[0] = measure q[0];'",
span=operation.span,
)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Remove duplicate measurement validation.

This exact validation block was also added to _visit_measurement at lines 321-326. Since _visit_measurement is the only caller that processes QuantumMeasurementStatement, this check is redundant here and can be safely removed.

♻️ Proposed refactor
-            if operation.target is None:
-                raise_qasm3_error(
-                    "Measurement result must be assigned to a classical bit, "
-                    "e.g. 'c[0] = measure q[0];'",
-                    span=operation.span,
-                )
             bit_list = [operation.measure.qubit] if qubits else [operation.target]
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
if operation.target is None:
raise_qasm3_error(
"Measurement result must be assigned to a classical bit, "
"e.g. 'c[0] = measure q[0];'",
span=operation.span,
)
bit_list = [operation.measure.qubit] if qubits else [operation.target]
🤖 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 `@qbraid_qir/qasm3/visitor.py` around lines 236 - 241, Remove the redundant
operation.target validation block from the surrounding visitor logic, since
_visit_measurement is the sole handler for QuantumMeasurementStatement and
already performs this check. Leave the validation and error behavior in
_visit_measurement unchanged.

@TheGupta2012

Copy link
Copy Markdown
Member

@ryanhill1 I've released pyqasm 1.0.4, could you please fix the CI errors?

@codecov

codecov Bot commented Jul 16, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 95.45455% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
qbraid_qir/qasm3/visitor.py 95.45% 1 Missing ⚠️

📢 Thoughts on this report? Let us know!

@ryanhill1
ryanhill1 force-pushed the fix/qasm3-physical-qubits branch from 6c0394c to d7ac111 Compare July 16, 2026 14:28
@ryanhill1

Copy link
Copy Markdown
Member Author

@TheGupta2012 Heads up on the CI failures here — they are not caused by any change in this PR. All 325 tests pass, including the new physical-qubit coverage; the only two failures are tests/squin_qir/test_cudaq_to_squin.py::{test_bell_state,test_ghz_state}, which are unrelated to this feature.

They're dependency drift: cuda-q 0.15 now emits opaque-pointer QIR (QIR 2.0), which the pyqir 0.11.x (typed pointers) CI leg cannot parse (ValueError: main:6:38: error: expected type on call void @__quantum__qis__h__body(ptr null)). cuda-q has no option to emit typed pointers, and pyqir 0.11.x (LLVM 14) cannot parse opaque pointers, so those tests can only run against pyqir 0.12+.

I've split the fix into its own PR so this one stays scoped to the physical-qubit feature: #292. Once #292 lands on main, re-running CI here will pick up the gate (the two cuda-q tests skip on the typed-pointer leg) and this PR goes green. Recommended order: merge #292 first, then re-run CI on this PR.

Comment thread CHANGELOG.md Outdated
Comment thread CHANGELOG.md Outdated
Co-authored-by: Harshit Gupta <harshit.11235@gmail.com>
@ryanhill1
ryanhill1 requested a review from TheGupta2012 July 17, 2026 13:15
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.

2 participants