-
Notifications
You must be signed in to change notification settings - Fork 2.5k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
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.
- Loading branch information
1 parent
69bb439
commit ff133cf
Showing
5 changed files
with
298 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
104 changes: 104 additions & 0 deletions
104
qiskit/transpiler/passes/optimization/contract_idle_wires_in_control_flow.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,104 @@ | ||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 2025 | ||
# | ||
# This code is licensed under the Apache License, Version 2.0. You may | ||
# obtain a copy of this license in the LICENSE.txt file in the root directory | ||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
# | ||
# Any modifications or derivative works of this code must retain this | ||
# copyright notice, and modified files need to carry a notice indicating | ||
# that they have been altered from the originals. | ||
|
||
"""Contract control-flow operations that contain idle wires.""" | ||
|
||
from qiskit.circuit import Qubit, Clbit, QuantumCircuit | ||
from qiskit.dagcircuit import DAGCircuit, DAGOpNode | ||
from qiskit.transpiler.basepasses import TransformationPass | ||
|
||
|
||
class ContractIdleWiresInControlFlow(TransformationPass): | ||
"""Remove idle qubits from control-flow operations of a :class:`.DAGCircuit`.""" | ||
|
||
def run(self, dag): | ||
# `control_flow_op_nodes` is eager and doesn't borrow; we're mutating the DAG in the loop. | ||
for node in dag.control_flow_op_nodes(): | ||
inst = node._to_circuit_instruction() | ||
new_inst = _contract_control_flow(inst) | ||
if new_inst is inst: | ||
# No top-level contraction; nothing to do. | ||
continue | ||
replacement = DAGCircuit() | ||
# Dictionaries to retain insertion order for reproducibility, and because we can | ||
# then re-use them as mapping dictionaries. | ||
qubits, clbits, vars_ = {}, {}, {} | ||
for _, _, wire in dag.edges(node): | ||
if isinstance(wire, Qubit): | ||
qubits[wire] = wire | ||
elif isinstance(wire, Clbit): | ||
clbits[wire] = wire | ||
else: | ||
vars_[wire] = wire | ||
replacement.add_qubits(list(qubits)) | ||
replacement.add_clbits(list(clbits)) | ||
for var in vars_: | ||
replacement.add_captured_var(var) | ||
replacement._apply_op_node_back(DAGOpNode.from_instruction(new_inst)) | ||
# The replacement DAG is defined over all the same qubits, but with the correct | ||
# qubits now explicitly marked as idle, so everything gets linked up correctly. | ||
dag.substitute_node_with_dag( | ||
node, replacement, wires=qubits | clbits | vars_, propagate_condition=False | ||
) | ||
return dag | ||
|
||
|
||
def _contract_control_flow(inst): | ||
"""Contract a `CircuitInstruction` containing a control-flow operation. | ||
Returns the input object by the same reference if there's no contraction to be done at the call | ||
site, though nested control-flow ops may have been contracted in place.""" | ||
op = inst.operation | ||
idle = set(inst.qubits) | ||
for block in op.blocks: | ||
qubit_map = dict(zip(block.qubits, inst.qubits)) | ||
for i, inner in enumerate(block.data): | ||
if inner.is_control_flow(): | ||
# In `QuantumCircuit` it's easy to replace an instruction with a narrower one, so it | ||
# doesn't matter much if this is replacing it with itself. | ||
block.data[i] = inner = _contract_control_flow(inner) | ||
for qubit in inner.qubits: | ||
idle.discard(qubit_map[qubit]) | ||
# If a box, we still want the prior side-effect of contracting any internal control-flow | ||
# operations (optimisations are still valid _within_ a box), but we don't want to contract the | ||
# box itself. If there's no idle qubits, we're also done here. | ||
if not idle or inst.name == "box": | ||
return inst | ||
|
||
def contract(block): | ||
out = QuantumCircuit( | ||
name=block.name, | ||
global_phase=block.global_phase, | ||
metadata=block.metadata, | ||
captures=block.iter_captured_vars(), | ||
) | ||
out.add_bits( | ||
[ | ||
block_qubit | ||
for (block_qubit, inst_qubit) in zip(block.qubits, inst.qubits) | ||
if inst_qubit not in idle | ||
] | ||
) | ||
out.add_bits(block.clbits) | ||
for creg in block.cregs: | ||
out.add_register(creg) | ||
# Control-flow ops can only have captures and locals, and we already added the captures. | ||
for var in block.iter_declared_vars(): | ||
out.add_uninitialized_var(var) | ||
for inner in block: | ||
out._append(inner) | ||
return out | ||
|
||
return inst.replace( | ||
operation=op.replace_blocks(contract(block) for block in op.blocks), | ||
qubits=[qubit for qubit in inst.qubits if qubit not in idle], | ||
) |
8 changes: 8 additions & 0 deletions
8
releasenotes/notes/contract-idle-wires-in-control-flow-264f7c92396b217e.yaml
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,8 @@ | ||
--- | ||
features_transpiler: | ||
- | | ||
A new transpiler pass, :class:`.ContractIdleWiresInControlFlow`, is available from | ||
:mod:`qiskit.transpiler.passes`. This pass removes qubits from control-flow blocks if the | ||
semantics allow this, and the qubit is idle throughout the control-flow operation. Previously, | ||
the routing stage of the preset pass managers might have done this as an accidental side-effect | ||
of how they worked, but the behavior is now more properly placed in an optimization pass. |
183 changes: 183 additions & 0 deletions
183
test/python/transpiler/test_contract_idle_wires_in_control_flow.py
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,183 @@ | ||
# This code is part of Qiskit. | ||
# | ||
# (C) Copyright IBM 2025 | ||
# | ||
# This code is licensed under the Apache License, Version 2.0. You may | ||
# obtain a copy of this license in the LICENSE.txt file in the root directory | ||
# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. | ||
# | ||
# Any modifications or derivative works of this code must retain this | ||
# copyright notice, and modified files need to carry a notice indicating | ||
# that they have been altered from the originals. | ||
|
||
# pylint: disable=missing-class-docstring,missing-module-docstring,missing-function-docstring | ||
|
||
from qiskit.circuit import QuantumCircuit, ClassicalRegister, QuantumRegister | ||
from qiskit.circuit.classical import expr, types | ||
from qiskit.transpiler.passes import ContractIdleWiresInControlFlow | ||
|
||
from test import QiskitTestCase # pylint: disable=wrong-import-order | ||
|
||
|
||
class TestContractIdleWiresInControlFlow(QiskitTestCase): | ||
def test_simple_body(self): | ||
qc = QuantumCircuit(3, 1) | ||
with qc.while_loop((qc.clbits[0], False)): | ||
qc.cx(0, 1) | ||
qc.noop(2) | ||
|
||
expected = QuantumCircuit(3, 1) | ||
with expected.while_loop((expected.clbits[0], False)): | ||
expected.cx(0, 1) | ||
|
||
self.assertEqual(ContractIdleWiresInControlFlow()(qc), expected) | ||
|
||
def test_nothing_to_do(self): | ||
qc = QuantumCircuit(3, 1) | ||
with qc.for_loop(range(3)): | ||
qc.h(0) | ||
qc.cx(0, 1) | ||
self.assertEqual(ContractIdleWiresInControlFlow()(qc), qc) | ||
|
||
def test_disparate_if_else_left_alone(self): | ||
qc = QuantumCircuit(3, 1) | ||
# The true body only uses 0, the false body only uses (1, 2), but because they're part of | ||
# the shared op, there is no valid contraction here. | ||
with qc.if_test((qc.clbits[0], True)) as else_: | ||
qc.h(0) | ||
with else_: | ||
qc.cx(1, 2) | ||
self.assertEqual(ContractIdleWiresInControlFlow()(qc), qc) | ||
|
||
def test_contract_if_else_both_bodies(self): | ||
qc = QuantumCircuit(3, 1) | ||
# Explicit idle in the true body only. | ||
with qc.if_test((qc.clbits[0], True)) as else_: | ||
qc.h(0) | ||
qc.cx(0, 2) | ||
qc.noop(1) | ||
with else_: | ||
qc.cz(0, 2) | ||
# Explicit idle in the false body only. | ||
with qc.if_test((qc.clbits[0], True)) as else_: | ||
qc.h(0) | ||
qc.cx(0, 1) | ||
with else_: | ||
qc.cz(0, 1) | ||
qc.noop(2) | ||
# Explicit idle in both bodies. | ||
with qc.if_test((qc.clbits[0], True)) as else_: | ||
qc.h(1) | ||
qc.cx(1, 2) | ||
qc.noop(0) | ||
with else_: | ||
qc.cz(1, 2) | ||
qc.noop(0) | ||
|
||
expected = QuantumCircuit(3, 1) | ||
with expected.if_test((expected.clbits[0], True)) as else_: | ||
expected.h(0) | ||
expected.cx(0, 2) | ||
with else_: | ||
expected.cz(0, 2) | ||
with expected.if_test((expected.clbits[0], True)) as else_: | ||
expected.h(0) | ||
expected.cx(0, 1) | ||
with else_: | ||
expected.cz(0, 1) | ||
with expected.if_test((expected.clbits[0], True)) as else_: | ||
expected.h(1) | ||
expected.cx(1, 2) | ||
with else_: | ||
expected.cz(1, 2) | ||
|
||
self.assertEqual(ContractIdleWiresInControlFlow()(qc), expected) | ||
|
||
def test_recursively_contract(self): | ||
qc = QuantumCircuit(3, 1) | ||
with qc.if_test((qc.clbits[0], True)): | ||
qc.h(0) | ||
with qc.if_test((qc.clbits[0], True)): | ||
qc.cx(0, 1) | ||
qc.noop(2) | ||
with qc.while_loop((qc.clbits[0], True)): | ||
with qc.if_test((qc.clbits[0], True)) as else_: | ||
qc.h(0) | ||
qc.noop(1, 2) | ||
with else_: | ||
qc.cx(0, 1) | ||
qc.noop(2) | ||
|
||
expected = QuantumCircuit(3, 1) | ||
with expected.if_test((expected.clbits[0], True)): | ||
expected.h(0) | ||
with expected.if_test((expected.clbits[0], True)): | ||
expected.cx(0, 1) | ||
with expected.while_loop((expected.clbits[0], True)): | ||
with expected.if_test((expected.clbits[0], True)) as else_: | ||
expected.h(0) | ||
with else_: | ||
expected.cx(0, 1) | ||
|
||
actual = ContractIdleWiresInControlFlow()(qc) | ||
self.assertNotEqual(qc, actual) # Smoke test. | ||
self.assertEqual(actual, expected) | ||
|
||
def test_handles_vars_in_contraction(self): | ||
a = expr.Var.new("a", types.Bool()) | ||
b = expr.Var.new("b", types.Uint(8)) | ||
c = expr.Var.new("c", types.Bool()) | ||
|
||
qc = QuantumCircuit(3, inputs=[a]) | ||
qc.add_var(b, 5) | ||
with qc.if_test(a): | ||
qc.add_var(c, False) | ||
with qc.if_test(c): | ||
qc.x(0) | ||
qc.noop(1, 2) | ||
with qc.switch(b) as case: | ||
with case(0): | ||
qc.x(0) | ||
with case(1): | ||
qc.noop(0, 1) | ||
with case(case.DEFAULT): | ||
with qc.if_test(a): | ||
qc.x(0) | ||
qc.noop(1, 2) | ||
|
||
expected = QuantumCircuit(3, inputs=[a]) | ||
expected.add_var(b, 5) | ||
with expected.if_test(a): | ||
expected.add_var(c, False) | ||
with expected.if_test(c): | ||
expected.x(0) | ||
with expected.switch(b) as case: | ||
with case(0): | ||
expected.x(0) | ||
with case(1): | ||
pass | ||
with case(case.DEFAULT): | ||
with expected.if_test(a): | ||
expected.x(0) | ||
|
||
actual = ContractIdleWiresInControlFlow()(qc) | ||
self.assertNotEqual(qc, actual) # Smoke test. | ||
self.assertEqual(actual, expected) | ||
|
||
def test_handles_registers_in_contraction(self): | ||
qr = QuantumRegister(3, "q") | ||
cr1 = ClassicalRegister(3, "cr1") | ||
cr2 = ClassicalRegister(3, "cr2") | ||
|
||
qc = QuantumCircuit(qr, cr1, cr2) | ||
with qc.if_test((cr1, 3)): | ||
with qc.if_test((cr2, 3)): | ||
qc.noop(0, 1, 2) | ||
expected = QuantumCircuit(qr, cr1, cr2) | ||
with expected.if_test((cr1, 3)): | ||
with expected.if_test((cr2, 3)): | ||
pass | ||
|
||
actual = ContractIdleWiresInControlFlow()(qc) | ||
self.assertNotEqual(qc, actual) # Smoke test. | ||
self.assertEqual(actual, expected) |