Skip to content
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
11 changes: 11 additions & 0 deletions qiskit_experiments/whole_backend/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
"""
Functions to facilitate experiments on entire backends
"""

from qiskit_experiments.whole_backend.whole_backend import build_whole_backend_experiment
from qiskit_experiments.whole_backend.partition import (
partition_qubits,
partition_edges,
verify_qubit_groups,
verify_edge_groups,
)
222 changes: 222 additions & 0 deletions qiskit_experiments/whole_backend/partition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
"""
Functions to partition qubits and edges into groups, such that qubits/edges
in a group are distance from each other
"""

import retworkx as rx


def distance_graph(graph, distance):
"""
The vertices of the distance graph are the same as
in the original graph. Two vertices are connected by an edge if the distance between them
in the original graph is smaller than `distance`.
"""
distance_matrix = rx.distance_matrix(graph)
dist_graph = rx.PyGraph()
indexes = graph.node_indices()

for graph_vertex1 in indexes:
dist_graph.add_node(graph[graph_vertex1])
for graph_vertex2 in range(graph_vertex1):
if distance_matrix[graph_vertex1, graph_vertex2] < distance:
dist_graph.add_edge(graph_vertex1, graph_vertex2, None)
Comment thread
yaelbh marked this conversation as resolved.

return dist_graph


def partition_qubits(backend, distance):
"""
Partitions the qubits into groups, such that in each group the
minimum distance (number of edges in the shortest path) between qubits is at least
`distance`.

Returns a list of list of integers, i.e., a list of groups of qubits.
"""

coupling = backend.configuration().coupling_map

# Construct the coupling graph using retworkx
graph = rx.PyGraph()
graph.add_nodes_from(list(range(backend.configuration().num_qubits)))
for coupling_edge in coupling:
graph.add_edge(coupling_edge[0], coupling_edge[1], None)

# A very naive algorithm, which it was chosen (at least for now) because it's easy
# to implement. I didn't check if it has any guarantees about performance, number of qubit
# groups (if we want to minimize it - this translates to minimizing the number of circuits),
# or equitability (if we want the groups - namely the circuits - to be more-or-less of
# equal size). The literature is filled with algorithms for vertex colorings,
# including for the case of the distance>2 constraint. In particular,
# we don't exploit information that we have about the structure of the coupling map, like
# the fact that the degree of the coupling graph is upper-bounded.

# Construct the distance graph: an edge between two qubits
# if the distance between them is smaller than `distance`
dist_graph = distance_graph(graph, distance)

# Color the distance graph: qubits that are adjacent in the distance graph
# will be assigned different colors
colors = rx.graph_greedy_color(dist_graph)

# Partition the qubits according to their colors
return [
[[graph[qubit]] for qubit in graph.node_indices() if colors[qubit] == c]
for c in set(colors.values())
]


def partition_edges(backend, distance):
"""
Partitions the edges in the coupling map into groups, such that in each group the
minimum distance (one plus number of edges in the shortest path) between edges is at least
`distance`.

Returns a list of list of pairs of integers, i.e., a list of groups of edges.
"""

coupling = backend.configuration().coupling_map

# Algorithm:
# - We name by "coupling graph" the graph whose vertices are the qubits and edges
# are the edges that are in the coupling map. In the code here we don't construct this
# graph.
# - We construct the line graph of the coupling graph. In the line graph,
# every vertex represents an edge of the coupling graph. Vertices in the line graph
# are connected by an edge if the respective edges in the coupling graph share at least
# one vertex.
# - We construct the distance graph. The vertices of the distance graph are the same as
# in the line graph. Two vertices are connected by an edge if the distance between them
# in the line graph is smaller than `distance`.
# - We color the vertices of the distance graph (coloring a graph means that adjacent
# vertices are assigned different colors). This induces a coloring to the edges of the
# coupling graph that satisfies the distance constraint.
#
# This is a very naive algorithm, and it was chosen (at least for now) because it's easy
# to implement. I didn't check if it has any guarantees about performance, number of edge
# groups (if we want to minimize it - this translates to minimizing the number of circuits),
# or equitability (if we want the groups - namely the circuits - to be more-or-less of
# equal size). The literature is filled with algorithms for vertex and edge colorings,
# including for the case of the distance>2 constraint. In particular,
# we don't exploit information that we have about the structure of the coupling map, like
# the fact that the degrees of the coupling and line graphs are upper-bounded.

# Construct the line graph
line_graph = rx.PyGraph()
# By "coupling edge" we mean "edge of the coupling graph"
for coupling_edge in coupling:
# By "line vertex" we mean "vertex of the line graph"
# Each vertex in the line graph is originated from an edge in the coupling map,
# we keep the original coupling edge in the label of the line vertex
line_vertex = line_graph.add_node(coupling_edge)
for existing_line_vertex in range(line_vertex):
existing_coupling_edge = line_graph[existing_line_vertex]
if set(coupling_edge).intersection(set(existing_coupling_edge)):
line_graph.add_edge(line_vertex, existing_line_vertex, None)

# Construct the distance graph
dist_graph = distance_graph(line_graph, distance)

# Color the distance graph
colors = rx.graph_greedy_color(dist_graph)

return [
[
line_graph[line_vertex]
for line_vertex in line_graph.node_indices()
if colors[line_vertex] == c
]
for c in set(colors.values())
]


def verify_qubit_groups(backend, qubit_groups, distance):
"""
We verify:
- Every qubit is contained in exactly one group.
- The distance between qubits belonging to the same group is at least `distance`.
"""

nqubits = backend.configuration().n_qubits
coupling = backend.configuration().coupling_map

# Build the coupling graph:
# - Vertices are the qubits.
# - Edges are the edges of the coupling map.
coupling_graph = rx.PyGraph()
coupling_graph.add_nodes_from(list(range(nqubits)))
for edge in coupling:
coupling_graph.add_edge(edge[0], edge[1], None)

# Compute the distances between vertices
distance_matrix = rx.distance_matrix(coupling_graph)

found_qubits = []
for group in qubit_groups:
for i, qubit1 in enumerate(group):
# Verify that no qubit repeats twice (either in the same group
# or in different groups)
assert 0 <= qubit1[0] < nqubits
assert qubit1[0] not in found_qubits
found_qubits.append(qubit1[0])

# Verify that the minimum distance between qubits
# that belong to the same group is at least `distance`
for j in range(i):
qubit2 = group[j]
assert distance_matrix[qubit1[0], qubit2[0]] >= distance

# Verify that every qubit belongs to some group.
assert len(found_qubits) == nqubits


def verify_edge_groups(backend, edge_groups, distance):
"""
We verify:
- Every edge in the coupling map is contained in exactly one group.
- Every edge in any of the groups is in the coupling map.
- The distance (one plus number of edges in the coupling map) between edges
belonging to the same group is at least `distance`.

In order not to repeat, in the test, bugs that the test actually aims to detect,
we intentionally perform computations differently from `partition_edges`.
Instead of working with the line graph, we work directly on the coupling graph.
To check the distance between edges in the coupling graph, we check the distance between
their end vertices.
"""

nqubits = backend.configuration().n_qubits
coupling = backend.configuration().coupling_map

# Build the coupling graph:
# - Vertices are the qubits.
# - Edges are the edges of the coupling map.
coupling_graph = rx.PyGraph()
coupling_graph.add_nodes_from(list(range(nqubits)))
for edge in coupling:
coupling_graph.add_edge(edge[0], edge[1], None)

# Compute the distances between vertices
distance_matrix = rx.distance_matrix(coupling_graph)

found_edges = []
for group in edge_groups:
for i, edge1 in enumerate(group):
# Verify that no edge repeats twice (either in the same group
# or in different groups)
assert edge1 not in found_edges
found_edges.append(edge1)

# Verify that all edges in the groups belong to the coupling map
assert edge1 in coupling

# Verify that the minimum distance between end nodes of two edges,
# that belong to the same group, is at least `distance`-1
for j in range(i):
edge2 = group[j]
for vertex1 in edge1:
for vertex2 in edge2:
assert distance_matrix[vertex1, vertex2] >= distance - 1

# Verify that every edge in the coupling map belongs to some group.
assert len(found_edges) == len(coupling)
79 changes: 79 additions & 0 deletions qiskit_experiments/whole_backend/whole_backend.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
"""
Functions to build composite experiments for entire backends
"""

from copy import deepcopy
from qiskit.circuit import QuantumCircuit, Qubit, Clbit
from qiskit_experiments.framework import (
BatchExperiment,
ParallelExperiment,
BaseExperiment,
)


class BasicExperiment(BaseExperiment):
"""
Basic atmoic experiment that mimics the template experiment,
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Suggested change
Basic atmoic experiment that mimics the template experiment,
Basic atomic experiment that mimics the template experiment,

but uses a pre-prepared transpiled circuit
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

__init.py__ needs to be formatted for generating API docs, and this docstring needs more information on how this works and how to use it. Including the example in your PR text would be helpful.

"""

def __init__(self, qubits, template_circs, analysis):
super().__init__(qubits)
self._template_circs = template_circs
self.analysis = analysis

def circuits(self):
pass

def _transpiled_circuits(self):
res_circs = []
for circ in self._template_circs:
qubit_indices = {bit: idx for idx, bit in enumerate(circ.qubits)}
clbit_indices = {bit: idx for idx, bit in enumerate(circ.clbits)}
new_circ = QuantumCircuit(1 + max(self.physical_qubits), circ.num_clbits)

for inst, qargs, cargs in circ.data:
new_qargs = []
new_cargs = []

for qubit in qargs:
original_qubit = qubit_indices[qubit]
if original_qubit < len(self.physical_qubits):
new_qargs.append(
Qubit(new_circ.qregs[0], self.physical_qubits[original_qubit])
)

for clbit in cargs:
original_clbit = clbit_indices[clbit]
new_cargs.append(Clbit(new_circ.cregs[0], original_clbit))

if len(qargs) == len(new_qargs):
new_circ.append(inst, new_qargs, new_cargs)

new_circ.metadata = circ.metadata
res_circs.append(new_circ)

return res_circs


def build_whole_backend_experiment(template_experiment, groups):
"""
Return an experiment that covers all the groups of qubits/edges
"""
circs = template_experiment._transpiled_circuits()

parexps = []
for group in groups:
exps = []
for qubits in group:
analysis = deepcopy(template_experiment.analysis)
exps.append(
BasicExperiment(
qubits,
circs,
analysis,
)
)
parexps.append(ParallelExperiment(exps))

return BatchExperiment(parexps, backend=template_experiment.backend, flatten_results=True)
41 changes: 41 additions & 0 deletions test/test_partition.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
"""
Test partition_qubits and partition_edges
"""
from test.base import QiskitExperimentsTestCase

from qiskit.providers.fake_provider import FakeGuadalupe

from qiskit_experiments.whole_backend import (
partition_qubits,
partition_edges,
verify_qubit_groups,
verify_edge_groups,
)


class TestPartition(QiskitExperimentsTestCase):
"""
Test partition_qubits and partition_edges
"""

def test_partition_qubits(self):
"""
Verify correctness of `partition_qubits`
(see details in the documentation of verify_qubit_groups)
"""

distance = 2
backend = FakeGuadalupe()
qubit_groups = partition_qubits(backend, distance)
verify_qubit_groups(backend, qubit_groups, distance)

def test_partition_edges(self):
"""
Verify correctness of `partition_edges`
(see details in the documentation of verify_edge_groups)
"""

distance = 3
backend = FakeGuadalupe()
edge_groups = partition_edges(backend, distance)
verify_edge_groups(backend, edge_groups, distance)