Skip to content

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

Draft
ryanhill1 wants to merge 2 commits into
mainfrom
fix/qasm3-physical-qubits
Draft

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

Conversation

@ryanhill1

Copy link
Copy Markdown
Member

Important

Draft: blocked on a pyqasm release. The pyqasm pin is bumped to >=1.0.4, which is the first release that will contain qBraid/pyqasm#325 (physical qubits preserved in reset statements). Mark ready once that release is out — and correct the pin here if the release lands under a different number.

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.

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

Important

Review skipped

Draft detected.

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: Organization UI

Review profile: ASSERTIVE

Plan: Pro

Run ID: b226ad1c-ad3d-4f49-83fc-072c403e28ed

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