-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Named Topologies #4370
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
Merged
Merged
Named Topologies #4370
Changes from all commits
Commits
Show all changes
32 commits
Select commit
Hold shift + click to select a range
b7b6fd4
Named topologies
mpharrigan 538c823
nicer subselection of placements
mpharrigan 50a9725
Tests and frontmatter
mpharrigan 6ca0c0a
JSON, coverage, lint
mpharrigan a2e5566
nbformat
mpharrigan 3e32e11
Annotations
mpharrigan e1bd4d5
dstrain docs and annotations
mpharrigan 0f00f85
line topology n_qubits 1 disallow
mpharrigan a17ce60
Names
mpharrigan d8f12f4
Document DiagonalRectangleTopology
mpharrigan b77f7ea
Placements
mpharrigan 1ae35ba
Merge remote-tracking branch 'origin/master' into 2021-08-named-topos
mpharrigan 866720d
More dataclass shenannigans
mpharrigan e78d37c
Merge remote-tracking branch 'origin/master' into 2021-08-named-topos
mpharrigan 9618d1f
Fix merge
mpharrigan 9abaa4f
Half units
mpharrigan 001062a
Move file
mpharrigan 16016df
Rename TiltedSquareLattice
mpharrigan fd275c3
Docs and fixups
mpharrigan 4bc9804
Merge remote-tracking branch 'origin/master' into 2021-08-named-topos
mpharrigan fff203c
cartesian draw parameter -> tilted
mpharrigan bc428dd
Parameterized test
mpharrigan 88332e4
Merge remote-tracking branch 'origin/master' into 2021-08-named-topos
mpharrigan 502b0aa
Simpler and descriptive formula for nodes, thanks Adam
mpharrigan d5360f9
Move to cirq-core
mpharrigan 2f7ef60
Use michael b's graph generator
mpharrigan 43bf048
get_placements document
mpharrigan c89ef0e
Draw .... with matploooooooooooootlib
mpharrigan ba2c6a5
Merge remote-tracking branch 'origin/master' into 2021-08-named-topos
mpharrigan af447a5
Notebooks test for unreleased feature
mpharrigan 1cdb121
Merge remote-tracking branch 'origin/master' into 2021-08-named-topos
mpharrigan 144e321
ci
mpharrigan File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,316 @@ | ||
# 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 abc | ||
import dataclasses | ||
import warnings | ||
from dataclasses import dataclass | ||
from typing import Dict, List, Tuple, Any, Sequence, Union, Iterable, TYPE_CHECKING | ||
|
||
import networkx as nx | ||
from cirq.devices import GridQubit | ||
from cirq.protocols.json_serialization import obj_to_dict_helper | ||
from matplotlib import pyplot as plt | ||
|
||
if TYPE_CHECKING: | ||
import cirq | ||
|
||
|
||
def dataclass_json_dict(obj: Any, namespace: str = None) -> Dict[str, Any]: | ||
return obj_to_dict_helper(obj, [f.name for f in dataclasses.fields(obj)], namespace=namespace) | ||
|
||
|
||
class NamedTopology(metaclass=abc.ABCMeta): | ||
"""A topology (graph) with a name. | ||
|
||
"Named topologies" provide a mapping from a simple dataclass to a unique graph for categories | ||
of relevant topologies. Relevant topologies may be hardware dependant, but common topologies | ||
are linear (1D) and rectangular grid topologies. | ||
""" | ||
|
||
name: str = NotImplemented | ||
"""A name that uniquely identifies this topology.""" | ||
|
||
n_nodes: int = NotImplemented | ||
"""The number of nodes in the topology.""" | ||
|
||
graph: nx.Graph = NotImplemented | ||
"""A networkx graph representation of the topology.""" | ||
|
||
|
||
_GRIDLIKE_NODE = Union['cirq.GridQubit', Tuple[int, int]] | ||
|
||
|
||
def _node_and_coordinates( | ||
nodes: Iterable[_GRIDLIKE_NODE], | ||
) -> Iterable[Tuple[_GRIDLIKE_NODE, Tuple[int, int]]]: | ||
"""Yield tuples whose first element is the input node and the second is guaranteed to be a tuple | ||
of two integers. The input node can be a tuple of ints or a GridQubit.""" | ||
for node in nodes: | ||
if isinstance(node, GridQubit): | ||
yield node, (node.row, node.col) | ||
else: | ||
x, y = node | ||
yield node, (x, y) | ||
|
||
|
||
def draw_gridlike( | ||
graph: nx.Graph, ax: plt.Axes = None, tilted: bool = True, **kwargs | ||
) -> Dict[Any, Tuple[int, int]]: | ||
"""Draw a grid-like graph using Matplotlib. | ||
|
||
This wraps nx.draw_networkx to produce a matplotlib drawing of the graph. Nodes | ||
should be two-dimensional gridlike objects. | ||
|
||
Args: | ||
graph: A NetworkX graph whose nodes are (row, column) coordinates or cirq.GridQubits. | ||
ax: Optional matplotlib axis to use for drawing. | ||
tilted: If True, directly position as (row, column); otherwise, | ||
rotate 45 degrees to accommodate google-style diagonal grids. | ||
kwargs: Additional arguments to pass to `nx.draw_networkx`. | ||
|
||
Returns: | ||
A positions dictionary mapping nodes to (x, y) coordinates suitable for future calls | ||
to NetworkX plotting functionality. | ||
""" | ||
if ax is None: | ||
ax = plt.gca() # coverage: ignore | ||
|
||
if tilted: | ||
pos = {node: (y, -x) for node, (x, y) in _node_and_coordinates(graph.nodes)} | ||
else: | ||
pos = {node: (x + y, y - x) for node, (x, y) in _node_and_coordinates(graph.nodes)} | ||
|
||
nx.draw_networkx(graph, pos=pos, ax=ax, **kwargs) | ||
ax.axis('equal') | ||
return pos | ||
|
||
|
||
@dataclass(frozen=True) | ||
class LineTopology(NamedTopology): | ||
"""A 1D linear topology. | ||
|
||
Node indices are contiguous integers starting from 0 with edges between | ||
adjacent integers. | ||
|
||
Args: | ||
n_nodes: The number of nodes in a line. | ||
""" | ||
|
||
n_nodes: int | ||
|
||
def __post_init__(self): | ||
if self.n_nodes <= 1: | ||
raise ValueError("`n_nodes` must be greater than 1.") | ||
object.__setattr__(self, 'name', f'line-{self.n_nodes}') | ||
graph = nx.from_edgelist( | ||
[(i1, i2) for i1, i2 in zip(range(self.n_nodes), range(1, self.n_nodes))] | ||
) | ||
object.__setattr__(self, 'graph', graph) | ||
|
||
def draw(self, ax=None, tilted: bool = True, **kwargs) -> Dict[Any, Tuple[int, int]]: | ||
"""Draw this graph using Matplotlib. | ||
|
||
Args: | ||
ax: Optional matplotlib axis to use for drawing. | ||
tilted: If True, draw as a horizontal line. Otherwise, draw on a diagonal. | ||
kwargs: Additional arguments to pass to `nx.draw_networkx`. | ||
""" | ||
g2 = nx.relabel_nodes(self.graph, {n: (n, 1) for n in self.graph.nodes}) | ||
return draw_gridlike(g2, ax=ax, tilted=tilted, **kwargs) | ||
|
||
def _json_dict_(self) -> Dict[str, Any]: | ||
return dataclass_json_dict(self) | ||
|
||
|
||
@dataclass(frozen=True) | ||
class TiltedSquareLattice(NamedTopology): | ||
"""A grid lattice rotated 45-degrees. | ||
|
||
This topology is based on Google devices where plaquettes consist of four qubits in a square | ||
connected to a central qubit: | ||
|
||
x x | ||
x | ||
x x | ||
|
||
The corner nodes are not connected to each other. `width` and `height` refer to the rectangle | ||
formed by rotating the lattice 45 degrees. `width` and `height` are measured in half-unit | ||
cells, or equivalently half the number of central nodes. | ||
An example diagram of this topology is shown below. It is a | ||
"tilted-square-lattice-6-4" with width 6 and height 4. | ||
|
||
x | ||
│ | ||
x────X────x | ||
│ │ │ | ||
x────X────x────X────x | ||
│ │ │ │ | ||
x────X────x────X───x | ||
│ │ │ | ||
x────X────x | ||
│ | ||
x | ||
|
||
Nodes are 2-tuples of integers which may be negative. Please see `get_placements` for | ||
mapping this topology to a GridQubit Device. | ||
""" | ||
|
||
width: int | ||
height: int | ||
|
||
def __post_init__(self): | ||
if self.width <= 0: | ||
raise ValueError("Width must be a positive integer") | ||
if self.height <= 0: | ||
raise ValueError("Height must be a positive integer") | ||
|
||
object.__setattr__(self, 'name', f'tilted-square-lattice-{self.width}-{self.height}') | ||
|
||
rect1 = set( | ||
(i + j, i - j) for i in range(self.width // 2 + 1) for j in range(self.height // 2 + 1) | ||
) | ||
rect2 = set( | ||
((i + j) // 2, (i - j) // 2) | ||
for i in range(1, self.width + 1, 2) | ||
for j in range(1, self.height + 1, 2) | ||
) | ||
nodes = rect1 | rect2 | ||
g = nx.Graph() | ||
for node in nodes: | ||
for dx, dy in [(1, 0), (-1, 0), (0, 1), (0, -1)]: | ||
neighbor = (node[0] + dx, node[1] + dy) | ||
if neighbor in nodes: | ||
g.add_edge(node, neighbor) | ||
|
||
object.__setattr__(self, 'graph', g) | ||
|
||
# The number of edges = width * height (see unit tests). This can be seen if you remove | ||
# all vertices and replace edges with dots. | ||
# The formula for the number of vertices is not that nice, but you can derive it by | ||
# summing big and small Xes in the asciiart in the docstring. | ||
# There are (width//2 + 1) * (height//2 + 1) small xes and | ||
# ((width + 1)//2) * ((height + 1)//2) big ones. | ||
n_nodes = (self.width // 2 + 1) * (self.height // 2 + 1) | ||
n_nodes += ((self.width + 1) // 2) * ((self.height + 1) // 2) | ||
object.__setattr__(self, 'n_nodes', n_nodes) | ||
|
||
def draw(self, ax=None, tilted=True, **kwargs): | ||
"""Draw this graph using Matplotlib. | ||
|
||
Args: | ||
ax: Optional matplotlib axis to use for drawing. | ||
tilted: If True, directly position as (row, column); otherwise, | ||
rotate 45 degrees to accommodate the diagonal nature of this topology. | ||
kwargs: Additional arguments to pass to `nx.draw_networkx`. | ||
""" | ||
return draw_gridlike(self.graph, ax=ax, tilted=tilted, **kwargs) | ||
|
||
def nodes_as_gridqubits(self) -> List['cirq.GridQubit']: | ||
"""Get the graph nodes as cirq.GridQubit""" | ||
return [GridQubit(r, c) for r, c in sorted(self.graph.nodes)] | ||
|
||
def _json_dict_(self) -> Dict[str, Any]: | ||
return dataclass_json_dict(self) | ||
|
||
|
||
def get_placements( | ||
big_graph: nx.Graph, small_graph: nx.Graph, max_placements=100_000 | ||
) -> List[Dict]: | ||
"""Get 'placements' mapping small_graph nodes onto those of `big_graph`. | ||
|
||
This function considers monomorphisms with a restriction: we restrict only to unique set | ||
of `big_graph` qubits. Some monomorphisms may be basically | ||
the same mapping just rotated/flipped which we purposefully exclude. This could | ||
exclude meaningful differences like using the same qubits but having the edges assigned | ||
differently, but it prevents the number of placements from blowing up. | ||
|
||
Args: | ||
big_graph: The parent, super-graph. We often consider the case where this is a | ||
nx.Graph representation of a Device whose nodes are `cirq.Qid`s like `GridQubit`s. | ||
small_graph: The subgraph. We often consider the case where this is a NamedTopology | ||
graph. | ||
max_placements: Raise a value error if there are more than this many placement | ||
possibilities. It is possible to use `big_graph`, `small_graph` combinations | ||
that result in an intractable number of placements. | ||
|
||
Raises: | ||
ValueError: if the number of placements exceeds `max_placements`. | ||
|
||
Returns: | ||
A list of placement dictionaries. Each dictionary maps the nodes in `small_graph` to | ||
nodes in `big_graph` with a monomorphic relationship. That's to say: if an edge exists | ||
in `small_graph` between two nodes, it will exist in `big_graph` between the mapped nodes. | ||
""" | ||
matcher = nx.algorithms.isomorphism.GraphMatcher(big_graph, small_graph) | ||
|
||
# de-duplicate rotations, see docstring. | ||
dedupe = {} | ||
for big_to_small_map in matcher.subgraph_monomorphisms_iter(): | ||
dedupe[frozenset(big_to_small_map.keys())] = big_to_small_map | ||
if len(dedupe) > max_placements: | ||
# coverage: ignore | ||
raise ValueError( | ||
f"We found more than {max_placements} placements. Please use a " | ||
f"more constraining `big_graph` or a more constrained `small_graph`." | ||
) | ||
|
||
small_to_bigs = [] | ||
for big in sorted(dedupe.keys()): | ||
big_to_small_map = dedupe[big] | ||
small_to_big_map = {v: k for k, v in big_to_small_map.items()} | ||
small_to_bigs.append(small_to_big_map) | ||
return small_to_bigs | ||
|
||
|
||
def draw_placements( | ||
big_graph: nx.Graph, | ||
small_graph: nx.Graph, | ||
small_to_big_mappings, | ||
max_plots=20, | ||
axes: Sequence[plt.Axes] = None, | ||
): | ||
"""Draw a visualization of placements from small_graph onto big_graph using Matplotlib. | ||
|
||
The entire `big_graph` will be drawn with default blue colored nodes. `small_graph` nodes | ||
and edges will be highlighted with a red color. | ||
""" | ||
if len(small_to_big_mappings) > max_plots: | ||
# coverage: ignore | ||
warnings.warn(f"You've provided a lot of mappings. Only plotting the first {max_plots}") | ||
small_to_big_mappings = small_to_big_mappings[:max_plots] | ||
|
||
call_show = False | ||
if axes is None: | ||
# coverage: ignore | ||
call_show = True | ||
|
||
for i, small_to_big_map in enumerate(small_to_big_mappings): | ||
if axes is not None: | ||
ax = axes[i] | ||
else: | ||
# coverage: ignore | ||
ax = plt.gca() | ||
|
||
small_mapped = nx.relabel_nodes(small_graph, small_to_big_map) | ||
draw_gridlike(big_graph, ax=ax) | ||
draw_gridlike( | ||
small_mapped, node_color='red', edge_color='red', width=2, with_labels=False, ax=ax | ||
) | ||
ax.axis('equal') | ||
if call_show: | ||
# coverage: ignore | ||
# poor man's multi-axis figure: call plt.show() after each plot | ||
# and jupyter will put the plots one after another. | ||
plt.show() |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can we just use
len(g.nodes)
org.number_of_nodes
here ?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We could. I wanted a formula for the number of nodes in terms of width and height; the unit test verifies that the two methods agree. The potential benefit of a contributor being able to read off a formula by looking at the code offsets the downside of not using
g.number_of_nodes()