Skip to content

Commit

Permalink
Do not contract control-flow operations during SabreSwap (Qiskit#13790
Browse files Browse the repository at this point in the history
)

The previous implementation of control-flow handling for `SabreSwap`
caused control-flow blocks to contract away from idle qubit wires as a
side effect of its algorithm.  This will no longer be always valid with
the addition of `Box`, and besides, that optimisation isn't part of a
routing algorithm's responsibilities, so it's cleaner to have it in a
separate pass (now `ContractIdleWiresInControlFlow`) where it can be
applied at times other than routing, like in the optimisation loop.
  • Loading branch information
jakelishman authored Feb 11, 2025
1 parent 7995cab commit 288d2f4
Show file tree
Hide file tree
Showing 3 changed files with 85 additions and 33 deletions.
12 changes: 11 additions & 1 deletion qiskit/transpiler/passes/routing/sabre_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,12 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout):
block_root_logical_map = {
inner: root_logical_map[outer] for inner, outer in zip(block.qubits, node.qargs)
}
# The virtual qubits originally incident to the block should be retained even if not
# actually used; the user might be marking them out specially (like in `box`).
# There are other transpiler passes to remove those dependencies if desired.
incident_qubits = {
layout.virtual_to_physical(block_root_logical_map[bit]) for bit in block.qubits
}
block_dag, block_layout = recurse(
empty_dag(block),
circuit_to_dag_dict[id(block)],
Expand All @@ -429,7 +435,11 @@ def recurse(dest_dag, source_dag, result, root_logical_map, layout):
)
apply_swaps(block_dag, block_result.swap_epilogue, block_layout)
mapped_block_dags.append(block_dag)
idle_qubits.intersection_update(block_dag.idle_wires())
idle_qubits.intersection_update(
bit
for bit in block_dag.idle_wires()
if block_dag.find_bit(bit).index not in incident_qubits
)

mapped_blocks = []
for mapped_block_dag in mapped_block_dags:
Expand Down
6 changes: 6 additions & 0 deletions releasenotes/notes/sabre-contraction-cbb7bffaeb826d67.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
fixes:
- |
:class:`.SabreSwap` will no longer contract idle qubit wires out of control-flow blocks during routing.
This was generally a valid optimization, but not an expected side effect of a routing pass.
You can now use the :class:`.ContractIdleWiresInControlFlow` pass to perform this contraction.
100 changes: 68 additions & 32 deletions test/python/transpiler/test_sabre_swap.py
Original file line number Diff line number Diff line change
Expand Up @@ -492,12 +492,12 @@ def test_pre_if_else_route(self):
expected.swap(1, 2)
expected.cx(0, 1)
expected.measure(1, 2)
etrue_body = QuantumCircuit(qreg[[3, 4]], creg[[2]])
etrue_body.x(0)
efalse_body = QuantumCircuit(qreg[[3, 4]], creg[[2]])
efalse_body.x(1)
etrue_body = QuantumCircuit(qreg, creg[[2]])
etrue_body.x(3)
efalse_body = QuantumCircuit(qreg, creg[[2]])
efalse_body.x(4)
new_order = [0, 2, 1, 3, 4]
expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg[[2]])
expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg, creg[[2]])
expected.barrier(qreg)
expected.measure(qreg, creg[new_order])
self.assertEqual(dag_to_circuit(cdag), expected)
Expand Down Expand Up @@ -533,11 +533,11 @@ def test_pre_if_else_route_post_x(self):
expected.cx(0, 1)
expected.measure(1, 2)
new_order = [0, 2, 1, 3, 4]
etrue_body = QuantumCircuit(qreg[[3, 4]], creg[[0]])
etrue_body.x(0)
efalse_body = QuantumCircuit(qreg[[3, 4]], creg[[0]])
efalse_body.x(1)
expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg[[0]])
etrue_body = QuantumCircuit(qreg, creg[[0]])
etrue_body.x(3)
efalse_body = QuantumCircuit(qreg, creg[[0]])
efalse_body.x(4)
expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg, creg[[0]])
expected.x(2)
expected.barrier(qreg)
expected.measure(qreg, creg[new_order])
Expand All @@ -552,12 +552,12 @@ def test_post_if_else_route(self):
qc = QuantumCircuit(qreg, creg)
qc.h(0)
qc.measure(0, 0)
true_body = QuantumCircuit(qreg, creg[[0]])
true_body.x(3)
false_body = QuantumCircuit(qreg, creg[[0]])
false_body.x(4)
true_body = QuantumCircuit(qreg[[3, 4]], creg[[0]])
true_body.x(0)
false_body = QuantumCircuit(qreg[[3, 4]], creg[[0]])
false_body.x(1)
qc.barrier(qreg)
qc.if_else((creg[0], 0), true_body, false_body, qreg, creg[[0]])
qc.if_else((creg[0], 0), true_body, false_body, qreg[[3, 4]], creg[[0]])
qc.barrier(qreg)
qc.cx(0, 2)
qc.barrier(qreg)
Expand Down Expand Up @@ -596,10 +596,10 @@ def test_pre_if_else2(self):
qc.cx(0, 2)
qc.x(1)
qc.measure(0, 0)
true_body = QuantumCircuit(qreg, creg[[0]])
true_body = QuantumCircuit(qreg[[0]], creg[[0]])
true_body.x(0)
false_body = QuantumCircuit(qreg, creg[[0]])
qc.if_else((creg[0], 0), true_body, false_body, qreg, creg[[0]])
false_body = QuantumCircuit(qreg[[0]], creg[[0]])
qc.if_else((creg[0], 0), true_body, false_body, qreg[[0]], creg[[0]])
qc.barrier(qreg)
qc.measure(qreg, creg)

Expand Down Expand Up @@ -748,16 +748,16 @@ def test_pre_intra_post_if_else(self):
expected.swap(0, 1)
expected.cx(1, 2)
expected.measure(1, 0)
etrue_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg[[0]])
etrue_body.cx(0, 1)
efalse_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg[[0]])
efalse_body.swap(0, 1)
efalse_body.swap(2, 3)
efalse_body.cx(1, 2)
efalse_body.swap(0, 1)
efalse_body.swap(2, 3)
etrue_body = QuantumCircuit(qreg, creg[[0]])
etrue_body.cx(1, 2)
efalse_body = QuantumCircuit(qreg, creg[[0]])
efalse_body.swap(1, 2)
efalse_body.swap(3, 4)
efalse_body.cx(2, 3)
efalse_body.swap(1, 2)
efalse_body.swap(3, 4)

expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 2, 3, 4]], creg[[0]])
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg[[0]])
expected.swap(1, 2)
expected.h(3)
expected.cx(3, 2)
Expand Down Expand Up @@ -834,11 +834,11 @@ def test_no_layout_change(self):
expected.swap(1, 2)
expected.cx(0, 1)
expected.measure(0, 0)
etrue_body = QuantumCircuit(qreg[[1, 4]], creg[[0]])
etrue_body.x(0)
efalse_body = QuantumCircuit(qreg[[1, 4]], creg[[0]])
efalse_body.x(1)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 4]], creg[[0]])
etrue_body = QuantumCircuit(qreg, creg[[0]])
etrue_body.x(1)
efalse_body = QuantumCircuit(qreg, creg[[0]])
efalse_body.x(4)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg[[0]])
expected.barrier(qreg)
expected.measure(qreg, creg[[0, 2, 1, 3, 4]])
self.assertEqual(dag_to_circuit(cdag), expected)
Expand Down Expand Up @@ -1336,6 +1336,42 @@ def test_if_no_else_restores_layout(self):
running_layout.swap(*instruction.qubits)
self.assertEqual(initial_layout, running_layout)

def test_idle_qubit_contraction(self):
"""Incident virtual qubits to a control-flow block should be maintained, even if idle, but
the blocks shouldn't contain further unnecessary qubits."""
qc = QuantumCircuit(8)
with qc.if_test(expr.lift(True)):
qc.cx(0, 3)
qc.noop(4)
# Both of these qubits will have been moved around by the prior necessary layout
# changes, so this is testing the recursion works for modified layouts.
with qc.if_test(expr.lift(True)) as else_:
qc.noop(0)
with else_:
qc.noop(3)

coupling = CouplingMap.from_line(8)

# With the `decay` heuristic set to penalise re-use of the same qubit swap, this expected
# circuit should be the only valid output (except for symmetries in the swap operation,
# which the equality check should handle).
expected = QuantumCircuit(8)
with expected.if_test(expr.lift(True)):
expected.noop(4)
expected.swap(0, 1)
expected.swap(2, 3)
expected.cx(1, 2)
with expected.if_test(expr.lift(True)) as else_:
expected.noop(1)
with else_:
expected.noop(2)
# We have to restore the output layout.
expected.swap(0, 1)
expected.swap(2, 3)

pass_ = SabreSwap(coupling, "decay", seed=2025_02_05, trials=1)
self.assertEqual(pass_(qc), expected)


@ddt.ddt
class TestSabreSwapRandomCircuitValidOutput(QiskitTestCase):
Expand Down

0 comments on commit 288d2f4

Please sign in to comment.