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."""