Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[WIP] Added InitialMapper base class and LineInitialMapper implementation #5826

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5044216
created routing utilities in cirq-core/transformers and added Mapping…
ammareltigani Aug 10, 2022
618fb3e
ran continuous integration checks
ammareltigani Aug 10, 2022
cce41a0
addressed first round of comments
ammareltigani Aug 11, 2022
1eef47c
typo
ammareltigani Aug 11, 2022
c850d79
remove unused distance matrix
ammareltigani Aug 11, 2022
d945995
updated shortest_path method
ammareltigani Aug 11, 2022
000c9b8
Merge branch 'master' into routing-mapping_manager
ammareltigani Aug 11, 2022
0c1b347
Merge remote-tracking branch 'upstream/master' into routing-mapping_m…
ammareltigani Aug 11, 2022
53e49db
Merge branch 'routing-mapping_manager' of https://github.com/ammarelt…
ammareltigani Aug 11, 2022
d86dee3
formatting
ammareltigani Aug 11, 2022
84e700f
formatting
ammareltigani Aug 11, 2022
85a2a6b
Merge branch 'routing-mapping_manager' into routing-initial_mapping
ammareltigani Aug 11, 2022
0514707
strated line initial mapper
ammareltigani Aug 11, 2022
3df6655
minor bug fix
ammareltigani Aug 11, 2022
5935c91
resolving merge conflicts
ammareltigani Aug 11, 2022
0de58d8
cleanup up
ammareltigani Aug 12, 2022
c731a2e
made changes to docstring
ammareltigani Aug 12, 2022
529d1a6
completed line_initial_mapper; needs tests
ammareltigani Aug 12, 2022
5ef5315
minor docstring fixes; shortest_path() now returns logical qubits ins…
ammareltigani Aug 12, 2022
5763c5b
Merge branch 'routing-mapping_manager' into routing-initial_mapping
ammareltigani Aug 12, 2022
6de28fc
nit
ammareltigani Aug 12, 2022
af3dead
removed some uneccessary comments
ammareltigani Aug 12, 2022
3926336
Merge branch 'routing-mapping_manager' into routing-initial_mapping
ammareltigani Aug 12, 2022
3b23a96
bug fix: maximize neighbor degree instead of minimize; other minor ch…
ammareltigani Aug 12, 2022
d6fb1ee
added grid-like device for testing; needs bug fix in validate_operati…
ammareltigani Aug 13, 2022
8c9f06e
added ring device and implemented operation validation on RoutingTest…
ammareltigani Aug 13, 2022
3eb5761
added simple test for random circuits
ammareltigani Aug 15, 2022
f10e2a2
merging with master
ammareltigani Aug 15, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions cirq-core/cirq/transformers/routing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,11 @@
"""Routing utilities in Cirq."""

from cirq.transformers.routing.mapping_manager import MappingManager
<<<<<<< HEAD
from cirq.transformers.routing.initial_mapper import AbstractInitialMapper
from cirq.transformers.routing.line_initial_mapper import LineInitialMapper
from cirq.transformers.routing.testing_devices import (
construct_grid_device,
)
=======
>>>>>>> master
31 changes: 31 additions & 0 deletions cirq-core/cirq/transformers/routing/initial_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
# Copyright 2022 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import TYPE_CHECKING, Dict
import abc

if TYPE_CHECKING:
import cirq


class AbstractInitialMapper(metaclass=abc.ABCMeta):
"""Base class for creating custom initial mapping strategies."""

@abc.abstractmethod
def initial_mapping(self) -> Dict['cirq.Qid', 'cirq.Qid']:
"""Maps the logical qubits of a circuit onto physical qubits on a device.

Returns:
qubit_map: the initial mapping of logical qubits to physical qubits.
"""
183 changes: 183 additions & 0 deletions cirq-core/cirq/transformers/routing/line_initial_mapper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,183 @@
# 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 typing import Dict, Optional, TYPE_CHECKING
import networkx as nx

from cirq import circuits
from cirq.transformers import routing

if TYPE_CHECKING:
import cirq


class LineInitialMapper(routing.AbstractInitialMapper):
"""Places logical qubits in the circuit onto physical qubits on the device."""

def __init__(self, device_graph: nx.Graph, circuit: circuits.AbstractCircuit):
"""Initializes a LineInitialMapper.

Args:
device_graph: device graph
circuit_graph: circuit graph
"""
self.device_graph = device_graph
self.circuit = circuit
self.circuit_graph = self._make_circuit_graph()
self._map: Dict['cirq.Qid', 'cirq.Qid'] = {}

def _make_circuit_graph(self) -> nx.Graph:
"""Creates a (potentially incomplete) qubit connectivity graph of the circuit.

Iterates over the moments circuit from left to right drawing edges between logical qubits
that:
(1) have degree < 2, and
(2) that are involved in a 2-qubit operation in the current moment.
At this point the graph is forest of paths and/or simple cycles. For each simple cycle, make
it a path by removing the last edge that was added to it.

Returns:
The qubit connectivity graph of the circuit.
"""
circuit_graph = nx.Graph()
edge_order = 0
for op in self.circuit.all_operations():
circuit_graph.add_nodes_from(op.qubits)
if len(op.qubits) == 2 and all(
circuit_graph.degree[op.qubits[i]] < 2 for i in range(2)
):
circuit_graph.add_edge(*op.qubits, edge_order=edge_order)
edge_order += 1
found = True
while found:
try:
cycle = nx.find_cycle(circuit_graph)
edge_to_remove = max(
cycle, key=lambda x: circuit_graph.edges[x[0], x[1]]['edge_order']
)
circuit_graph.remove_edge(*edge_to_remove)
except nx.exception.NetworkXNoCycle:
found = False
return circuit_graph

def initial_mapping(self) -> 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.

Returns:
a dictionary that maps logical qubits in the circuit (keys) to physical qubits on the
device (values).
"""
if len(self._map) is not 0:
return self._map

physical_center = nx.center(self.device_graph)[0]

def next_physical(current_physical: 'cirq.Qid') -> 'cirq.Qid':
# use current physical if last logical line ended before mapping to it.
if self.device_graph.nodes[current_physical]["mapped"] is False:
return current_physical
# else 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),
)
for neighbor in reversed(sorted_neighbors):
if self.device_graph.nodes[neighbor]["mapped"] is False:
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)

def next_logical(current_logical: 'cirq.Qid') -> Optional['cirq.Qid']:
for neighbor in self.circuit_graph.neighbors(current_logical):
if self.circuit_graph.nodes[neighbor]["mapped"] is False:
return neighbor
return None

for pq in self.device_graph.nodes:
self.device_graph.nodes[pq]["mapped"] = False
for lq in self.circuit_graph.nodes:
self.circuit_graph.nodes[lq]["mapped"] = False

current_physical = physical_center
for logical_cc in nx.connected_components(self.circuit_graph):
if len(logical_cc) == 1:
continue

current_physical = next_physical(current_physical)
# start by mapping a logical line from one of its endpoints.
current_logical = next(q for q in logical_cc if self.circuit_graph.degree(q) == 1)

while current_logical is not None:
self.device_graph.nodes[current_physical]["mapped"] = True
self.circuit_graph.nodes[current_logical]["mapped"] = True
self._map[current_logical] = current_physical
current_physical = next_physical(current_physical)
current_logical = next_logical(current_logical)

self._map_remaining_qubits()
return self._map

def _map_remaining_qubits(self) -> None:
# map logical qubits that interact in 'self.circuit' but have missing edges in the circuit
# graph
for op in self.circuit.all_operations():
if len(op.qubits) == 2:
q1, q2 = op.qubits
if q1 not in self._map.keys():
physical = self._closest_unmapped_qubit(self._map[q2])
self._map[q1] = physical
self.device_graph.nodes[physical]["mapped"] = True
# 'elif' because at least one must be mapped already
elif q2 not in self._map.keys():
physical = self._closest_unmapped_qubit(self._map[q1])
self._map[q2] = physical
self.device_graph.nodes[physical]["mapped"] = True

# map logical qubits that don't interact with any other logical qubits in the circuit
for isolated_qubit in (q for q in self.circuit_graph.nodes if q not in self._map):
physical = self._closest_unmapped_qubit(self._map[next(iter(self._map))])
self._map[isolated_qubit] = physical
self.device_graph.nodes[physical]["mapped"] = True

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 self.device_graph.nodes[successor]["mapped"] is False:
return successor
raise ValueError("No available physical qubits left on the device.")
48 changes: 48 additions & 0 deletions cirq-core/cirq/transformers/routing/line_initial_mapper_test.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
# 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
from cirq.transformers.routing import construct_grid_device
from cirq.transformers.routing import LineInitialMapper


@pytest.mark.parametrize(
"qubits, n_moments, op_density, random_state",
[(5*size, 10*size, 0.4, seed) for size in range(1,3) for seed in range(20) 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 = construct_grid_device(7)
device_graph = device.metadata.nx_graph
mapper = LineInitialMapper(circuit=c_orig, device_graph=device_graph)
mapping = mapper.initial_mapping()
c_mapped = c_orig.transform_qubits(mapping)

# all qubits in the input circuit are placed on the device
assert set(mapping.keys()) == set(c_orig.all_qubits())

# the first two moments are executable
device.validate_circuit(c_mapped[: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()))

55 changes: 55 additions & 0 deletions cirq-core/cirq/transformers/routing/testing_devices.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Copyright 2022 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from typing import Optional
import networkx as nx

import cirq


class RoutingTestingDevice(cirq.Device):
def __init__(self, metadata: cirq.DeviceMetadata) -> None:
self._metadata = metadata

@property
def metadata(self) -> Optional[cirq.DeviceMetadata]:
return self._metadata

def validate_operation(self, operation: 'cirq.Operation') -> None:
for q in operation.qubits:
if q not in self._metadata.qubit_set:
raise ValueError(f'Qubit not on device: {q!r}.')

if (
len(operation.qubits) == 2
and operation.qubits[0] not in self._metadata.nx_graph[operation.qubits[1]]
):
raise ValueError(f'Qubit pair is not valid on device: {operation.qubits!r}.')


def construct_grid_device(d: int) -> RoutingTestingDevice:
qubits = (cirq.GridQubit(i, j) for i in range(d) for j in range(d))

nx_graph = nx.Graph()
row_edges = [
(cirq.GridQubit(i, j), cirq.GridQubit(i, j + 1)) for i in range(d) for j in range(d - 1)
]
col_edges = [
(cirq.GridQubit(i, j), cirq.GridQubit(i + 1, j)) for j in range(d) for i in range(d - 1)
]
nx_graph.add_edges_from(row_edges)
nx_graph.add_edges_from(col_edges)

metadata = cirq.DeviceMetadata(qubits, nx_graph)
return RoutingTestingDevice(metadata)