diff --git a/dwave_networkx/drawing/pegasus_layout.py b/dwave_networkx/drawing/pegasus_layout.py index 04711095..e540b4d4 100644 --- a/dwave_networkx/drawing/pegasus_layout.py +++ b/dwave_networkx/drawing/pegasus_layout.py @@ -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) @@ -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) diff --git a/dwave_networkx/drawing/zephyr_layout.py b/dwave_networkx/drawing/zephyr_layout.py index 2aa51183..4ec7dc9a 100644 --- a/dwave_networkx/drawing/zephyr_layout.py +++ b/dwave_networkx/drawing/zephyr_layout.py @@ -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.") diff --git a/dwave_networkx/generators/chimera.py b/dwave_networkx/generators/chimera.py index c3a164ec..11a9fe2f 100644 --- a/dwave_networkx/generators/chimera.py +++ b/dwave_networkx/generators/chimera.py @@ -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): @@ -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 @@ -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': + 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. @@ -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): @@ -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) + diff --git a/dwave_networkx/generators/pegasus.py b/dwave_networkx/generators/pegasus.py index ec24284e..b570614c 100644 --- a/dwave_networkx/generators/pegasus.py +++ b/dwave_networkx/generators/pegasus.py @@ -22,8 +22,12 @@ from dwave_networkx.exceptions import DWaveNetworkXException import warnings +from itertools import product +from .chimera import _chimera_coordinates_cache + __all__ = ['pegasus_graph', 'pegasus_coordinates', + 'pegasus_sublattice_mappings', ] @@ -192,14 +196,16 @@ def pegasus_graph(m, create_using=None, node_list=None, edge_list=None, data=Tru if offsets_index != 0: raise NotImplementedError("nice coordinate system is only implemented for offsets_index 0") labels = 'nice' - p2n = pegasus_coordinates.pegasus_to_nice - c2i = lambda *q: p2n(q) + pegasus_to_nice = pegasus_coordinates.pegasus_to_nice + nice_to_pegasus = pegasus_coordinates.nice_to_pegasus + label = lambda *q: pegasus_to_nice(q) elif coordinates: - c2i = lambda *q: q + label = lambda *q: q labels = 'coordinate' else: labels = 'int' - def c2i(u, w, k, z): return u * 12 * m * m1 + w * 12 * m1 + k * m1 + z + def label(u, w, k, z): + return u * 12 * m * m1 + w * 12 * m1 + k * m1 + z construction = (("family", "pegasus"), ("rows", m), ("columns", m), ("tile", 12), ("vertical_offsets", offset_lists[0]), @@ -220,13 +226,13 @@ def c2i(u, w, k, z): return u * 12 * m * m1 + w * 12 * m1 + k * m1 + z else: fabric_end = fabric_start = 0, 0 - G.add_edges_from((c2i(u, w, k, z), c2i(u, w, k, z + 1)) + G.add_edges_from((label(u, w, k, z), label(u, w, k, z + 1)) for u in (0, 1) for w in range(m) for k in range(fabric_start[u] if w == 0 else 0, 12 - (fabric_end[u] if w == m1 else 0)) for z in range(m1 - 1)) - G.add_edges_from((c2i(u, w, k, z), c2i(u, w, k + 1, z)) + G.add_edges_from((label(u, w, k, z), label(u, w, k + 1, z)) for u in (0, 1) for w in range(m) for k in range(fabric_start[u] if w == 0 else 0, 12 - (fabric_end[u] if w == m1 else 0), 2) @@ -244,7 +250,7 @@ def efilter(e): return qfilter(*e[0]) and qfilter(*e[1]) for kk in range(12) for k in range(0 if w else off1[kk], 12 if w < m1 else off1[kk]) for z in range(m1)) - G.add_edges_from((c2i(*e[0]), c2i(*e[1])) for e in internal_couplers if efilter(e)) + G.add_edges_from((label(*e[0]), label(*e[1])) for e in internal_couplers if efilter(e)) else: G.add_edges_from(edge_list) @@ -256,22 +262,30 @@ def efilter(e): return qfilter(*e[0]) and qfilter(*e[1]) if data: v = 0 + if nice_coordinates: + def fill_data(): + q = (u, w, k, z) + d = get_node_data(pegasus_to_nice(q)) + if d is not None: + d['linear_index'] = v + d['pegasus_index'] = q + elif coordinates: + def fill_data(): + d = get_node_data((u, w, k, z)) + if d is not None: + d['linear_index'] = v + else: + def fill_data(): + d = get_node_data(v) + if d is not None: + d['pegasus_index'] = (u, w, k, z) + + get_node_data = G.nodes.get for u in range(2): for w in range(m): for k in range(12): for z in range(m1): - q = u, w, k, z - if nice_coordinates: - p = c2i(*q) - if p in G: - G.nodes[p]['linear_index'] = v - G.nodes[p]['pegasus_index'] = q - elif coordinates: - if q in G: - G.nodes[q]['linear_index'] = v - else: - if v in G: - G.nodes[v]['pegasus_index'] = q + fill_data() v += 1 return G @@ -488,6 +502,7 @@ def fragmented_edges(pegasus_graph): else: yield ((fw1, fw0, u0, k0&1), (fw1, fw0, u1, k1&1)) + # Developer note: we could implement a function that creates the iter_*_to_* and # iter_*_to_*_pairs methods just-in-time, but there are a small enough number # that for now it makes sense to do them by hand. @@ -744,6 +759,86 @@ def iter_nice_to_linear_pairs(self, nlist): """ return self._pair_repack(self.iter_nice_to_linear, nlist) + 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': + return g.copy() + elif labels == 'coordinate': + nodes = self.iter_pegasus_to_linear(g) + edges = self.iter_pegasus_to_linear_pairs(g.edges) + elif labels == 'nice': + nodes = self.iter_nice_to_linear(g) + edges = self.iter_nice_to_linear_pairs(g.edges) + else: + raise ValueError( + f"Node labeling {labels} not recognized. Input must be generated by dwave_networkx.pegasus_graph." + ) + + return pegasus_graph( + g.graph['rows'], + node_list=nodes, + edge_list=edges, + data=g.graph['data'], + offset_lists=( + g.graph["vertical_offsets"], + g.graph["horizontal_offsets"], + ), + ) + + def graph_to_pegasus(self, g): + """Return a copy of the graph g relabeled to have pegasus coordinates""" + labels = g.graph.get('labels') + if labels == 'int': + nodes = self.iter_linear_to_pegasus(g) + edges = self.iter_linear_to_pegasus_pairs(g.edges) + elif labels == 'coordinate': + return g.copy() + elif labels == 'nice': + nodes = self.iter_nice_to_pegasus(g) + edges = self.iter_nice_to_pegasus_pairs(g.edges) + else: + raise ValueError( + f"Node labeling {labels} not recognized. Input must be generated by dwave_networkx.pegasus_graph." + ) + + return pegasus_graph( + g.graph['rows'], + node_list=nodes, + edge_list=edges, + data=g.graph['data'], + coordinates=True, + offset_lists=( + g.graph["vertical_offsets"], + g.graph["horizontal_offsets"], + ), + ) + + def graph_to_nice(self, g): + """Return a copy of the graph p relabeled to have nice coordinates""" + labels = g.graph.get('labels') + if labels == 'int': + nodes = self.iter_linear_to_nice(g) + edges = self.iter_linear_to_nice_pairs(g.edges) + elif labels == 'coordinate': + nodes = self.iter_pegasus_to_nice(g) + edges = self.iter_pegasus_to_nice_pairs(g.edges) + elif labels == 'nice': + return g.copy() + else: + raise ValueError( + f"Node labeling {labels} not recognized. Input must be generated by dwave_networkx.pegasus_graph." + ) + + return pegasus_graph( + g.graph['rows'], + node_list=nodes, + edge_list=edges, + data=g.graph['data'], + nice_coordinates=True, + offsets_index = 0, + ) + def int(self, q): """Deprecated alias of `pegasus_to_linear`.""" msg = ('pegasus_coordinates.int is deprecated and will be removed in ' @@ -849,3 +944,209 @@ def get_nice_to_pegasus_fn(*args, **kwargs): warnings.warn(msg, DeprecationWarning) return lambda *args: pegasus_coordinates.nice_to_pegasus(args) + + +def _chimera_pegasus_sublattice_mapping(source_to_chimera, nice_to_target, offset): + """Constructs a mapping from a Chimera graph to a Pegasus graph, via an offset. + This function is used by pegasus_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 + nice_to_target: function + A function mapping a pegasus nice-coordinate to a target node + offset : tuple (int, int, int) + A triplet of ints representing the t-, y- and x-offset of the + sublattice. + + Returns + ------- + mapping : function + The function implementing the mapping from the source Chimera + graph to the target Pegasus graph. We store ``offset`` in the + attribute ``mapping.offset`` for later reconstruction. + + """ + t_offset, y_offset, x_offset = offset + + def mapping(q): + y, x, u, k = source_to_chimera(q) + return nice_to_target((t_offset, 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 + + +class __pegasus_coordinates_cache_dict(dict): + """An internal-use cached factory for `pegasus_coordinates` objects""" + def __missing__(self, key): + self[key] = val = pegasus_coordinates(key) + return val + +_pegasus_coordinates_cache = __pegasus_coordinates_cache_dict() + +# a set of manually-computed values to speed up the generation of sublattice +# mappings, used in _pegasus_pegasus_sublattice_mapping +_sublattice_delta_list = ( + ((0, 0, 0), (1, 0, 0), (2, 0, 0)), + ((1, 1, 0), (2, 1, 0), (0, 0, 1)), + ((2, 1, 0), (0, 0, 1), (1, 0, 1)), +) + +def _pegasus_pegasus_sublattice_mapping(source_to_nice, nice_to_target, offset): + """Constructs a mapping from a Pegasus graph to a Pegasus graph, via an offset. + This function is used by pegasus_sublattice_mappings, and serves to + construct a closure that is stable under iteration therein. + + Parameters + ---------- + source_to_nice : function + A function mapping a source node to a pegasus nice-coordinate + nice_to_target: function + A function mapping a pegasus nice-coordinate to a target node. + offset : tuple (int, int, int) + A triplet of ints representing the t-, y- and x-offset of the + sublattice. + + Returns + ------- + mapping : function + The function implementing the mapping from the source Pegasus + graph to the target Pegasus graph. We store ``offset`` in the + attribute ``mapping.offset`` for later reconstruction. + + """ + t_offset, y_offset, x_offset = offset + delta = _sublattice_delta_list[t_offset] + def mapping(q): + T, Y, X, u, k = source_to_nice(q) + t, dy, dx = delta[T] + return nice_to_target((t, Y + dy + y_offset, X + dx + x_offset, u, k)) + + #store the offset in the mapping, so the user can reconstruct it + mapping.offset = offset + + return mapping + + +def pegasus_sublattice_mappings(source, target, offset_list=None): + """Yields mappings from a Chimera or Pegasus graph into a Pegasus graph. + + A sublattice mapping is a function from nodes of a ``pegasus_graph(m_s)`` or + ``chimera_graph(m_c, n_c, 4)`` to nodes of a ``pegasus_graph(m_t)`` with + ``m_s <= m_t`` or ``m_c <= m_t - 1`` and ``n_c <= m_t - 1``. This is used + to identify subgraphs of the target Pegasus graphs which are isomorphic to + the source 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 require the tile parameter of Chimera graphs to be 4, and the + mappings produced are not exhaustive. The mappings take the form + + ``(y, x, u, k) -> (t_offset, y+y_offset, x+x_offset, u, k)`` + + when the source is a Chimera graph, or + + ``(t, y, x, u, k) -> ((t + t_offset)%3, y+y_offset, x+x_offset, u, k)`` + + when the source is a Pegasus graph; preserving the orientation and tile + index of nodes. We use the notation of Chimera coordinates and Pegasus nice + coordinates above, but the mapping produced will respect the labelings of + the source and target graph. Note, the notation above for Pegasus->Pegasus + mappings is only suggestive. See _pegasus_pegasus_sublattice_mapping for a + precise formula. + + 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 + isomorphisms of Pegasus graphs permit the swapping across rows and columns + of odd couplers, as well as a reflection about the main antidiagonal which + induces 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 or Pegasus graph that nodes are input from + target : NetworkX Graph + The Pegasus graph that nodes are output to + 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 target.graph.get('family') != 'pegasus': + raise ValueError("source graphs must a Pegasus graph constructed by dwave_networkx.pegasus_graph") + + m_t = target.graph['rows'] + labels_t = target.graph['labels'] + if labels_t == 'int': + nice_to_target = _pegasus_coordinates_cache[m_t].nice_to_linear + elif labels_t == 'coordinate': + nice_to_target = _pegasus_coordinates_cache[m_t].nice_to_pegasus + elif labels_t == 'nice': + def nice_to_target(q): + return q + else: + raise ValueError(f"Pegasus node labeling {labels_t} not recognized") + + labels_s = source.graph['labels'] + if source.graph.get('family') == 'chimera': + if source.graph['tile'] != 4: + raise ValueError("Cannot construct sublattice mappings from Chimera to Pegasus unless the Chimera tile parameter is 4") + + m_s = source.graph['rows'] + n_s = source.graph['columns'] + if offset_list is None: + offset_list = product([0, 1, 2], range(m_t - m_s), range(m_t - n_s)) + if labels_s == 'coordinate': + def source_to_inner(q): + return q + elif labels_s == 'int': + source_to_inner = _chimera_coordinates_cache[m_s, n_s, 4].linear_to_chimera + else: + raise ValueError(f"Chimera node labeling {labels_s} not recognized") + + make_mapping = _chimera_pegasus_sublattice_mapping + + elif source.graph.get('family') == 'pegasus': + m_s = source.graph['rows'] + if offset_list is None: + ranges = range(m_t - m_s + 1), range(m_t - m_s), range(m_t - m_s) + offset_list = ((t, y, x) for t in range(3) for y in ranges[t] for x in ranges[t]) + + labels_s = source.graph['labels'] + if labels_s == 'int': + source_to_inner = _pegasus_coordinates_cache[m_s].linear_to_nice + elif labels_s == 'coordinate': + source_to_inner = _pegasus_coordinates_cache[m_s].pegasus_to_nice + elif labels_s == 'nice': + def source_to_inner(q): + return q + else: + raise ValueError(f"Pegasus node labeling {labels_s} not recognized") + + make_mapping = _pegasus_pegasus_sublattice_mapping + + else: + raise ValueError("source graph must be a Chimera graph or Pegasus graph constructed by dwave_networkx.chimera_graph or dwave_networkx.pegasus_graph respectively") + + for offset in offset_list: + yield make_mapping(source_to_inner, nice_to_target, offset) + diff --git a/dwave_networkx/generators/zephyr.py b/dwave_networkx/generators/zephyr.py index d45848dc..0362c27f 100644 --- a/dwave_networkx/generators/zephyr.py +++ b/dwave_networkx/generators/zephyr.py @@ -23,13 +23,15 @@ from dwave_networkx.exceptions import DWaveNetworkXException +from .chimera import _chimera_coordinates_cache __all__ = ['zephyr_graph', 'zephyr_coordinates', + 'zephyr_sublattice_mappings', ] -def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, +def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, data=True, coordinates=False): """ Creates a Zephyr graph [brk]_ with grid parameter ``m`` and tile parameter ``t``. @@ -45,7 +47,7 @@ def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, with the new graph. Usually used to set the type of the graph. node_list : iterable, optional (default None) Iterable of nodes in the graph. If None, calculated from ``m``. - Note that this list is used to remove nodes, so only specified nodes + Note that this list is used to remove nodes, so only specified nodes that belong to the base node set (described in the ``coordinates`` parameter below) will be added. edge_list : iterable, optional (default None) @@ -143,7 +145,7 @@ def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, G.name = "zephyr_graph(%s, %s)" % (m, t) M = 2*m+1 - + if coordinates: def label(*q): return q @@ -187,28 +189,26 @@ def label(u, w, k, j, z): G.add_nodes_from(nodes) # for singleton nodes if data: - v = 0 - def coord_label(): - return q - def int_label(): - return v if coordinates: - other_name = 'linear_index' - this_label = coord_label - other_label = int_label + def fill_data(): + d = get_node_data((u, w, k, j, z)) + if d is not None: + d['linear_index'] = v + else: - other_name = 'zephyr_index' - this_label = int_label - other_label = coord_label + def fill_data(): + d = get_node_data(v) + if d is not None: + d['zephyr_index'] = (u, w, k, j, z) + + v = 0 + get_node_data = G.nodes.get for u in range(2): for w in range(M): for k in range(t): for j in (0, 1): for z in range(m): - q = u, w, k, j, z - p = this_label() - if p in G: - G.nodes[p][other_name] = other_label() + fill_data() v += 1 return G @@ -226,14 +226,14 @@ class zephyr_coordinates(object): Grid parameter for the Zephyr lattice. t : int Tile parameter for the Zephyr lattice; must be even. - + See also -------- :func:`.zephyr_graph` : Describes the various coordinate conventions. """ def __init__(self, m, t=4): - self.args = m, 2*m+1, t + self.args = m, 2 * m + 1, t def zephyr_to_linear(self, q): """Convert a 5-term Zephyr coordinate into a linear index. @@ -293,7 +293,6 @@ def iter_linear_to_zephyr(self, rlist): u, w = divmod(r, M) yield u, w, k, j, z - @staticmethod def _pair_repack(f, plist): """Flattens a sequence of pairs to pass through `f`, and then @@ -315,3 +314,338 @@ def iter_linear_to_zephyr_pairs(self, plist): to pairs of 5-term Zephyr coordinates. """ return self._pair_repack(self.iter_linear_to_zephyr, 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': + return g.copy() + elif labels == 'coordinate': + nodes = self.iter_zephyr_to_linear(g) + edges = self.iter_zephyr_to_linear_pairs(g.edges) + else: + raise ValueError( + f"Node labeling {labels} not recognized. Input must be generated by dwave_networkx.zephyr_graph." + ) + + return zephyr_graph( + g.graph['rows'], + t = g.graph['tile'], + node_list=nodes, + edge_list=edges, + data=g.graph['data'], + ) + + def graph_to_zephyr(self, g): + """Return a copy of the graph g relabeled to have zephyr coordinates""" + labels = g.graph.get('labels') + if labels == 'int': + nodes = self.iter_linear_to_zephyr(g) + edges = self.iter_linear_to_zephyr_pairs(g.edges) + elif labels == 'coordinate': + return g.copy() + else: + raise ValueError( + f"Node labeling {labels} not recognized. Input must be generated by dwave_networkx.zephyr_graph." + ) + + return zephyr_graph( + g.graph['rows'], + t=g.graph['tile'], + node_list=nodes, + edge_list=edges, + data=g.graph['data'], + coordinates=True, + ) + + +class __zephyr_coordinates_cache_dict(dict): + """An internal-use cached factory for `zephyr_coordinates` objects""" + + def __missing__(self, key): + self[key] = val = zephyr_coordinates(*key) + return val + + +_zephyr_coordinates_cache = __zephyr_coordinates_cache_dict() + + +def _zephyr_zephyr_sublattice_mapping(source_to_zephyr, zephyr_to_target, offset): + """Constructs a mapping from a Zephyr graph to a Zephyr graph, via an offset. + This function is used by zephyr_sublattice_mappings, and serves to construct + a closure that is stable under iteration therein. + + The mappings implemented by this function interpret offsets in the grid of + the Chimera(2m+1, 2m+1, 2*t) graphs underlying the source and tartget Zephyr + graphs. The formulas (see implementation) are somewhat complex, because + + * a shift by a y-unit induces a reversal of the orthogonal minor offset + (j index) of vertical qubits, + * a shift by an x-unit induces a reversal of the j index of horizontal + qubits, and + * a shift by a unit parallel to a qubit is equivalent to 1/2-unit shift + in the z-direction (but z-coordinates are integral) which is + mediated by the j index. + + Parameters + ---------- + source_to_zephyr : function + A function mapping a source node to a zephyr coordinate + zephyr_to_target: function + A function mapping a zephyr coordinate to a target node + 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 Zephyr + graph to the target Zephyr graph. We store ``offset`` in the + attribute ``mapping.offset`` for later reconstruction. + + """ + y_offset, x_offset = offset + + delta = [ + [y_offset % 2, x_offset, y_offset], + [x_offset % 2, y_offset, x_offset], + ] + + def mapping(q): + u, w, k, j, z = source_to_zephyr(q) + dj, dw, dz = delta[u] + return zephyr_to_target((u, w + dw, k, j ^ dj, z + (dz + j) // 2)) + + #store the offset in the mapping, so the user can reconstruct it + mapping.offset = offset + + return mapping + +def _single_chimera_zephyr_sublattice_mapping(source_to_chimera, zephyr_to_target, offset): + """Constructs a mapping from a Chimera graph to a Zephyr graph, via an offset. + This function is used by zephyr_sublattice_mappings, and serves to construct + a closure that is stable under iteration therein. + + The mappings implemented by this function view a ``chimera(2*m, 2*m, t)`` as + a subgraph of ``zephyr_graph(m, t)`` through the mapping + + (2*y+j, x, 0, k) -> (0, x, k, j, y) + (y, 2*x+j, 1, k) -> (1, y, k, j, x) + + which interprets odd couplers of Zephyr as external couplers of Chimera. + The above is a slight simplification of matters; it is the simplest of a + family of :math:`(t+1)^2` offsets (see how ``k_offset0`` and ``k_offset`` + are used in the implementation). + + Additionally, the sublattice represented by the source graph can have x- + and y-offsets into the chimera graph above, as with ordinary Chimera + subgraph mappings. + + Parameters + ---------- + source_to_chimera : function + A function mapping a source node to a chimera coordinate + zephyr_to_target: function + A function mapping a zephyr coordinate to a target node + offset : tuple (int, int, int, int, int) + A tuple of ints (t, k_offset0, k_offset1, y_offset, x_offset) + defining the sublattice mapping + + Returns + ------- + mapping : function + The function implementing the mapping from the source Zephyr + graph to the target Zephyr graph. We store ``offset`` in the + attribute ``mapping.offset`` for later reconstruction. + + """ + t, y_offset, x_offset, k_offset0, k_offset1 = offset + + def mapping(q): + y, x, u, k = source_to_chimera(q) + if u: + dw, k = divmod(k + k_offset1, t) + z, j = divmod(x + x_offset, 2) + return zephyr_to_target((u, y + y_offset + dw, k, j, z)) + else: + dw, k = divmod(k + k_offset0, t) + z, j = divmod(y + y_offset, 2) + return zephyr_to_target((u, x + x_offset + dw, k, j, z)) + + #store the offset in the mapping, so the user can reconstruct it + mapping.offset = offset + + return mapping + +def _double_chimera_zephyr_sublattice_mapping(source_to_chimera, zephyr_to_target, offset): + """Constructs a mapping from a Chimera graph to a Zephyr graph, via an offset. + This function is used by zephyr_sublattice_mappings, and serves to construct + a closure that is stable under iteration therein. + + The mappings implemented by this function view a ``chimera(m, m, 2*t)`` as + a subgraph of ``zephyr_graph(m, t)`` through the mappings + + (y, x, 0, k) -> (0, x, k, j0, y) + (y, x, 1, k) -> (1, y, k, j1, x) + + where j0 and j1 are each 0 or 1. Additionally, the sublattice represented + by the source graph can have x- and y-offsets into the chimera graph above, + as with ordinary Chimera subgraph mappings. + + Parameters + ---------- + source_to_chimera : function + A function mapping a source node to a chimera coordinate + zephyr_to_target: function + A function mapping a zephyr coordinate to a target node + offset : tuple (int, int, int, int, int) + A tuple of ints (t, j0, j1, y_offset, x_offset) defining the + sublattice mapping + + Returns + ------- + mapping : function + The function implementing the mapping from the source Zephyr + graph to the target Zephyr graph. We store ``offset`` in the + attribute ``mapping.offset`` for later reconstruction. + + """ + t, y_offset, x_offset, j0, j1 = offset + def mapping(q): + y, x, u, k = source_to_chimera(q) + wz, kz = divmod(k, t) + if u: + return zephyr_to_target((u, 2 * (y + y_offset) + j0 + wz, kz, j1, x + x_offset)) + else: + return zephyr_to_target((u, 2 * (x + x_offset) + j1 + wz, kz, j0, y + y_offset)) + + #store the offset in the mapping, so the user can reconstruct it + mapping.offset = offset + + return mapping + + +def zephyr_sublattice_mappings(source, target, offset_list=None): + """Yields mappings from a Chimera or Zephyr graph into a Zephyr graph. + + A sublattice mapping is a function from nodes of + * a ``zephyr_graph(m_s, t)`` to nodes of a ``zephyr_graph(m_t, t)`` + where ``m_s <= m_t``, + * a ``chimera_graph(m_s, n_s, t)`` to nodes of a ``zephyr_graph(m_t, t)`` + where ``m_s <= 2*m_t`` and ``n_s <= 2*m_t``, or + * a ``chimera_graph(m_s, n_s, 2*t)`` to nodes of a ``zephyr_graph(m_t, t)`` + where ``m_s <= m_t`` and ``n_s <= m_t``, or + + This is used to identify subgraphs of the target Zephyr graphs which are + isomorphic to the source 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 require the tile parameter of Chimera graphs to be either the + same our double that of the target Zephyr graphs; or if both graphs are + Zephyr graphs, we require the tile parameters to be the same. The mappings + we produce preserve the linear ordering of tile indices; see + ``_zephyr_zephyr_sublattice_mapping``, + ``_double_chimera_zephyr_sublattice_mapping``, and + ``_single_chimera_zephyr_sublattice_mapping`` for more details. + + 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 + isomorphisms of Zephyr graphs permit permutations of major tile indices on a + per-row and per-column basis, in addition to reflections of the grid which + induce inversion of orthogonal minor offsets, and rotations which induce + inversions of minor offsets and/or 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 or Zephyr graph that nodes are input from + target : NetworkX Graph + The Zephyr graph that nodes are output to + 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 target.graph.get('family') != 'zephyr': + raise ValueError("source graphs must a Zephyr graph constructed by dwave_networkx.zephyr_graph") + + m_t = target.graph['rows'] + t = target.graph['tile'] + labels_t = target.graph['labels'] + if labels_t == 'int': + zephyr_to_target = _zephyr_coordinates_cache[m_t, t].zephyr_to_linear + elif labels_t == 'coordinate': + def zephyr_to_target(q): + return q + else: + raise ValueError(f"Zephyr node labeling {labels_t} not recognized") + + labels_s = source.graph['labels'] + if source.graph.get('family') == 'chimera': + t_t = source.graph['tile'] + m_s = source.graph['rows'] + n_s = source.graph['columns'] + + if t_t == t: + make_mapping = _single_chimera_zephyr_sublattice_mapping + if offset_list is None: + krange = range(t+1) + mrange = range(2*m_t - m_s + 1) + nrange = range(2*m_t - n_s + 1) + offset_list = product([t], mrange, nrange, krange, krange) + elif t_t == 2*t: + make_mapping = _double_chimera_zephyr_sublattice_mapping + if offset_list is None: + jrange = range(2) + mrange = range(m_t - m_s + 1) + nrange = range(m_t - n_s + 1) + offset_list = product([t], mrange, nrange, jrange, jrange) + else: + raise ValueError(f"Cannot construct sublattice mappings from Chimera to this Zephyr graph unless the tile parameter of the chimera graph is {t} or {2*t}.") + + if labels_s == 'coordinate': + def source_to_inner(q): + return q + elif labels_s == 'int': + source_to_inner = _chimera_coordinates_cache[m_s, n_s, t_t].linear_to_chimera + else: + raise ValueError(f"Chimera node labeling {labels_s} not recognized") + + elif source.graph.get('family') == 'zephyr': + m_s = source.graph['rows'] + if offset_list is None: + mrange = range((2*m_t+1) - (2*m_s+1) + 1) + offset_list = product(mrange, mrange) + + labels_s = source.graph['labels'] + if labels_s == 'int': + source_to_inner = _zephyr_coordinates_cache[m_s, t].linear_to_zephyr + elif labels_s == 'coordinate': + def source_to_inner(q): + return q + else: + raise ValueError(f"Zephyr node labeling {labels_s} not recognized") + + make_mapping = _zephyr_zephyr_sublattice_mapping + + else: + raise ValueError("source graph must be a Chimera graph or Zephyr graph constructed by dwave_networkx.chimera_graph or dwave_networkx.zephyr_graph respectively") + + for offset in offset_list: + yield make_mapping(source_to_inner, zephyr_to_target, offset) + diff --git a/tests/test_generator_chimera.py b/tests/test_generator_chimera.py index 1963219c..bcab14dc 100644 --- a/tests/test_generator_chimera.py +++ b/tests/test_generator_chimera.py @@ -225,3 +225,60 @@ def test_nonsquare_coordinate_generator(self): E = nx.relabel_nodes(H, coords.linear_to_chimera, copy=True) self.assertEqual(set(map(frozenset, E.edges)), set(map(frozenset, G.edges))) + + + def test_graph_relabeling(self): + def graph_equal(g, h): + self.assertEqual(set(g), set(h)) + self.assertEqual( + set(map(tuple, map(sorted, g.edges))), + set(map(tuple, map(sorted, g.edges))) + ) + for v, d in g.nodes(data=True): + self.assertEqual(h.nodes[v], d) + + coords = dnx.chimera_coordinates(3) + for data in True, False: + c3l = dnx.chimera_graph(3, data=data) + c3c = dnx.chimera_graph(3, data=data, coordinates=True) + + graph_equal(c3l, coords.graph_to_linear(c3c)) + graph_equal(c3l, coords.graph_to_linear(c3l)) + graph_equal(c3c, coords.graph_to_chimera(c3l)) + graph_equal(c3c, coords.graph_to_chimera(c3c)) + + h = dnx.chimera_graph(2) + del h.graph['labels'] + with self.assertRaises(ValueError): + coords.graph_to_linear(h) + with self.assertRaises(ValueError): + coords.graph_to_chimera(h) + + + def test_sublattice_mappings(self): + def check_subgraph_mapping(f, g, h): + for v in g: + if not h.has_node(f(v)): + raise RuntimeError(f"node {v} mapped to {f(v)} is not in {h.graph['name']} ({h.graph['labels']})") + for u, v in g.edges: + if not h.has_edge(f(u), f(v)): + raise RuntimeError(f"edge {(u, v)} mapped to {(f(u), f(v))} not present in {h.graph['name']} ({h.graph['labels']})") + + c2l = dnx.chimera_graph(2) + c2c = dnx.chimera_graph(2, coordinates=True) + c32l = dnx.chimera_graph(3, 2) + c23c = dnx.chimera_graph(2, 3, coordinates=True) + + c5l = dnx.chimera_graph(5) + c5c = dnx.chimera_graph(5, coordinates=True) + c54l = dnx.chimera_graph(5, 4) + c45c = dnx.chimera_graph(4, 5, coordinates=True) + + for target in c5l, c5c, c54l, c45c: + for source in c2l, c2c, c32l, c23c, target: + covered = set() + for f in dnx.chimera_sublattice_mappings(source, target): + check_subgraph_mapping(f, source, target) + covered.update(map(f, source)) + self.assertEqual(covered, set(target)) + diff --git a/tests/test_generator_pegasus.py b/tests/test_generator_pegasus.py index 6fc6df6b..9c7e75c5 100644 --- a/tests/test_generator_pegasus.py +++ b/tests/test_generator_pegasus.py @@ -88,12 +88,25 @@ def test_nice_coordinates(self): pg = (t,) + p qg = (t,) + q self.assertTrue(G.has_edge(pg, qg)) - n2p = dnx.pegasus_coordinates.nice_to_pegasus - p2n = dnx.pegasus_coordinates.pegasus_to_nice + coords = dnx.pegasus_coordinates(4) + n2p = coords.nice_to_pegasus + p2n = coords.pegasus_to_nice + n2l = coords.nice_to_linear + l2n = coords.linear_to_nice for p in G.nodes(): self.assertEqual(p2n(n2p(p)), p) + self.assertEqual(l2n(n2l(p)), p) self.assertTrue(H.has_node(p[1:])) + G = dnx.pegasus_graph(4) + for p in G.nodes(): + self.assertEqual(n2l(l2n(p)), p) + + G = dnx.pegasus_graph(4, coordinates=True) + for p in G.nodes(): + self.assertEqual(n2p(p2n(p)), p) + + def test_consistent_linear_nice_pegasus(self): P4 = dnx.pegasus_graph(4, nice_coordinates=True) @@ -190,6 +203,92 @@ def test_coordinate_subgraphs(self): self.assertEqual(EG, sorted(map(sorted, coords.iter_pegasus_to_linear_pairs(Hn.edges())))) self.assertEqual(EH, sorted(map(sorted, coords.iter_linear_to_pegasus_pairs(Gn.edges())))) + def test_graph_relabeling(self): + def graph_equal(g, h): + self.assertEqual(set(g), set(h)) + self.assertEqual( + set(map(tuple, map(sorted, g.edges))), + set(map(tuple, map(sorted, g.edges))) + ) + for v, d in g.nodes(data=True): + self.assertEqual(h.nodes[v], d) + + coords = dnx.pegasus_coordinates(3) + nodes_nice = dnx.pegasus_graph(3, nice_coordinates=True) + nodes_linear = list(coords.iter_nice_to_linear(nodes_nice)) + nodes_pegasus = list(coords.iter_nice_to_pegasus(nodes_nice)) + + for data in True, False: + p3l = dnx.pegasus_graph(3, data=data).subgraph(nodes_linear) + p3p = dnx.pegasus_graph(3, data=data, coordinates=True).subgraph(nodes_pegasus) + p3n = dnx.pegasus_graph(3, data=data, nice_coordinates=True) + + graph_equal(p3l, coords.graph_to_linear(p3l)) + graph_equal(p3l, coords.graph_to_linear(p3p)) + graph_equal(p3l, coords.graph_to_linear(p3n)) + + graph_equal(p3p, coords.graph_to_pegasus(p3l)) + graph_equal(p3p, coords.graph_to_pegasus(p3p)) + graph_equal(p3p, coords.graph_to_pegasus(p3n)) + + graph_equal(p3n, coords.graph_to_nice(p3l)) + graph_equal(p3n, coords.graph_to_nice(p3p)) + graph_equal(p3n, coords.graph_to_nice(p3n)) + + h = dnx.pegasus_graph(2) + del h.graph['labels'] + with self.assertRaises(ValueError): + coords.graph_to_nice(h) + with self.assertRaises(ValueError): + coords.graph_to_linear(h) + with self.assertRaises(ValueError): + coords.graph_to_pegasus(h) + + def test_sublattice_mappings(self): + def check_subgraph_mapping(f, g, h): + for v in g: + if not h.has_node(f(v)): + raise RuntimeError(f"node {v} mapped to {f(v)} is not in {h.graph['name']} ({h.graph['labels']})") + for u, v in g.edges: + if not h.has_edge(f(u), f(v)): + raise RuntimeError(f"edge {(u, v)} mapped to {(f(u), f(v))} not present in {h.graph['name']} ({h.graph['labels']})") + + coords5 = dnx.pegasus_coordinates(5) + coords3 = dnx.pegasus_coordinates(3) + + p3l = dnx.pegasus_graph(3) + p3c = dnx.pegasus_graph(3, coordinates=True) + p3n = coords3.graph_to_nice(p3c) + + p5l = dnx.pegasus_graph(5) + p5c = dnx.pegasus_graph(5, coordinates=True) + p5n = coords5.graph_to_nice(p5c) + + for target in p5l, p5c, p5n: + for source in p3l, p3c, p3n: + covered = set() + for f in dnx.pegasus_sublattice_mappings(source, target): + check_subgraph_mapping(f, source, target) + covered.update(map(f, source)) + self.assertEqual(covered, set(target)) + + c2l = dnx.chimera_graph(2) + c2c = dnx.chimera_graph(2, coordinates=True) + c23l = dnx.chimera_graph(2, 3) + c32c = dnx.chimera_graph(3, 2, coordinates=True) + + p5n = dnx.pegasus_graph(5, nice_coordinates=True) + p5l = coords5.graph_to_linear(p5n) + p5c = coords5.graph_to_pegasus(p5n) + + for target in p5l, p5c, p5n: + for source in c2l, c2c, c23l, c32c, target: + covered = set() + for f in dnx.pegasus_sublattice_mappings(source, target): + check_subgraph_mapping(f, source, target) + covered.update(map(f, source)) + self.assertEqual(covered, set(target)) + class TestTupleFragmentation(unittest.TestCase): diff --git a/tests/test_generator_zephyr.py b/tests/test_generator_zephyr.py index 954ae051..33aefad3 100644 --- a/tests/test_generator_zephyr.py +++ b/tests/test_generator_zephyr.py @@ -127,3 +127,64 @@ def test_coordinate_subgraphs(self): self.assertEqual(EG, sorted(map(sorted, coords.iter_zephyr_to_linear_pairs(Hn.edges())))) self.assertEqual(EH, sorted(map(sorted, coords.iter_linear_to_zephyr_pairs(Gn.edges())))) + + def test_graph_relabeling(self): + def graph_equal(g, h): + self.assertEqual(set(g), set(h)) + self.assertEqual( + set(map(tuple, map(sorted, g.edges))), + set(map(tuple, map(sorted, g.edges))) + ) + for v, d in g.nodes(data=True): + self.assertEqual(h.nodes[v], d) + + coords = dnx.zephyr_coordinates(3) + for data in True, False: + z3l = dnx.zephyr_graph(3, data=data) + z3c = dnx.zephyr_graph(3, data=data, coordinates=True) + + graph_equal(z3l, coords.graph_to_linear(z3l)) + graph_equal(z3l, coords.graph_to_linear(z3c)) + + graph_equal(z3c, coords.graph_to_zephyr(z3c)) + graph_equal(z3c, coords.graph_to_zephyr(z3l)) + + h = dnx.zephyr_graph(2) + del h.graph['labels'] + with self.assertRaises(ValueError): + coords.graph_to_linear(h) + with self.assertRaises(ValueError): + coords.graph_to_zephyr(h) + + + def test_sublattice_mappings(self): + def check_subgraph_mapping(f, g, h): + for v in g: + if not h.has_node(f(v)): + raise RuntimeError(f"node {v} mapped to {f(v)} is not in {h.graph['name']} ({h.graph['labels']})") + for u, v in g.edges: + if not h.has_edge(f(u), f(v)): + raise RuntimeError(f"edge {(u, v)} mapped to {(f(u), f(v))} not present in {h.graph['name']} ({h.graph['labels']})") + + z2l = dnx.zephyr_graph(2) + z2c = dnx.zephyr_graph(2, coordinates=True) + c2l = dnx.chimera_graph(2) + c2c = dnx.chimera_graph(2, coordinates=True) + c23l = dnx.chimera_graph(2, 3) + c32c = dnx.chimera_graph(3, 2, coordinates=True) + c2l8 = dnx.chimera_graph(2, t=8) + c2c8 = dnx.chimera_graph(2, t=8, coordinates=True) + c23l8 = dnx.chimera_graph(2, 3, t=8) + c32c8 = dnx.chimera_graph(3, 2, t=8, coordinates=True) + + z5l = dnx.zephyr_graph(5) + z5c = dnx.zephyr_graph(5, coordinates=True) + + for target in z5l, z5c: + for source in z2l, z2c, c2l, c2c, c2l8, c2c8, c23l, c32c, c23l8, c32c8, target: + covered = set() + for f in dnx.zephyr_sublattice_mappings(source, target): + check_subgraph_mapping(f, source, target) + covered.update(map(f, source)) + self.assertEqual(covered, set(target)) +