|
| 1 | +# Copyright 2022 The Cirq Developers |
| 2 | +# |
| 3 | +# Licensed under the Apache License, Version 2.0 (the "License"); |
| 4 | +# you may not use this file except in compliance with the License. |
| 5 | +# You may obtain a copy of the License at |
| 6 | +# |
| 7 | +# https://www.apache.org/licenses/LICENSE-2.0 |
| 8 | +# |
| 9 | +# Unless required by applicable law or agreed to in writing, software |
| 10 | +# distributed under the License is distributed on an "AS IS" BASIS, |
| 11 | +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| 12 | +# See the License for the specific language governing permissions and |
| 13 | +# limitations under the License. |
| 14 | + |
| 15 | +"""Maps logical to physical qubits by greedily placing lines of logical qubits on the device. |
| 16 | +
|
| 17 | +This is the default placement strategy used in the CQC router. |
| 18 | +
|
| 19 | +It first creates a partial connectivity graph between logical qubits in the given circuit and then |
| 20 | +maps these logical qubits on physical qubits on the device by starting at the center of the device |
| 21 | +and greedily choosing the highest degree neighbor. |
| 22 | +
|
| 23 | +If some logical qubits are unampped after this first procedure then there are two cases: |
| 24 | + (1) These unmammep logical qubits do interact in the circuit with some other logical partner. |
| 25 | + In this case we map such a qubit to the nearest available physical qubit on the device to the |
| 26 | + one that its partner was mapped to. |
| 27 | +
|
| 28 | + (2) These unampped logical qubits only have single qubit operations on them (i.e they do not |
| 29 | + interact with any other logical qubit at any point in the circuit). In this case we map them to |
| 30 | + the nearest available neighbor to the center of the device. |
| 31 | +""" |
| 32 | + |
| 33 | +from typing import Deque, Dict, List, Set, Tuple, TYPE_CHECKING |
| 34 | +from collections import deque |
| 35 | +import networkx as nx |
| 36 | + |
| 37 | +from cirq.transformers.routing import initial_mapper |
| 38 | +from cirq import protocols, value |
| 39 | + |
| 40 | +if TYPE_CHECKING: |
| 41 | + import cirq |
| 42 | + |
| 43 | + |
| 44 | +@value.value_equality |
| 45 | +class LineInitialMapper(initial_mapper.AbstractInitialMapper): |
| 46 | + """Places logical qubits in the circuit onto physical qubits on the device. |
| 47 | +
|
| 48 | + Starting from the center physical qubit on the device, attempts to map disjoint lines of |
| 49 | + logical qubits given by the circuit graph onto one long line of physical qubits on the |
| 50 | + device, greedily maximizing each physical qubit's degree. |
| 51 | + If this mapping cannot be completed as one long line of qubits in the circuit graph mapped |
| 52 | + to qubits in the device graph, the line can be split as several line segments and then we: |
| 53 | + (i) Map first line segment. |
| 54 | + (ii) Find another high degree vertex in G near the center. |
| 55 | + (iii) Map the second line segment |
| 56 | + (iv) etc. |
| 57 | + A line is split by mapping the next logical qubit to the nearest available physical qubit |
| 58 | + to the center of the device graph. |
| 59 | +
|
| 60 | + The expected runtime of this strategy is O(m logn + n^2) where m is the # of operations in the |
| 61 | + given circuit and n is the number of qubits. The first term corresponds to the runtime of |
| 62 | + 'make_circuit_graph()' and the second for 'initial_mapping()'. |
| 63 | + """ |
| 64 | + |
| 65 | + def __init__(self, device_graph: nx.Graph) -> None: |
| 66 | + """Initializes a LineInitialMapper. |
| 67 | +
|
| 68 | + Args: |
| 69 | + device_graph: device graph |
| 70 | + """ |
| 71 | + if nx.is_directed(device_graph): |
| 72 | + self.device_graph = nx.DiGraph() |
| 73 | + self.device_graph.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) |
| 74 | + self.device_graph.add_edges_from(sorted(list(device_graph.edges))) |
| 75 | + else: |
| 76 | + self.device_graph = nx.Graph() |
| 77 | + self.device_graph.add_nodes_from(sorted(list(device_graph.nodes(data=True)))) |
| 78 | + self.device_graph.add_edges_from( |
| 79 | + sorted(list(sorted(edge) for edge in device_graph.edges)) |
| 80 | + ) |
| 81 | + self.center = nx.center(self.device_graph)[0] |
| 82 | + |
| 83 | + def _make_circuit_graph( |
| 84 | + self, circuit: 'cirq.AbstractCircuit' |
| 85 | + ) -> Tuple[List[Deque['cirq.Qid']], Dict['cirq.Qid', 'cirq.Qid']]: |
| 86 | + """Creates a (potentially incomplete) qubit connectivity graph of the circuit. |
| 87 | +
|
| 88 | + Iterates over moments in the circuit from left to right and adds edges between logical |
| 89 | + qubits if the logical qubit pair l1 and l2 |
| 90 | + (1) have degree < 2, |
| 91 | + (2) are involved in a 2-qubit operation in the current moment, and |
| 92 | + (3) adding such an edge will not produce a cycle in the graph. |
| 93 | +
|
| 94 | + Args: |
| 95 | + circuit: the input circuit with logical qubits |
| 96 | +
|
| 97 | + Returns: |
| 98 | + The (potentially incomplete) qubit connectivity graph of the circuit, which is |
| 99 | + guaranteed to be a forest of line graphs. |
| 100 | + """ |
| 101 | + circuit_graph: List[Deque['cirq.Qid']] = [deque([q]) for q in sorted(circuit.all_qubits())] |
| 102 | + component_id: Dict['cirq.Qid', int] = {q[0]: i for i, q in enumerate(circuit_graph)} |
| 103 | + partners: Dict['cirq.Qid', 'cirq.Qid'] = {} |
| 104 | + |
| 105 | + def degree_lt_two(q: 'cirq.Qid'): |
| 106 | + return any(circuit_graph[component_id[q]][i] == q for i in [-1, 0]) |
| 107 | + |
| 108 | + for op in circuit.all_operations(): |
| 109 | + if protocols.num_qubits(op) != 2: |
| 110 | + continue |
| 111 | + |
| 112 | + q0, q1 = op.qubits |
| 113 | + c0, c1 = component_id[q0], component_id[q1] |
| 114 | + # Keep track of partners for mapping isolated qubits later. |
| 115 | + partners[q0] = partners[q0] if q0 in partners else q1 |
| 116 | + partners[q1] = partners[q1] if q1 in partners else q0 |
| 117 | + |
| 118 | + if not (degree_lt_two(q0) and degree_lt_two(q1) and c0 != c1): |
| 119 | + continue |
| 120 | + |
| 121 | + # Make sure c0/q0 are for the largest component. |
| 122 | + if len(circuit_graph[c0]) < len(circuit_graph[c1]): |
| 123 | + c0, c1, q0, q1 = c1, c0, q1, q0 |
| 124 | + |
| 125 | + # copy smaller component into larger one. |
| 126 | + c1_order = ( |
| 127 | + reversed(circuit_graph[c1]) |
| 128 | + if circuit_graph[c1][-1] == q1 |
| 129 | + else iter(circuit_graph[c1]) |
| 130 | + ) |
| 131 | + for q in c1_order: |
| 132 | + if circuit_graph[c0][0] == q0: |
| 133 | + circuit_graph[c0].appendleft(q) |
| 134 | + else: |
| 135 | + circuit_graph[c0].append(q) |
| 136 | + component_id[q] = c0 |
| 137 | + |
| 138 | + graph = sorted( |
| 139 | + [circuit_graph[c] for c in set(component_id.values())], key=len, reverse=True |
| 140 | + ) |
| 141 | + return graph, partners |
| 142 | + |
| 143 | + def initial_mapping(self, circuit: 'cirq.AbstractCircuit') -> Dict['cirq.Qid', 'cirq.Qid']: |
| 144 | + """Maps disjoint lines of logical qubits onto lines of physical qubits. |
| 145 | +
|
| 146 | + Args: |
| 147 | + circuit: the input circuit with logical qubits |
| 148 | +
|
| 149 | + Returns: |
| 150 | + a dictionary that maps logical qubits in the circuit (keys) to physical qubits on the |
| 151 | + device (values). |
| 152 | + """ |
| 153 | + mapped_physicals: Set['cirq.Qid'] = set() |
| 154 | + qubit_map: Dict['cirq.Qid', 'cirq.Qid'] = {} |
| 155 | + circuit_graph, partners = self._make_circuit_graph(circuit) |
| 156 | + |
| 157 | + def next_physical( |
| 158 | + current_physical: 'cirq.Qid', partner: 'cirq.Qid', isolated: bool = False |
| 159 | + ) -> 'cirq.Qid': |
| 160 | + # Handle the first physical qubit getting mapped. |
| 161 | + if current_physical not in mapped_physicals: |
| 162 | + return current_physical |
| 163 | + # Greedily map to highest degree neighbor that is available |
| 164 | + if not isolated: |
| 165 | + sorted_neighbors = sorted( |
| 166 | + self.device_graph.neighbors(current_physical), |
| 167 | + key=lambda x: self.device_graph.degree(x), |
| 168 | + reverse=True, |
| 169 | + ) |
| 170 | + for neighbor in sorted_neighbors: |
| 171 | + if neighbor not in mapped_physicals: |
| 172 | + return neighbor |
| 173 | + # If cannot map onto one long line of physical qubits, then break down into multiple |
| 174 | + # small lines by finding nearest available qubit to the physical center |
| 175 | + return self._closest_unmapped_qubit(partner, mapped_physicals) |
| 176 | + |
| 177 | + pq = self.center |
| 178 | + for logical_line in circuit_graph: |
| 179 | + for lq in logical_line: |
| 180 | + is_isolated = len(logical_line) == 1 |
| 181 | + partner = ( |
| 182 | + qubit_map[partners[lq]] if (lq in partners and is_isolated) else self.center |
| 183 | + ) |
| 184 | + pq = next_physical(pq, partner, isolated=is_isolated) |
| 185 | + mapped_physicals.add(pq) |
| 186 | + qubit_map[lq] = pq |
| 187 | + |
| 188 | + return qubit_map |
| 189 | + |
| 190 | + def _closest_unmapped_qubit( |
| 191 | + self, source: 'cirq.Qid', mapped_physicals: Set['cirq.Qid'] |
| 192 | + ) -> 'cirq.Qid': |
| 193 | + """Finds the closest available neighbor to a physical qubit 'source' on the device. |
| 194 | +
|
| 195 | + Args: |
| 196 | + source: a physical qubit on the device. |
| 197 | +
|
| 198 | + Returns: |
| 199 | + the closest available physical qubit to 'source'. |
| 200 | +
|
| 201 | + Raises: |
| 202 | + ValueError: if there are no available qubits left on the device. |
| 203 | + """ |
| 204 | + for _, successors in nx.bfs_successors(self.device_graph, source): |
| 205 | + for successor in successors: |
| 206 | + if successor not in mapped_physicals: |
| 207 | + return successor |
| 208 | + raise ValueError("No available physical qubits left on the device.") |
| 209 | + |
| 210 | + def _value_equality_values_(self): |
| 211 | + return ( |
| 212 | + tuple(self.device_graph.nodes), |
| 213 | + tuple(self.device_graph.edges), |
| 214 | + nx.is_directed(self.device_graph), |
| 215 | + ) |
| 216 | + |
| 217 | + def __repr__(self): |
| 218 | + graph_type = type(self.device_graph).__name__ |
| 219 | + return f'cirq.LineInitialMapper(nx.{graph_type}({dict(self.device_graph.adjacency())}))' |
0 commit comments