diff --git a/recirq/quantum_chess/circuit_transformer.py b/recirq/quantum_chess/circuit_transformer.py index 359cecd7..1eb6f233 100644 --- a/recirq/quantum_chess/circuit_transformer.py +++ b/recirq/quantum_chess/circuit_transformer.py @@ -17,6 +17,8 @@ import cirq 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)] @@ -305,6 +307,49 @@ 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-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- + 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 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. + """ + initial_mapping = imu.calculate_initial_mapping(self.device, circuit) + 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 sqrt-ISWAPs. diff --git a/recirq/quantum_chess/circuit_transformer_test.py b/recirq/quantum_chess/circuit_transformer_test.py index 49b6514c..c04a29cd 100644 --- a/recirq/quantum_chess/circuit_transformer_test.py +++ b/recirq/quantum_chess/circuit_transformer_test.py @@ -30,62 +30,82 @@ 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): - transformer = ct.ConnectivityHeuristicCircuitTransformer(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)) - transformer.qubit_mapping(c) - c = transformer.transform(c) - device.validate_circuit(c) + 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): - transformer = ct.ConnectivityHeuristicCircuitTransformer(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) - transformer.qubit_mapping(c) - device.validate_circuit(transformer.transform(c)) + 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): - transformer = ct.ConnectivityHeuristicCircuitTransformer(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)) - transformer.qubit_mapping(c) - device.validate_circuit(transformer.transform(c)) + 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_disconnected(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(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)) - transformer.qubit_mapping(c) - device.validate_circuit(transformer.transform(c)) + 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_move_around_square(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(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)) - transformer.qubit_mapping(c) - device.validate_circuit(transformer.transform(c)) + 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_split_then_merge(device): - transformer = ct.ConnectivityHeuristicCircuitTransformer(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)) - transformer.qubit_mapping(c) - device.validate_circuit(transformer.transform(c)) + 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 new file mode 100644 index 00000000..80b0f2e9 --- /dev/null +++ b/recirq/quantum_chess/initial_mapping_utils.py @@ -0,0 +1,369 @@ +# 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]]], + ], +) -> 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. + """ + dist = defaultdict(lambda: math.inf) + for q in g: + dist[(q, q)] = 0 + for neighbor in g[q]: + if isinstance(neighbor, tuple): + neighbor = neighbor[0] + 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( + 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. + """ + shortest = find_all_pairs_shortest_paths(g) + + # For each node, find the length of the shortest path to the farthest + # node. + 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 = None + for q in g: + if not center or farthest[q] < farthest[center]: + center = q + + return 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/initial_mapping_utils_test.py b/recirq/quantum_chess/initial_mapping_utils_test.py new file mode 100644 index 00000000..17a1a6d9 --- /dev/null +++ b/recirq/quantum_chess/initial_mapping_utils_test.py @@ -0,0 +1,431 @@ +# 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.initial_mapping_utils as imu + +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') + +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(): + g = imu.build_physical_qubits_graph(cirq.google.Foxtail) + 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(): + 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], + 2: [1], + 3: [4], + 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(): + # One connected component. + c = cirq.Circuit( + cirq.ISWAP(a2, a0), + cirq.ISWAP(a0, a1), + cirq.ISWAP(a0, a2), + cirq.ISWAP(a1, a2), + cirq.ISWAP(a2, a3), + ) + 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)], + } + # 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(a1, a2), + cirq.ISWAP(a2, a3), + cirq.ISWAP(a4, a5), + cirq.X(a6), + ) + 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), (a6, 6)], + a4: [(a5, 0), (a6, 5)], + a5: [(a4, 0)], + 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 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) + + +@pytest.mark.parametrize('g', [ + { + 0: [1], + 1: [0, 2], + 2: [1, 3, 5], + 3: [2, 4, 6], + 4: [3, 5], + 5: [2], + 6: [3], + }, + { + 0: [(1,)], + 1: [(0,), (2,)], + 2: [(1,), (3,), (5,)], + 3: [(2,), (4,), (6,)], + 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, + } + 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(): + g = { + 0: [1, 4], + 1: [0, 2, 4], + 2: [1, 4, 5], + 3: [4], + 4: [0, 1, 2, 3, 5], + 5: [2, 4], + } + assert imu.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 imu.find_graph_center(g) == 4 + + +def test_traverse(): + 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 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(): + 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(imu.find_reference_qubits(mapping, g, a2)) == { + grid_qubits['0_5'], + } + mapping = { + a0: grid_qubits['0_5'], + a2: grid_qubits['1_5'], + } + assert set(imu.find_reference_qubits(mapping, g, a1)) == { + grid_qubits['0_5'], + grid_qubits['1_5'], + } + + +def test_find_candidate_qubits(): + g = imu.build_physical_qubits_graph(cirq.google.Foxtail) + # First level has free qubits. + mapped = { + 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), + } + # Second level has free qubits. + mapped = { + grid_qubits['0_4'], + grid_qubits['0_5'], + grid_qubits['0_6'], + grid_qubits['1_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), + cirq.GridQubit(0, 7), + } + # Third level has free qubits. + 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(imu.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(): + 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 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]))