From d379cfbb2e05190c9510800c41338751e3b2f4a9 Mon Sep 17 00:00:00 2001 From: Wing Li <74674811+wingers@users.noreply.github.com> Date: Thu, 11 Feb 2021 09:19:46 -0800 Subject: [PATCH 1/6] Improve initial qubit mapping algorithm. --- recirq/quantum_chess/circuit_transformer.py | 389 +++++++++++++++++- .../quantum_chess/circuit_transformer_test.py | 9 +- ...head_heuristic_circuit_transformer_test.py | 227 ++++++++++ 3 files changed, 616 insertions(+), 9 deletions(-) create mode 100644 recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py diff --git a/recirq/quantum_chess/circuit_transformer.py b/recirq/quantum_chess/circuit_transformer.py index 359cecd7..3486bcc6 100644 --- a/recirq/quantum_chess/circuit_transformer.py +++ b/recirq/quantum_chess/circuit_transformer.py @@ -12,7 +12,9 @@ # See the License for the specific language governing permissions and # limitations under the License. import copy -from typing import Dict, Iterable, List, Optional, Set +import math +from collections import defaultdict, deque +from typing import Deque, Dict, Iterable, List, Optional, Set, Tuple, Union import cirq @@ -305,6 +307,391 @@ def transform(self, circuit: cirq.Circuit) -> cirq.Circuit: return circuit.transform_qubits(lambda q: self.mapping[q]) +class DynamicLookAheadHeuristicCircuitTransformer(CircuitTransformer): + """Optimizer that transforms a circuit to satify a device's constraints. + + This implements the initial mapping algorithm and the SWAP update algorithm + proposed by the paper "A Dynamic Look-Ahead Heuristic for the Qubit Mapping + Problem of NISQ Computer: + https://ieeexplore.ieee.org/abstract/document/8976109. + + Reference: + P. Zhu, Z. Guan and X. Cheng, "A Dynamic Look-Ahead Heuristic for the + Qubit Mapping Problem of NISQ Computers," in IEEE Transactions on Computer- + Aided Design of Integrated Circuits and Systems, vol. 39, no. 12, pp. 4721- + 4735, Dec. 2020, doi: 10.1109/TCAD.2020.2970594. + """ + def __init__(self, device: cirq.Device): + super().__init__() + self.device = device + + def build_physical_qubits_graph( + self, + ) -> Dict[cirq.GridQubit, List[cirq.GridQubit]]: + """Returns an adjacency graph of physical qubits of the device. + + Each edge is bidirectional, and represents a valid two-qubit gate. + """ + g = defaultdict(list) + for q in self.device.qubit_set(): + neighbors = [n for n in q.neighbors() if n in self.device.qubit_set()] + for neighbor in neighbors: + if q not in g[neighbor]: + g[neighbor].append(q) + return g + + def get_least_connected_qubit( + self, + g: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + component: Deque[cirq.Qid], + ) -> cirq.Qid: + """Returns the least connected qubit. + + Args: + g: A logical qubits graph. + component: A deque of qubits belonging to the same component. + """ + qubit = None + min_degree = math.inf + for q in component: + degree = len(g[q]) + if degree < min_degree: + qubit = q + min_degree = degree + return qubit + + def build_logical_qubits_graph( + self, + circuit: cirq.Circuit, + ) -> Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]]: + """Returns an adjacency graph of logical qubits of the circuit. + + Uses the heuristic of adding an edge between the nodes of each + disjoint component that are least connected if the graph contains more + than one connected component. + + Each edge is a tuple containing an adjacent node and the index of the + moment at which the operation occurs. + + Arg: + circuit: The circuit from which to build a logical qubits graph. + """ + g = defaultdict(list) + + # Build an adjacency graph based on the circuit. + for i, m in enumerate(circuit): + for op in m: + if len(op.qubits) == 2: + q1, q2 = op.qubits + q1_neighbors = [n[0] for n in g[q1]] + if q2 not in q1_neighbors: + g[q1].append((q2, i)) + q2_neighbors = [n[0] for n in g[q2]] + if q1 not in q2_neighbors: + g[q2].append((q1, i)) + + # Find the connected components in the graph. + components = deque() + visited = set() + for q in g: + if q not in visited: + components.append(self.traverse(g, q, visited)) + + if len(components) == 1: + return g + + # Connect disjoint components by adding an edge between the nodes of + # each disjoint component that are least connected. + while len(components) > 1: + first_comp = components.pop() + first_q = self.get_least_connected_qubit(g, first_comp) + second_comp = components.pop() + second_q = self.get_least_connected_qubit(g, second_comp) + + # Add an edge between the two least connected nodes, and set its + # moment index to infinity since this edge doesn't represent a + # actual gate in the circuit. + g[first_q].append((second_q, math.inf)) + g[second_q].append((first_q, math.inf)) + + # Combine the two components and add it back to the components + # deque to continue connecting disjoint components. + first_comp += second_comp + components.append(first_comp) + + return g + + def find_graph_center( + self, + g: Union[ + Dict[cirq.GridQubit, List[cirq.GridQubit]], + Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + ], + ) -> Union[cirq.GridQubit, cirq.Qid]: + """Returns a qubit that is a graph center. + + Uses the Floyd-Warshall algorithm to calculate the length of the + shortest path between each pair of nodes. Then, finds the graph center + such that the length of the shortest path to the farthest node is the + smallest. Returns the first graph center if there are multiple. + + Args: + g: A physical qubits graph or a logical qubits graph. + """ + qubit_to_index_mapping = defaultdict() + index_to_qubit_mapping = defaultdict() + for i, q in enumerate(g): + qubit_to_index_mapping[q] = i + index_to_qubit_mapping[i] = q + + v = len(g) + + # Use the Floyd–Warshall algorithm to calculate the length of the + # shortest path between each pair of nodes. + shortest = [[math.inf for j in range(v)] for i in range(v)] + for q in g: + i = qubit_to_index_mapping[q] + shortest[i][i] = 0 + for neighbor in g[q]: + if isinstance(neighbor, tuple): + neighbor = neighbor[0] + j = qubit_to_index_mapping[neighbor] + shortest[i][j] = 1 + for k in range(v): + for i in range(v): + for j in range(v): + shortest[i][j] = min(shortest[i][j], shortest[i][k] + shortest[k][j]) + + # For each node, find the length of the shortest path to the farthest + # node + farthest = [0 for i in range(v)] + for i in range(v): + for j in range(v): + if i != j and shortest[i][j] > farthest[i]: + farthest[i] = shortest[i][j] + + # Find the graph center such that the length of the shortest path to the + # farthest node is the smallest. Use the first graph center if there are + # multiple graph centers. + center = 0 + for i in range(v): + if farthest[i] < farthest[center]: + center = i + + return index_to_qubit_mapping[center] + + def traverse( + self, + g: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + s: cirq.Qid, + visited: Optional[set] = None, + ) -> Deque[cirq.Qid]: + """Returns a deque of qubits ordered by breadth-first search traversal. + + During each iteration of breadth-first search, the adjacent nodes are + sorted by their corresponding moments before being traversed. + + Args: + g: A logical qubits graph. + s: The source qubit from which to start breadth-first search. + """ + order = deque() + if visited is None: + visited = set() + visited.add(s) + queue = deque() + queue.append(s) + while queue: + q = queue.popleft() + order.append(q) + neighbors_sorted_by_moment = sorted(g[q], key=lambda x: x[1]) + for neighbor in neighbors_sorted_by_moment: + neighbor = neighbor[0] + if neighbor not in visited: + visited.add(neighbor) + queue.append(neighbor) + return order + + def find_reference_qubits( + self, + mapping: Dict[cirq.Qid, cirq.GridQubit], + lg: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + lq: cirq.Qid, + ) -> List[cirq.GridQubit]: + """Returns a list of physical qubits from which to find the next mapping. + + The nodes adjacent to the logical qubit parameter are sorted by their + corresponding moments before being traversed. For each adjacent node + that has been mapped to a physical qubit, the mapped physical qubit is + added to the result. + + Args: + mapping: The current mapping of logical qubits to physical qubits. + lg: A logical qubits graph. + lq: The logical qubit from which to find reference qubits. + """ + qubits = [] + neighbors_sorted_by_moment = sorted(lg[lq], key=lambda x: x[1]) + for neighbor in neighbors_sorted_by_moment: + neighbor = neighbor[0] + if neighbor in mapping: + # This neighbor has been mapped to a physical qubit. Add the + # physical qubit to reference qubits. + qubits.append(mapping[neighbor]) + return qubits + + def find_candidate_qubits( + self, + mapping: Dict[cirq.Qid, cirq.GridQubit], + pg: Dict[cirq.GridQubit, List[cirq.GridQubit]], + pq: cirq.GridQubit, + ) -> List[cirq.GridQubit]: + """Returns a list of physical qubits available to be mapped. + + Uses level order traversal until a level with free adjacent node(s) is + found. + + Args: + mapping: The current mapping of logical qubits to physical qubits. + lg: A physical qubits graph. + lq: The physical qubit from which to find candidate qubits. + """ + qubits = [] + visited = set() + visited.add(pq) + queue = deque() + queue.append(pq) + while queue: + level = len(queue) + while level > 0: + q = queue.popleft() + for neighbor in pg[q]: + if neighbor not in visited: + visited.add(neighbor) + queue.append(neighbor) + if neighbor not in mapping and neighbor not in qubits: + qubits.append(neighbor) + level -= 1 + if len(qubits) > 0: + break + return qubits + + def find_shortest_path_distance( + self, + g: Dict[cirq.GridQubit, List[cirq.GridQubit]], + s: cirq.GridQubit, + t: cirq.GridQubit, + ) -> int: + """Returns the shortest distance between the source and target qubits. + + Uses breadth-first search traversal. + + Args: + g: A physical qubits graph. + s: The source qubit from which to start breadth-first search. + t: The target qubit to search. + """ + dist = defaultdict(int) + visited = set() + visited.add(s) + queue = deque() + queue.append(s) + while queue: + q = queue.popleft() + if q == t: + return dist[t] + for neighbor in g[q]: + if neighbor not in visited: + dist[neighbor] = dist[q] + 1 + visited.add(neighbor) + queue.append(neighbor) + return math.inf + + def calculate_initial_mapping( + self, + pg: Dict[cirq.Qid, List[cirq.Qid]], + lg: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + ) -> Dict[cirq.Qid, cirq.GridQubit]: + """Returns an initial mapping of logical qubits to physical qubits. + + This initial mapping algorithm is proposed by the paper "A Dynamic + Look-Ahead Heuristic for the Qubit Mapping Problem of NISQ Computer: + https://ieeexplore.ieee.org/abstract/document/8976109. + + Args: + pg: A physical qubits graph. + lg: A logical qubits graph. + """ + mapping = defaultdict() + + # The circuit doesn't contain any 2-qubit gates, return an empty + # mapping. + if not lg: + return mapping + + pg_center = self.find_graph_center(pg) + lg_center = self.find_graph_center(lg) + mapping[lg_center] = pg_center + + traversal_order = self.traverse(lg, lg_center) + + while traversal_order: + lq = traversal_order.popleft() + if lq == lg_center: + continue + pq = None + reference_qubits = self.find_reference_qubits(mapping, lg, lq) + ref_q = reference_qubits[0] + candidate_qubits = self.find_candidate_qubits(mapping.values(), pg, ref_q) + if len(reference_qubits) > 1: + # For each reference location, find the shortest path distance + # to each of the candidate qubits. Only keep the nearest + # candidate qubits with smallest distance. + for ref_q in reference_qubits[1:]: + distances = defaultdict(list) + for cand_q in candidate_qubits: + d = self.find_shortest_path_distance(pg, ref_q, cand_q) + distances[d].append(cand_q) + nearest_candidate_qubits = None + min_dist = math.inf + for dist in distances: + if dist < min_dist: + min_dist = dist + nearest_candidate_qubits = distances[dist] + candidate_qubits = nearest_candidate_qubits + if candidate_qubits == 1: + pq = candidate_qubits[0] + break + # If there are still more than one candidate qubit at this point, + # choose the one with the closest degree to the logical qubit. + if len(candidate_qubits) > 1: + lq_degree = len(lg[lq]) + min_diff = math.inf + for cand_q in candidate_qubits: + cand_q_degree = len(pg[cand_q]) + diff = abs(cand_q_degree - lq_degree) + if diff < min_diff: + min_diff = diff + pq = cand_q + mapping[lq] = pq + + return mapping + + def transform(self, circuit: cirq.Circuit) -> cirq.Circuit: + """Returns a transformed circuit. + + The transformed circuit satisfies all physical adjacency constraints + of the device. + + Args: + circuit: The circuit to transform. + """ + pg = self.build_physical_qubits_graph() + lg = self.build_logical_qubits_graph(circuit) + initial_mapping = self.calculate_initial_mapping(pg, lg) + # TODO: return the transformed circuit here instead. + return initial_mapping + class SycamoreDecomposer(cirq.PointOptimizer): """Optimizer that decomposes all three qubit operations into sqrt-ISWAPs. diff --git a/recirq/quantum_chess/circuit_transformer_test.py b/recirq/quantum_chess/circuit_transformer_test.py index 49b6514c..798aec65 100644 --- a/recirq/quantum_chess/circuit_transformer_test.py +++ b/recirq/quantum_chess/circuit_transformer_test.py @@ -35,9 +35,7 @@ def test_single_qubit_ops(device): transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) - transformer.qubit_mapping(c) - c = transformer.transform(c) - device.validate_circuit(c) + device.validate_circuit(transformer.transform(c)) @pytest.mark.parametrize('device', @@ -46,7 +44,6 @@ def test_single_qubit_with_two_qubits(device): transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3), cirq.ISWAP(a3, a4) ** 0.5) - transformer.qubit_mapping(c) device.validate_circuit(transformer.transform(c)) @@ -56,7 +53,6 @@ def test_three_split_moves(device): transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(qm.split_move(a1, a2, b1), qm.split_move(a2, a3, b3), qm.split_move(b1, c1, c2)) - transformer.qubit_mapping(c) device.validate_circuit(transformer.transform(c)) @@ -66,7 +62,6 @@ def test_disconnected(device): transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(qm.split_move(a1, a2, a3), qm.split_move(a3, a4, d1), qm.split_move(b1, b2, b3), qm.split_move(c1, c2, c3)) - transformer.qubit_mapping(c) device.validate_circuit(transformer.transform(c)) @@ -76,7 +71,6 @@ def test_move_around_square(device): transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(qm.normal_move(a1, a2), qm.normal_move(a2, b2), qm.normal_move(b2, b1), qm.normal_move(b1, a1)) - transformer.qubit_mapping(c) device.validate_circuit(transformer.transform(c)) @@ -87,5 +81,4 @@ def test_split_then_merge(device): c = cirq.Circuit(qm.split_move(a1, a2, b1), qm.split_move(a2, a3, b3), qm.split_move(b1, c1, c2), qm.normal_move(c1, d1), qm.normal_move(a3, a4), qm.merge_move(a4, d1, a1)) - transformer.qubit_mapping(c) device.validate_circuit(transformer.transform(c)) diff --git a/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py b/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py new file mode 100644 index 00000000..5653933a --- /dev/null +++ b/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py @@ -0,0 +1,227 @@ +# Copyright 2020 Google +# +# 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 math +import pytest +from collections import deque + +import cirq + +import recirq.quantum_chess.circuit_transformer as ct + +a0 = cirq.NamedQubit('a0') +a1 = cirq.NamedQubit('a1') +a2 = cirq.NamedQubit('a2') +a3 = cirq.NamedQubit('a3') +a4 = cirq.NamedQubit('a4') +a5 = cirq.NamedQubit('a5') +a6 = cirq.NamedQubit('a6') +a7 = cirq.NamedQubit('a7') + +logical = { + a0: [(a2, 0), (a1, 1), (a3, 6)], + a1: [(a0, 1), (a2, 3)], + a2: [(a0, 0), (a1, 3), (a3, 5)], + a3: [(a2, 5), (a0, 6)], +} + +logical2 = { + a0: [(a2, 0), (a1, 1), (a3, 6)], + a1: [(a0, 1), (a2, 3), (a7, math.inf)], + a2: [(a0, 0), (a1, 3), (a3, 5)], + a3: [(a2, 5), (a0, 6)], + a4: [(a5, 0), (a6, math.inf)], + a5: [(a4, 0)], + a6: [(a7, 0), (a4, math.inf)], + a7: [(a6, 0), (a1, math.inf)], +} + +zero_zero = cirq.GridQubit(0, 0) +zero_one = cirq.GridQubit(0, 1) +zero_two = cirq.GridQubit(0, 2) +zero_three = cirq.GridQubit(0, 3) +zero_four = cirq.GridQubit(0, 4) +zero_five = cirq.GridQubit(0, 5) +zero_six = cirq.GridQubit(0, 6) +zero_seven = cirq.GridQubit(0, 7) +zero_eight = cirq.GridQubit(0, 8) +zero_nine = cirq.GridQubit(0, 9) +zero_ten = cirq.GridQubit(0, 10) +one_zero = cirq.GridQubit(1, 0) +one_one = cirq.GridQubit(1, 1) +one_two = cirq.GridQubit(1, 2) +one_three = cirq.GridQubit(1, 3) +one_four = cirq.GridQubit(1, 4) +one_five = cirq.GridQubit(1, 5) +one_six = cirq.GridQubit(1, 6) +one_seven = cirq.GridQubit(1, 7) +one_eight = cirq.GridQubit(1, 8) +one_nine = cirq.GridQubit(1, 9) +one_ten = cirq.GridQubit(1, 10) + +physical = { + zero_zero: [zero_one, one_zero], + zero_one: [zero_zero, one_one, zero_two], + zero_two: [zero_one, one_two, zero_three], + zero_three: [zero_two, one_three, zero_four], + zero_four: [zero_three, one_four, zero_five], + zero_five: [zero_four, one_five, zero_six], + zero_six: [zero_five, one_six, zero_seven], + zero_seven: [zero_six, one_seven, zero_eight], + zero_eight: [zero_seven, one_eight, zero_nine], + zero_nine: [zero_eight, one_nine, zero_ten], + zero_ten: [zero_nine, one_ten], + one_zero: [one_one, zero_zero], + one_one: [one_zero, zero_one, one_two], + one_two: [one_one, zero_two, one_three], + one_three: [one_two, zero_three, one_four], + one_four: [one_three, zero_four, one_five], + one_five: [one_four, zero_five, one_six], + one_six: [one_five, zero_six, one_seven], + one_seven: [one_six, zero_seven, one_eight], + one_eight: [one_seven, zero_eight, one_nine], + one_nine: [one_eight, zero_nine, one_ten], + one_ten: [one_nine, zero_ten], +} + +def test_build_physical_qubits_graph(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + g = t.build_physical_qubits_graph() + assert len(g) == len(physical) + for q in physical: + assert set(physical[q]) == set(g[q]) + +def test_get_least_connected_qubit(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + g = { + 0: [1], + 1: [0, 2], + 2: [1], + 3: [4], + 4: [3], + } + assert t.get_least_connected_qubit(g, deque([0, 1, 2])) == 0 + assert t.get_least_connected_qubit(g, deque([3, 4])) == 3 + +def test_build_logical_qubits_graph(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + # One connected component. + c = cirq.Circuit( + cirq.ISWAP(a2, a0), + cirq.ISWAP(a0, a1), + cirq.ISWAP(a0, a2), + cirq.ISWAP(a2, a1), + cirq.ISWAP(a1, a2), + cirq.ISWAP(a2, a3), + cirq.ISWAP(a0, a3), + ) + assert t.build_logical_qubits_graph(c) == logical + # Three connected components. + c2 = cirq.Circuit( + cirq.ISWAP(a2, a0), + cirq.ISWAP(a0, a1), + cirq.ISWAP(a0, a2), + cirq.ISWAP(a2, a1), + cirq.ISWAP(a1, a2), + cirq.ISWAP(a2, a3), + cirq.ISWAP(a0, a3), + cirq.ISWAP(a4, a5), + cirq.ISWAP(a6, a7), + ) + assert t.build_logical_qubits_graph(c2) == logical2 + +def test_graph_center(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + assert t.find_graph_center(physical) == zero_five + assert t.find_graph_center(logical) == a0 + +def test_traverse(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + assert t.traverse(logical, a0) == deque([a0, a2, a1, a3]) + +def test_find_reference_qubits(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + mapping = { + a0: zero_five, + } + assert t.find_reference_qubits(mapping, logical, a2) == [zero_five] + +def test_find_candidate_qubits(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + # First level has free qubits. + mapping = { + zero_five: "mapped", + } + assert t.find_candidate_qubits(mapping, physical, zero_five) == [ + cirq.GridQubit(0, 4), + cirq.GridQubit(1, 5), + cirq.GridQubit(0, 6), + ] + # Second level has free qubits. + mapping = { + zero_five: "mapped", + zero_four: "mapped", + zero_six: "mapped", + one_five: "mapped", + } + assert t.find_candidate_qubits(mapping, physical, zero_five) == [ + cirq.GridQubit(0, 3), + cirq.GridQubit(1, 4), + cirq.GridQubit(1, 6), + cirq.GridQubit(0, 7), + ] + # Third level has free qubits. + mapping = { + zero_three: "mapped", + zero_four: "mapped", + zero_five: "mapped", + zero_six: "mapped", + zero_seven: "mapped", + one_four: "mapped", + one_five: "mapped", + one_six: "mapped", + } + assert t.find_candidate_qubits(mapping, physical, zero_five) == [ + cirq.GridQubit(0, 2), + cirq.GridQubit(1, 3), + cirq.GridQubit(1, 7), + cirq.GridQubit(0, 8), + ] + +def test_find_shortest_path_distance(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + g = { + 0: [1, 7], + 1: [0, 2, 7], + 2: [1, 3, 5, 8], + 3: [2, 4, 5], + 4: [3, 5], + 5: [2, 3, 4, 6], + 6: [5, 7], + 7: [0, 1, 6, 8], + 8: [2, 7], + } + assert t.find_shortest_path_distance(g, 0, 5) == 3 + +def test_calculate_initial_mapping(): + t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + c = cirq.Circuit( + cirq.ISWAP(a2, a0), + cirq.ISWAP(a0, a1), + cirq.ISWAP(a0, a2), + cirq.ISWAP(a2, a1), + cirq.ISWAP(a1, a2), + cirq.ISWAP(a2, a3), + cirq.ISWAP(a0, a3), + ) + print(t.transform(c)) From 25df0a2eb6af73aa69e65f373d4bd0dcaf87ef98 Mon Sep 17 00:00:00 2001 From: Wing Li <74674811+wingers@users.noreply.github.com> Date: Thu, 11 Feb 2021 19:29:03 -0800 Subject: [PATCH 2/6] Include qubits from 1-qubit gates in the initial mapping. --- recirq/quantum_chess/circuit_transformer.py | 29 +++++++-------- .../quantum_chess/circuit_transformer_test.py | 36 ++++++++++++------- ...head_heuristic_circuit_transformer_test.py | 14 +++++--- 3 files changed, 49 insertions(+), 30 deletions(-) diff --git a/recirq/quantum_chess/circuit_transformer.py b/recirq/quantum_chess/circuit_transformer.py index 3486bcc6..f3e29bb6 100644 --- a/recirq/quantum_chess/circuit_transformer.py +++ b/recirq/quantum_chess/circuit_transformer.py @@ -377,10 +377,16 @@ def build_logical_qubits_graph( circuit: The circuit from which to build a logical qubits graph. """ g = defaultdict(list) + moment_index = 0 # Build an adjacency graph based on the circuit. for i, m in enumerate(circuit): + moment_index = i for op in m: + if len(op.qubits) == 1: + q = op.qubits[0] + if q not in g: + g[q] = [] if len(op.qubits) == 2: q1, q2 = op.qubits q1_neighbors = [n[0] for n in g[q1]] @@ -403,16 +409,15 @@ def build_logical_qubits_graph( # Connect disjoint components by adding an edge between the nodes of # each disjoint component that are least connected. while len(components) > 1: + moment_index += 1 first_comp = components.pop() first_q = self.get_least_connected_qubit(g, first_comp) second_comp = components.pop() second_q = self.get_least_connected_qubit(g, second_comp) - # Add an edge between the two least connected nodes, and set its - # moment index to infinity since this edge doesn't represent a - # actual gate in the circuit. - g[first_q].append((second_q, math.inf)) - g[second_q].append((first_q, math.inf)) + # Add an edge between the two least connected nodes. + g[first_q].append((second_q, moment_index)) + g[second_q].append((first_q, moment_index)) # Combine the two components and add it back to the components # deque to continue connecting disjoint components. @@ -624,11 +629,6 @@ def calculate_initial_mapping( """ mapping = defaultdict() - # The circuit doesn't contain any 2-qubit gates, return an empty - # mapping. - if not lg: - return mapping - pg_center = self.find_graph_center(pg) lg_center = self.find_graph_center(lg) mapping[lg_center] = pg_center @@ -659,9 +659,10 @@ def calculate_initial_mapping( min_dist = dist nearest_candidate_qubits = distances[dist] candidate_qubits = nearest_candidate_qubits - if candidate_qubits == 1: - pq = candidate_qubits[0] + if len(candidate_qubits) == 1: break + if len(candidate_qubits) == 1: + pq = candidate_qubits[0] # If there are still more than one candidate qubit at this point, # choose the one with the closest degree to the logical qubit. if len(candidate_qubits) > 1: @@ -689,8 +690,8 @@ def transform(self, circuit: cirq.Circuit) -> cirq.Circuit: pg = self.build_physical_qubits_graph() lg = self.build_logical_qubits_graph(circuit) initial_mapping = self.calculate_initial_mapping(pg, lg) - # TODO: return the transformed circuit here instead. - return initial_mapping + # TODO: change this to use the result of the SWAP update algorithm. + return circuit.transform_qubits(lambda q: initial_mapping[q]) class SycamoreDecomposer(cirq.PointOptimizer): """Optimizer that decomposes all three qubit operations into diff --git a/recirq/quantum_chess/circuit_transformer_test.py b/recirq/quantum_chess/circuit_transformer_test.py index 798aec65..27186858 100644 --- a/recirq/quantum_chess/circuit_transformer_test.py +++ b/recirq/quantum_chess/circuit_transformer_test.py @@ -33,52 +33,64 @@ @pytest.mark.parametrize('device', (cirq.google.Sycamore23, cirq.google.Sycamore)) def test_single_qubit_ops(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) - device.validate_circuit(transformer.transform(c)) + t = ct.ConnectivityHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) + t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + device.validate_circuit(t2.transform(c)) @pytest.mark.parametrize('device', (cirq.google.Sycamore23, cirq.google.Sycamore)) def test_single_qubit_with_two_qubits(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3), cirq.ISWAP(a3, a4) ** 0.5) - device.validate_circuit(transformer.transform(c)) + t = ct.ConnectivityHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) + t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + device.validate_circuit(t2.transform(c)) @pytest.mark.parametrize('device', (cirq.google.Sycamore23, cirq.google.Sycamore)) def test_three_split_moves(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(qm.split_move(a1, a2, b1), qm.split_move(a2, a3, b3), qm.split_move(b1, c1, c2)) - device.validate_circuit(transformer.transform(c)) + t = ct.ConnectivityHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) + t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + device.validate_circuit(t2.transform(c)) @pytest.mark.parametrize('device', (cirq.google.Sycamore23, cirq.google.Sycamore)) def test_disconnected(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(qm.split_move(a1, a2, a3), qm.split_move(a3, a4, d1), qm.split_move(b1, b2, b3), qm.split_move(c1, c2, c3)) - device.validate_circuit(transformer.transform(c)) + t = ct.ConnectivityHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) +# t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) +# device.validate_circuit(t2.transform(c)) @pytest.mark.parametrize('device', (cirq.google.Sycamore23, cirq.google.Sycamore)) def test_move_around_square(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(qm.normal_move(a1, a2), qm.normal_move(a2, b2), qm.normal_move(b2, b1), qm.normal_move(b1, a1)) - device.validate_circuit(transformer.transform(c)) + t = ct.ConnectivityHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) +# t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) +# device.validate_circuit(t2.transform(c)) @pytest.mark.parametrize('device', (cirq.google.Sycamore23, cirq.google.Sycamore)) def test_split_then_merge(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(device) c = cirq.Circuit(qm.split_move(a1, a2, b1), qm.split_move(a2, a3, b3), qm.split_move(b1, c1, c2), qm.normal_move(c1, d1), qm.normal_move(a3, a4), qm.merge_move(a4, d1, a1)) - device.validate_circuit(transformer.transform(c)) + t = ct.ConnectivityHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) +# t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) +# device.validate_circuit(t2.transform(c)) diff --git a/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py b/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py index 5653933a..8f2a52ad 100644 --- a/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py +++ b/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py @@ -37,13 +37,13 @@ logical2 = { a0: [(a2, 0), (a1, 1), (a3, 6)], - a1: [(a0, 1), (a2, 3), (a7, math.inf)], + a1: [(a0, 1), (a2, 3), (a7, 8)], a2: [(a0, 0), (a1, 3), (a3, 5)], a3: [(a2, 5), (a0, 6)], - a4: [(a5, 0), (a6, math.inf)], + a4: [(a5, 0), (a6, 7)], a5: [(a4, 0)], - a6: [(a7, 0), (a4, math.inf)], - a7: [(a6, 0), (a1, math.inf)], + a6: [(a7, 0), (a4, 7)], + a7: [(a6, 0), (a1, 8)], } zero_zero = cirq.GridQubit(0, 0) @@ -139,6 +139,12 @@ def test_build_logical_qubits_graph(): cirq.ISWAP(a6, a7), ) assert t.build_logical_qubits_graph(c2) == logical2 + c3 = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) + assert t.build_logical_qubits_graph(c3) == { + a1: [(a3, 2)], + a2: [(a3, 1)], + a3: [(a2, 1), (a1, 2)], + } def test_graph_center(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) From 7ab8ffa8f4645fde863a1ee3dcba3aa9ea777790 Mon Sep 17 00:00:00 2001 From: Wing Li <74674811+wingers@users.noreply.github.com> Date: Sun, 14 Feb 2021 20:15:02 -0800 Subject: [PATCH 3/6] Add better test cases. --- recirq/quantum_chess/circuit_transformer.py | 32 +- .../quantum_chess/circuit_transformer_test.py | 24 +- ...head_heuristic_circuit_transformer_test.py | 335 +++++++++++------- 3 files changed, 245 insertions(+), 146 deletions(-) diff --git a/recirq/quantum_chess/circuit_transformer.py b/recirq/quantum_chess/circuit_transformer.py index f3e29bb6..07d252c0 100644 --- a/recirq/quantum_chess/circuit_transformer.py +++ b/recirq/quantum_chess/circuit_transformer.py @@ -14,7 +14,17 @@ import copy import math from collections import defaultdict, deque -from typing import Deque, Dict, Iterable, List, Optional, Set, Tuple, Union +from typing import ( + Deque, + Dict, + Iterable, + List, + Optional, + Set, + Tuple, + Union, + ValuesView, +) import cirq @@ -335,9 +345,8 @@ def build_physical_qubits_graph( g = defaultdict(list) for q in self.device.qubit_set(): neighbors = [n for n in q.neighbors() if n in self.device.qubit_set()] - for neighbor in neighbors: - if q not in g[neighbor]: - g[neighbor].append(q) + for n in neighbors: + g[q].append(n) return g def get_least_connected_qubit( @@ -351,14 +360,7 @@ def get_least_connected_qubit( g: A logical qubits graph. component: A deque of qubits belonging to the same component. """ - qubit = None - min_degree = math.inf - for q in component: - degree = len(g[q]) - if degree < min_degree: - qubit = q - min_degree = degree - return qubit + return min(component, key=lambda q: len(g[q])) def build_logical_qubits_graph( self, @@ -547,7 +549,7 @@ def find_reference_qubits( def find_candidate_qubits( self, - mapping: Dict[cirq.Qid, cirq.GridQubit], + mapped: ValuesView[cirq.GridQubit], pg: Dict[cirq.GridQubit, List[cirq.GridQubit]], pq: cirq.GridQubit, ) -> List[cirq.GridQubit]: @@ -557,7 +559,7 @@ def find_candidate_qubits( found. Args: - mapping: The current mapping of logical qubits to physical qubits. + mapped: The set of currently mapped physical qubits. lg: A physical qubits graph. lq: The physical qubit from which to find candidate qubits. """ @@ -574,7 +576,7 @@ def find_candidate_qubits( if neighbor not in visited: visited.add(neighbor) queue.append(neighbor) - if neighbor not in mapping and neighbor not in qubits: + if neighbor not in mapped and neighbor not in qubits: qubits.append(neighbor) level -= 1 if len(qubits) > 0: diff --git a/recirq/quantum_chess/circuit_transformer_test.py b/recirq/quantum_chess/circuit_transformer_test.py index 27186858..cd8d871a 100644 --- a/recirq/quantum_chess/circuit_transformer_test.py +++ b/recirq/quantum_chess/circuit_transformer_test.py @@ -36,8 +36,8 @@ def test_single_qubit_ops(device): c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) t = ct.ConnectivityHeuristicCircuitTransformer(device) device.validate_circuit(t.transform(c)) - t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) - device.validate_circuit(t2.transform(c)) + t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) @pytest.mark.parametrize('device', @@ -47,8 +47,8 @@ def test_single_qubit_with_two_qubits(device): cirq.ISWAP(a3, a4) ** 0.5) t = ct.ConnectivityHeuristicCircuitTransformer(device) device.validate_circuit(t.transform(c)) - t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) - device.validate_circuit(t2.transform(c)) + t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) @pytest.mark.parametrize('device', @@ -58,8 +58,8 @@ def test_three_split_moves(device): qm.split_move(b1, c1, c2)) t = ct.ConnectivityHeuristicCircuitTransformer(device) device.validate_circuit(t.transform(c)) - t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) - device.validate_circuit(t2.transform(c)) + t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) @pytest.mark.parametrize('device', @@ -69,8 +69,8 @@ def test_disconnected(device): qm.split_move(b1, b2, b3), qm.split_move(c1, c2, c3)) t = ct.ConnectivityHeuristicCircuitTransformer(device) device.validate_circuit(t.transform(c)) -# t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) -# device.validate_circuit(t2.transform(c)) +# t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) +# device.validate_circuit(t.transform(c)) @pytest.mark.parametrize('device', @@ -80,8 +80,8 @@ def test_move_around_square(device): qm.normal_move(b2, b1), qm.normal_move(b1, a1)) t = ct.ConnectivityHeuristicCircuitTransformer(device) device.validate_circuit(t.transform(c)) -# t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) -# device.validate_circuit(t2.transform(c)) +# t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) +# device.validate_circuit(t.transform(c)) @pytest.mark.parametrize('device', @@ -92,5 +92,5 @@ def test_split_then_merge(device): qm.normal_move(a3, a4), qm.merge_move(a4, d1, a1)) t = ct.ConnectivityHeuristicCircuitTransformer(device) device.validate_circuit(t.transform(c)) -# t2 = ct.DynamicLookAheadHeuristicCircuitTransformer(device) -# device.validate_circuit(t2.transform(c)) +# t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) +# device.validate_circuit(t.transform(c)) diff --git a/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py b/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py index 8f2a52ad..5e09d5ee 100644 --- a/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py +++ b/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py @@ -28,78 +28,127 @@ a6 = cirq.NamedQubit('a6') a7 = cirq.NamedQubit('a7') -logical = { - a0: [(a2, 0), (a1, 1), (a3, 6)], - a1: [(a0, 1), (a2, 3)], - a2: [(a0, 0), (a1, 3), (a3, 5)], - a3: [(a2, 5), (a0, 6)], -} - -logical2 = { - a0: [(a2, 0), (a1, 1), (a3, 6)], - a1: [(a0, 1), (a2, 3), (a7, 8)], - a2: [(a0, 0), (a1, 3), (a3, 5)], - a3: [(a2, 5), (a0, 6)], - a4: [(a5, 0), (a6, 7)], - a5: [(a4, 0)], - a6: [(a7, 0), (a4, 7)], - a7: [(a6, 0), (a1, 8)], -} - -zero_zero = cirq.GridQubit(0, 0) -zero_one = cirq.GridQubit(0, 1) -zero_two = cirq.GridQubit(0, 2) -zero_three = cirq.GridQubit(0, 3) -zero_four = cirq.GridQubit(0, 4) -zero_five = cirq.GridQubit(0, 5) -zero_six = cirq.GridQubit(0, 6) -zero_seven = cirq.GridQubit(0, 7) -zero_eight = cirq.GridQubit(0, 8) -zero_nine = cirq.GridQubit(0, 9) -zero_ten = cirq.GridQubit(0, 10) -one_zero = cirq.GridQubit(1, 0) -one_one = cirq.GridQubit(1, 1) -one_two = cirq.GridQubit(1, 2) -one_three = cirq.GridQubit(1, 3) -one_four = cirq.GridQubit(1, 4) -one_five = cirq.GridQubit(1, 5) -one_six = cirq.GridQubit(1, 6) -one_seven = cirq.GridQubit(1, 7) -one_eight = cirq.GridQubit(1, 8) -one_nine = cirq.GridQubit(1, 9) -one_ten = cirq.GridQubit(1, 10) - -physical = { - zero_zero: [zero_one, one_zero], - zero_one: [zero_zero, one_one, zero_two], - zero_two: [zero_one, one_two, zero_three], - zero_three: [zero_two, one_three, zero_four], - zero_four: [zero_three, one_four, zero_five], - zero_five: [zero_four, one_five, zero_six], - zero_six: [zero_five, one_six, zero_seven], - zero_seven: [zero_six, one_seven, zero_eight], - zero_eight: [zero_seven, one_eight, zero_nine], - zero_nine: [zero_eight, one_nine, zero_ten], - zero_ten: [zero_nine, one_ten], - one_zero: [one_one, zero_zero], - one_one: [one_zero, zero_one, one_two], - one_two: [one_one, zero_two, one_three], - one_three: [one_two, zero_three, one_four], - one_four: [one_three, zero_four, one_five], - one_five: [one_four, zero_five, one_six], - one_six: [one_five, zero_six, one_seven], - one_seven: [one_six, zero_seven, one_eight], - one_eight: [one_seven, zero_eight, one_nine], - one_nine: [one_eight, zero_nine, one_ten], - one_ten: [one_nine, zero_ten], -} +grid_qubits = dict( + (f'{row}_{col}', cirq.GridQubit(row, col)) + for row in range(2) for col in range(11) +) + def test_build_physical_qubits_graph(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) g = t.build_physical_qubits_graph() - assert len(g) == len(physical) - for q in physical: - assert set(physical[q]) == set(g[q]) + expected = { + grid_qubits['0_0']: [ + grid_qubits['0_1'], + grid_qubits['1_0'] + ], + grid_qubits['0_1']: [ + grid_qubits['0_0'], + grid_qubits['1_1'], + grid_qubits['0_2'], + ], + grid_qubits['0_2']: [ + grid_qubits['0_1'], + grid_qubits['1_2'], + grid_qubits['0_3'], + ], + grid_qubits['0_3']: [ + grid_qubits['0_2'], + grid_qubits['1_3'], + grid_qubits['0_4'], + ], + grid_qubits['0_4']: [ + grid_qubits['0_3'], + grid_qubits['1_4'], + grid_qubits['0_5'], + ], + grid_qubits['0_5']: [ + grid_qubits['0_4'], + grid_qubits['1_5'], + grid_qubits['0_6'], + ], + grid_qubits['0_6']: [ + grid_qubits['0_5'], + grid_qubits['1_6'], + grid_qubits['0_7'], + ], + grid_qubits['0_7']: [ + grid_qubits['0_6'], + grid_qubits['1_7'], + grid_qubits['0_8'], + ], + grid_qubits['0_8']: [ + grid_qubits['0_7'], + grid_qubits['1_8'], + grid_qubits['0_9'], + ], + grid_qubits['0_9']: [ + grid_qubits['0_8'], + grid_qubits['1_9'], + grid_qubits['0_10'], + ], + grid_qubits['0_10']: [ + grid_qubits['0_9'], + grid_qubits['1_10'], + ], + grid_qubits['1_0']: [ + grid_qubits['1_1'], + grid_qubits['0_0'], + ], + grid_qubits['1_1']: [ + grid_qubits['1_0'], + grid_qubits['0_1'], + grid_qubits['1_2'], + ], + grid_qubits['1_2']: [ + grid_qubits['1_1'], + grid_qubits['0_2'], + grid_qubits['1_3'], + ], + grid_qubits['1_3']: [ + grid_qubits['1_2'], + grid_qubits['0_3'], + grid_qubits['1_4'], + ], + grid_qubits['1_4']: [ + grid_qubits['1_3'], + grid_qubits['0_4'], + grid_qubits['1_5'], + ], + grid_qubits['1_5']: [ + grid_qubits['1_4'], + grid_qubits['0_5'], + grid_qubits['1_6'], + ], + grid_qubits['1_6']: [ + grid_qubits['1_5'], + grid_qubits['0_6'], + grid_qubits['1_7'], + ], + grid_qubits['1_7']: [ + grid_qubits['1_6'], + grid_qubits['0_7'], + grid_qubits['1_8'], + ], + grid_qubits['1_8']: [ + grid_qubits['1_7'], + grid_qubits['0_8'], + grid_qubits['1_9'], + ], + grid_qubits['1_9']: [ + grid_qubits['1_8'], + grid_qubits['0_9'], + grid_qubits['1_10'], + ], + grid_qubits['1_10']: [ + grid_qubits['1_9'], + grid_qubits['0_10'], + ], + } + assert len(g) == len(expected) + for q in expected: + assert set(g[q]) == set(expected[q]) + def test_get_least_connected_qubit(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) @@ -113,6 +162,7 @@ def test_get_least_connected_qubit(): assert t.get_least_connected_qubit(g, deque([0, 1, 2])) == 0 assert t.get_least_connected_qubit(g, deque([3, 4])) == 3 + def test_build_logical_qubits_graph(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) # One connected component. @@ -120,89 +170,147 @@ def test_build_logical_qubits_graph(): cirq.ISWAP(a2, a0), cirq.ISWAP(a0, a1), cirq.ISWAP(a0, a2), - cirq.ISWAP(a2, a1), cirq.ISWAP(a1, a2), cirq.ISWAP(a2, a3), - cirq.ISWAP(a0, a3), ) - assert t.build_logical_qubits_graph(c) == logical - # Three connected components. - c2 = cirq.Circuit( + assert t.build_logical_qubits_graph(c) == { + a0: [(a2, 0), (a1, 1)], + a1: [(a0, 1), (a2, 3)], + a2: [(a0, 0), (a1, 3), (a3, 4)], + a3: [(a2, 4)], + } + # Three connected components with one-qubit and two-qubit gates. + c = cirq.Circuit( cirq.ISWAP(a2, a0), cirq.ISWAP(a0, a1), cirq.ISWAP(a0, a2), - cirq.ISWAP(a2, a1), cirq.ISWAP(a1, a2), cirq.ISWAP(a2, a3), - cirq.ISWAP(a0, a3), cirq.ISWAP(a4, a5), cirq.ISWAP(a6, a7), ) - assert t.build_logical_qubits_graph(c2) == logical2 - c3 = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) - assert t.build_logical_qubits_graph(c3) == { + assert t.build_logical_qubits_graph(c) == { + a0: [(a2, 0), (a1, 1)], + a1: [(a0, 1), (a2, 3)], + a2: [(a0, 0), (a1, 3), (a3, 4)], + a3: [(a2, 4), (a7, 6)], + a4: [(a5, 0), (a6, 5)], + a5: [(a4, 0)], + a6: [(a7, 0), (a4, 5)], + a7: [(a6, 0), (a3, 6)], + } + # Three connected components with only one-qubit gates. + c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) + assert t.build_logical_qubits_graph(c) == { a1: [(a3, 2)], a2: [(a3, 1)], a3: [(a2, 1), (a1, 2)], } + def test_graph_center(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) - assert t.find_graph_center(physical) == zero_five - assert t.find_graph_center(logical) == a0 + g = { + 0: [1, 4], + 1: [0, 2, 4], + 2: [1, 4, 5], + 3: [4], + 4: [0, 1, 2, 3, 5], + 5: [2, 4], + } + assert t.find_graph_center(g) == 4 + g = { + 0: [(1,), (4,)], + 1: [(0,), (2,), (4,)], + 2: [(1,), (4,), (5,)], + 3: [(4,)], + 4: [(0,), (1,), (2,), (3,), (5,)], + 5: [(2,), (4,)], + } + assert t.find_graph_center(g) == 4 + def test_traverse(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) - assert t.traverse(logical, a0) == deque([a0, a2, a1, a3]) + g = { + 0: [(2, 0), (1, 1), (3, 6)], + 1: [(0, 1), (2, 3)], + 2: [(0, 0), (1, 3), (3, 5)], + 3: [(2, 5), (0, 6)], + } + assert t.traverse(g, 0) == deque([0, 2, 1, 3]) + assert t.traverse(g, 1) == deque([1, 0, 2, 3]) + assert t.traverse(g, 2) == deque([2, 0, 1, 3]) + assert t.traverse(g, 3) == deque([3, 2, 0, 1]) + def test_find_reference_qubits(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + g = { + a0: [(a2, 0), (a1, 1)], + a1: [(a0, 1), (a2, 3)], + a2: [(a0, 0), (a1, 3), (a3, 5)], + a3: [(a2, 5)], + } + mapping = { + a0: grid_qubits['0_5'], + } + assert set(t.find_reference_qubits(mapping, g, a2)) == { + grid_qubits['0_5'], + } mapping = { - a0: zero_five, + a0: grid_qubits['0_5'], + a2: grid_qubits['1_5'], + } + assert set(t.find_reference_qubits(mapping, g, a1)) == { + grid_qubits['0_5'], + grid_qubits['1_5'], } - assert t.find_reference_qubits(mapping, logical, a2) == [zero_five] + def test_find_candidate_qubits(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) + g = t.build_physical_qubits_graph() # First level has free qubits. - mapping = { - zero_five: "mapped", + mapped = { + grid_qubits['0_5'], } - assert t.find_candidate_qubits(mapping, physical, zero_five) == [ + assert set(t.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { cirq.GridQubit(0, 4), cirq.GridQubit(1, 5), cirq.GridQubit(0, 6), - ] + } # Second level has free qubits. - mapping = { - zero_five: "mapped", - zero_four: "mapped", - zero_six: "mapped", - one_five: "mapped", + mapped = { + grid_qubits['0_4'], + grid_qubits['0_5'], + grid_qubits['0_6'], + grid_qubits['1_5'], } - assert t.find_candidate_qubits(mapping, physical, zero_five) == [ + assert set(t.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { cirq.GridQubit(0, 3), cirq.GridQubit(1, 4), cirq.GridQubit(1, 6), cirq.GridQubit(0, 7), - ] + } # Third level has free qubits. - mapping = { - zero_three: "mapped", - zero_four: "mapped", - zero_five: "mapped", - zero_six: "mapped", - zero_seven: "mapped", - one_four: "mapped", - one_five: "mapped", - one_six: "mapped", - } - assert t.find_candidate_qubits(mapping, physical, zero_five) == [ + mapped = { + grid_qubits['0_3'], + grid_qubits['0_4'], + grid_qubits['0_5'], + grid_qubits['0_6'], + grid_qubits['0_7'], + grid_qubits['1_4'], + grid_qubits['1_5'], + grid_qubits['1_6'], + } + assert set(t.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { cirq.GridQubit(0, 2), cirq.GridQubit(1, 3), cirq.GridQubit(1, 7), cirq.GridQubit(0, 8), - ] + } + def test_find_shortest_path_distance(): t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) @@ -218,16 +326,5 @@ def test_find_shortest_path_distance(): 8: [2, 7], } assert t.find_shortest_path_distance(g, 0, 5) == 3 - -def test_calculate_initial_mapping(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) - c = cirq.Circuit( - cirq.ISWAP(a2, a0), - cirq.ISWAP(a0, a1), - cirq.ISWAP(a0, a2), - cirq.ISWAP(a2, a1), - cirq.ISWAP(a1, a2), - cirq.ISWAP(a2, a3), - cirq.ISWAP(a0, a3), - ) - print(t.transform(c)) + assert t.find_shortest_path_distance(g, 1, 8) == 2 + assert t.find_shortest_path_distance(g, 4, 7) == 3 From 823eb32415a56d0d740843ca33de30b3afd8b8b2 Mon Sep 17 00:00:00 2001 From: Wing Li <74674811+wingers@users.noreply.github.com> Date: Wed, 17 Feb 2021 19:18:38 -0800 Subject: [PATCH 4/6] Refactor the initial mapping logic into a utils file. --- recirq/quantum_chess/circuit_transformer.py | 384 +----------------- .../quantum_chess/circuit_transformer_test.py | 60 +-- recirq/quantum_chess/initial_mapping_utils.py | 383 +++++++++++++++++ ..._test.py => initial_mapping_utils_test.py} | 126 ++++-- 4 files changed, 523 insertions(+), 430 deletions(-) create mode 100644 recirq/quantum_chess/initial_mapping_utils.py rename recirq/quantum_chess/{dynamic_look_ahead_heuristic_circuit_transformer_test.py => initial_mapping_utils_test.py} (69%) diff --git a/recirq/quantum_chess/circuit_transformer.py b/recirq/quantum_chess/circuit_transformer.py index 07d252c0..1af1fa25 100644 --- a/recirq/quantum_chess/circuit_transformer.py +++ b/recirq/quantum_chess/circuit_transformer.py @@ -12,23 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. import copy -import math -from collections import defaultdict, deque -from typing import ( - Deque, - Dict, - Iterable, - List, - Optional, - Set, - Tuple, - Union, - ValuesView, -) +from typing import Dict, Iterable, List, Optional, Set import cirq import recirq.quantum_chess.controlled_iswap as controlled_iswap +import recirq.quantum_chess.initial_mapping_utils as imu ADJACENCY = [(0, 1), (0, -1), (1, 0), (-1, 0)] @@ -320,11 +309,23 @@ def transform(self, circuit: cirq.Circuit) -> cirq.Circuit: class DynamicLookAheadHeuristicCircuitTransformer(CircuitTransformer): """Optimizer that transforms a circuit to satify a device's constraints. - This implements the initial mapping algorithm and the SWAP update algorithm - proposed by the paper "A Dynamic Look-Ahead Heuristic for the Qubit Mapping - Problem of NISQ Computer: + This implements the initial mapping algorithm and the SWAP-based update + algorithm proposed by the paper "A Dynamic Look-Ahead Heuristic for the + Qubit Mapping Problem of NISQ Computer": https://ieeexplore.ieee.org/abstract/document/8976109. + The initial mapping algorithm first maps the center of the logical qubits + graph to the center of the physical qubits graph. It then traverses the + logical qubits in a breadth-first traversal order starting from the center + of the logical qubits graph. For each logical qubit, it finds the physical + qubit that minimizes the nearest neighbor distance for the leftmost gates. + + The SWAP-based update algorithm uses a heuristic cost function of a SWAP + operation called maximum consecutive positive effect (MCPE) to greedily + look ahead in each moment for SWAP operations that will reduce the nearest + neighbor distance for the largest number of gates in the current look-ahead + window. + Reference: P. Zhu, Z. Guan and X. Cheng, "A Dynamic Look-Ahead Heuristic for the Qubit Mapping Problem of NISQ Computers," in IEEE Transactions on Computer- @@ -335,351 +336,6 @@ def __init__(self, device: cirq.Device): super().__init__() self.device = device - def build_physical_qubits_graph( - self, - ) -> Dict[cirq.GridQubit, List[cirq.GridQubit]]: - """Returns an adjacency graph of physical qubits of the device. - - Each edge is bidirectional, and represents a valid two-qubit gate. - """ - g = defaultdict(list) - for q in self.device.qubit_set(): - neighbors = [n for n in q.neighbors() if n in self.device.qubit_set()] - for n in neighbors: - g[q].append(n) - return g - - def get_least_connected_qubit( - self, - g: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], - component: Deque[cirq.Qid], - ) -> cirq.Qid: - """Returns the least connected qubit. - - Args: - g: A logical qubits graph. - component: A deque of qubits belonging to the same component. - """ - return min(component, key=lambda q: len(g[q])) - - def build_logical_qubits_graph( - self, - circuit: cirq.Circuit, - ) -> Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]]: - """Returns an adjacency graph of logical qubits of the circuit. - - Uses the heuristic of adding an edge between the nodes of each - disjoint component that are least connected if the graph contains more - than one connected component. - - Each edge is a tuple containing an adjacent node and the index of the - moment at which the operation occurs. - - Arg: - circuit: The circuit from which to build a logical qubits graph. - """ - g = defaultdict(list) - moment_index = 0 - - # Build an adjacency graph based on the circuit. - for i, m in enumerate(circuit): - moment_index = i - for op in m: - if len(op.qubits) == 1: - q = op.qubits[0] - if q not in g: - g[q] = [] - if len(op.qubits) == 2: - q1, q2 = op.qubits - q1_neighbors = [n[0] for n in g[q1]] - if q2 not in q1_neighbors: - g[q1].append((q2, i)) - q2_neighbors = [n[0] for n in g[q2]] - if q1 not in q2_neighbors: - g[q2].append((q1, i)) - - # Find the connected components in the graph. - components = deque() - visited = set() - for q in g: - if q not in visited: - components.append(self.traverse(g, q, visited)) - - if len(components) == 1: - return g - - # Connect disjoint components by adding an edge between the nodes of - # each disjoint component that are least connected. - while len(components) > 1: - moment_index += 1 - first_comp = components.pop() - first_q = self.get_least_connected_qubit(g, first_comp) - second_comp = components.pop() - second_q = self.get_least_connected_qubit(g, second_comp) - - # Add an edge between the two least connected nodes. - g[first_q].append((second_q, moment_index)) - g[second_q].append((first_q, moment_index)) - - # Combine the two components and add it back to the components - # deque to continue connecting disjoint components. - first_comp += second_comp - components.append(first_comp) - - return g - - def find_graph_center( - self, - g: Union[ - Dict[cirq.GridQubit, List[cirq.GridQubit]], - Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], - ], - ) -> Union[cirq.GridQubit, cirq.Qid]: - """Returns a qubit that is a graph center. - - Uses the Floyd-Warshall algorithm to calculate the length of the - shortest path between each pair of nodes. Then, finds the graph center - such that the length of the shortest path to the farthest node is the - smallest. Returns the first graph center if there are multiple. - - Args: - g: A physical qubits graph or a logical qubits graph. - """ - qubit_to_index_mapping = defaultdict() - index_to_qubit_mapping = defaultdict() - for i, q in enumerate(g): - qubit_to_index_mapping[q] = i - index_to_qubit_mapping[i] = q - - v = len(g) - - # Use the Floyd–Warshall algorithm to calculate the length of the - # shortest path between each pair of nodes. - shortest = [[math.inf for j in range(v)] for i in range(v)] - for q in g: - i = qubit_to_index_mapping[q] - shortest[i][i] = 0 - for neighbor in g[q]: - if isinstance(neighbor, tuple): - neighbor = neighbor[0] - j = qubit_to_index_mapping[neighbor] - shortest[i][j] = 1 - for k in range(v): - for i in range(v): - for j in range(v): - shortest[i][j] = min(shortest[i][j], shortest[i][k] + shortest[k][j]) - - # For each node, find the length of the shortest path to the farthest - # node - farthest = [0 for i in range(v)] - for i in range(v): - for j in range(v): - if i != j and shortest[i][j] > farthest[i]: - farthest[i] = shortest[i][j] - - # Find the graph center such that the length of the shortest path to the - # farthest node is the smallest. Use the first graph center if there are - # multiple graph centers. - center = 0 - for i in range(v): - if farthest[i] < farthest[center]: - center = i - - return index_to_qubit_mapping[center] - - def traverse( - self, - g: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], - s: cirq.Qid, - visited: Optional[set] = None, - ) -> Deque[cirq.Qid]: - """Returns a deque of qubits ordered by breadth-first search traversal. - - During each iteration of breadth-first search, the adjacent nodes are - sorted by their corresponding moments before being traversed. - - Args: - g: A logical qubits graph. - s: The source qubit from which to start breadth-first search. - """ - order = deque() - if visited is None: - visited = set() - visited.add(s) - queue = deque() - queue.append(s) - while queue: - q = queue.popleft() - order.append(q) - neighbors_sorted_by_moment = sorted(g[q], key=lambda x: x[1]) - for neighbor in neighbors_sorted_by_moment: - neighbor = neighbor[0] - if neighbor not in visited: - visited.add(neighbor) - queue.append(neighbor) - return order - - def find_reference_qubits( - self, - mapping: Dict[cirq.Qid, cirq.GridQubit], - lg: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], - lq: cirq.Qid, - ) -> List[cirq.GridQubit]: - """Returns a list of physical qubits from which to find the next mapping. - - The nodes adjacent to the logical qubit parameter are sorted by their - corresponding moments before being traversed. For each adjacent node - that has been mapped to a physical qubit, the mapped physical qubit is - added to the result. - - Args: - mapping: The current mapping of logical qubits to physical qubits. - lg: A logical qubits graph. - lq: The logical qubit from which to find reference qubits. - """ - qubits = [] - neighbors_sorted_by_moment = sorted(lg[lq], key=lambda x: x[1]) - for neighbor in neighbors_sorted_by_moment: - neighbor = neighbor[0] - if neighbor in mapping: - # This neighbor has been mapped to a physical qubit. Add the - # physical qubit to reference qubits. - qubits.append(mapping[neighbor]) - return qubits - - def find_candidate_qubits( - self, - mapped: ValuesView[cirq.GridQubit], - pg: Dict[cirq.GridQubit, List[cirq.GridQubit]], - pq: cirq.GridQubit, - ) -> List[cirq.GridQubit]: - """Returns a list of physical qubits available to be mapped. - - Uses level order traversal until a level with free adjacent node(s) is - found. - - Args: - mapped: The set of currently mapped physical qubits. - lg: A physical qubits graph. - lq: The physical qubit from which to find candidate qubits. - """ - qubits = [] - visited = set() - visited.add(pq) - queue = deque() - queue.append(pq) - while queue: - level = len(queue) - while level > 0: - q = queue.popleft() - for neighbor in pg[q]: - if neighbor not in visited: - visited.add(neighbor) - queue.append(neighbor) - if neighbor not in mapped and neighbor not in qubits: - qubits.append(neighbor) - level -= 1 - if len(qubits) > 0: - break - return qubits - - def find_shortest_path_distance( - self, - g: Dict[cirq.GridQubit, List[cirq.GridQubit]], - s: cirq.GridQubit, - t: cirq.GridQubit, - ) -> int: - """Returns the shortest distance between the source and target qubits. - - Uses breadth-first search traversal. - - Args: - g: A physical qubits graph. - s: The source qubit from which to start breadth-first search. - t: The target qubit to search. - """ - dist = defaultdict(int) - visited = set() - visited.add(s) - queue = deque() - queue.append(s) - while queue: - q = queue.popleft() - if q == t: - return dist[t] - for neighbor in g[q]: - if neighbor not in visited: - dist[neighbor] = dist[q] + 1 - visited.add(neighbor) - queue.append(neighbor) - return math.inf - - def calculate_initial_mapping( - self, - pg: Dict[cirq.Qid, List[cirq.Qid]], - lg: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], - ) -> Dict[cirq.Qid, cirq.GridQubit]: - """Returns an initial mapping of logical qubits to physical qubits. - - This initial mapping algorithm is proposed by the paper "A Dynamic - Look-Ahead Heuristic for the Qubit Mapping Problem of NISQ Computer: - https://ieeexplore.ieee.org/abstract/document/8976109. - - Args: - pg: A physical qubits graph. - lg: A logical qubits graph. - """ - mapping = defaultdict() - - pg_center = self.find_graph_center(pg) - lg_center = self.find_graph_center(lg) - mapping[lg_center] = pg_center - - traversal_order = self.traverse(lg, lg_center) - - while traversal_order: - lq = traversal_order.popleft() - if lq == lg_center: - continue - pq = None - reference_qubits = self.find_reference_qubits(mapping, lg, lq) - ref_q = reference_qubits[0] - candidate_qubits = self.find_candidate_qubits(mapping.values(), pg, ref_q) - if len(reference_qubits) > 1: - # For each reference location, find the shortest path distance - # to each of the candidate qubits. Only keep the nearest - # candidate qubits with smallest distance. - for ref_q in reference_qubits[1:]: - distances = defaultdict(list) - for cand_q in candidate_qubits: - d = self.find_shortest_path_distance(pg, ref_q, cand_q) - distances[d].append(cand_q) - nearest_candidate_qubits = None - min_dist = math.inf - for dist in distances: - if dist < min_dist: - min_dist = dist - nearest_candidate_qubits = distances[dist] - candidate_qubits = nearest_candidate_qubits - if len(candidate_qubits) == 1: - break - if len(candidate_qubits) == 1: - pq = candidate_qubits[0] - # If there are still more than one candidate qubit at this point, - # choose the one with the closest degree to the logical qubit. - if len(candidate_qubits) > 1: - lq_degree = len(lg[lq]) - min_diff = math.inf - for cand_q in candidate_qubits: - cand_q_degree = len(pg[cand_q]) - diff = abs(cand_q_degree - lq_degree) - if diff < min_diff: - min_diff = diff - pq = cand_q - mapping[lq] = pq - - return mapping - def transform(self, circuit: cirq.Circuit) -> cirq.Circuit: """Returns a transformed circuit. @@ -689,10 +345,8 @@ def transform(self, circuit: cirq.Circuit) -> cirq.Circuit: Args: circuit: The circuit to transform. """ - pg = self.build_physical_qubits_graph() - lg = self.build_logical_qubits_graph(circuit) - initial_mapping = self.calculate_initial_mapping(pg, lg) - # TODO: change this to use the result of the SWAP update algorithm. + initial_mapping = imu.calculate_initial_mapping(self.device, circuit) + # TODO(wingers): change this to use the result of the SWAP update algorithm. return circuit.transform_qubits(lambda q: initial_mapping[q]) class SycamoreDecomposer(cirq.PointOptimizer): diff --git a/recirq/quantum_chess/circuit_transformer_test.py b/recirq/quantum_chess/circuit_transformer_test.py index cd8d871a..b1887c95 100644 --- a/recirq/quantum_chess/circuit_transformer_test.py +++ b/recirq/quantum_chess/circuit_transformer_test.py @@ -30,67 +30,67 @@ d1 = cirq.NamedQubit('d1') +@pytest.mark.parametrize('transformer', + [ct.ConnectivityHeuristicCircuitTransformer, + ct.DynamicLookAheadHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', - (cirq.google.Sycamore23, cirq.google.Sycamore)) -def test_single_qubit_ops(device): + [cirq.google.Sycamore23, cirq.google.Sycamore]) +def test_single_qubit_ops(transformer, device): c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) - t = ct.ConnectivityHeuristicCircuitTransformer(device) - device.validate_circuit(t.transform(c)) - t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + t = transformer(device) device.validate_circuit(t.transform(c)) +@pytest.mark.parametrize('transformer', + [ct.ConnectivityHeuristicCircuitTransformer, + ct.DynamicLookAheadHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', - (cirq.google.Sycamore23, cirq.google.Sycamore)) -def test_single_qubit_with_two_qubits(device): + [cirq.google.Sycamore23, cirq.google.Sycamore]) +def test_single_qubit_and_two_qubits_ops(transformer, device): c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3), cirq.ISWAP(a3, a4) ** 0.5) - t = ct.ConnectivityHeuristicCircuitTransformer(device) - device.validate_circuit(t.transform(c)) - t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + t = transformer(device) device.validate_circuit(t.transform(c)) +@pytest.mark.parametrize('transformer', + [ct.ConnectivityHeuristicCircuitTransformer, + ct.DynamicLookAheadHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', - (cirq.google.Sycamore23, cirq.google.Sycamore)) -def test_three_split_moves(device): + [cirq.google.Sycamore23, cirq.google.Sycamore]) +def test_three_split_moves(transformer, device): c = cirq.Circuit(qm.split_move(a1, a2, b1), qm.split_move(a2, a3, b3), qm.split_move(b1, c1, c2)) - t = ct.ConnectivityHeuristicCircuitTransformer(device) - device.validate_circuit(t.transform(c)) - t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + t = transformer(device) device.validate_circuit(t.transform(c)) +@pytest.mark.parametrize('transformer', [ct.ConnectivityHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', - (cirq.google.Sycamore23, cirq.google.Sycamore)) -def test_disconnected(device): + [cirq.google.Sycamore23, cirq.google.Sycamore]) +def test_disconnected(transformer, device): c = cirq.Circuit(qm.split_move(a1, a2, a3), qm.split_move(a3, a4, d1), qm.split_move(b1, b2, b3), qm.split_move(c1, c2, c3)) - t = ct.ConnectivityHeuristicCircuitTransformer(device) + t = transformer(device) device.validate_circuit(t.transform(c)) -# t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) -# device.validate_circuit(t.transform(c)) +@pytest.mark.parametrize('transformer', [ct.ConnectivityHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', - (cirq.google.Sycamore23, cirq.google.Sycamore)) -def test_move_around_square(device): + [cirq.google.Sycamore23, cirq.google.Sycamore]) +def test_move_around_square(transformer, device): c = cirq.Circuit(qm.normal_move(a1, a2), qm.normal_move(a2, b2), qm.normal_move(b2, b1), qm.normal_move(b1, a1)) - t = ct.ConnectivityHeuristicCircuitTransformer(device) + t = transformer(device) device.validate_circuit(t.transform(c)) -# t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) -# device.validate_circuit(t.transform(c)) +@pytest.mark.parametrize('transformer', [ct.ConnectivityHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', - (cirq.google.Sycamore23, cirq.google.Sycamore)) -def test_split_then_merge(device): + [cirq.google.Sycamore23, cirq.google.Sycamore]) +def test_split_then_merge(transformer, device): c = cirq.Circuit(qm.split_move(a1, a2, b1), qm.split_move(a2, a3, b3), qm.split_move(b1, c1, c2), qm.normal_move(c1, d1), qm.normal_move(a3, a4), qm.merge_move(a4, d1, a1)) - t = ct.ConnectivityHeuristicCircuitTransformer(device) + t = transformer(device) device.validate_circuit(t.transform(c)) -# t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) -# device.validate_circuit(t.transform(c)) diff --git a/recirq/quantum_chess/initial_mapping_utils.py b/recirq/quantum_chess/initial_mapping_utils.py new file mode 100644 index 00000000..34d82fe2 --- /dev/null +++ b/recirq/quantum_chess/initial_mapping_utils.py @@ -0,0 +1,383 @@ +# Copyright 2021 Google +# +# 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 math +from collections import defaultdict, deque +from typing import Deque, Dict, List, Optional, Tuple, Union, ValuesView + +import cirq + + +def build_physical_qubits_graph( + device: cirq.Device, +) -> Dict[cirq.GridQubit, List[cirq.GridQubit]]: + """Returns an adjacency graph of physical qubits of the device. + + Each edge is bidirectional, and represents a valid two-qubit gate. + + Args: + device: The device from which to build a physical qubits graph. + """ + g = defaultdict(list) + for q in device.qubit_set(): + g[q] = [n for n in q.neighbors() if n in device.qubit_set()] + return g + + +def get_least_connected_qubit( + g: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + component: Deque[cirq.Qid], +) -> cirq.Qid: + """Returns the least connected qubit. + + Args: + g: A logical qubits graph. + component: A deque of qubits belonging to the same component. + """ + return min(component, key=lambda q: len(g[q])) + + +def build_logical_qubits_graph( + circuit: cirq.Circuit, +) -> Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]]: + """Returns an adjacency graph of logical qubits of the circuit. + + Uses the heuristic of adding an edge between the nodes of each + disjoint component that are least connected if the graph contains more + than one connected component. + + Each edge is a tuple containing an adjacent node and the index of the + moment at which the operation occurs. + + Arg: + circuit: The circuit from which to build a logical qubits graph. + """ + g = defaultdict(list) + moment_index = 0 + + # Build an adjacency graph based on the circuit. + for i, m in enumerate(circuit): + moment_index = i + for op in m: + if isinstance(op.gate, cirq.MeasurementGate): + # Skip measurement gates. + continue + if len(op.qubits) == 1: + q = op.qubits[0] + if q not in g: + g[q] = [] + elif len(op.qubits) == 2: + q1, q2 = op.qubits + q1_neighbors = [n[0] for n in g[q1]] + if q2 not in q1_neighbors: + g[q1].append((q2, i)) + q2_neighbors = [n[0] for n in g[q2]] + if q1 not in q2_neighbors: + g[q2].append((q1, i)) + else: + raise ValueError(f'Operation {op} has more than 2 qubits!') + + # Find the connected components in the graph. + components = deque() + visited = set() + for q in g: + if q not in visited: + components.append(traverse(g, q, visited)) + + if len(components) == 1: + return g + + # Connect disjoint components by adding an edge between the nodes of + # each disjoint component that are least connected. + while len(components) > 1: + moment_index += 1 + first_comp = components.pop() + first_q = get_least_connected_qubit(g, first_comp) + second_comp = components.pop() + second_q = get_least_connected_qubit(g, second_comp) + + # Add an edge between the two least connected nodes. + g[first_q].append((second_q, moment_index)) + g[second_q].append((first_q, moment_index)) + + # Combine the two components and add it back to the components + # deque to continue connecting disjoint components. + first_comp += second_comp + components.append(first_comp) + + return g + + +def find_all_pairs_shortest_paths( + g: Union[ + Dict[cirq.GridQubit, List[cirq.GridQubit]], + Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + ], + v: int, + m: Dict[cirq.Qid, int], +) -> List[List[int]]: + """ Returns a matrix of the shortest distance between each pair of nodes. + + Implements the Floyd–Warshall algorithm. + + Args: + g: A physical qubits graph or a logical qubits graph. + v: The size of the graph. + m: A mapping of qubit to index. + """ + shortest = [[math.inf for j in range(v)] for i in range(v)] + for q in g: + i = m[q] + shortest[i][i] = 0 + for neighbor in g[q]: + if isinstance(neighbor, tuple): + neighbor = neighbor[0] + j = m[neighbor] + shortest[i][j] = 1 + for k in range(v): + for i in range(v): + for j in range(v): + shortest[i][j] = min(shortest[i][j], shortest[i][k] + shortest[k][j]) + return shortest + + +def find_graph_center( + g: Union[ + Dict[cirq.GridQubit, List[cirq.GridQubit]], + Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + ], +) -> cirq.Qid: + """Returns a qubit that is a graph center. + + Uses the Floyd-Warshall algorithm to calculate the length of the + shortest path between each pair of nodes. Then, finds the graph center + such that the length of the shortest path to the farthest node is the + smallest. Returns the first graph center if there are multiple. + + Args: + g: A physical qubits graph or a logical qubits graph. + """ + qubit_to_index_mapping = defaultdict() + index_to_qubit_mapping = defaultdict() + for i, q in enumerate(g): + qubit_to_index_mapping[q] = i + index_to_qubit_mapping[i] = q + + v = len(g) + + shortest = find_all_pairs_shortest_paths(g, v, qubit_to_index_mapping) + + # For each node, find the length of the shortest path to the farthest + # node. + farthest = [0 for i in range(v)] + for i in range(v): + for j in range(v): + if i != j and shortest[i][j] > farthest[i]: + farthest[i] = shortest[i][j] + + # Find the graph center such that the length of the shortest path to the + # farthest node is the smallest. Use the first graph center if there are + # multiple graph centers. + center = 0 + for i in range(v): + if farthest[i] < farthest[center]: + center = i + + return index_to_qubit_mapping[center] + + +def traverse( + g: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + s: cirq.Qid, + visited: Optional[set] = None, +) -> Deque[cirq.Qid]: + """Returns a deque of qubits ordered by breadth-first search traversal. + + During each iteration of breadth-first search, the adjacent nodes are + sorted by their corresponding moments before being traversed. + + Args: + g: A logical qubits graph. + s: The source qubit from which to start breadth-first search. + """ + order = deque() + if visited is None: + visited = set() + visited.add(s) + queue = deque() + queue.append(s) + while queue: + q = queue.popleft() + order.append(q) + neighbors_sorted_by_moment = sorted(g[q], key=lambda x: x[1]) + for neighbor, _ in neighbors_sorted_by_moment: + if neighbor not in visited: + visited.add(neighbor) + queue.append(neighbor) + return order + + +def find_reference_qubits( + mapping: Dict[cirq.Qid, cirq.GridQubit], + lg: Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], + lq: cirq.Qid, +) -> List[cirq.GridQubit]: + """Returns a list of physical qubits from which to find the next mapping. + + The nodes adjacent to the logical qubit parameter are sorted by their + corresponding moments before being traversed. For each adjacent node + that has been mapped to a physical qubit, the mapped physical qubit is + added to the result. + + Args: + mapping: The current mapping of logical qubits to physical qubits. + lg: A logical qubits graph. + lq: The logical qubit from which to find reference qubits. + """ + qubits = [] + neighbors_sorted_by_moment = sorted(lg[lq], key=lambda x: x[1]) + for neighbor, _ in neighbors_sorted_by_moment: + if neighbor in mapping: + # This neighbor has been mapped to a physical qubit. Add the + # physical qubit to reference qubits. + qubits.append(mapping[neighbor]) + return qubits + + +def find_candidate_qubits( + mapped: ValuesView[cirq.GridQubit], + pg: Dict[cirq.GridQubit, List[cirq.GridQubit]], + pq: cirq.GridQubit, +) -> List[cirq.GridQubit]: + """Returns a list of physical qubits available to be mapped. + + Uses level order traversal until a level with free adjacent node(s) is + found. + + Args: + mapped: The set of currently mapped physical qubits. + pg: A physical qubits graph. + pq: The physical qubit from which to find candidate qubits. + """ + qubits = [] + visited = set() + visited.add(pq) + queue = deque() + queue.append(pq) + while queue: + level = len(queue) + while level > 0: + q = queue.popleft() + for neighbor in pg[q]: + if neighbor not in visited: + visited.add(neighbor) + queue.append(neighbor) + if neighbor not in mapped and neighbor not in qubits: + qubits.append(neighbor) + level -= 1 + if len(qubits) > 0: + break + return qubits + + +def find_shortest_path( + g: Dict[cirq.GridQubit, List[cirq.GridQubit]], + s: cirq.GridQubit, + t: cirq.GridQubit, +) -> int: + """Returns the shortest distance between the source and target qubits. + + Uses breadth-first search traversal. + + Args: + g: A physical qubits graph. + s: The source qubit from which to start breadth-first search. + t: The target qubit to search. + """ + dist = defaultdict(int) + visited = set() + visited.add(s) + queue = deque() + queue.append(s) + while queue: + q = queue.popleft() + if q == t: + return dist[t] + for neighbor in g[q]: + if neighbor not in visited: + dist[neighbor] = dist[q] + 1 + visited.add(neighbor) + queue.append(neighbor) + return math.inf + + +def calculate_initial_mapping( + device: cirq.Device, + circuit: cirq.Circuit, +) -> Dict[cirq.Qid, cirq.GridQubit]: + """Returns an initial mapping of logical qubits to physical qubits. + + This initial mapping algorithm is proposed by the paper "A Dynamic + Look-Ahead Heuristic for the Qubit Mapping Problem of NISQ Computer: + https://ieeexplore.ieee.org/abstract/document/8976109. + + Args: + device: The device on which to run the circuit. + circuit: The circuit from which to calculate an initial mapping. + """ + mapping = defaultdict() + + pg = build_physical_qubits_graph(device) + lg = build_logical_qubits_graph(circuit) + pg_center = find_graph_center(pg) + lg_center = find_graph_center(lg) + mapping[lg_center] = pg_center + + traversal_order = traverse(lg, lg_center) + + while traversal_order: + lq = traversal_order.popleft() + if lq == lg_center: + continue + pq = None + reference_qubits = find_reference_qubits(mapping, lg, lq) + candidate_qubits = find_candidate_qubits(mapping.values(), pg, reference_qubits[0]) + if len(reference_qubits) > 1: + # For each reference location, find the shortest path distance + # to each of the candidate qubits. Only keep the nearest + # candidate qubits with smallest distance. + for ref_q in reference_qubits[1:]: + distances = defaultdict(list) + for cand_q in candidate_qubits: + d = find_shortest_path(pg, ref_q, cand_q) + distances[d].append(cand_q) + min_dist = min(distances.keys()) + candidate_qubits = distances[min_dist] + if len(candidate_qubits) == 1: + break + if len(candidate_qubits) == 1: + pq = candidate_qubits[0] + # If there are still more than one candidate qubit at this point, + # choose the one with the closest degree to the logical qubit. + if len(candidate_qubits) > 1: + lq_degree = len(lg[lq]) + min_diff = math.inf + for cand_q in candidate_qubits: + cand_q_degree = len(pg[cand_q]) + diff = abs(cand_q_degree - lq_degree) + if diff < min_diff: + min_diff = diff + pq = cand_q + mapping[lq] = pq + + return mapping diff --git a/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py b/recirq/quantum_chess/initial_mapping_utils_test.py similarity index 69% rename from recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py rename to recirq/quantum_chess/initial_mapping_utils_test.py index 5e09d5ee..006363ce 100644 --- a/recirq/quantum_chess/dynamic_look_ahead_heuristic_circuit_transformer_test.py +++ b/recirq/quantum_chess/initial_mapping_utils_test.py @@ -17,7 +17,7 @@ import cirq -import recirq.quantum_chess.circuit_transformer as ct +import recirq.quantum_chess.initial_mapping_utils as imu a0 = cirq.NamedQubit('a0') a1 = cirq.NamedQubit('a1') @@ -35,8 +35,7 @@ def test_build_physical_qubits_graph(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) - g = t.build_physical_qubits_graph() + g = imu.build_physical_qubits_graph(cirq.google.Foxtail) expected = { grid_qubits['0_0']: [ grid_qubits['0_1'], @@ -151,7 +150,6 @@ def test_build_physical_qubits_graph(): def test_get_least_connected_qubit(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) g = { 0: [1], 1: [0, 2], @@ -159,12 +157,11 @@ def test_get_least_connected_qubit(): 3: [4], 4: [3], } - assert t.get_least_connected_qubit(g, deque([0, 1, 2])) == 0 - assert t.get_least_connected_qubit(g, deque([3, 4])) == 3 + assert imu.get_least_connected_qubit(g, deque([0, 1, 2])) in {0, 2} + assert imu.get_least_connected_qubit(g, deque([3, 4])) in {3, 4} def test_build_logical_qubits_graph(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) # One connected component. c = cirq.Circuit( cirq.ISWAP(a2, a0), @@ -173,7 +170,7 @@ def test_build_logical_qubits_graph(): cirq.ISWAP(a1, a2), cirq.ISWAP(a2, a3), ) - assert t.build_logical_qubits_graph(c) == { + assert imu.build_logical_qubits_graph(c) == { a0: [(a2, 0), (a1, 1)], a1: [(a0, 1), (a2, 3)], a2: [(a0, 0), (a1, 3), (a3, 4)], @@ -187,29 +184,79 @@ def test_build_logical_qubits_graph(): cirq.ISWAP(a1, a2), cirq.ISWAP(a2, a3), cirq.ISWAP(a4, a5), - cirq.ISWAP(a6, a7), + cirq.X(a6), ) - assert t.build_logical_qubits_graph(c) == { + assert imu.build_logical_qubits_graph(c) == { a0: [(a2, 0), (a1, 1)], a1: [(a0, 1), (a2, 3)], a2: [(a0, 0), (a1, 3), (a3, 4)], - a3: [(a2, 4), (a7, 6)], + a3: [(a2, 4), (a6, 6)], a4: [(a5, 0), (a6, 5)], a5: [(a4, 0)], - a6: [(a7, 0), (a4, 5)], - a7: [(a6, 0), (a3, 6)], + a6: [(a4, 5), (a3, 6)], } # Three connected components with only one-qubit gates. c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3)) - assert t.build_logical_qubits_graph(c) == { + assert imu.build_logical_qubits_graph(c) == { a1: [(a3, 2)], a2: [(a3, 1)], a3: [(a2, 1), (a1, 2)], } + # Three connected components with a measurement gates. + c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.X(a3), cirq.measure(a1)) + assert imu.build_logical_qubits_graph(c) == { + a1: [(a3, 3)], + a2: [(a3, 2)], + a3: [(a2, 2), (a1, 3)], + } + # One connected component with an invalid gate. + with pytest.raises(ValueError, match='Operation.*has more than 2 qubits!'): + c = cirq.Circuit(cirq.X(a1), cirq.X(a2), cirq.CCNOT(a1, a2, a3)) + imu.build_logical_qubits_graph(c) + + +def test_find_all_pairs_shortest_paths(): + v = 7 + m = dict((x, x) for x in range(v)) + g = { + 0: [1], + 1: [0, 2], + 2: [1, 3, 5], + 3: [2, 4, 6], + 4: [3, 5], + 5: [2], + 6: [3], + } + assert imu.find_all_pairs_shortest_paths(g, v, m) == [ + [0, 1, 2, 3, 4, 3, 4], + [1, 0, 1, 2, 3, 2, 3], + [2, 1, 0, 1, 2, 1, 2], + [3, 2, 1, 0, 1, 2, 1], + [4, 3, 2, 1, 0, 1, 2], + [3, 2, 1, 2, 3, 0, 3], + [4, 3, 2, 1, 2, 3, 0], + ] + g = { + 0: [(1,)], + 1: [(0,), (2,)], + 2: [(1,), (3,), (5,)], + 3: [(2,), (4,), (6,)], + 4: [(3,), (5,)], + 5: [(2,)], + 6: [(3,)], + } + assert imu.find_all_pairs_shortest_paths(g, v, m) == [ + [0, 1, 2, 3, 4, 3, 4], + [1, 0, 1, 2, 3, 2, 3], + [2, 1, 0, 1, 2, 1, 2], + [3, 2, 1, 0, 1, 2, 1], + [4, 3, 2, 1, 0, 1, 2], + [3, 2, 1, 2, 3, 0, 3], + [4, 3, 2, 1, 2, 3, 0], + ] def test_graph_center(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) g = { 0: [1, 4], 1: [0, 2, 4], @@ -218,7 +265,7 @@ def test_graph_center(): 4: [0, 1, 2, 3, 5], 5: [2, 4], } - assert t.find_graph_center(g) == 4 + assert imu.find_graph_center(g) == 4 g = { 0: [(1,), (4,)], 1: [(0,), (2,), (4,)], @@ -227,25 +274,23 @@ def test_graph_center(): 4: [(0,), (1,), (2,), (3,), (5,)], 5: [(2,), (4,)], } - assert t.find_graph_center(g) == 4 + assert imu.find_graph_center(g) == 4 def test_traverse(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) g = { 0: [(2, 0), (1, 1), (3, 6)], 1: [(0, 1), (2, 3)], 2: [(0, 0), (1, 3), (3, 5)], 3: [(2, 5), (0, 6)], } - assert t.traverse(g, 0) == deque([0, 2, 1, 3]) - assert t.traverse(g, 1) == deque([1, 0, 2, 3]) - assert t.traverse(g, 2) == deque([2, 0, 1, 3]) - assert t.traverse(g, 3) == deque([3, 2, 0, 1]) + assert imu.traverse(g, 0) == deque([0, 2, 1, 3]) + assert imu.traverse(g, 1) == deque([1, 0, 2, 3]) + assert imu.traverse(g, 2) == deque([2, 0, 1, 3]) + assert imu.traverse(g, 3) == deque([3, 2, 0, 1]) def test_find_reference_qubits(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) g = { a0: [(a2, 0), (a1, 1)], a1: [(a0, 1), (a2, 3)], @@ -255,27 +300,26 @@ def test_find_reference_qubits(): mapping = { a0: grid_qubits['0_5'], } - assert set(t.find_reference_qubits(mapping, g, a2)) == { + assert set(imu.find_reference_qubits(mapping, g, a2)) == { grid_qubits['0_5'], } mapping = { a0: grid_qubits['0_5'], a2: grid_qubits['1_5'], } - assert set(t.find_reference_qubits(mapping, g, a1)) == { + assert set(imu.find_reference_qubits(mapping, g, a1)) == { grid_qubits['0_5'], grid_qubits['1_5'], } def test_find_candidate_qubits(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) - g = t.build_physical_qubits_graph() + g = imu.build_physical_qubits_graph(cirq.google.Foxtail) # First level has free qubits. mapped = { grid_qubits['0_5'], } - assert set(t.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { + assert set(imu.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { cirq.GridQubit(0, 4), cirq.GridQubit(1, 5), cirq.GridQubit(0, 6), @@ -287,7 +331,7 @@ def test_find_candidate_qubits(): grid_qubits['0_6'], grid_qubits['1_5'], } - assert set(t.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { + assert set(imu.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { cirq.GridQubit(0, 3), cirq.GridQubit(1, 4), cirq.GridQubit(1, 6), @@ -304,7 +348,7 @@ def test_find_candidate_qubits(): grid_qubits['1_5'], grid_qubits['1_6'], } - assert set(t.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { + assert set(imu.find_candidate_qubits(mapped, g, grid_qubits['0_5'])) == { cirq.GridQubit(0, 2), cirq.GridQubit(1, 3), cirq.GridQubit(1, 7), @@ -312,8 +356,7 @@ def test_find_candidate_qubits(): } -def test_find_shortest_path_distance(): - t = ct.DynamicLookAheadHeuristicCircuitTransformer(cirq.google.Foxtail) +def test_find_shortest_path(): g = { 0: [1, 7], 1: [0, 2, 7], @@ -325,6 +368,19 @@ def test_find_shortest_path_distance(): 7: [0, 1, 6, 8], 8: [2, 7], } - assert t.find_shortest_path_distance(g, 0, 5) == 3 - assert t.find_shortest_path_distance(g, 1, 8) == 2 - assert t.find_shortest_path_distance(g, 4, 7) == 3 + assert imu.find_shortest_path(g, 0, 5) == 3 + assert imu.find_shortest_path(g, 1, 8) == 2 + assert imu.find_shortest_path(g, 4, 7) == 3 + + +@pytest.mark.parametrize('device', [ + cirq.google.Sycamore23, cirq.google.Sycamore +]) +def test_calculate_initial_mapping(device): + c = cirq.Circuit( + cirq.X(a1), + cirq.X(a2), + cirq.ISWAP(a0, a2) ** 0.5, + ) + mapping = imu.calculate_initial_mapping(device, c) + device.validate_circuit(c.transform_qubits(lambda q: mapping[q])) From fdcda5f48fc8cef5628d987a5db8f1cd1acd736e Mon Sep 17 00:00:00 2001 From: Wing Li <74674811+wingers@users.noreply.github.com> Date: Thu, 4 Mar 2021 17:58:39 -0800 Subject: [PATCH 5/6] Address comments; integrate with SwapUpdater. --- recirq/quantum_chess/circuit_transformer.py | 5 +- .../quantum_chess/circuit_transformer_test.py | 21 +++- recirq/quantum_chess/initial_mapping_utils.py | 52 ++++------ .../initial_mapping_utils_test.py | 95 ++++++++++++++----- 4 files changed, 111 insertions(+), 62 deletions(-) diff --git a/recirq/quantum_chess/circuit_transformer.py b/recirq/quantum_chess/circuit_transformer.py index 1af1fa25..1eb6f233 100644 --- a/recirq/quantum_chess/circuit_transformer.py +++ b/recirq/quantum_chess/circuit_transformer.py @@ -18,6 +18,7 @@ import recirq.quantum_chess.controlled_iswap as controlled_iswap import recirq.quantum_chess.initial_mapping_utils as imu +import recirq.quantum_chess.swap_updater as su ADJACENCY = [(0, 1), (0, -1), (1, 0), (-1, 0)] @@ -346,8 +347,8 @@ def transform(self, circuit: cirq.Circuit) -> cirq.Circuit: circuit: The circuit to transform. """ initial_mapping = imu.calculate_initial_mapping(self.device, circuit) - # TODO(wingers): change this to use the result of the SWAP update algorithm. - return circuit.transform_qubits(lambda q: initial_mapping[q]) + updater = su.SwapUpdater(circuit, self.device.qubit_set(), initial_mapping) + return cirq.Circuit(updater.add_swaps()) class SycamoreDecomposer(cirq.PointOptimizer): """Optimizer that decomposes all three qubit operations into diff --git a/recirq/quantum_chess/circuit_transformer_test.py b/recirq/quantum_chess/circuit_transformer_test.py index b1887c95..c04a29cd 100644 --- a/recirq/quantum_chess/circuit_transformer_test.py +++ b/recirq/quantum_chess/circuit_transformer_test.py @@ -65,7 +65,9 @@ def test_three_split_moves(transformer, device): device.validate_circuit(t.transform(c)) -@pytest.mark.parametrize('transformer', [ct.ConnectivityHeuristicCircuitTransformer]) +@pytest.mark.parametrize('transformer', + [ct.ConnectivityHeuristicCircuitTransformer, + ct.DynamicLookAheadHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', [cirq.google.Sycamore23, cirq.google.Sycamore]) def test_disconnected(transformer, device): @@ -75,7 +77,9 @@ def test_disconnected(transformer, device): device.validate_circuit(t.transform(c)) -@pytest.mark.parametrize('transformer', [ct.ConnectivityHeuristicCircuitTransformer]) +@pytest.mark.parametrize('transformer', + [ct.ConnectivityHeuristicCircuitTransformer, + ct.DynamicLookAheadHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', [cirq.google.Sycamore23, cirq.google.Sycamore]) def test_move_around_square(transformer, device): @@ -85,7 +89,9 @@ def test_move_around_square(transformer, device): device.validate_circuit(t.transform(c)) -@pytest.mark.parametrize('transformer', [ct.ConnectivityHeuristicCircuitTransformer]) +@pytest.mark.parametrize('transformer', + [ct.ConnectivityHeuristicCircuitTransformer, + ct.DynamicLookAheadHeuristicCircuitTransformer]) @pytest.mark.parametrize('device', [cirq.google.Sycamore23, cirq.google.Sycamore]) def test_split_then_merge(transformer, device): @@ -94,3 +100,12 @@ def test_split_then_merge(transformer, device): qm.normal_move(a3, a4), qm.merge_move(a4, d1, a1)) t = transformer(device) device.validate_circuit(t.transform(c)) + + +@pytest.mark.parametrize('device', + [cirq.google.Sycamore23, cirq.google.Sycamore]) +def test_split_then_merge_trapezoid(device): + c = cirq.Circuit(qm.split_move(a1, a2, b1), qm.normal_move(a2, a3), + qm.merge_move(a3, b1, b3)) + t = ct.DynamicLookAheadHeuristicCircuitTransformer(device) + device.validate_circuit(t.transform(c)) diff --git a/recirq/quantum_chess/initial_mapping_utils.py b/recirq/quantum_chess/initial_mapping_utils.py index 34d82fe2..3f8249fc 100644 --- a/recirq/quantum_chess/initial_mapping_utils.py +++ b/recirq/quantum_chess/initial_mapping_utils.py @@ -123,10 +123,8 @@ def find_all_pairs_shortest_paths( Dict[cirq.GridQubit, List[cirq.GridQubit]], Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], ], - v: int, - m: Dict[cirq.Qid, int], ) -> List[List[int]]: - """ Returns a matrix of the shortest distance between each pair of nodes. + """Returns a matrix of the shortest distance between each pair of nodes. Implements the Floyd–Warshall algorithm. @@ -135,20 +133,18 @@ def find_all_pairs_shortest_paths( v: The size of the graph. m: A mapping of qubit to index. """ - shortest = [[math.inf for j in range(v)] for i in range(v)] + dist = defaultdict(lambda: math.inf) for q in g: - i = m[q] - shortest[i][i] = 0 + dist[(q, q)] = 0 for neighbor in g[q]: if isinstance(neighbor, tuple): neighbor = neighbor[0] - j = m[neighbor] - shortest[i][j] = 1 - for k in range(v): - for i in range(v): - for j in range(v): - shortest[i][j] = min(shortest[i][j], shortest[i][k] + shortest[k][j]) - return shortest + dist[(q, neighbor)] = 1 + for k in g: + for i in g: + for j in g: + dist[(i, j)] = min(dist[(i, j)], dist[(i, k)] + dist[(k, j)]) + return dist def find_graph_center( @@ -167,33 +163,25 @@ def find_graph_center( Args: g: A physical qubits graph or a logical qubits graph. """ - qubit_to_index_mapping = defaultdict() - index_to_qubit_mapping = defaultdict() - for i, q in enumerate(g): - qubit_to_index_mapping[q] = i - index_to_qubit_mapping[i] = q - - v = len(g) - - shortest = find_all_pairs_shortest_paths(g, v, qubit_to_index_mapping) + shortest = find_all_pairs_shortest_paths(g) # For each node, find the length of the shortest path to the farthest # node. - farthest = [0 for i in range(v)] - for i in range(v): - for j in range(v): - if i != j and shortest[i][j] > farthest[i]: - farthest[i] = shortest[i][j] + farthest = defaultdict(int) + for i in g: + for j in g: + if i != j and shortest[(i, j)] > farthest[i]: + farthest[i] = shortest[(i, j)] # Find the graph center such that the length of the shortest path to the # farthest node is the smallest. Use the first graph center if there are # multiple graph centers. - center = 0 - for i in range(v): - if farthest[i] < farthest[center]: - center = i + center = None + for q in g: + if not center or farthest[q] < farthest[center]: + center = q - return index_to_qubit_mapping[center] + return center def traverse( diff --git a/recirq/quantum_chess/initial_mapping_utils_test.py b/recirq/quantum_chess/initial_mapping_utils_test.py index 006363ce..17a1a6d9 100644 --- a/recirq/quantum_chess/initial_mapping_utils_test.py +++ b/recirq/quantum_chess/initial_mapping_utils_test.py @@ -150,6 +150,13 @@ def test_build_physical_qubits_graph(): def test_get_least_connected_qubit(): + g = { + 0: [1, 2], + 1: [0, 2], + 2: [0, 1, 3], + 3: [2], + } + assert imu.get_least_connected_qubit(g, deque([0, 1, 2, 3])) == 3 g = { 0: [1], 1: [0, 2], @@ -215,10 +222,8 @@ def test_build_logical_qubits_graph(): imu.build_logical_qubits_graph(c) -def test_find_all_pairs_shortest_paths(): - v = 7 - m = dict((x, x) for x in range(v)) - g = { +@pytest.mark.parametrize('g', [ + { 0: [1], 1: [0, 2], 2: [1, 3, 5], @@ -226,17 +231,8 @@ def test_find_all_pairs_shortest_paths(): 4: [3, 5], 5: [2], 6: [3], - } - assert imu.find_all_pairs_shortest_paths(g, v, m) == [ - [0, 1, 2, 3, 4, 3, 4], - [1, 0, 1, 2, 3, 2, 3], - [2, 1, 0, 1, 2, 1, 2], - [3, 2, 1, 0, 1, 2, 1], - [4, 3, 2, 1, 0, 1, 2], - [3, 2, 1, 2, 3, 0, 3], - [4, 3, 2, 1, 2, 3, 0], - ] - g = { + }, + { 0: [(1,)], 1: [(0,), (2,)], 2: [(1,), (3,), (5,)], @@ -244,16 +240,64 @@ def test_find_all_pairs_shortest_paths(): 4: [(3,), (5,)], 5: [(2,)], 6: [(3,)], + }, +]) +def test_find_all_pairs_shortest_paths(g): + expected = { + (0, 0): 0, + (0, 1): 1, + (0, 2): 2, + (0, 3): 3, + (0, 4): 4, + (0, 5): 3, + (0, 6): 4, + (1, 0): 1, + (1, 1): 0, + (1, 2): 1, + (1, 3): 2, + (1, 4): 3, + (1, 5): 2, + (1, 6): 3, + (2, 0): 2, + (2, 1): 1, + (2, 2): 0, + (2, 3): 1, + (2, 4): 2, + (2, 5): 1, + (2, 6): 2, + (3, 0): 3, + (3, 1): 2, + (3, 2): 1, + (3, 3): 0, + (3, 4): 1, + (3, 5): 2, + (3, 6): 1, + (4, 0): 4, + (4, 1): 3, + (4, 2): 2, + (4, 3): 1, + (4, 4): 0, + (4, 5): 1, + (4, 6): 2, + (5, 0): 3, + (5, 1): 2, + (5, 2): 1, + (5, 3): 2, + (5, 4): 3, + (5, 5): 0, + (5, 6): 3, + (6, 0): 4, + (6, 1): 3, + (6, 2): 2, + (6, 3): 1, + (6, 4): 2, + (6, 5): 3, + (6, 6): 0, } - assert imu.find_all_pairs_shortest_paths(g, v, m) == [ - [0, 1, 2, 3, 4, 3, 4], - [1, 0, 1, 2, 3, 2, 3], - [2, 1, 0, 1, 2, 1, 2], - [3, 2, 1, 0, 1, 2, 1], - [4, 3, 2, 1, 0, 1, 2], - [3, 2, 1, 2, 3, 0, 3], - [4, 3, 2, 1, 2, 3, 0], - ] + result = imu.find_all_pairs_shortest_paths(g) + assert len(result) == len(expected) + for k in expected: + assert result[k] == expected[k] def test_graph_center(): @@ -374,7 +418,8 @@ def test_find_shortest_path(): @pytest.mark.parametrize('device', [ - cirq.google.Sycamore23, cirq.google.Sycamore + cirq.google.Sycamore23, + cirq.google.Sycamore, ]) def test_calculate_initial_mapping(device): c = cirq.Circuit( From c95be9ac2c6f63469fb9988c51c406410a7b239d Mon Sep 17 00:00:00 2001 From: Wing Li <74674811+wingers@users.noreply.github.com> Date: Thu, 4 Mar 2021 21:32:12 -0800 Subject: [PATCH 6/6] Fix type annotation and comment. --- recirq/quantum_chess/initial_mapping_utils.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/recirq/quantum_chess/initial_mapping_utils.py b/recirq/quantum_chess/initial_mapping_utils.py index 3f8249fc..80b0f2e9 100644 --- a/recirq/quantum_chess/initial_mapping_utils.py +++ b/recirq/quantum_chess/initial_mapping_utils.py @@ -123,15 +123,13 @@ def find_all_pairs_shortest_paths( Dict[cirq.GridQubit, List[cirq.GridQubit]], Dict[cirq.Qid, List[Tuple[cirq.Qid, int]]], ], -) -> List[List[int]]: - """Returns a matrix of the shortest distance between each pair of nodes. +) -> Dict[Tuple[cirq.Qid, cirq.Qid], int]: + """Returns a dict of the shortest distance between each pair of nodes. Implements the Floyd–Warshall algorithm. Args: g: A physical qubits graph or a logical qubits graph. - v: The size of the graph. - m: A mapping of qubit to index. """ dist = defaultdict(lambda: math.inf) for q in g: