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

adding graph relabeling and sublattice mappings. #211

Merged
merged 3 commits into from
Dec 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
6 changes: 4 additions & 2 deletions dwave_networkx/drawing/pegasus_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,8 @@ def pegasus_layout(G, scale=1., center=None, dim=2, crosses=False):
if G.graph.get('labels') == 'nice':
m = 3*(G.graph['rows']-1)
c_coords = chimera_node_placer_2d(m, m, 4, scale=scale, center=center, dim=dim)
def xy_coords(t, y, x, u, k): return c_coords(3*y+2-t, 3*x+t, u, k)
def xy_coords(t, y, x, u, k):
return c_coords(3*y+2-t, 3*x+t, u, k)
pos = {v: xy_coords(*v) for v in G.nodes()}
else:
xy_coords = pegasus_node_placer_2d(G, scale, center, dim, crosses=crosses)
Expand Down Expand Up @@ -312,12 +313,13 @@ def draw_pegasus_yield(G, **kwargs):
m = G.graph['columns']
offset_lists = (G.graph['vertical_offsets'], G.graph['horizontal_offsets'])
coordinates = G.graph["labels"] == "coordinate"
nice = G.graph["labels"] == "nice"
# Can't interpret fabric_only from graph attributes
except:
raise ValueError("Target pegasus graph needs to have columns, rows, \
tile, and label attributes to be able to identify faulty qubits.")


perfect_graph = pegasus_graph(m, offset_lists=offset_lists, coordinates=coordinates)
perfect_graph = pegasus_graph(m, offset_lists=offset_lists, coordinates=coordinates, nice_coordinates=nice)

draw_yield(G, pegasus_layout(perfect_graph), perfect_graph, **kwargs)
1 change: 0 additions & 1 deletion dwave_networkx/drawing/zephyr_layout.py
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,6 @@ def draw_zephyr_yield(G, **kwargs):
m = G.graph['columns']
t = G.graph['tile']
coordinates = G.graph["labels"] == "coordinate"
# Can't interpret fabric_only from graph attributes
except:
raise ValueError("Target zephyr graph needs to have columns, rows, \
tile, and label attributes to be able to identify faulty qubits.")
Expand Down
182 changes: 178 additions & 4 deletions dwave_networkx/generators/chimera.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,11 +24,15 @@

from dwave_networkx.exceptions import DWaveNetworkXException

from itertools import product

__all__ = ['chimera_graph',
'chimera_coordinates',
'find_chimera_indices',
'chimera_to_linear',
'linear_to_chimera']
'linear_to_chimera',
'chimera_sublattice_mappings',
]


def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_list=None, data=True, coordinates=False):
Expand All @@ -48,7 +52,7 @@ def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_lis
node_list : iterable (optional, default None)
Iterable of nodes in the graph. If None, calculated
from (m, n, t). Note that this list is used to remove nodes,
so any nodes specified not in `range(m * n * 2 * t)` are not added.
so any nodes specified not in ``range(m * n * 2 * t)`` are not added.
edge_list : iterable (optional, default None)
Iterable of edges in the graph. If None, edges are
generated as described below. The nodes in each edge must be
Expand Down Expand Up @@ -449,6 +453,54 @@ def iter_linear_to_chimera_pairs(self, plist):
"""
return self._pair_repack(self.iter_linear_to_chimera, plist)

def graph_to_linear(self, g):
"""Return a copy of the graph g relabeled to have linear indices"""
labels = g.graph.get('labels')
if labels == 'int':
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have two alternative nomenclatures with linear and int coordinates?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I'm following the convention set in chimera_graph

Copy link
Contributor

@pau557 pau557 Nov 19, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, so int just gets used inside G.graph["labels"] for Chimera, Pegasus, Zephyr. Outside of there we call them linear

return g.copy()
elif labels == 'coordinate':
return chimera_graph(
g.graph['rows'],
n=g.graph['columns'],
t=g.graph['tile'],
node_list=self.iter_chimera_to_linear(g),
edge_list=self.iter_chimera_to_linear_pairs(g.edges),
data=g.graph['data'],
)
else:
raise ValueError(
f"Node labeling {labels} not recognized. Input must be generated by dwave_networkx.chimera_graph."
)

def graph_to_chimera(self, g):
"""Return a copy of the graph g relabeled to have chimera coordinates"""
labels = g.graph.get('labels')
if labels == 'int':
return chimera_graph(
g.graph['rows'],
n=g.graph['columns'],
t=g.graph['tile'],
node_list=self.iter_linear_to_chimera(g),
edge_list=self.iter_linear_to_chimera_pairs(g.edges),
data=g.graph['data'],
coordinates=True,
)
elif labels == 'coordinate':
return g.copy()
else:
raise ValueError(
f"Node labeling {labels} not recognized. Input must be generated by dwave_networkx.chimera_graph."
)

class __chimera_coordinates_cache_dict(dict):
"""An internal-use cached factory for `chimera_coordinates` objects"""

def __missing__(self, key):
self[key] = val = chimera_coordinates(*key)
return val


_chimera_coordinates_cache = __chimera_coordinates_cache_dict()

def linear_to_chimera(r, m, n=None, t=None):
"""Convert the linear index `r` into a chimera index.
Expand Down Expand Up @@ -484,7 +536,7 @@ def linear_to_chimera(r, m, n=None, t=None):
(3, 2, 1, 0)

"""
return chimera_coordinates(m, n, t).linear_to_chimera(r)
return _chimera_coordinates_cache[m, n, t].linear_to_chimera(r)


def chimera_to_linear(i, j, u, k, m, n=None, t=None):
Expand Down Expand Up @@ -521,4 +573,126 @@ def chimera_to_linear(i, j, u, k, m, n=None, t=None):
212

"""
return chimera_coordinates(m, n, t).chimera_to_linear((i, j, u, k))
return _chimera_coordinates_cache[m, n, t].chimera_to_linear((i, j, u, k))


def _chimera_sublattice_mapping(source_to_chimera, chimera_to_target, offset):
"""Constructs a mapping from one chimera graph to another, via an offset.
This function is used by chimera_sublattice_mappings, and serves to
construct a closure that is stable under iteration therein.

Parameters
----------
source_to_chimera : function
A function mapping a source node to a chimera-coordinate
chimera_to_target: function
A function mapping a chimera coordinate to a target nodes
offset : tuple (int, int)
A pair of ints representing the y- and x-offset of the sublattice

Returns
-------
mapping : function
The function implementing the mapping from the source Chimera
graph to the target Chimera graph. We store ``offset`` in the
attribute ``mapping.offset`` for later reconstruction.

"""
y_offset, x_offset = offset

def mapping(q):
y, x, u, k = source_to_chimera(q)
return chimera_to_target((y + y_offset, x + x_offset, u, k))

#store the offset in the mapping, so the user can reconstruct it
mapping.offset = offset

return mapping


def chimera_sublattice_mappings(source, target, offset_list=None):
"""Yields mappings from a Chimera graph into a larger Chimera graph.

A sublattice mapping is a function from nodes of a
``chimera_graph(m_s, n_s, t)`` to nodes of a ``chimera_graph(m_t, n_t, t)``
with ``m_s <= m_t`` and ``n_s <= n_t``. This is used to identify subgraphs
of the target Chimera graphs which are isomorphic to the source Chimera
graph. However, if the target graph is not of perfect yield, these
functions do not generally produce isomorphisms (for example, if a node is
missing in the target graph, it may still appear in the image of the source
graph).

Note that we do not produce mappings between Chimera graphs of different
tile parameters, and the mappings produced are not exhaustive. The mappings
take the form

``(y, x, u, k) -> (y+y_offset, x+x_offset, u, k)``

preserving the orientation and tile index of nodes. We use the notation of
Chimera coordinates above, but either or both of the target graph may have
integer or coordinate labels.

Academic note: the full group of isomorphisms of a Chimera graph includes
mappings which permute tile indices on a per-row and per-column basis, in
addition to reflections and rotations of the grid of unit cells where
rotations by 90 and 270 degrees induce a change in orientation. The full
set of sublattice mappings would take those isomorphisms into account; we do
not undertake that complexity here.

Parameters
----------
source : NetworkX Graph
The Chimera graph that nodes are input from
target : NetworkX Graph
The Chimera graph that nodes are input from
offset_list : iterable (tuple), optional (default None)
An iterable of offsets. This can be used to reconstruct a set of
mappings, as the offset used to generate a single mapping is stored
in the ``offset`` attribute of that mapping.

Yields
------
mapping : function
A function from nodes of the source graph, to nodes of the target
graph. The offset used to generate this mapping is stored in
``mapping.offset`` -- these can be collected and passed into
``offset_list`` in a later session.

"""
if not (source.graph.get('family') == target.graph.get('family') == 'chimera'):
raise ValueError("source and target graphs must be Chimera graphs constructed by dwave_networkx.chimera_graph")

t = source.graph['tile']
if t != target.graph['tile']:
raise ValueError("Cannot construct a sublattice mappings between Chimera graphs with different tile parameters")

m_s = source.graph['rows']
n_s = source.graph['columns']
labels_s = source.graph['labels']
if labels_s == 'int':
source_to_chimera = _chimera_coordinates_cache[m_s, n_s, t].linear_to_chimera
elif labels_s == 'coordinate':
def source_to_chimera(q):
return q
else:
raise ValueError(f"Chimera node labeling {labels_s} not recognized")

m_t = target.graph['rows']
n_t = target.graph['columns']
labels_t = target.graph['labels']
if labels_t == 'int':
chimera_to_target = _chimera_coordinates_cache[m_t, n_t, t].chimera_to_linear
elif labels_t == 'coordinate':
def chimera_to_target(q):
return q
else:
raise ValueError(f"Chimera node labeling {labels_t} not recognized")

if offset_list is None:
y_offsets = range(m_t - m_s + 1)
x_offsets = range(n_t - n_s + 1)
offset_list = product(y_offsets, x_offsets)

for offset in offset_list:
yield _chimera_sublattice_mapping(source_to_chimera, chimera_to_target, offset)

Loading