diff --git a/qiskit/transpiler/passes/layout/vf2_post_layout.py b/qiskit/transpiler/passes/layout/vf2_post_layout.py index 96ffc745b451..c1b1da8d4190 100644 --- a/qiskit/transpiler/passes/layout/vf2_post_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_post_layout.py @@ -35,6 +35,7 @@ class VF2PostLayoutStopReason(Enum): """Stop reasons for VF2PostLayout pass.""" SOLUTION_FOUND = "solution found" + NO_BETTER_SOLUTION_FOUND = "no better solution found" NO_SOLUTION_FOUND = "nonexistent solution" MORE_THAN_2Q = ">2q gates in basis" @@ -51,7 +52,7 @@ def _target_match(node_a, node_b): class VF2PostLayout(AnalysisPass): - """A pass for choosing a Layout after transpilation of a circuit onto a + """A pass for improving an existing Layout after transpilation of a circuit onto a Coupling graph, as a subgraph isomorphism problem, solved by VF2++. Unlike the :class:`~.VF2Layout` transpiler pass which is designed to find an @@ -66,15 +67,16 @@ class VF2PostLayout(AnalysisPass): If a solution is found that means there is a lower error layout available for the circuit. If a solution is found the layout will be set in the property set as - ``property_set['post_layout']``. However, if no solution is found, no + ``property_set['post_layout']``. However, if no solution or no better solution is found, no ``property_set['post_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 solution was found. + * ``"no better solution found"``: If the initial layout of the circuit is the best solution. * ``"nonexistent solution"``: If no solution was found. - * ``">2q gates in basis"``: If VF2PostLayout can't work with basis + * ``">2q gates in basis"``: If VF2PostLayout can't work with the basis of the circuit. By default this pass will construct a heuristic scoring map based on the the error rates in the provided ``target`` (or ``properties`` if ``target`` @@ -278,6 +280,8 @@ def run(self, dag): self.strict_direction, run_in_parallel, ) + chosen_layout = initial_layout + stop_reason = VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND # Circuit not in basis so we have nothing to compare against return here except KeyError: self.property_set[ @@ -292,7 +296,6 @@ def run(self, dag): for mapping in mappings: trials += 1 logger.debug("Running trial: %s", trials) - stop_reason = VF2PostLayoutStopReason.SOLUTION_FOUND layout_mapping = {im_i: cm_nodes[cm_i] for cm_i, im_i in mapping.items()} if self.strict_direction: layout = Layout( @@ -325,6 +328,7 @@ def run(self, dag): ) chosen_layout = layout chosen_layout_score = layout_score + stop_reason = VF2PostLayoutStopReason.SOLUTION_FOUND if self.max_trials and trials >= self.max_trials: logger.debug("Trial %s is >= configured max trials %s", trials, self.max_trials) @@ -338,9 +342,7 @@ def run(self, dag): self.time_limit, ) break - if chosen_layout is None: - stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND - else: + if stop_reason == VF2PostLayoutStopReason.SOLUTION_FOUND: chosen_layout = vf2_utils.map_free_qubits( free_nodes, chosen_layout, @@ -364,7 +366,10 @@ def run(self, dag): chosen_layout.add(bit, i) break self.property_set["post_layout"] = chosen_layout - + else: + if chosen_layout is None: + stop_reason = VF2PostLayoutStopReason.NO_SOLUTION_FOUND + # else the initial layout is optimal -> don't set post_layout, return 'no better solution' self.property_set["VF2PostLayout_stop_reason"] = stop_reason def _score_layout(self, layout, bit_map, reverse_bit_map, im_graph): diff --git a/releasenotes/notes/vf2postlayout-no-better-solution-eb5ced3c8a60ea23.yaml b/releasenotes/notes/vf2postlayout-no-better-solution-eb5ced3c8a60ea23.yaml new file mode 100644 index 000000000000..25d361e51896 --- /dev/null +++ b/releasenotes/notes/vf2postlayout-no-better-solution-eb5ced3c8a60ea23.yaml @@ -0,0 +1,7 @@ +--- +features: + - | + :class:`.VF2PostLayout` now distinguishes between 'no solution' and 'no better solution' when + determining a :class:`.Layout` for a given quantum circuit. 'no better solution' is set when the + initial layout of a quantum circuit is also the optimal one, i.e. incurs the least cost in terms of + error rates. diff --git a/test/python/transpiler/test_vf2_post_layout.py b/test/python/transpiler/test_vf2_post_layout.py index d06e46a229bd..da0dbb274c2b 100644 --- a/test/python/transpiler/test_vf2_post_layout.py +++ b/test/python/transpiler/test_vf2_post_layout.py @@ -108,7 +108,7 @@ def test_empty_circuit(self): vf2_pass.run(circuit_to_dag(qc)) self.assertEqual( vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) def test_empty_circuit_v2(self): @@ -119,7 +119,7 @@ def test_empty_circuit_v2(self): vf2_pass.run(circuit_to_dag(qc)) self.assertEqual( vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) def test_skip_3q_circuit(self): @@ -389,6 +389,46 @@ def test_target_some_error(self): # No layout selected because nothing will beat initial layout self.assertNotIn("post_layout", vf2_pass.property_set) + def test_trivial_layout_is_best(self): + """Test that vf2postlayout reports no better solution if the trivial layout is the best layout""" + n_qubits = 4 + trivial_target = Target() + trivial_target.add_instruction( + CXGate(), {(i, i + 1): InstructionProperties(error=0.001) for i in range(n_qubits - 1)} + ) + + circuit = QuantumCircuit(n_qubits) + circuit.cx(0, 1) + circuit.cx(1, 2) + + vf2_pass = VF2PostLayout(target=trivial_target, seed=self.seed, strict_direction=False) + dag = circuit_to_dag(circuit) + vf2_pass.run(dag) + self.assertEqual( + vf2_pass.property_set["VF2PostLayout_stop_reason"], + VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, + ) + + def test_last_qubits_best(self): + """Test that vf2postlayout determines the best layout when the last qubits have least error""" + n_qubits = 4 + target_last_qubits_best = Target() + target_last_qubits_best.add_instruction( + CXGate(), + {(i, i + 1): InstructionProperties(error=10**-i) for i in range(n_qubits - 1)}, + ) + + circuit = QuantumCircuit(n_qubits) + circuit.cx(0, 1) + circuit.cx(1, 2) + + vf2_pass = VF2PostLayout( + target=target_last_qubits_best, seed=self.seed, strict_direction=False + ) + dag = circuit_to_dag(circuit) + vf2_pass.run(dag) + self.assertLayout(dag, target_last_qubits_best.build_coupling_map(), vf2_pass.property_set) + class TestVF2PostLayoutScoring(QiskitTestCase): """Test scoring heuristic function for VF2PostLayout.""" @@ -480,7 +520,7 @@ def test_empty_circuit(self): vf2_pass.run(circuit_to_dag(qc)) self.assertEqual( vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) def test_empty_circuit_v2(self): @@ -491,7 +531,7 @@ def test_empty_circuit_v2(self): vf2_pass.run(circuit_to_dag(qc)) self.assertEqual( vf2_pass.property_set["VF2PostLayout_stop_reason"], - VF2PostLayoutStopReason.NO_SOLUTION_FOUND, + VF2PostLayoutStopReason.NO_BETTER_SOLUTION_FOUND, ) def test_skip_3q_circuit(self):