diff --git a/dwave_networkx/__init__.py b/dwave_networkx/__init__.py index f948c483..f18f96a1 100644 --- a/dwave_networkx/__init__.py +++ b/dwave_networkx/__init__.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from dwave_networkx.topology import * + import dwave_networkx.generators from dwave_networkx.generators import * diff --git a/dwave_networkx/drawing/chimera_layout.py b/dwave_networkx/drawing/chimera_layout.py index 89db7f4d..df4d0306 100644 --- a/dwave_networkx/drawing/chimera_layout.py +++ b/dwave_networkx/drawing/chimera_layout.py @@ -21,12 +21,13 @@ from networkx import draw from dwave_networkx.drawing.qubit_layout import draw_qubit_graph, draw_embedding, draw_yield -from dwave_networkx.generators.chimera import chimera_graph, find_chimera_indices, chimera_coordinates - +from dwave_networkx.generators.chimera import find_chimera_indices, chimera_coordinates, defect_free_chimera +from dwave_networkx.topology import CHIMERA __all__ = ['chimera_layout', 'draw_chimera', 'draw_chimera_embedding', 'draw_chimera_yield'] +@CHIMERA.layout.implementation def chimera_layout(G, scale=1., center=None, dim=2): """Positions the nodes of graph ``G`` in a Chimera layout. @@ -71,7 +72,7 @@ def chimera_layout(G, scale=1., center=None, dim=2): # now we get chimera coordinates for the translation # first, check if we made it - if G.graph.get("family") == "chimera": + if G.graph.get("family") == CHIMERA: m = G.graph['rows'] n = G.graph['columns'] t = G.graph['tile'] @@ -189,6 +190,7 @@ def _xy_coords(i, j, u, k): return _xy_coords +@CHIMERA.draw.implementation def draw_chimera(G, **kwargs): """Draws graph ``G`` in a Chimera layout. @@ -230,6 +232,7 @@ def draw_chimera(G, **kwargs): draw_qubit_graph(G, chimera_layout(G), **kwargs) +@CHIMERA.draw_embedding.implementation def draw_chimera_embedding(G, *args, **kwargs): """Draws an embedding onto the Chimera graph ``G``. @@ -282,6 +285,7 @@ def draw_chimera_embedding(G, *args, **kwargs): draw_embedding(G, chimera_layout(G), *args, **kwargs) +@CHIMERA.draw_yield.implementation def draw_chimera_yield(G, **kwargs): """Draws graph ``G`` with highlighted faults. @@ -312,16 +316,5 @@ def draw_chimera_yield(G, **kwargs): the :func:`~networkx.drawing.nx_pylab.draw_networkx` ``node_color`` or ``edge_color`` parameters are ignored. """ - try: - assert(G.graph["family"] == "chimera") - m = G.graph["rows"] - n = G.graph["columns"] - t = G.graph["tile"] - coordinates = G.graph["labels"] == "coordinate" - except: - raise ValueError("Target chimera graph needs to have columns, rows, \ - tile, and label attributes to be able to identify faulty qubits.") - - perfect_graph = chimera_graph(m,n,t, coordinates=coordinates) - + perfect_graph = defect_free_chimera(G) draw_yield(G, chimera_layout(perfect_graph), perfect_graph, **kwargs) diff --git a/dwave_networkx/drawing/pegasus_layout.py b/dwave_networkx/drawing/pegasus_layout.py index 30ac3d16..58997fc2 100644 --- a/dwave_networkx/drawing/pegasus_layout.py +++ b/dwave_networkx/drawing/pegasus_layout.py @@ -20,9 +20,9 @@ import numpy as np from dwave_networkx.drawing.qubit_layout import draw_qubit_graph, draw_embedding, draw_yield -from dwave_networkx.generators.pegasus import pegasus_graph, pegasus_coordinates +from dwave_networkx.generators.pegasus import pegasus_coordinates, defect_free_pegasus from dwave_networkx.drawing.chimera_layout import chimera_node_placer_2d - +from dwave_networkx.topology import PEGASUS __all__ = ['pegasus_layout', 'draw_pegasus', @@ -30,7 +30,7 @@ 'draw_pegasus_yield', ] - +@PEGASUS.layout.implementation def pegasus_layout(G, scale=1., center=None, dim=2, crosses=False): """Positions the nodes of graph ``G`` in a Pegasus topology. @@ -70,7 +70,7 @@ def pegasus_layout(G, scale=1., center=None, dim=2, crosses=False): """ - if not isinstance(G, nx.Graph) or G.graph.get("family") != "pegasus": + if not isinstance(G, nx.Graph) or G.graph.get("family") != PEGASUS: raise ValueError("G must be generated by dwave_networkx.pegasus_graph") if G.graph.get('labels') == 'nice': @@ -188,7 +188,7 @@ def _xy_coords(u, w, k, z): return _xy_coords - +@PEGASUS.draw.implementation def draw_pegasus(G, crosses=False, **kwargs): """Draws graph ``G`` in a Pegasus topology. @@ -236,6 +236,7 @@ def draw_pegasus(G, crosses=False, **kwargs): draw_qubit_graph(G, pegasus_layout(G, crosses=crosses), **kwargs) +@PEGASUS.draw_embedding.implementation def draw_pegasus_embedding(G, *args, **kwargs): """Draws an embedding onto Pegasus graph ``G``. @@ -292,6 +293,7 @@ def draw_pegasus_embedding(G, *args, **kwargs): crosses = kwargs.pop("crosses", False) draw_embedding(G, pegasus_layout(G, crosses=crosses), *args, **kwargs) +@PEGASUS.draw_yield.implementation def draw_pegasus_yield(G, **kwargs): """Draws graph ``G`` with highlighted faults. @@ -322,18 +324,5 @@ def draw_pegasus_yield(G, **kwargs): the :func:`~networkx.drawing.nx_pylab.draw_networkx` ``node_color`` or ``edge_color`` parameters are ignored. """ - try: - assert(G.graph["family"] == "pegasus") - 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, nice_coordinates=nice) - - draw_yield(G, pegasus_layout(perfect_graph), perfect_graph, **kwargs) \ No newline at end of file + perfect_graph = defect_free_pegasus(G) + draw_yield(G, pegasus_layout(perfect_graph), perfect_graph, **kwargs) diff --git a/dwave_networkx/drawing/qubit_layout.py b/dwave_networkx/drawing/qubit_layout.py index cb379b5d..90adb498 100644 --- a/dwave_networkx/drawing/qubit_layout.py +++ b/dwave_networkx/drawing/qubit_layout.py @@ -27,6 +27,7 @@ __all__ = ['draw_qubit_graph'] + def draw_qubit_graph(G, layout, linear_biases=None, quadratic_biases=None, nodelist=None, edgelist=None, midpoint=None, **kwargs): @@ -430,7 +431,6 @@ def draw_yield(G, layout, perfect_graph, unused_color=(0.9, 0.9, 0.9, 1.0), perfect_graph : NetworkX graph The graph to be drawn with highlighted faults - unused_color : tuple or color string (optional, default (0.9,0.9,0.9,1.0)) The color to use for nodes and edges of G which are not faults. If unused_color is None, these nodes and edges will not be shown at all. @@ -487,3 +487,5 @@ def draw_yield(G, layout, perfect_graph, unused_color=(0.9, 0.9, 0.9, 1.0), draw(perfect_graph, layout, nodelist=nodelist, edgelist=edgelist, node_color=unused_node_color, edge_color=unused_edge_color, **kwargs) + + diff --git a/dwave_networkx/drawing/zephyr_layout.py b/dwave_networkx/drawing/zephyr_layout.py index 4ac869d1..4d79cfe8 100644 --- a/dwave_networkx/drawing/zephyr_layout.py +++ b/dwave_networkx/drawing/zephyr_layout.py @@ -21,8 +21,8 @@ import numpy as np from dwave_networkx.drawing.qubit_layout import draw_qubit_graph, draw_embedding, draw_yield -from dwave_networkx.generators.zephyr import zephyr_graph, zephyr_coordinates - +from dwave_networkx.generators.zephyr import zephyr_coordinates, defect_free_zephyr +from dwave_networkx.topology import ZEPHYR __all__ = ['zephyr_layout', 'draw_zephyr', @@ -30,7 +30,7 @@ 'draw_zephyr_yield', ] - +@ZEPHYR.layout.implementation def zephyr_layout(G, scale=1., center=None, dim=2): """Positions the nodes of graph ``G`` in a Zephyr topology. @@ -65,7 +65,7 @@ def zephyr_layout(G, scale=1., center=None, dim=2): """ - if not isinstance(G, nx.Graph) or G.graph.get("family") != "zephyr": + if not isinstance(G, nx.Graph) or G.graph.get("family") != ZEPHYR: raise ValueError("G must be generated by dwave_networkx.zephyr_graph") xy_coords = zephyr_node_placer_2d(G, scale, center, dim) @@ -160,6 +160,7 @@ def _xy_coords(u, w, k, j, z): return _xy_coords +@ZEPHYR.draw.implementation def draw_zephyr(G, **kwargs): """Draws graph ``G`` in a Zephyr topology. @@ -202,6 +203,7 @@ def draw_zephyr(G, **kwargs): draw_qubit_graph(G, zephyr_layout(G), **kwargs) +@ZEPHYR.draw_embedding.implementation def draw_zephyr_embedding(G, *args, **kwargs): """Draws an embedding onto a Zephyr graph ``G``. @@ -252,6 +254,7 @@ def draw_zephyr_embedding(G, *args, **kwargs): """ draw_embedding(G, zephyr_layout(G), *args, **kwargs) +@ZEPHYR.draw_yield.implementation def draw_zephyr_yield(G, **kwargs): """Draws graph ``G`` with highlighted faults, according to the Zephyr layout. @@ -282,16 +285,5 @@ def draw_zephyr_yield(G, **kwargs): the :func:`~networkx.drawing.nx_pylab.draw_networkx` ``node_color`` or ``edge_color`` parameters are ignored. """ - try: - assert(G.graph["family"] == "zephyr") - m = G.graph['columns'] - t = G.graph['tile'] - coordinates = G.graph["labels"] == "coordinate" - except: - raise ValueError("Target zephyr graph needs to have columns, rows, \ - tile, and label attributes to be able to identify faulty qubits.") - - - perfect_graph = zephyr_graph(m, t, coordinates=coordinates) - + perfect_graph = defect_free_zephyr(G) draw_yield(G, zephyr_layout(perfect_graph), perfect_graph, **kwargs) diff --git a/dwave_networkx/generators/chimera.py b/dwave_networkx/generators/chimera.py index 7223051e..97365849 100644 --- a/dwave_networkx/generators/chimera.py +++ b/dwave_networkx/generators/chimera.py @@ -26,7 +26,8 @@ from itertools import product -from .common import _add_compatible_nodes, _add_compatible_edges, _add_compatible_terms +from dwave_networkx.generators.common import _add_compatible_nodes, _add_compatible_edges, _add_compatible_terms +from dwave_networkx.topology import CHIMERA __all__ = ['chimera_graph', 'chimera_coordinates', @@ -39,6 +40,7 @@ ] +@CHIMERA.generator.implementation def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_list=None, data=True, coordinates=False, check_node_list=False, check_edge_list=False): """Creates a Chimera lattice of size (m, n, t). @@ -154,7 +156,7 @@ def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_lis G.name = "chimera_graph(%s, %s, %s)" % (m, n, t) - construction = (("family", "chimera"), ("rows", m), ("columns", n), + construction = (("family", CHIMERA), ("rows", m), ("columns", n), ("tile", t), ("data", data), ("labels", "coordinate" if coordinates else "int")) @@ -252,6 +254,18 @@ def checkadd(v, q): return G +@CHIMERA.defect_free_graph.implementation +def defect_free_chimera(G): + """Construct a defect-free Chimera graph based on the properties of G.""" + attrib = G.graph + family = attrib.get('family') + if family != CHIMERA: + raise ValueError("G must be constructed by dwave_networkx.chimera_graph") + args = attrib['rows'], attrib['columns'], attrib['tile'] + kwargs = {'coordinates': attrib['labels'] == 'coordinate'} + return chimera_graph(*args, **kwargs) + + def find_chimera_indices(G): """Determines the Chimera indices of the nodes in graph ``G``. @@ -331,6 +345,7 @@ def find_chimera_indices(G): raise Exception('not yet implemented for Chimera graphs with more than one tile') +@CHIMERA.coordinates.implementation class chimera_coordinates(object): """Provides coordinate converters for the chimera indexing scheme. @@ -681,7 +696,7 @@ def mapping(q): return mapping - +@CHIMERA.sublattice_mappings.implementation def chimera_sublattice_mappings(source, target, offset_list=None): r"""Yields mappings from a Chimera graph into a larger Chimera graph. @@ -738,7 +753,7 @@ def chimera_sublattice_mappings(source, target, offset_list=None): into account, this function does not handle that complex task. """ - if not (source.graph.get('family') == target.graph.get('family') == 'chimera'): + 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'] @@ -775,6 +790,8 @@ def chimera_to_target(q): for offset in offset_list: yield _chimera_sublattice_mapping(source_to_chimera, chimera_to_target, offset) + +@CHIMERA.torus_generator.implementation def chimera_torus(m, n=None, t=None, node_list=None, edge_list=None): """Creates a defect-free Chimera lattice of size :math:`(m, n, t)` subject to periodic boundary conditions. diff --git a/dwave_networkx/generators/common.py b/dwave_networkx/generators/common.py index 5855a575..79ada662 100644 --- a/dwave_networkx/generators/common.py +++ b/dwave_networkx/generators/common.py @@ -1,3 +1,17 @@ +# Copyright 2022 D-Wave +# +# 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 +# +# http://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. + def _add_compatible_edges(G, edge_list): # Check edge_list defines a subgraph of G and create subgraph. diff --git a/dwave_networkx/generators/pegasus.py b/dwave_networkx/generators/pegasus.py index f97d7b13..e2b718c9 100644 --- a/dwave_networkx/generators/pegasus.py +++ b/dwave_networkx/generators/pegasus.py @@ -23,8 +23,9 @@ import warnings from itertools import product -from .chimera import _chimera_coordinates_cache -from .common import _add_compatible_edges, _add_compatible_nodes, _add_compatible_terms +from dwave_networkx.generators.chimera import _chimera_coordinates_cache +from dwave_networkx.generators.common import _add_compatible_edges, _add_compatible_nodes, _add_compatible_terms +from dwave_networkx.topology import CHIMERA, PEGASUS __all__ = ['pegasus_graph', 'pegasus_coordinates', @@ -33,6 +34,7 @@ 'pegasus_four_color', ] +@PEGASUS.generator.implementation def pegasus_graph(m, create_using=None, node_list=None, edge_list=None, data=True, offset_lists=None, offsets_index=None, coordinates=False, fabric_only=True, nice_coordinates=False, check_node_list=False, check_edge_list=False): @@ -229,7 +231,7 @@ def pegasus_graph(m, create_using=None, node_list=None, edge_list=None, data=Tru def label(u, w, k, z): return u * 12 * m * m1 + w * 12 * m1 + k * m1 + z - construction = (("family", "pegasus"), ("rows", m), ("columns", m), + construction = (("family", PEGASUS), ("rows", m), ("columns", m), ("tile", 12), ("vertical_offsets", offset_lists[0]), ("horizontal_offsets", offset_lists[1]), ("data", data), ("labels", labels)) @@ -328,6 +330,25 @@ def fill_data(): return G +@PEGASUS.defect_free_graph.implementation +def defect_free_pegasus(G): + """Construct a defect-free Pegasus graph based on the properties of G.""" + attrib = G.graph + family = attrib.get('family') + if family != PEGASUS: + raise ValueError("G must be constructed by dwave_networkx.pegasus_graph") + + offsets = eval(G.name.replace('pegasus_graph', ''))[1] + args = attrib['rows'], + kwargs = { + 'offsets_index' if isinstance(offsets, int) else 'offset_lists': offsets, + 'coordinates': 'coordinate' == attrib['labels'], + 'nice_coordinates': 'nice' == attrib['labels'], + } + + return pegasus_graph(*args, **kwargs) + + def get_tuple_fragmentation_fn(pegasus_graph): """ Returns a fragmentation function that is specific to pegasus_graph. This fragmentation function, @@ -543,6 +564,7 @@ def fragmented_edges(pegasus_graph): # 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. +@PEGASUS.coordinates.implementation class pegasus_coordinates(object): """Provides coordinate converters for the Pegasus indexing schemes. @@ -1091,6 +1113,7 @@ def mapping(q): return mapping +@PEGASUS.sublattice_mappings.implementation def pegasus_sublattice_mappings(source, target, offset_list=None): r"""Yields mappings from a Chimera or Pegasus graph into a Pegasus graph. @@ -1155,7 +1178,7 @@ def pegasus_sublattice_mappings(source, target, offset_list=None): would take those isomorphisms into account, this function does not handle that complex task. """ - if target.graph.get('family') != 'pegasus': + 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'] @@ -1171,7 +1194,7 @@ def nice_to_target(q): raise ValueError(f"Pegasus node labeling {labels_t} not recognized") labels_s = source.graph['labels'] - if source.graph.get('family') == 'chimera': + 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") @@ -1189,7 +1212,7 @@ def source_to_inner(q): make_mapping = _chimera_pegasus_sublattice_mapping - elif source.graph.get('family') == 'pegasus': + 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) @@ -1215,6 +1238,7 @@ def source_to_inner(q): yield make_mapping(source_to_inner, nice_to_target, offset) +@PEGASUS.torus_generator.implementation def pegasus_torus(m, node_list=None, edge_list=None, offset_lists=None, offsets_index=None): """ diff --git a/dwave_networkx/generators/zephyr.py b/dwave_networkx/generators/zephyr.py index 5b039be1..8691c4eb 100644 --- a/dwave_networkx/generators/zephyr.py +++ b/dwave_networkx/generators/zephyr.py @@ -23,9 +23,11 @@ from dwave_networkx.exceptions import DWaveNetworkXException -from .chimera import _chimera_coordinates_cache +from dwave_networkx.generators.chimera import _chimera_coordinates_cache + +from dwave_networkx.generators.common import _add_compatible_edges, _add_compatible_nodes, _add_compatible_terms +from dwave_networkx.topology import CHIMERA, ZEPHYR -from .common import _add_compatible_edges, _add_compatible_nodes, _add_compatible_terms __all__ = ['zephyr_graph', 'zephyr_coordinates', @@ -34,6 +36,7 @@ 'zephyr_four_color', ] +@ZEPHYR.generator.implementation def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, data=True, coordinates=False, check_node_list=False, check_edge_list=False): @@ -180,7 +183,7 @@ def label(*q): def label(u, w, k, j, z): return (((u * M + w) * t + k) * 2 + j) * m + z - construction = (("family", "zephyr"), ("rows", m), ("columns", m), + construction = (("family", ZEPHYR), ("rows", m), ("columns", m), ("tile", t), ("data", data), ("labels", labels)) G.graph.update(construction) @@ -254,9 +257,22 @@ def fill_data(): return G +@ZEPHYR.defect_free_graph.implementation +def defect_free_zephyr(G): + """Construct a defect-free Zephyr graph based on the properties of G.""" + attrib = G.graph + family = attrib.get('family') + if family != ZEPHYR: + raise ValueError("G must be constructed by dwave_networkx.zephyr_graph") + args = attrib['rows'], attrib['tile'] + kwargs = {'coordinates': attrib['labels'] == 'coordinate'} + return zephyr_graph(*args, **kwargs) + + # 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. +@ZEPHYR.coordinates.implementation class zephyr_coordinates(object): """Provides coordinate converters for the Zephyr indexing schemes. @@ -582,7 +598,7 @@ def mapping(q): return mapping - +@ZEPHYR.sublattice_mappings.implementation def zephyr_sublattice_mappings(source, target, offset_list=None): r"""Yields mappings from a Chimera or Zephyr graph into a Zephyr graph. @@ -646,7 +662,7 @@ def zephyr_sublattice_mappings(source, target, offset_list=None): of sublattice mappings would take those isomorphisms into account, this function does not handle that complex task. """ - if target.graph.get('family') != 'zephyr': + 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'] @@ -661,7 +677,7 @@ def zephyr_to_target(q): raise ValueError(f"Zephyr node labeling {labels_t} not recognized") labels_s = source.graph['labels'] - if source.graph.get('family') == 'chimera': + if source.graph.get('family') == CHIMERA: t_t = source.graph['tile'] m_s = source.graph['rows'] n_s = source.graph['columns'] @@ -691,7 +707,7 @@ def source_to_inner(q): else: raise ValueError(f"Chimera node labeling {labels_s} not recognized") - elif source.graph.get('family') == 'zephyr': + 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) @@ -714,6 +730,8 @@ def source_to_inner(q): for offset in offset_list: yield make_mapping(source_to_inner, zephyr_to_target, offset) + +@ZEPHYR.torus_generator.implementation def zephyr_torus(m, t=4, node_list=None, edge_list=None): """ Creates a Zephyr graph modified to allow for periodic boundary conditions and translational invariance. diff --git a/dwave_networkx/topology.py b/dwave_networkx/topology.py new file mode 100644 index 00000000..950c71dd --- /dev/null +++ b/dwave_networkx/topology.py @@ -0,0 +1,105 @@ +# Copyright 2024 D-Wave +# +# 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 +# +# http://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 enum import Enum as _Enum, auto as _auto + +try: + from enum import global_enum as _global_enum +except ImportError: + # delete after Py3.10 EOL (2026/10) + def _global_enum(cls): + """This function is a specialized version of global_enum defined in py3.11 + + We've omitted the functionality we aren't using, and kept the two pieces + that are important. + + 1. The members of an enum should be exported to the module containing + them: `topology.ZEPHYR` is `topology.Topology.ZEPHYR` + 2. When `repr` is called on an enum member, it should look like + 'topology.ZEPHYR' rather than '' + """ + import sys + + toplevel = cls.__module__.split(".")[-1] + cls.__repr__ = lambda self: f"{toplevel}.{self._name_}" + # the following two lines are quoted directly from CPython. It's hard + # to imagine another way of writing this; hopefully this constitutes + # fair use. + sys.modules[cls.__module__].__dict__.update(cls.__members__) + return cls + + +from dwave_networkx.utils.decorators import ImplementationHook as _ImplementationHook + + +class TopologyFamily: + __slots__ = [ + "coordinates", + "defect_free_graph", + "draw", + "draw_embedding", + "draw_yield", + "generator", + "layout", + "sublattice_mappings", + "torus_generator", + ] + + # Let's act like a string when we need to: str(), equality, and hash + def __eq__(self, other): + return other == self.value + + def __str__(self): + return self.value.lower() + + def __hash__(self): + return hash(self.value) + + def __init__(self, name): + # DEV NOTE + # + # Throughout the codebase, we use the pattern + # + # @ZEPHYR.draw_embedding.implementation + # def draw_zephyr_embedding(...): + # ... + # + # This is accomplished by using an ImplementationHook object. + for slot in self.__slots__: + setattr(self, slot, _ImplementationHook(self, slot)) + + +class TopologyEnum(TopologyFamily, _Enum): + @staticmethod + def _generate_next_value_(name, *_): + # make auto() work like in StrEnum + return name.lower() + + +@_global_enum +class Topology(TopologyEnum): + """An enumeration of qubit topologies supported by dwave_networkx. + + Each member of this enumeration functions as a string for the purpose of + equality, hashing, and printing; and also contains a variety of utility + functions specific to that topology. + """ + + CHIMERA = _auto() + PEGASUS = _auto() + ZEPHYR = _auto() + + +__all__ = ["Topology", *Topology.__members__] diff --git a/dwave_networkx/utils/decorators.py b/dwave_networkx/utils/decorators.py index 48e65450..378849fb 100644 --- a/dwave_networkx/utils/decorators.py +++ b/dwave_networkx/utils/decorators.py @@ -22,7 +22,7 @@ import dwave_networkx as dnx -__all__ = ['binary_quadratic_model_sampler'] +__all__ = ['binary_quadratic_model_sampler', 'ImplementationHook'] def binary_quadratic_model_sampler(which_args): @@ -74,3 +74,47 @@ def func(*args, **kwargs): return f(*new_args, **kw) return func return decorator + + +class ImplementationHook: + """A decorator class to provide a temporary slot value which overwrites itself + + This is used by the ``dwave_networkx.TopologyFamily`` enum. Each family + object holds references to specific implementations of generic functions. + But to avoid circular dependencies, we first install this hook when the + family object is constructed. Then, when each generic function is + implemented, that implementation gets decorated with the ``implementation`` + method of the respective ``ImplementationHook`` object. When the decoration + occurs, the hook replaces itself with the decorated function. The decorated + function itself is left unchanged. + + For example, in ``dwave_networkx.drawing.chimera_layout``, we implement the + function ``draw_chimera_embedding``. We also want to make an alias of that + named ``dwave_networkx.CHIMERA.draw_embedding``. So, in the construction + of ``dwave_networkx.CHIMERA`` we have put an ``ImplementationHook`` in place + of ``draw_embedding``. Then, when we implement ``draw_chimera_embedding``, + we write + + @CHIMERA.draw_embedding.implementation + def draw_chimera_embedding(...) + ... + + which provides the desired alias. + + """ + + def __init__(self, obj, name): + self.obj = obj + self.name = name + + def implementation(self, f): + # monkeypatch the object with this implementation + setattr(self.obj, self.name, f) + + # don't forget to return f, or the decorated function will be None + return f + + def __call__(self, *args, **kwargs): + raise NotImplementedError( + f"the {self.name} method of {self.obj!r} has not been attached" + ) diff --git a/tests/common.py b/tests/common.py new file mode 100644 index 00000000..adb67e2d --- /dev/null +++ b/tests/common.py @@ -0,0 +1,47 @@ +# Copyright 2025 D-Wave +# +# 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 +# +# http://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. + + +class NullError(BaseException): + def __init__(self, *args): + raise RuntimeError("do not use this exception class") + + +class GraphTesting: + def assertGraphsEqual(self, G, H, msg=None): + try: + self.assertIsInstance( + G, type(H), "graphs G and H should have the same type" + ) + self.assertEqual( + set(G.nodes), + set(H.nodes), + "graphs G and H should have the same node set", + ) + self.assertEqual( + len(G.edges), + len(H.edges), + "graphs G and H should have the same number of edges", + ) + for e in G.edges: + self.assertTrue( + H.has_edge(*e), "graphs G and H have different edge sets" + ) + except NullError if msg is None else Exception as e: + # Sorry for the weirdness here. We don't want to create strings on + # the happy path, so we define a NullError that is never raised so + # exceptions fall straight through in that case. + + sub_msg, *_ = e.args + self.fail(f"{msg} : {sub_msg}") diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 81e771a2..7fcb9f0f 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -15,7 +15,10 @@ import unittest import dwave_networkx as dnx -from dwave_networkx.utils import binary_quadratic_model_sampler +from dwave_networkx.utils.decorators import ( + binary_quadratic_model_sampler, + ImplementationHook, +) class MockSampler: @@ -31,7 +34,7 @@ def mock_function(G, sampler=None, **sampler_args): assert sampler is not None -class TestDecorators(unittest.TestCase): +class TestBQMSampler(unittest.TestCase): def test_default_set(self): dnx.set_default_sampler(MockSampler()) @@ -47,3 +50,26 @@ def test_no_sampler_set(self): def test_sampler_provided(self): mock_function(0, MockSampler()) + + +class HooksNeeded: + pass + + +hooks_needed = HooksNeeded() +hooks_needed.implemented = ImplementationHook(hooks_needed, "implemented") +hooks_needed.not_implemented = ImplementationHook(hooks_needed, "not_implemented") + + +@hooks_needed.implemented.implementation +def implemented(): + pass + + +class TestImplementationHook(unittest.TestCase): + def test_implemented_hook(self): + hooks_needed.implemented() + + def test_not_implemented_hook(self): + with self.assertRaises(NotImplementedError): + hooks_needed.not_implemented() diff --git a/tests/test_generator_chimera.py b/tests/test_generator_chimera.py index 4317ba7c..6c5887d1 100644 --- a/tests/test_generator_chimera.py +++ b/tests/test_generator_chimera.py @@ -17,11 +17,12 @@ import networkx as nx import dwave_networkx as dnx import numpy as np +from .common import GraphTesting alpha_map = dict(enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')) -class TestChimeraGraph(unittest.TestCase): +class TestChimeraGraph(unittest.TestCase, GraphTesting): def test_single_tile(self): # fully specified @@ -229,24 +230,15 @@ def test_nonsquare_coordinate_generator(self): 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)) + self.assertGraphsEqual(c3l, coords.graph_to_linear(c3c)) + self.assertGraphsEqual(c3l, coords.graph_to_linear(c3l)) + self.assertGraphsEqual(c3c, coords.graph_to_chimera(c3l)) + self.assertGraphsEqual(c3c, coords.graph_to_chimera(c3c)) h = dnx.chimera_graph(2) del h.graph['labels'] @@ -370,7 +362,21 @@ def test_edge_list(self): edge_list = [(0, t), (0, t)] G = dnx.chimera_graph(m, edge_list=edge_list, check_edge_list=True) - + + def test_defect_free_chimera(self): + G = dnx.chimera_graph(2, 4, 2) + H = G.copy() + H.remove_nodes_from([*H][::3]) + H.remove_edges_from([*H.edges][::3]) + self.assertGraphsEqual(G, dnx.generators.chimera.defect_free_chimera(H)) + + G = dnx.chimera_graph(2, 4, 2, coordinates=True) + H = G.copy() + H.remove_nodes_from([*H][::3]) + H.remove_edges_from([*H.edges][::3]) + self.assertGraphsEqual(G, dnx.generators.chimera.defect_free_chimera(H)) + + class TestChimeraTorus(unittest.TestCase): def test(self): for m in range(1,4): diff --git a/tests/test_generator_pegasus.py b/tests/test_generator_pegasus.py index 698d2df5..b7131909 100644 --- a/tests/test_generator_pegasus.py +++ b/tests/test_generator_pegasus.py @@ -27,10 +27,12 @@ get_tuple_fragmentation_fn, ) +from .common import GraphTesting + alpha_map = dict(enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')) -class TestPegasusGraph(unittest.TestCase): +class TestPegasusGraph(unittest.TestCase, GraphTesting): def test_p2(self): G = dnx.pegasus_graph(2, fabric_only=False) @@ -71,7 +73,7 @@ def test(self): g.remove_nodes_from(g_translated.nodes()) self.assertEqual(g.number_of_nodes(),0) -class TestPegasusCoordinates(unittest.TestCase): +class TestPegasusCoordinates(unittest.TestCase, GraphTesting): def test_connected_component(self): test_offsets = [[0] * 12] * 2, [[2] * 12, [6] * 12], [[6] * 12, [2, 2, 6, 6, 10, 10] * 2], [[2, 2, 6, 6, 10, 10] * 2] * 2 @@ -225,15 +227,6 @@ def test_coordinate_subgraphs(self): 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)) @@ -244,17 +237,17 @@ def graph_equal(g, h): 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)) + self.assertGraphsEqual(p3l, coords.graph_to_linear(p3l)) + self.assertGraphsEqual(p3l, coords.graph_to_linear(p3p)) + self.assertGraphsEqual(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)) + self.assertGraphsEqual(p3p, coords.graph_to_pegasus(p3l)) + self.assertGraphsEqual(p3p, coords.graph_to_pegasus(p3p)) + self.assertGraphsEqual(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)) + self.assertGraphsEqual(p3n, coords.graph_to_nice(p3l)) + self.assertGraphsEqual(p3n, coords.graph_to_nice(p3p)) + self.assertGraphsEqual(p3n, coords.graph_to_nice(p3n)) h = dnx.pegasus_graph(2) del h.graph['labels'] @@ -402,6 +395,30 @@ def test_edge_list(self): G = dnx.pegasus_graph(m, edge_list=edge_list, fabric_only=False, check_edge_list=True) + def test_defect_free_pegasus(self): + G = dnx.pegasus_graph(3, offset_lists=[(10,)*12, (6,)*12]) + H = G.copy() + H.remove_nodes_from([*H][::3]) + H.remove_edges_from([*H.edges][::3]) + self.assertGraphsEqual(G, dnx.generators.pegasus.defect_free_pegasus(H)) + + G = dnx.pegasus_graph(3, offsets_index=1, coordinates=True) + H = G.copy() + H.remove_nodes_from([*H][::3]) + H.remove_edges_from([*H.edges][::3]) + self.assertGraphsEqual(G, dnx.generators.pegasus.defect_free_pegasus(H)) + + + G = dnx.pegasus_graph(3, nice_coordinates=True) + H = G.copy() + H.remove_nodes_from([*H][::3]) + H.remove_edges_from([*H.edges][::3]) + self.assertGraphsEqual(G, dnx.generators.pegasus.defect_free_pegasus(H)) + + + + + class TestTupleFragmentation(unittest.TestCase): def test_empty_list(self): diff --git a/tests/test_generator_zephyr.py b/tests/test_generator_zephyr.py index 246f453f..e63b2b05 100644 --- a/tests/test_generator_zephyr.py +++ b/tests/test_generator_zephyr.py @@ -17,8 +17,9 @@ import networkx as nx import dwave_networkx as dnx import numpy as np +from .common import GraphTesting -class TestZephyrGraph(unittest.TestCase): +class TestZephyrGraph(unittest.TestCase, GraphTesting): def test_single_tile(self): # fully specified @@ -130,25 +131,16 @@ def test_coordinate_subgraphs(self): 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)) + self.assertGraphsEqual(z3l, coords.graph_to_linear(z3l)) + self.assertGraphsEqual(z3l, coords.graph_to_linear(z3c)) - graph_equal(z3c, coords.graph_to_zephyr(z3c)) - graph_equal(z3c, coords.graph_to_zephyr(z3l)) + self.assertGraphsEqual(z3c, coords.graph_to_zephyr(z3c)) + self.assertGraphsEqual(z3c, coords.graph_to_zephyr(z3l)) h = dnx.zephyr_graph(2) del h.graph['labels'] @@ -274,7 +266,20 @@ def test_edge_list(self): G = dnx.zephyr_graph(m, t, edge_list=edge_list, check_edge_list=True) - + def test_defect_free_zephyr(self): + G = dnx.zephyr_graph(2, 4) + H = G.copy() + H.remove_nodes_from([*H][::3]) + H.remove_edges_from([*H.edges][::3]) + self.assertGraphsEqual(G, dnx.generators.zephyr.defect_free_zephyr(H)) + + G = dnx.zephyr_graph(2, 2, coordinates=True) + H = G.copy() + H.remove_nodes_from([*H][::3]) + H.remove_edges_from([*H.edges][::3]) + self.assertGraphsEqual(G, dnx.generators.zephyr.defect_free_zephyr(H)) + + class TestZephyrTorus(unittest.TestCase): def test(self): for m in [2,3,4]: