You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
QasmModule.remove_idle_qubits() raises KeyError on any program whose unrolled AST contains aliased operand nodes — the same IndexedIdentifier object reused across multiple statements — whenever removing an idle qubit shifts the indices of the qubits those statements use. The minimal trigger is a single crz with an idle lower-indexed qubit.
Found while testing the statevector simulator work in #316, but the bug is in modules/base.py and pre-dates that branch (reproduces on main / released 1.0.3).
File "src/pyqasm/modules/base.py", line 387, in _remap_qubits
bit.indices[0][0].value = idx_map[old_idx]
KeyError: 0
Root cause
unroll()'s decomposition of crz emits gate statements whose qubit operands are the same IndexedIdentifier object, not copies. For the program above, one q[2] node is shared by 11 statements and the cx operand pair is shared by both cx gates:
fromcollectionsimportCountergates= [sforsinm._unrolled_ast.statementsiftype(s).__name__=="QuantumGate"]
Counter(id(q) forsingatesforqins.qubits).most_common(2)
# [(<id>, 12), (<id>, 2)] <- one operand object reused 12 times
_remap_qubits (base.py:380-387) then walks every statement and rewrites bit.indices[0][0].valuein place. A node shared by k statements is remapped k times: with idx_map = {1: 0, 2: 1} the shared q[2] operand goes 2 -> 1 on the first visit, 1 -> 0 on the second, and the third visit looks up idx_map[0] -> KeyError: 0.
When the remap happens to be the identity (no lower-indexed idle qubit), the repeated remapping is silently harmless, which is why the bug stayed hidden.
Notes
Any decomposition that reuses operand nodes triggers this; crz is just the minimal known case (cx, swap, ccx unroll with fresh nodes and are unaffected).
Two possible fixes, not mutually exclusive:
unroll() should emit deep-copied operand nodes (aliasing in an AST that later gets mutated in place is a footgun beyond this one crash);
_remap_qubits should be idempotent per node, e.g. track visited node ids or rebuild operands instead of mutating shared ones.
Summary
QasmModule.remove_idle_qubits()raisesKeyErroron any program whose unrolled AST contains aliased operand nodes — the sameIndexedIdentifierobject reused across multiple statements — whenever removing an idle qubit shifts the indices of the qubits those statements use. The minimal trigger is a singlecrzwith an idle lower-indexed qubit.Found while testing the statevector simulator work in #316, but the bug is in
modules/base.pyand pre-dates that branch (reproduces onmain/ released 1.0.3).Reproduction
Root cause
unroll()'s decomposition ofcrzemits gate statements whose qubit operands are the sameIndexedIdentifierobject, not copies. For the program above, oneq[2]node is shared by 11 statements and thecxoperand pair is shared by bothcxgates:_remap_qubits(base.py:380-387) then walks every statement and rewritesbit.indices[0][0].valuein place. A node shared by k statements is remapped k times: withidx_map = {1: 0, 2: 1}the sharedq[2]operand goes2 -> 1on the first visit,1 -> 0on the second, and the third visit looks upidx_map[0]->KeyError: 0.When the remap happens to be the identity (no lower-indexed idle qubit), the repeated remapping is silently harmless, which is why the bug stayed hidden.
Notes
crzis just the minimal known case (cx,swap,ccxunroll with fresh nodes and are unaffected).unroll()should emit deep-copied operand nodes (aliasing in an AST that later gets mutated in place is a footgun beyond this one crash);_remap_qubitsshould be idempotent per node, e.g. track visited node ids or rebuild operands instead of mutating shared ones.Simulator.run(str)convenience path in Optimize statevector simulator preprocessing #316, which callsremove_idle_qubits()internally.🤖 Filed with Claude Code