diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index 0114e508fd0..0fb1b4b4828 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -348,6 +348,7 @@ expand_composite, HardCodedInitialMapper, is_negligible_turn, + LineInitialMapper, MappingManager, map_moments, map_operations, diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index 95f5a46e042..589d03e57bd 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -85,6 +85,7 @@ 'TransformerContext', # Routing utilities 'HardCodedInitialMapper', + 'LineInitialMapper', 'MappingManager', 'RouteCQC', # global objects diff --git a/cirq-core/cirq/testing/__init__.py b/cirq-core/cirq/testing/__init__.py index 81a38d020ca..96af44ba7a2 100644 --- a/cirq-core/cirq/testing/__init__.py +++ b/cirq-core/cirq/testing/__init__.py @@ -98,4 +98,10 @@ FakePrinter, ) +from cirq.testing.routing_devices import ( + construct_grid_device, + construct_ring_device, + RoutingTestingDevice, +) + from cirq.testing.sample_circuits import nonoptimal_toffoli_circuit diff --git a/cirq-core/cirq/testing/routing_devices.py b/cirq-core/cirq/testing/routing_devices.py new file mode 100644 index 00000000000..392b678a89e --- /dev/null +++ b/cirq-core/cirq/testing/routing_devices.py @@ -0,0 +1,67 @@ +# Copyright 2021 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +"""Provides test devices that can validate circuits during a routing procedure.""" + +from typing import TYPE_CHECKING + +import networkx as nx + +from cirq import devices, ops + +if TYPE_CHECKING: + import cirq + + +class RoutingTestingDevice(devices.Device): + """Testing device to be used for testing qubit connectivity in routing procedures.""" + + def __init__(self, nx_graph: nx.Graph) -> None: + relabeling_map = { + old: ops.q(old) if isinstance(old, (int, str)) else ops.q(*old) for old in nx_graph + } + # Relabel nodes in-place. + nx.relabel_nodes(nx_graph, relabeling_map, copy=False) + + self._metadata = devices.DeviceMetadata(relabeling_map.values(), nx_graph) + + @property + def metadata(self) -> devices.DeviceMetadata: + return self._metadata + + def validate_operation(self, operation: 'cirq.Operation') -> None: + if not self._metadata.qubit_set.issuperset(operation.qubits): + raise ValueError(f'Qubits not on device: {operation.qubits!r}.') + + if len(operation.qubits) > 1: + if len(operation.qubits) == 2: + if operation.qubits not in self._metadata.nx_graph.edges: + raise ValueError( + f'Qubit pair is not a valid edge on device: {operation.qubits!r}.' + ) + return + + if not isinstance(operation.gate, ops.MeasurementGate): + raise ValueError( + f'Unsupported operation: {operation}. ' + f'Routing device only supports 1 / 2 qubit operations.' + ) + + +def construct_grid_device(m: int, n: int) -> RoutingTestingDevice: + return RoutingTestingDevice(nx.grid_2d_graph(m, n)) + + +def construct_ring_device(l: int, directed: bool = False) -> RoutingTestingDevice: + nx_graph = nx.cycle_graph(l, create_using=nx.DiGraph if directed else nx.Graph) + return RoutingTestingDevice(nx_graph) diff --git a/cirq-core/cirq/testing/routing_devices_test.py b/cirq-core/cirq/testing/routing_devices_test.py new file mode 100644 index 00000000000..5369b0fc20c --- /dev/null +++ b/cirq-core/cirq/testing/routing_devices_test.py @@ -0,0 +1,116 @@ +# Copyright 2021 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import pytest +import networkx as nx +import cirq + + +def test_grid_device(): + rect_device = cirq.testing.construct_grid_device(5, 7) + rect_device_graph = rect_device.metadata.nx_graph + isomorphism_class = nx.Graph() + row_edges = [ + (cirq.GridQubit(i, j), cirq.GridQubit(i, j + 1)) for i in range(5) for j in range(6) + ] + col_edges = [ + (cirq.GridQubit(i, j), cirq.GridQubit(i + 1, j)) for j in range(7) for i in range(4) + ] + isomorphism_class.add_edges_from(row_edges) + isomorphism_class.add_edges_from(col_edges) + assert all(q in rect_device_graph.nodes for q in cirq.GridQubit.rect(5, 7)) + assert nx.is_isomorphic(isomorphism_class, rect_device_graph) + + +def test_grid_op_validation(): + device = cirq.testing.construct_grid_device(5, 7) + + with pytest.raises(ValueError, match="Qubits not on device"): + device.validate_operation(cirq.X(cirq.NamedQubit("a"))) + with pytest.raises(ValueError, match="Qubits not on device"): + device.validate_operation(cirq.CNOT(cirq.NamedQubit("a"), cirq.GridQubit(0, 0))) + with pytest.raises(ValueError, match="Qubits not on device"): + device.validate_operation(cirq.CNOT(cirq.GridQubit(5, 4), cirq.GridQubit(4, 4))) + with pytest.raises(ValueError, match="Qubits not on device"): + device.validate_operation(cirq.CNOT(cirq.GridQubit(4, 7), cirq.GridQubit(4, 6))) + + with pytest.raises(ValueError, match="Qubit pair is not a valid edge on device"): + device.validate_operation(cirq.CNOT(cirq.GridQubit(0, 0), cirq.GridQubit(0, 2))) + with pytest.raises(ValueError, match="Qubit pair is not a valid edge on device"): + device.validate_operation(cirq.CNOT(cirq.GridQubit(2, 0), cirq.GridQubit(0, 0))) + + device.validate_operation(cirq.CNOT(cirq.GridQubit(0, 0), cirq.GridQubit(0, 1))) + device.validate_operation(cirq.CNOT(cirq.GridQubit(0, 1), cirq.GridQubit(0, 0))) + device.validate_operation(cirq.CNOT(cirq.GridQubit(1, 0), cirq.GridQubit(0, 0))) + device.validate_operation(cirq.CNOT(cirq.GridQubit(0, 0), cirq.GridQubit(1, 0))) + + +def test_ring_device(): + undirected_device = cirq.testing.construct_ring_device(5) + undirected_device_graph = undirected_device.metadata.nx_graph + assert all(q in undirected_device_graph.nodes for q in cirq.LineQubit.range(5)) + isomorphism_class = nx.Graph() + edges = [(cirq.LineQubit(i % 5), cirq.LineQubit((i + 1) % 5)) for i in range(5)] + isomorphism_class.add_edges_from(edges) + assert nx.is_isomorphic(isomorphism_class, undirected_device_graph) + + directed_device = cirq.testing.construct_ring_device(5, directed=True) + directed_device_graph = directed_device.metadata.nx_graph + assert all(q in directed_device_graph.nodes for q in cirq.LineQubit.range(5)) + isomorphism_class = nx.DiGraph() + edges = [(cirq.LineQubit(i % 5), cirq.LineQubit((i + 1) % 5)) for i in range(5)] + isomorphism_class.add_edges_from(edges) + assert nx.is_isomorphic(isomorphism_class, directed_device_graph) + + +def test_ring_op_validation(): + directed_device = cirq.testing.construct_ring_device(5, directed=True) + undirected_device = cirq.testing.construct_ring_device(5, directed=False) + + with pytest.raises(ValueError, match="Qubits not on device"): + directed_device.validate_operation(cirq.X(cirq.LineQubit(5))) + with pytest.raises(ValueError, match="Qubits not on device"): + undirected_device.validate_operation(cirq.X(cirq.LineQubit(5))) + + with pytest.raises(ValueError, match="Qubit pair is not a valid edge on device"): + undirected_device.validate_operation(cirq.CNOT(cirq.LineQubit(0), cirq.LineQubit(2))) + with pytest.raises(ValueError, match="Qubit pair is not a valid edge on device"): + directed_device.validate_operation(cirq.CNOT(cirq.LineQubit(1), cirq.LineQubit(0))) + + undirected_device.validate_operation(cirq.CNOT(cirq.LineQubit(0), cirq.LineQubit(1))) + undirected_device.validate_operation(cirq.CNOT(cirq.LineQubit(1), cirq.LineQubit(0))) + directed_device.validate_operation(cirq.CNOT(cirq.LineQubit(0), cirq.LineQubit(1))) + + +def test_allowed_multi_qubit_gates(): + device = cirq.testing.construct_ring_device(5) + + device.validate_operation(cirq.MeasurementGate(1).on(cirq.LineQubit(0))) + device.validate_operation(cirq.MeasurementGate(2).on(*cirq.LineQubit.range(2))) + device.validate_operation(cirq.MeasurementGate(3).on(*cirq.LineQubit.range(3))) + + with pytest.raises(ValueError, match=f"Unsupported operation"): + device.validate_operation(cirq.CCNOT(*cirq.LineQubit.range(3))) + + device.validate_operation(cirq.CNOT(*cirq.LineQubit.range(2))) + + +def test_namedqubit_device(): + # 4-star graph + nx_graph = nx.Graph([("a", "b"), ("a", "c"), ("a", "d")]) + + device = cirq.testing.RoutingTestingDevice(nx_graph) + relabeled_graph = device.metadata.nx_graph + qubit_set = {cirq.NamedQubit(n) for n in "abcd"} + assert set(relabeled_graph.nodes) == qubit_set + assert nx.is_isomorphic(nx_graph, relabeled_graph) diff --git a/cirq-core/cirq/transformers/__init__.py b/cirq-core/cirq/transformers/__init__.py index 7136319ef88..20f202945ed 100644 --- a/cirq-core/cirq/transformers/__init__.py +++ b/cirq-core/cirq/transformers/__init__.py @@ -47,6 +47,7 @@ from cirq.transformers.routing import ( AbstractInitialMapper, HardCodedInitialMapper, + LineInitialMapper, MappingManager, RouteCQC, ) diff --git a/cirq-core/cirq/transformers/routing/__init__.py b/cirq-core/cirq/transformers/routing/__init__.py index 4814e7e9bd8..2ec3e856d3e 100644 --- a/cirq-core/cirq/transformers/routing/__init__.py +++ b/cirq-core/cirq/transformers/routing/__init__.py @@ -16,4 +16,5 @@ from cirq.transformers.routing.initial_mapper import AbstractInitialMapper, HardCodedInitialMapper from cirq.transformers.routing.mapping_manager import MappingManager +from cirq.transformers.routing.line_initial_mapper import LineInitialMapper from cirq.transformers.routing.route_circuit_cqc import RouteCQC diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper.py b/cirq-core/cirq/transformers/routing/line_initial_mapper.py new file mode 100644 index 00000000000..d36675b8213 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper.py @@ -0,0 +1,192 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Concrete implementation of AbstractInitialMapper that places lines of qubits onto the device.""" + +from audioop import reverse +from typing import Dict, List, Set, TYPE_CHECKING +import networkx as nx + +from cirq.transformers.routing import AbstractInitialMapper +from cirq import protocols + +if TYPE_CHECKING: + import cirq + + +class LineInitialMapper(AbstractInitialMapper): + """Places logical qubits in the circuit onto physical qubits on the device.""" + + def __init__(self, device_graph: nx.Graph) -> None: + """Initializes a LineInitialMapper. + + Args: + device_graph: device graph + """ + self.device_graph = device_graph + self.mapped_physicals: Set['cirq.Qid'] = set() + self.partners: Dict['cirq.Qid', 'cirq.Qid'] = {} + + def _make_circuit_graph(self, circuit: 'cirq.AbstractCircuit') -> List[List['cirq.Qid']]: + """Creates a (potentially incomplete) qubit connectivity graph of the circuit. + + Iterates over moments in the circuit from left to right and adds edges between logical + qubits if the logical qubit pair l1 and l2 + (1) have degree < 2, + (2) are involved in a 2-qubit operation in the current moment, and + (3) adding such an edge will not produce a cycle in the graph. + + Args: + circuit: the input circuit with logical qubits + + Returns: + The (potentially incomplete) qubit connectivity graph of the circuit, which is + guaranteed to be a forest of line graphs. + """ + circuit_graph: List[List['cirq.Qid']] = [[q] for q in sorted(circuit.all_qubits())] + component_id: Dict['cirq.Qid', int] = {q[0]: i for i, q in enumerate(circuit_graph)} + + def degree_lt_two(q: 'cirq.Qid'): + return any(circuit_graph[component_id[q]][i] == q for i in [-1, 0]) + + for op in circuit.all_operations(): + if protocols.num_qubits(op) != 2: + continue + + q0, q1 = op.qubits + c0, c1 = component_id[q0], component_id[q1] + + # Keep track of partners for mapping isolated qubits later. + if q0 not in self.partners: + self.partners[q0] = q1 + if q1 not in self.partners: + self.partners[q1] = q0 + + if not (degree_lt_two(q0) and degree_lt_two(q1) and c0 != c1): + continue + + # Make sure c0/q0 are for the largest component. + if len(circuit_graph[c0]) < len(circuit_graph[c1]): + c0, c1, q0, q1 = c1, c0, q1, q0 + + # copy smaller component into larger one. + if circuit_graph[c0][0] == q0: + if circuit_graph[c1][0] == q1: + for q in circuit_graph[c1]: + circuit_graph[c0].insert(0, q) + component_id[q] = c0 + else: + for q in reversed(circuit_graph[c1]): + circuit_graph[c0].insert(0, q) + component_id[q] = c0 + else: + if circuit_graph[c1][0] == q1: + for q in circuit_graph[c1]: + circuit_graph[c0].append(q) + component_id[q] = c0 + else: + for q in reversed(circuit_graph[c1]): + circuit_graph[c0].append(q) + component_id[q] = c0 + + return sorted([circuit_graph[c] for c in set(component_id.values())], key=len, reverse=True) + + def initial_mapping(self, circuit: 'cirq.AbstractCircuit') -> Dict['cirq.Qid', 'cirq.Qid']: + """Maps disjoint lines of logical qubits onto lines of physical qubits. + + Starting from the center physical qubit on the device, attempts to map disjoint lines of + logical qubits given by the circuit graph onto one long line of physical qubits on the + device, greedily maximizing each physical qubit's degree. + If this mapping cannot be completed as one long line of qubits in the circuit graph mapped + to qubits in the device graph, the line can be split as several line segments and then we: + (i) Map first line segment. + (ii) Find another high degree vertex in G near the center. + (iii) Map the second line segment + (iv) etc. + A line is split by mapping the next logical qubit to the nearest available physical qubit + to the center of the device graph. + + Args: + circuit: the input circuit with logical qubits + + Returns: + a dictionary that maps logical qubits in the circuit (keys) to physical qubits on the + device (values). + """ + qubit_map: Dict['cirq.Qid', 'cirq.Qid'] = {} + circuit_graph = self._make_circuit_graph(circuit) + physical_center = nx.center(self.device_graph)[0] + + def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid': + # Greedily map to highest degree neighbor that that is available + sorted_neighbors = sorted( + self.device_graph.neighbors(current_physical), + key=lambda x: self.device_graph.degree(x), + reverse=True, + ) + for neighbor in sorted_neighbors: + if neighbor not in self.mapped_physicals: + return neighbor + # If cannot map onto one long line of physical qubits, then break down into multiple + # small lines by finding nearest available qubit to the physical center + return self._closest_unmapped_qubit(physical_center) + + pq = physical_center + first_isolated_idx = len(circuit_graph) + for idx, logical_line in enumerate(circuit_graph): + if len(logical_line) == 1: + first_isolated_idx = idx + break + + for lq in logical_line: + self.mapped_physicals.add(pq) + qubit_map[lq] = pq + # Edge case: if mapping n qubits on an n-qubit device should not call next_physical + # when finished mapping the last logical qubit else will raise an error + if len(circuit.all_qubits()) != len(self.mapped_physicals): + pq = next_physical(pq) + + for i in range(first_isolated_idx, len(circuit_graph)): + lq = circuit_graph[i][0] + partner = qubit_map[self.partners[lq]] if lq in self.partners else physical_center + pq = self._closest_unmapped_qubit(partner) + self.mapped_physicals.add(pq) + qubit_map[lq] = pq + + return qubit_map + + def _closest_unmapped_qubit(self, source: 'cirq.Qid') -> 'cirq.Qid': + """Finds the closest available neighbor to a physical qubit 'source' on the device. + + Args: + source: a physical qubit on the device. + + Returns: + the closest available physical qubit to 'source'. + + Raises: + ValueError: if there are no available qubits left on the device. + """ + for _, successors in nx.bfs_successors(self.device_graph, source): + for successor in successors: + if successor not in self.mapped_physicals: + return successor + raise ValueError("No available physical qubits left on the device.") + + def __eq__(self, other) -> bool: + return nx.utils.graphs_equal(self.device_graph, other.device_graph) + + def __repr__(self): + graph_type = type(self.device_graph).__name__ + return f'cirq.LineInitialMapper(nx.{graph_type}({dict(self.device_graph.adjacency())}))' diff --git a/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py new file mode 100644 index 00000000000..8e659bfc2a5 --- /dev/null +++ b/cirq-core/cirq/transformers/routing/line_initial_mapper_test.py @@ -0,0 +1,116 @@ +# Copyright 2022 The Cirq Developers +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import networkx as nx +import pytest + +import cirq + + +def construct_small_circuit(): + return cirq.Circuit( + [ + cirq.Moment(cirq.CNOT(cirq.NamedQubit('1'), cirq.NamedQubit('3'))), + cirq.Moment(cirq.CNOT(cirq.NamedQubit('2'), cirq.NamedQubit('3'))), + cirq.Moment( + cirq.CNOT(cirq.NamedQubit('4'), cirq.NamedQubit('3')), cirq.X(cirq.NamedQubit('5')) + ), + ] + ) + + +def construct_step_circuit(k: int): + q = cirq.LineQubit.range(k) + return cirq.Circuit([cirq.CNOT(q[i], q[i + 1]) for i in range(k - 1)]) + + +def test_line_breaking_on_grid_device(): + # tests + # -if strategy is able to map into several small lines if fails to map onto one long line + # -if # of physical qubits <= # of logical qubits then strategy should succeed + + step_circuit = construct_step_circuit(49) + device = cirq.testing.construct_grid_device(7, 7) + device_graph = device.metadata.nx_graph + mapper = cirq.LineInitialMapper(device_graph) + mapping = mapper.initial_mapping(step_circuit) + mapped_circuit = step_circuit.transform_qubits(mapping) + + # all qubits in the input circuit are placed on the device + assert set(mapping.keys()) == set(step_circuit.all_qubits()) + + # the first two moments are executable + device.validate_circuit(mapped_circuit[:2]) + + # the induced graph of the device on the physical qubits in the map is connected + assert nx.is_connected(nx.induced_subgraph(device_graph, mapping.values())) + + step_circuit = construct_step_circuit(50) + with pytest.raises(ValueError, match="No available physical qubits left on the device"): + mapper.initial_mapping(step_circuit) + + +def test_small_circuit_on_grid_device(): + circuit = construct_small_circuit() + + device_graph = cirq.testing.construct_grid_device(7, 7).metadata.nx_graph + mapper = cirq.LineInitialMapper(device_graph) + mapping = mapper.initial_mapping(circuit) + + assert nx.center(device_graph)[0] == cirq.GridQubit(3, 3) + mapped_circuit = circuit.transform_qubits(mapping) + diagram = """(2, 2): ───@─────────── + │ +(2, 3): ───┼───────X─── + │ +(3, 2): ───X───X───X─── + │ │ +(3, 3): ───────@───┼─── + │ +(4, 2): ───────────@───""" + cirq.testing.assert_has_diagram(mapped_circuit, diagram) + + +@pytest.mark.parametrize( + "qubits, n_moments, op_density, random_state", + [ + (5 * size, 20 * size, density, seed) + for size in range(1, 3) + for seed in range(10) + for density in [0.4, 0.5, 0.6] + ], +) +def test_random_circuits_grid_device( + qubits: int, n_moments: int, op_density: float, random_state: int +): + c_orig = cirq.testing.random_circuit( + qubits=qubits, n_moments=n_moments, op_density=op_density, random_state=random_state + ) + device = cirq.testing.construct_grid_device(7, 7) + device_graph = device.metadata.nx_graph + mapper = cirq.LineInitialMapper(device_graph) + mapping = mapper.initial_mapping(c_orig) + c_mapped = c_orig.transform_qubits(mapping) + + assert set(mapping.keys()) == set(c_orig.all_qubits()) + + device.validate_circuit(c_mapped[:2]) + + assert nx.is_connected(nx.induced_subgraph(device_graph, mapping.values())) + + +def test_repr(): + device_graph = cirq.testing.construct_grid_device(7, 7).metadata.nx_graph + mapper = cirq.LineInitialMapper(device_graph) + cirq.testing.assert_equivalent_repr(mapper, setup_code='import cirq\nimport networkx as nx')