From 9863290d2e69a5d1d1dcff20c3e515e9e5198226 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Thu, 8 Sep 2022 17:50:03 -0700 Subject: [PATCH 01/12] Add benchmarks for circuit routing --- benchmarks/transformers/__init__.py | 0 benchmarks/transformers/routing.py | 39 +++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 benchmarks/transformers/__init__.py create mode 100644 benchmarks/transformers/routing.py diff --git a/benchmarks/transformers/__init__.py b/benchmarks/transformers/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/benchmarks/transformers/routing.py b/benchmarks/transformers/routing.py new file mode 100644 index 00000000000..72296e0ec2e --- /dev/null +++ b/benchmarks/transformers/routing.py @@ -0,0 +1,39 @@ +# 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 cirq + + +class RouteCQC: + params = [[10, 50, 100], [10, 100, 1000], [0.5], [10]] + param_names = ["qubits", "depth", "op_density", "grid_device_size"] + timeout = 300 # Increase timeout to 5 minutes instead of default 60 seconds. + + def setup(self, qubits: int, depth: int, op_density: float, grid_device_size: int): + gate_domain = {cirq.CNOT: 2, cirq.X: 1} + self.circuit = cirq.testing.random_circuit( + qubits, depth, op_density, gate_domain=gate_domain, random_state=12345 + ) + self.device = cirq.testing.construct_grid_device(grid_device_size, grid_device_size) + self.router = cirq.RouteCQC(self.device.metadata.nx_graph) + + def time_circuit_routing(self, *_): + self.routed_circuit = self.router(self.circuit) + + def track_routed_circuit_depth_ratio(self, *_) -> float: + self.routed_circuit = self.router(self.circuit) + return len(self.routed_circuit) / len(self.circuit) + + def teardown(self, *_): + self.device.validate_circuit(self.routed_circuit) From f64464ad34f9385662358257ed1a16203b5562e1 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Thu, 8 Sep 2022 18:30:04 -0700 Subject: [PATCH 02/12] Add more data points --- benchmarks/transformers/routing.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/benchmarks/transformers/routing.py b/benchmarks/transformers/routing.py index 72296e0ec2e..263fb00d7c3 100644 --- a/benchmarks/transformers/routing.py +++ b/benchmarks/transformers/routing.py @@ -16,9 +16,9 @@ class RouteCQC: - params = [[10, 50, 100], [10, 100, 1000], [0.5], [10]] + params = [[10, 25, 50, 75, 100], [10, 50, 100, 250, 500, 1000], [0.5], [10]] param_names = ["qubits", "depth", "op_density", "grid_device_size"] - timeout = 300 # Increase timeout to 5 minutes instead of default 60 seconds. + timeout = 120 # Increase timeout to 2 minutes instead of default 60 seconds. def setup(self, qubits: int, depth: int, op_density: float, grid_device_size: int): gate_domain = {cirq.CNOT: 2, cirq.X: 1} From 8c914550f652d790cb9231845c2909d4ee9081a5 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Fri, 9 Sep 2022 15:34:56 -0700 Subject: [PATCH 03/12] Increase timeout to 3 minutes --- benchmarks/transformers/routing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/benchmarks/transformers/routing.py b/benchmarks/transformers/routing.py index 263fb00d7c3..f6fbf44baa3 100644 --- a/benchmarks/transformers/routing.py +++ b/benchmarks/transformers/routing.py @@ -18,7 +18,7 @@ class RouteCQC: params = [[10, 25, 50, 75, 100], [10, 50, 100, 250, 500, 1000], [0.5], [10]] param_names = ["qubits", "depth", "op_density", "grid_device_size"] - timeout = 120 # Increase timeout to 2 minutes instead of default 60 seconds. + timeout = 180 # Increase timeout to 3 minutes instead of default 60 seconds. def setup(self, qubits: int, depth: int, op_density: float, grid_device_size: int): gate_domain = {cirq.CNOT: 2, cirq.X: 1} From ea611c7d2722f6d5a6e2cfdc36cb8fa24de8f54a Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Fri, 9 Sep 2022 16:18:46 -0700 Subject: [PATCH 04/12] Speeds up circuit routing by using integers instead of qubits for indexing in hot path --- .../transformers/routing/mapping_manager.py | 206 +++++++++++------- .../routing/mapping_manager_test.py | 145 +++--------- .../transformers/routing/route_circuit_cqc.py | 112 ++++++---- 3 files changed, 224 insertions(+), 239 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index 2b21be25ca4..bb354d0707e 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -14,21 +14,25 @@ """Manages the mapping from logical to physical qubits during a routing procedure.""" -from typing import Dict, Sequence, TYPE_CHECKING +from typing import List, Dict, Sequence, TYPE_CHECKING import networkx as nx -from cirq import protocols, value +import numpy as np if TYPE_CHECKING: import cirq -@value.value_equality class MappingManager: """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. + For efficiency, the mapping manager maps all logical and physical qubits to integers, and + maintains a mapping from logical qubit integers to physical qubit integers. This speedup is + important to avoid qubit hashing in hot-paths like querying distance of two logical qubits + on the device (via `dist_on_device` method). + + All public methods of this class expect logical qubits (or corresponding integers that the + logical qubits are mapped to, via `self.logical_qid_to_int` map). """ def __init__( @@ -36,132 +40,170 @@ def __init__( ) -> None: """Initializes MappingManager. - Sorts the nodes and edges in the device graph to guarantee graph equality. If undirected, - also sorts the nodes within each edge. - Args: device_graph: connectivity graph of qubits in the hardware device. initial_mapping: the initial mapping of logical (keys) to physical qubits (values). """ - # make sure edge insertion order is the same amongst equivalent graphs. - if nx.is_directed(device_graph): - self.device_graph = nx.DiGraph() - self.device_graph.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) - self.device_graph.add_edges_from(sorted(list(device_graph.edges))) - else: - self.device_graph = nx.Graph() - self.device_graph.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) - self.device_graph.add_edges_from( - sorted(list(sorted(edge) for edge in device_graph.edges)) - ) - - 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()) + # Map both logical and physical qubits to integers. + self._logical_qid_to_int = {q: i for i, q in enumerate(sorted(initial_mapping.keys()))} + self._int_to_logical_qid = sorted( + self._logical_qid_to_int.keys(), key=lambda x: self._logical_qid_to_int[x] + ) + self._physical_qid_to_int = {q: i for i, q in enumerate(sorted(initial_mapping.values()))} + self._int_to_physical_qid = sorted( + self._physical_qid_to_int.keys(), key=lambda x: self._physical_qid_to_int[x] + ) + logical_qubits, physical_qubits = ( + zip(*[(k, v) for k, v in initial_mapping.items()]) if initial_mapping else ([], []) + ) + num_qubits = len(logical_qubits) + self._logical_to_physical = np.asarray( + [ + self._physical_qid_to_int[physical_qubits[i]] + for i in sorted( + range(num_qubits), key=lambda x: self._logical_qid_to_int[logical_qubits[x]] + ) + ] + ) + self._physical_to_logical = np.asarray( + [ + self._logical_qid_to_int[logical_qubits[i]] + for i in sorted( + range(num_qubits), key=lambda x: self._physical_qid_to_int[physical_qubits[x]] + ) + ] + ) + # Construct the induced subgraph (on integers) and corresponding distance matrix. + self._induced_subgraph_int = nx.relabel_nodes( + nx.induced_subgraph(device_graph, initial_mapping.values()), + {q: self._physical_qid_to_int[q] for q in initial_mapping.values()}, + ) + # Compute floyd warshall dictionary. self._predecessors, self._distances = nx.floyd_warshall_predecessor_and_distance( - self._induced_subgraph + self._induced_subgraph_int ) @property - def map(self) -> Dict['cirq.Qid', 'cirq.Qid']: - """The mapping of logical qubits (keys) to physical qubits (values).""" - return self._map + def physical_qid_to_int(self) -> Dict['cirq.Qid', int]: + """Mapping of physical qubits, that were part of the initial mapping, to unique integers.""" + return self._physical_qid_to_int + + @property + def int_to_physical_qid(self) -> List['cirq.Qid']: + """Inverse mapping of unique integers to corresponding physical qubits. + + `self.physical_qid_to_int[self.int_to_physical_qid[i]] == i` for each i. + """ + return self._int_to_physical_qid + + @property + def logical_qid_to_int(self) -> Dict['cirq.Qid', int]: + """Mapping of logical qubits, that were part of the initial mapping, to unique integers.""" + return self._logical_qid_to_int @property - def inverse_map(self) -> Dict['cirq.Qid', 'cirq.Qid']: - """The mapping of physical qubits (keys) to logical qubits (values).""" - return self._inverse_map + def int_to_logical_qid(self) -> List['cirq.Qid']: + """Inverse mapping of unique integers to corresponding physical qubits. + + `self.logical_qid_to_int[self.int_to_logical_qid[i]] == i` for each i. + """ + return self._int_to_logical_qid + + @property + def logical_to_physical(self) -> np.ndarray: + """The mapping of logical qubit integers to physical qubit integers. + + Let `lq: cirq.Qid` be a logical qubit. Then the corresponding physical qubit that it + maps to can be obtained by: + `self.int_to_physical_qid[self.logical_to_physical[self.logical_qid_to_int[lq]]]` + """ + return self._logical_to_physical + + @property + def physical_to_logical(self) -> np.ndarray: + """The mapping of physical qubits integers to logical qubits integers. + + Let `pq: cirq.Qid` be a physical qubit. Then the corresponding logical qubit that it + maps to can be obtained by: + `self.int_to_logical_qid[self.physical_to_logical[self.physical_qid_to_int[pq]]]` + """ + return self._physical_to_logical @property - def induced_subgraph(self) -> nx.Graph: - """The induced subgraph on the set of physical qubits which are part of `self.map`.""" - return self._induced_subgraph + def induced_subgraph_int(self) -> nx.Graph: + """Induced subgraph on physical qubit integers present in `self.logical_to_physical`.""" + return self._induced_subgraph_int - def dist_on_device(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> int: + def dist_on_device(self, lq1: int, lq2: int) -> int: """Finds distance between logical qubits 'lq1' and 'lq2' on the device. Args: - lq1: the first logical qubit. - lq2: the second logical qubit. + lq1: integer corresponding to the first logical qubit. + lq2: integer corresponding to the second logical qubit. Returns: The shortest path distance. """ - return self._distances[self._map[lq1]][self._map[lq2]] + return self._distances[self.logical_to_physical[lq1]][self.logical_to_physical[lq2]] - def can_execute(self, op: 'cirq.Operation') -> bool: - """Finds whether the given operation acts on qubits that are adjacent on the device. + def can_execute(self, lq1: int, lq2: int) -> bool: + """Finds whether logical qubits `lq1` and `lq2` are adjacent on the device. Args: - op: an operation on logical qubits. + lq1: integer corresponding to the first logical qubit. + lq2: integer corresponding to the second logical qubit. Returns: - True, if physical qubits corresponding to logical qubits `op.qubits` are adjacent on + True, if physical qubits corresponding to `lq1` and `lq2` are adjacent on the device. """ - return protocols.num_qubits(op) < 2 or self.dist_on_device(*op.qubits) == 1 + return self.dist_on_device(lq1, lq2) == 1 - def apply_swap(self, lq1: 'cirq.Qid', lq2: 'cirq.Qid') -> None: + def apply_swap(self, lq1: int, lq2: int) -> None: """Updates the mapping to simulate inserting a swap operation between `lq1` and `lq2`. Args: - lq1: the first logical qubit. - lq2: the second logical qubit. + lq1: integer corresponding to the first logical qubit. + lq2: integer corresponding to the second logical qubit. Raises: - ValueError: whenever lq1 and lq2 are no adjacent on the device. + ValueError: whenever lq1 and lq2 are not 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] - - self._inverse_map[pq1], self._inverse_map[pq2] = ( - self._inverse_map[pq2], - self._inverse_map[pq1], - ) + pq1, pq2 = self.logical_to_physical[lq1], self.logical_to_physical[lq2] + self._logical_to_physical[[lq1, lq2]] = self._logical_to_physical[[lq2, lq1]] + self._physical_to_logical[[pq1, pq2]] = self._physical_to_logical[[pq2, pq1]] def mapped_op(self, op: 'cirq.Operation') -> 'cirq.Operation': - """Transforms the given operation with the qubits in self._map. + """Transforms the given logical operation to act on corresponding physical qubits. Args: - op: an operation on logical qubits. + op: logical operation acting on logical qubits. Returns: - The same operation on corresponding physical qubits.""" - return op.transform_qubits(self._map) + The same operation acting on corresponding physical qubits. + """ + logical_ints = [self._logical_qid_to_int[q] for q in op.qubits] + physical_ints = self.logical_to_physical[logical_ints] + qubit_map: Dict['cirq.Qid', 'cirq.Qid'] = { + q: self._int_to_physical_qid[physical_ints[i]] for i, q in enumerate(op.qubits) + } + return op.transform_qubits(qubit_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. + def shortest_path(self, lq1: int, lq2: int) -> Sequence[int]: + """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. + lq1: integer corresponding to the first logical qubit. + lq2: integer corresponding to the second logical qubit. Returns: - A sequence of logical qubits on the shortest path from lq1 to lq2. + A sequence of logical qubit integers on the shortest path from `lq1` to `lq2`. """ - return [ - self._inverse_map[pq] - for pq in nx.reconstruct_path(self._map[lq1], self._map[lq2], self._predecessors) + return self.physical_to_logical[ + nx.reconstruct_path(*self.logical_to_physical[[lq1, lq2]], self._predecessors) ] - - def _value_equality_values_(self): - graph_equality = ( - tuple(self.device_graph.nodes), - tuple(self.device_graph.edges), - nx.is_directed(self.device_graph), - ) - map_equality = tuple(sorted(self._map.items())) - return (graph_equality, map_equality) - - def __repr__(self) -> str: - graph_type = type(self.device_graph).__name__ - return ( - f'cirq.MappingManager(' - f'nx.{graph_type}({dict(self.device_graph.adjacency())}),' - f' {self._map})' - ) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager_test.py b/cirq-core/cirq/transformers/routing/mapping_manager_test.py index 87b4696cf42..1ea85a0ab0d 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager_test.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager_test.py @@ -50,12 +50,15 @@ def test_induced_subgraph(): (cirq.NamedQubit("c"), cirq.NamedQubit("d")), ] ) - assert graphs_equal(mm.induced_subgraph, expected_induced_subgraph) + assert graphs_equal( + mm.induced_subgraph_int, nx.relabel_nodes(expected_induced_subgraph, mm.physical_qid_to_int) + ) def test_mapped_op(): device_graph, initial_mapping, q = construct_device_graph_and_mapping() mm = cirq.MappingManager(device_graph, initial_mapping) + q_int = [mm.logical_qid_to_int[q[i]] if q[i] in initial_mapping else -1 for i in range(len(q))] assert mm.mapped_op(cirq.CNOT(q[1], q[3])).qubits == ( cirq.NamedQubit("a"), @@ -68,7 +71,7 @@ def test_mapped_op(): ) # correctly changes mapped qubits when swapped - mm.apply_swap(q[2], q[3]) + mm.apply_swap(q_int[2], q_int[3]) assert mm.mapped_op(cirq.CNOT(q[1], q[2])).qubits == ( cirq.NamedQubit("a"), cirq.NamedQubit("b"), @@ -83,143 +86,67 @@ def test_mapped_op(): def test_distance_on_device_and_can_execute(): device_graph, initial_mapping, q = construct_device_graph_and_mapping() mm = cirq.MappingManager(device_graph, initial_mapping) + q_int = [mm.logical_qid_to_int[q[i]] if q[i] in initial_mapping else -1 for i in range(len(q))] # 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])) + assert mm.dist_on_device(q_int[1], q_int[3]) == 1 + assert mm.can_execute(q_int[1], q_int[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 + assert mm.dist_on_device(q_int[1], q_int[2]) == 2 + assert mm.can_execute(q_int[1], q_int[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 + assert mm.dist_on_device(q_int[1], q_int[4]) == 3 # 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 - assert mm.dist_on_device(q[1], q[2]) == 1 - assert mm.can_execute(cirq.CNOT(q[1], q[2])) + mm.apply_swap(q_int[2], q_int[3]) + assert mm.dist_on_device(q_int[1], q_int[3]) == 2 + assert mm.can_execute(q_int[1], q_int[3]) is False + assert mm.dist_on_device(q_int[1], q_int[2]) == 1 + assert mm.can_execute(q_int[1], q_int[2]) # distance between other qubits doesn't change - assert mm.dist_on_device(q[1], q[4]) == 3 + assert mm.dist_on_device(q_int[1], q_int[4]) == 3 def test_apply_swap(): device_graph, initial_mapping, q = construct_device_graph_and_mapping() mm = cirq.MappingManager(device_graph, initial_mapping) + q_int = [mm.logical_qid_to_int[q[i]] if q[i] in initial_mapping else -1 for i in range(len(q))] # swapping non-adjacent qubits raises error with pytest.raises(ValueError): - mm.apply_swap(q[1], q[2]) + mm.apply_swap(q_int[1], q_int[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 + logical_to_physical_before_swap = mm.logical_to_physical.copy() + mm.apply_swap(q_int[1], q_int[1]) + assert all(logical_to_physical_before_swap == mm.logical_to_physical) # 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 + mm.apply_swap(q_int[1], q_int[3]) + mm.apply_swap(q_int[1], q_int[3]) + assert all(logical_to_physical_before_swap == mm.logical_to_physical) # qubits in inverse map get swapped correctly - assert mm.inverse_map == {v: k for k, v in mm.map.items()} + for i in range(len(mm.logical_to_physical)): + assert mm.logical_to_physical[mm.physical_to_logical[i]] == i + assert mm.physical_to_logical[mm.logical_to_physical[i]] == i def test_shortest_path(): device_graph, initial_mapping, q = construct_device_graph_and_mapping() mm = cirq.MappingManager(device_graph, initial_mapping) - - 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 + q_int = [mm.logical_qid_to_int[q[i]] if q[i] in initial_mapping else -1 for i in range(len(q))] + one_to_four = [q_int[1], q_int[3], q_int[2], q_int[4]] + assert all(mm.shortest_path(q_int[1], q_int[2]) == one_to_four[:3]) + assert all(mm.shortest_path(q_int[1], q_int[4]) == one_to_four) # shortest path on symmetric qubit reverses the list - assert mm.shortest_path(q[4], q[1]) == one_to_four[::-1] + assert all(mm.shortest_path(q_int[4], q_int[1]) == one_to_four[::-1]) # swapping changes shortest paths involving the swapped qubits - mm.apply_swap(q[3], q[2]) + mm.apply_swap(q_int[3], q_int[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]] - - -def test_value_equality(): - equals_tester = cirq.testing.EqualsTester() - device_graph, initial_mapping, q = construct_device_graph_and_mapping() - - mm = cirq.MappingManager(device_graph, initial_mapping) - - # same as 'device_graph' but with different insertion order of edges - diff_edge_order = nx.Graph( - [ - (cirq.NamedQubit("a"), cirq.NamedQubit("b")), - (cirq.NamedQubit("e"), cirq.NamedQubit("d")), - (cirq.NamedQubit("c"), cirq.NamedQubit("d")), - (cirq.NamedQubit("a"), cirq.NamedQubit("e")), - (cirq.NamedQubit("b"), cirq.NamedQubit("c")), - ] - ) - mm_edge_order = cirq.MappingManager(diff_edge_order, initial_mapping) - equals_tester.add_equality_group(mm, mm_edge_order) - - # same as 'device_graph' but with directed edges (DiGraph) - device_digraph = nx.DiGraph( - [ - (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")), - ] - ) - mm_digraph = cirq.MappingManager(device_digraph, initial_mapping) - equals_tester.add_equality_group(mm_digraph) - - # same as 'device_graph' but with an added isolated node - isolated_vertex_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")), - ] - ) - isolated_vertex_graph.add_node(cirq.NamedQubit("z")) - mm = cirq.MappingManager(isolated_vertex_graph, initial_mapping) - equals_tester.add_equality_group(isolated_vertex_graph) - - # mapping manager with same initial graph and initial mapping as 'mm' but with different - # current state - mm_with_swap = cirq.MappingManager(device_graph, initial_mapping) - mm_with_swap.apply_swap(q[1], q[3]) - equals_tester.add_equality_group(mm_with_swap) - - -def test_repr(): - device_graph, initial_mapping, _ = construct_device_graph_and_mapping() - mm = cirq.MappingManager(device_graph, initial_mapping) - cirq.testing.assert_equivalent_repr(mm, setup_code='import cirq\nimport networkx as nx') - - device_digraph = nx.DiGraph( - [ - (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")), - ] - ) - mm_digraph = cirq.MappingManager(device_digraph, initial_mapping) - cirq.testing.assert_equivalent_repr(mm_digraph, setup_code='import cirq\nimport networkx as nx') - - -def test_str(): - device_graph, initial_mapping, _ = construct_device_graph_and_mapping() - mm = cirq.MappingManager(device_graph, initial_mapping) - assert ( - str(mm) - == f'cirq.MappingManager(nx.Graph({dict(device_graph.adjacency())}), {initial_mapping})' - ) + assert all(mm.shortest_path(q_int[1], q_int[4]) == one_to_four) + assert all(mm.shortest_path(q_int[1], q_int[2]) == [q_int[1], q_int[2]]) diff --git a/cirq-core/cirq/transformers/routing/route_circuit_cqc.py b/cirq-core/cirq/transformers/routing/route_circuit_cqc.py index 7a35824e5d1..546e0a4cd54 100644 --- a/cirq-core/cirq/transformers/routing/route_circuit_cqc.py +++ b/cirq-core/cirq/transformers/routing/route_circuit_cqc.py @@ -14,7 +14,7 @@ """Heuristic qubit routing algorithm based on arxiv:1902.08091.""" -from typing import Any, Dict, List, Optional, Set, Tuple, TYPE_CHECKING +from typing import Any, Dict, List, Optional, Set, Sequence, Tuple, TYPE_CHECKING from itertools import combinations import networkx as nx @@ -25,22 +25,21 @@ if TYPE_CHECKING: import cirq -QidPair = Tuple['cirq.Qid', 'cirq.Qid'] +QidIntPair = Tuple[int, int] -def disjoint_nc2_combinations(qubit_pairs: List[QidPair]) -> List[Tuple[QidPair, ...]]: +def disjoint_nc2_combinations( + qubit_pairs: Sequence[QidIntPair], +) -> List[Tuple[QidIntPair, QidIntPair]]: """Gets disjoint pair combinations of qubits pairs. For example: - >>> q = cirq.LineQubit.range(5) + >>> q = [*range(5)] >>> disjoint_swaps = cirq.transformers.routing.route_circuit_cqc.disjoint_nc2_combinations( ... [(q[0], q[1]), (q[2], q[3]), (q[1], q[4])] ... ) - >>> disjoint_swaps == [ - ... ((cirq.LineQubit(0), cirq.LineQubit(1)), (cirq.LineQubit(2), cirq.LineQubit(3))), - ... ((cirq.LineQubit(2), cirq.LineQubit(3)), (cirq.LineQubit(1), cirq.LineQubit(4))) - ... ] + >>> disjoint_swaps == [((q[0], q[1]), (q[2], q[3])), ((q[2], q[3]), (q[1], q[4]))] True Args: @@ -48,7 +47,6 @@ def disjoint_nc2_combinations(qubit_pairs: List[QidPair]) -> List[Tuple[QidPair, Returns: All 2-combinations between qubit pairs that are disjoint. - """ return [pair for pair in combinations(qubit_pairs, 2) if set(pair[0]).isdisjoint(pair[1])] @@ -232,7 +230,10 @@ def route_circuit( return ( circuits.Circuit(circuits.Circuit(m) for m in routed_ops), initial_mapping, - {initial_mapping[k]: v for k, v in mm.map.items()}, + { + initial_mapping[mm.int_to_logical_qid[k]]: mm.int_to_physical_qid[v] + for k, v in enumerate(mm.logical_to_physical) + }, ) @classmethod @@ -288,19 +289,29 @@ def _route( Returns: a list of lists corresponding to timesteps of the routed circuit. """ + two_qubit_ops_ints: List[List[QidIntPair]] = [ + [ + (mm.logical_qid_to_int[op.qubits[0]], mm.logical_qid_to_int[op.qubits[1]]) + for op in timestep_ops + ] + for timestep_ops in two_qubit_ops + ] + routed_ops: List[List['cirq.Operation']] = [] def process_executable_two_qubit_ops(timestep: int) -> int: - unexecutable_ops = [] - for op in two_qubit_ops[timestep]: - if mm.can_execute(op): + unexecutable_ops: List['cirq.Operation'] = [] + unexecutable_ops_ints: List[QidIntPair] = [] + for op, op_ints in zip(two_qubit_ops[timestep], two_qubit_ops_ints[timestep]): + if mm.can_execute(*op_ints): routed_ops[timestep].append(mm.mapped_op(op)) else: unexecutable_ops.append(op) + unexecutable_ops_ints.append(op_ints) two_qubit_ops[timestep] = unexecutable_ops + two_qubit_ops_ints[timestep] = unexecutable_ops_ints return len(unexecutable_ops) strats = [cls._choose_single_swap, cls._choose_pair_of_swaps] - routed_ops: List[List['cirq.Operation']] = [] for timestep in range(len(two_qubit_ops)): # Add single-qubit ops with qubits given by the current mapping. @@ -308,22 +319,24 @@ def process_executable_two_qubit_ops(timestep: int) -> int: # swaps applied in the current timestep thus far. This ensures the same swaps # don't get executed twice in the same timestep. - seen: Set[Tuple[Tuple['cirq.Qid', cirq.Qid], ...]] = set() + seen: Set[Tuple[QidIntPair, ...]] = set() while process_executable_two_qubit_ops(timestep): - chosen_swaps: Optional[Tuple[QidPair, ...]] = None + chosen_swaps: Optional[Tuple[QidIntPair, ...]] = None for strat in strats: - chosen_swaps = strat(mm, two_qubit_ops, timestep, lookahead_radius) + chosen_swaps = strat(mm, two_qubit_ops_ints, timestep, lookahead_radius) if chosen_swaps is not None: break if chosen_swaps is None or chosen_swaps in seen: - chosen_swaps = cls._brute_force_strategy(mm, two_qubit_ops, timestep) + chosen_swaps = cls._brute_force_strategy(mm, two_qubit_ops_ints, timestep) else: seen.add(chosen_swaps) for swap in chosen_swaps: - inserted_swap = mm.mapped_op(ops.SWAP(*swap)) + inserted_swap = mm.mapped_op( + ops.SWAP(mm.int_to_logical_qid[swap[0]], mm.int_to_logical_qid[swap[1]]) + ) if tag_inserted_swaps: inserted_swap = inserted_swap.with_tags(ops.RoutingSwapTag()) routed_ops[timestep].append(inserted_swap) @@ -335,56 +348,58 @@ def process_executable_two_qubit_ops(timestep: int) -> int: def _brute_force_strategy( cls, mm: mapping_manager.MappingManager, - two_qubit_ops: List[List['cirq.Operation']], + two_qubit_ops_ints: Sequence[Sequence[QidIntPair]], timestep: int, - ) -> Tuple[QidPair, ...]: + ) -> Tuple[QidIntPair, ...]: """Inserts SWAPS along the shortest path of the qubits that are the farthest. Since swaps along the shortest path are being executed one after the other, in order to achieve the physical swaps (M[q1], M[q2]), (M[q2], M[q3]), ..., (M[q_{i-1}], M[q_i]), we must execute the logical swaps (q1, q2), (q1, q3), ..., (q_1, qi). """ - furthest_op = max(two_qubit_ops[timestep], key=lambda op: mm.dist_on_device(*op.qubits)) - path = mm.shortest_path(*furthest_op.qubits) + furthest_op = max(two_qubit_ops_ints[timestep], key=lambda op: mm.dist_on_device(*op)) + path = mm.shortest_path(*furthest_op) return tuple([(path[0], path[i + 1]) for i in range(len(path) - 2)]) @classmethod def _choose_pair_of_swaps( cls, mm: mapping_manager.MappingManager, - two_qubit_ops: List[List['cirq.Operation']], + two_qubit_ops_ints: Sequence[Sequence[QidIntPair]], timestep: int, lookahead_radius: int, - ) -> Optional[Tuple[QidPair, ...]]: + ) -> Optional[Tuple[QidIntPair, ...]]: """Computes cost function with pairs of candidate swaps that act on disjoint qubits.""" pair_sigma = disjoint_nc2_combinations( - cls._initial_candidate_swaps(mm, two_qubit_ops[timestep]) + cls._initial_candidate_swaps(mm, two_qubit_ops_ints[timestep]) + ) + return cls._choose_optimal_swap( + mm, two_qubit_ops_ints, timestep, lookahead_radius, pair_sigma ) - return cls._choose_optimal_swap(mm, two_qubit_ops, timestep, lookahead_radius, pair_sigma) @classmethod def _choose_single_swap( cls, mm: mapping_manager.MappingManager, - two_qubit_ops: List[List['cirq.Operation']], + two_qubit_ops_ints: Sequence[Sequence[QidIntPair]], timestep: int, lookahead_radius: int, - ) -> Optional[Tuple[QidPair, ...]]: + ) -> Optional[Tuple[QidIntPair, ...]]: """Computes cost function with list of single candidate swaps.""" - sigma: List[Tuple[QidPair, ...]] = [ - (swap,) for swap in cls._initial_candidate_swaps(mm, two_qubit_ops[timestep]) + sigma: List[Tuple[QidIntPair, ...]] = [ + (swap,) for swap in cls._initial_candidate_swaps(mm, two_qubit_ops_ints[timestep]) ] - return cls._choose_optimal_swap(mm, two_qubit_ops, timestep, lookahead_radius, sigma) + return cls._choose_optimal_swap(mm, two_qubit_ops_ints, timestep, lookahead_radius, sigma) @classmethod def _choose_optimal_swap( cls, mm: mapping_manager.MappingManager, - two_qubit_ops: List[List['cirq.Operation']], + two_qubit_ops_ints: Sequence[Sequence[QidIntPair]], timestep: int, lookahead_radius: int, - sigma: List[Tuple[QidPair, ...]], - ) -> Optional[Tuple[QidPair, ...]]: + sigma: Sequence[Tuple[QidIntPair, ...]], + ) -> Optional[Tuple[QidIntPair, ...]]: """Optionally returns the swap with minimum cost from a list of n-tuple candidate swaps. Computes a cost (as defined by the overridable function `_cost`) for each candidate swap @@ -393,37 +408,39 @@ def _choose_optimal_swap( timestep. Iterate this this looking ahead process up to the next `lookahead_radius` timesteps. If there still doesn't exist a unique swap with minial cost then returns None. """ - for s in range(timestep, min(lookahead_radius + timestep, len(two_qubit_ops))): + for s in range(timestep, min(lookahead_radius + timestep, len(two_qubit_ops_ints))): if len(sigma) <= 1: break costs = {} for swaps in sigma: - costs[swaps] = cls._cost(mm, swaps, two_qubit_ops[s]) + costs[swaps] = cls._cost(mm, swaps, two_qubit_ops_ints[s]) _, min_cost = min(costs.items(), key=lambda x: x[1]) sigma = [swaps for swaps, cost in costs.items() if cost == min_cost] return ( None - if len(sigma) > 1 and timestep + lookahead_radius <= len(two_qubit_ops) + if len(sigma) > 1 and timestep + lookahead_radius <= len(two_qubit_ops_ints) else sigma[0] ) @classmethod def _initial_candidate_swaps( - cls, mm: mapping_manager.MappingManager, two_qubit_ops: List['cirq.Operation'] - ) -> List[QidPair]: + cls, mm: mapping_manager.MappingManager, two_qubit_ops: Sequence[QidIntPair] + ) -> List[QidIntPair]: """Finds all feasible SWAPs between qubits involved in 2-qubit operations.""" - physical_qubits = (mm.map[op.qubits[i]] for op in two_qubit_ops for i in range(2)) - physical_swaps = mm.induced_subgraph.edges(nbunch=physical_qubits) - return [(mm.inverse_map[q1], mm.inverse_map[q2]) for q1, q2 in physical_swaps] + physical_qubits = (mm.logical_to_physical[lq[i]] for lq in two_qubit_ops for i in range(2)) + physical_swaps = mm.induced_subgraph_int.edges(nbunch=physical_qubits) + return [ + (mm.physical_to_logical[q1], mm.physical_to_logical[q2]) for q1, q2 in physical_swaps + ] @classmethod def _cost( cls, mm: mapping_manager.MappingManager, - swaps: Tuple[QidPair, ...], - two_qubit_ops: List['cirq.Operation'], + swaps: Tuple[QidIntPair, ...], + two_qubit_ops: Sequence[QidIntPair], ) -> Any: """Computes the cost function for the given list of swaps over the current timestep ops. @@ -433,9 +450,8 @@ def _cost( for swap in swaps: mm.apply_swap(*swap) max_length, sum_length = 0, 0 - for op in two_qubit_ops: - q1, q2 = op.qubits - dist = mm.dist_on_device(q1, q2) + for lq in two_qubit_ops: + dist = mm.dist_on_device(*lq) max_length = max(max_length, dist) sum_length += dist for swap in swaps: From f640d2ea478c1ba79bfdde8d9c33f0409c780a34 Mon Sep 17 00:00:00 2001 From: Ammar Eltigani <52700536+ammareltigani@users.noreply.github.com> Date: Mon, 12 Sep 2022 18:01:28 -0400 Subject: [PATCH 05/12] Added visualization for qubit permutations for routed circuits (#5848) * added circuit visualization code * made print gate private and added more tests * removed unused import and added gate to private list * addressed comments * added safeguard for wrong use of RoutingSwapTag * addressed nits * added return type for * added _SwapPrintGate again to and fixed type issue --- cirq-core/cirq/__init__.py | 1 + cirq-core/cirq/ops/gate_operation_test.py | 1 + cirq-core/cirq/transformers/__init__.py | 1 + .../cirq/transformers/routing/__init__.py | 1 + .../routing/visualize_routed_circuit.py | 78 ++++++++++++++++ .../routing/visualize_routed_circuit_test.py | 89 +++++++++++++++++++ 6 files changed, 171 insertions(+) create mode 100644 cirq-core/cirq/transformers/routing/visualize_routed_circuit.py create mode 100644 cirq-core/cirq/transformers/routing/visualize_routed_circuit_test.py diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 66284f363d0..5b5491aae12 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -367,6 +367,7 @@ prepare_two_qubit_state_using_cz, prepare_two_qubit_state_using_sqrt_iswap, RouteCQC, + routed_circuit_with_mapping, SqrtIswapTargetGateset, single_qubit_matrix_to_gates, single_qubit_matrix_to_pauli_rotations, diff --git a/cirq-core/cirq/ops/gate_operation_test.py b/cirq-core/cirq/ops/gate_operation_test.py index f2f888c8902..bfd5e1d156d 100644 --- a/cirq-core/cirq/ops/gate_operation_test.py +++ b/cirq-core/cirq/ops/gate_operation_test.py @@ -494,6 +494,7 @@ def all_subclasses(cls): cirq.Pauli, # Private gates. cirq.transformers.analytical_decompositions.two_qubit_to_fsim._BGate, + cirq.transformers.routing.visualize_routed_circuit._SwapPrintGate, cirq.ops.raw_types._InverseCompositeGate, cirq.circuits.qasm_output.QasmTwoQubitGate, cirq.ops.MSGate, diff --git a/cirq-core/cirq/transformers/__init__.py b/cirq-core/cirq/transformers/__init__.py index 20f202945ed..25f8ba35080 100644 --- a/cirq-core/cirq/transformers/__init__.py +++ b/cirq-core/cirq/transformers/__init__.py @@ -50,6 +50,7 @@ LineInitialMapper, MappingManager, RouteCQC, + routed_circuit_with_mapping, ) from cirq.transformers.target_gatesets import ( diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index 2ec3e856d3e..c35f7eb1598 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -18,3 +18,4 @@ from cirq.transformers.routing.mapping_manager import MappingManager from cirq.transformers.routing.line_initial_mapper import LineInitialMapper from cirq.transformers.routing.route_circuit_cqc import RouteCQC +from cirq.transformers.routing.visualize_routed_circuit import routed_circuit_with_mapping diff --git a/cirq-core/cirq/transformers/routing/visualize_routed_circuit.py b/cirq-core/cirq/transformers/routing/visualize_routed_circuit.py new file mode 100644 index 00000000000..05a3ba71323 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/visualize_routed_circuit.py @@ -0,0 +1,78 @@ +# 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, Optional, Tuple, TYPE_CHECKING +from cirq import circuits, ops + +if TYPE_CHECKING: + import cirq + + +class _SwapPrintGate(ops.Gate): + """A gate that displays the string representation of each qubits on the circuit.""" + + def __init__(self, qubits: Tuple[Tuple['cirq.Qid', 'cirq.Qid'], ...]) -> None: + self.qubits = qubits + + def num_qubits(self): + return len(self.qubits) + + def _circuit_diagram_info_(self, args: 'cirq.CircuitDiagramInfoArgs') -> Tuple[str, ...]: + return tuple(f'{str(q[1])}' for q in self.qubits) + + +def routed_circuit_with_mapping( + routed_circuit: 'cirq.AbstractCircuit', + initial_map: Optional[Dict['cirq.Qid', 'cirq.Qid']] = None, +) -> 'cirq.AbstractCircuit': + """Returns the same circuits with information about the permutation of qubits after each swap. + + Args: + routed_circuit: a routed circuit that potentially has inserted swaps tagged with a + RoutingSwapTag. + initial_map: the initial mapping from logical to physical qubits. If this is not specified + then the identity mapping of the qubits in routed_circuit will be used as initial_map. + + Raises: + ValueError: if a non-SWAP gate is tagged with a RoutingSwapTag. + """ + all_qubits = sorted(routed_circuit.all_qubits()) + qdict = {q: q for q in all_qubits} + if initial_map is None: + initial_map = qdict.copy() + inverse_map = {v: k for k, v in initial_map.items()} + + def swap_print_moment() -> 'cirq.Operation': + return _SwapPrintGate( + tuple(zip(qdict.values(), [inverse_map[x] for x in qdict.values()])) + ).on(*all_qubits) + + ret_circuit = circuits.Circuit(swap_print_moment()) + for m in routed_circuit: + swap_in_moment = False + for op in m: + if ops.RoutingSwapTag() in op.tags: + if type(op.gate) != ops.swap_gates.SwapPowGate: + raise ValueError( + "Invalid circuit. A non-SWAP gate cannot be tagged a RoutingSwapTag." + ) + swap_in_moment = True + q1, q2 = op.qubits + qdict[q1], qdict[q2] = qdict[q2], qdict[q1] + + ret_circuit.append(m) + if swap_in_moment: + ret_circuit.append(swap_print_moment()) + + return ret_circuit diff --git a/cirq-core/cirq/transformers/routing/visualize_routed_circuit_test.py b/cirq-core/cirq/transformers/routing/visualize_routed_circuit_test.py new file mode 100644 index 00000000000..7ab797ed10c --- /dev/null +++ b/cirq-core/cirq/transformers/routing/visualize_routed_circuit_test.py @@ -0,0 +1,89 @@ +# 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 pytest +import cirq + + +def test_routed_circuit_with_mapping_simple(): + q = cirq.LineQubit.range(2) + circuit = cirq.Circuit([cirq.Moment(cirq.SWAP(q[0], q[1]).with_tags(cirq.RoutingSwapTag()))]) + expected_diagram = """ +0: ───q(0)───×[cirq.RoutingSwapTag()]───q(1)─── + │ │ │ +1: ───q(1)───×──────────────────────────q(0)───""" + cirq.testing.assert_has_diagram(cirq.routed_circuit_with_mapping(circuit), expected_diagram) + + expected_diagram_with_initial_mapping = """ +0: ───a───×[cirq.RoutingSwapTag()]───b─── + │ │ │ +1: ───b───×──────────────────────────a───""" + cirq.testing.assert_has_diagram( + cirq.routed_circuit_with_mapping( + circuit, {cirq.NamedQubit("a"): q[0], cirq.NamedQubit("b"): q[1]} + ), + expected_diagram_with_initial_mapping, + ) + + # if swap is untagged should not affect the mapping + circuit = cirq.Circuit([cirq.Moment(cirq.SWAP(q[0], q[1]))]) + expected_diagram = """ +0: ───q(0)───×─── + │ │ +1: ───q(1)───×───""" + cirq.testing.assert_has_diagram(cirq.routed_circuit_with_mapping(circuit), expected_diagram) + + circuit = cirq.Circuit( + [ + cirq.Moment(cirq.X(q[0]).with_tags(cirq.RoutingSwapTag())), + cirq.Moment(cirq.SWAP(q[0], q[1])), + ] + ) + with pytest.raises( + ValueError, match="Invalid circuit. A non-SWAP gate cannot be tagged a RoutingSwapTag." + ): + cirq.routed_circuit_with_mapping(circuit) + + +def test_routed_circuit_with_mapping_multi_swaps(): + q = cirq.LineQubit.range(6) + circuit = cirq.Circuit( + [ + cirq.Moment(cirq.CNOT(q[3], q[4])), + cirq.Moment(cirq.CNOT(q[5], q[4]), cirq.CNOT(q[2], q[3])), + cirq.Moment( + cirq.CNOT(q[2], q[1]), cirq.SWAP(q[4], q[3]).with_tags(cirq.RoutingSwapTag()) + ), + cirq.Moment( + cirq.SWAP(q[0], q[1]).with_tags(cirq.RoutingSwapTag()), + cirq.SWAP(q[3], q[2]).with_tags(cirq.RoutingSwapTag()), + ), + cirq.Moment(cirq.CNOT(q[2], q[1])), + cirq.Moment(cirq.CNOT(q[1], q[0])), + ] + ) + expected_diagram = """ +0: ───q(0)──────────────────────────────────────q(0)───×[cirq.RoutingSwapTag()]───q(1)───────X─── + │ │ │ │ │ +1: ───q(1)───────────X──────────────────────────q(1)───×──────────────────────────q(0)───X───@─── + │ │ │ │ │ +2: ───q(2)───────@───@──────────────────────────q(2)───×──────────────────────────q(4)───@─────── + │ │ │ │ │ +3: ───q(3)───@───X───×──────────────────────────q(4)───×[cirq.RoutingSwapTag()]───q(2)─────────── + │ │ │ │ │ +4: ───q(4)───X───X───×[cirq.RoutingSwapTag()]───q(3)──────────────────────────────q(3)─────────── + │ │ │ │ +5: ───q(5)───────@──────────────────────────────q(5)──────────────────────────────q(5)─────────── +""" + cirq.testing.assert_has_diagram(cirq.routed_circuit_with_mapping(circuit), expected_diagram) From e813729f63297dfdac8ac5f6773cf7622cc9e866 Mon Sep 17 00:00:00 2001 From: MichaelBroughton Date: Mon, 12 Sep 2022 16:29:58 -0700 Subject: [PATCH 06/12] Replace pure python loops with numpy where possible in channels.py. (#5839) Boosts speed. --- cirq-core/cirq/qis/channels.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/cirq-core/cirq/qis/channels.py b/cirq-core/cirq/qis/channels.py index 1caaf41ae3a..d0127c2e27f 100644 --- a/cirq-core/cirq/qis/channels.py +++ b/cirq-core/cirq/qis/channels.py @@ -50,11 +50,9 @@ def kraus_to_choi(kraus_operators: Sequence[np.ndarray]) -> np.ndarray: Choi matrix of the channel specified by kraus_operators. """ d = np.prod(kraus_operators[0].shape, dtype=np.int64) - c = np.zeros((d, d), dtype=np.complex128) - for k in kraus_operators: - v = np.reshape(k, d) - c += np.outer(v, v.conj()) - return c + choi_rank = len(kraus_operators) + k = np.reshape(kraus_operators, (choi_rank, d)) + return np.einsum('bi,bj->ij', k, k.conj()) def choi_to_kraus(choi: np.ndarray, atol: float = 1e-10) -> Sequence[np.ndarray]: @@ -105,7 +103,8 @@ def choi_to_kraus(choi: np.ndarray, atol: float = 1e-10) -> Sequence[np.ndarray] w = np.maximum(w, 0) u = np.sqrt(w) * v - return [k.reshape(d, d) for k in u.T if np.linalg.norm(k) > atol] + keep = np.linalg.norm(u.T, axis=-1) > atol + return [k.reshape(d, d) for k, keep_i in zip(u.T, keep) if keep_i] def kraus_to_superoperator(kraus_operators: Sequence[np.ndarray]) -> np.ndarray: @@ -140,10 +139,9 @@ def kraus_to_superoperator(kraus_operators: Sequence[np.ndarray]) -> np.ndarray: Superoperator matrix of the channel specified by kraus_operators. """ d_out, d_in = kraus_operators[0].shape - m = np.zeros((d_out * d_out, d_in * d_in), dtype=np.complex128) - for k in kraus_operators: - m += np.kron(k, k.conj()) - return m + ops_arr = np.asarray(kraus_operators) + m = np.einsum('bij,bkl->ikjl', ops_arr, ops_arr.conj()) + return m.reshape((d_out * d_out, d_in * d_in)) def superoperator_to_kraus(superoperator: np.ndarray, atol: float = 1e-10) -> Sequence[np.ndarray]: From a1695ed94308764dfea1b5a84a63ff2bd69d2604 Mon Sep 17 00:00:00 2001 From: Pavol Juhas Date: Wed, 14 Sep 2022 22:20:02 -0700 Subject: [PATCH 07/12] Tell git to ignore build directories from distutils (#5875) It is sufficient to have the pattern once in the top .gitignore. --- .gitignore | 1 + cirq-web/.gitignore | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index e3d01d87b35..91874362c50 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,7 @@ __pycache__ # packaging *.egg-info/ +build/ # Sphinx _build diff --git a/cirq-web/.gitignore b/cirq-web/.gitignore index 9919c51a1e8..c74db4cf1cf 100644 --- a/cirq-web/.gitignore +++ b/cirq-web/.gitignore @@ -6,7 +6,3 @@ node_modules/ # Coverage testing information .nyc_output/ - -# Extras -build/ - From 24f57d26a00d00076a57306b6af6659fd9e7f4de Mon Sep 17 00:00:00 2001 From: Pavol Juhas Date: Fri, 16 Sep 2022 13:53:18 -0700 Subject: [PATCH 08/12] Fix docstring typos in cirq.devices submodule (#5876) No change in code function. --- cirq-core/cirq/devices/grid_device_metadata.py | 4 ++-- cirq-core/cirq/devices/insertion_noise_model.py | 2 +- cirq-core/cirq/devices/noise_model.py | 2 +- cirq-core/cirq/devices/thermal_noise_model.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/cirq-core/cirq/devices/grid_device_metadata.py b/cirq-core/cirq/devices/grid_device_metadata.py index 70b9d22d9b0..58b49df7f7f 100644 --- a/cirq-core/cirq/devices/grid_device_metadata.py +++ b/cirq-core/cirq/devices/grid_device_metadata.py @@ -71,7 +71,7 @@ def __init__( if a == b: raise ValueError(f"Self loop encountered in qubit {a}") - # Keep lexigraphically smaller tuples for undirected edges. + # Keep lexicographically smaller tuples for undirected edges. edge_set = set() node_set = set() for a, b in sorted_pairs: @@ -133,7 +133,7 @@ def qubit_pairs(self) -> FrozenSet[FrozenSet['cirq.GridQubit']]: @property def isolated_qubits(self) -> FrozenSet['cirq.GridQubit']: - """Returns the set of all isolated qubits on the device (if appliable).""" + """Returns the set of all isolated qubits on the device (if applicable).""" return self._isolated_qubits @property diff --git a/cirq-core/cirq/devices/insertion_noise_model.py b/cirq-core/cirq/devices/insertion_noise_model.py index 06682fd0ba7..ab6604868fc 100644 --- a/cirq-core/cirq/devices/insertion_noise_model.py +++ b/cirq-core/cirq/devices/insertion_noise_model.py @@ -35,7 +35,7 @@ class InsertionNoiseModel(devices.NoiseModel): operations that should be added. If two gate types provided apply to a target gate, the most specific type will match; if neither type is more specific (e.g. A is a subtype of B, but B defines - qubits and A does not) then the first one appering in this dict + qubits and A does not) then the first one appearing in this dict will match. prepend: If True, put noise before affected gates. Default: False. require_physical_tag: whether to only apply noise to operations tagged diff --git a/cirq-core/cirq/devices/noise_model.py b/cirq-core/cirq/devices/noise_model.py index afe095dfbdc..e8c6d049362 100644 --- a/cirq-core/cirq/devices/noise_model.py +++ b/cirq-core/cirq/devices/noise_model.py @@ -42,7 +42,7 @@ class NoiseModel(metaclass=value.ABCMetaImplementAnyOneOf): @classmethod def from_noise_model_like(cls, noise: 'cirq.NOISE_MODEL_LIKE') -> 'cirq.NoiseModel': - """Transforms an object into a noise model if umambiguously possible. + """Transforms an object into a noise model if unambiguously possible. Args: noise: `None`, a `cirq.NoiseModel`, or a single qubit operation. diff --git a/cirq-core/cirq/devices/thermal_noise_model.py b/cirq-core/cirq/devices/thermal_noise_model.py index 8639d371ffb..d516f9fa0b1 100644 --- a/cirq-core/cirq/devices/thermal_noise_model.py +++ b/cirq-core/cirq/devices/thermal_noise_model.py @@ -188,17 +188,17 @@ def __init__( heat_rate_GHz: single number (units GHz) specifying heating rate, either per qubit, or global value for all. Given a rate gh, the Lindblad op will be sqrt(gh)*a^dag - (where a is annihilation), so that the heating Lindbldian is + (where a is annihilation), so that the heating Lindbladian is gh(a^dag • a - 0.5{a*a^dag, •}). cool_rate_GHz: single number (units GHz) specifying cooling rate, either per qubit, or global value for all. Given a rate gc, the Lindblad op will be sqrt(gc)*a - so that the cooling Lindbldian is gc(a • a^dag - 0.5{n, •}) + so that the cooling Lindbladian is gc(a • a^dag - 0.5{n, •}) This number is equivalent to 1/T1. dephase_rate_GHz: single number (units GHz) specifying dephasing rate, either per qubit, or global value for all. Given a rate gd, Lindblad op will be sqrt(2*gd)*n where - n = a^dag * a, so that the dephasing Lindbldian is + n = a^dag * a, so that the dephasing Lindbladian is 2 * gd * (n • n - 0.5{n^2, •}). This number is equivalent to 1/Tphi. require_physical_tag: whether to only apply noise to operations From 760e63a6167b2594fec06b4725bac9b40149ab10 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Fri, 16 Sep 2022 17:43:06 -0700 Subject: [PATCH 09/12] Address feedback --- cirq-core/cirq/transformers/routing/mapping_manager.py | 2 +- .../cirq/transformers/routing/mapping_manager_test.py | 10 +++++----- .../cirq/transformers/routing/route_circuit_cqc.py | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/cirq-core/cirq/transformers/routing/mapping_manager.py b/cirq-core/cirq/transformers/routing/mapping_manager.py index bb354d0707e..aee2eab7757 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager.py @@ -146,7 +146,7 @@ def dist_on_device(self, lq1: int, lq2: int) -> int: """ return self._distances[self.logical_to_physical[lq1]][self.logical_to_physical[lq2]] - def can_execute(self, lq1: int, lq2: int) -> bool: + def is_adjacent(self, lq1: int, lq2: int) -> bool: """Finds whether logical qubits `lq1` and `lq2` are adjacent on the device. Args: diff --git a/cirq-core/cirq/transformers/routing/mapping_manager_test.py b/cirq-core/cirq/transformers/routing/mapping_manager_test.py index 1ea85a0ab0d..d4ef161d119 100644 --- a/cirq-core/cirq/transformers/routing/mapping_manager_test.py +++ b/cirq-core/cirq/transformers/routing/mapping_manager_test.py @@ -83,18 +83,18 @@ def test_mapped_op(): ) -def test_distance_on_device_and_can_execute(): +def test_distance_on_device_and_is_adjacent(): device_graph, initial_mapping, q = construct_device_graph_and_mapping() mm = cirq.MappingManager(device_graph, initial_mapping) q_int = [mm.logical_qid_to_int[q[i]] if q[i] in initial_mapping else -1 for i in range(len(q))] # adjacent qubits have distance 1 and are thus executable assert mm.dist_on_device(q_int[1], q_int[3]) == 1 - assert mm.can_execute(q_int[1], q_int[3]) + assert mm.is_adjacent(q_int[1], q_int[3]) # non-adjacent qubits with distance > 1 are not executable assert mm.dist_on_device(q_int[1], q_int[2]) == 2 - assert mm.can_execute(q_int[1], q_int[2]) is False + assert mm.is_adjacent(q_int[1], q_int[2]) is False # 'dist_on_device' does not use cirq.NamedQubit("e") to find shorter shortest path assert mm.dist_on_device(q_int[1], q_int[4]) == 3 @@ -102,9 +102,9 @@ def test_distance_on_device_and_can_execute(): # distance changes after applying swap mm.apply_swap(q_int[2], q_int[3]) assert mm.dist_on_device(q_int[1], q_int[3]) == 2 - assert mm.can_execute(q_int[1], q_int[3]) is False + assert mm.is_adjacent(q_int[1], q_int[3]) is False assert mm.dist_on_device(q_int[1], q_int[2]) == 1 - assert mm.can_execute(q_int[1], q_int[2]) + assert mm.is_adjacent(q_int[1], q_int[2]) # distance between other qubits doesn't change assert mm.dist_on_device(q_int[1], q_int[4]) == 3 diff --git a/cirq-core/cirq/transformers/routing/route_circuit_cqc.py b/cirq-core/cirq/transformers/routing/route_circuit_cqc.py index 546e0a4cd54..32868af9338 100644 --- a/cirq-core/cirq/transformers/routing/route_circuit_cqc.py +++ b/cirq-core/cirq/transformers/routing/route_circuit_cqc.py @@ -28,7 +28,7 @@ QidIntPair = Tuple[int, int] -def disjoint_nc2_combinations( +def _disjoint_nc2_combinations( qubit_pairs: Sequence[QidIntPair], ) -> List[Tuple[QidIntPair, QidIntPair]]: """Gets disjoint pair combinations of qubits pairs. @@ -36,7 +36,7 @@ def disjoint_nc2_combinations( For example: >>> q = [*range(5)] - >>> disjoint_swaps = cirq.transformers.routing.route_circuit_cqc.disjoint_nc2_combinations( + >>> disjoint_swaps = cirq.transformers.routing.route_circuit_cqc._disjoint_nc2_combinations( ... [(q[0], q[1]), (q[2], q[3]), (q[1], q[4])] ... ) >>> disjoint_swaps == [((q[0], q[1]), (q[2], q[3])), ((q[2], q[3]), (q[1], q[4]))] @@ -302,7 +302,7 @@ def process_executable_two_qubit_ops(timestep: int) -> int: unexecutable_ops: List['cirq.Operation'] = [] unexecutable_ops_ints: List[QidIntPair] = [] for op, op_ints in zip(two_qubit_ops[timestep], two_qubit_ops_ints[timestep]): - if mm.can_execute(*op_ints): + if mm.is_adjacent(*op_ints): routed_ops[timestep].append(mm.mapped_op(op)) else: unexecutable_ops.append(op) @@ -370,7 +370,7 @@ def _choose_pair_of_swaps( lookahead_radius: int, ) -> Optional[Tuple[QidIntPair, ...]]: """Computes cost function with pairs of candidate swaps that act on disjoint qubits.""" - pair_sigma = disjoint_nc2_combinations( + pair_sigma = _disjoint_nc2_combinations( cls._initial_candidate_swaps(mm, two_qubit_ops_ints[timestep]) ) return cls._choose_optimal_swap( From 9bfd71f9426989d6404fd2acb8a12ad2ccc7b128 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Mon, 19 Sep 2022 10:40:11 -0700 Subject: [PATCH 10/12] Update routing_transformer.ipynb notebook --- docs/transform/routing_transformer.ipynb | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/transform/routing_transformer.ipynb b/docs/transform/routing_transformer.ipynb index 39cb1827148..1348b0233d1 100644 --- a/docs/transform/routing_transformer.ipynb +++ b/docs/transform/routing_transformer.ipynb @@ -399,22 +399,22 @@ }, "outputs": [], "source": [ - "from typing import Tuple, List, Any\n", + "from typing import Tuple, Sequence, Any\n", "\n", - "QidPair = QidPair = Tuple['cirq.Qid', 'cirq.Qid']\n", + "QidIntPair = Tuple[int, int]\n", "\n", "class RouteCQCSimpleCostFunction(cirq.RouteCQC):\n", " @classmethod\n", " def _cost(\n", " cls,\n", " mm: cirq.MappingManager,\n", - " swaps: Tuple[QidPair, ...],\n", - " two_qubit_ops: List[cirq.Operation],\n", + " swaps: Tuple[QidIntPair, ...],\n", + " two_qubit_ops: Sequence[QidIntPair],\n", " ) -> Any:\n", " \"\"\"Computes the # of 2-qubit gates executable after applying SWAPs.\"\"\"\n", " for swap in swaps:\n", " mm.apply_swap(*swap)\n", - " ret = sum(1 for op in two_qubit_ops if mm.can_execute(op))\n", + " ret = sum(1 for op_ints in two_qubit_ops if mm.is_adjacent(*op_ints))\n", " for swap in swaps:\n", " mm.apply_swap(*swap)\n", " return ret" From 2e92638628bb008b337918ee724e89eeba9e8764 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Mon, 19 Sep 2022 10:44:52 -0700 Subject: [PATCH 11/12] Add circuit routing notebook to skip list --- dev_tools/notebooks/isolated_notebook_test.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/dev_tools/notebooks/isolated_notebook_test.py b/dev_tools/notebooks/isolated_notebook_test.py index 521ab3b3bda..c131994a7b9 100644 --- a/dev_tools/notebooks/isolated_notebook_test.py +++ b/dev_tools/notebooks/isolated_notebook_test.py @@ -55,6 +55,8 @@ 'docs/noise/qcvv/xeb_calibration_example.ipynb', 'docs/named_topologies.ipynb', 'docs/start/intro.ipynb', + # Circuit routing + 'docs/transform/routing_transformer.ipynb', ] # By default all notebooks should be tested, however, this list contains exceptions to the rule From 389d4477e1d450ca40af426aa39dd66b0b3cd9a5 Mon Sep 17 00:00:00 2001 From: Tanuj Khattar Date: Mon, 19 Sep 2022 10:51:44 -0700 Subject: [PATCH 12/12] Add mandatory message to notebook --- docs/transform/routing_transformer.ipynb | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/transform/routing_transformer.ipynb b/docs/transform/routing_transformer.ipynb index 1348b0233d1..fc7695965a2 100644 --- a/docs/transform/routing_transformer.ipynb +++ b/docs/transform/routing_transformer.ipynb @@ -69,7 +69,8 @@ "id": "RrRN9ilV0Ltg" }, "source": [ - "## Setup" + "## Setup\n", + "Note: this notebook relies on unreleased Cirq features. If you want to try these features, make sure you install cirq via `pip install cirq --pre`." ] }, {