Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Make VF2Layout pass Target aware #7735

Merged
merged 15 commits into from
Mar 31, 2022
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 49 additions & 19 deletions qiskit/transpiler/passes/layout/vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -54,13 +55,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

Expand All @@ -80,18 +82,31 @@ 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.
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
If specified it will supersede a set value for ``properties`` and
``coupling_map``.

Raises:
TypeError: At runtime, 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()
else:
self.coupling_map = coupling_map
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):
"""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)}

Expand Down Expand Up @@ -196,21 +211,36 @@ 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
if self.properties is None:
# Sum qubit degree for each qubit in chosen layout as really rough estimate of error
score = None
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
if self.target is not None:
if "measure" in self.target:
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
score = 0
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
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
if score is None:
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
score = 0
mtreinish marked this conversation as resolved.
Show resolved Hide resolved
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
Comment on lines +228 to +232
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is pre-existing and doesn't need to be changed (nor should it, this close to release!), but it's weird to me that this path doesn't divide by the size of the graph, unlike other paths this approximation is used in.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's mostly a matter of relative values on the other qubits for the heuristic score that's returned. In the other paths when we have an error rate on some qubits that's always going to be < 1. So in the other paths we divide by the number of nodes in the absence of an error rate to make the value < 1 to hopefully not score super unfavorably towards qubits that could be potentially good and just happen to be missing an error rate in the backend but are still defined in the connectivity graph.

In the case of this path when we have no error rates it doesn't really matter because all the other scores are going to be in the same situation so we want to prefer to use a node with lower degree. We could divide but we don't have to as it wouldn't change the relative score compared to the other qubits.

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
1 change: 1 addition & 0 deletions qiskit/transpiler/preset_passmanagers/level1.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,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,
)
)

Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/preset_passmanagers/level2.py
Original file line number Diff line number Diff line change
Expand Up @@ -154,6 +154,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,
)
)

Expand Down
1 change: 1 addition & 0 deletions qiskit/transpiler/preset_passmanagers/level3.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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).
Expand Down
9 changes: 9 additions & 0 deletions releasenotes/notes/vf2layout-target-51cc8f77fdfcde67.yaml
Original file line number Diff line number Diff line change
@@ -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.
27 changes: 25 additions & 2 deletions test/python/transpiler/test_vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,12 @@
import retworkx

from qiskit import QuantumRegister, QuantumCircuit
from qiskit.transpiler import CouplingMap, Layout
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
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):
Expand Down Expand Up @@ -119,6 +119,29 @@ 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]])
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)
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."""
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):
"""Fit in 25x25 hexagonal lattice coupling map"""
Expand Down