Fix control-flow routing in StochasticSwap (#8880)

* Fix control-flow routing in StochasticSwap

The `StochasticSwap` pass has some fairly complex hand-offs between
different parts of its API, including in recursive calls and the regular
hand-off between Python and Rust.  In the course of adding the
control-flow support, some of these became muddled, and the mapping
between different virtual/physical/integer representations got mixed up,
resulting in invalid swaps being output in the final circuit.

This commit simplifies much of the internal mapping, removing many
superfluous `DAGCircuit` creations and compositions.  This also removes
instances where two layouts were "chained"; this was not well typed (the
output of a "virtual -> physical" mapping can't be the input for another
"virtual -> physical" mapping), and in general was being used to "undo"
some of compositions that were about to be applied.

This fixes a tacit assumption in the original code that the initial
layout was a trivial layout in the hand-off between Rust and Python.
This worked until the recursive call added the `initial_layout` option,
making this assumption invalid. Previously, virtual qubit bit instances
were converted to integers (to allow them to be passed to Rust) using
their indices into the original DAG, but the integer outputs were then
converted _back_ using the `initial_layout`.  In the old form, this
worked anyway, but wasn't logically correct and consequently broke when
the assumptions about `initial_layout` changed.

For the recursive calls, we now ensure that the inner passes are
essentially created with the same internal structure as the outer pass;
the passed in `DAGCircuit` uses the same bit instances and same meaning
of the virtual qubits as the outer circuit, and the `initial_layout`
ensures that the inner passes start with at the same layout as the outer
pass.  This makes the inner passes more like a logical continuation of
the current operation, rather than a completely separate entity that
needs to have its virtual qubits remapped.

The changes to the tests are twofold:

- move the `CheckMap` calls earlier and apply them directly to the
  `StochasticSwap` output rather than the expected circuit, to improve
  the quality of failure error messages

- use the same physical qubits inside the expected control-flow blocks;
  the new simpler form of doing the circuit rewriting internally in the
  pass ensures that the same bit objects are used all the way through
  the control-flow stack now, rather than creating new instances.

* Add tests for stochastic swap valid output

This commit adds full path transpile() tests for running with stochastic
swap that validates a full path transpilation outputs a valid physical
circuit. These tests are purposefully high level to provide some
validation that stochastic swap is not creating invalid output by
inserting incorrect swaps. It's not meant as a test of valid unitary
equivalent output of the full transpilation.

Co-authored-by: Jake Lishman <jake.lishman@ibm.com>

Co-authored-by: Matthew Treinish <mtreinish@kortar.org>
This commit is contained in:
Jake Lishman 2022-10-12 22:15:07 +01:00 committed by GitHub
parent 985b0050f0
commit b3cf64f49c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 268 additions and 235 deletions

View File

@ -26,7 +26,7 @@ from qiskit.transpiler.layout import Layout
from qiskit.circuit import IfElseOp, WhileLoopOp, ForLoopOp, ControlFlowOp
from qiskit._accelerate import stochastic_swap as stochastic_swap_rs
from .utils import combine_permutations, get_swap_map_dag
from .utils import get_swap_map_dag
logger = logging.getLogger(__name__)
@ -70,7 +70,8 @@ class StochasticSwap(TransformationPass):
self.fake_run = fake_run
self.qregs = None
self.initial_layout = initial_layout
self._qubit_indices = None
self._qubit_to_int = None
self._int_to_qubit = None
def run(self, dag):
"""Run the StochasticSwap pass on `dag`.
@ -97,7 +98,10 @@ class StochasticSwap(TransformationPass):
canonical_register = dag.qregs["q"]
if self.initial_layout is None:
self.initial_layout = Layout.generate_trivial_layout(canonical_register)
self._qubit_indices = {bit: idx for idx, bit in enumerate(dag.qubits)}
# Qubit indices are used to assign an integer to each virtual qubit during the routing: it's
# a mapping of {virtual: virtual}, for converting between Python and Rust forms.
self._qubit_to_int = {bit: idx for idx, bit in enumerate(dag.qubits)}
self._int_to_qubit = tuple(dag.qubits)
self.qregs = dag.qregs
logger.debug("StochasticSwap rng seeded with seed=%s", self.seed)
@ -174,18 +178,18 @@ class StochasticSwap(TransformationPass):
cdist2 = coupling._dist_matrix**2
int_qubit_subset = np.fromiter(
(self._qubit_indices[bit] for bit in qubit_subset),
(self._qubit_to_int[bit] for bit in qubit_subset),
dtype=np.uintp,
count=len(qubit_subset),
)
int_gates = np.fromiter(
(self._qubit_indices[bit] for gate in gates for bit in gate),
(self._qubit_to_int[bit] for gate in gates for bit in gate),
dtype=np.uintp,
count=2 * len(gates),
)
layout_mapping = {self._qubit_indices[k]: v for k, v in layout.get_virtual_bits().items()}
layout_mapping = {self._qubit_to_int[k]: v for k, v in layout.get_virtual_bits().items()}
int_layout = stochastic_swap_rs.NLayout(layout_mapping, num_qubits, coupling.size())
trial_circuit = DAGCircuit() # SWAP circuit for slice of swaps in this trial
@ -204,16 +208,15 @@ class StochasticSwap(TransformationPass):
edges,
seed=self.seed,
)
# If we have no best circuit for this layer, all of the
# trials have failed
# If we have no best circuit for this layer, all of the trials have failed
if best_layout is None:
logger.debug("layer_permutation: failed!")
return False, None, None, None
edges = best_edges.edges()
for idx in range(len(edges) // 2):
swap_src = self.initial_layout._p2v[edges[2 * idx]]
swap_tgt = self.initial_layout._p2v[edges[2 * idx + 1]]
swap_src = self._int_to_qubit[edges[2 * idx]]
swap_tgt = self._int_to_qubit[edges[2 * idx + 1]]
trial_circuit.apply_operation_back(SwapGate(), [swap_src, swap_tgt], [])
best_circuit = trial_circuit
@ -234,24 +237,17 @@ class StochasticSwap(TransformationPass):
best_depth (int): depth returned from _layer_permutation
best_circuit (DAGCircuit): swap circuit returned from _layer_permutation
"""
layout = best_layout
logger.debug("layer_update: layout = %s", layout)
logger.debug("layer_update: layout = %s", best_layout)
logger.debug("layer_update: self.initial_layout = %s", self.initial_layout)
# Output any swaps
if best_depth > 0:
logger.debug("layer_update: there are swaps in this layer, depth %d", best_depth)
dag.compose(best_circuit)
dag.compose(best_circuit, qubits={bit: bit for bit in best_circuit.qubits})
else:
logger.debug("layer_update: there are no swaps in this layer")
# Output this layer
layer_circuit = layer["graph"]
initial_v2p = self.initial_layout.get_virtual_bits()
new_v2p = layout.get_virtual_bits()
initial_order = [initial_v2p[qubit] for qubit in dag.qubits]
new_order = [new_v2p[qubit] for qubit in dag.qubits]
order = combine_permutations(initial_order, new_order)
dag.compose(layer_circuit, qubits=order)
dag.compose(layer["graph"], qubits=best_layout.reorder_bits(dag.qubits))
def _mapper(self, circuit_graph, coupling_graph, trials=20):
"""Map a DAGCircuit onto a CouplingMap using swap gates.
@ -376,29 +372,18 @@ class StochasticSwap(TransformationPass):
"""
cf_opnode = layer_dag.op_nodes()[0]
if isinstance(cf_opnode.op, IfElseOp):
updated_ctrl_op, cf_layout, idle_qubits = self._route_control_flow_multiblock(
new_op, new_qargs, new_layout = self._route_control_flow_multiblock(
cf_opnode, current_layout, root_dag
)
elif isinstance(cf_opnode.op, (ForLoopOp, WhileLoopOp)):
updated_ctrl_op, cf_layout, idle_qubits = self._route_control_flow_looping(
new_op, new_qargs, new_layout = self._route_control_flow_looping(
cf_opnode, current_layout, root_dag
)
else:
raise TranspilerError(f"unsupported control flow operation: {cf_opnode}")
if self.fake_run:
return cf_layout
cf_layer_dag = DAGCircuit()
cf_qubits = [qubit for qubit in root_dag.qubits if qubit not in idle_qubits]
qreg = QuantumRegister(len(cf_qubits), "q")
cf_layer_dag.add_qreg(qreg)
for creg in layer_dag.cregs.values():
cf_layer_dag.add_creg(creg)
cf_layer_dag.apply_operation_back(updated_ctrl_op, cf_layer_dag.qubits, cf_opnode.cargs)
target_qubits = [qubit for qubit in dagcircuit_output.qubits if qubit not in idle_qubits]
order = current_layout.reorder_bits(target_qubits)
dagcircuit_output.compose(cf_layer_dag, qubits=order)
return cf_layout
if not self.fake_run:
dagcircuit_output.apply_operation_back(new_op, new_qargs, cf_opnode.cargs)
return new_layout
def _new_seed(self):
"""Get a seed for a new RNG instance."""
@ -432,19 +417,19 @@ class StochasticSwap(TransformationPass):
Returns:
ControlFlowOp: routed control flow operation.
final_layout (Layout): layout after instruction.
list(Qubit): list of idle qubits in controlflow layer.
List[Qubit]: the new physical-qubit arguments that the output `ControlFlowOp` should be
applied to. This might be wider than the input node if internal routing was needed.
Layout: the new layout after the control-flow operation is applied.
"""
# For each block, expand it up be the full width of the containing DAG so we can be certain
# that it is routable, then route it within that. When we recombine later, we'll reduce all
# these blocks down to remove any qubits that are idle.
block_dags = []
block_layouts = []
order = [self._qubit_indices[bit] for bit in node.qargs]
for block in node.op.blocks:
inner_pass = self._recursive_pass(current_layout)
full_dag_block = root_dag.copy_empty_like()
full_dag_block.compose(circuit_to_dag(block), qubits=order)
full_dag_block.compose(circuit_to_dag(block), qubits=node.qargs)
block_dags.append(inner_pass.run(full_dag_block))
block_layouts.append(inner_pass.property_set["final_layout"].copy())
@ -455,23 +440,19 @@ class StochasticSwap(TransformationPass):
deepest_index = np.argmax([block.depth(recurse=True) for block in block_dags])
final_layout = block_layouts[deepest_index]
if self.fake_run:
return None, final_layout, None
p2v = current_layout.get_physical_bits()
return None, None, final_layout
idle_qubits = set(root_dag.qubits)
for i, updated_dag_block in enumerate(block_dags):
if i != deepest_index:
swap_circuit, swap_qubits = get_swap_map_dag(
swap_dag, swap_qubits = get_swap_map_dag(
root_dag,
self.coupling_map,
block_layouts[i],
final_layout,
seed=self._new_seed(),
)
if swap_circuit.depth():
virtual_swap_dag = updated_dag_block.copy_empty_like()
order = [p2v[virtual_swap_dag.qubits.index(qubit)] for qubit in swap_qubits]
virtual_swap_dag.compose(swap_circuit, qubits=order)
updated_dag_block.compose(virtual_swap_dag)
if swap_dag.depth():
updated_dag_block.compose(swap_dag, qubits=swap_qubits)
idle_qubits &= set(updated_dag_block.idle_wires())
# Now for each block, expand it to be full width over all active wires (all blocks of a
@ -479,18 +460,8 @@ class StochasticSwap(TransformationPass):
block_circuits = []
for i, updated_dag_block in enumerate(block_dags):
updated_dag_block.remove_qubits(*idle_qubits)
new_dag_block = DAGCircuit()
new_num_qubits = updated_dag_block.num_qubits()
qreg = QuantumRegister(new_num_qubits, "q")
new_dag_block.add_qreg(qreg)
for creg in updated_dag_block.cregs.values():
new_dag_block.add_creg(creg)
for inner_node in updated_dag_block.op_nodes():
new_qargs = [qreg[updated_dag_block.qubits.index(bit)] for bit in inner_node.qargs]
new_dag_block.apply_operation_back(inner_node.op, new_qargs, inner_node.cargs)
block_circuits.append(dag_to_circuit(new_dag_block))
return node.op.replace_blocks(block_circuits), final_layout, idle_qubits
block_circuits.append(dag_to_circuit(updated_dag_block))
return node.op.replace_blocks(block_circuits), block_circuits[0].qubits, final_layout
def _route_control_flow_looping(self, node, current_layout, root_dag):
"""Route a control-flow operation that represents a loop, such as :class:`.ForOpLoop` or
@ -505,45 +476,36 @@ class StochasticSwap(TransformationPass):
Returns:
ControlFlowOp: routed control flow operation.
Layout: layout after instruction (this will be the same as the input layout).
list(Qubit): list of idle qubits in controlflow layer.
List[Qubit]: the new physical-qubit arguments that the output `ControlFlowOp` should be
applied to. This might be wider than the input node if internal routing was needed.
Layout: the new layout after the control-flow operation is applied.
"""
if self.fake_run:
return None, current_layout, None
return None, None, current_layout
# Temporarily expand to full width, and route within that.
inner_pass = self._recursive_pass(current_layout)
order = [self._qubit_indices[bit] for bit in node.qargs]
full_dag_block = root_dag.copy_empty_like()
full_dag_block.compose(circuit_to_dag(node.op.blocks[0]), qubits=order)
full_dag_block.compose(circuit_to_dag(node.op.blocks[0]), qubits=node.qargs)
updated_dag_block = inner_pass.run(full_dag_block)
# Ensure that the layout at the end of the block is returned to being the layout at the
# start of the block again, so the loop works.
swap_circuit, swap_qubits = get_swap_map_dag(
swap_dag, swap_qubits = get_swap_map_dag(
root_dag,
self.coupling_map,
inner_pass.property_set["final_layout"],
current_layout,
seed=self._new_seed(),
)
if swap_circuit.depth():
p2v = current_layout.get_physical_bits()
virtual_swap_dag = updated_dag_block.copy_empty_like()
order = [p2v[virtual_swap_dag.qubits.index(qubit)] for qubit in swap_qubits]
virtual_swap_dag.compose(swap_circuit, qubits=order)
updated_dag_block.compose(virtual_swap_dag)
if swap_dag.depth():
updated_dag_block.compose(swap_dag, qubits=swap_qubits)
# Contract the routed block back down to only operate on the qubits that it actually needs.
idle_qubits = set(root_dag.qubits) & set(updated_dag_block.idle_wires())
updated_dag_block.remove_qubits(*idle_qubits)
new_dag_block = DAGCircuit()
new_num_qubits = updated_dag_block.num_qubits()
qreg = QuantumRegister(new_num_qubits, "q")
new_dag_block.add_qreg(qreg)
for creg in updated_dag_block.cregs.values():
new_dag_block.add_creg(creg)
for inner_node in updated_dag_block.op_nodes():
new_qargs = [qreg[updated_dag_block.qubits.index(bit)] for bit in inner_node.qargs]
new_dag_block.apply_operation_back(inner_node.op, new_qargs, inner_node.cargs)
updated_circ_block = dag_to_circuit(new_dag_block)
return node.op.replace_blocks([updated_circ_block]), current_layout, idle_qubits
updated_circ_block = dag_to_circuit(updated_dag_block)
return (
node.op.replace_blocks([updated_circ_block]),
updated_dag_block.qubits,
current_layout,
)

View File

@ -12,49 +12,24 @@
"""Utility functions for routing"""
from qiskit.transpiler import CouplingMap
from qiskit.transpiler.exceptions import TranspilerError
from .algorithms import ApproximateTokenSwapper
def combine_permutations(*permutations):
"""
Chain a series of permutations.
Args:
*permutations (list(int)): permutations to combine
Returns:
list: combined permutation
"""
order = permutations[0]
for this_order in permutations[1:]:
order = [order[i] for i in this_order]
return order
def get_swap_map_dag(dag, coupling_map, from_layout, to_layout, seed, trials=4):
"""Get the circuit of swaps to go from from_layout to to_layout, and the qubit ordering of the
qubits in that circuit."""
"""Get the circuit of swaps to go from from_layout to to_layout, and the physical qubits
(integers) that the swap circuit should be applied on."""
if len(dag.qregs) != 1 or dag.qregs.get("q", None) is None:
raise TranspilerError("layout transformation runs on physical circuits only")
if len(dag.qubits) > len(coupling_map.physical_qubits):
raise TranspilerError("The layout does not match the amount of qubits in the DAG")
if coupling_map:
graph = coupling_map.graph.to_undirected()
else:
coupling_map = CouplingMap.from_full(len(to_layout))
graph = coupling_map.graph.to_undirected()
token_swapper = ApproximateTokenSwapper(graph, seed)
token_swapper = ApproximateTokenSwapper(coupling_map.graph.to_undirected(), seed)
# Find the permutation between the initial physical qubits and final physical qubits.
permutation = {
pqubit: to_layout.get_virtual_bits()[vqubit]
for vqubit, pqubit in from_layout.get_virtual_bits().items()
pqubit: to_layout[vqubit] for vqubit, pqubit in from_layout.get_virtual_bits().items()
}
permutation_circ = token_swapper.permutation_circuit(permutation, trials)
permutation_qubits = [dag.qubits[i] for i in sorted(permutation_circ.inputmap.keys())]
return permutation_circ.circuit, permutation_qubits
# The mapping produced here maps physical qubit indices of the outer dag to the bits used to
# represent them in the inner map. For later composing, we actually want the opposite map.
swap_circuit, phys_to_circuit_qubits = token_swapper.permutation_circuit(permutation, trials)
circuit_to_phys = {inner: outer for outer, inner in phys_to_circuit_qubits.items()}
return swap_circuit, [circuit_to_phys[bit] for bit in swap_circuit.qubits]

View File

@ -13,6 +13,9 @@
"""Test the Stochastic Swap pass"""
import unittest
import numpy.random
from ddt import ddt, data
from qiskit.transpiler.passes import StochasticSwap
from qiskit.transpiler import CouplingMap, PassManager
@ -21,6 +24,10 @@ from qiskit.converters import circuit_to_dag, dag_to_circuit
from qiskit import QuantumRegister, ClassicalRegister, QuantumCircuit
from qiskit.test import QiskitTestCase
from qiskit.transpiler.passes.utils import CheckMap
from qiskit.circuit.random import random_circuit
from qiskit.providers.fake_provider import FakeMumbai
from qiskit.compiler.transpiler import transpile
from qiskit.circuit import ControlFlowOp, Clbit
@ddt
@ -569,12 +576,17 @@ class TestStochasticSwap(QiskitTestCase):
after = circuit_to_dag(after)
self.assertEqual(expected_dag, after)
def test_controlflow_pre_if_else_route(self):
@ddt
class TestStochasticSwapControlFlow(QiskitTestCase):
"""Tests for control flow in stochastic swap."""
def test_pre_if_else_route(self):
"""test swap with if else controlflow construct"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
creg = ClassicalRegister(num_qubits)
coupling = CouplingMap([(i, i + 1) for i in range(num_qubits - 1)])
coupling = CouplingMap.from_line(num_qubits)
qc = QuantumCircuit(qreg, creg)
qc.h(0)
qc.cx(0, 2)
@ -589,28 +601,26 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=82).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
expected.swap(0, 1)
expected.cx(1, 2)
expected.measure(2, 2)
eqreg = QuantumRegister(2, name="q")
etrue_body = QuantumCircuit(eqreg, creg)
etrue_body = QuantumCircuit(qreg[[3, 4]], creg)
etrue_body.x(0)
efalse_body = QuantumCircuit(eqreg, creg)
efalse_body = QuantumCircuit(qreg[[3, 4]], creg)
efalse_body.x(1)
new_order = [1, 0, 2, 3, 4]
expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg)
expected.barrier(qreg)
expected.measure(qreg, creg[new_order])
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(dag_to_circuit(cdag), expected)
def test_controlflow_pre_if_else_route_post_x(self):
def test_pre_if_else_route_post_x(self):
"""test swap with if else controlflow construct; pre-cx and post x"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
@ -631,7 +641,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=431).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -639,22 +651,17 @@ class TestStochasticSwap(QiskitTestCase):
expected.cx(0, 1)
expected.measure(1, 2)
new_order = [0, 2, 1, 3, 4]
eqreg = QuantumRegister(2, "q")
etrue_body = QuantumCircuit(eqreg, creg)
etrue_body = QuantumCircuit(qreg[[3, 4]], creg)
etrue_body.x(0)
efalse_body = QuantumCircuit(eqreg, creg)
efalse_body = QuantumCircuit(qreg[[3, 4]], creg)
efalse_body.x(1)
expected.if_else((creg[2], 0), etrue_body, efalse_body, qreg[[3, 4]], creg)
expected.x(2)
expected.barrier(qreg)
expected.measure(qreg, creg[new_order])
self.assertEqual(dag_to_circuit(cdag), expected)
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
def test_controlflow_post_if_else_route(self):
def test_post_if_else_route(self):
"""test swap with if else controlflow construct; post cx"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
@ -676,15 +683,16 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=6508).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
expected.measure(0, 0)
eqreg = QuantumRegister(2, "q")
etrue_body = QuantumCircuit(eqreg, creg)
etrue_body = QuantumCircuit(qreg[[3, 4]], creg)
etrue_body.x(0)
efalse_body = QuantumCircuit(eqreg, creg)
efalse_body = QuantumCircuit(qreg[[3, 4]], creg)
efalse_body.x(1)
expected.barrier(qreg)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[3, 4]], creg)
@ -693,12 +701,9 @@ class TestStochasticSwap(QiskitTestCase):
expected.cx(1, 2)
expected.barrier(qreg)
expected.measure(qreg, creg[[1, 0, 2, 3, 4]])
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(dag_to_circuit(cdag), expected)
def test_controlflow_pre_if_else2(self):
def test_pre_if_else2(self):
"""test swap with if else controlflow construct; cx in if statement"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
@ -718,7 +723,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=38).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -726,21 +733,16 @@ class TestStochasticSwap(QiskitTestCase):
expected.swap(0, 1)
expected.cx(1, 2)
expected.measure(1, 0)
eqreg = QuantumRegister(1, "q")
etrue_body = QuantumCircuit(eqreg, creg)
etrue_body = QuantumCircuit(qreg[[1]], creg)
etrue_body.x(0)
efalse_body = QuantumCircuit(eqreg, creg)
efalse_body = QuantumCircuit(qreg[[1]], creg)
new_order = [1, 0, 2, 3, 4]
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1]], creg)
expected.barrier(qreg)
expected.measure(qreg, creg[new_order])
self.assertEqual(dag_to_circuit(cdag), expected)
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
def test_controlflow_intra_if_else_route(self):
def test_intra_if_else_route(self):
"""test swap with if else controlflow construct"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
@ -759,7 +761,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=8).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -778,13 +782,9 @@ class TestStochasticSwap(QiskitTestCase):
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg)
new_order = [1, 2, 0, 4, 3]
expected.measure(qreg, creg[new_order])
self.assertEqual(dag_to_circuit(cdag), expected)
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
def test_controlflow_pre_intra_if_else(self):
def test_pre_intra_if_else(self):
"""test swap with if else controlflow construct; cx in if statement"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
@ -804,12 +804,13 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=2, trials=20).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
eqreg = QuantumRegister(4, "q")
etrue_body = QuantumCircuit(eqreg, creg)
efalse_body = QuantumCircuit(eqreg, creg)
etrue_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg)
efalse_body = QuantumCircuit(qreg[[1, 2, 3, 4]], creg)
expected.h(0)
expected.x(1)
expected.swap(0, 1)
@ -825,19 +826,15 @@ class TestStochasticSwap(QiskitTestCase):
efalse_body.cx(1, 2)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 2, 3, 4]], creg)
expected.measure(qreg, creg[[1, 2, 0, 4, 3]])
self.assertEqual(dag_to_circuit(cdag), expected)
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
def test_controlflow_pre_intra_post_if_else(self):
def test_pre_intra_post_if_else(self):
"""test swap with if else controlflow construct; cx before, in, and after if
statement"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
creg = ClassicalRegister(num_qubits)
coupling = CouplingMap([(i, i + 1) for i in range(num_qubits - 1)])
coupling = CouplingMap.from_line(num_qubits)
qc = QuantumCircuit(qreg, creg)
qc.h(0)
qc.cx(0, 2)
@ -855,7 +852,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=1).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -864,29 +863,25 @@ class TestStochasticSwap(QiskitTestCase):
expected.cx(0, 1)
expected.measure(0, 0)
etrue_body = QuantumCircuit(qreg, creg)
etrue_body.cx(0, 2)
etrue_body.swap(0, 2)
etrue_body.cx(0, 1)
etrue_body.swap(0, 1)
etrue_body.swap(4, 3)
etrue_body.swap(1, 3)
etrue_body.swap(2, 3)
efalse_body = QuantumCircuit(qreg, creg)
efalse_body.swap(0, 2)
efalse_body.swap(0, 1)
efalse_body.swap(3, 4)
efalse_body.swap(1, 3)
efalse_body.cx(2, 1)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[0, 2, 1, 3, 4]], creg)
efalse_body.swap(2, 3)
efalse_body.cx(1, 2)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[0, 1, 2, 3, 4]], creg)
expected.swap(1, 2)
expected.h(4)
expected.swap(3, 4)
expected.cx(3, 2)
expected.barrier()
expected.measure(qreg, creg[[2, 4, 0, 3, 1]])
self.assertEqual(dag_to_circuit(cdag), expected)
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
def test_controlflow_no_layout_change(self):
def test_no_layout_change(self):
"""test controlflow with no layout change needed"""
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
@ -907,7 +902,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=23).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -915,22 +912,17 @@ class TestStochasticSwap(QiskitTestCase):
expected.swap(1, 2)
expected.cx(0, 1)
expected.measure(0, 0)
eqreg = QuantumRegister(2, "q")
etrue_body = QuantumCircuit(eqreg, creg)
etrue_body = QuantumCircuit(qreg[[1, 4]], creg)
etrue_body.x(0)
efalse_body = QuantumCircuit(eqreg, creg)
efalse_body = QuantumCircuit(qreg[[1, 4]], creg)
efalse_body.x(1)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg[[1, 4]], creg)
expected.barrier(qreg)
expected.measure(qreg, creg[[0, 2, 1, 3, 4]])
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(dag_to_circuit(cdag), expected)
@data(1, 2, 3)
def test_controlflow_for_loop(self, nloops):
def test_for_loop(self, nloops):
"""test stochastic swap with for_loop"""
# if the loop has only one iteration it isn't necessary for the pass
# to swap back to the starting layout. This test would check that
@ -950,7 +942,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=687).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -962,18 +956,14 @@ class TestStochasticSwap(QiskitTestCase):
loop_parameter = None
expected.for_loop(range(nloops), loop_parameter, efor_body, qreg, creg)
expected.measure(qreg, creg)
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(dag_to_circuit(cdag), expected)
def test_controlflow_while_loop(self):
def test_while_loop(self):
"""test while loop"""
num_qubits = 4
qreg = QuantumRegister(num_qubits, "q")
creg = ClassicalRegister(len(qreg))
coupling = CouplingMap.from_line(num_qubits)
check_map_pass = CheckMap(coupling)
qc = QuantumCircuit(qreg, creg)
while_body = QuantumCircuit(qreg, creg)
while_body.reset(qreg[2:])
@ -986,7 +976,10 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=58).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
ewhile_body = QuantumCircuit(qreg, creg)
ewhile_body.reset(qreg[2:])
@ -1000,11 +993,9 @@ class TestStochasticSwap(QiskitTestCase):
expected.while_loop((creg, 0), ewhile_body, expected.qubits, expected.clbits)
expected.barrier()
expected.measure(qreg, creg)
check_map_pass.run(circuit_to_dag(expected))
self.assertEqual(cqc, expected)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(dag_to_circuit(cdag), expected)
def test_controlflow_nested_inner_cnot(self):
def test_nested_inner_cnot(self):
"""test swap in nested if else controlflow construct; swap in inner"""
seed = 1
num_qubits = 3
@ -1033,7 +1024,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=seed).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -1055,19 +1048,15 @@ class TestStochasticSwap(QiskitTestCase):
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg)
expected.measure(qreg, creg)
self.assertEqual(cqc, expected)
check_map_pass = CheckMap(coupling)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(dag_to_circuit(cdag), expected)
def test_controlflow_nested_outer_cnot(self):
def test_nested_outer_cnot(self):
"""test swap with nested if else controlflow construct; swap in outer"""
seed = 200
num_qubits = 5
qreg = QuantumRegister(num_qubits, "q")
creg = ClassicalRegister(num_qubits)
coupling = CouplingMap.from_line(num_qubits)
check_map_pass = CheckMap(coupling)
qc = QuantumCircuit(qreg, creg)
qc.h(0)
qc.x(1)
@ -1090,7 +1079,9 @@ class TestStochasticSwap(QiskitTestCase):
dag = circuit_to_dag(qc)
cdag = StochasticSwap(coupling, seed=seed).run(dag)
cqc = dag_to_circuit(cdag)
check_map_pass = CheckMap(coupling)
check_map_pass.run(cdag)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qreg, creg)
expected.h(0)
@ -1104,20 +1095,17 @@ class TestStochasticSwap(QiskitTestCase):
efor_body = QuantumCircuit(qreg, creg)
efor_body.delay(10, 0)
efor_body.barrier(qreg)
efor_body.cx(1, 3)
etrue_body.for_loop(range(3), loop_parameter, efor_body, qreg[[0, 2, 1, 3, 4]], creg)
efor_body.cx(2, 3)
etrue_body.for_loop(range(3), loop_parameter, efor_body, qreg[[0, 1, 2, 3, 4]], creg)
efalse_body = QuantumCircuit(qreg, creg)
efalse_body.y(0)
efalse_body.swap(1, 2)
expected.if_else((creg[0], 0), etrue_body, efalse_body, qreg, creg)
expected.measure(qreg, creg[[0, 2, 1, 3, 4]])
self.assertEqual(dag_to_circuit(cdag), expected)
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(cqc, expected)
def test_controlflow_disjoint_looping(self):
def test_disjoint_looping(self):
"""Test looping controlflow on different qubit register"""
num_qubits = 4
cm = CouplingMap.from_line(num_qubits)
@ -1129,14 +1117,14 @@ class TestStochasticSwap(QiskitTestCase):
cqc = StochasticSwap(cm, seed=0)(qc)
expected = QuantumCircuit(qr)
efor_body = QuantumCircuit(3)
efor_body = QuantumCircuit(qr[[0, 1, 2]])
efor_body.swap(1, 2)
efor_body.cx(0, 1)
efor_body.swap(1, 2)
expected.for_loop((0,), None, efor_body, [0, 1, 2], [])
self.assertEqual(cqc, expected)
def test_controlflow_disjoint_multiblock(self):
def test_disjoint_multiblock(self):
"""Test looping controlflow on different qubit register"""
num_qubits = 4
cm = CouplingMap.from_line(num_qubits)
@ -1151,16 +1139,16 @@ class TestStochasticSwap(QiskitTestCase):
cqc = StochasticSwap(cm, seed=353)(qc)
expected = QuantumCircuit(qr, cr)
etrue_body = QuantumCircuit(true_body.qregs[0], cr)
etrue_body = QuantumCircuit(qr[[0, 1, 2]], cr)
etrue_body.cx(0, 1)
etrue_body.swap(0, 1)
efalse_body = QuantumCircuit(false_body.qregs[0], cr)
efalse_body = QuantumCircuit(qr[[0, 1, 2]], cr)
efalse_body.swap(0, 1)
efalse_body.cx(1, 2)
expected.if_else((cr[0], 1), etrue_body, efalse_body, [0, 1, 2], [])
self.assertEqual(cqc, expected)
def test_controlflow_multiple_ops_per_layer(self):
def test_multiple_ops_per_layer(self):
"""Test circuits with multiple operations per layer"""
num_qubits = 6
coupling = CouplingMap.from_line(num_qubits)
@ -1172,20 +1160,128 @@ class TestStochasticSwap(QiskitTestCase):
with qc.for_loop((0,)):
qc.cx(3, 5)
cqc = StochasticSwap(coupling, seed=0)(qc)
check_map_pass(cqc)
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
expected = QuantumCircuit(qr)
expected.swap(0, 1)
expected.cx(1, 2)
efor_body = QuantumCircuit(3)
efor_body = QuantumCircuit(qr[[3, 4, 5]])
efor_body.swap(1, 2)
efor_body.cx(0, 1)
efor_body.swap(2, 1)
expected.for_loop((0,), None, efor_body, [3, 4, 5], [])
check_map_pass.run(circuit_to_dag(expected))
self.assertTrue(check_map_pass.property_set["is_swap_mapped"])
self.assertEqual(cqc, expected)
@ddt
class TestStochasticSwapRandomCircuitValidOutput(QiskitTestCase):
"""Assert the output of a transpilation with stochastic swap is a physical circuit."""
@classmethod
def setUpClass(cls):
super().setUpClass()
cls.backend = FakeMumbai()
cls.coupling_edge_set = set(tuple(x) for x in cls.backend.configuration().coupling_map)
cls.basis_gates = set(cls.backend.configuration().basis_gates)
cls.basis_gates.update(["for_loop", "while_loop", "if_else"])
def assert_valid_circuit(self, transpiled):
"""Assert circuit complies with constraints of backend."""
self.assertIsInstance(transpiled, QuantumCircuit)
self.assertIsNotNone(getattr(transpiled, "_layout", None))
def _visit_block(circuit, qubit_mapping=None):
for instruction in circuit:
if instruction.operation.name in {"barrier", "measure"}:
continue
self.assertIn(instruction.operation.name, self.basis_gates)
qargs = tuple(qubit_mapping[x] for x in instruction.qubits)
if not isinstance(instruction.operation, ControlFlowOp):
if len(qargs) > 2 or len(qargs) < 0:
raise Exception("Invalid number of qargs for instruction")
if len(qargs) == 2:
self.assertIn(qargs, self.coupling_edge_set)
else:
self.assertLessEqual(qargs[0], 26)
if isinstance(instruction.operation, ControlFlowOp):
for block in instruction.operation.blocks:
new_mapping = {
inner: qubit_mapping[outer]
for outer, inner in zip(instruction.qubits, block.qubits)
}
_visit_block(block, new_mapping)
# Assert routing ran.
_visit_block(
transpiled,
qubit_mapping={qubit: index for index, qubit in enumerate(transpiled.qubits)},
)
@data(*range(1, 27))
def test_random_circuit_no_control_flow(self, size):
"""Test that transpiled random circuits without control flow are physical circuits."""
circuit = random_circuit(size, 3, measure=True, seed=12342)
tqc = transpile(
circuit,
self.backend,
routing_method="stochastic",
layout_method="dense",
seed_transpiler=12342,
)
self.assert_valid_circuit(tqc)
@data(*range(4, 27))
def test_random_circuit_for_loop(self, size):
"""Test that transpiled random circuits with nested for loops are physical circuits."""
circuit = random_circuit(size, 3, measure=False, seed=12342)
for_block = random_circuit(3, 2, measure=False, seed=12342)
inner_for_block = random_circuit(2, 1, measure=False, seed=12342)
with circuit.for_loop((1,)):
with circuit.for_loop((1,)):
circuit.append(inner_for_block, [0, 3])
circuit.append(for_block, [1, 0, 2])
circuit.measure_all()
tqc = transpile(
circuit,
self.backend,
basis_gates=list(self.basis_gates),
routing_method="stochastic",
layout_method="dense",
seed_transpiler=12342,
)
self.assert_valid_circuit(tqc)
@data(*range(6, 27))
def test_random_circuit_if_else(self, size):
"""Test that transpiled random circuits with if else blocks are physical circuits."""
circuit = random_circuit(size, 3, measure=True, seed=12342)
if_block = random_circuit(3, 2, measure=True, seed=12342)
else_block = random_circuit(2, 1, measure=True, seed=12342)
rng = numpy.random.default_rng(seed=12342)
inner_clbit_count = max((if_block.num_clbits, else_block.num_clbits))
if inner_clbit_count > circuit.num_clbits:
circuit.add_bits([Clbit() for _ in [None] * (inner_clbit_count - circuit.num_clbits)])
clbit_indices = list(range(circuit.num_clbits))
rng.shuffle(clbit_indices)
with circuit.if_test((circuit.clbits[0], True)) as else_:
circuit.append(if_block, [0, 2, 1], clbit_indices[: if_block.num_clbits])
with else_:
circuit.append(else_block, [2, 5], clbit_indices[: else_block.num_clbits])
tqc = transpile(
circuit,
self.backend,
basis_gates=list(self.basis_gates),
routing_method="stochastic",
layout_method="dense",
seed_transpiler=12342,
)
self.assert_valid_circuit(tqc)
if __name__ == "__main__":
unittest.main()