Skip to content

Commit c3ba4bf

Browse files
committed
Add ContractIdleWiresInControlFlow optimisation pass
This transpiler pass removes data dependencies on idle qubits from control-flow operations. For example, given a circuit such as:: from qiskit.circuit import QuantumCircuit qc = QuantumCircuit(1, 1) qc.x(0) with qc.if_test((qc.clbits[0], True)): qc.x(0) qc.x(0) qc.x(0) the current optimisation passes will collapse the inner control-flow block to the identity, but the qubit dependency will remain, preventing the outer two X gates from being cancelled. This pass removes the now-spurious dependency, making it possible to detect and remove the two X gates in a follow-up loop iteration. As an accidental side-effect of their algorithms, the control-flow-aware routing passes currently do this when they run. This aims to move the logic into a suitable place to run before routing (so the spurious dependency never arises in routing in the first place) and in the low-level optimisation stage. The aim of this pass is also to centralise the logic, so when the addition of the new `box` scope with different semantics around whether a wire is truly idle in the box or not, the routers aren't accidentally breaking them, and it's clearer when the modifications happen.
1 parent 260f41f commit c3ba4bf

File tree

5 files changed

+298
-0
lines changed

5 files changed

+298
-0
lines changed

qiskit/transpiler/passes/__init__.py

+2
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@
9393
OptimizeAnnotated
9494
Split2QUnitaries
9595
RemoveIdentityEquivalent
96+
ContractIdleWiresInControlFlow
9697
9798
Calibration
9899
=============
@@ -248,6 +249,7 @@
248249
from .optimization import OptimizeAnnotated
249250
from .optimization import RemoveIdentityEquivalent
250251
from .optimization import Split2QUnitaries
252+
from .optimization import ContractIdleWiresInControlFlow
251253

252254
# circuit analysis
253255
from .analysis import ResourceEstimation

qiskit/transpiler/passes/optimization/__init__.py

+1
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,4 @@
4040
from .remove_identity_equiv import RemoveIdentityEquivalent
4141
from .split_2q_unitaries import Split2QUnitaries
4242
from .collect_and_collapse import CollectAndCollapse
43+
from .contract_idle_wires_in_control_flow import ContractIdleWiresInControlFlow
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2025
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
"""Contract control-flow operations that contain idle wires."""
14+
15+
from qiskit.circuit import Qubit, Clbit, QuantumCircuit
16+
from qiskit.dagcircuit import DAGCircuit, DAGOpNode
17+
from qiskit.transpiler.basepasses import TransformationPass
18+
19+
20+
class ContractIdleWiresInControlFlow(TransformationPass):
21+
"""Remove idle qubits from control-flow operations of a :class:`.DAGCircuit`."""
22+
23+
def run(self, dag):
24+
# `control_flow_op_nodes` is eager and doesn't borrow; we're mutating the DAG in the loop.
25+
for node in dag.control_flow_op_nodes():
26+
inst = node._to_circuit_instruction()
27+
new_inst = _contract_control_flow(inst)
28+
if new_inst is inst:
29+
# No top-level contraction; nothing to do.
30+
continue
31+
replacement = DAGCircuit()
32+
# Dictionaries to retain insertion order for reproducibility, and because we can
33+
# then re-use them as mapping dictionaries.
34+
qubits, clbits, vars_ = {}, {}, {}
35+
for _, _, wire in dag.edges(node):
36+
if isinstance(wire, Qubit):
37+
qubits[wire] = wire
38+
elif isinstance(wire, Clbit):
39+
clbits[wire] = wire
40+
else:
41+
vars_[wire] = wire
42+
replacement.add_qubits(list(qubits))
43+
replacement.add_clbits(list(clbits))
44+
for var in vars_:
45+
replacement.add_captured_var(var)
46+
replacement._apply_op_node_back(DAGOpNode.from_instruction(new_inst))
47+
# The replacement DAG is defined over all the same qubits, but with the correct
48+
# qubits now explicitly marked as idle, so everything gets linked up correctly.
49+
dag.substitute_node_with_dag(
50+
node, replacement, wires=qubits | clbits | vars_, propagate_condition=False
51+
)
52+
return dag
53+
54+
55+
def _contract_control_flow(inst):
56+
"""Contract a `CircuitInstruction` containing a control-flow operation.
57+
58+
Returns the input object by the same reference if there's no contraction to be done at the call
59+
site, though nested control-flow ops may have been contracted in place."""
60+
op = inst.operation
61+
idle = set(inst.qubits)
62+
for block in op.blocks:
63+
qubit_map = dict(zip(block.qubits, inst.qubits))
64+
for i, inner in enumerate(block.data):
65+
if inner.is_control_flow():
66+
# In `QuantumCircuit` it's easy to replace an instruction with a narrower one, so it
67+
# doesn't matter much if this is replacing it with itself.
68+
block.data[i] = inner = _contract_control_flow(inner)
69+
for qubit in inner.qubits:
70+
idle.discard(qubit_map[qubit])
71+
# If a box, we still want the prior side-effect of contracting any internal control-flow
72+
# operations (optimisations are still valid _within_ a box), but we don't want to contract the
73+
# box itself. If there's no idle qubits, we're also done here.
74+
if not idle or inst.name == "box":
75+
return inst
76+
77+
def contract(block):
78+
out = QuantumCircuit(
79+
name=block.name,
80+
global_phase=block.global_phase,
81+
metadata=block.metadata,
82+
captures=block.iter_captured_vars(),
83+
)
84+
out.add_bits(
85+
[
86+
block_qubit
87+
for (block_qubit, inst_qubit) in zip(block.qubits, inst.qubits)
88+
if inst_qubit not in idle
89+
]
90+
)
91+
out.add_bits(block.clbits)
92+
for creg in block.cregs:
93+
out.add_register(creg)
94+
# Control-flow ops can only have captures and locals, and we already added the captures.
95+
for var in block.iter_declared_vars():
96+
out.add_uninitialized_var(var)
97+
for inner in block:
98+
out._append(inner)
99+
return out
100+
101+
return inst.replace(
102+
operation=op.replace_blocks(contract(block) for block in op.blocks),
103+
qubits=[qubit for qubit in inst.qubits if qubit not in idle],
104+
)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
---
2+
features_transpiler:
3+
- |
4+
A new transpiler pass, :class:`.ContractIdleWiresInControlFlow`, is available from
5+
:mod:`qiskit.transpiler.passes`. This pass removes qubits from control-flow blocks if the
6+
semantics allow this, and the qubit is idle throughout the control-flow operation. Previously,
7+
the routing stage of the preset pass managers might have done this as an accidental side-effect
8+
of how they worked, but the behavior is now more properly placed in an optimization pass.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
# This code is part of Qiskit.
2+
#
3+
# (C) Copyright IBM 2025
4+
#
5+
# This code is licensed under the Apache License, Version 2.0. You may
6+
# obtain a copy of this license in the LICENSE.txt file in the root directory
7+
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0.
8+
#
9+
# Any modifications or derivative works of this code must retain this
10+
# copyright notice, and modified files need to carry a notice indicating
11+
# that they have been altered from the originals.
12+
13+
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring
14+
15+
from qiskit.circuit import QuantumCircuit, ClassicalRegister, QuantumRegister
16+
from qiskit.circuit.classical import expr, types
17+
from qiskit.transpiler.passes import ContractIdleWiresInControlFlow
18+
19+
from test import QiskitTestCase # pylint: disable=wrong-import-order
20+
21+
22+
class TestContractIdleWiresInControlFlow(QiskitTestCase):
23+
def test_simple_body(self):
24+
qc = QuantumCircuit(3, 1)
25+
with qc.while_loop((qc.clbits[0], False)):
26+
qc.cx(0, 1)
27+
qc.noop(2)
28+
29+
expected = QuantumCircuit(3, 1)
30+
with expected.while_loop((expected.clbits[0], False)):
31+
expected.cx(0, 1)
32+
33+
self.assertEqual(ContractIdleWiresInControlFlow()(qc), expected)
34+
35+
def test_nothing_to_do(self):
36+
qc = QuantumCircuit(3, 1)
37+
with qc.for_loop(range(3)):
38+
qc.h(0)
39+
qc.cx(0, 1)
40+
self.assertEqual(ContractIdleWiresInControlFlow()(qc), qc)
41+
42+
def test_disparate_if_else_left_alone(self):
43+
qc = QuantumCircuit(3, 1)
44+
# The true body only uses 0, the false body only uses (1, 2), but because they're part of
45+
# the shared op, there is no valid contraction here.
46+
with qc.if_test((qc.clbits[0], True)) as else_:
47+
qc.h(0)
48+
with else_:
49+
qc.cx(1, 2)
50+
self.assertEqual(ContractIdleWiresInControlFlow()(qc), qc)
51+
52+
def test_contract_if_else_both_bodies(self):
53+
qc = QuantumCircuit(3, 1)
54+
# Explicit idle in the true body only.
55+
with qc.if_test((qc.clbits[0], True)) as else_:
56+
qc.h(0)
57+
qc.cx(0, 2)
58+
qc.noop(1)
59+
with else_:
60+
qc.cz(0, 2)
61+
# Explicit idle in the false body only.
62+
with qc.if_test((qc.clbits[0], True)) as else_:
63+
qc.h(0)
64+
qc.cx(0, 1)
65+
with else_:
66+
qc.cz(0, 1)
67+
qc.noop(2)
68+
# Explicit idle in both bodies.
69+
with qc.if_test((qc.clbits[0], True)) as else_:
70+
qc.h(1)
71+
qc.cx(1, 2)
72+
qc.noop(0)
73+
with else_:
74+
qc.cz(1, 2)
75+
qc.noop(0)
76+
77+
expected = QuantumCircuit(3, 1)
78+
with expected.if_test((expected.clbits[0], True)) as else_:
79+
expected.h(0)
80+
expected.cx(0, 2)
81+
with else_:
82+
expected.cz(0, 2)
83+
with expected.if_test((expected.clbits[0], True)) as else_:
84+
expected.h(0)
85+
expected.cx(0, 1)
86+
with else_:
87+
expected.cz(0, 1)
88+
with expected.if_test((expected.clbits[0], True)) as else_:
89+
expected.h(1)
90+
expected.cx(1, 2)
91+
with else_:
92+
expected.cz(1, 2)
93+
94+
self.assertEqual(ContractIdleWiresInControlFlow()(qc), expected)
95+
96+
def test_recursively_contract(self):
97+
qc = QuantumCircuit(3, 1)
98+
with qc.if_test((qc.clbits[0], True)):
99+
qc.h(0)
100+
with qc.if_test((qc.clbits[0], True)):
101+
qc.cx(0, 1)
102+
qc.noop(2)
103+
with qc.while_loop((qc.clbits[0], True)):
104+
with qc.if_test((qc.clbits[0], True)) as else_:
105+
qc.h(0)
106+
qc.noop(1, 2)
107+
with else_:
108+
qc.cx(0, 1)
109+
qc.noop(2)
110+
111+
expected = QuantumCircuit(3, 1)
112+
with expected.if_test((expected.clbits[0], True)):
113+
expected.h(0)
114+
with expected.if_test((expected.clbits[0], True)):
115+
expected.cx(0, 1)
116+
with expected.while_loop((expected.clbits[0], True)):
117+
with expected.if_test((expected.clbits[0], True)) as else_:
118+
expected.h(0)
119+
with else_:
120+
expected.cx(0, 1)
121+
122+
actual = ContractIdleWiresInControlFlow()(qc)
123+
self.assertNotEqual(qc, actual) # Smoke test.
124+
self.assertEqual(actual, expected)
125+
126+
def test_handles_vars_in_contraction(self):
127+
a = expr.Var.new("a", types.Bool())
128+
b = expr.Var.new("b", types.Uint(8))
129+
c = expr.Var.new("c", types.Bool())
130+
131+
qc = QuantumCircuit(3, inputs=[a])
132+
qc.add_var(b, 5)
133+
with qc.if_test(a):
134+
qc.add_var(c, False)
135+
with qc.if_test(c):
136+
qc.x(0)
137+
qc.noop(1, 2)
138+
with qc.switch(b) as case:
139+
with case(0):
140+
qc.x(0)
141+
with case(1):
142+
qc.noop(0, 1)
143+
with case(case.DEFAULT):
144+
with qc.if_test(a):
145+
qc.x(0)
146+
qc.noop(1, 2)
147+
148+
expected = QuantumCircuit(3, inputs=[a])
149+
expected.add_var(b, 5)
150+
with expected.if_test(a):
151+
expected.add_var(c, False)
152+
with expected.if_test(c):
153+
expected.x(0)
154+
with expected.switch(b) as case:
155+
with case(0):
156+
expected.x(0)
157+
with case(1):
158+
pass
159+
with case(case.DEFAULT):
160+
with expected.if_test(a):
161+
expected.x(0)
162+
163+
actual = ContractIdleWiresInControlFlow()(qc)
164+
self.assertNotEqual(qc, actual) # Smoke test.
165+
self.assertEqual(actual, expected)
166+
167+
def test_handles_registers_in_contraction(self):
168+
qr = QuantumRegister(3, "q")
169+
cr1 = ClassicalRegister(3, "cr1")
170+
cr2 = ClassicalRegister(3, "cr2")
171+
172+
qc = QuantumCircuit(qr, cr1, cr2)
173+
with qc.if_test((cr1, 3)):
174+
with qc.if_test((cr2, 3)):
175+
qc.noop(0, 1, 2)
176+
expected = QuantumCircuit(qr, cr1, cr2)
177+
with expected.if_test((cr1, 3)):
178+
with expected.if_test((cr2, 3)):
179+
pass
180+
181+
actual = ContractIdleWiresInControlFlow()(qc)
182+
self.assertNotEqual(qc, actual) # Smoke test.
183+
self.assertEqual(actual, expected)

0 commit comments

Comments
 (0)