Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ Types of changes:
### Removed

### Fixed
- Fixed barrier unrolling to preserve multi-qubit barrier statements instead of splitting into individual per-qubit barriers. ([#295](https://github.com/qBraid/pyqasm/pull/295))
- Added support for physical qubit identifiers (`$0`, `$1`, …) in plain QASM 3 programs, including gates, barriers, measurements, and duplicate-qubit detection. ([#291](https://github.com/qBraid/pyqasm/pull/291))
- Updated CI to use `macos-15-intel` image due to deprecation of `macos-13` image. ([#283](https://github.com/qBraid/pyqasm/pull/283))

Expand Down
58 changes: 30 additions & 28 deletions src/pyqasm/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -479,35 +479,37 @@ def _get_pyqasm_device_qubit_index(
return _offsets[reg] + idx

if isinstance(unrolled_stmts, QuantumBarrier):
_qubit_id = cast(Identifier, unrolled_stmts.qubits[0]) # type: ignore[union-attr]
if not isinstance(_qubit_id, IndexedIdentifier):
_start = _get_pyqasm_device_qubit_index(
_qubit_id.name, 0, qubit_register_offsets, global_qreg_size_map
)
_end = _get_pyqasm_device_qubit_index(
_qubit_id.name,
global_qreg_size_map[_qubit_id.name] - 1,
qubit_register_offsets,
global_qreg_size_map,
)
if _start == 0:
_qubit_id.name = f"__PYQASM_QUBITS__[:{_end+1}]"
elif _end == device_qubits - 1:
_qubit_id.name = f"__PYQASM_QUBITS__[{_start}:]"
for _qubit_id in unrolled_stmts.qubits: # type: ignore[union-attr]
_qubit_id = cast(Identifier, _qubit_id)
if not isinstance(_qubit_id, IndexedIdentifier):
_start = _get_pyqasm_device_qubit_index(
_qubit_id.name, 0, qubit_register_offsets, global_qreg_size_map
)
_end = _get_pyqasm_device_qubit_index(
_qubit_id.name,
global_qreg_size_map[_qubit_id.name] - 1,
qubit_register_offsets,
global_qreg_size_map,
)
if _start == 0:
_qubit_id.name = f"__PYQASM_QUBITS__[:{_end+1}]"
elif _end == device_qubits - 1:
_qubit_id.name = f"__PYQASM_QUBITS__[{_start}:]"
else:
_qubit_id.name = f"__PYQASM_QUBITS__[{_start}:{_end+1}]"
else:
_qubit_id.name = f"__PYQASM_QUBITS__[{_start}:{_end+1}]"
else:
_qubit_str = cast(str, unrolled_stmts.qubits[0].name) # type: ignore[union-attr]
_qubit_ind = cast(
list, unrolled_stmts.qubits[0].indices
) # type: ignore[union-attr]
for multi_ind in _qubit_ind:
for ind in multi_ind:
pyqasm_ind = _get_pyqasm_device_qubit_index(
_qubit_str.name, ind.value, qubit_register_offsets, global_qreg_size_map
)
ind.value = pyqasm_ind
_qubit_str.name = "__PYQASM_QUBITS__"
_qubit_str = cast(str, _qubit_id.name) # type: ignore[union-attr]
_qubit_ind = cast(list, _qubit_id.indices) # type: ignore[union-attr]
for multi_ind in _qubit_ind:
for ind in multi_ind:
pyqasm_ind = _get_pyqasm_device_qubit_index(
_qubit_str.name,
ind.value,
qubit_register_offsets,
global_qreg_size_map,
)
ind.value = pyqasm_ind
_qubit_str.name = "__PYQASM_QUBITS__"

if isinstance(unrolled_stmts, list): # pylint: disable=too-many-nested-blocks
if isinstance(unrolled_stmts[0], QuantumMeasurementStatement):
Expand Down
55 changes: 50 additions & 5 deletions src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,42 @@ def _visit_reset(self, statement: qasm3_ast.QuantumReset) -> list[qasm3_ast.Quan

return unrolled_resets

def _expand_barrier_ranges(
self,
barrier: qasm3_ast.QuantumBarrier,
barrier_qubits: list[qasm3_ast.IndexedIdentifier | qasm3_ast.Identifier],
) -> list:
"""Replace RangeDefinition-containing qubits in a barrier with their
expanded IndexedIdentifier equivalents so that consolidate_qubit_registers
only sees IntegerLiteral indices."""
consolidated_qubits: list = []
expanded_idx = 0
for op_qubit in barrier.qubits:
# Expand this single operand to find how many bits it produces.
temp_barrier = qasm3_ast.QuantumBarrier(qubits=[op_qubit])
temp_barrier.span = barrier.span
op_expanded = self._get_op_bits(temp_barrier, qubits=True)
num_bits = len(op_expanded)

if isinstance(op_qubit, qasm3_ast.IndexedIdentifier):
has_range = any(
isinstance(ind, qasm3_ast.RangeDefinition)
for dim in op_qubit.indices
for ind in dim # type: ignore[union-attr]
)
if has_range:
consolidated_qubits.extend(
barrier_qubits[expanded_idx : expanded_idx + num_bits]
)
else:
consolidated_qubits.append(op_qubit)
else:
# Bare Identifier — keep as-is for compact slice notation
consolidated_qubits.append(op_qubit)

expanded_idx += num_bits
return consolidated_qubits

def _visit_barrier( # pylint: disable=too-many-locals, too-many-branches
self, barrier: qasm3_ast.QuantumBarrier
) -> list[qasm3_ast.QuantumBarrier]:
Expand Down Expand Up @@ -834,29 +870,38 @@ def _visit_barrier( # pylint: disable=too-many-locals, too-many-branches

if not self._unroll_barriers:
if self._consolidate_qubits:
consolidated_qubits = self._expand_barrier_ranges(barrier, barrier_qubits)
Copy link
Copy Markdown
Member

@TheGupta2012 TheGupta2012 Mar 11, 2026

Choose a reason for hiding this comment

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

There seems to be an edge case here -

from pyqasm import loads

qasm3_string = """OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
qreg q2[3];
barrier q2;
barrier q, q2;
"""
mod = loads(qasm3_string)
mod.unroll(unroll_barriers = False, consolidate_qubits = True)
mod.unroll(unroll_barriers = True, consolidate_qubits = True)

raises -

ERROR:pyqasm: Error at line 5, column 4 in QASM file

 >>>>>> barrier __PYQASM_QUBITS__[2:];

... 

File ~/Desktop/qBraid/repos/pyqasm/src/pyqasm/visitor.py:849, in QasmVisitor._visit_barrier(self, barrier)
    839     for transform_map, size_map in zip(
    840         reversed(self._function_qreg_transform_map), reversed(self._function_qreg_size_map)
    841     ):
    842         barrier.qubits = (
    843             Qasm3Transformer.transform_function_qubits(  # type: ignore [assignment]
    844                 barrier,
   (...)    847             )
    848         )
--> 849 barrier_qubits = self._get_op_bits(barrier, qubits=True)
    850 unrolled_barriers = []
    851 max_involved_depth = 0

File ~/Desktop/qBraid/repos/pyqasm/src/pyqasm/visitor.py:340, in QasmVisitor._get_op_bits(self, operation, qubits, function_qubit_sizes)
    335 if function_qubit_sizes is None:
    336     err_msg = (
    337         f"Missing {'qubit' if qubits else 'clbit'} register declaration "
    338         f"for '{reg_name}' in {type(operation).__name__}"
    339     )
--> 340     raise_qasm3_error(
    341         err_msg,
    342         error_node=operation,
    343         span=operation.span,
    344     )
    345 # we are trying to replace the qubits inside a nested function
    346 assert function_qubit_sizes is not None

File ~/Desktop/qBraid/repos/pyqasm/src/pyqasm/exceptions.py:137, in raise_qasm3_error(message, err_type, error_node, span, raised_from)
    135 if raised_from:
    136     raise err_type(message) from raised_from
--> 137 raise err_type(message)

ValidationError: Missing qubit register declaration for '__PYQASM_QUBITS__[2:]' in QuantumBarrier

Some weird behaviour where the commands work individually , and with switched order -

# works individually and in this order
mod.unroll(unroll_barriers = True, consolidate_qubits = True)
mod.unroll(unroll_barriers = False, consolidate_qubits = True)

but fail with -

# fails
mod.unroll(unroll_barriers = False, consolidate_qubits = True)
mod.unroll(unroll_barriers = True, consolidate_qubits = True)

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.

Edit : seems like this is a consolidate_qubits error , not particularly related to the barriers. Invoking unroll twice raises it

expanded = qasm3_ast.QuantumBarrier(
qubits=consolidated_qubits # type: ignore[arg-type]
)
barrier = cast(
qasm3_ast.QuantumBarrier,
Qasm3Transformer.consolidate_qubit_registers(
barrier,
expanded,
self._qubit_register_offsets,
self._global_qreg_size_map,
self._module._device_qubits,
),
)
return [barrier]

# Keep barrier as a single multi-qubit statement with expanded qubit
# references (e.g. q -> q[0], q[1], q[2]) instead of splitting into
# individual per-qubit barriers.
expanded_barrier = qasm3_ast.QuantumBarrier(qubits=barrier_qubits) # type: ignore[arg-type]

if self._consolidate_qubits:
unrolled_barriers = cast(
list[qasm3_ast.QuantumBarrier],
expanded_barrier = cast(
qasm3_ast.QuantumBarrier,
Qasm3Transformer.consolidate_qubit_registers(
unrolled_barriers,
expanded_barrier,
self._qubit_register_offsets,
self._global_qreg_size_map,
self._module._device_qubits,
),
)

return unrolled_barriers
return [expanded_barrier]

def _get_op_parameters(self, operation: qasm3_ast.QuantumGate) -> list[float]:
"""Get the parameters for the operation.
Expand Down
3 changes: 1 addition & 2 deletions tests/qasm2/test_operations.py
Original file line number Diff line number Diff line change
Expand Up @@ -51,8 +51,7 @@ def test_whitelisted_ops():
include 'qelib1.inc';
qreg q[2];
creg c[2];
barrier q[0];
barrier q[1];
barrier q[0], q[1];
reset q[0];
reset q[1];
measure q[0] -> c[0];
Expand Down
25 changes: 4 additions & 21 deletions tests/qasm3/test_barrier.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,23 +47,9 @@ def test_barrier():
qubit[2] q1;
qubit[3] q2;
qubit[1] q3;
barrier q1[0];
barrier q1[1];
barrier q2[0];
barrier q2[1];
barrier q2[2];
barrier q3[0];
barrier q1[0];
barrier q1[1];
barrier q2[0];
barrier q2[1];
barrier q2[2];
barrier q3[0];
barrier q1[0];
barrier q1[1];
barrier q2[0];
barrier q2[1];
barrier q3[0];
barrier q1[0], q1[1], q2[0], q2[1], q2[2], q3[0];
barrier q1[0], q1[1], q2[0], q2[1], q2[2], q3[0];
barrier q1[0], q1[1], q2[0], q2[1], q3[0];
"""
module = loads(qasm_str)
module.unroll()
Expand All @@ -87,10 +73,7 @@ def my_function(qubit[4] a) {
expected_qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[4] q;
barrier q[0];
barrier q[1];
barrier q[2];
barrier q[3];
barrier q[0], q[1], q[2], q[3];
"""
module = loads(qasm_str)
module.unroll()
Expand Down
23 changes: 20 additions & 3 deletions tests/qasm3/test_device_qubits.py
Original file line number Diff line number Diff line change
Expand Up @@ -57,9 +57,7 @@ def test_barrier():
expected_qasm = """OPENQASM 3.0;
qubit[5] __PYQASM_QUBITS__;
include "stdgates.inc";
barrier __PYQASM_QUBITS__[2];
barrier __PYQASM_QUBITS__[3];
barrier __PYQASM_QUBITS__[4];
barrier __PYQASM_QUBITS__[2], __PYQASM_QUBITS__[3], __PYQASM_QUBITS__[4];
barrier __PYQASM_QUBITS__[1];
"""
result = loads(qasm, device_qubits=5)
Expand Down Expand Up @@ -91,6 +89,25 @@ def test_unrolled_barrier():
check_unrolled_qasm(dumps(result), expected_qasm)


def test_unrolled_barrier_with_range():
qasm = """OPENQASM 3.0;
include "stdgates.inc";
qubit[4] q;
qubit[2] q2;
barrier q[0:2];
barrier q2[0:2];
"""
expected_qasm = """OPENQASM 3.0;
qubit[6] __PYQASM_QUBITS__;
include "stdgates.inc";
barrier __PYQASM_QUBITS__[0], __PYQASM_QUBITS__[1];
barrier __PYQASM_QUBITS__[4], __PYQASM_QUBITS__[5];
"""
result = loads(qasm, device_qubits=6)
result.unroll(unroll_barriers=False, consolidate_qubits=True)
check_unrolled_qasm(dumps(result), expected_qasm)


def test_measurement():
qasm = """OPENQASM 3.0;
include "stdgates.inc";
Expand Down
26 changes: 26 additions & 0 deletions tests/visualization/test_mpl_draw.py
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,32 @@ def test_draw_qasm2_simple():
_check_fig(circ, fig)


def test_draw_barriers():
"""Test drawing barriers with various qubit patterns."""
qasm = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
qubit[2] r;
barrier q[0], q[2];
barrier q[0:2];
barrier r;
barrier r[0], q[1];
"""
circ = loads(qasm)
fig = mpl_draw(circ)
_check_fig(circ, fig)

from matplotlib.patches import Rectangle

ax = fig.axes[0]
# Barriers are drawn as Rectangle patches (one per qubit line per barrier)
# and dashed vlines (added to collections). 4 barriers touching 2+2+2+2=8 lines total.
barrier_patches = [p for p in ax.patches if isinstance(p, Rectangle)]
assert len(barrier_patches) == 8
assert len(ax.collections) > 0


@pytest.mark.mpl_image_compare(baseline_dir="images", filename="bell.png")
def test_draw_bell():
"""Test drawing a simple Bell state circuit."""
Expand Down