From a1a67a11741000f8f3b6d6a8766dab7e3cd06847 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 24 Mar 2023 14:34:53 -0400 Subject: [PATCH] Add support to CouplingMap for disjoint qubits (#9710) * 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. * 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. * Change DenseLayout guard to only prevent running when it won't work * 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. * Drop caching of connected components * Fix check for dense layout to do a valid comparison * 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. * Fix lint * Update CouplingMap.components() docstring Co-authored-by: Kevin Krsulich * Expand test coverage * Remove unused option for strongly connected components * Expand docstring to explain return list order * Use infinity for disconnected nodes in distance matrix * Update releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml * 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. * Fix typo in relaese note * Update method name in release note * 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. * Add missing import --------- Co-authored-by: Kevin Krsulich --- qiskit/transpiler/coupling.py | 89 +++++++++++++++++-- qiskit/transpiler/passes/layout/csp_layout.py | 6 ++ .../transpiler/passes/layout/dense_layout.py | 5 ++ .../transpiler/passes/layout/sabre_layout.py | 6 +- .../passes/layout/trivial_layout.py | 5 ++ .../add-cmap-componets-7ed56cdf294150f1.yaml | 14 +++ test/python/transpiler/test_coupling.py | 54 +++++++++++ 7 files changed, 172 insertions(+), 7 deletions(-) create mode 100644 releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml diff --git a/qiskit/transpiler/coupling.py b/qiskit/transpiler/coupling.py index 26d4052aa02b..a15d142cf544 100644 --- a/qiskit/transpiler/coupling.py +++ b/qiskit/transpiler/coupling.py @@ -19,6 +19,8 @@ onto a device with this coupling. """ +import math +from typing import List import warnings import numpy as np @@ -37,7 +39,14 @@ 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", + ) def __init__(self, couplinglist=None, description=None): """ @@ -160,7 +169,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 @@ -175,9 +188,9 @@ 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) + 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. @@ -197,7 +210,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 = self._dist_matrix[physical_qubit1, physical_qubit2] + if res == math.inf: + raise CouplingError(f"No path from {physical_qubit1} to {physical_qubit2}") + return int(res) def shortest_undirected_path(self, physical_qubit1, physical_qubit2): """Returns the shortest undirected path between physical_qubit1 and physical_qubit2. @@ -268,6 +284,7 @@ def reduce(self, mapping): Raises: CouplingError: Reduced coupling map must be connected. """ + from scipy.sparse import coo_matrix, csgraph reduced_qubits = len(mapping) @@ -403,6 +420,66 @@ 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 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 + :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. + + Returns: + list: A list of :class:`~.CouplingMap` objects for each connected + 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(): + self.graph[node] = node + components = rx.weakly_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..cd87fad134ca 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 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." + ) 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/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..7c385d1eb896 --- /dev/null +++ b/releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml @@ -0,0 +1,14 @@ +--- +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, such as hardware + with qubits on multiple separate chips. + - | + 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` + equivalent to the original. diff --git a/test/python/transpiler/test_coupling.py b/test/python/transpiler/test_coupling.py index b15c389353e5..5a41326b1bbf 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 @@ -81,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.0, graph.distance(0, 0)) + def test_init_with_couplinglist(self): coupling_list = [[0, 1], [1, 2]] coupling = CouplingMap(coupling_list) @@ -448,6 +458,50 @@ 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, 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) + + 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.connected_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()) + 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.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)) + self.assertTrue(rx.is_isomorphic(expected_subgraph.graph, subgraphs[1].graph)) + def test_equality(self): """Test that equality checks that the graphs have the same nodes, node labels, and edges."""