-
Notifications
You must be signed in to change notification settings - Fork 134
[WIP] Functions to facilitate experiments on entire backends #859
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
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||
| ) |
| 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) | ||
|
|
||
| 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) | ||
| 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, | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||
| but uses a pre-prepared transpiled circuit | ||||||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||||||
| """ | ||||||
|
|
||||||
| 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) | ||||||
| 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) |
Uh oh!
There was an error while loading. Please reload this page.