diff --git a/qiskit/transpiler/passes/__init__.py b/qiskit/transpiler/passes/__init__.py index 6a5d24a6a921..cf0f5f2493b1 100644 --- a/qiskit/transpiler/passes/__init__.py +++ b/qiskit/transpiler/passes/__init__.py @@ -140,6 +140,14 @@ LinearFunctionsSynthesis LinearFunctionsToPermutations +Post Layout (Post transpile qubit selection) +============================================ + +.. autosummary:: + :toctree: ../stubs/ + + VF2PostLayout + Additional Passes ================= @@ -169,6 +177,7 @@ from .layout import SabreLayout from .layout import CSPLayout from .layout import VF2Layout +from .layout import VF2PostLayout from .layout import ApplyLayout from .layout import Layout2qDistance from .layout import EnlargeWithAncilla diff --git a/qiskit/transpiler/passes/layout/__init__.py b/qiskit/transpiler/passes/layout/__init__.py index dc7efe9bf4f1..736ea3add2f4 100644 --- a/qiskit/transpiler/passes/layout/__init__.py +++ b/qiskit/transpiler/passes/layout/__init__.py @@ -19,6 +19,7 @@ from .sabre_layout import SabreLayout from .csp_layout import CSPLayout from .vf2_layout import VF2Layout +from .vf2_post_layout import VF2PostLayout from .apply_layout import ApplyLayout from .layout_2q_distance import Layout2qDistance from .enlarge_with_ancilla import EnlargeWithAncilla diff --git a/qiskit/transpiler/passes/layout/apply_layout.py b/qiskit/transpiler/passes/layout/apply_layout.py index f4f65af7338b..42da1aea713c 100644 --- a/qiskit/transpiler/passes/layout/apply_layout.py +++ b/qiskit/transpiler/passes/layout/apply_layout.py @@ -16,6 +16,7 @@ from qiskit.dagcircuit import DAGCircuit from qiskit.transpiler.basepasses import TransformationPass from qiskit.transpiler.exceptions import TranspilerError +from qiskit.transpiler.layout import Layout class ApplyLayout(TransformationPass): @@ -25,6 +26,13 @@ class ApplyLayout(TransformationPass): by applying the Layout given in `property_set`. Requires either of passes to set/select Layout, e.g. `SetLayout`, `TrivialLayout`. Assumes the Layout has full physical qubits. + + If a post layout pass is run and sets the ``post_layout`` property set field with + a new layout to use after ``ApplyLayout`` has already run once this pass will + compact the layouts so that we apply + ``original_virtual`` -> ``existing_layout`` -> ``new_layout`` -> ``new_physical`` + so that the output circuit and layout combination become: + ``original_virtual`` -> ``new_physical`` """ def run(self, dag): @@ -47,8 +55,7 @@ def run(self, dag): if len(layout) != (1 + max(layout.get_physical_bits())): raise TranspilerError("The 'layout' must be full (with ancilla).") - for qreg in dag.qregs.values(): - self.property_set["layout"].add_register(qreg) + post_layout = self.property_set["post_layout"] q = QuantumRegister(len(layout), "q") @@ -58,10 +65,33 @@ def run(self, dag): new_dag.add_clbits(dag.clbits) for creg in dag.cregs.values(): new_dag.add_creg(creg) - virtual_phsyical_map = layout.get_virtual_bits() - for node in dag.topological_op_nodes(): - qargs = [q[virtual_phsyical_map[qarg]] for qarg in node.qargs] - new_dag.apply_operation_back(node.op, qargs, node.cargs) + if post_layout is None: + for qreg in dag.qregs.values(): + self.property_set["layout"].add_register(qreg) + virtual_phsyical_map = layout.get_virtual_bits() + for node in dag.topological_op_nodes(): + qargs = [q[virtual_phsyical_map[qarg]] for qarg in node.qargs] + new_dag.apply_operation_back(node.op, qargs, node.cargs) + else: + # First build a new layout object going from: + # old virtual -> old phsyical -> new virtual -> new physical + # to: + # old virtual -> new physical + full_layout = Layout() + old_phys_to_virtual = layout.get_physical_bits() + new_virtual_to_physical = post_layout.get_virtual_bits() + qubit_index_map = {bit: index for index, bit in enumerate(dag.qubits)} + for new_virt, new_phys in new_virtual_to_physical.items(): + old_phys = qubit_index_map[new_virt] + old_virt = old_phys_to_virtual[old_phys] + full_layout.add(old_virt, new_phys) + for reg in layout.get_registers(): + full_layout.add_register(reg) + # Apply new layout to the circuit + for node in dag.topological_op_nodes(): + qargs = [q[new_virtual_to_physical[qarg]] for qarg in node.qargs] + new_dag.apply_operation_back(node.op, qargs, node.cargs) + self.property_set["layout"] = full_layout new_dag._global_phase = dag._global_phase return new_dag diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py new file mode 100644 index 000000000000..87f8d9ee2cb7 --- /dev/null +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -0,0 +1,384 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""VF2PostLayout pass to find a layout after transpile using subgraph isomorphism""" +from enum import Enum +import logging +import random +import time +from collections import defaultdict +import statistics + +from retworkx import PyDiGraph, vf2_mapping, PyGraph + +from qiskit.transpiler.layout import Layout +from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError +from qiskit.providers.exceptions import BackendPropertyError + + +logger = logging.getLogger(__name__) + + +class VF2PostLayoutStopReason(Enum): + """Stop reasons for VF2PostLayout pass.""" + + SOLUTION_FOUND = "solution found" + NO_SOLUTION_FOUND = "nonexistent solution" + MORE_THAN_2Q = ">2q gates in basis" + + +def _target_match(node_a, node_b): + # Node A is the set of operations in the target. Node B is the count dict + # of oeprations on the node or edge in the circuit. + if isinstance(node_a, set): + return node_a.issuperset(node_b.keys()) + # Node A is the count dict of operations on the node or edge in the circuit + # Node B is the set of operations in the target on the same qubit(s). + else: + return set(node_a).issubset(node_b) + + +class VF2PostLayout(AnalysisPass): + """A pass for choosing a Layout after transpilation of a circuit onto a + Coupling graph, as a subgraph isomorphism problem, solved by VF2++. + + Unlike the :class:`~.VF2PostLayout` transpiler pass which is designed to find an + initial layout for a circuit early in the transpilation pipeline this transpiler + pass is designed to try and find a better layout after transpilation is complete. + The initial layout phase of the transpiler doesn't have as much information available + as we do after transpilation. This pass is designed to be paired in a similar pipeline + as the layout passes. This pass will strip any idle wires from the circuit, use VF2 + to find a subgraph in the coupling graph for the circuit to run on with better fidelity + and then update the circuit layout to use the new qubits. + + If a solution is found that means there is a "perfect layout" and that no + further swap mapping or routing is needed. If a solution is found the layout + will be set in the property set as ``property_set['layout']``. However, if no + solution is found, no ``property_set['layout']`` is set. The stopping reason is + set in ``property_set['VF2PostLayout_stop_reason']`` in all the cases and will be + one of the values enumerated in ``VF2PostLayoutStopReason`` which has the + following values: + + * ``"solution found"``: If a perfect layout was found. + * ``"nonexistent solution"``: If no perfect layout was found. + * ``">2q gates in basis"``: If VF2PostLayout can't work with basis + + """ + + def __init__( + self, + target=None, + coupling_map=None, + properties=None, + seed=None, + call_limit=None, + time_limit=None, + strict_direction=True, + ): + """Initialize a ``VF2PostLayout`` pass instance + + Args: + target (Target): A target representing the backend device to run ``VF2PostLayout`` on. + If specified it will supersede a set value for ``properties`` and + ``coupling_map``. + coupling_map (CouplingMap): Directed graph representing a coupling map. + properties (BackendProperties): The backend properties for the backend. If + :meth:`~qiskit.providers.models.BackendProperties.readout_error` is available + it is used to score the layout. + seed (int): Sets the seed of the PRNG. -1 Means no node shuffling. + call_limit (int): The number of state visits to attempt in each execution of + VF2. + time_limit (float): The total time limit in seconds to run ``VF2PostLayout`` + strict_direction (bool): Whether the pass is configured to follow + the strict direction in the coupling graph. If this is set to + false, the pass will treat any edge in the coupling graph as + a weak edge and the interaction graph will be undirected. For + the purposes of evaluating layouts the avg error rate for + each qubit and 2q link will be used. This enables the pass to be + run prior to basis translation and work with any 1q and 2q operations. + However, if ``strict_direction=True`` the pass expects the input + :class:`~.DAGCircuit` object to :meth:`~.VF2PostLayout.run` to be in + the target set of instructions. + + Raises: + TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided. + """ + super().__init__() + self.target = target + self.coupling_map = coupling_map + self.properties = properties + self.call_limit = call_limit + self.time_limit = time_limit + self.seed = seed + self.strict_direction = strict_direction + self.avg_error_map = None + + def run(self, dag): + """run the layout method""" + if self.target is None and (self.coupling_map is None or self.properties is None): + raise TranspilerError( + "A target must be specified or a coupling map and properties must be provided" + ) + if self.strict_direction: + im_graph = PyDiGraph(multigraph=False) + else: + if self.avg_error_map is None: + self.avg_error_map = self._build_average_error_map() + im_graph = PyGraph(multigraph=False) + im_graph_node_map = {} + reverse_im_graph_node_map = {} + + for node in dag.op_nodes(include_directives=False): + len_args = len(node.qargs) + if len_args == 1: + if node.qargs[0] not in im_graph_node_map: + weight = defaultdict(int) + weight[node.name] += 1 + im_graph_node_map[node.qargs[0]] = im_graph.add_node(weight) + reverse_im_graph_node_map[im_graph_node_map[node.qargs[0]]] = node.qargs[0] + else: + im_graph[im_graph_node_map[node.qargs[0]]][node.op.name] += 1 + if len_args == 2: + if node.qargs[0] not in im_graph_node_map: + im_graph_node_map[node.qargs[0]] = im_graph.add_node(defaultdict(int)) + reverse_im_graph_node_map[im_graph_node_map[node.qargs[0]]] = node.qargs[0] + if node.qargs[1] not in im_graph_node_map: + im_graph_node_map[node.qargs[1]] = im_graph.add_node(defaultdict(int)) + reverse_im_graph_node_map[im_graph_node_map[node.qargs[1]]] = node.qargs[1] + edge = (im_graph_node_map[node.qargs[0]], im_graph_node_map[node.qargs[1]]) + if im_graph.has_edge(*edge): + im_graph.get_edge_data(*edge)[node.name] += 1 + else: + weight = defaultdict(int) + weight[node.name] += 1 + im_graph.add_edge(*edge, weight) + if len_args >= 3: + self.property_set[ + "VF2PostLayout_stop_reason" + ] = VF2PostLayoutStopReason.MORE_THAN_2Q + return + + if self.target is not None: + if self.strict_direction: + cm_graph = PyDiGraph(multigraph=False) + else: + cm_graph = PyGraph(multigraph=False) + cm_graph.add_nodes_from( + [self.target.operation_names_for_qargs((i,)) for i in range(self.target.num_qubits)] + ) + for qargs in self.target.qargs: + len_args = len(qargs) + # If qargs == 1 we already populated it and if qargs > 2 there are no instructions + # using those in the circuit because we'd have already returned by this point + if len_args == 2: + cm_graph.add_edge( + qargs[0], qargs[1], self.target.operation_names_for_qargs(qargs) + ) + cm_nodes = list(cm_graph.node_indexes()) + else: + if self.strict_direction: + cm_graph = self.coupling_map.graph + else: + cm_graph = self.coupling_map.graph.to_undirected(multigraph=False) + cm_nodes = list(cm_graph.node_indexes()) + if self.seed != -1: + random.Random(self.seed).shuffle(cm_nodes) + shuffled_cm_graph = type(cm_graph)() + shuffled_cm_graph.add_nodes_from(cm_nodes) + new_edges = [ + (cm_nodes[edge[0]], cm_nodes[edge[1]]) for edge in cm_graph.edge_list() + ] + shuffled_cm_graph.add_edges_from_no_data(new_edges) + cm_nodes = [k for k, v in sorted(enumerate(cm_nodes), key=lambda item: item[1])] + cm_graph = shuffled_cm_graph + + logger.debug("Running VF2 to find post transpile mappings") + if self.target and self.strict_direction: + mappings = vf2_mapping( + cm_graph, + im_graph, + node_matcher=_target_match, + edge_matcher=_target_match, + subgraph=True, + id_order=False, + induced=False, + call_limit=self.call_limit, + ) + else: + mappings = vf2_mapping( + cm_graph, + im_graph, + subgraph=True, + id_order=False, + induced=False, + call_limit=self.call_limit, + ) + chosen_layout = None + initial_layout = Layout(dict(enumerate(dag.qubits))) + try: + if self.strict_direction: + chosen_layout_score = self._score_layout( + initial_layout, im_graph_node_map, reverse_im_graph_node_map, im_graph + ) + else: + chosen_layout_score = self._approx_score_layout( + initial_layout, im_graph_node_map, reverse_im_graph_node_map, im_graph + ) + # Circuit not in basis so we have nothing to compare against return here + except KeyError: + self.property_set[ + "VF2PostLayout_stop_reason" + ] = VF2PostLayoutStopReason.NO_SOLUTION_FOUND + return + + logger.debug("Initial layout has score %s", chosen_layout_score) + + start_time = time.time() + trials = 0 + for mapping in mappings: + trials += 1 + logger.debug("Running trial: %s", trials) + stop_reason = VF2PostLayoutStopReason.SOLUTION_FOUND + layout = Layout( + {reverse_im_graph_node_map[im_i]: cm_nodes[cm_i] for cm_i, im_i in mapping.items()} + ) + if self.strict_direction: + layout_score = self._score_layout( + layout, im_graph_node_map, reverse_im_graph_node_map, im_graph + ) + else: + layout_score = self._approx_score_layout( + layout, im_graph_node_map, reverse_im_graph_node_map, im_graph + ) + logger.debug("Trial %s has score %s", trials, layout_score) + if layout_score < chosen_layout_score: + logger.debug( + "Found layout %s has a lower score (%s) than previous best %s (%s)", + layout, + layout_score, + chosen_layout, + chosen_layout_score, + ) + chosen_layout = layout + chosen_layout_score = layout_score + elapsed_time = time.time() - start_time + if self.time_limit is not None and elapsed_time >= self.time_limit: + logger.debug( + "VFPostLayout has taken %s which exceeds configured max time: %s", + elapsed_time, + self.time_limit, + ) + break + if chosen_layout is None: + stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND + else: + existing_layout = self.property_set["layout"] + # If any ancillas in initial layout map them back to the final layout output + if existing_layout is not None and len(existing_layout) > len(chosen_layout): + virtual_bits = chosen_layout.get_virtual_bits() + used_bits = set(virtual_bits.values()) + num_qubits = len(cm_graph) + for bit in dag.qubits: + if len(chosen_layout) == len(existing_layout): + break + if bit not in virtual_bits: + for i in range(num_qubits): + if i not in used_bits: + used_bits.add(i) + chosen_layout.add(bit, i) + break + self.property_set["post_layout"] = chosen_layout + + self.property_set["VF2PostLayout_stop_reason"] = stop_reason + + def _approx_score_layout(self, layout, bit_map, reverse_bit_map, im_graph): + bits = layout.get_virtual_bits() + fidelity = 1 + for bit, node_index in bit_map.items(): + gate_count = sum(im_graph[node_index].values()) + fidelity *= (1 - self.avg_error_map[(bits[bit],)]) ** gate_count + for edge in im_graph.edge_index_map().values(): + gate_count = sum(edge[2].values()) + qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]]) + if qargs not in self.avg_error_map: + qargs = (qargs[1], qargs[0]) + fidelity *= (1 - self.avg_error_map[qargs]) ** gate_count + return 1 - fidelity + + def _score_layout(self, layout, bit_map, reverse_bit_map, im_graph): + bits = layout.get_virtual_bits() + fidelity = 1 + if self.target is not None: + for bit, node_index in bit_map.items(): + gate_counts = im_graph[node_index] + for gate, count in gate_counts.items(): + if self.target[gate] is not None and None not in self.target[gate]: + props = self.target[gate][(bits[bit],)] + if props is not None and props.error is not None: + fidelity *= (1 - props.error) ** count + + for edge in im_graph.edge_index_map().values(): + qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]]) + gate_counts = edge[2] + for gate, count in gate_counts.items(): + if self.target[gate] is not None and None not in self.target[gate]: + props = self.target[gate][qargs] + if props is not None and props.error is not None: + fidelity *= (1 - props.error) ** count + else: + for bit, node_index in bit_map.items(): + gate_counts = im_graph[node_index] + for gate, count in gate_counts.items(): + if gate == "measure": + try: + fidelity *= (1 - self.properties.readout_error(bits[bit])) ** count + except BackendPropertyError: + pass + else: + try: + fidelity *= (1 - self.properties.gate_error(gate, bits[bit])) ** count + except BackendPropertyError: + pass + for edge in im_graph.edge_index_map().values(): + qargs = (bits[reverse_bit_map[edge[0]]], bits[reverse_bit_map[edge[1]]]) + gate_counts = edge[2] + for gate, count in gate_counts.items(): + try: + fidelity *= (1 - self.properties.gate_error(gate, qargs)) ** count + except BackendPropertyError: + pass + return 1 - fidelity + + def _build_average_error_map(self): + avg_map = {} + if self.target is not None: + for qargs in self.target.qargs: + qarg_error = 0.0 + count = 0 + for op in self.target.operation_names_for_qargs(qargs): + inst_props = self.target[op].get(qargs, None) + if inst_props is not None and inst_props.error is not None: + count += 1 + qarg_error += inst_props.error + avg_map[qargs] = qarg_error / count + else: + errors = defaultdict(list) + for qubit in range(len(self.properties.qubits)): + errors[(qubit,)].append(self.properties.readout_error(qubit)) + for gate in self.properties.gates: + qubits = tuple(gate.qubits) + for param in gate.parameters: + if param.name == "gate_error": + errors[qubits].append(param.value) + avg_map = {k: statistics.mean(v) for k, v in errors.items()} + return avg_map diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 11a57f53da7a..d7bcd8a9940c 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -28,6 +28,7 @@ from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout +from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout @@ -61,6 +62,7 @@ from qiskit.transpiler.passes import Error from qiskit.transpiler.passes import ContainsInstruction from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason +from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler import TranspilerError @@ -299,6 +301,36 @@ def _opt_control(property_set): pm1.append(_embed) pm1.append(_swap_check) pm1.append(_swap, condition=_swap_condition) + if ( + (coupling_map and backend_properties) + and initial_layout is None + and pass_manager_config.layout_method is None + ): + + def _post_layout_condition(property_set): + # if VF2 layout stopped for any reason other than solution found we need + # to run layout since VF2 didn't converge. + if ( + property_set["VF2PostLayout_stop_reason"] is not None + and property_set["VF2PostLayout_stop_reason"] + is VF2PostLayoutStopReason.SOLUTION_FOUND + ): + return True + return False + + pm1.append( + VF2PostLayout( + target, + coupling_map, + backend_properties, + seed_transpiler, + call_limit=int(5e4), # Set call limit to ~100ms with retworkx 0.10.2 + time_limit=0.1, + strict_direction=False, + ), + condition=_trivial_not_perfect, + ) + pm1.append(ApplyLayout(), condition=_post_layout_condition) pm1.append(_unroll) if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) @@ -308,6 +340,7 @@ def _opt_control(property_set): pm1.append(_reset) pm1.append(_depth_check + _size_check) pm1.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) + if inst_map and inst_map.has_custom_gate(): pm1.append(PulseGates(inst_map=inst_map)) diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index bbea2c36b107..bfed693636aa 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -28,6 +28,7 @@ from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout +from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout @@ -61,6 +62,7 @@ from qiskit.transpiler.passes import Error from qiskit.transpiler.passes import ContainsInstruction from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason +from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler import TranspilerError @@ -286,6 +288,36 @@ def _opt_control(property_set): pm2.append(_embed) pm2.append(_swap_check) pm2.append(_swap, condition=_swap_condition) + if ( + (coupling_map and backend_properties) + and initial_layout is None + and pass_manager_config.layout_method is None + ): + + def _post_layout_condition(property_set): + # if VF2 layout stopped for any reason other than solution found we need + # to run layout since VF2 didn't converge. + if ( + property_set["VF2PostLayout_stop_reason"] is not None + and property_set["VF2PostLayout_stop_reason"] + is VF2PostLayoutStopReason.SOLUTION_FOUND + ): + return True + return False + + pm2.append( + VF2PostLayout( + target, + coupling_map, + backend_properties, + seed_transpiler, + call_limit=int(5e6), # Set call limit to ~10 sec with retworkx 0.10.2 + time_limit=10.0, + strict_direction=False, + ) + ) + pm2.append(ApplyLayout(), condition=_post_layout_condition) + pm2.append(_unroll) if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index 707ff263bf9f..3827d7aab151 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -29,6 +29,7 @@ from qiskit.transpiler.passes import GateDirection from qiskit.transpiler.passes import SetLayout from qiskit.transpiler.passes import VF2Layout +from qiskit.transpiler.passes import VF2PostLayout from qiskit.transpiler.passes import TrivialLayout from qiskit.transpiler.passes import DenseLayout from qiskit.transpiler.passes import NoiseAdaptiveLayout @@ -64,6 +65,7 @@ from qiskit.transpiler.passes import Error from qiskit.transpiler.passes import ContainsInstruction from qiskit.transpiler.passes.layout.vf2_layout import VF2LayoutStopReason +from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayoutStopReason from qiskit.transpiler import TranspilerError @@ -297,6 +299,36 @@ def _opt_control(property_set): pm3.append(_embed) pm3.append(_swap_check) pm3.append(_swap, condition=_swap_condition) + if ( + (coupling_map and backend_properties) + and initial_layout is None + and pass_manager_config.layout_method is None + ): + + def _post_layout_condition(property_set): + # if VF2 layout stopped for any reason other than solution found we need + # to run layout since VF2 didn't converge. + if ( + property_set["VF2PostLayout_stop_reason"] is not None + and property_set["VF2PostLayout_stop_reason"] + is VF2PostLayoutStopReason.SOLUTION_FOUND + ): + return True + return False + + pm3.append( + VF2PostLayout( + target, + coupling_map, + backend_properties, + seed_transpiler, + call_limit=int(3e7), # Set call limit to ~60 sec with retworkx 0.10.2 + time_limit=60, + strict_direction=False, + ) + ) + pm3.append(ApplyLayout(), condition=_post_layout_condition) + pm3.append(_unroll) if (coupling_map and not coupling_map.is_symmetric) or ( target is not None and target.get_non_global_operation_names(strict_direction=True) @@ -319,6 +351,7 @@ def _opt_control(property_set): pm3.append(_reset) pm3.append(_depth_check + _size_check) pm3.append(_opt + _unroll + _depth_check + _size_check, do_while=_opt_control) + if inst_map and inst_map.has_custom_gate(): pm3.append(PulseGates(inst_map=inst_map)) diff --git a/releasenotes/notes/vf2-post-layout-f0213e2c7ebb645c.yaml b/releasenotes/notes/vf2-post-layout-f0213e2c7ebb645c.yaml new file mode 100644 index 000000000000..5446d12e9ee6 --- /dev/null +++ b/releasenotes/notes/vf2-post-layout-f0213e2c7ebb645c.yaml @@ -0,0 +1,47 @@ +--- +features: + - | + Added a new transpiler pass, :class:`~.VF2PostLayout`. This pass is of a + new type to perform a new phase/function in the compilation pipeline, + post-layout or post optimization qubit selection. The idea behind this + pass is after we finish the optimization loop in transpiler we + know what the final gate counts will be on each qubit in the circuit so + we can potentially find a better-performing subset of qubits on a backend + to execute the circuit. The pass will search for an isomorphic subgraph in + the connectivity graph of the target backend and look at the full error + rate of the complete circuit on any subgraph found and return the + layout found with the lowest error rate for the circuit. + + This pass is similar to the :class:`~.VF2Layout` pass and both internally + use the same VF2 implementation from + `retworkx `__. However, + :class:`~.VF2PostLayout` is deisgned to run after initial layout, routing, + basis translation, and any optimization passes run and will only work if + a layout has already been applied, the circuit has been routed, and all + gates are in the target basis. This is required so that when a new layout + is applied the circuit can still be run on the target device. :class:`~.VF2Layout` + on the other hand is designed to find a perfect initial layout and can + work with any circuit. + - | + The :class:`~.ApplyLayout` transpiler pass now has support for updating + a layout on a circuit after a layout has been applied once before. If + the ``post_layout`` field is present (in addition to the required + ``layout`` field) the ``property_set`` when the :class:`~.ApplyLayout` pass + is run the pass will update the layout to apply the new layout. This will + return a :class:`~.DAGCircuit` with the qubits in the new physical order + and the ``layout`` property set will be updated so that it maps the + virtual qubits from the original layout to the physical qubits in the new + ``post_layout`` field. + - | + The preset pass managers generated by :func:`~.level_1_pass_manager`, + :func:`~.level_2_pass_manager`, and :func:`~.level_3_pass_manager` which + correspond to ``optimization_level`` 1, 2, and 3 respectively on the + :func:`~.transpile` function now run the :class:`~.VF2PostLayout` pass + after running the routing pass. This enables the transpiler to + potentially find a different set of physical qubits on the target backend + to run the circuit on which have lower error rates. The + :class:`~.VF2PostLayout` pass will not be run if you manually specify a + ``layout_method``, ``routing_method``, or ``initial_layout`` arguments + to :func:`~.transpile`. If the pass can find a better performing subset of + qubits on backend to run the physical circuit it will adjust the layout of + the circuit to use the alternative qubits instead. diff --git a/test/python/providers/test_backend_v2.py b/test/python/providers/test_backend_v2.py index 63a809551700..60571d4ec61f 100644 --- a/test/python/providers/test_backend_v2.py +++ b/test/python/providers/test_backend_v2.py @@ -180,7 +180,7 @@ def test_transpile_mumbai_target(self): expected.sx(0) expected.rz(math.pi / 2, 0) expected.cx(1, 0) - expected.barrier(qr[0:2]) + expected.barrier(qr[0], qr[1]) expected.measure(qr[0], cr[0]) expected.measure(qr[1], cr[1]) self.assertEqual(expected, tqc) diff --git a/test/python/pulse/test_builder.py b/test/python/pulse/test_builder.py index f2a93e45b559..eb3223695b90 100644 --- a/test/python/pulse/test_builder.py +++ b/test/python/pulse/test_builder.py @@ -819,7 +819,8 @@ def test_cx(self): def test_u1(self): """Test u1 gate.""" with pulse.build(self.backend) as schedule: - pulse.u1(np.pi / 2, 0) + with pulse.transpiler_settings(layout_method="trivial"): + pulse.u1(np.pi / 2, 0) reference_qc = circuit.QuantumCircuit(1) reference_qc.append(circuit.library.U1Gate(np.pi / 2), [0]) diff --git a/test/python/transpiler/test_preset_passmanagers.py b/test/python/transpiler/test_preset_passmanagers.py index ef7b30a453c9..33fe942efc5f 100644 --- a/test/python/transpiler/test_preset_passmanagers.py +++ b/test/python/transpiler/test_preset_passmanagers.py @@ -557,24 +557,23 @@ def test_layout_tokyo_2845(self, level): 1: Qubit(QuantumRegister(15, "ancilla"), 1), 2: Qubit(QuantumRegister(15, "ancilla"), 2), 3: Qubit(QuantumRegister(15, "ancilla"), 3), - 4: Qubit(QuantumRegister(3, "qr1"), 0), - 5: Qubit(QuantumRegister(15, "ancilla"), 4), - 6: Qubit(QuantumRegister(15, "ancilla"), 5), + 4: Qubit(QuantumRegister(15, "ancilla"), 4), + 5: Qubit(QuantumRegister(15, "ancilla"), 5), + 6: Qubit(QuantumRegister(3, "qr1"), 1), 7: Qubit(QuantumRegister(15, "ancilla"), 6), - 8: Qubit(QuantumRegister(3, "qr1"), 1), - 9: Qubit(QuantumRegister(15, "ancilla"), 7), - 10: Qubit(QuantumRegister(15, "ancilla"), 8), - 11: Qubit(QuantumRegister(15, "ancilla"), 9), - 12: Qubit(QuantumRegister(15, "ancilla"), 10), - 13: Qubit(QuantumRegister(3, "qr1"), 2), - 14: Qubit(QuantumRegister(2, "qr2"), 0), - 15: Qubit(QuantumRegister(15, "ancilla"), 11), - 16: Qubit(QuantumRegister(15, "ancilla"), 12), - 17: Qubit(QuantumRegister(15, "ancilla"), 13), - 18: Qubit(QuantumRegister(15, "ancilla"), 14), - 19: Qubit(QuantumRegister(2, "qr2"), 1), + 8: Qubit(QuantumRegister(15, "ancilla"), 7), + 9: Qubit(QuantumRegister(15, "ancilla"), 8), + 10: Qubit(QuantumRegister(3, "qr1"), 0), + 11: Qubit(QuantumRegister(3, "qr1"), 2), + 12: Qubit(QuantumRegister(15, "ancilla"), 9), + 13: Qubit(QuantumRegister(15, "ancilla"), 10), + 14: Qubit(QuantumRegister(15, "ancilla"), 11), + 15: Qubit(QuantumRegister(15, "ancilla"), 12), + 16: Qubit(QuantumRegister(2, "qr2"), 0), + 17: Qubit(QuantumRegister(2, "qr2"), 1), + 18: Qubit(QuantumRegister(15, "ancilla"), 13), + 19: Qubit(QuantumRegister(15, "ancilla"), 14), } - # Trivial layout expected_layout_level0 = trivial_layout # Dense layout @@ -652,6 +651,11 @@ def test_layout_tokyo_fully_connected_cx(self, level): } sabre_layout = { + 18: qr[0], + 13: qr[1], + 14: qr[2], + 17: qr[3], + 19: qr[4], 0: ancilla[0], 1: ancilla[1], 2: ancilla[2], @@ -659,19 +663,14 @@ def test_layout_tokyo_fully_connected_cx(self, level): 4: ancilla[4], 5: ancilla[5], 6: ancilla[6], - 7: qr[4], - 8: qr[1], - 9: ancilla[7], - 10: ancilla[8], - 11: ancilla[9], - 12: qr[2], - 13: qr[0], - 14: ancilla[10], - 15: ancilla[11], - 16: ancilla[12], - 17: ancilla[13], - 18: ancilla[14], - 19: qr[3], + 7: ancilla[7], + 8: ancilla[8], + 9: ancilla[9], + 10: ancilla[10], + 11: ancilla[11], + 12: ancilla[12], + 15: ancilla[13], + 16: ancilla[14], } expected_layout_level0 = trivial_layout diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py new file mode 100644 index 000000000000..5c4162c93f27 --- /dev/null +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -0,0 +1,443 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# 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. + +"""Test the VF2Layout pass""" + +import retworkx + +from qiskit import QuantumRegister, QuantumCircuit +from qiskit.transpiler import CouplingMap, Layout, TranspilerError +from qiskit.transpiler.passes.layout.vf2_post_layout import VF2PostLayout, VF2PostLayoutStopReason +from qiskit.converters import circuit_to_dag +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeYorktown, FakeLima, FakeLimaV2, FakeYorktownV2 +from qiskit.circuit import Qubit +from qiskit.compiler.transpiler import transpile + + +class TestVF2PostLayout(QiskitTestCase): + """Tests the VF2Layout pass""" + + seed = 42 + + def assertLayout(self, dag, coupling_map, property_set): + """Checks if the circuit in dag was a perfect layout in property_set for the given + coupling_map""" + self.assertEqual( + property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND + ) + + layout = property_set["post_layout"] + for gate in dag.two_qubit_ops(): + if dag.has_calibration_for(gate): + continue + physical_q0 = layout[gate.qargs[0]] + physical_q1 = layout[gate.qargs[1]] + self.assertTrue(coupling_map.graph.has_edge(physical_q0, physical_q1)) + + def assertLayoutV2(self, dag, target, property_set): + """Checks if the circuit in dag was a perfect layout in property_set for the given + coupling_map""" + self.assertEqual( + property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND + ) + + layout = property_set["post_layout"] + for gate in dag.two_qubit_ops(): + if dag.has_calibration_for(gate): + continue + physical_q0 = layout[gate.qargs[0]] + physical_q1 = layout[gate.qargs[1]] + qargs = (physical_q0, physical_q1) + self.assertTrue(target.instruction_supported(gate.name, qargs)) + + def test_no_constraints(self): + """Test we raise at runtime if no target or coupling graph specified.""" + qc = QuantumCircuit(2) + empty_pass = VF2PostLayout() + with self.assertRaises(TranspilerError): + empty_pass.run(circuit_to_dag(qc)) + + def test_no_backend_properties(self): + """Test we raise at runtime if no properties are provided with a coupling graph.""" + qc = QuantumCircuit(2) + empty_pass = VF2PostLayout(coupling_map=CouplingMap([(0, 1), (1, 2)])) + with self.assertRaises(TranspilerError): + empty_pass.run(circuit_to_dag(qc)) + + def test_empty_circuit(self): + """Test no solution found for empty circuit""" + qc = QuantumCircuit(2, 2) + backend = FakeLima() + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], + VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + ) + + def test_empty_circuit_v2(self): + """Test no solution found for empty circuit with v2 backend""" + qc = QuantumCircuit(2, 2) + backend = FakeLimaV2() + vf2_pass = VF2PostLayout(target=backend.target) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], + VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + ) + + def test_skip_3q_circuit(self): + """Test that the pass is a no-op on circuits with >2q gates.""" + qc = QuantumCircuit(3) + qc.ccx(0, 1, 2) + backend = FakeLima() + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q + ) + + def test_skip_3q_circuit_v2(self): + """Test that the pass is a no-op on circuits with >2q gates with a target.""" + qc = QuantumCircuit(3) + qc.ccx(0, 1, 2) + backend = FakeLimaV2() + vf2_pass = VF2PostLayout(target=backend.target) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q + ) + + def test_best_mapping_ghz_state_full_device_multiple_qregs(self): + """Test best mappings with multiple registers""" + backend = FakeLima() + qr_a = QuantumRegister(2) + qr_b = QuantumRegister(3) + qc = QuantumCircuit(qr_a, qr_b) + qc.h(qr_a[0]) + qc.cx(qr_a[0], qr_a[1]) + qc.cx(qr_a[0], qr_b[0]) + qc.cx(qr_a[0], qr_b[1]) + qc.cx(qr_a[0], qr_b[2]) + qc.measure_all() + tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + pass_ = VF2PostLayout(coupling_map=cmap, properties=props, seed=self.seed) + pass_.run(dag) + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_2q_circuit_5q_backend(self): + """A simple example, without considering the direction + 0 - 1 + qr1 - qr0 + """ + backend = FakeYorktown() + + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) # qr1 -> qr0 + tqc = transpile(circuit, backend, layout_method="dense") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + pass_ = VF2PostLayout(coupling_map=cmap, properties=props, seed=self.seed) + pass_.run(dag) + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_best_mapping_ghz_state_full_device_multiple_qregs_v2(self): + """Test best mappings with multiple registers""" + backend = FakeLimaV2() + qr_a = QuantumRegister(2) + qr_b = QuantumRegister(3) + qc = QuantumCircuit(qr_a, qr_b) + qc.h(qr_a[0]) + qc.cx(qr_a[0], qr_a[1]) + qc.cx(qr_a[0], qr_b[0]) + qc.cx(qr_a[0], qr_b[1]) + qc.cx(qr_a[0], qr_b[2]) + qc.measure_all() + tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed) + pass_.run(dag) + self.assertLayoutV2(dag, backend.target, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_2q_circuit_5q_backend_v2(self): + """A simple example, without considering the direction + 0 - 1 + qr1 - qr0 + """ + backend = FakeYorktownV2() + + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) # qr1 -> qr0 + tqc = transpile(circuit, backend, layout_method="dense") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed) + pass_.run(dag) + self.assertLayoutV2(dag, backend.target, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_target_invalid_2q_gate(self): + """Test that we don't find a solution with a gate outside target.""" + backend = FakeYorktownV2() + qc = QuantumCircuit(2) + qc.ecr(0, 1) + dag = circuit_to_dag(qc) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed) + pass_.run(dag) + self.assertEqual( + pass_.property_set["VF2PostLayout_stop_reason"], + VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + ) + + +class TestVF2PostLayoutScoring(QiskitTestCase): + """Test scoring heuristic function for VF2PostLayout.""" + + def test_empty_score(self): + """Test error rate is 0 for empty circuit.""" + bit_map = {} + reverse_bit_map = {} + im_graph = retworkx.PyDiGraph() + backend = FakeYorktownV2() + vf2_pass = VF2PostLayout(target=backend.target) + layout = Layout() + score = vf2_pass._score_layout(layout, bit_map, reverse_bit_map, im_graph) + self.assertEqual(0, score) + + def test_all_1q_score(self): + """Test error rate for all 1q input.""" + bit_map = {Qubit(): 0, Qubit(): 1} + reverse_bit_map = {v: k for k, v in bit_map.items()} + im_graph = retworkx.PyDiGraph() + im_graph.add_node({"sx": 1}) + im_graph.add_node({"sx": 1}) + backend = FakeYorktownV2() + vf2_pass = VF2PostLayout(target=backend.target) + layout = Layout(bit_map) + score = vf2_pass._score_layout(layout, bit_map, reverse_bit_map, im_graph) + self.assertAlmostEqual(0.002925, score, places=5) + + def test_all_1q_avg_score(self): + """Test average scoring for all 1q input.""" + bit_map = {Qubit(): 0, Qubit(): 1} + reverse_bit_map = {v: k for k, v in bit_map.items()} + im_graph = retworkx.PyDiGraph() + im_graph.add_node({"sx": 1}) + im_graph.add_node({"sx": 1}) + backend = FakeYorktownV2() + vf2_pass = VF2PostLayout(target=backend.target) + vf2_pass.avg_error_map = vf2_pass._build_average_error_map() + layout = Layout(bit_map) + score = vf2_pass._approx_score_layout(layout, bit_map, reverse_bit_map, im_graph) + self.assertAlmostEqual(0.02054, score, places=5) + + +class TestVF2PostLayoutUndirected(QiskitTestCase): + """Tests the VF2Layout pass""" + + seed = 42 + + def assertLayout(self, dag, coupling_map, property_set): + """Checks if the circuit in dag was a perfect layout in property_set for the given + coupling_map""" + self.assertEqual( + property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND + ) + + layout = property_set["post_layout"] + for gate in dag.two_qubit_ops(): + if dag.has_calibration_for(gate): + continue + physical_q0 = layout[gate.qargs[0]] + physical_q1 = layout[gate.qargs[1]] + self.assertTrue(coupling_map.graph.has_edge(physical_q0, physical_q1)) + + def assertLayoutV2(self, dag, target, property_set): + """Checks if the circuit in dag was a perfect layout in property_set for the given + coupling_map""" + self.assertEqual( + property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.SOLUTION_FOUND + ) + + layout = property_set["post_layout"] + for gate in dag.two_qubit_ops(): + if dag.has_calibration_for(gate): + continue + physical_q0 = layout[gate.qargs[0]] + physical_q1 = layout[gate.qargs[1]] + qargs = (physical_q0, physical_q1) + self.assertTrue(target.instruction_supported(gate.name, qargs)) + + def test_no_constraints(self): + """Test we raise at runtime if no target or coupling graph specified.""" + qc = QuantumCircuit(2) + empty_pass = VF2PostLayout(strict_direction=False) + with self.assertRaises(TranspilerError): + empty_pass.run(circuit_to_dag(qc)) + + def test_no_backend_properties(self): + """Test we raise at runtime if no properties are provided with a coupling graph.""" + qc = QuantumCircuit(2) + empty_pass = VF2PostLayout( + coupling_map=CouplingMap([(0, 1), (1, 2)]), strict_direction=False + ) + with self.assertRaises(TranspilerError): + empty_pass.run(circuit_to_dag(qc)) + + def test_empty_circuit(self): + """Test no solution found for empty circuit""" + qc = QuantumCircuit(2, 2) + backend = FakeLima() + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], + VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + ) + + def test_empty_circuit_v2(self): + """Test no solution found for empty circuit with v2 backend""" + qc = QuantumCircuit(2, 2) + backend = FakeLimaV2() + vf2_pass = VF2PostLayout(target=backend.target, strict_direction=False) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], + VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + ) + + def test_skip_3q_circuit(self): + """Test that the pass is a no-op on circuits with >2q gates.""" + qc = QuantumCircuit(3) + qc.ccx(0, 1, 2) + backend = FakeLima() + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + vf2_pass = VF2PostLayout(coupling_map=cmap, properties=props, strict_direction=False) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q + ) + + def test_skip_3q_circuit_v2(self): + """Test that the pass is a no-op on circuits with >2q gates with a target.""" + qc = QuantumCircuit(3) + qc.ccx(0, 1, 2) + backend = FakeLimaV2() + vf2_pass = VF2PostLayout(target=backend.target, strict_direction=False) + vf2_pass.run(circuit_to_dag(qc)) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], VF2PostLayoutStopReason.MORE_THAN_2Q + ) + + def test_best_mapping_ghz_state_full_device_multiple_qregs(self): + """Test best mappings with multiple registers""" + backend = FakeLima() + qr_a = QuantumRegister(2) + qr_b = QuantumRegister(3) + qc = QuantumCircuit(qr_a, qr_b) + qc.h(qr_a[0]) + qc.cx(qr_a[0], qr_a[1]) + qc.cx(qr_a[0], qr_b[0]) + qc.cx(qr_a[0], qr_b[1]) + qc.cx(qr_a[0], qr_b[2]) + qc.measure_all() + tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + pass_ = VF2PostLayout( + coupling_map=cmap, properties=props, seed=self.seed, strict_direction=False + ) + pass_.run(dag) + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_2q_circuit_5q_backend(self): + """A simple example, without considering the direction + 0 - 1 + qr1 - qr0 + """ + backend = FakeYorktown() + + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) # qr1 -> qr0 + tqc = transpile(circuit, backend, layout_method="dense") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + cmap = CouplingMap(backend.configuration().coupling_map) + props = backend.properties() + pass_ = VF2PostLayout( + coupling_map=cmap, properties=props, seed=self.seed, strict_direction=False + ) + pass_.run(dag) + self.assertLayout(dag, cmap, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_best_mapping_ghz_state_full_device_multiple_qregs_v2(self): + """Test best mappings with multiple registers""" + backend = FakeLimaV2() + qr_a = QuantumRegister(2) + qr_b = QuantumRegister(3) + qc = QuantumCircuit(qr_a, qr_b) + qc.h(qr_a[0]) + qc.cx(qr_a[0], qr_a[1]) + qc.cx(qr_a[0], qr_b[0]) + qc.cx(qr_a[0], qr_b[1]) + qc.cx(qr_a[0], qr_b[2]) + qc.measure_all() + tqc = transpile(qc, backend, seed_transpiler=self.seed, layout_method="trivial") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) + pass_.run(dag) + self.assertLayoutV2(dag, backend.target, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout) + + def test_2q_circuit_5q_backend_v2(self): + """A simple example, without considering the direction + 0 - 1 + qr1 - qr0 + """ + backend = FakeYorktownV2() + + qr = QuantumRegister(2, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[1], qr[0]) # qr1 -> qr0 + tqc = transpile(circuit, backend, layout_method="dense") + initial_layout = tqc._layout + dag = circuit_to_dag(tqc) + pass_ = VF2PostLayout(target=backend.target, seed=self.seed, strict_direction=False) + pass_.run(dag) + self.assertLayoutV2(dag, backend.target, pass_.property_set) + self.assertNotEqual(pass_.property_set["post_layout"], initial_layout)