From 91a91296c5b9be4342e3a7432ace0f4ea27383e0 Mon Sep 17 00:00:00 2001 From: Ed Younis Date: Thu, 5 Sep 2024 12:14:26 -0400 Subject: [PATCH 1/6] remote link specification in graph --- bqskit/qis/graph.py | 159 ++++++++++++++++++++++++++++++++++++++------ 1 file changed, 140 insertions(+), 19 deletions(-) diff --git a/bqskit/qis/graph.py b/bqskit/qis/graph.py index 7a7ae7c3..5439a3b4 100644 --- a/bqskit/qis/graph.py +++ b/bqskit/qis/graph.py @@ -14,6 +14,7 @@ from typing import Tuple from typing import TYPE_CHECKING from typing import Union +from typing import Mapping import numpy as np @@ -23,7 +24,7 @@ from bqskit.ir.location import CircuitLocation from bqskit.ir.location import CircuitLocationLike from bqskit.utils.typing import is_integer -from bqskit.utils.typing import is_iterable +from bqskit.utils.typing import is_iterable, is_mapping _logger = logging.getLogger(__name__) @@ -33,31 +34,143 @@ class CouplingGraph(Collection[Tuple[int, int]]): def __init__( self, - graph: Iterable[tuple[int, int]], + graph: CouplingGraphLike, num_qudits: int | None = None, + remote_edges: Iterable[tuple[int, int]] = [], + default_weight: int = 1, + default_remote_weight: int = 100, + edge_weights_overrides: Mapping[tuple[int, int], int] = {}, ) -> None: - if isinstance(graph, CouplingGraph): - self.num_qudits: int = graph.num_qudits - self._edges: set[tuple[int, int]] = graph._edges - self._adj: list[set[int]] = graph._adj - return + """ + Construct a new CouplingGraph. + + Args: + graph (CouplingGraphLike): The undirected graph edges. + + num_qudits (int | None): The number of qudits in the graph. If + None, the number of qudits is inferred from the maximum seen + in the edge list. (Default: None) + + remote_edges (Iterable[tuple[int, int]]): The edges that cross + QPU chip boundaries. Distributed QPUs will have remote links + connect them. Notes, remote edges must specified both in + `graph` and here. (Default: []) + + default_weight (int): The default weight of an edge in the + graph. (Default: 1) + + default_remote_weight (int): The default weight of a remote + edge in the graph. (Default: 100) + + edge_weights_overrides (Mapping[tuple[int, int], int]): A mapping + of edges to their weights. These override the defaults on + a case-by-case basis. (Default: {}) + + Raises: + ValueError: If `num_qudits` is too small for the edges in `graph`. + ValueError: If `num_qudits` is less than zero. + + ValueError: If any edge in `remote_edges` is not in `graph`. + + ValueError: If any edge in `edge_weights_overrides` is not in + `graph`. + """ if not CouplingGraph.is_valid_coupling_graph(graph): raise TypeError('Invalid coupling graph.') - self._edges = {g if g[0] <= g[1] else (g[1], g[0]) for g in graph} + if num_qudits is not None and not is_integer(num_qudits): + raise TypeError( + 'Expected integer for num_qudits,' + f' got {type(num_qudits)}', + ) - calced_num_qudits = 0 + if num_qudits is not None and num_qudits < 0: + raise ValueError( + 'Expected nonnegative num_qudits,' + f' got {num_qudits}.' + ) + + if not CouplingGraph.is_valid_coupling_graph(remote_edges): + raise TypeError('Invalid remote links.') + + if any(edge not in graph for edge in remote_edges): + invalids = [e for e in remote_edges if e not in graph] + raise ValueError( + f'Remote links {invalids} not in graph.' + ' All remote links must also be specified in the graph input.', + ) + + if not isinstance(default_weight, int): + raise TypeError( + 'Expected integer for default_weight,' + f' got {type(default_weight)}', + ) + + if not isinstance(default_remote_weight, int): + raise TypeError( + 'Expected integer for default_remote_weight,' + f' got {type(default_remote_weight)}', + ) + + if not is_mapping(edge_weights_overrides): + raise TypeError( + 'Expected mapping for edge_weights_overrides,' + f' got {type(edge_weights_overrides)}', + ) + + if any( + not isinstance(v, int) + for v in edge_weights_overrides.values() + ): + invalids = [ + v for v in edge_weights_overrides.values() + if not isinstance(v, int) + ] + raise TypeError( + 'Expected integer values for edge_weights_overrides,' + f' got non-integer values: {invalids}.', + ) + + if any(edge not in graph for edge in edge_weights_overrides): + invalids = [ + e for e in edge_weights_overrides + if e not in graph + ] + raise ValueError( + f'Edges {invalids} from edge_weights_overrides are not in ' + 'the graph. All edge_weights_overrides must also be ' + 'specified in the graph input.', + ) + + calc_num_qudits = 0 for q1, q2 in self._edges: - calced_num_qudits = max(calced_num_qudits, max(q1, q2)) - calced_num_qudits += 1 + calc_num_qudits = max(calc_num_qudits, max(q1, q2)) + calc_num_qudits += 1 - if num_qudits is None: - self.num_qudits = calced_num_qudits - elif calced_num_qudits > num_qudits: - raise ValueError('Edges between invalid qudits.') - else: - self.num_qudits = num_qudits + if num_qudits is not None and calc_num_qudits > num_qudits: + raise ValueError( + 'Edges between invalid qudits or num_qudits too small.' + ) + + if isinstance(graph, CouplingGraph): + self.num_qudits: int = graph.num_qudits + self._edges: set[tuple[int, int]] = graph._edges + self._remote_edges: set[tuple[int, int]] = graph._remote_edges + self._adj: list[set[int]] = graph._adj + self._mat: list[list[int]] = graph._mat + self.default_weight: int = graph.default_weight + self.default_remote_weight: int = graph.default_remote_weight + return + + self.num_qudits = calc_num_qudits if num_qudits is None else num_qudits + self._edges = {g if g[0] <= g[1] else (g[1], g[0]) for g in graph} + self._remote_edges = { + e if e[0] <= e[1] else (e[1], e[0]) + for e in remote_edges + } + self.default_weight = default_weight + self.default_remote_weight = default_remote_weight self._adj = [set() for _ in range(self.num_qudits)] for q1, q2 in self._edges: @@ -69,8 +182,16 @@ def __init__( for _ in range(self.num_qudits) ] for q1, q2 in self._edges: - self._mat[q1][q2] = 1 - self._mat[q2][q1] = 1 + self._mat[q1][q2] = default_weight + self._mat[q2][q1] = default_weight + + for q1, q2 in self._remote_links: + self._mat[q1][q2] = default_remote_weight + self._mat[q2][q1] = default_remote_weight + + for (q1, q2), weight in edge_weights_overrides.items(): + self._mat[q1][q2] = weight + self._mat[q2][q1] = weight def is_fully_connected(self) -> bool: """Return true if the graph is fully connected.""" From 659fae823630ab3c4d5caae00b00fd363a49595a Mon Sep 17 00:00:00 2001 From: Ed Younis Date: Thu, 5 Sep 2024 14:48:01 -0400 Subject: [PATCH 2/6] Some progress --- bqskit/qis/graph.py | 69 +++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/bqskit/qis/graph.py b/bqskit/qis/graph.py index 5439a3b4..f6b1684a 100644 --- a/bqskit/qis/graph.py +++ b/bqskit/qis/graph.py @@ -11,10 +11,10 @@ from typing import Iterable from typing import Iterator from typing import List +from typing import Mapping from typing import Tuple from typing import TYPE_CHECKING from typing import Union -from typing import Mapping import numpy as np @@ -88,7 +88,7 @@ def __init__( if num_qudits is not None and num_qudits < 0: raise ValueError( 'Expected nonnegative num_qudits,' - f' got {num_qudits}.' + f' got {num_qudits}.', ) if not CouplingGraph.is_valid_coupling_graph(remote_edges): @@ -150,7 +150,7 @@ def __init__( if num_qudits is not None and calc_num_qudits > num_qudits: raise ValueError( - 'Edges between invalid qudits or num_qudits too small.' + 'Edges between invalid qudits or num_qudits too small.', ) if isinstance(graph, CouplingGraph): @@ -193,6 +193,69 @@ def __init__( self._mat[q1][q2] = weight self._mat[q2][q1] = weight + def is_distributed(self) -> bool: + """Return true if the graph represents multiple connected QPUs.""" + return len(self._remote_edges) > 0 + + def qpu_count(self) -> int: + """Return the number of connected QPUs.""" + return len(self.get_qpu_to_qudit_map()) + + def get_individual_qpu_graphs(self) -> list[CouplingGraph]: + """Return a list of individual QPU graphs.""" + if not self.is_distributed(): + return [self] + + qpu_to_qudit = self.get_qpu_to_qudit_map() + return [self.get_subgraph(qpu) for qpu in qpu_to_qudit] + + def get_qpu_to_qudit_map(self) -> list[list[int]]: + """Return a mapping of QPU indices to qudit indices.""" + # TODO: Cache this? + seen = set() + qpus = [] + for qudit in range(self.num_qudits): + if qudit in seen: + continue + qpu = [] + frontier = {qudit} + while len(frontier) > 0: + node = frontier.pop() + qpu.append(node) + seen.add(node) + for neighbor in self._adj[node]: + if (node, neighbor) in self._remote_edges: + continue + if (neighbor, node) in self._remote_edges: + continue + if neighbor not in seen: + frontier.add(neighbor) + qpus.append(qpu) + # TODO: Assumes that the individual qpus are connected + # If seen is not everything, throw an error? + return qpus + + def get_qudit_to_qpu_map(self) -> dict[int, int]: + """Return a mapping of qudit indices to QPU indices.""" + qpu_to_qudit = self.get_qpu_to_qudit_map() + qudit_to_qpu = {} + for qpu, qudits in enumerate(qpu_to_qudit): + for qudit in qudits: + qudit_to_qpu[qudit] = qpu + return qudit_to_qpu + + def get_qpu_connectivity(self) -> list[list[int]]: + """Return the adjacency list of the QPUs.""" + qpu_to_qudit = self.get_qpu_to_qudit_map() + qudit_to_qpu = self.get_qudit_to_qpu_map() + qpu_adj = [set() for _ in range(len(qpu_to_qudit))] + for q1, q2 in self._remote_edges: + qpu1 = qudit_to_qpu[q1] + qpu2 = qudit_to_qpu[q2] + qpu_adj[qpu1].add(qpu2) + qpu_adj[qpu2].add(qpu1) + return qpu_adj + def is_fully_connected(self) -> bool: """Return true if the graph is fully connected.""" frontier: set[int] = {0} From 2df5770b4a5ddf9a0ed4251dcd708c260ebbdec6 Mon Sep 17 00:00:00 2001 From: Ed Younis Date: Sun, 8 Sep 2024 09:00:41 -0400 Subject: [PATCH 3/6] Check for error --- bqskit/qis/graph.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/bqskit/qis/graph.py b/bqskit/qis/graph.py index f6b1684a..834599f1 100644 --- a/bqskit/qis/graph.py +++ b/bqskit/qis/graph.py @@ -231,8 +231,13 @@ def get_qpu_to_qudit_map(self) -> list[list[int]]: if neighbor not in seen: frontier.add(neighbor) qpus.append(qpu) - # TODO: Assumes that the individual qpus are connected - # If seen is not everything, throw an error? + + if len(seen) != self.num_qudits: + raise RuntimeError( + 'Graph is not fully connected and pathological' + ' for distributed subroutines.' + ) + return qpus def get_qudit_to_qpu_map(self) -> dict[int, int]: From 6c18c1fa1ed84975692dd67b7f3504d45549d1ef Mon Sep 17 00:00:00 2001 From: Ed Younis Date: Sun, 8 Sep 2024 11:12:09 -0400 Subject: [PATCH 4/6] Edge weights to floats --- bqskit/passes/mapping/pam.py | 4 +-- bqskit/passes/mapping/sabre.py | 8 +++--- bqskit/qis/graph.py | 48 ++++++++++++++++------------------ 3 files changed, 29 insertions(+), 31 deletions(-) diff --git a/bqskit/passes/mapping/pam.py b/bqskit/passes/mapping/pam.py index 549ab2b6..92127a2c 100644 --- a/bqskit/passes/mapping/pam.py +++ b/bqskit/passes/mapping/pam.py @@ -277,7 +277,7 @@ def _get_best_perm( cg: CouplingGraph, F: set[CircuitPoint], pi: list[int], - D: list[list[int]], + D: list[list[float]], E: set[CircuitPoint], qudits: Sequence[int], ) -> tuple[tuple[int, ...], Circuit, tuple[int, ...]]: @@ -366,7 +366,7 @@ def _score_perm( circuit: Circuit, F: set[CircuitPoint], pi: list[int], - D: list[list[int]], + D: list[list[float]], perm: tuple[Sequence[int], Sequence[int]], E: set[CircuitPoint], ) -> float: diff --git a/bqskit/passes/mapping/sabre.py b/bqskit/passes/mapping/sabre.py index b257fda8..27d52c0d 100644 --- a/bqskit/passes/mapping/sabre.py +++ b/bqskit/passes/mapping/sabre.py @@ -363,7 +363,7 @@ def _get_best_swap( circuit: Circuit, F: set[CircuitPoint], E: set[CircuitPoint], - D: list[list[int]], + D: list[list[float]], cg: CouplingGraph, pi: list[int], decay: list[float], @@ -416,7 +416,7 @@ def _score_swap( circuit: Circuit, F: set[CircuitPoint], pi: list[int], - D: list[list[int]], + D: list[list[float]], swap: tuple[int, int], decay: list[float], E: set[CircuitPoint], @@ -475,7 +475,7 @@ def _get_distance( self, logical_qudits: Sequence[int], pi: list[int], - D: list[list[int]], + D: list[list[float]], ) -> float: """Calculate the expected number of swaps to connect logical qudits.""" min_term = np.inf @@ -493,7 +493,7 @@ def _uphill_swaps( logical_qudits: Sequence[int], cg: CouplingGraph, pi: list[int], - D: list[list[int]], + D: list[list[float]], ) -> Iterator[tuple[int, int]]: """Yield the swaps necessary to bring some of the qudits together.""" center_qudit = min( diff --git a/bqskit/qis/graph.py b/bqskit/qis/graph.py index 834599f1..2c0d31fa 100644 --- a/bqskit/qis/graph.py +++ b/bqskit/qis/graph.py @@ -6,11 +6,9 @@ import logging from random import shuffle from typing import Any -from typing import cast from typing import Collection from typing import Iterable from typing import Iterator -from typing import List from typing import Mapping from typing import Tuple from typing import TYPE_CHECKING @@ -24,7 +22,7 @@ from bqskit.ir.location import CircuitLocation from bqskit.ir.location import CircuitLocationLike from bqskit.utils.typing import is_integer -from bqskit.utils.typing import is_iterable, is_mapping +from bqskit.utils.typing import is_iterable, is_mapping, is_real_number _logger = logging.getLogger(__name__) @@ -37,9 +35,9 @@ def __init__( graph: CouplingGraphLike, num_qudits: int | None = None, remote_edges: Iterable[tuple[int, int]] = [], - default_weight: int = 1, - default_remote_weight: int = 100, - edge_weights_overrides: Mapping[tuple[int, int], int] = {}, + default_weight: float = 1.0, + default_remote_weight: float = 100.0, + edge_weights_overrides: Mapping[tuple[int, int], float] = {}, ) -> None: """ Construct a new CouplingGraph. @@ -56,13 +54,13 @@ def __init__( connect them. Notes, remote edges must specified both in `graph` and here. (Default: []) - default_weight (int): The default weight of an edge in the - graph. (Default: 1) + default_weight (float): The default weight of an edge in the + graph. (Default: 1.0) - default_remote_weight (int): The default weight of a remote - edge in the graph. (Default: 100) + default_remote_weight (float): The default weight of a remote + edge in the graph. (Default: 100.0) - edge_weights_overrides (Mapping[tuple[int, int], int]): A mapping + edge_weights_overrides (Mapping[tuple[int, int], float]): A mapping of edges to their weights. These override the defaults on a case-by-case basis. (Default: {}) @@ -101,13 +99,13 @@ def __init__( ' All remote links must also be specified in the graph input.', ) - if not isinstance(default_weight, int): + if not is_real_number(default_weight): raise TypeError( 'Expected integer for default_weight,' f' got {type(default_weight)}', ) - if not isinstance(default_remote_weight, int): + if not is_real_number(default_remote_weight): raise TypeError( 'Expected integer for default_remote_weight,' f' got {type(default_remote_weight)}', @@ -120,12 +118,12 @@ def __init__( ) if any( - not isinstance(v, int) + not is_real_number(v) for v in edge_weights_overrides.values() ): invalids = [ v for v in edge_weights_overrides.values() - if not isinstance(v, int) + if not is_real_number(v) ] raise TypeError( 'Expected integer values for edge_weights_overrides,' @@ -158,9 +156,9 @@ def __init__( self._edges: set[tuple[int, int]] = graph._edges self._remote_edges: set[tuple[int, int]] = graph._remote_edges self._adj: list[set[int]] = graph._adj - self._mat: list[list[int]] = graph._mat - self.default_weight: int = graph.default_weight - self.default_remote_weight: int = graph.default_remote_weight + self._mat: list[list[float]] = graph._mat + self.default_weight: float = graph.default_weight + self.default_remote_weight: float = graph.default_remote_weight return self.num_qudits = calc_num_qudits if num_qudits is None else num_qudits @@ -185,7 +183,7 @@ def __init__( self._mat[q1][q2] = default_weight self._mat[q2][q1] = default_weight - for q1, q2 in self._remote_links: + for q1, q2 in self._remote_edges: self._mat[q1][q2] = default_remote_weight self._mat[q2][q1] = default_remote_weight @@ -235,7 +233,7 @@ def get_qpu_to_qudit_map(self) -> list[list[int]]: if len(seen) != self.num_qudits: raise RuntimeError( 'Graph is not fully connected and pathological' - ' for distributed subroutines.' + ' for distributed subroutines.', ) return qpus @@ -249,11 +247,11 @@ def get_qudit_to_qpu_map(self) -> dict[int, int]: qudit_to_qpu[qudit] = qpu return qudit_to_qpu - def get_qpu_connectivity(self) -> list[list[int]]: + def get_qpu_connectivity(self) -> list[set[int]]: """Return the adjacency list of the QPUs.""" qpu_to_qudit = self.get_qpu_to_qudit_map() qudit_to_qpu = self.get_qudit_to_qpu_map() - qpu_adj = [set() for _ in range(len(qpu_to_qudit))] + qpu_adj: list[set[int]] = [set() for _ in range(len(qpu_to_qudit))] for q1, q2 in self._remote_edges: qpu1 = qudit_to_qpu[q1] qpu2 = qudit_to_qpu[q2] @@ -339,12 +337,12 @@ def __repr__(self) -> str: def get_qudit_degrees(self) -> list[int]: return [len(l) for l in self._adj] - def all_pairs_shortest_path(self) -> list[list[int]]: + def all_pairs_shortest_path(self) -> list[list[float]]: """ Calculate all pairs shortest path matrix using Floyd-Warshall. Returns: - D (list[list[int]]): D[i][j] is the length of the shortest + D (list[list[float]]): D[i][j] is the length of the shortest path from i to j. """ D = copy.deepcopy(self._mat) @@ -352,7 +350,7 @@ def all_pairs_shortest_path(self) -> list[list[int]]: for i in range(self.num_qudits): for j in range(self.num_qudits): D[i][j] = min(D[i][j], D[i][k] + D[k][j]) - return cast(List[List[int]], D) + return D def get_shortest_path_tree(self, source: int) -> list[tuple[int, ...]]: """Return shortest path from `source` to every node in `self`.""" From 502cd197d5df8dae1719540bfff160e221397628 Mon Sep 17 00:00:00 2001 From: Ed Younis Date: Mon, 9 Sep 2024 08:19:07 -0400 Subject: [PATCH 5/6] More tests and cleanup --- bqskit/qis/graph.py | 79 +++++++-------- tests/qis/test_graph.py | 208 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 244 insertions(+), 43 deletions(-) diff --git a/bqskit/qis/graph.py b/bqskit/qis/graph.py index 2c0d31fa..54e0664e 100644 --- a/bqskit/qis/graph.py +++ b/bqskit/qis/graph.py @@ -141,16 +141,6 @@ def __init__( 'specified in the graph input.', ) - calc_num_qudits = 0 - for q1, q2 in self._edges: - calc_num_qudits = max(calc_num_qudits, max(q1, q2)) - calc_num_qudits += 1 - - if num_qudits is not None and calc_num_qudits > num_qudits: - raise ValueError( - 'Edges between invalid qudits or num_qudits too small.', - ) - if isinstance(graph, CouplingGraph): self.num_qudits: int = graph.num_qudits self._edges: set[tuple[int, int]] = graph._edges @@ -161,6 +151,16 @@ def __init__( self.default_remote_weight: float = graph.default_remote_weight return + calc_num_qudits = 0 + for q1, q2 in graph: + calc_num_qudits = max(calc_num_qudits, max(q1, q2)) + calc_num_qudits += 1 + + if num_qudits is not None and calc_num_qudits > num_qudits: + raise ValueError( + 'Edges between invalid qudits or num_qudits too small.', + ) + self.num_qudits = calc_num_qudits if num_qudits is None else num_qudits self._edges = {g if g[0] <= g[1] else (g[1], g[0]) for g in graph} self._remote_edges = { @@ -191,6 +191,30 @@ def __init__( self._mat[q1][q2] = weight self._mat[q2][q1] = weight + def get_qpu_to_qudit_map(self) -> list[list[int]]: + """Return a mapping of QPU indices to qudit indices.""" + if not hasattr(self, '_qpu_to_qudit'): + seen = set() + self._qpu_to_qudit = [] + for qudit in range(self.num_qudits): + if qudit in seen: + continue + qpu = [] + frontier = {qudit} + while len(frontier) > 0: + node = frontier.pop() + qpu.append(node) + seen.add(node) + for neighbor in self._adj[node]: + if (node, neighbor) in self._remote_edges: + continue + if (neighbor, node) in self._remote_edges: + continue + if neighbor not in seen: + frontier.add(neighbor) + self._qpu_to_qudit.append(qpu) + return self._qpu_to_qudit + def is_distributed(self) -> bool: """Return true if the graph represents multiple connected QPUs.""" return len(self._remote_edges) > 0 @@ -207,45 +231,14 @@ def get_individual_qpu_graphs(self) -> list[CouplingGraph]: qpu_to_qudit = self.get_qpu_to_qudit_map() return [self.get_subgraph(qpu) for qpu in qpu_to_qudit] - def get_qpu_to_qudit_map(self) -> list[list[int]]: - """Return a mapping of QPU indices to qudit indices.""" - # TODO: Cache this? - seen = set() - qpus = [] - for qudit in range(self.num_qudits): - if qudit in seen: - continue - qpu = [] - frontier = {qudit} - while len(frontier) > 0: - node = frontier.pop() - qpu.append(node) - seen.add(node) - for neighbor in self._adj[node]: - if (node, neighbor) in self._remote_edges: - continue - if (neighbor, node) in self._remote_edges: - continue - if neighbor not in seen: - frontier.add(neighbor) - qpus.append(qpu) - - if len(seen) != self.num_qudits: - raise RuntimeError( - 'Graph is not fully connected and pathological' - ' for distributed subroutines.', - ) - - return qpus - - def get_qudit_to_qpu_map(self) -> dict[int, int]: + def get_qudit_to_qpu_map(self) -> list[int]: """Return a mapping of qudit indices to QPU indices.""" qpu_to_qudit = self.get_qpu_to_qudit_map() qudit_to_qpu = {} for qpu, qudits in enumerate(qpu_to_qudit): for qudit in qudits: qudit_to_qpu[qudit] = qpu - return qudit_to_qpu + return list(qudit_to_qpu.values()) def get_qpu_connectivity(self) -> list[set[int]]: """Return the adjacency list of the QPUs.""" diff --git a/tests/qis/test_graph.py b/tests/qis/test_graph.py index 5a205e8e..15823895 100644 --- a/tests/qis/test_graph.py +++ b/tests/qis/test_graph.py @@ -1,9 +1,217 @@ """This module tests the CouplingGraph class.""" from __future__ import annotations +from typing import Any + import pytest from bqskit.qis.graph import CouplingGraph +from bqskit.qis.graph import CouplingGraphLike + + +def test_coupling_graph_init_valid() -> None: + # Test with valid inputs + graph = {(0, 1), (1, 2), (2, 3)} + num_qudits = 4 + remote_edges = [(1, 2)] + default_weight = 1.0 + default_remote_weight = 10.0 + edge_weights_overrides = {(1, 2): 0.5} + + coupling_graph = CouplingGraph( + graph, + num_qudits, + remote_edges, + default_weight, + default_remote_weight, + edge_weights_overrides, + ) + + assert coupling_graph.num_qudits == num_qudits + assert coupling_graph._edges == graph + assert coupling_graph._remote_edges == set(remote_edges) + assert coupling_graph.default_weight == default_weight + assert coupling_graph.default_remote_weight == default_remote_weight + assert all( + coupling_graph._mat[q1][q2] == weight + for (q1, q2), weight in edge_weights_overrides.items() + ) + + +@pytest.mark.parametrize( + 'graph, num_qudits, remote_edges, default_weight, default_remote_weight,' + ' edge_weights_overrides, expected_exception', + [ + # Invalid graph + (None, 4, [], 1.0, 100.0, {}, TypeError), + # num_qudits is not an integer + ({(0, 1)}, '4', [], 1.0, 100.0, {}, TypeError), + # num_qudits is negative + ({(0, 1)}, -1, [], 1.0, 100.0, {}, ValueError), + # Invalid remote_edges + ({(0, 1)}, 4, None, 1.0, 100.0, {}, TypeError), + # Remote edge not in graph + ({(0, 1)}, 4, [(1, 2)], 1.0, 100.0, {}, ValueError), + # Invalid default_weight + ({(0, 1)}, 4, [], '1.0', 100.0, {}, TypeError), + # Invalid default_remote_weight + ({(0, 1)}, 4, [], 1.0, '100.0', {}, TypeError), + # Invalid edge_weights_overrides + ({(0, 1)}, 4, [], 1.0, 100.0, None, TypeError), + # Non-integer value in edge_weights_overrides + ({(0, 1)}, 4, [], 1.0, 100.0, {(0, 1): '0.5'}, TypeError), + # Edge in edge_weights_overrides not in graph + ({(0, 1)}, 4, [], 1.0, 100.0, {(1, 2): 0.5}, ValueError), + ], +) +def test_coupling_graph_init_invalid( + graph: CouplingGraphLike, + num_qudits: Any, + remote_edges: Any, + default_weight: Any, + default_remote_weight: Any, + edge_weights_overrides: Any, + expected_exception: Exception, +) -> None: + with pytest.raises(expected_exception): + CouplingGraph( + graph, + num_qudits, + remote_edges, + default_weight, + default_remote_weight, + edge_weights_overrides, + ) + + +def test_get_qpu_to_qudit_map_single_qpu() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)]) + expected_map = [[0, 1, 2, 3]] + assert graph.get_qpu_to_qudit_map() == expected_map + + +def test_get_qpu_to_qudit_map_multiple_qpus() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)], remote_edges=[(1, 2)]) + expected_map = [[0, 1], [2, 3]] + assert graph.get_qpu_to_qudit_map() == expected_map + + +def test_get_qpu_to_qudit_map_disconnected() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (3, 4)], remote_edges=[(1, 2)]) + expected_map = [[0, 1], [2], [3, 4]] + assert graph.get_qpu_to_qudit_map() == expected_map + + +def test_get_qpu_to_qudit_map_empty_graph() -> None: + graph = CouplingGraph([]) + expected_map = [[0]] + assert graph.get_qpu_to_qudit_map() == expected_map + + +def test_get_qpu_to_qudit_map_complex_topology() -> None: + graph = CouplingGraph( + [(0, 1), (1, 2), (0, 2), (2, 5), (3, 4), (4, 5), (3, 5)], + remote_edges=[(2, 5)], + ) + expected_map = [[0, 1, 2], [3, 4, 5]] + assert graph.get_qpu_to_qudit_map() == expected_map + + +def test_get_qudit_to_qpu_map_three_qpu() -> None: + graph = CouplingGraph( + [(0, 1), (1, 2), (2, 3), (3, 4), (4, 5), (5, 6), (6, 7)], + remote_edges=[(2, 3), (5, 6)], + ) + expected_map = [[0, 1, 2], [3, 4, 5], [6, 7]] + assert graph.get_qpu_to_qudit_map() == expected_map + + +def test_is_distributed() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)]) + assert not graph.is_distributed() + + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)], remote_edges=[(1, 2)]) + assert graph.is_distributed() + + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)], remote_edges=[(1, 2)]) + assert graph.is_distributed() + + graph = CouplingGraph( + [(0, 1), (1, 2), (2, 3)], + remote_edges=[(1, 2), (2, 3)], + ) + assert graph.is_distributed() + + +def test_qpu_count() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)]) + assert graph.qpu_count() == 1 + + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)], remote_edges=[(1, 2)]) + assert graph.qpu_count() == 2 + + graph = CouplingGraph( + [(0, 1), (1, 2), (2, 3)], + remote_edges=[(1, 2), (2, 3)], + ) + assert graph.qpu_count() == 3 + + graph = CouplingGraph([]) + assert graph.qpu_count() == 1 + + +def test_get_individual_qpu_graphs() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)]) + qpus = graph.get_individual_qpu_graphs() + assert len(qpus) == 1 + assert qpus[0] == graph + + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)], remote_edges=[(1, 2)]) + qpus = graph.get_individual_qpu_graphs() + assert len(qpus) == 2 + assert qpus[0] == CouplingGraph([(0, 1)]) + assert qpus[1] == CouplingGraph([(0, 1)]) + + graph = CouplingGraph( + [(0, 1), (1, 2), (2, 3)], + remote_edges=[(1, 2), (2, 3)], + ) + qpus = graph.get_individual_qpu_graphs() + assert len(qpus) == 3 + assert qpus[0] == CouplingGraph([(0, 1)]) + assert qpus[1] == CouplingGraph([]) + assert qpus[2] == CouplingGraph([]) + + +def test_get_qudit_to_qpu_map() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)]) + assert graph.get_qudit_to_qpu_map() == [0, 0, 0, 0] + + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)], remote_edges=[(1, 2)]) + assert graph.get_qudit_to_qpu_map() == [0, 0, 1, 1] + + graph = CouplingGraph( + [(0, 1), (1, 2), (2, 3)], + remote_edges=[(1, 2), (2, 3)], + ) + assert graph.get_qudit_to_qpu_map() == [0, 0, 1, 2] + + graph = CouplingGraph([]) + assert graph.get_qudit_to_qpu_map() == [0] + + +def test_get_qpu_connectivity() -> None: + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)]) + assert graph.get_qpu_connectivity() == [set()] + + graph = CouplingGraph([(0, 1), (1, 2), (2, 3)], remote_edges=[(1, 2)]) + assert graph.get_qpu_connectivity() == [{1}, {0}] + + graph = CouplingGraph( + [(0, 1), (1, 2), (2, 3)], + remote_edges=[(1, 2), (2, 3)], + ) + assert graph.get_qpu_connectivity() == [{1}, {0, 2}, {1}] class TestGraphGetSubgraphsOfSize: From 5f219aace5980a4841c61cb1e9fcefec9d5f8152 Mon Sep 17 00:00:00 2001 From: Ed Younis Date: Mon, 9 Sep 2024 08:19:27 -0400 Subject: [PATCH 6/6] Distributed Predicate --- .../passes/control/predicates/distributed.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 bqskit/passes/control/predicates/distributed.py diff --git a/bqskit/passes/control/predicates/distributed.py b/bqskit/passes/control/predicates/distributed.py new file mode 100644 index 00000000..a1ceb227 --- /dev/null +++ b/bqskit/passes/control/predicates/distributed.py @@ -0,0 +1,26 @@ +"""This module implements the DistributedPredicate class.""" +from __future__ import annotations + +import logging +from typing import TYPE_CHECKING + +from bqskit.passes.control.predicate import PassPredicate + +if TYPE_CHECKING: + from bqskit.compiler.passdata import PassData + from bqskit.ir.circuit import Circuit + +_logger = logging.getLogger(__name__) + + +class DistributedPredicate(PassPredicate): + """ + The DistributedPredicate class. + + The DistributedPredicate returns true if the targeted machine is distributed + across multiple chips. + """ + + def get_truth_value(self, circuit: Circuit, data: PassData) -> bool: + """Call this predicate, see :class:`PassPredicate` for more info.""" + return data.model.coupling_graph.is_distributed()