From 504421635df355e271bd9176b4d7d02dd7aa506a Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Wed, 10 Aug 2022 15:25:18 -0700 Subject: [PATCH 01/11] created routing utilities in cirq-core/transformers and added MappingManager module --- .../cirq/transformers/routing/__init__.py | 17 +++ .../transformers/routing/mapping_manager.py | 113 ++++++++++++++++++ .../routing/mapping_manager_test.py | 76 ++++++++++++ 3 files changed, 206 insertions(+) create mode 100644 cirq-core/cirq/transformers/routing/__init__.py create mode 100644 cirq-core/cirq/transformers/routing/mapping_manager.py create mode 100644 cirq-core/cirq/transformers/routing/mapping_manager_test.py diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py new file mode 100644 index 00000000000..374eb30c1ec --- /dev/null +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -0,0 +1,17 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Routing utilities in Cirq.""" + +from cirq.transformers.routing.mapping_manager import MappingManager \ No newline at end of file diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py new file mode 100644 index 00000000000..1143afb0ccd --- /dev/null +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -0,0 +1,113 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import networkx as nx +from typing import Dict, TYPE_CHECKING + +from cirq import ops, protocols + +if TYPE_CHECKING: + import cirq + + +class MappingManager: + """Class that keeps track of the mapping of logical to physical qubits and provides + convenience methods for distance queries on the physical qubits. + + Qubit variables with the characer 'p' preppended to them are physical and qubits with the + character 'l' preppended to them are logical qubits. + """ + def __init__( + self, + device_graph: nx.Graph, + initial_mapping: Dict[ops.Qid, ops.Qid] + ) -> None: + """Initializes MappingManager. + + Args: + device_graph: connectivity graph of qubits in the hardware device. + circuit_graph: connectivity graph of the qubits in the input circuit. + initial_mapping: the initial mapping of logical (keys) to physical qubits (values). + """ + self.device_graph = device_graph + self._map = initial_mapping.copy() + self._inverse_map = {v:k for k,v in self._map.items()} + self._induced_subgraph = nx.induced_subgraph(self.device_graph, self._map.values()) + self._shortest_paths_matrix = dict(nx.all_pairs_shortest_path(self._induced_subgraph)) + + @property + def map(self) -> Dict[ops.Qid, ops.Qid]: + """The mapping of logical qubits (keys) to physical qubits (values).""" + return self._map + + @property + def inverse_map(self) -> Dict[ops.Qid, ops.Qid]: + """The mapping of physical qubits (keys) to logical qubits (values).""" + return self._inverse_map + + @property + def induced_subgraph(self) -> nx.Graph: + """The device_graph induced on the physical qubits that are mapped to.""" + return self._induced_subgraph + + def dist_on_device(self, lq1: ops.Qid, lq2: ops.Qid) -> int: + """Finds shortest path distance path between the corresponding physical qubits for logical + qubits q1 and q2 on the device. + + Args: + lq1: the first logical qubit. + lq2: the second logical qubit. + + Returns: + The shortest path distance. + """ + return len(self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]])-1 + + def can_execute(self, op: ops.Operation) -> bool: + """Finds whether the given operation can be executed on the device. + + Args: + op: an operation on logical qubits. + + Returns: + Whether the given operation is executable on the device. + """ + return protocols.num_qubits(op) < 2 or self.dist_on_device(*op.qubits) == 1 + + def apply_swap(self, lq1: ops.Qid, lq2: ops.Qid) -> None: + """Swaps two logical qubits in the map and in the inverse map. + + Args: + lq1: the first logical qubit. + lq2: the second logical qubit. + """ + self._map[lq1], self._map[lq2] = self._map[lq2], self._map[lq1] + + pq1 = self._map[lq1] + pq2 = self._map[lq2] + self._inverse_map[pq1], self._inverse_map[pq2] = self._inverse_map[pq2], self._inverse_map[pq1] + + def mapped_op(self, op: ops.Operation) -> ops.Operation: + """Transforms the given operation with the qubits in self._map. + + Args: + op: an operation on logical qubits. + + Returns: + The same operation on corresponding physical qubits.""" + return op.transform_qubits(self._map) + + def shortest_path(self, lq1: ops.Qid, lq2: ops.Qid): + """Find that shortest path between two logical qubits on the device given their mapping.""" + return self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]] \ No newline at end of file diff --git a/cirq-core/cirq/transformers/routing/mapping_manager_test.py b/cirq-core/cirq/transformers/routing/mapping_manager_test.py new file mode 100644 index 00000000000..08b00e532c5 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/mapping_manager_test.py @@ -0,0 +1,76 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import networkx as nx + +import cirq + + +def test_mapping_manager(): + device_graph = nx.Graph([ + (cirq.NamedQubit("a"), cirq.NamedQubit("b")), + (cirq.NamedQubit("b"), cirq.NamedQubit("c")), + (cirq.NamedQubit("c"), cirq.NamedQubit("d")), + + (cirq.NamedQubit("a"), cirq.NamedQubit("e")), + (cirq.NamedQubit("e"), cirq.NamedQubit("d")), + ]) + q = cirq.LineQubit.range(5) + initial_mapping = { + q[1]: cirq.NamedQubit("a"), + q[3]: cirq.NamedQubit("b"), + q[2]: cirq.NamedQubit("c"), + q[4]: cirq.NamedQubit("d"), + } + mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) + + # adjacent qubits have distance 1 and are thus executable + assert mm.dist_on_device(q[1], q[3]) == 1 + assert mm.can_execute(cirq.CNOT(q[1], q[3])) + + # non-adjacent qubits with distance > 1 are not executable + assert mm.dist_on_device(q[1], q[2]) == 2 + assert mm.can_execute(cirq.CNOT(q[1], q[2])) is False + + # 'dist_on_device' does not use cirq.NamedQubit("e") to find shorter shortest path + assert mm.dist_on_device(q[1], q[4]) == 3 + + # after swapping q[2] and q[3], qubits adjacent to q[2] are now adjacent to q[3] and vice-versa + mm.apply_swap(q[3], q[2]) + assert mm.dist_on_device(q[1], q[2]) == 1 + assert mm.can_execute(cirq.CNOT(q[1], q[2])) + assert mm.dist_on_device(q[1], q[3]) == 2 + assert mm.can_execute(cirq.CNOT(q[1], q[3])) is False + + # the swapped qubits are still executable + assert mm.can_execute(cirq.CNOT(q[2], q[3])) + + # distance between other qubits doesn't change + assert mm.dist_on_device(q[1], q[4]) == 3 + + # test applying swaps to inverse map is correct + mm.inverse_map == {v:k for k,v in mm.map.items()} + + # apply same swap and test shortest path for a couple pairs + mm.apply_swap(q[3], q[2]) + assert mm.shortest_path(q[1], q[2]) == [cirq.NamedQubit("a"), cirq.NamedQubit("b"), cirq.NamedQubit("c")] + assert mm.shortest_path(q[2], q[3]) == [cirq.NamedQubit("c"), cirq.NamedQubit("b")] + assert mm.shortest_path(q[1], q[3]) == [cirq.NamedQubit("a"), cirq.NamedQubit("b")] + + shortest_one_to_four = [cirq.NamedQubit("a"), cirq.NamedQubit("b"), cirq.NamedQubit("c"), cirq.NamedQubit("d")] + assert mm.shortest_path(q[1], q[4]) == shortest_one_to_four + + # shortest path on symmetric qubit reverses the list + assert mm.shortest_path(q[4], q[1]) == shortest_one_to_four[::-1] + From 618fb3e4a04a10b16e9c62241fb4ea38203dd9a1 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Wed, 10 Aug 2022 16:31:36 -0700 Subject: [PATCH 02/11] ran continuous integration checks --- .../cirq/transformers/routing/__init__.py | 2 +- .../transformers/routing/mapping_manager.py | 38 ++++++------- .../routing/mapping_manager_test.py | 56 +++++++++++++------ 3 files changed, 60 insertions(+), 36 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index 374eb30c1ec..94f8787cec3 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -14,4 +14,4 @@ """Routing utilities in Cirq.""" -from cirq.transformers.routing.mapping_manager import MappingManager \ No newline at end of file +from cirq.transformers.routing.mapping_manager import MappingManager diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index 1143afb0ccd..a6cf06a9efb 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -12,8 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -import networkx as nx from typing import Dict, TYPE_CHECKING +import networkx as nx from cirq import ops, protocols @@ -24,15 +24,12 @@ class MappingManager: """Class that keeps track of the mapping of logical to physical qubits and provides convenience methods for distance queries on the physical qubits. - + Qubit variables with the characer 'p' preppended to them are physical and qubits with the character 'l' preppended to them are logical qubits. """ - def __init__( - self, - device_graph: nx.Graph, - initial_mapping: Dict[ops.Qid, ops.Qid] - ) -> None: + + def __init__(self, device_graph: nx.Graph, initial_mapping: Dict[ops.Qid, ops.Qid]) -> None: """Initializes MappingManager. Args: @@ -40,9 +37,9 @@ def __init__( circuit_graph: connectivity graph of the qubits in the input circuit. initial_mapping: the initial mapping of logical (keys) to physical qubits (values). """ - self.device_graph = device_graph + self.device_graph = device_graph self._map = initial_mapping.copy() - self._inverse_map = {v:k for k,v in self._map.items()} + self._inverse_map = {v: k for k, v in self._map.items()} self._induced_subgraph = nx.induced_subgraph(self.device_graph, self._map.values()) self._shortest_paths_matrix = dict(nx.all_pairs_shortest_path(self._induced_subgraph)) @@ -64,7 +61,7 @@ def induced_subgraph(self) -> nx.Graph: def dist_on_device(self, lq1: ops.Qid, lq2: ops.Qid) -> int: """Finds shortest path distance path between the corresponding physical qubits for logical qubits q1 and q2 on the device. - + Args: lq1: the first logical qubit. lq2: the second logical qubit. @@ -72,42 +69,45 @@ def dist_on_device(self, lq1: ops.Qid, lq2: ops.Qid) -> int: Returns: The shortest path distance. """ - return len(self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]])-1 + return len(self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]]) - 1 def can_execute(self, op: ops.Operation) -> bool: """Finds whether the given operation can be executed on the device. - + Args: op: an operation on logical qubits. - Returns: + Returns: Whether the given operation is executable on the device. """ return protocols.num_qubits(op) < 2 or self.dist_on_device(*op.qubits) == 1 def apply_swap(self, lq1: ops.Qid, lq2: ops.Qid) -> None: """Swaps two logical qubits in the map and in the inverse map. - + Args: lq1: the first logical qubit. lq2: the second logical qubit. """ - self._map[lq1], self._map[lq2] = self._map[lq2], self._map[lq1] + self._map[lq1], self._map[lq2] = self._map[lq2], self._map[lq1] pq1 = self._map[lq1] pq2 = self._map[lq2] - self._inverse_map[pq1], self._inverse_map[pq2] = self._inverse_map[pq2], self._inverse_map[pq1] + self._inverse_map[pq1], self._inverse_map[pq2] = ( + self._inverse_map[pq2], + self._inverse_map[pq1], + ) def mapped_op(self, op: ops.Operation) -> ops.Operation: """Transforms the given operation with the qubits in self._map. - + Args: op: an operation on logical qubits. - + Returns: The same operation on corresponding physical qubits.""" return op.transform_qubits(self._map) def shortest_path(self, lq1: ops.Qid, lq2: ops.Qid): """Find that shortest path between two logical qubits on the device given their mapping.""" - return self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]] \ No newline at end of file + return self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]] diff --git a/cirq-core/cirq/transformers/routing/mapping_manager_test.py b/cirq-core/cirq/transformers/routing/mapping_manager_test.py index 08b00e532c5..6865978bb88 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager_test.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager_test.py @@ -13,28 +13,44 @@ # limitations under the License. import networkx as nx +from networkx.utils.misc import graphs_equal import cirq def test_mapping_manager(): - device_graph = nx.Graph([ - (cirq.NamedQubit("a"), cirq.NamedQubit("b")), - (cirq.NamedQubit("b"), cirq.NamedQubit("c")), - (cirq.NamedQubit("c"), cirq.NamedQubit("d")), - - (cirq.NamedQubit("a"), cirq.NamedQubit("e")), - (cirq.NamedQubit("e"), cirq.NamedQubit("d")), - ]) + device_graph = nx.Graph( + [ + (cirq.NamedQubit("a"), cirq.NamedQubit("b")), + (cirq.NamedQubit("b"), cirq.NamedQubit("c")), + (cirq.NamedQubit("c"), cirq.NamedQubit("d")), + (cirq.NamedQubit("a"), cirq.NamedQubit("e")), + (cirq.NamedQubit("e"), cirq.NamedQubit("d")), + ] + ) q = cirq.LineQubit.range(5) initial_mapping = { q[1]: cirq.NamedQubit("a"), q[3]: cirq.NamedQubit("b"), q[2]: cirq.NamedQubit("c"), q[4]: cirq.NamedQubit("d"), - } + } mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) + # test correct induced subgraph + expected_induced_subgraph = nx.Graph( + [ + (cirq.NamedQubit("a"), cirq.NamedQubit("b")), + (cirq.NamedQubit("b"), cirq.NamedQubit("c")), + (cirq.NamedQubit("c"), cirq.NamedQubit("d")), + ] + ) + assert graphs_equal(mm.induced_subgraph, expected_induced_subgraph) + + # test mapped_op + mapped_one_three = mm.mapped_op(cirq.CNOT(q[1], q[3])) + assert mapped_one_three.qubits == (cirq.NamedQubit("a"), cirq.NamedQubit("b")) + # adjacent qubits have distance 1 and are thus executable assert mm.dist_on_device(q[1], q[3]) == 1 assert mm.can_execute(cirq.CNOT(q[1], q[3])) @@ -52,25 +68,33 @@ def test_mapping_manager(): assert mm.can_execute(cirq.CNOT(q[1], q[2])) assert mm.dist_on_device(q[1], q[3]) == 2 assert mm.can_execute(cirq.CNOT(q[1], q[3])) is False - # the swapped qubits are still executable assert mm.can_execute(cirq.CNOT(q[2], q[3])) - # distance between other qubits doesn't change assert mm.dist_on_device(q[1], q[4]) == 3 - # test applying swaps to inverse map is correct - mm.inverse_map == {v:k for k,v in mm.map.items()} + assert mm.inverse_map == {v: k for k, v in mm.map.items()} + # test mapped_op after switching qubits + mapped_one_two = mm.mapped_op(cirq.CNOT(q[1], q[2])) + assert mapped_one_two.qubits == (cirq.NamedQubit("a"), cirq.NamedQubit("b")) # apply same swap and test shortest path for a couple pairs mm.apply_swap(q[3], q[2]) - assert mm.shortest_path(q[1], q[2]) == [cirq.NamedQubit("a"), cirq.NamedQubit("b"), cirq.NamedQubit("c")] + assert mm.shortest_path(q[1], q[2]) == [ + cirq.NamedQubit("a"), + cirq.NamedQubit("b"), + cirq.NamedQubit("c"), + ] assert mm.shortest_path(q[2], q[3]) == [cirq.NamedQubit("c"), cirq.NamedQubit("b")] assert mm.shortest_path(q[1], q[3]) == [cirq.NamedQubit("a"), cirq.NamedQubit("b")] - shortest_one_to_four = [cirq.NamedQubit("a"), cirq.NamedQubit("b"), cirq.NamedQubit("c"), cirq.NamedQubit("d")] + shortest_one_to_four = [ + cirq.NamedQubit("a"), + cirq.NamedQubit("b"), + cirq.NamedQubit("c"), + cirq.NamedQubit("d"), + ] assert mm.shortest_path(q[1], q[4]) == shortest_one_to_four # shortest path on symmetric qubit reverses the list assert mm.shortest_path(q[4], q[1]) == shortest_one_to_four[::-1] - From cce41a0d65436aaa94ae18d076cd83d9fdd24201 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Thu, 11 Aug 2022 13:00:52 -0700 Subject: [PATCH 03/11] addressed first round of comments --- .../transformers/routing/mapping_manager.py | 53 ++++++---- .../routing/mapping_manager_test.py | 98 ++++++++++++++----- 2 files changed, 108 insertions(+), 43 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index a6cf06a9efb..258f3ba466b 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -12,29 +12,30 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Dict, TYPE_CHECKING +from typing import Dict, Sequence, TYPE_CHECKING +from cirq._compat import cached_method import networkx as nx -from cirq import ops, protocols +from cirq import protocols if TYPE_CHECKING: import cirq class MappingManager: - """Class that keeps track of the mapping of logical to physical qubits and provides - convenience methods for distance queries on the physical qubits. + """Class that keeps track of the mapping of logical to physical qubits. - Qubit variables with the characer 'p' preppended to them are physical and qubits with the - character 'l' preppended to them are logical qubits. + Convenience methods that over distance and mapping queries of the physical qubits are also + provided. All such public methods of this class expect logical qubits. """ - def __init__(self, device_graph: nx.Graph, initial_mapping: Dict[ops.Qid, ops.Qid]) -> None: + def __init__( + self, device_graph: nx.Graph, initial_mapping: Dict['cirq.Qid', 'cirq.Qid'] + ) -> None: """Initializes MappingManager. Args: device_graph: connectivity graph of qubits in the hardware device. - circuit_graph: connectivity graph of the qubits in the input circuit. initial_mapping: the initial mapping of logical (keys) to physical qubits (values). """ self.device_graph = device_graph @@ -44,23 +45,22 @@ def __init__(self, device_graph: nx.Graph, initial_mapping: Dict[ops.Qid, ops.Qi self._shortest_paths_matrix = dict(nx.all_pairs_shortest_path(self._induced_subgraph)) @property - def map(self) -> Dict[ops.Qid, ops.Qid]: + def map(self) -> Dict['cirq.Qid', 'cirq.Qid']: """The mapping of logical qubits (keys) to physical qubits (values).""" return self._map @property - def inverse_map(self) -> Dict[ops.Qid, ops.Qid]: + def inverse_map(self) -> Dict['cirq.Qid', 'cirq.Qid']: """The mapping of physical qubits (keys) to logical qubits (values).""" return self._inverse_map @property def induced_subgraph(self) -> nx.Graph: - """The device_graph induced on the physical qubits that are mapped to.""" + """The induced subgraph on the set of physical qubits which are part of `self.map`.""" return self._induced_subgraph - def dist_on_device(self, lq1: ops.Qid, lq2: ops.Qid) -> int: - """Finds shortest path distance path between the corresponding physical qubits for logical - qubits q1 and q2 on the device. + def dist_on_device(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> int: + """Finds distance between logical qubits q1 and q2 on the device. Args: lq1: the first logical qubit. @@ -69,9 +69,13 @@ def dist_on_device(self, lq1: ops.Qid, lq2: ops.Qid) -> int: Returns: The shortest path distance. """ - return len(self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]]) - 1 + return self._physical_dist_on_device(self._map[lq1], self._map[lq2]) - def can_execute(self, op: ops.Operation) -> bool: + @cached_method + def _physical_dist_on_device(self, pq1: 'cirq.Qid', pq2: 'cirq.Qid'): + return len(nx.shortest_path(self._induced_subgraph, pq1, pq2)) - 1 + + def can_execute(self, op: 'cirq.Operation') -> bool: """Finds whether the given operation can be executed on the device. Args: @@ -82,23 +86,30 @@ def can_execute(self, op: ops.Operation) -> bool: """ return protocols.num_qubits(op) < 2 or self.dist_on_device(*op.qubits) == 1 - def apply_swap(self, lq1: ops.Qid, lq2: ops.Qid) -> None: + def apply_swap(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> None: """Swaps two logical qubits in the map and in the inverse map. Args: lq1: the first logical qubit. lq2: the second logical qubit. + + Raises: + ValueError: whenever lq1 and lq2 are no adjacent on the device. """ + if self.dist_on_device(lq1, lq2) > 1: + raise ValueError( + f"q1: {lq1} and q2: {lq2} are not adjacent on the device. Cannot swap them." + ) + + pq1, pq2 = self._map[lq1], self._map[lq2] self._map[lq1], self._map[lq2] = self._map[lq2], self._map[lq1] - pq1 = self._map[lq1] - pq2 = self._map[lq2] self._inverse_map[pq1], self._inverse_map[pq2] = ( self._inverse_map[pq2], self._inverse_map[pq1], ) - def mapped_op(self, op: ops.Operation) -> ops.Operation: + def mapped_op(self, op: 'cirq.Operation') -> 'cirq.Operation': """Transforms the given operation with the qubits in self._map. Args: @@ -108,6 +119,6 @@ def mapped_op(self, op: ops.Operation) -> ops.Operation: The same operation on corresponding physical qubits.""" return op.transform_qubits(self._map) - def shortest_path(self, lq1: ops.Qid, lq2: ops.Qid): + def shortest_path(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> Sequence['cirq.Qid']: """Find that shortest path between two logical qubits on the device given their mapping.""" return self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]] diff --git a/cirq-core/cirq/transformers/routing/mapping_manager_test.py b/cirq-core/cirq/transformers/routing/mapping_manager_test.py index 6865978bb88..1844258eb39 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager_test.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager_test.py @@ -12,13 +12,14 @@ # See the License for the specific language governing permissions and # limitations under the License. -import networkx as nx from networkx.utils.misc import graphs_equal +import pytest +import networkx as nx import cirq -def test_mapping_manager(): +def construct_device_graph_and_mapping(): device_graph = nx.Graph( [ (cirq.NamedQubit("a"), cirq.NamedQubit("b")), @@ -35,9 +36,13 @@ def test_mapping_manager(): q[2]: cirq.NamedQubit("c"), q[4]: cirq.NamedQubit("d"), } + return device_graph, initial_mapping, q + + +def test_induced_subgraph(): + device_graph, initial_mapping, _ = construct_device_graph_and_mapping() mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) - # test correct induced subgraph expected_induced_subgraph = nx.Graph( [ (cirq.NamedQubit("a"), cirq.NamedQubit("b")), @@ -47,9 +52,37 @@ def test_mapping_manager(): ) assert graphs_equal(mm.induced_subgraph, expected_induced_subgraph) - # test mapped_op - mapped_one_three = mm.mapped_op(cirq.CNOT(q[1], q[3])) - assert mapped_one_three.qubits == (cirq.NamedQubit("a"), cirq.NamedQubit("b")) + +def test_mapped_op(): + device_graph, initial_mapping, q = construct_device_graph_and_mapping() + mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) + + assert mm.mapped_op(cirq.CNOT(q[1], q[3])).qubits == ( + cirq.NamedQubit("a"), + cirq.NamedQubit("b"), + ) + # does not fail if qubits non-adjacent + assert mm.mapped_op(cirq.CNOT(q[3], q[4])).qubits == ( + cirq.NamedQubit("b"), + cirq.NamedQubit("d"), + ) + + # correctly changes mapped qubits when swapped + mm.apply_swap(q[2], q[3]) + assert mm.mapped_op(cirq.CNOT(q[1], q[2])).qubits == ( + cirq.NamedQubit("a"), + cirq.NamedQubit("b"), + ) + # does not fial if qubits non-adjacent + assert mm.mapped_op(cirq.CNOT(q[1], q[3])).qubits == ( + cirq.NamedQubit("a"), + cirq.NamedQubit("c"), + ) + + +def test_distance_on_device_and_can_execute(): + device_graph, initial_mapping, q = construct_device_graph_and_mapping() + mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) # adjacent qubits have distance 1 and are thus executable assert mm.dist_on_device(q[1], q[3]) == 1 @@ -62,32 +95,48 @@ def test_mapping_manager(): # 'dist_on_device' does not use cirq.NamedQubit("e") to find shorter shortest path assert mm.dist_on_device(q[1], q[4]) == 3 - # after swapping q[2] and q[3], qubits adjacent to q[2] are now adjacent to q[3] and vice-versa - mm.apply_swap(q[3], q[2]) - assert mm.dist_on_device(q[1], q[2]) == 1 - assert mm.can_execute(cirq.CNOT(q[1], q[2])) + # distance changes after applying swap + mm.apply_swap(q[2], q[3]) assert mm.dist_on_device(q[1], q[3]) == 2 assert mm.can_execute(cirq.CNOT(q[1], q[3])) is False - # the swapped qubits are still executable - assert mm.can_execute(cirq.CNOT(q[2], q[3])) + assert mm.dist_on_device(q[1], q[2]) == 1 + assert mm.can_execute(cirq.CNOT(q[1], q[2])) + # distance between other qubits doesn't change assert mm.dist_on_device(q[1], q[4]) == 3 - # test applying swaps to inverse map is correct + + +def test_apply_swap(): + device_graph, initial_mapping, q = construct_device_graph_and_mapping() + mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) + + # swapping non-adjacent qubits raises error + with pytest.raises(ValueError): + mm.apply_swap(q[1], q[2]) + + # applying swap on same qubit does nothing + map_before_swap = mm.map.copy() + mm.apply_swap(q[1], q[1]) + assert map_before_swap == mm.map + + # applying same swap twice does nothing + mm.apply_swap(q[1], q[3]) + mm.apply_swap(q[1], q[3]) + assert map_before_swap == mm.map + + # qubits in inverse map get swapped correctly assert mm.inverse_map == {v: k for k, v in mm.map.items()} - # test mapped_op after switching qubits - mapped_one_two = mm.mapped_op(cirq.CNOT(q[1], q[2])) - assert mapped_one_two.qubits == (cirq.NamedQubit("a"), cirq.NamedQubit("b")) - # apply same swap and test shortest path for a couple pairs - mm.apply_swap(q[3], q[2]) + +def test_shortest_path(): + device_graph, initial_mapping, q = construct_device_graph_and_mapping() + mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) + assert mm.shortest_path(q[1], q[2]) == [ cirq.NamedQubit("a"), cirq.NamedQubit("b"), cirq.NamedQubit("c"), ] - assert mm.shortest_path(q[2], q[3]) == [cirq.NamedQubit("c"), cirq.NamedQubit("b")] - assert mm.shortest_path(q[1], q[3]) == [cirq.NamedQubit("a"), cirq.NamedQubit("b")] - shortest_one_to_four = [ cirq.NamedQubit("a"), cirq.NamedQubit("b"), @@ -95,6 +144,11 @@ def test_mapping_manager(): cirq.NamedQubit("d"), ] assert mm.shortest_path(q[1], q[4]) == shortest_one_to_four - # shortest path on symmetric qubit reverses the list assert mm.shortest_path(q[4], q[1]) == shortest_one_to_four[::-1] + + # swapping doesn't change shortest paths involving other qubits + mm.apply_swap(q[3], q[2]) + assert mm.shortest_path(q[1], q[4]) == shortest_one_to_four + # swapping changes shortest paths involving the swapped qubits + assert mm.shortest_path(q[1], q[2]) == [cirq.NamedQubit("a"), cirq.NamedQubit("b")] From 1eef47cd788c16fd421c207541107dfe98845cc3 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Thu, 11 Aug 2022 13:13:20 -0700 Subject: [PATCH 04/11] typo --- cirq-core/cirq/transformers/routing/mapping_manager.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index 258f3ba466b..d6bfc95138f 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -25,8 +25,8 @@ class MappingManager: """Class that keeps track of the mapping of logical to physical qubits. - Convenience methods that over distance and mapping queries of the physical qubits are also - provided. All such public methods of this class expect logical qubits. + Convenience methods over distance and mapping queries of the physical qubits are also provided. + All such public methods of this class expect logical qubits. """ def __init__( From c850d791c1998ca583fecd4af8008c7a48f52a28 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Thu, 11 Aug 2022 14:25:29 -0700 Subject: [PATCH 05/11] remove unused distance matrix --- cirq-core/cirq/transformers/routing/mapping_manager.py | 1 - 1 file changed, 1 deletion(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index d6bfc95138f..06258dc4f95 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -42,7 +42,6 @@ def __init__( self._map = initial_mapping.copy() self._inverse_map = {v: k for k, v in self._map.items()} self._induced_subgraph = nx.induced_subgraph(self.device_graph, self._map.values()) - self._shortest_paths_matrix = dict(nx.all_pairs_shortest_path(self._induced_subgraph)) @property def map(self) -> Dict['cirq.Qid', 'cirq.Qid']: From d945995cb81b76463e654f3c33f55b4d05a543b1 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Thu, 11 Aug 2022 15:09:32 -0700 Subject: [PATCH 06/11] updated shortest_path method --- .../cirq/transformers/routing/mapping_manager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index 06258dc4f95..ca736cab8b7 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -68,11 +68,8 @@ def dist_on_device(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> int: Returns: The shortest path distance. """ - return self._physical_dist_on_device(self._map[lq1], self._map[lq2]) + return len(self._physical_shortest_path(self._map[lq1], self._map[lq2]))-1 - @cached_method - def _physical_dist_on_device(self, pq1: 'cirq.Qid', pq2: 'cirq.Qid'): - return len(nx.shortest_path(self._induced_subgraph, pq1, pq2)) - 1 def can_execute(self, op: 'cirq.Operation') -> bool: """Finds whether the given operation can be executed on the device. @@ -120,4 +117,8 @@ def mapped_op(self, op: 'cirq.Operation') -> 'cirq.Operation': def shortest_path(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> Sequence['cirq.Qid']: """Find that shortest path between two logical qubits on the device given their mapping.""" - return self._shortest_paths_matrix[self._map[lq1]][self._map[lq2]] + return self._physical_shortest_path[self._map[lq1]][self._map[lq2]] + + @cached_method + def _physical_shortest_path(self, pq1: 'cirq.Qid', pq2: 'cirq.Qid') -> Sequence['cirq.Qid']: + return nx.shortest_path(self._induced_subgraph, pq1, pq2) \ No newline at end of file From 84e700f5cb04f76f798b14ecb526d941f971614e Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Thu, 11 Aug 2022 15:21:01 -0700 Subject: [PATCH 07/11] formatting --- cirq-core/cirq/transformers/routing/mapping_manager.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index ca736cab8b7..030bd45cb3b 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -68,8 +68,7 @@ def dist_on_device(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> int: Returns: The shortest path distance. """ - return len(self._physical_shortest_path(self._map[lq1], self._map[lq2]))-1 - + return len(self._physical_shortest_path(self._map[lq1], self._map[lq2])) - 1 def can_execute(self, op: 'cirq.Operation') -> bool: """Finds whether the given operation can be executed on the device. @@ -121,4 +120,4 @@ def shortest_path(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> Sequence['cirq.Qid' @cached_method def _physical_shortest_path(self, pq1: 'cirq.Qid', pq2: 'cirq.Qid') -> Sequence['cirq.Qid']: - return nx.shortest_path(self._induced_subgraph, pq1, pq2) \ No newline at end of file + return nx.shortest_path(self._induced_subgraph, pq1, pq2) From 3df665545b5742cf3c562203397b757f839a7a55 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Thu, 11 Aug 2022 15:49:40 -0700 Subject: [PATCH 08/11] minor bug fix --- cirq-core/cirq/transformers/routing/mapping_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index 030bd45cb3b..4b22ed6776e 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -116,7 +116,7 @@ def mapped_op(self, op: 'cirq.Operation') -> 'cirq.Operation': def shortest_path(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> Sequence['cirq.Qid']: """Find that shortest path between two logical qubits on the device given their mapping.""" - return self._physical_shortest_path[self._map[lq1]][self._map[lq2]] + return self._physical_shortest_path(self._map[lq1], self._map[lq2]) @cached_method def _physical_shortest_path(self, pq1: 'cirq.Qid', pq2: 'cirq.Qid') -> Sequence['cirq.Qid']: From c731a2e07fc9ea05abec612d86296aaaeb596814 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Thu, 11 Aug 2022 17:53:57 -0700 Subject: [PATCH 09/11] made changes to docstring --- .../cirq/transformers/routing/mapping_manager.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index 4b22ed6776e..dffd58bf230 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -59,7 +59,7 @@ def induced_subgraph(self) -> nx.Graph: return self._induced_subgraph def dist_on_device(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> int: - """Finds distance between logical qubits q1 and q2 on the device. + """Finds distance between logical qubits 'lq1' and 'lq2' on the device. Args: lq1: the first logical qubit. @@ -71,18 +71,19 @@ def dist_on_device(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> int: return len(self._physical_shortest_path(self._map[lq1], self._map[lq2])) - 1 def can_execute(self, op: 'cirq.Operation') -> bool: - """Finds whether the given operation can be executed on the device. + """Finds whether the given operation acts on qubits that are adjacent on the device. Args: op: an operation on logical qubits. Returns: - Whether the given operation is executable on the device. + True, if physical qubits corresponding to logical qubits `op.qubits` are adjacent on + the device. """ return protocols.num_qubits(op) < 2 or self.dist_on_device(*op.qubits) == 1 def apply_swap(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> None: - """Swaps two logical qubits in the map and in the inverse map. + """Updates the mapping to simulate inserting a swap operation between `lq1` and `lq2`. Args: lq1: the first logical qubit. @@ -115,7 +116,7 @@ def mapped_op(self, op: 'cirq.Operation') -> 'cirq.Operation': return op.transform_qubits(self._map) def shortest_path(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> Sequence['cirq.Qid']: - """Find that shortest path between two logical qubits on the device given their mapping.""" + """Find the shortest path between two logical qubits on the device given their mapping.""" return self._physical_shortest_path(self._map[lq1], self._map[lq2]) @cached_method From 5ef5315887b265ce9968d8ce1d4bb3ad206113e8 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani Date: Fri, 12 Aug 2022 13:37:58 -0700 Subject: [PATCH 10/11] minor docstring fixes; shortest_path() now returns logical qubits instead of physical qubits --- .../transformers/routing/mapping_manager.py | 17 ++++++++++--- .../routing/mapping_manager_test.py | 25 ++++++------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index dffd58bf230..e3a92693045 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Manages the mapping from logical to physical qubits during a routing procedure.""" + from typing import Dict, Sequence, TYPE_CHECKING from cirq._compat import cached_method import networkx as nx @@ -23,7 +25,7 @@ class MappingManager: - """Class that keeps track of the mapping of logical to physical qubits. + """Class that manages the mapping from logical to physical qubits. Convenience methods over distance and mapping queries of the physical qubits are also provided. All such public methods of this class expect logical qubits. @@ -116,8 +118,17 @@ def mapped_op(self, op: 'cirq.Operation') -> 'cirq.Operation': return op.transform_qubits(self._map) def shortest_path(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> Sequence['cirq.Qid']: - """Find the shortest path between two logical qubits on the device given their mapping.""" - return self._physical_shortest_path(self._map[lq1], self._map[lq2]) + """Find the shortest path between two logical qubits on the device given their mapping. + + Args: + lq1: the first logical qubit. + lq2: the second logical qubit. + + Returns: + a sequence of logical qubits on the shortest path from lq1 to lq2. + """ + physical_shortest_path = self._physical_shortest_path(self._map[lq1], self._map[lq2]) + return [self._inverse_map[pq] for pq in physical_shortest_path] @cached_method def _physical_shortest_path(self, pq1: 'cirq.Qid', pq2: 'cirq.Qid') -> Sequence['cirq.Qid']: diff --git a/cirq-core/cirq/transformers/routing/mapping_manager_test.py b/cirq-core/cirq/transformers/routing/mapping_manager_test.py index 1844258eb39..2211458e28d 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager_test.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager_test.py @@ -132,23 +132,14 @@ def test_shortest_path(): device_graph, initial_mapping, q = construct_device_graph_and_mapping() mm = cirq.transformers.routing.MappingManager(device_graph, initial_mapping) - assert mm.shortest_path(q[1], q[2]) == [ - cirq.NamedQubit("a"), - cirq.NamedQubit("b"), - cirq.NamedQubit("c"), - ] - shortest_one_to_four = [ - cirq.NamedQubit("a"), - cirq.NamedQubit("b"), - cirq.NamedQubit("c"), - cirq.NamedQubit("d"), - ] - assert mm.shortest_path(q[1], q[4]) == shortest_one_to_four + one_to_four = [q[1], q[3], q[2], q[4]] + assert mm.shortest_path(q[1], q[2]) == one_to_four[:3] + assert mm.shortest_path(q[1], q[4]) == one_to_four # shortest path on symmetric qubit reverses the list - assert mm.shortest_path(q[4], q[1]) == shortest_one_to_four[::-1] + assert mm.shortest_path(q[4], q[1]) == one_to_four[::-1] - # swapping doesn't change shortest paths involving other qubits - mm.apply_swap(q[3], q[2]) - assert mm.shortest_path(q[1], q[4]) == shortest_one_to_four # swapping changes shortest paths involving the swapped qubits - assert mm.shortest_path(q[1], q[2]) == [cirq.NamedQubit("a"), cirq.NamedQubit("b")] + mm.apply_swap(q[3], q[2]) + one_to_four[1], one_to_four[2] = one_to_four[2], one_to_four[1] + assert mm.shortest_path(q[1], q[4]) == one_to_four + assert mm.shortest_path(q[1], q[2]) == [q[1], q[2]] From 6de28fc0347a7aab702da06562fc9d1c2fcbcd03 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <52700536+ammareltigani@users.noreply.github.com> Date: Fri, 12 Aug 2022 13:46:03 -0700 Subject: [PATCH 11/11] nit Co-authored-by: Tanuj Khattar --- cirq-core/cirq/transformers/routing/mapping_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index e3a92693045..96fe34d35ce 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -125,7 +125,7 @@ def shortest_path(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> Sequence['cirq.Qid' lq2: the second logical qubit. Returns: - a sequence of logical qubits on the shortest path from lq1 to lq2. + A sequence of logical qubits on the shortest path from lq1 to lq2. """ physical_shortest_path = self._physical_shortest_path(self._map[lq1], self._map[lq2]) return [self._inverse_map[pq] for pq in physical_shortest_path]