Skip to content
Open
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 external and verbatim-box gates counting the depth of the decomposition they skipped: `unroll(external_gates=["crz"])` on a single `crz` reported `depth() == 12` while emitting one statement. An external gate now records its own depth, like the custom-gate path already did. ([#352](https://github.com/qBraid/pyqasm/issues/352))
- Fixed inaccurate `device_qubits` entry in `QasmModule.unroll()` docstring ([#349](https://github.com/qBraid/pyqasm/pull/349))
- Fixed `remove_idle_qubits()` and `reverse_qubit_order()` ignoring statements nested inside `box` and `if` blocks. Top-level operands were rewritten while nested ones kept their old indices, so the result silently addressed the wrong qubits — and when a nested index fell outside the shrunken register, the output was not a loadable program at all. Both passes now walk nested bodies, as do `has_measurements()` / `remove_measurements()` and `has_barriers()` / `remove_barriers()`; a box left empty by a removal is dropped, since pyqasm rejects a box with no statements. Two consequences of the same blind spot are fixed alongside: a qubit operated on only inside an `if` block no longer counts as idle, and `remove_idle_qubits()` no longer raises `AssertionError` on a program that mixes physical qubits with declared registers. ([#345](https://github.com/qBraid/pyqasm/pull/345))
- Fixed `unroll(consolidate_qubits=True)` raising `AttributeError: 'str' object has no attribute 'name'` for any gate applied to a physical qubit, e.g. `h $1;`. Consolidation assumed every gate operand was an `IndexedIdentifier`, but a physical qubit survives unrolling as `Identifier("$1")`. Physical qubits are absolute hardware indices belonging to no declared register, so they are now left as written — matching how `measure`, `reset` and `barrier` already treat them. ([#344](https://github.com/qBraid/pyqasm/pull/344))
Expand Down
21 changes: 19 additions & 2 deletions src/pyqasm/visitor.py
Original file line number Diff line number Diff line change
Expand Up @@ -1364,8 +1364,15 @@ def _visit_external_gate_operation(
# Don't need to check if custom gate exists, since we just validated the call
gate_qubit_count = len(self._custom_gates[gate_name].qubits)
else:
# Ignore result, this is just for validation
self._visit_basic_gate_operation(operation)
# Ignore result, this is just for validation. Suppress depth recording so the
# skipped decomposition does not count; the gate's own depth is recorded
# below (issue #352)
prev_recording = self._recording_ext_gate_depth
self._recording_ext_gate_depth = True
try:
self._visit_basic_gate_operation(operation)
finally:
self._recording_ext_gate_depth = prev_recording
# Don't need to check if basic gate exists, since we just validated the call
_, gate_qubit_count = map_qasm_op_to_callable(operation)

Expand Down Expand Up @@ -1399,6 +1406,16 @@ def gate_function(*qubits):
all_targets = self._unroll_multiple_target_qubits(operation, gate_qubit_count)
result = self._broadcast_gate_operation(gate_function, all_targets)

# record the external gate's own depth; the custom-gate path has already done so
if gate_name not in self._custom_gates:
if not self._in_branching_statement:
self._update_qubit_depth_for_gate(all_targets, ctrls)
else:
for qubit_subset in all_targets + [ctrls]:
for qubit in qubit_subset:
qubit_name, qubit_idx = QasmVisitor._get_qubit_name_and_id(qubit)
self._mark_branch_qubit(qubit_name, qubit_idx)

# check for any duplicates
for final_gate in result:
Qasm3Analyzer.verify_gate_qubits(final_gate, operation.span)
Expand Down
43 changes: 39 additions & 4 deletions tests/qasm3/test_depth.py
Original file line number Diff line number Diff line change
Expand Up @@ -680,12 +680,47 @@ def test_gate_depth_decomposable_gates(input_qasm_str, before_decompose, after_d


@pytest.mark.parametrize(
["input_qasm_str", "before_decompose", "after_decompose"],
[(QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH, 2, 2)],
["input_qasm_str", "external_gates", "before_decompose", "after_decompose"],
[
(QASM3_DECOMPOSE_CUSTOM_GATE_DEPTH, ["custom_crx", "custom_rccx"], 2, 2),
(QASM3_DECOMPOSE_GATE_DEPTH, ["crx", "rccx"], 2, 2),
],
)
def test_gate_depth_decomposable_external_gates(input_qasm_str, before_decompose, after_decompose):
def test_gate_depth_decomposable_external_gates(
input_qasm_str, external_gates, before_decompose, after_decompose
):
"""An external gate skips its decomposition, so it must not count the depth of
the decomposition it skipped (issue #352)"""
result = loads(input_qasm_str)
result._external_gates = ["custom_crx", "custom_rccx"]
result._external_gates = external_gates
assert result.depth(decompose_native_gates=False) == before_decompose
# by default its true
assert result.depth() == after_decompose


def test_external_basic_gate_counts_own_depth():
"""One external crz statement is emitted, so it counts as depth 1 (issue #352)"""
qasm3_string = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
crz(0.5) q[0], q[1];
"""
result = loads(qasm3_string)
result.unroll(external_gates=["crz"])
assert result.depth() == 1


def test_external_basic_gate_depth_with_neighbours():
"""External gate depth composes with surrounding gates like any single gate"""
qasm3_string = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
x q[0];
crz(0.5) q[0], q[1];
x q[1];
"""
result = loads(qasm3_string)
result.unroll(external_gates=["crz"])
assert result.depth() == 3
17 changes: 17 additions & 0 deletions tests/qasm3/test_pragma.py
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,23 @@ def test_verbatim_custom_gate_counts_once_towards_depth():
assert module.depth() == 1


def test_verbatim_basic_gate_counts_once_towards_depth():
"""A decomposable stdgates gate inside a verbatim box is emitted as written,
so its depth is that of one gate, not of the skipped decomposition (issue #352)."""
qasm_str = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[2] q;
#pragma braket verbatim
box {
crz(0.5) q[0], q[1];
}
"""
module = loads(qasm_str)
module.unroll()
assert module.depth() == 1


def test_verbatim_marker_does_not_escape_a_box():
"""A pragma at the end of a box body must not mark the next box verbatim.

Expand Down
Loading