From 504421635df355e271bd9176b4d7d02dd7aa506a Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Wed, 10 Aug 2022 15:25:18 -0700 Subject: [PATCH 01/20] 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 <ammartigani12@gmail.com> Date: Wed, 10 Aug 2022 16:31:36 -0700 Subject: [PATCH 02/20] 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 <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 13:00:52 -0700 Subject: [PATCH 03/20] 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 <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 13:13:20 -0700 Subject: [PATCH 04/20] 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 <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 14:25:29 -0700 Subject: [PATCH 05/20] 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 <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 15:09:32 -0700 Subject: [PATCH 06/20] 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 d86dee36dcb77575c9de68995da87910413de028 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 15:14:44 -0700 Subject: [PATCH 07/20] formatting --- .../cirq/transformers/routing/__init__.py | 2 + .../transformers/routing/initial_mapper.py | 30 ++++++++ .../routing/line_initial_mapper.py | 70 +++++++++++++++++++ .../routing/line_initial_mapper_test.py | 13 ++++ .../transformers/routing/mapping_manager.py | 5 +- 5 files changed, 117 insertions(+), 3 deletions(-) create mode 100644 cirq-core/cirq/transformers/routing/initial_mapper.py create mode 100644 cirq-core/cirq/transformers/routing/line_initial_mapper.py create mode 100644 cirq-core/cirq/transformers/routing/line_initial_mapper_test.py diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index 94f8787cec3..7fc7ed850f5 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -15,3 +15,5 @@ """Routing utilities in Cirq.""" from cirq.transformers.routing.mapping_manager import MappingManager +from cirq.transformers.routing.initial_mapper import AbstractInitialMapper +from cirq.transformers.routing.line_initial_mapper import LineInitialMapper diff --git a/cirq-core/cirq/transformers/routing/initial_mapper.py b/cirq-core/cirq/transformers/routing/initial_mapper.py new file mode 100644 index 00000000000..8d862ab1e92 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/initial_mapper.py @@ -0,0 +1,30 @@ +# 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. + +from typing import TYPE_CHECKING, Dict +import abc + +if TYPE_CHECKING: + import cirq + + +class AbstractInitialMapper(metaclass=abc.ABCMeta): + """Base class for creating custom initial mapping strategies.""" + @abc.abstractmethod + def initial_mapping(self) -> Dict['cirq.Qid', 'cirq.Qid']: + """Gets the initial mapping of logical qubits onto physical qubits. + + Returns: + qubit_map: the initial mapping from logical to physical qubits + """ diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper.py b/cirq-core/cirq/transformers/routing/line_initial_mapper.py new file mode 100644 index 00000000000..72756812699 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper.py @@ -0,0 +1,70 @@ +# 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. + +from typing import Dict, TYPE_CHECKING +import networkx as nx + +from cirq import circuits, routing + + +class LineInitialMapper(routing.AbstractInitialMapper): + """Maps disjoint lines of logical qubits onto lines of physical qubits.""" + + def __init__( + self, + device_graph: nx.Graph, + circuit: circuits.AbstractCircuit, + ): + """Initializes a LineInitialMapper. + + Args: + device_graph: device graph + circuit_graph: circuit graph + """ + self.device_graph = device_graph + self.circuit = circuit + self.circuit_graph = self._make_circuit_graph() + self._map = None + + def _make_circuit_graph(self) -> nx.Graph: + """Creates a (potentially incomplete) qubit connectivity graph of the circuit. + + Iterates over the moments circuit from left to right drawing edges between logical qubits + that: + (1) have degree < 2, and + (2) that are involved in a 2-qubit operation in the current moment. + At this point the graph is forest of paths and/or simple cycles. For each simple cycle, make + it a path by removing the last edge that was added to it. + + Returns: + The qubit connectivity graph of the circuit. + """ + circuit_graph = nx.Graph() + edge_order, node_order = 0, 0 + for op in self.circuit.all_operations(): + circuit_graph.add_nodes_from(op.qubits, node_order=node_order) + node_order += 1 + if len(op.qubits) == 2 and all(circuit_graph.degree[op.qubits[i]] < 2 for i in range(2)): + circuit_graph.add_edge(*op.qubits, edge_order=edge_order) + edge_order += 1 + # make cycles into paths by removing last edge that was added + found = True + while found: + try: + cycle = nx.find_cycle(circuit_graph) + edge_to_remove = max(cycle, key= lambda x: circuit_graph.edges[x[0], x[1]]['edge_order']) + circuit_graph.remove_edge(*edge_to_remove) + except nx.exception.NetworkXNoCycle: + found = False + return circuit_graph diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py new file mode 100644 index 00000000000..c5fa2e3cdf4 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py @@ -0,0 +1,13 @@ +# 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. \ 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 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 84e700f5cb04f76f798b14ecb526d941f971614e Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 15:21:01 -0700 Subject: [PATCH 08/20] 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 0514707260fc471dbcfe5d05d79f9503fd9a6830 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 15:48:15 -0700 Subject: [PATCH 09/20] strated line initial mapper --- .../transformers/routing/initial_mapper.py | 3 ++- .../routing/line_initial_mapper.py | 18 +++++++++--------- .../routing/line_initial_mapper_test.py | 2 +- .../transformers/routing/mapping_manager.py | 2 +- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/initial_mapper.py b/cirq-core/cirq/transformers/routing/initial_mapper.py index 8d862ab1e92..e7a82825520 100644 --- a/cirq-core/cirq/transformers/routing/initial_mapper.py +++ b/cirq-core/cirq/transformers/routing/initial_mapper.py @@ -21,10 +21,11 @@ class AbstractInitialMapper(metaclass=abc.ABCMeta): """Base class for creating custom initial mapping strategies.""" + @abc.abstractmethod def initial_mapping(self) -> Dict['cirq.Qid', 'cirq.Qid']: """Gets the initial mapping of logical qubits onto physical qubits. - + Returns: qubit_map: the initial mapping from logical to physical qubits """ diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper.py b/cirq-core/cirq/transformers/routing/line_initial_mapper.py index 72756812699..cebc3e421f2 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper.py @@ -21,13 +21,9 @@ class LineInitialMapper(routing.AbstractInitialMapper): """Maps disjoint lines of logical qubits onto lines of physical qubits.""" - def __init__( - self, - device_graph: nx.Graph, - circuit: circuits.AbstractCircuit, - ): + def __init__(self, device_graph: nx.Graph, circuit: circuits.AbstractCircuit): """Initializes a LineInitialMapper. - + Args: device_graph: device graph circuit_graph: circuit graph @@ -40,7 +36,7 @@ def __init__( def _make_circuit_graph(self) -> nx.Graph: """Creates a (potentially incomplete) qubit connectivity graph of the circuit. - Iterates over the moments circuit from left to right drawing edges between logical qubits + Iterates over the moments circuit from left to right drawing edges between logical qubits that: (1) have degree < 2, and (2) that are involved in a 2-qubit operation in the current moment. @@ -55,7 +51,9 @@ def _make_circuit_graph(self) -> nx.Graph: for op in self.circuit.all_operations(): circuit_graph.add_nodes_from(op.qubits, node_order=node_order) node_order += 1 - if len(op.qubits) == 2 and all(circuit_graph.degree[op.qubits[i]] < 2 for i in range(2)): + if len(op.qubits) == 2 and all( + circuit_graph.degree[op.qubits[i]] < 2 for i in range(2) + ): circuit_graph.add_edge(*op.qubits, edge_order=edge_order) edge_order += 1 # make cycles into paths by removing last edge that was added @@ -63,7 +61,9 @@ def _make_circuit_graph(self) -> nx.Graph: while found: try: cycle = nx.find_cycle(circuit_graph) - edge_to_remove = max(cycle, key= lambda x: circuit_graph.edges[x[0], x[1]]['edge_order']) + edge_to_remove = max( + cycle, key=lambda x: circuit_graph.edges[x[0], x[1]]['edge_order'] + ) circuit_graph.remove_edge(*edge_to_remove) except nx.exception.NetworkXNoCycle: found = False diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py index c5fa2e3cdf4..7ec98eeb769 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py @@ -10,4 +10,4 @@ # 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. \ No newline at end of file +# limitations under the License. diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index 030bd45cb3b..703e48baa68 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 3df665545b5742cf3c562203397b757f839a7a55 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 15:49:40 -0700 Subject: [PATCH 10/20] 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 0de58d84860e254bd42f88cdc361f7c9760edd0e Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 17:23:37 -0700 Subject: [PATCH 11/20] cleanup up --- cirq-core/cirq/transformers/routing/line_initial_mapper.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper.py b/cirq-core/cirq/transformers/routing/line_initial_mapper.py index cebc3e421f2..8f0e969da19 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper.py @@ -17,6 +17,9 @@ from cirq import circuits, routing +if TYPE_CHECKING: + import cirq + class LineInitialMapper(routing.AbstractInitialMapper): """Maps disjoint lines of logical qubits onto lines of physical qubits.""" @@ -56,6 +59,7 @@ def _make_circuit_graph(self) -> nx.Graph: ): circuit_graph.add_edge(*op.qubits, edge_order=edge_order) edge_order += 1 + # make cycles into paths by removing last edge that was added found = True while found: @@ -67,4 +71,4 @@ def _make_circuit_graph(self) -> nx.Graph: circuit_graph.remove_edge(*edge_to_remove) except nx.exception.NetworkXNoCycle: found = False - return circuit_graph + return circuit_graph \ No newline at end of file From c731a2e07fc9ea05abec612d86296aaaeb596814 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Thu, 11 Aug 2022 17:53:57 -0700 Subject: [PATCH 12/20] 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 529d1a6f4385910a236ad7ec37f449922845c836 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Fri, 12 Aug 2022 13:05:03 -0700 Subject: [PATCH 13/20] completed line_initial_mapper; needs tests --- .../transformers/routing/initial_mapper.py | 4 +- .../routing/line_initial_mapper.py | 129 ++++++++++++++++-- 2 files changed, 123 insertions(+), 10 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/initial_mapper.py b/cirq-core/cirq/transformers/routing/initial_mapper.py index e7a82825520..fe06bae7337 100644 --- a/cirq-core/cirq/transformers/routing/initial_mapper.py +++ b/cirq-core/cirq/transformers/routing/initial_mapper.py @@ -24,8 +24,8 @@ class AbstractInitialMapper(metaclass=abc.ABCMeta): @abc.abstractmethod def initial_mapping(self) -> Dict['cirq.Qid', 'cirq.Qid']: - """Gets the initial mapping of logical qubits onto physical qubits. + """Maps the logical qubits of a circuit onto physical qubits on a device. Returns: - qubit_map: the initial mapping from logical to physical qubits + qubit_map: the initial mapping of logical qubits to physical qubits. """ diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper.py b/cirq-core/cirq/transformers/routing/line_initial_mapper.py index 8f0e969da19..5e15b121c91 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper.py @@ -12,17 +12,20 @@ # See the License for the specific language governing permissions and # limitations under the License. +"""Concrete implementation of AbstractInitialMapper that places lines of qubits onto the device.""" + from typing import Dict, TYPE_CHECKING import networkx as nx -from cirq import circuits, routing +from cirq import circuits +from cirq.transformers import routing if TYPE_CHECKING: import cirq class LineInitialMapper(routing.AbstractInitialMapper): - """Maps disjoint lines of logical qubits onto lines of physical qubits.""" + """Places logical qubits in the circuit onto physical qubits on the device.""" def __init__(self, device_graph: nx.Graph, circuit: circuits.AbstractCircuit): """Initializes a LineInitialMapper. @@ -34,7 +37,7 @@ def __init__(self, device_graph: nx.Graph, circuit: circuits.AbstractCircuit): self.device_graph = device_graph self.circuit = circuit self.circuit_graph = self._make_circuit_graph() - self._map = None + self._map = {} def _make_circuit_graph(self) -> nx.Graph: """Creates a (potentially incomplete) qubit connectivity graph of the circuit. @@ -50,16 +53,14 @@ def _make_circuit_graph(self) -> nx.Graph: The qubit connectivity graph of the circuit. """ circuit_graph = nx.Graph() - edge_order, node_order = 0, 0 + edge_order = 0 for op in self.circuit.all_operations(): - circuit_graph.add_nodes_from(op.qubits, node_order=node_order) - node_order += 1 + circuit_graph.add_nodes_from(op.qubits) if len(op.qubits) == 2 and all( circuit_graph.degree[op.qubits[i]] < 2 for i in range(2) ): circuit_graph.add_edge(*op.qubits, edge_order=edge_order) edge_order += 1 - # make cycles into paths by removing last edge that was added found = True while found: @@ -71,4 +72,116 @@ def _make_circuit_graph(self) -> nx.Graph: circuit_graph.remove_edge(*edge_to_remove) except nx.exception.NetworkXNoCycle: found = False - return circuit_graph \ No newline at end of file + return circuit_graph + + def initial_mapping(self) -> Dict['cirq.Qid', 'cirq.Qid']: + """Maps disjoint lines of logical qubits onto lines of physical qubits. + + Starting from the center physical qubit on the device, attempts to map disjoint lines of + logical qubits given by the circuit graph onto one long line of physical qubits on the + device, greedily maximizing each physical qubit's degree. + If this mapping cannot be completed as one long line of qubits in the circuit graph mapped + to qubits in the device graph, the line can be split as several line segments and then we: + (i) Map first line segment. + (ii) Find another high degree vertex in G near the center. + (iii) Map the second line segment + (iv) etc. + + Returns: + a dictionary that maps logical qubits in the circuit (keys) to physical qubits on the + device (values). + """ + if len(self._map) is not 0: + return self._map + + physical_center = nx.center(self.device_graph)[0] + + def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid': + # map the first logical qubit to the center physical qubit fof the device graph + if ( + current_physical == physical_center + and self.device_graph.nodes[current_physical]["mapped"] is False + ): + return current_physical + # else greedily map to highest degree neighbor that that is available + sorted_neighbors = sorted( + self.device_graph.neighbors(current_physical), + key=lambda x: self.device_graph.degree(x), + ) + for neighbor in sorted_neighbors: + if self.device_graph.nodes[neighbor]["mapped"] is False: + return neighbor + # if cannot map onto one long line of physical qubits, then break down into multiple + # small lines by finding nearest available qubit to the physical center + return self._closest_unmapped_qubit(physical_center) + + # initialize all nodes in device graph to be unmapped + for x in self.device_graph.nodes: + self.device_graph.nodes[x]["mapped"] = False + + # start by mapping onto the center of the device graph + current_physical = physical_center + for logical_cc in nx.connected_components(self.circuit_graph): + # ignore isolated logical qubits for now, will map them later. + if len(logical_cc) == 1: + continue + + # start by mapping logical line from one of its endpoints. + logical_endpoint = next(q for q in logical_cc if self.circuit_graph.degree(q) == 1) + + current_physical = next_physical(current_physical) + last_logical = None + for current_logical in nx.bfs_successors(self.circuit_graph, source=logical_endpoint): + self.device_graph.nodes[current_physical]["mapped"] = True + self._map[current_logical[0]] = current_physical + current_physical = next_physical(current_physical) + last_logical = current_logical + # have to manually map the last one + self._map[last_logical[1][0]] = current_physical + self.device_graph.nodes[current_physical]["mapped"] = True + + self._map_remaining_qubits() + return self._map + + def _map_remaining_qubits(self) -> None: + """Maps logical qubits in 'self.circuit_graph' that are isolated.""" + # first, map logical qubits that interact in 'self.circuit' but have missing edges in + # 'self.circuit_graph' + for op in self.circuit.all_operations(): + if len(op.qubits) == 2: + q1, q2 = op.qubits + if q1 not in self._map.keys(): + physical = self._closest_unmapped_qubit(self._map[q2]) + self._map[q1] = physical + self.device_graph.nodes[physical]["mapped"] = True + # both cannot be unmapped + elif q2 not in self._map.keys(): + physical = self._closest_unmapped_qubit(self._map[q1]) + self._map[q2] = physical + self.device_graph.nodes[physical]["mapped"] = True + + # then map logical qubits that don't interact with any other logical qubits in + # 'self.circuit' + for isolated_qubit in (q for q in self.circuit_graph.nodes if q not in self._map): + physical = self._closest_unmapped_qubit(self._map[next(iter(self._map))]) + self._map[isolated_qubit] = physical + self.device_graph.nodes[physical]["mapped"] = True + + def _closest_unmapped_qubit(self, source: 'cirq.Qid') -> 'cirq.Qid': + """Finds the closest available neighbor to a physical qubit 'source' on the device. + + Args: + source: a physical qubit on the device. + + Returns: + the closest available physical qubit to 'source'. + + Raises: + ValueError if there are no available qubits left on the device. + + """ + for _, successors in nx.bfs_successors(self.device_graph, source): + for successor in successors: + if self.device_graph.nodes[successor]["mapped"] is False: + return successor + raise ValueError("No available physical qubits left on the device.") From 5ef5315887b265ce9968d8ce1d4bb3ad206113e8 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Fri, 12 Aug 2022 13:37:58 -0700 Subject: [PATCH 14/20] 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 15/20] nit Co-authored-by: Tanuj Khattar <tanujkhattar@google.com> --- 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] From af3dead4f3b663ee36a2fbd64b42cb4bbb4dd3ba Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Fri, 12 Aug 2022 13:47:05 -0700 Subject: [PATCH 16/20] removed some uneccessary comments --- .../routing/line_initial_mapper.py | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper.py b/cirq-core/cirq/transformers/routing/line_initial_mapper.py index 5e15b121c91..4cda3207e60 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper.py @@ -61,7 +61,6 @@ def _make_circuit_graph(self) -> nx.Graph: ): circuit_graph.add_edge(*op.qubits, edge_order=edge_order) edge_order += 1 - # make cycles into paths by removing last edge that was added found = True while found: try: @@ -97,7 +96,7 @@ def initial_mapping(self) -> Dict['cirq.Qid', 'cirq.Qid']: physical_center = nx.center(self.device_graph)[0] def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid': - # map the first logical qubit to the center physical qubit fof the device graph + # map the first logical qubit to the center physical qubit of the device graph if ( current_physical == physical_center and self.device_graph.nodes[current_physical]["mapped"] is False @@ -115,18 +114,15 @@ def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid': # small lines by finding nearest available qubit to the physical center return self._closest_unmapped_qubit(physical_center) - # initialize all nodes in device graph to be unmapped for x in self.device_graph.nodes: self.device_graph.nodes[x]["mapped"] = False - # start by mapping onto the center of the device graph current_physical = physical_center for logical_cc in nx.connected_components(self.circuit_graph): - # ignore isolated logical qubits for now, will map them later. if len(logical_cc) == 1: continue - # start by mapping logical line from one of its endpoints. + # start by mapping a logical line from one of its endpoints. logical_endpoint = next(q for q in logical_cc if self.circuit_graph.degree(q) == 1) current_physical = next_physical(current_physical) @@ -136,7 +132,7 @@ def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid': self._map[current_logical[0]] = current_physical current_physical = next_physical(current_physical) last_logical = current_logical - # have to manually map the last one + # map the last one self._map[last_logical[1][0]] = current_physical self.device_graph.nodes[current_physical]["mapped"] = True @@ -144,9 +140,8 @@ def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid': return self._map def _map_remaining_qubits(self) -> None: - """Maps logical qubits in 'self.circuit_graph' that are isolated.""" - # first, map logical qubits that interact in 'self.circuit' but have missing edges in - # 'self.circuit_graph' + # map logical qubits that interact in 'self.circuit' but have missing edges in the circuit + # graph for op in self.circuit.all_operations(): if len(op.qubits) == 2: q1, q2 = op.qubits @@ -154,14 +149,13 @@ def _map_remaining_qubits(self) -> None: physical = self._closest_unmapped_qubit(self._map[q2]) self._map[q1] = physical self.device_graph.nodes[physical]["mapped"] = True - # both cannot be unmapped + # 'elif' because at least one must be mapped already elif q2 not in self._map.keys(): physical = self._closest_unmapped_qubit(self._map[q1]) self._map[q2] = physical self.device_graph.nodes[physical]["mapped"] = True - # then map logical qubits that don't interact with any other logical qubits in - # 'self.circuit' + # map logical qubits that don't interact with any other logical qubits in the circuit for isolated_qubit in (q for q in self.circuit_graph.nodes if q not in self._map): physical = self._closest_unmapped_qubit(self._map[next(iter(self._map))]) self._map[isolated_qubit] = physical From 3b23a9679142a4b4db0415501a9fafea3ecad2cc Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Fri, 12 Aug 2022 15:37:32 -0700 Subject: [PATCH 17/20] bug fix: maximize neighbor degree instead of minimize; other minor changes --- .../routing/line_initial_mapper.py | 42 ++++++++++--------- 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper.py b/cirq-core/cirq/transformers/routing/line_initial_mapper.py index 4cda3207e60..a31077edede 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper.py @@ -14,7 +14,7 @@ """Concrete implementation of AbstractInitialMapper that places lines of qubits onto the device.""" -from typing import Dict, TYPE_CHECKING +from typing import Dict, Optional, TYPE_CHECKING import networkx as nx from cirq import circuits @@ -37,7 +37,7 @@ def __init__(self, device_graph: nx.Graph, circuit: circuits.AbstractCircuit): self.device_graph = device_graph self.circuit = circuit self.circuit_graph = self._make_circuit_graph() - self._map = {} + self._map: Dict['cirq.Qid', 'cirq.Qid'] = {} def _make_circuit_graph(self) -> nx.Graph: """Creates a (potentially incomplete) qubit connectivity graph of the circuit. @@ -96,45 +96,47 @@ def initial_mapping(self) -> Dict['cirq.Qid', 'cirq.Qid']: physical_center = nx.center(self.device_graph)[0] def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid': - # map the first logical qubit to the center physical qubit of the device graph - if ( - current_physical == physical_center - and self.device_graph.nodes[current_physical]["mapped"] is False - ): + # use current physical if last logical line ended before mapping to it. + if self.device_graph.nodes[current_physical]["mapped"] is False: return current_physical # else greedily map to highest degree neighbor that that is available sorted_neighbors = sorted( self.device_graph.neighbors(current_physical), key=lambda x: self.device_graph.degree(x), ) - for neighbor in sorted_neighbors: + for neighbor in reversed(sorted_neighbors): if self.device_graph.nodes[neighbor]["mapped"] is False: return neighbor # if cannot map onto one long line of physical qubits, then break down into multiple # small lines by finding nearest available qubit to the physical center return self._closest_unmapped_qubit(physical_center) - for x in self.device_graph.nodes: - self.device_graph.nodes[x]["mapped"] = False + def next_logical(current_logical: 'cirq.Qid') -> Optional['cirq.Qid']: + for neighbor in self.circuit_graph.neighbors(current_logical): + if self.circuit_graph.nodes[neighbor]["mapped"] is False: + return neighbor + return None + + for pq in self.device_graph.nodes: + self.device_graph.nodes[pq]["mapped"] = False + for lq in self.circuit_graph.nodes: + self.circuit_graph.nodes[lq]["mapped"] = False current_physical = physical_center for logical_cc in nx.connected_components(self.circuit_graph): if len(logical_cc) == 1: continue + current_physical = next_physical(current_physical) # start by mapping a logical line from one of its endpoints. - logical_endpoint = next(q for q in logical_cc if self.circuit_graph.degree(q) == 1) + current_logical = next(q for q in logical_cc if self.circuit_graph.degree(q) == 1) - current_physical = next_physical(current_physical) - last_logical = None - for current_logical in nx.bfs_successors(self.circuit_graph, source=logical_endpoint): + while current_logical is not None: self.device_graph.nodes[current_physical]["mapped"] = True - self._map[current_logical[0]] = current_physical + self.circuit_graph.nodes[current_logical]["mapped"] = True + self._map[current_logical] = current_physical current_physical = next_physical(current_physical) - last_logical = current_logical - # map the last one - self._map[last_logical[1][0]] = current_physical - self.device_graph.nodes[current_physical]["mapped"] = True + current_logical = next_logical(current_logical) self._map_remaining_qubits() return self._map @@ -171,7 +173,7 @@ def _closest_unmapped_qubit(self, source: 'cirq.Qid') -> 'cirq.Qid': the closest available physical qubit to 'source'. Raises: - ValueError if there are no available qubits left on the device. + ValueError: if there are no available qubits left on the device. """ for _, successors in nx.bfs_successors(self.device_graph, source): From d6fb1ee820ff1da58d42a15bda3afe0b48d8b5e7 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Fri, 12 Aug 2022 18:22:26 -0700 Subject: [PATCH 18/20] added grid-like device for testing; needs bug fix in validate_operation() --- .../cirq/transformers/routing/__init__.py | 4 ++ .../routing/line_initial_mapper_test.py | 9 ++++ .../transformers/routing/testing_devices.py | 51 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 cirq-core/cirq/transformers/routing/testing_devices.py diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index 7fc7ed850f5..d1f1fac03f4 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -17,3 +17,7 @@ from cirq.transformers.routing.mapping_manager import MappingManager from cirq.transformers.routing.initial_mapper import AbstractInitialMapper from cirq.transformers.routing.line_initial_mapper import LineInitialMapper +from cirq.transformers.routing.testing_devices import ( + GridTestingDevice, + construct_grid_device, +) diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py index 7ec98eeb769..5b063ff18a7 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py @@ -11,3 +11,12 @@ # 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. + +from typing import Optional +import networkx as nx + +import cirq +from cirq.transformers.routing import construct_grid_device + +def test_simple(): + device = construct_grid_device(7) diff --git a/cirq-core/cirq/transformers/routing/testing_devices.py b/cirq-core/cirq/transformers/routing/testing_devices.py new file mode 100644 index 00000000000..9db1af4392a --- /dev/null +++ b/cirq-core/cirq/transformers/routing/testing_devices.py @@ -0,0 +1,51 @@ +# 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. + +from typing import Optional +import networkx as nx + +import cirq + + +class GridTestingDevice(cirq.Device): + def __init__(self, metadata: cirq.DeviceMetadata) -> None: + self._metadata = metadata + + @property + def metadata(self) -> Optional[cirq.DeviceMetadata]: + return self._metadata + + def validate_operation(self, operation: 'cirq.Operation') -> None: + for q in operation.qubits: + if q not in self._metadata.qubit_set: + raise ValueError(f'Qubit not on device: {q!r}.') + + #TODO: update if stattement so it doesn't use qubit_pairs + if ( + len(operation.qubits) == 2 + and frozenset(operation.qubits) not in self._metadata.qubit_pairs + ): + raise ValueError(f'Qubit pair is not valid on device: {operation.qubits!r}.') + +def construct_grid_device(d: int) -> GridTestingDevice: + qubits = (cirq.GridQubit(i,j) for i in range(d) for j in range(d)) + + nx_graph = nx.Graph() + row_edges = [(cirq.GridQubit(i,j), cirq.GridQubit(i,j+1)) for i in range(d) for j in range(d-1)] + col_edges = [(cirq.GridQubit(i,j), cirq.GridQubit(i+1,j)) for j in range(d) for i in range(d-1)] + nx_graph.add_edges_from(row_edges) + nx_graph.add_edges_from(col_edges) + + metadata = cirq.DeviceMetadata(qubits, nx_graph) + return GridTestingDevice(metadata) \ No newline at end of file From 8c9f06e103b86ff19eb473298f0a0fb85f9d5a1f Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Sat, 13 Aug 2022 09:19:57 -0700 Subject: [PATCH 19/20] added ring device and implemented operation validation on RoutingTestingDevice --- .../cirq/transformers/routing/__init__.py | 1 + .../routing/line_initial_mapper_test.py | 7 +++++-- .../transformers/routing/testing_devices.py | 19 ++++++++++++++----- 3 files changed, 20 insertions(+), 7 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index d1f1fac03f4..a991f3f1770 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -20,4 +20,5 @@ from cirq.transformers.routing.testing_devices import ( GridTestingDevice, construct_grid_device, + construct_ring_device, ) diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py index 5b063ff18a7..9d1bd15c720 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py @@ -14,9 +14,12 @@ from typing import Optional import networkx as nx +import matplotlib.pyplot as plt import cirq -from cirq.transformers.routing import construct_grid_device +from cirq.transformers.routing import construct_grid_device, construct_ring_device def test_simple(): - device = construct_grid_device(7) + grid_device = construct_grid_device(7) + ring_device = construct_ring_device(49) + diff --git a/cirq-core/cirq/transformers/routing/testing_devices.py b/cirq-core/cirq/transformers/routing/testing_devices.py index 9db1af4392a..f5045baafd5 100644 --- a/cirq-core/cirq/transformers/routing/testing_devices.py +++ b/cirq-core/cirq/transformers/routing/testing_devices.py @@ -18,7 +18,7 @@ import cirq -class GridTestingDevice(cirq.Device): +class RoutingTestingDevice(cirq.Device): def __init__(self, metadata: cirq.DeviceMetadata) -> None: self._metadata = metadata @@ -31,14 +31,14 @@ def validate_operation(self, operation: 'cirq.Operation') -> None: if q not in self._metadata.qubit_set: raise ValueError(f'Qubit not on device: {q!r}.') - #TODO: update if stattement so it doesn't use qubit_pairs if ( len(operation.qubits) == 2 - and frozenset(operation.qubits) not in self._metadata.qubit_pairs + and operation.qubits[0] not in self._metadata.nx_graph[operation.qubits[1]] ): raise ValueError(f'Qubit pair is not valid on device: {operation.qubits!r}.') + -def construct_grid_device(d: int) -> GridTestingDevice: +def construct_grid_device(d: int) -> RoutingTestingDevice: qubits = (cirq.GridQubit(i,j) for i in range(d) for j in range(d)) nx_graph = nx.Graph() @@ -48,4 +48,13 @@ def construct_grid_device(d: int) -> GridTestingDevice: nx_graph.add_edges_from(col_edges) metadata = cirq.DeviceMetadata(qubits, nx_graph) - return GridTestingDevice(metadata) \ No newline at end of file + return RoutingTestingDevice(metadata) + +def construct_ring_device(d: int) -> RoutingTestingDevice: + qubits = cirq.LineQubit.range(d) + nx_graph = nx.Graph() + edges = [(qubits[i % d], qubits[(i+1) % d]) for i in range(d)] + nx_graph.add_edges_from(edges) + + metadata = cirq.DeviceMetadata(qubits, nx_graph) + return RoutingTestingDevice(metadata) \ No newline at end of file From 3eb5761f8e582e11a7811b189f10df485cc6cfe4 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <ammartigani12@gmail.com> Date: Mon, 15 Aug 2022 11:25:09 -0700 Subject: [PATCH 20/20] added simple test for random circuits --- .../cirq/transformers/routing/__init__.py | 2 -- .../routing/line_initial_mapper_test.py | 35 +++++++++++++++---- .../transformers/routing/testing_devices.py | 21 +++++------ 3 files changed, 37 insertions(+), 21 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index a991f3f1770..60b34e7969b 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -18,7 +18,5 @@ from cirq.transformers.routing.initial_mapper import AbstractInitialMapper from cirq.transformers.routing.line_initial_mapper import LineInitialMapper from cirq.transformers.routing.testing_devices import ( - GridTestingDevice, construct_grid_device, - construct_ring_device, ) diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py index 9d1bd15c720..ed09521aee1 100644 --- a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py @@ -12,14 +12,37 @@ # See the License for the specific language governing permissions and # limitations under the License. -from typing import Optional import networkx as nx -import matplotlib.pyplot as plt +import pytest import cirq -from cirq.transformers.routing import construct_grid_device, construct_ring_device +from cirq.transformers.routing import construct_grid_device +from cirq.transformers.routing import LineInitialMapper -def test_simple(): - grid_device = construct_grid_device(7) - ring_device = construct_ring_device(49) + +@pytest.mark.parametrize( + "qubits, n_moments, op_density, random_state", + [(5*size, 10*size, 0.4, seed) for size in range(1,3) for seed in range(20) for density in [0.4, 0.5, 0.6]], +) +def test_random_circuits_grid_device(qubits: int, n_moments: int, op_density: float, random_state: int): + c_orig = cirq.testing.random_circuit( + qubits=qubits, + n_moments=n_moments, + op_density=op_density, + random_state=random_state, + ) + device = construct_grid_device(7) + device_graph = device.metadata.nx_graph + mapper = LineInitialMapper(circuit=c_orig, device_graph=device_graph) + mapping = mapper.initial_mapping() + c_mapped = c_orig.transform_qubits(mapping) + + # all qubits in the input circuit are placed on the device + assert set(mapping.keys()) == set(c_orig.all_qubits()) + + # the first two moments are executable + device.validate_circuit(c_mapped[:2]) + + # the induced graph of the device on the physical qubits in the map is connected + assert nx.is_connected(nx.induced_subgraph(device_graph, mapping.values())) diff --git a/cirq-core/cirq/transformers/routing/testing_devices.py b/cirq-core/cirq/transformers/routing/testing_devices.py index f5045baafd5..701b8ffa90e 100644 --- a/cirq-core/cirq/transformers/routing/testing_devices.py +++ b/cirq-core/cirq/transformers/routing/testing_devices.py @@ -36,25 +36,20 @@ def validate_operation(self, operation: 'cirq.Operation') -> None: and operation.qubits[0] not in self._metadata.nx_graph[operation.qubits[1]] ): raise ValueError(f'Qubit pair is not valid on device: {operation.qubits!r}.') - + def construct_grid_device(d: int) -> RoutingTestingDevice: - qubits = (cirq.GridQubit(i,j) for i in range(d) for j in range(d)) + qubits = (cirq.GridQubit(i, j) for i in range(d) for j in range(d)) nx_graph = nx.Graph() - row_edges = [(cirq.GridQubit(i,j), cirq.GridQubit(i,j+1)) for i in range(d) for j in range(d-1)] - col_edges = [(cirq.GridQubit(i,j), cirq.GridQubit(i+1,j)) for j in range(d) for i in range(d-1)] + row_edges = [ + (cirq.GridQubit(i, j), cirq.GridQubit(i, j + 1)) for i in range(d) for j in range(d - 1) + ] + col_edges = [ + (cirq.GridQubit(i, j), cirq.GridQubit(i + 1, j)) for j in range(d) for i in range(d - 1) + ] nx_graph.add_edges_from(row_edges) nx_graph.add_edges_from(col_edges) metadata = cirq.DeviceMetadata(qubits, nx_graph) return RoutingTestingDevice(metadata) - -def construct_ring_device(d: int) -> RoutingTestingDevice: - qubits = cirq.LineQubit.range(d) - nx_graph = nx.Graph() - edges = [(qubits[i % d], qubits[(i+1) % d]) for i in range(d)] - nx_graph.add_edges_from(edges) - - metadata = cirq.DeviceMetadata(qubits, nx_graph) - return RoutingTestingDevice(metadata) \ No newline at end of file