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 @@ -25,6 +25,7 @@ Types of changes:
### Removed

### Fixed
- Fixed `remove_idle_qubits(in_place=False)` updating the qubit count on the wrong module: the original module's `num_qubits` was decremented while the returned copy kept the stale pre-removal count. The copy's AST was already correct; only the counters were swapped. ([#336](https://github.com/qBraid/pyqasm/pull/336))
- Fixed `remove_idle_qubits()` raising `KeyError` when the unrolled AST contains operand nodes shared across multiple statements (e.g. the `crz` decomposition) and an idle lower-indexed qubit shifts the register indices. `_remap_qubits` now remaps each operand node exactly once instead of once per statement that references it. ([#332](https://github.com/qBraid/pyqasm/pull/332))
- Fixed `box` duration validation summing `delay` durations across all qubits instead of tracking each qubit's timeline. Delays on disjoint qubits run in parallel, so `box[300ns] { delay[200ns] q[0]; delay[200ns] q[1]; }` was rejected while the identical schedule written as a broadcast delay (`delay[200ns] q;`) was accepted. Delays are now accumulated per qubit and the box is validated against the busiest single timeline; the error message names the offending qubit. Nested boxes now also contribute their declared duration to the enclosing box's timelines (previously the accumulator was reset when an inner box closed, dropping all inner delay accounting). ([#330](https://github.com/qBraid/pyqasm/pull/330))
- Fixed `reset` on a physical qubit rewriting the operand to the internal pulse register, e.g. `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`). Physical qubits are now kept as-is in plain QASM programs, matching how gate and measurement operands already treat them; the rename is still applied for OpenPulse programs, where the pulse visitor expects it. ([#325](https://github.com/qBraid/pyqasm/pull/325))
Expand Down
4 changes: 2 additions & 2 deletions src/pyqasm/modules/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ def remove_idle_qubits(self, in_place: bool = True):
for idle_idx in idle_indices:
del qasm_module._qubit_depths[(reg_name, idle_idx)]

size = self._qubit_registers[reg_name]
size = qasm_module._qubit_registers[reg_name]

if len(idle_indices) == size: # all qubits are idle

Expand All @@ -497,7 +497,7 @@ def remove_idle_qubits(self, in_place: bool = True):
qasm_module._remap_qubits(reg_name, size, idle_indices)

# update the number of qubits
self._num_qubits -= len(idle_indices)
qasm_module._num_qubits -= len(idle_indices)

# the original ast will need to be updated to the unrolled ast as if we call the
# unroll operation again, it will incorrectly choose the original ast WITH THE IDLE QUBITS
Expand Down
22 changes: 12 additions & 10 deletions tests/qasm3/test_transformations.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,23 +45,25 @@ def test_remove_idle_qubits_qasm3_small():
check_unrolled_qasm(dumps(module), expected_qasm3_str)


def test_remove_idle_qubits_qasm3_shared_operand_nodes():
"""Test remove_idle_qubits when a gate decomposition reuses operand nodes"""
def test_remove_idle_qubits_not_in_place():
"""Test remove_idle_qubits(in_place=False) updates the copy and leaves the
original module untouched"""
qasm3_str = """
OPENQASM 3.0;
include "stdgates.inc";
qubit[3] q;
crz(0.5) q[1], q[2];
qubit[4] q;
h q[1];
cx q[1], q[3];
"""
module = loads(qasm3_str)
module.unroll()
assert module.num_qubits == 3
module.remove_idle_qubits()
assert module.num_qubits == 2
new_module = module.remove_idle_qubits(in_place=False)

assert new_module.num_qubits == 2
assert "qubit[2] q;" in dumps(new_module)

unrolled_qasm = dumps(module)
assert "q[2]" not in unrolled_qasm
assert "cx q[0], q[1];" in unrolled_qasm
assert module.num_qubits == 4
assert "qubit[4] q;" in dumps(module)


def test_remove_idle_qubits_qasm3():
Expand Down
Loading