From b2c9f7008e6697d72ac3bc7639bd9f6540086841 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 4 Mar 2022 07:45:23 -0500 Subject: [PATCH 1/9] Make VF2Layout pass Target aware This commit updates the VF2Layout pass to be target aware. It adds a new kwarg for specifying a target and if it's specified that target object is used to define the parameters for finding the isomorphic mapping used for the layout. However, with this commit the extra information contained in the target isn't really leveraged yet and just the global coupling graph and measurement error rates are pulled from the target just as in the BackendV1 case. This commit is mostly to facilitate future expansion where we will improve the layout scoring heuristic used and having the full set of data available in the target will be useful. Part of #7113 --- qiskit/transpiler/passes/layout/vf2_layout.py | 62 +++++++++++++------ .../transpiler/preset_passmanagers/level1.py | 1 + .../transpiler/preset_passmanagers/level2.py | 1 + .../transpiler/preset_passmanagers/level3.py | 1 + test/python/transpiler/test_vf2_layout.py | 23 ++++++- 5 files changed, 68 insertions(+), 20 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index fb4d95718a1b..81e0ef5f9b14 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -54,13 +54,14 @@ class VF2Layout(AnalysisPass): def __init__( self, - coupling_map, + coupling_map=None, strict_direction=False, seed=None, call_limit=None, time_limit=None, properties=None, max_trials=None, + target=None, ): """Initialize a ``VF2Layout`` pass instance @@ -80,14 +81,26 @@ def __init__( based on the number of edges in the interaction graph or the coupling graph (whichever is larger). If set to a value <= 0 no limit on the number of trials will be set. + target (Target): A target representing the backend device to run vf2layout on. + If specified it will supersede a set value for ``properties`` and + ``coupling_map``. + + Raises: + TypeError: If neither ``coupling_map`` or ``target`` are provided. """ super().__init__() - self.coupling_map = coupling_map + self.target = target + if target is not None: + self.coupling_map = self.target.build_coupling_map() + elif coupling_map is not None: + self.coupling_map = coupling_map + else: + raise TypeError("coupling_map or target must be specified.") + self.properties = properties self.strict_direction = strict_direction self.seed = seed self.call_limit = call_limit self.time_limit = time_limit - self.properties = properties self.max_trials = max_trials def run(self, dag): @@ -197,20 +210,33 @@ def _score_layout(self, layout): to weight against higher connectivity qubits.""" bits = layout.get_physical_bits() score = 0 - if self.properties is None: - # Sum qubit degree for each qubit in chosen layout as really rough estimate of error + if self.target: + if "measure" in self.target: + for bit in bits: + props = self.target["measure"].get((bit,)) + if props is None or props.error is None: + score += ( + self.coupling_map.graph.out_degree(bit) + + self.coupling_map.graph.in_degree(bit) + ) / len(self.coupling_map.graph) + else: + score += props.error + else: + if self.properties is None: + # Sum qubit degree for each qubit in chosen layout as really rough estimate of error + for bit in bits: + score += self.coupling_map.graph.out_degree( + bit + ) + self.coupling_map.graph.in_degree(bit) + return score for bit in bits: - score += self.coupling_map.graph.out_degree( - bit - ) + self.coupling_map.graph.in_degree(bit) - return score - for bit in bits: - try: - score += self.properties.readout_error(bit) - # If readout error can't be found in properties fallback to degree - # divided by number of qubits as a terrible approximation - except BackendPropertyError: - score += ( - self.coupling_map.graph.out_degree(bit) + self.coupling_map.graph.in_degree(bit) - ) / len(self.coupling_map.graph) + try: + score += self.properties.readout_error(bit) + # If readout error can't be found in properties fallback to degree + # divided by number of qubits as a terrible approximation + except BackendPropertyError: + score += ( + self.coupling_map.graph.out_degree(bit) + + self.coupling_map.graph.in_degree(bit) + ) / len(self.coupling_map.graph) return score diff --git a/qiskit/transpiler/preset_passmanagers/level1.py b/qiskit/transpiler/preset_passmanagers/level1.py index 86d2de27cb9f..4c966ead5a4b 100644 --- a/qiskit/transpiler/preset_passmanagers/level1.py +++ b/qiskit/transpiler/preset_passmanagers/level1.py @@ -153,6 +153,7 @@ def _vf2_match_not_found(property_set): call_limit=int(5e4), # Set call limit to ~100ms with retworkx 0.10.2 time_limit=0.1, properties=backend_properties, + target=target, ) ) diff --git a/qiskit/transpiler/preset_passmanagers/level2.py b/qiskit/transpiler/preset_passmanagers/level2.py index a091b992d851..16ed4b7ab44c 100644 --- a/qiskit/transpiler/preset_passmanagers/level2.py +++ b/qiskit/transpiler/preset_passmanagers/level2.py @@ -150,6 +150,7 @@ def _vf2_match_not_found(property_set): call_limit=int(5e6), # Set call limit to ~10 sec with retworkx 0.10.2 time_limit=10.0, properties=backend_properties, + target=target, ) ) diff --git a/qiskit/transpiler/preset_passmanagers/level3.py b/qiskit/transpiler/preset_passmanagers/level3.py index d8e6a70448ba..91af4cf7d9e8 100644 --- a/qiskit/transpiler/preset_passmanagers/level3.py +++ b/qiskit/transpiler/preset_passmanagers/level3.py @@ -153,6 +153,7 @@ def _vf2_match_not_found(property_set): call_limit=int(3e7), # Set call limit to ~60 sec with retworkx 0.10.2 time_limit=60, properties=backend_properties, + target=target, ) ) # 2b. if VF2 didn't converge on a solution use layout_method (dense). diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index f39ea8bf8346..1ee1b0f6f874 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -17,12 +17,12 @@ import retworkx from qiskit import QuantumRegister, QuantumCircuit -from qiskit.transpiler import CouplingMap, Layout +from qiskit.transpiler import CouplingMap, Layout, Target from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase from qiskit.test.mock import FakeTenerife, FakeRueschlikon, FakeManhattan, FakeYorktown -from qiskit.circuit.library import GraphState +from qiskit.circuit.library import GraphState, CXGate class LayoutTestCase(QiskitTestCase): @@ -119,6 +119,25 @@ def test_call_limit(self): pass_.property_set["VF2Layout_stop_reason"], VF2LayoutStopReason.NO_SOLUTION_FOUND ) + def test_coupling_map_and_target(self): + """Test that a Target is used instead of a CouplingMap if both are specified.""" + cmap = CouplingMap([[0, 1], [1, 2], [2, 0]]) + target = Target() + target.add_instruction(CXGate(), {(0, 1): None, (1, 2): None, (1, 0): None}) + qr = QuantumRegister(3, "qr") + circuit = QuantumCircuit(qr) + circuit.cx(qr[0], qr[1]) # qr0-> qr1 + circuit.cx(qr[1], qr[2]) # qr1-> qr2 + dag = circuit_to_dag(circuit) + pass_ = VF2Layout(cmap, seed=-1, max_trials=1, target=target) + pass_.run(dag) + self.assertLayout(dag, target.build_coupling_map(), pass_.property_set) + + def test_neither_coupling_map_or_target(self): + """Test that we raise if neither a target or coupling map is specified.""" + with self.assertRaises(TypeError): + VF2Layout(seed=123, call_limit=1000, time_limit=20, max_trials=7) + class TestVF2LayoutLattice(LayoutTestCase): """Fit in 25x25 hexagonal lattice coupling map""" From 23c0ad1451e416709da434df965168a3609a73f6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 9 Mar 2022 16:05:53 -0500 Subject: [PATCH 2/9] Don't raise in __init__ with no coupling map --- qiskit/transpiler/passes/layout/vf2_layout.py | 8 +++++--- test/python/transpiler/test_vf2_layout.py | 9 ++++++--- 2 files changed, 11 insertions(+), 6 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 81e0ef5f9b14..7b52e242422e 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -20,6 +20,7 @@ from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError from qiskit.providers.exceptions import BackendPropertyError @@ -92,10 +93,8 @@ def __init__( self.target = target if target is not None: self.coupling_map = self.target.build_coupling_map() - elif coupling_map is not None: - self.coupling_map = coupling_map else: - raise TypeError("coupling_map or target must be specified.") + self.coupling_map = coupling_map self.properties = properties self.strict_direction = strict_direction self.seed = seed @@ -105,6 +104,9 @@ def __init__( def run(self, dag): """run the layout method""" + if self.coupling_map is None: + raise TranspilerError("coupling_map or target must be specified.") + qubits = dag.qubits qubit_indices = {qubit: index for index, qubit in enumerate(qubits)} diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 1ee1b0f6f874..014e557e1a44 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -17,7 +17,7 @@ import retworkx from qiskit import QuantumRegister, QuantumCircuit -from qiskit.transpiler import CouplingMap, Layout, Target +from qiskit.transpiler import CouplingMap, Layout, Target, TranspilerError from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason from qiskit.converters import circuit_to_dag from qiskit.test import QiskitTestCase @@ -135,8 +135,11 @@ def test_coupling_map_and_target(self): def test_neither_coupling_map_or_target(self): """Test that we raise if neither a target or coupling map is specified.""" - with self.assertRaises(TypeError): - VF2Layout(seed=123, call_limit=1000, time_limit=20, max_trials=7) + vf2_pass = VF2Layout(seed=123, call_limit=1000, time_limit=20, max_trials=7) + circuit = QuantumCircuit(2) + dag = circuit_to_dag(circuit) + with self.assertRaises(TranspilerError): + vf2_pass.run(dag) class TestVF2LayoutLattice(LayoutTestCase): From 668731fd55e5d464c8ee8291b78943270f30ef1d Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 9 Mar 2022 16:15:52 -0500 Subject: [PATCH 3/9] Add release note --- .../notes/vf2layout-target-51cc8f77fdfcde67.yaml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml diff --git a/releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml b/releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml new file mode 100644 index 000000000000..e53d7855e106 --- /dev/null +++ b/releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml @@ -0,0 +1,9 @@ +--- +features: + - | + The :class:`~.VF2Layout` transpiler pass has a new keyword argument, + ``target`` which is used to provide a :class:`~.Target` object for + the pass. When specified the :class:`~.Target` will be used by the + pass for all information about the target device. If it is specified + the ``target`` option will take priority over the ``coupling_map`` and + ``properties`` arguments. From 1daa36b6220192ecda6246e9fbf1138b7560c3e4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 30 Mar 2022 14:27:58 -0400 Subject: [PATCH 4/9] Apply suggestions from code review Co-authored-by: Jake Lishman --- qiskit/transpiler/passes/layout/vf2_layout.py | 4 ++-- releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 7b52e242422e..ec23ac90eb2b 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -87,7 +87,7 @@ def __init__( ``coupling_map``. Raises: - TypeError: If neither ``coupling_map`` or ``target`` are provided. + TypeError: At runtime, if neither ``coupling_map`` or ``target`` are provided. """ super().__init__() self.target = target @@ -212,7 +212,7 @@ def _score_layout(self, layout): to weight against higher connectivity qubits.""" bits = layout.get_physical_bits() score = 0 - if self.target: + if self.target is not None: if "measure" in self.target: for bit in bits: props = self.target["measure"].get((bit,)) diff --git a/releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml b/releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml index e53d7855e106..47238955ee0b 100644 --- a/releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml +++ b/releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml @@ -3,7 +3,7 @@ features: - | The :class:`~.VF2Layout` transpiler pass has a new keyword argument, ``target`` which is used to provide a :class:`~.Target` object for - the pass. When specified the :class:`~.Target` will be used by the - pass for all information about the target device. If it is specified + the pass. When specified, the :class:`~.Target` will be used by the + pass for all information about the target device. If it is specified, the ``target`` option will take priority over the ``coupling_map`` and ``properties`` arguments. From 7ffec344defda4f47d73f759a4079995bfc61ea1 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 30 Mar 2022 14:31:50 -0400 Subject: [PATCH 5/9] Fix test to make it clear it's using target graph and not the coupling map --- test/python/transpiler/test_vf2_layout.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test/python/transpiler/test_vf2_layout.py b/test/python/transpiler/test_vf2_layout.py index 014e557e1a44..3c1ed51bc99d 100644 --- a/test/python/transpiler/test_vf2_layout.py +++ b/test/python/transpiler/test_vf2_layout.py @@ -121,13 +121,14 @@ def test_call_limit(self): def test_coupling_map_and_target(self): """Test that a Target is used instead of a CouplingMap if both are specified.""" - cmap = CouplingMap([[0, 1], [1, 2], [2, 0]]) + cmap = CouplingMap([[0, 1], [1, 2]]) target = Target() target.add_instruction(CXGate(), {(0, 1): None, (1, 2): None, (1, 0): None}) qr = QuantumRegister(3, "qr") circuit = QuantumCircuit(qr) circuit.cx(qr[0], qr[1]) # qr0-> qr1 circuit.cx(qr[1], qr[2]) # qr1-> qr2 + circuit.cx(qr[1], qr[0]) # qr1-> qr0 dag = circuit_to_dag(circuit) pass_ = VF2Layout(cmap, seed=-1, max_trials=1, target=target) pass_.run(dag) From f7fb8a59968f465c1016f451e4a6462d2b920c8a Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 30 Mar 2022 14:52:34 -0400 Subject: [PATCH 6/9] Handle edge case where target doesn't have measurement defined This commit fixes an edge case in the heurstic scoring where if a target is present but doesn't have measurement defined we always return a score of 0. In the case measure ment doesn't have measurement defined this will fall back to looking at the degree of the qubit instead of trying to use the readout error rate. --- qiskit/transpiler/passes/layout/vf2_layout.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index ec23ac90eb2b..3cb0dbf8a6a0 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -211,9 +211,10 @@ def _score_layout(self, layout): on the chosen qubits. If BackendProperties are not available it uses the coupling map degree to weight against higher connectivity qubits.""" bits = layout.get_physical_bits() - score = 0 + score = None if self.target is not None: if "measure" in self.target: + score = 0 for bit in bits: props = self.target["measure"].get((bit,)) if props is None or props.error is None: @@ -223,7 +224,8 @@ def _score_layout(self, layout): ) / len(self.coupling_map.graph) else: score += props.error - else: + if score is None: + score = 0 if self.properties is None: # Sum qubit degree for each qubit in chosen layout as really rough estimate of error for bit in bits: From f16799672613fd0e3c6127bc1350f3efb6a2e968 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 30 Mar 2022 15:01:37 -0400 Subject: [PATCH 7/9] Update qiskit/transpiler/passes/layout/vf2_layout.py Co-authored-by: Luciano Bello --- qiskit/transpiler/passes/layout/vf2_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 3cb0dbf8a6a0..ae898465ede0 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -82,7 +82,7 @@ def __init__( based on the number of edges in the interaction graph or the coupling graph (whichever is larger). If set to a value <= 0 no limit on the number of trials will be set. - target (Target): A target representing the backend device to run vf2layout on. + target (Target): A target representing the backend device to run ``VF2Layout`` on. If specified it will supersede a set value for ``properties`` and ``coupling_map``. From 81a289738543e1ed5e6265d4f7dd5e09b61c95d4 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 30 Mar 2022 15:28:03 -0400 Subject: [PATCH 8/9] Simplify heuristic scoring logic Co-authored-by: Luciano Bello --- qiskit/transpiler/passes/layout/vf2_layout.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index ae898465ede0..859bf3076f0f 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -211,10 +211,8 @@ def _score_layout(self, layout): on the chosen qubits. If BackendProperties are not available it uses the coupling map degree to weight against higher connectivity qubits.""" bits = layout.get_physical_bits() - score = None - if self.target is not None: - if "measure" in self.target: - score = 0 + score = 0 + if self.target is not None and "measure" in self.target: for bit in bits: props = self.target["measure"].get((bit,)) if props is None or props.error is None: @@ -224,8 +222,7 @@ def _score_layout(self, layout): ) / len(self.coupling_map.graph) else: score += props.error - if score is None: - score = 0 + else: if self.properties is None: # Sum qubit degree for each qubit in chosen layout as really rough estimate of error for bit in bits: From eea2444e08cba657f458c4ab75963221755181ab Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 30 Mar 2022 15:58:39 -0400 Subject: [PATCH 9/9] Fix lint --- qiskit/transpiler/passes/layout/vf2_layout.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/passes/layout/vf2_layout.py b/qiskit/transpiler/passes/layout/vf2_layout.py index 859bf3076f0f..c5987e8e8bba 100644 --- a/qiskit/transpiler/passes/layout/vf2_layout.py +++ b/qiskit/transpiler/passes/layout/vf2_layout.py @@ -213,15 +213,15 @@ def _score_layout(self, layout): bits = layout.get_physical_bits() score = 0 if self.target is not None and "measure" in self.target: - for bit in bits: - props = self.target["measure"].get((bit,)) - if props is None or props.error is None: - score += ( - self.coupling_map.graph.out_degree(bit) - + self.coupling_map.graph.in_degree(bit) - ) / len(self.coupling_map.graph) - else: - score += props.error + for bit in bits: + props = self.target["measure"].get((bit,)) + if props is None or props.error is None: + score += ( + self.coupling_map.graph.out_degree(bit) + + self.coupling_map.graph.in_degree(bit) + ) / len(self.coupling_map.graph) + else: + score += props.error else: if self.properties is None: # Sum qubit degree for each qubit in chosen layout as really rough estimate of error