From 4d134bb5f49ec2144fbd501ce0a25c69d4f037de Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 Mar 2023 14:13:11 -0500 Subject: [PATCH 01/19] Add support to CouplingMap for disjoint qubits Previously the CouplingMap class only supported graphs which were fully connected. This prevented us from modeling potential hardware which didn't have a path between all qubits. This isn't an inherent limitation of the underlying graph data structure but was a limitation put on the CouplingMap class because several pieces of the transpiler assume a path always exists between 2 qubits (mainly in layout and routing). This commit removes this limitation and also adds a method to get a subgraph CouplingMap for all the components of the CouplingMap. This enables us to model these devices with a CouplingMap, which is the first step towards supporting these devices in the transpiler. One limitation with this PR is most fo the layout and routing algorithms do not support disjoint connectivity. The primary exception being TrivialLayout (although the output might be invalid) VF2Layout and VF2PostLayout which inherently support this already. This commit lays the groundwork to fix this limitation in a follow-up PR but for the time being it just raises an error in those passes if a disconnected CouplingMap is being used. The intent here is to follow up to this commit soon for adding support for SabreLayout, SabreSwap, DenseLayout, and StochasticSwap to leverage the method get_component_subgraphs added here to make them usable on such coupling maps. --- qiskit/transpiler/coupling.py | 61 +++++++++++++------ qiskit/transpiler/passes/layout/csp_layout.py | 6 ++ .../transpiler/passes/layout/dense_layout.py | 5 ++ .../passes/layout/noise_adaptive_layout.py | 5 ++ .../transpiler/passes/layout/sabre_layout.py | 6 +- .../passes/layout/trivial_layout.py | 5 ++ .../add-cmap-componets-7ed56cdf294150f1.yaml | 13 ++++ test/python/transpiler/test_coupling.py | 41 ++++++++++--- 8 files changed, 116 insertions(+), 26 deletions(-) create mode 100644 releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 7d8dc6e5467b..0c7e00877e27 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -19,9 +19,9 @@ onto a device with this coupling. """ +from typing import List import warnings -import numpy as np import rustworkx as rx from rustworkx.visualization import graphviz_draw @@ -175,8 +175,6 @@ def compute_distance_matrix(self): those or want to pre-generate it. """ if self._dist_matrix is None: - if not self.is_connected(): - raise CouplingError("coupling graph not connected") self._dist_matrix = rx.digraph_distance_matrix(self.graph, as_undirected=True) def distance(self, physical_qubit1, physical_qubit2): @@ -197,7 +195,10 @@ def distance(self, physical_qubit1, physical_qubit2): if physical_qubit2 >= self.size(): raise CouplingError("%s not in coupling graph" % physical_qubit2) self.compute_distance_matrix() - return int(self._dist_matrix[physical_qubit1, physical_qubit2]) + res = int(self._dist_matrix[physical_qubit1, physical_qubit2]) + if res == 0: + raise CouplingError(f"No path from {physical_qubit1} to {physical_qubit2}") + return res def shortest_undirected_path(self, physical_qubit1, physical_qubit2): """Returns the shortest undirected path between physical_qubit1 and physical_qubit2. @@ -268,9 +269,6 @@ def reduce(self, mapping): Raises: CouplingError: Reduced coupling map must be connected. """ - from scipy.sparse import coo_matrix, csgraph - - reduced_qubits = len(mapping) inv_map = [None] * (max(mapping) + 1) for idx, val in enumerate(mapping): inv_map[val] = idx @@ -281,16 +279,6 @@ def reduce(self, mapping): if edge[0] in mapping and edge[1] in mapping: reduced_cmap.append([inv_map[edge[0]], inv_map[edge[1]]]) - # Verify coupling_map is connected - rows = np.array([edge[0] for edge in reduced_cmap], dtype=int) - cols = np.array([edge[1] for edge in reduced_cmap], dtype=int) - data = np.ones_like(rows) - - mat = coo_matrix((data, (rows, cols)), shape=(reduced_qubits, reduced_qubits)).tocsr() - - if csgraph.connected_components(mat)[0] != 1: - raise CouplingError("coupling_map must be connected.") - return CouplingMap(reduced_cmap) @classmethod @@ -403,6 +391,45 @@ def largest_connected_component(self): """Return a set of qubits in the largest connected component.""" return max(rx.weakly_connected_components(self.graph), key=len) + def get_component_subgraphs(self, respect_direction: bool = False) -> List["CouplingMap"]: + """Separate a CouplingMap into subgraph Coupling Maps for each connected component. + + This method will return a list of :class:`~.CouplingMap` objects for each connected + component in this :class:`~.CouplingMap`. The data payload of each node in the + :attr:`~.CouplingMap.graph` attribute will contain the qubit number in the original + graph. This will enables mapping the qubit index in a component subgraph to + the original qubit in the combined :class:`~.CouplingMap`. For example:: + + from qiskit.transpiler import CouplingMap + + cmap = CouplingMap([[0, 1], [1, 2], [2, 0], [3, 4], [4, 5], [5, 3]]) + component_cmaps = cmap.get_component_subgraphs() + print(component_cmaps[1].graph[0]) + + will print ``3`` as index ``0`` in the second component is qubit 3 in the original cmap. + + Args: + respect_direction: If set to ``True`` the connected components will be strongly connected + + Returns: + list: A list of :class:`~.CouplingMap` objects for each connected + components. + """ + + # Set payload to index + for node in self.graph.node_indices(): + self.graph[node] = node + if not respect_direction: + components = rx.weakly_connected_components(self.graph) + else: + components = rx.strongly_connected_components(self.graph) + output_list = [] + for component in components: + new_cmap = CouplingMap() + new_cmap.graph = self.graph.subgraph(list(sorted(component))) + output_list.append(new_cmap) + return output_list + def __str__(self): """Return a string representation of the coupling graph.""" string = "" diff --git a/qiskit/transpiler/passes/layout/csp_layout.py b/qiskit/transpiler/passes/layout/csp_layout.py index 704c91c4de46..97f1d34b01df 100644 --- a/qiskit/transpiler/passes/layout/csp_layout.py +++ b/qiskit/transpiler/passes/layout/csp_layout.py @@ -19,6 +19,7 @@ from qiskit.transpiler.layout import Layout from qiskit.transpiler.basepasses import AnalysisPass +from qiskit.transpiler.exceptions import TranspilerError from qiskit.utils import optionals as _optionals @@ -60,6 +61,11 @@ def __init__( def run(self, dag): """run the layout method""" + if not self.coupling_map.is_connected(): + raise TranspilerError( + "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " + "map." + ) qubits = dag.qubits cxs = set() diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index 030374de0ba1..5ecdb87deceb 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -79,6 +79,11 @@ def run(self, dag): raise TranspilerError( "A coupling_map or target with constrained qargs is necessary to run the pass." ) + if not self.coupling_map.is_connected(): + raise TranspilerError( + "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " + "map." + ) num_dag_qubits = len(dag.qubits) if num_dag_qubits > self.coupling_map.size(): raise TranspilerError("Number of qubits greater than device.") diff --git a/qiskit/transpiler/passes/layout/noise_adaptive_layout.py b/qiskit/transpiler/passes/layout/noise_adaptive_layout.py index f8dcc566d1be..47bd2c7273a7 100644 --- a/qiskit/transpiler/passes/layout/noise_adaptive_layout.py +++ b/qiskit/transpiler/passes/layout/noise_adaptive_layout.py @@ -211,6 +211,11 @@ def _select_best_remaining_qubit(self, prog_qubit): def run(self, dag): """Run the NoiseAdaptiveLayout pass on `dag`.""" + if not self.coupling_map.is_connected(): + raise TranspilerError( + "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " + "map." + ) self.swap_graph = rx.PyDiGraph() self.cx_reliability = {} self.readout_reliability = {} diff --git a/qiskit/transpiler/passes/layout/sabre_layout.py b/qiskit/transpiler/passes/layout/sabre_layout.py index 3948635a27fb..fbe3abb8cdef 100644 --- a/qiskit/transpiler/passes/layout/sabre_layout.py +++ b/qiskit/transpiler/passes/layout/sabre_layout.py @@ -166,7 +166,11 @@ def run(self, dag): """ if len(dag.qubits) > self.coupling_map.size(): raise TranspilerError("More virtual qubits exist than physical.") - + if not self.coupling_map.is_connected(): + raise TranspilerError( + "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " + "map." + ) # Choose a random initial_layout. if self.routing_pass is not None: if self.seed is None: diff --git a/qiskit/transpiler/passes/layout/trivial_layout.py b/qiskit/transpiler/passes/layout/trivial_layout.py index 343c5d09e68c..b9469878add6 100644 --- a/qiskit/transpiler/passes/layout/trivial_layout.py +++ b/qiskit/transpiler/passes/layout/trivial_layout.py @@ -50,6 +50,11 @@ def run(self, dag): Raises: TranspilerError: if dag wider than self.coupling_map """ + if not self.coupling_map.is_connected(): + raise TranspilerError( + "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " + "map." + ) if dag.num_qubits() > self.coupling_map.size(): raise TranspilerError("Number of qubits greater than device.") self.property_set["layout"] = Layout.generate_trivial_layout( diff --git a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml new file mode 100644 index 000000000000..18a354c07178 --- /dev/null +++ b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml @@ -0,0 +1,13 @@ +--- +features: + - | + Added support to the :class:`~.CouplingMap` object to have a disjoint + connectivity. Previously, a :class:`~.CouplingMap` could only be + constructed if the graph was connected. This will enable using + :class:`~.CouplingMap` to represent hardware with disjoint qubits. + - | + Added a new method :meth:`.CouplingMap.get_component_subgraphs` which + is used to get a list of :class:`~.CouplingMap` component subgraphs for + a disjoint :class:`~.CouplingMap`. If the :class:`~.CouplingMap` object + is connected this will just return a single :class:`~.CouplingMap` + equivalent to the original. diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index cffc22c59844..6c26099f50e2 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -14,6 +14,9 @@ import unittest +import numpy as np +import rustworkx as rx + from qiskit.transpiler import CouplingMap from qiskit.transpiler.exceptions import CouplingError from qiskit.providers.fake_provider import FakeRueschlikon @@ -101,14 +104,6 @@ def test_successful_reduced_map(self): ans = [(1, 2), (3, 2), (0, 1)] self.assertEqual(set(out), set(ans)) - def test_failed_reduced_map(self): - """Generate a bad disconnected reduced map""" - fake = FakeRueschlikon() - cmap = fake.configuration().coupling_map - coupling_map = CouplingMap(cmap) - with self.assertRaises(CouplingError): - coupling_map.reduce([12, 11, 10, 3]) - def test_symmetric_small_true(self): coupling_list = [[0, 1], [1, 0]] coupling = CouplingMap(coupling_list) @@ -448,6 +443,36 @@ def test_implements_iter(self): expected = [(0, 1), (1, 0), (1, 2), (2, 1)] self.assertEqual(sorted(coupling), expected) + def test_disjoint_coupling_map(self): + cmap = CouplingMap([[0, 1], [1, 0], [2, 3], [3, 2]]) + self.assertFalse(cmap.is_connected()) + distance_matrix = cmap.distance_matrix + expected = np.array( + [ + [0, 1, 0, 0], + [1, 0, 0, 0], + [0, 0, 0, 1], + [0, 0, 1, 0], + ] + ) + np.testing.assert_array_equal(expected, distance_matrix) + + def test_get_component_subgraphs_connected_graph(self): + cmap = CouplingMap.from_line(5) + self.assertTrue(cmap.is_connected()) + subgraphs = cmap.get_component_subgraphs() + self.assertEqual(len(subgraphs), 1) + self.assertTrue(rx.is_isomorphic(cmap.graph, subgraphs[0].graph)) + + def test_get_component_subgraphs_disconnected_graph(self): + cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) + self.assertFalse(cmap.is_connected()) + subgraphs = cmap.get_component_subgraphs() + self.assertEqual(len(subgraphs), 2) + expected_subgraph = CouplingMap([[0, 1], [1, 2]]) + self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[0].graph)) + self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[1].graph)) + class CouplingVisualizationTest(QiskitVisualizationTestCase): @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") From b506d88825aa0714d5ee87e5ab22c7c94841585f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 Mar 2023 16:57:06 -0500 Subject: [PATCH 02/19] Remove coupling map connected check from NoiseAdaptiveLayout Noise adaptive layout doesn't use a CouplingMap so we can't check for a disconnected coupling map in it. --- qiskit/transpiler/passes/layout/noise_adaptive_layout.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/qiskit/transpiler/passes/layout/noise_adaptive_layout.py b/qiskit/transpiler/passes/layout/noise_adaptive_layout.py index 47bd2c7273a7..f8dcc566d1be 100644 --- a/qiskit/transpiler/passes/layout/noise_adaptive_layout.py +++ b/qiskit/transpiler/passes/layout/noise_adaptive_layout.py @@ -211,11 +211,6 @@ def _select_best_remaining_qubit(self, prog_qubit): def run(self, dag): """Run the NoiseAdaptiveLayout pass on `dag`.""" - if not self.coupling_map.is_connected(): - raise TranspilerError( - "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " - "map." - ) self.swap_graph = rx.PyDiGraph() self.cx_reliability = {} self.readout_reliability = {} From 6b565170acd687fb8d3adba75f9097537281f768 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 Mar 2023 17:00:55 -0500 Subject: [PATCH 03/19] Change DenseLayout guard to only prevent running when it won't work --- qiskit/transpiler/passes/layout/dense_layout.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index 5ecdb87deceb..b786ce99accf 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -79,10 +79,10 @@ def run(self, dag): raise TranspilerError( "A coupling_map or target with constrained qargs is necessary to run the pass." ) - if not self.coupling_map.is_connected(): + if dag.num_qubits() > self.coupling_map.largest_connected_component(): raise TranspilerError( "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " - "map." + "map for a circuit this wide." ) num_dag_qubits = len(dag.qubits) if num_dag_qubits > self.coupling_map.size(): From 25d3379ffe4860433c33476907d41195fafbd0f6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 Mar 2023 17:14:53 -0500 Subject: [PATCH 04/19] Rename get_component_subgraphs to components and cache result This commit renames the get_component_subgraphs() method to components() which is much more consise name. At the same time this adds caching to the return just in case building the component subgraphs is expensive to compute we only need to ever do it once. --- qiskit/transpiler/coupling.py | 27 +++++++++++++++-- .../add-cmap-componets-7ed56cdf294150f1.yaml | 2 +- test/python/transpiler/test_coupling.py | 30 ++++++++++++++++--- 3 files changed, 52 insertions(+), 7 deletions(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 0c7e00877e27..7b8196a639be 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -37,7 +37,16 @@ class CouplingMap: and target qubits, respectively. """ - __slots__ = ("description", "graph", "_dist_matrix", "_qubit_list", "_size", "_is_symmetric") + __slots__ = ( + "description", + "graph", + "_dist_matrix", + "_qubit_list", + "_size", + "_is_symmetric", + "_strong_components", + "_weak_components", + ) def __init__(self, couplinglist=None, description=None): """ @@ -60,6 +69,8 @@ def __init__(self, couplinglist=None, description=None): # number of qubits in the graph self._size = None self._is_symmetric = None + self._strong_components = None + self._weak_components = None if couplinglist is not None: self.graph.extend_from_edge_list([tuple(x) for x in couplinglist]) @@ -100,6 +111,8 @@ def add_physical_qubit(self, physical_qubit): self._dist_matrix = None # invalidate self._qubit_list = None # invalidate self._size = None # invalidate + self._weak_components = None # invalidate + self._strong_components = None # invalidate def add_edge(self, src, dst): """ @@ -115,6 +128,8 @@ def add_edge(self, src, dst): self.graph.add_edge(src, dst, None) self._dist_matrix = None # invalidate self._is_symmetric = None # invalidate + self._weak_components = None # invalidate + self._strong_components = None # invalidate def subgraph(self, nodelist): """Return a CouplingMap object for a subgraph of self. @@ -391,7 +406,7 @@ def largest_connected_component(self): """Return a set of qubits in the largest connected component.""" return max(rx.weakly_connected_components(self.graph), key=len) - def get_component_subgraphs(self, respect_direction: bool = False) -> List["CouplingMap"]: + def components(self, respect_direction: bool = False) -> List["CouplingMap"]: """Separate a CouplingMap into subgraph Coupling Maps for each connected component. This method will return a list of :class:`~.CouplingMap` objects for each connected @@ -415,6 +430,10 @@ def get_component_subgraphs(self, respect_direction: bool = False) -> List["Coup list: A list of :class:`~.CouplingMap` objects for each connected components. """ + if respect_direction and self._strong_components is not None: + return self._strong_components + if self._weak_components is not None: + return self._weak_components # Set payload to index for node in self.graph.node_indices(): @@ -428,6 +447,10 @@ def get_component_subgraphs(self, respect_direction: bool = False) -> List["Coup new_cmap = CouplingMap() new_cmap.graph = self.graph.subgraph(list(sorted(component))) output_list.append(new_cmap) + if respect_direction: + self._strong_components = output_list + else: + self._weak_components = output_list return output_list def __str__(self): diff --git a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml index 18a354c07178..7475ad51d3ce 100644 --- a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml +++ b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml @@ -6,7 +6,7 @@ features: constructed if the graph was connected. This will enable using :class:`~.CouplingMap` to represent hardware with disjoint qubits. - | - Added a new method :meth:`.CouplingMap.get_component_subgraphs` which + Added a new method :meth:`.CouplingMap.components` which is used to get a list of :class:`~.CouplingMap` component subgraphs for a disjoint :class:`~.CouplingMap`. If the :class:`~.CouplingMap` object is connected this will just return a single :class:`~.CouplingMap` diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 6c26099f50e2..61ee6983bfb2 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -457,22 +457,44 @@ def test_disjoint_coupling_map(self): ) np.testing.assert_array_equal(expected, distance_matrix) - def test_get_component_subgraphs_connected_graph(self): + def test_components_connected_graph(self): cmap = CouplingMap.from_line(5) self.assertTrue(cmap.is_connected()) - subgraphs = cmap.get_component_subgraphs() + subgraphs = cmap.components() self.assertEqual(len(subgraphs), 1) self.assertTrue(rx.is_isomorphic(cmap.graph, subgraphs[0].graph)) - def test_get_component_subgraphs_disconnected_graph(self): + def test_components_disconnected_graph(self): cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) self.assertFalse(cmap.is_connected()) - subgraphs = cmap.get_component_subgraphs() + subgraphs = cmap.components() self.assertEqual(len(subgraphs), 2) expected_subgraph = CouplingMap([[0, 1], [1, 2]]) self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[0].graph)) self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[1].graph)) + def test_strongly_connected_components_disconnected_graph(self): + cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) + self.assertFalse(cmap.is_connected()) + subgraphs = cmap.components(True) + self.assertEqual(len(subgraphs), 6) + expected_subgraph = CouplingMap() + expected_subgraph.add_physical_qubit(0) + for i in range(6): + self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[i].graph)) + + def test_components_disconnected_graph_cached(self): + cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) + self.assertFalse(cmap.is_connected()) + subgraphs = cmap.components() + self.assertEqual(subgraphs, cmap.components()) + + def test_strongly_connected_components_disconnected_graph_cached(self): + cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) + self.assertFalse(cmap.is_connected()) + subgraphs = cmap.components(True) + self.assertEqual(subgraphs, cmap.components(True)) + class CouplingVisualizationTest(QiskitVisualizationTestCase): @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") From 4d293ba33276c80f2cacbc2818388d7568e143bd Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 Mar 2023 17:49:36 -0500 Subject: [PATCH 05/19] Drop caching of connected components --- qiskit/transpiler/coupling.py | 17 ----------------- test/python/transpiler/test_coupling.py | 12 ------------ 2 files changed, 29 deletions(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 7b8196a639be..a3ff0f5492d7 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -44,8 +44,6 @@ class CouplingMap: "_qubit_list", "_size", "_is_symmetric", - "_strong_components", - "_weak_components", ) def __init__(self, couplinglist=None, description=None): @@ -69,8 +67,6 @@ def __init__(self, couplinglist=None, description=None): # number of qubits in the graph self._size = None self._is_symmetric = None - self._strong_components = None - self._weak_components = None if couplinglist is not None: self.graph.extend_from_edge_list([tuple(x) for x in couplinglist]) @@ -111,8 +107,6 @@ def add_physical_qubit(self, physical_qubit): self._dist_matrix = None # invalidate self._qubit_list = None # invalidate self._size = None # invalidate - self._weak_components = None # invalidate - self._strong_components = None # invalidate def add_edge(self, src, dst): """ @@ -128,8 +122,6 @@ def add_edge(self, src, dst): self.graph.add_edge(src, dst, None) self._dist_matrix = None # invalidate self._is_symmetric = None # invalidate - self._weak_components = None # invalidate - self._strong_components = None # invalidate def subgraph(self, nodelist): """Return a CouplingMap object for a subgraph of self. @@ -430,11 +422,6 @@ def components(self, respect_direction: bool = False) -> List["CouplingMap"]: list: A list of :class:`~.CouplingMap` objects for each connected components. """ - if respect_direction and self._strong_components is not None: - return self._strong_components - if self._weak_components is not None: - return self._weak_components - # Set payload to index for node in self.graph.node_indices(): self.graph[node] = node @@ -447,10 +434,6 @@ def components(self, respect_direction: bool = False) -> List["CouplingMap"]: new_cmap = CouplingMap() new_cmap.graph = self.graph.subgraph(list(sorted(component))) output_list.append(new_cmap) - if respect_direction: - self._strong_components = output_list - else: - self._weak_components = output_list return output_list def __str__(self): diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 61ee6983bfb2..a2b5f148d36f 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -483,18 +483,6 @@ def test_strongly_connected_components_disconnected_graph(self): for i in range(6): self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[i].graph)) - def test_components_disconnected_graph_cached(self): - cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) - self.assertFalse(cmap.is_connected()) - subgraphs = cmap.components() - self.assertEqual(subgraphs, cmap.components()) - - def test_strongly_connected_components_disconnected_graph_cached(self): - cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) - self.assertFalse(cmap.is_connected()) - subgraphs = cmap.components(True) - self.assertEqual(subgraphs, cmap.components(True)) - class CouplingVisualizationTest(QiskitVisualizationTestCase): @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") From 033b95930a6e21f72791f3b86f125cae632bc6ff Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Thu, 2 Mar 2023 17:50:57 -0500 Subject: [PATCH 06/19] Fix check for dense layout to do a valid comparison --- qiskit/transpiler/passes/layout/dense_layout.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/passes/layout/dense_layout.py b/qiskit/transpiler/passes/layout/dense_layout.py index b786ce99accf..cd87fad134ca 100644 --- a/qiskit/transpiler/passes/layout/dense_layout.py +++ b/qiskit/transpiler/passes/layout/dense_layout.py @@ -79,7 +79,7 @@ def run(self, dag): raise TranspilerError( "A coupling_map or target with constrained qargs is necessary to run the pass." ) - if dag.num_qubits() > self.coupling_map.largest_connected_component(): + if dag.num_qubits() > len(self.coupling_map.largest_connected_component()): raise TranspilerError( "Coupling Map is disjoint, this pass can't be used with a disconnected coupling " "map for a circuit this wide." From e50a4c6f13c4494c8d2a41ce45396a91b946c7f6 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 Mar 2023 08:30:31 -0500 Subject: [PATCH 07/19] Ensure self loops in CouplingMap.distance() return 0 In a previous commit the distance() method was updated to handle disjoint graphs correctly. Prior to this PR it was expected to raise when a path didn't exist between 2 qubits by nature of the distance matrix construction failing if there was a disconnected coupling map. Since that isn't the case after this PR the error condition was changed to check explicitly that there is no path available and then error. However, there was an issue in this case and self loops would incorrectly error as well when instead they should return 0. This commit updates the error check to ignore self loops so they return correctly. --- qiskit/transpiler/coupling.py | 2 +- test/python/transpiler/test_coupling.py | 7 +++++++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index a3ff0f5492d7..17e20d0b4959 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -203,7 +203,7 @@ def distance(self, physical_qubit1, physical_qubit2): raise CouplingError("%s not in coupling graph" % physical_qubit2) self.compute_distance_matrix() res = int(self._dist_matrix[physical_qubit1, physical_qubit2]) - if res == 0: + if res == 0 and physical_qubit1 != physical_qubit2: raise CouplingError(f"No path from {physical_qubit1} to {physical_qubit2}") return res diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index a2b5f148d36f..ac4f8783f819 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -84,6 +84,13 @@ def test_distance_error(self): graph.add_physical_qubit(1) self.assertRaises(CouplingError, graph.distance, 0, 1) + def test_distance_self_loop(self): + """Test distance between the same physical qubit.""" + graph = CouplingMap() + graph.add_physical_qubit(0) + graph.add_physical_qubit(1) + self.assertEqual(0., graph.distance(0, 0)) + def test_init_with_couplinglist(self): coupling_list = [[0, 1], [1, 2]] coupling = CouplingMap(coupling_list) From 9111a7a98a43587a701292c00c2c90d3c82856d7 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 3 Mar 2023 08:34:10 -0500 Subject: [PATCH 08/19] Fix lint --- test/python/transpiler/test_coupling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index ac4f8783f819..37f4b740c923 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -89,7 +89,7 @@ def test_distance_self_loop(self): graph = CouplingMap() graph.add_physical_qubit(0) graph.add_physical_qubit(1) - self.assertEqual(0., graph.distance(0, 0)) + self.assertEqual(0.0, graph.distance(0, 0)) def test_init_with_couplinglist(self): coupling_list = [[0, 1], [1, 2]] From 850e58137190f3e812835744ea0d0cd493c92c38 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 7 Mar 2023 13:05:47 -0500 Subject: [PATCH 09/19] Update CouplingMap.components() docstring Co-authored-by: Kevin Krsulich --- qiskit/transpiler/coupling.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 17e20d0b4959..8f6c5f3c4d9e 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -401,7 +401,7 @@ def largest_connected_component(self): def components(self, respect_direction: bool = False) -> List["CouplingMap"]: """Separate a CouplingMap into subgraph Coupling Maps for each connected component. - This method will return a list of :class:`~.CouplingMap` objects for each connected + This method will return a list of :class:`~.CouplingMap` objects, one for each connected component in this :class:`~.CouplingMap`. The data payload of each node in the :attr:`~.CouplingMap.graph` attribute will contain the qubit number in the original graph. This will enables mapping the qubit index in a component subgraph to From df6b891792e76685ac05464a9b2d88fb3e462452 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 7 Mar 2023 13:19:36 -0500 Subject: [PATCH 10/19] Expand test coverage --- test/python/transpiler/test_coupling.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 37f4b740c923..61a61fe39df0 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -464,6 +464,20 @@ def test_disjoint_coupling_map(self): ) np.testing.assert_array_equal(expected, distance_matrix) + def test_disjoint_coupling_map_distance_no_path_qubits(self): + cmap = CouplingMap([[0, 1], [1, 0], [2, 3], [3, 2]]) + self.assertFalse(cmap.is_connected()) + with self.assertRaises(CouplingError): + cmap.distance(0, 3) + + def test_component_mapping(self): + cmap = CouplingMap([[0, 1], [1, 0], [2, 3], [3, 2]]) + components = cmap.components() + self.assertEqual(components[1].graph[0], 2) + self.assertEqual(components[1].graph[1], 3) + self.assertEqual(components[0].graph[0], 0) + self.assertEqual(components[0].graph[1], 1) + def test_components_connected_graph(self): cmap = CouplingMap.from_line(5) self.assertTrue(cmap.is_connected()) From 80796997b2e08046ace06625b4e80f46c909234f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 7 Mar 2023 13:21:50 -0500 Subject: [PATCH 11/19] Remove unused option for strongly connected components --- qiskit/transpiler/coupling.py | 10 ++-------- test/python/transpiler/test_coupling.py | 10 ---------- 2 files changed, 2 insertions(+), 18 deletions(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 8f6c5f3c4d9e..7c4c00436e28 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -398,7 +398,7 @@ def largest_connected_component(self): """Return a set of qubits in the largest connected component.""" return max(rx.weakly_connected_components(self.graph), key=len) - def components(self, respect_direction: bool = False) -> List["CouplingMap"]: + def components(self) -> List["CouplingMap"]: """Separate a CouplingMap into subgraph Coupling Maps for each connected component. This method will return a list of :class:`~.CouplingMap` objects, one for each connected @@ -415,9 +415,6 @@ def components(self, respect_direction: bool = False) -> List["CouplingMap"]: will print ``3`` as index ``0`` in the second component is qubit 3 in the original cmap. - Args: - respect_direction: If set to ``True`` the connected components will be strongly connected - Returns: list: A list of :class:`~.CouplingMap` objects for each connected components. @@ -425,10 +422,7 @@ def components(self, respect_direction: bool = False) -> List["CouplingMap"]: # Set payload to index for node in self.graph.node_indices(): self.graph[node] = node - if not respect_direction: - components = rx.weakly_connected_components(self.graph) - else: - components = rx.strongly_connected_components(self.graph) + components = rx.weakly_connected_components(self.graph) output_list = [] for component in components: new_cmap = CouplingMap() diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 61a61fe39df0..1f8ec17f9a20 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -494,16 +494,6 @@ def test_components_disconnected_graph(self): self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[0].graph)) self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[1].graph)) - def test_strongly_connected_components_disconnected_graph(self): - cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) - self.assertFalse(cmap.is_connected()) - subgraphs = cmap.components(True) - self.assertEqual(len(subgraphs), 6) - expected_subgraph = CouplingMap() - expected_subgraph.add_physical_qubit(0) - for i in range(6): - self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[i].graph)) - class CouplingVisualizationTest(QiskitVisualizationTestCase): @unittest.skipUnless(optionals.HAS_GRAPHVIZ, "Graphviz not installed") From 654a965024348799858f158c3958ae044c651700 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 7 Mar 2023 15:52:55 -0500 Subject: [PATCH 12/19] Expand docstring to explain return list order --- qiskit/transpiler/coupling.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 7c4c00436e28..578a9bd4ea97 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -417,7 +417,9 @@ def components(self) -> List["CouplingMap"]: Returns: list: A list of :class:`~.CouplingMap` objects for each connected - components. + components. The order of this list is deterministic but + implementation specific and shouldn't be relied upon as + part of the API. """ # Set payload to index for node in self.graph.node_indices(): From b847ebca388be244de98fe6464e5553644d57990 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 7 Mar 2023 16:09:43 -0500 Subject: [PATCH 13/19] Use infinity for disconnected nodes in distance matrix --- qiskit/transpiler/coupling.py | 17 ++++++++++++----- test/python/transpiler/test_coupling.py | 8 ++++---- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 578a9bd4ea97..b00d07ab89cd 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -19,6 +19,7 @@ onto a device with this coupling. """ +import math from typing import List import warnings @@ -167,7 +168,11 @@ def neighbors(self, physical_qubit): @property def distance_matrix(self): - """Return the distance matrix for the coupling map.""" + """Return the distance matrix for the coupling map. + + For any qubits where there isn't a path available between them the value + in this position of the distance matrix will be ``math.inf``. + """ self.compute_distance_matrix() return self._dist_matrix @@ -182,7 +187,9 @@ def compute_distance_matrix(self): those or want to pre-generate it. """ if self._dist_matrix is None: - self._dist_matrix = rx.digraph_distance_matrix(self.graph, as_undirected=True) + self._dist_matrix = rx.digraph_distance_matrix( + self.graph, as_undirected=True, null_value=math.inf + ) def distance(self, physical_qubit1, physical_qubit2): """Returns the undirected distance between physical_qubit1 and physical_qubit2. @@ -202,10 +209,10 @@ def distance(self, physical_qubit1, physical_qubit2): if physical_qubit2 >= self.size(): raise CouplingError("%s not in coupling graph" % physical_qubit2) self.compute_distance_matrix() - res = int(self._dist_matrix[physical_qubit1, physical_qubit2]) - if res == 0 and physical_qubit1 != physical_qubit2: + res = self._dist_matrix[physical_qubit1, physical_qubit2] + if res == math.inf: raise CouplingError(f"No path from {physical_qubit1} to {physical_qubit2}") - return res + return int(res) def shortest_undirected_path(self, physical_qubit1, physical_qubit2): """Returns the shortest undirected path between physical_qubit1 and physical_qubit2. diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 1f8ec17f9a20..f8b544b7ef2e 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -456,10 +456,10 @@ def test_disjoint_coupling_map(self): distance_matrix = cmap.distance_matrix expected = np.array( [ - [0, 1, 0, 0], - [1, 0, 0, 0], - [0, 0, 0, 1], - [0, 0, 1, 0], + [0, 1, np.inf, np.inf], + [1, 0, np.inf, np.inf], + [np.inf, np.inf, 0, 1], + [np.inf, np.inf, 1, 0], ] ) np.testing.assert_array_equal(expected, distance_matrix) From 42474dfc378bf92474861b1a45b0e3dcf99b9d16 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Tue, 7 Mar 2023 16:10:05 -0500 Subject: [PATCH 14/19] Update releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml --- releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml index 7475ad51d3ce..9327e748ae09 100644 --- a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml +++ b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml @@ -4,7 +4,8 @@ features: Added support to the :class:`~.CouplingMap` object to have a disjoint connectivity. Previously, a :class:`~.CouplingMap` could only be constructed if the graph was connected. This will enable using - :class:`~.CouplingMap` to represent hardware with disjoint qubits. + :class:`~.CouplingMap` to represent hardware with disjoint qubits, such as hardware + with qubits on multiple separate chips? - | Added a new method :meth:`.CouplingMap.components` which is used to get a list of :class:`~.CouplingMap` component subgraphs for From 0936fb044b89de21cec8728c20519f5a6986c241 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Mar 2023 12:39:02 -0500 Subject: [PATCH 15/19] Rename CouplingMap.components to connected_components() THis commit renames the CouplingMap.components() method to connected_components(). It also adds an example to the docstring to better explain what a connected component is. --- qiskit/transpiler/coupling.py | 30 +++++++++++++++++++++++-- test/python/transpiler/test_coupling.py | 6 ++--- 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index b00d07ab89cd..a5acc63b2014 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -405,8 +405,34 @@ def largest_connected_component(self): """Return a set of qubits in the largest connected component.""" return max(rx.weakly_connected_components(self.graph), key=len) - def components(self) -> List["CouplingMap"]: - """Separate a CouplingMap into subgraph Coupling Maps for each connected component. + def connected_components(self) -> List["CouplingMap"]: + """Separate a :Class:`~.CouplingMap` into subgraph :class:`~.CouplingMap` + for each connected component. + + The connected components of a :class:`~.CouplingMap` are the subgraphs + that are not part of any larger subgraph. For example, if you had a + coupling map that looked like:: + + 0 --> 1 4 --> 5 ---> 6 --> 7 + | | + | | + V V + 2 --> 3 + + then the connected components of that graph are the subgraphs:: + + 0 --> 1 + | | + | | + V V + 2 --> 3 + + and:: + + 4 --> 5 ---> 6 --> 7 + + For a connected :class:`~.CouplingMap` object there is only a single connected + component, the entire :class:`~.CouplingMap`. This method will return a list of :class:`~.CouplingMap` objects, one for each connected component in this :class:`~.CouplingMap`. The data payload of each node in the diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index f8b544b7ef2e..9e131ab87420 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -472,7 +472,7 @@ def test_disjoint_coupling_map_distance_no_path_qubits(self): def test_component_mapping(self): cmap = CouplingMap([[0, 1], [1, 0], [2, 3], [3, 2]]) - components = cmap.components() + components = cmap.connected_components() self.assertEqual(components[1].graph[0], 2) self.assertEqual(components[1].graph[1], 3) self.assertEqual(components[0].graph[0], 0) @@ -481,14 +481,14 @@ def test_component_mapping(self): def test_components_connected_graph(self): cmap = CouplingMap.from_line(5) self.assertTrue(cmap.is_connected()) - subgraphs = cmap.components() + subgraphs = cmap.connected_components() self.assertEqual(len(subgraphs), 1) self.assertTrue(rx.is_isomorphic(cmap.graph, subgraphs[0].graph)) def test_components_disconnected_graph(self): cmap = CouplingMap([[0, 1], [1, 2], [3, 4], [4, 5]]) self.assertFalse(cmap.is_connected()) - subgraphs = cmap.components() + subgraphs = cmap.connected_components() self.assertEqual(len(subgraphs), 2) expected_subgraph = CouplingMap([[0, 1], [1, 2]]) self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[0].graph)) From 91b8a0d8377b485503d1535923885d799ad7dd1e Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Mar 2023 12:40:43 -0500 Subject: [PATCH 16/19] Fix typo in relaese note --- releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml index 9327e748ae09..9a4f56e8f17e 100644 --- a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml +++ b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml @@ -5,7 +5,7 @@ features: connectivity. Previously, a :class:`~.CouplingMap` could only be constructed if the graph was connected. This will enable using :class:`~.CouplingMap` to represent hardware with disjoint qubits, such as hardware - with qubits on multiple separate chips? + with qubits on multiple separate chips. - | Added a new method :meth:`.CouplingMap.components` which is used to get a list of :class:`~.CouplingMap` component subgraphs for From 89579973e3a36a423bc81e3537a1db6f6cc7f32f Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 8 Mar 2023 12:44:08 -0500 Subject: [PATCH 17/19] Update method name in release note --- releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml index 9a4f56e8f17e..7c385d1eb896 100644 --- a/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml +++ b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml @@ -7,7 +7,7 @@ features: :class:`~.CouplingMap` to represent hardware with disjoint qubits, such as hardware with qubits on multiple separate chips. - | - Added a new method :meth:`.CouplingMap.components` which + Added a new method :meth:`.CouplingMap.connected_components` which is used to get a list of :class:`~.CouplingMap` component subgraphs for a disjoint :class:`~.CouplingMap`. If the :class:`~.CouplingMap` object is connected this will just return a single :class:`~.CouplingMap` From a3cf7ffa7831d1098defc1df7dd72774832f0efd Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 22 Mar 2023 10:24:15 -0400 Subject: [PATCH 18/19] Restore previous reduce() behavior The current reduce() error behavior of raising on trying to reduce to a disconnected coupling map is being depended on in other locations. To avoid a potentially breaking change this commit reverts the removal of that limitation in the method. We can look at doing that in the future independently of this PR because removing this specific restriction on the reduce() method is not 100% tied to generally allowing disconnected coupling map objects. --- qiskit/transpiler/coupling.py | 14 ++++++++++++++ test/python/transpiler/test_coupling.py | 8 ++++++++ 2 files changed, 22 insertions(+) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index c291bd7f2837..570258fea90c 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -283,6 +283,10 @@ def reduce(self, mapping): Raises: CouplingError: Reduced coupling map must be connected. """ + + from scipy.sparse import coo_matrix, csgraph + + reduced_qubits = len(mapping) inv_map = [None] * (max(mapping) + 1) for idx, val in enumerate(mapping): inv_map[val] = idx @@ -293,6 +297,16 @@ def reduce(self, mapping): if edge[0] in mapping and edge[1] in mapping: reduced_cmap.append([inv_map[edge[0]], inv_map[edge[1]]]) + # Verify coupling_map is connected + rows = np.array([edge[0] for edge in reduced_cmap], dtype=int) + cols = np.array([edge[1] for edge in reduced_cmap], dtype=int) + data = np.ones_like(rows) + + mat = coo_matrix((data, (rows, cols)), shape=(reduced_qubits, reduced_qubits)).tocsr() + + if csgraph.connected_components(mat)[0] != 1: + raise CouplingError("coupling_map must be connected.") + return CouplingMap(reduced_cmap) @classmethod diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index 576d81dc69ff..5a41326b1bbf 100644 --- a/test/python/transpiler/test_coupling.py +++ b/test/python/transpiler/test_coupling.py @@ -111,6 +111,14 @@ def test_successful_reduced_map(self): ans = [(1, 2), (3, 2), (0, 1)] self.assertEqual(set(out), set(ans)) + def test_failed_reduced_map(self): + """Generate a bad disconnected reduced map""" + fake = FakeRueschlikon() + cmap = fake.configuration().coupling_map + coupling_map = CouplingMap(cmap) + with self.assertRaises(CouplingError): + coupling_map.reduce([12, 11, 10, 3]) + def test_symmetric_small_true(self): coupling_list = [[0, 1], [1, 0]] coupling = CouplingMap(coupling_list) From acebb43f2be6d40275a4dd669a3e6efa86d03776 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Wed, 22 Mar 2023 13:19:02 -0400 Subject: [PATCH 19/19] Add missing import --- qiskit/transpiler/coupling.py | 1 + 1 file changed, 1 insertion(+) diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 570258fea90c..a15d142cf544 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -23,6 +23,7 @@ from typing import List import warnings +import numpy as np import rustworkx as rx from rustworkx.visualization import graphviz_draw