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

Extend VF2Layout with 2-qubit-noise-scoring #7722

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
114 changes: 92 additions & 22 deletions qiskit/transpiler/passes/layout/vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,14 +14,17 @@
from enum import Enum
import logging
import random
from itertools import combinations
import time
import numpy as np

from retworkx import PyGraph, PyDiGraph, vf2_mapping

from qiskit.transpiler.layout import Layout
from qiskit.transpiler.basepasses import AnalysisPass
from qiskit.providers.exceptions import BackendPropertyError

DEFAULT_CX_ERROR = 5 * 10 ** (-2)

logger = logging.getLogger(__name__)

Expand Down Expand Up @@ -61,6 +64,7 @@ def __init__(
time_limit=None,
properties=None,
max_trials=None,
scoring_function=None,
):
"""Initialize a ``VF2Layout`` pass instance

Expand Down Expand Up @@ -89,6 +93,7 @@ def __init__(
self.time_limit = time_limit
self.properties = properties
self.max_trials = max_trials
self.scoring_function = scoring_function

def run(self, dag):
"""run the layout method"""
Expand Down Expand Up @@ -143,6 +148,7 @@ def run(self, dag):
)
chosen_layout = None
chosen_layout_score = None
scoring_function = self.scoring_function or readout_error_score
start_time = time.time()
trials = 0
for mapping in mappings:
Expand All @@ -156,7 +162,7 @@ def run(self, dag):
if len(cm_graph) == len(im_graph):
chosen_layout = layout
break
layout_score = self._score_layout(layout)
layout_score = scoring_function(dag, layout, self.properties, self.coupling_map)
logger.debug("Trial %s has score %s", trials, layout_score)
if chosen_layout is None:
chosen_layout = layout
Expand Down Expand Up @@ -191,26 +197,90 @@ def run(self, dag):

self.property_set["VF2Layout_stop_reason"] = stop_reason

def _score_layout(self, layout):
"""Score heurstic to determine the quality of the layout by looking at the readout fidelity
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
for bit in bits:
score += self.coupling_map.graph.out_degree(
bit
) + self.coupling_map.graph.in_degree(bit)
return score

def two_q_score(dag, layout, properties, coupling_map):
"""Evaluate the score on a layout and dag.
Calculate the score as the product of all two qubit gate fidelities.
Assign an artificial fidelity to virtual gates that require swap
operations in the implementation.

Args:
dag (DAGCircuit): DAG to evaluate
layout (Layout): Layout to evaluate
properties (BackendProperties): The properties of the backend
coupling_map (CouplingMap): The backend coupling map

Return:
float: the layout fidelity estimation
"""
layout_fidelity = 1.0
for node in dag.op_nodes():
physical_qubits = [layout[qubit] for qubit in node.qargs]
if len(physical_qubits) == 2:
layout_fidelity *= _calculate_2q_fidelity(physical_qubits, properties, coupling_map)
return layout_fidelity


def _calculate_2q_fidelity(qubits, backend_properties, coupling_map):
"""Calculate the 2q fidelity
Depending on the distance of the qubits, there are different options
for introducing the additional swaps. Therefore the average over all paths is used.
See also Arxiv 2103.15695 on page 5 as a reference.

As an example the fidelity for a cx-gate between qb1 and qb4 in a chain is given as:
f_14 = 1/3 * f_12 f_23 f_34 (f_12^5 f_23^5 + f_23^5 f_34^5 f_12^5 f_34^5)
"""

def cx_fid(qubits):
if backend_properties:
return backend_properties.gate_error("cx", qubits)
else:
return DEFAULT_CX_ERROR

cplpath = coupling_map.shortest_undirected_path(*qubits)
cplpath_length = len(cplpath) - 1
cplpath_edges = [[cplpath[k], cplpath[k + 1]] for k in range(cplpath_length)]

path_fid = 1.0
path_fid *= np.sum(
[
np.prod([cx_fid(qubits) ** 5 for qubits in qubits_subset])
for qubits_subset in combinations(cplpath_edges, cplpath_length - 1)
]
)
path_fid *= np.prod([cx_fid(qubits) for qubits in cplpath_edges]) / cplpath_length

return path_fid


def readout_error_score(_, layout, properties, coupling_map):
"""Score heurstic to determine the quality of the layout by looking at the readout fidelity
on the chosen qubits. If BackendProperties are not available it uses the coupling map degree
to weight against higher connectivity qubits.

Args:
dag:
layout:
properties:
coupling_map:

Returns:
int: Score
"""
bits = layout.get_physical_bits()
score = 0
if properties is None:
# Sum qubit degree for each qubit in chosen layout as really rough estimate of error
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)
score += coupling_map.graph.out_degree(bit) + coupling_map.graph.in_degree(bit)
return score
for bit in bits:
try:
score += 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 += (coupling_map.graph.out_degree(bit) + coupling_map.graph.in_degree(bit)) / len(
coupling_map.graph
)
return score
77 changes: 49 additions & 28 deletions test/python/transpiler/test_vf2_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,12 @@

from qiskit import QuantumRegister, QuantumCircuit
from qiskit.transpiler import CouplingMap, Layout
from qiskit.transpiler.passes.layout.vf2_layout import VF2Layout, VF2LayoutStopReason
from qiskit.transpiler.passes.layout.vf2_layout import (
VF2Layout,
VF2LayoutStopReason,
readout_error_score,
two_q_score,
)
from qiskit.converters import circuit_to_dag
from qiskit.test import QiskitTestCase
from qiskit.test.mock import FakeTenerife, FakeRueschlikon, FakeManhattan, FakeYorktown
Expand Down Expand Up @@ -365,52 +370,68 @@ class TestScoreHeuristic(QiskitTestCase):

def test_no_properties(self):
"""Test scores with no properties."""
vf2_pass = VF2Layout(
CouplingMap(
[
(0, 1),
(0, 2),
(0, 3),
(1, 0),
(1, 2),
(1, 3),
(2, 0),
(2, 1),
(2, 2),
(2, 3),
(3, 0),
(3, 1),
(3, 2),
(4, 0),
(0, 4),
(5, 1),
(1, 5),
]
)
cmap = CouplingMap(
[
(0, 1),
(0, 2),
(0, 3),
(1, 0),
(1, 2),
(1, 3),
(2, 0),
(2, 1),
(2, 2),
(2, 3),
(3, 0),
(3, 1),
(3, 2),
(4, 0),
(0, 4),
(5, 1),
(1, 5),
]
)
qr = QuantumRegister(2)
layout = Layout({qr[0]: 0, qr[1]: 1})
score = vf2_pass._score_layout(layout)
score = readout_error_score(None, layout, None, cmap)
self.assertEqual(score, 16)
better_layout = Layout({qr[0]: 4, qr[1]: 5})
better_score = vf2_pass._score_layout(better_layout)
better_score = readout_error_score(None, better_layout, None, cmap)
self.assertEqual(4, better_score)

def test_with_properties(self):
"""Test scores with properties."""
backend = FakeYorktown()
cmap = CouplingMap(backend.configuration().coupling_map)
properties = backend.properties()
vf2_pass = VF2Layout(cmap, properties=properties)
qr = QuantumRegister(2)
layout = Layout({qr[0]: 4, qr[1]: 2})
bad_score = vf2_pass._score_layout(layout)
bad_score = readout_error_score(None, layout, properties, cmap)
self.assertAlmostEqual(0.4075, bad_score)
better_layout = Layout({qr[0]: 1, qr[1]: 3})
better_score = vf2_pass._score_layout(better_layout)
better_score = readout_error_score(None, better_layout, properties, cmap)
self.assertAlmostEqual(0.0588, better_score)


class TestScore2qubit(QiskitTestCase):
"""VF2Layout test with scoring_function=two_q_score"""

def test_single_cx(self):
"""Test scoring_function=two_q_score with a single CX in a 2qubit circuit."""
qr = QuantumRegister(2, name="qr")
expected = Layout({qr[0]: 4, qr[1]: 3})

backend = FakeYorktown()
cmap = CouplingMap(backend.configuration().coupling_map)
properties = backend.properties()
vf2_pass = VF2Layout(cmap, properties=properties, scoring_function=two_q_score, seed=42)
circuit = QuantumCircuit(qr)
circuit.cx(qr[0], qr[1])
dag = circuit_to_dag(circuit)
vf2_pass.run(dag)
self.assertEqual(vf2_pass.property_set["layout"], expected)


class TestMultipleTrials(QiskitTestCase):
"""Test the passes behavior with >1 trial."""

Expand Down