Skip to content

Commit

Permalink
Add support to CouplingMap for disjoint qubits (Qiskit#9710)
Browse files Browse the repository at this point in the history
* 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 <kevin@krsulich.net>

* 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 <kevin@krsulich.net>
  • Loading branch information
2 people authored and king-p3nguin committed May 22, 2023
1 parent 69dc92e commit 39b2b5c
Show file tree
Hide file tree
Showing 7 changed files with 172 additions and 7 deletions.
89 changes: 83 additions & 6 deletions qiskit/transpiler/coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
onto a device with this coupling.
"""

import math
from typing import List
import warnings

import numpy as np
Expand All @@ -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):
"""
Expand Down Expand Up @@ -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

Expand All @@ -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.
Expand All @@ -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.
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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 = ""
Expand Down
6 changes: 6 additions & 0 deletions qiskit/transpiler/passes/layout/csp_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down Expand Up @@ -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()

Expand Down
5 changes: 5 additions & 0 deletions qiskit/transpiler/passes/layout/dense_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.")
Expand Down
6 changes: 5 additions & 1 deletion qiskit/transpiler/passes/layout/sabre_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
5 changes: 5 additions & 0 deletions qiskit/transpiler/passes/layout/trivial_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
14 changes: 14 additions & 0 deletions releasenotes/notes/add-cmap-componets-7ed56cdf294150f1.yaml
Original file line number Diff line number Diff line change
@@ -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.
54 changes: 54 additions & 0 deletions test/python/transpiler/test_coupling.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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."""

Expand Down

0 comments on commit 39b2b5c

Please sign in to comment.