diff --git a/docs/bibliography.rst b/docs/bibliography.rst index 9f7b4f24..c2eab615 100644 --- a/docs/bibliography.rst +++ b/docs/bibliography.rst @@ -1,12 +1,18 @@ Bibliography ============ -.. [NX] Aric A. Hagberg, Daniel A. Schult and Pieter J. Swart, “Exploring network structure, dynamics, and function using NetworkX”, in Proceedings of the 7th Python in Science Conference (SciPy2008), Gäel Varoquaux, Travis Vaught, and Jarrod Millman (Eds), (Pasadena, CA USA), pp. 11–15, Aug 2008 +.. [NX] A. A. Hagberg, D. A. Schult and P. J. Swart, “Exploring network structure, dynamics, and function using NetworkX”, in Proceedings of the 7th Python in Science Conference (SciPy2008), Gäel Varoquaux, Travis Vaught, and Jarrod Millman (Eds), (Pasadena, CA USA), pp. 11–15, Aug 2008 -.. [GD] Gogate & Dechter, "A Complete Anytime Algorithm for Treewidth", https://arxiv.org/abs/1207.4109 +.. [GD] V. Gogate and R. Dechter, "A Complete Anytime Algorithm for Treewidth", https://arxiv.org/abs/1207.4109 -.. [AL] Lucas, A. (2014). Ising formulations of many NP problems. Frontiers in Physics, Volume 2, Article 5. +.. [AL] A. Lucas (2014). Ising formulations of many NP problems. Frontiers in Physics, Volume 2, Article 5. -.. [FIA] Facchetti, G., Iacono G., and Altafini C. (2011). Computing global structural balance in large-scale signed social networks. PNAS, 108, no. 52, 20953-20958 +.. [FIA] G. Facchetti, G. Iacono and C. Altafini (2011). Computing global structural balance in large-scale signed social networks. PNAS, 108, no. 52, 20953-20958 -.. [DWMP] Dahl, E., "Programming the D-Wave: Map Coloring Problem", https://www.dwavesys.com/media/htfgw5bk/map-coloring-wp2.pdf +.. [DWMP] E. Dahl, "Programming the D-Wave: Map Coloring Problem", https://www.dwavesys.com/media/htfgw5bk/map-coloring-wp2.pdf + +.. [BBRR] K. Boothby, P. Bunyk, J. Raymond and A. Roy (2019). Next-Generation Topology of D-Wave Quantum Processors. https://arxiv.org/abs/2003.00133 + +.. [BRK] K. Boothby, J. Raymond and A. D. King (2021). Zephyr Topology of D-Wave Quantum Processors. https://dwavesys.com/media/fawfas04/14-1056a-a_zephyr_topology_of_d-wave_quantum_processors.pdf + +.. [RH] J. Raymond, R. Stevanovic, W. Bernoudy, K. Boothby, C. C. McGeoch, A. J. Berkley, P. Farré and A. D. King (2021). Hybrid quantum annealing for larger-than-QPU lattice-structured problems. https://arxiv.org/abs/2202.03044 diff --git a/docs/reference/generators.rst b/docs/reference/generators.rst index e3e9818c..9be9df2b 100644 --- a/docs/reference/generators.rst +++ b/docs/reference/generators.rst @@ -17,6 +17,8 @@ D-Wave Systems pegasus_graph zephyr_graph + + Example ~~~~~~~ @@ -48,6 +50,16 @@ the `find_chimera()` function to determine the Chimera indices. Indices of a Chimera unit cell found by creating a lattice of size (1, 1, 4). +Toruses +------- + +.. autosummary:: + :toctree: generated/ + + chimera_torus + pegasus_torus + zephyr_torus + Other Graphs ------------ diff --git a/dwave_networkx/algorithms/coloring.py b/dwave_networkx/algorithms/coloring.py index 1bf73b4c..18584c11 100644 --- a/dwave_networkx/algorithms/coloring.py +++ b/dwave_networkx/algorithms/coloring.py @@ -171,7 +171,8 @@ def _chromatic_number_upper_bound(G): # be the largest eigenvalue of A. Then chi <= theta_1 + 1 with # equality iff G is complete or an odd cycle. # this is strictly better than brooks theorem - bound = math.ceil(max(np.linalg.eigvals(nx.to_numpy_array(G)))) + # G is real symmetric, use eigvalsh for real valued output. + bound = math.ceil(max(np.linalg.eigvalsh(nx.to_numpy_array(G)))) else: # we know it's connected bound = n_nodes diff --git a/dwave_networkx/algorithms/elimination_ordering.py b/dwave_networkx/algorithms/elimination_ordering.py index 625758ba..9c07182a 100644 --- a/dwave_networkx/algorithms/elimination_ordering.py +++ b/dwave_networkx/algorithms/elimination_ordering.py @@ -497,9 +497,7 @@ def treewidth_branch_and_bound(G, elimination_order=None, treewidth_upperbound=N References ---------- - .. [GD] Gogate & Dechter, "A Complete Anytime Algorithm for Treewidth", - https://arxiv.org/abs/1207.4109 - + Based on the algorithm presented in [GD]_ """ # empty graphs have treewidth 0 and the nodes can be eliminated in # any order @@ -875,7 +873,7 @@ def pegasus_elimination_order(n, coordinates=False): """Provides a variable elimination order for the Pegasus graph. The treewidth of a Pegasus graph ``pegasus_graph(n)`` is lower-bounded by - :math:`12n-11` and upper bounded by :math:`12n-4` [bbrr]_ . + :math:`12n-11` and upper bounded by :math:`12n-4` [BBRR]_ . Simple pegasus variable elimination order rules: @@ -897,11 +895,6 @@ def pegasus_elimination_order(n, coordinates=False): order : list An elimination order that provides an upper bound on the treewidth. - - .. [bbrr] Boothby, K., P. Bunky, J. Raymond, A. Roy. Next-Generation Topology - of D-Wave Quantum Processors. Technical Report, Februrary 2019. - https://www.dwavesys.com/resources/publications?type=white - """ m = n l = 12 @@ -932,7 +925,7 @@ def zephyr_elimination_order(m, t=4, coordinates=False): """Provides a variable elimination order for the zephyr graph. The treewidth of a Zephyr graph ``zephyr_graph(m,t)`` is upper-bounded by - :math:`4tm+2t` and lower-bounded by :math:`4tm` [brk]_ . + :math:`4tm+2t` and lower-bounded by :math:`4tm` [BRK]_ . Simple zephyr variable elimination rules: - eliminate vertical qubits, one column at a time @@ -953,13 +946,6 @@ def zephyr_elimination_order(m, t=4, coordinates=False): order : list An elimination order that achieves an upper bound on the treewidth. - - References - ---------- - .. [brk] Boothby, Raymond, King, Zephyr Topology of D-Wave Quantum - Processors, October 2021. - https://dwavesys.com/media/fawfas04/14-1056a-a_zephyr_topology_of_d-wave_quantum_processors.pdf - """ order = ([(0,w,k,j,z) for w in range(2*m+1) for k in range(t) for z in range(m) for j in range(2)] + [(1,w,k,j,z) for z in range(m) for j in range(2) for w in range(2*m+1) for k in range(t)]) diff --git a/dwave_networkx/generators/chimera.py b/dwave_networkx/generators/chimera.py index 950faa0f..61fcb876 100644 --- a/dwave_networkx/generators/chimera.py +++ b/dwave_networkx/generators/chimera.py @@ -26,7 +26,7 @@ from itertools import product -from .common import _add_compatible_edges +from .common import _add_compatible_nodes, _add_compatible_edges, _add_compatible_terms __all__ = ['chimera_graph', 'chimera_coordinates', @@ -34,6 +34,7 @@ 'chimera_to_linear', 'linear_to_chimera', 'chimera_sublattice_mappings', + 'chimera_torus', ] @@ -78,7 +79,8 @@ def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_lis the graph topology and node labeling conventions, and an error is thrown if any node is incompatible or duplicates exist. In other words, the ``node_list`` must specify a subgraph of the - full-yield graph described below. + full-yield graph described below. An exception is allowed if + ``check_edge_list=False``, in which case any node in ``edge_list`` is treated as valid. check_edge_list : bool (optional, default :code:`False`) If :code:`True`, the ``edge_list`` elements are checked for compatibility with the graph topology and node labeling conventions, an error is thrown @@ -95,14 +97,14 @@ def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_lis A Chimera lattice is an m-by-n grid of Chimera tiles. Each Chimera tile is itself a bipartite graph with shores of size t. The connection in a Chimera lattice can be expressed using a node-indexing - notation (i,j,u,k) for each node. + notation (i, j, u, k) for each node. - * (i,j) indexes the (row, column) of the Chimera tile. i must be - between 0 and m-1, inclusive, and j must be between 0 and - n-1, inclusive. + * (i, j) indexes the (row, column) of the Chimera tile. i must be + between 0 and m - 1, inclusive, and j must be between 0 and + n - 1, inclusive. * u=0 indicates the left-hand nodes in the tile, and u=1 indicates the right-hand nodes. - * k=0,1,...,t-1 indexes nodes within either the left- or + * k=0, 1, ..., t - 1 indexes nodes within either the left- or right-hand shores of a tile. In this notation, two nodes (i, j, u, k) and (i', j', u', k') are @@ -159,6 +161,11 @@ def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_lis max_size = m * n * 2 * t # max number of nodes G can have + if edge_list is None: + check_edge_list = False + if node_list is None: + check_node_list = False + if edge_list is None or check_edge_list is True: if coordinates: # tile edges @@ -169,13 +176,13 @@ def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_lis for k1 in range(t)) # horizontal edges - G.add_edges_from(((i, j, 1, k), (i, j+1, 1, k)) + G.add_edges_from(((i, j, 1, k), (i, j + 1, 1, k)) for i in range(m) - for j in range(n-1) + for j in range(n - 1) for k in range(t)) # vertical edges - G.add_edges_from(((i, j, 0, k), (i+1, j, 0, k)) - for i in range(m-1) + G.add_edges_from(((i, j, 0, k), (i + 1, j, 0, k)) + for i in range(m - 1) for j in range(n) for k in range(t)) else: @@ -204,20 +211,25 @@ def chimera_graph(m, n=None, t=None, create_using=None, node_list=None, edge_lis _add_compatible_edges(G, edge_list) else: + if check_node_list or node_list is None: + if coordinates: + G.add_nodes_from((i, j, u, k) for i in range(m) + for j in range(n) + for u in range(2) + for k in range(t)) + else: + G.add_nodes_from(i for i in range(m*n*t*2)) + G.add_edges_from(edge_list) if node_list is not None: - nodes = set(node_list) - G.remove_nodes_from(set(G) - nodes) if check_node_list: - if G.number_of_nodes() != len(node_list): - raise ValueError("node_list contains nodes incompatible with " - "the specified topology and node-labeling " - "convention.") - + _add_compatible_nodes(G, node_list) else: + nodes = set(node_list) + G.remove_nodes_from(set(G) - nodes) G.add_nodes_from(nodes) # for singleton nodes - + if data: if coordinates: def checkadd(v, q): @@ -634,7 +646,7 @@ 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 + # store the offset in the mapping, so the user can reconstruct it mapping.offset = offset return mapping @@ -656,7 +668,7 @@ def chimera_sublattice_mappings(source, target, offset_list=None): 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)`` + ``(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 @@ -726,3 +738,94 @@ def chimera_to_target(q): for offset in offset_list: yield _chimera_sublattice_mapping(source_to_chimera, chimera_to_target, offset) +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. + + + Parameters + ---------- + m : int + Number of rows in the Chimera torus lattice. + If :math:`m<3` translational invariance already applies in the rows. If + :math:`m>=3` additional external couplers are added, reestablishing + translational invariance. + Connectivity of all horizontal qubits is :math:`min(m - 1, 2) + 2t`. + n : int (optional, default m) + Number of columns in the Chimera torus lattice. + If :math:`n<3` translational invariance already applies in the columns. If + :math:`n>=3` additional external couplers are added, reestablishing + translational invariance. + Connectivity of all vertical qubits is :math:`min(n - 1, 2) + 2t`. + t : int (optional, default 4) + Size of the shore within each Chimera tile. + node_list : iterable (optional, default None) + Iterable of nodes in the graph. If None, nodes are generated + for an undiluted torus calculated from ``m``, ``n`` and ``t`` + as described below. The node list must describe a subset + of the torus nodes to be maintained in the graph + using the coordinate node labeling scheme. + edge_list : iterable (optional, default None) + Iterable of edges in the graph. If None, edges are generated + for an undiluted torus calculated from ``m``, ``n`` and ``t`` + as described below. The edge list must describe + a subgraph of the torus, using the coordinate node labeling scheme. + + Returns + ------- + G : NetworkX Graph + A Chimera torus with shape (m, n, t), with Chimera coordinate node labels. + + + A Chimera torus is a generalization of the standard chimera graph + whereby degree-six connectivity is maintained, but the boundary + condition is modified to enforce an additional translational-invariance + symmetry [RH]_. Local connectivity in the Chimera torus + is identical to connectivity for chimera graph nodes away from the boundary. + The graph has :code:`V=8*m*n` nodes, and :code:`min(6, 4 + m)V//2 + + min(6, 4 + n)V/2` edges. With the standard :math:`K_{t, t}` Chimera tile definition, + any tile displacement :math:`(x, y)` modulo :math:`(m, n)`, rows and columns respectively, + that is, :code:`(i, j, u, k)` -> :code:`((i + x)%m, (i + y)%n, u, k)`, + defines an automorphism. + + See :func:`.chimera_graph` for additional information. + + Examples + ======== + >>> G = dnx.chimera_torus(3, 3, 4) # a 3x3 tile chimera graph (connectivity 6) + >>> len(G) + 72 + >>> any([len(list(G.neighbors(n))) != 6 for n in G.nodes]) + False + + """ + # Graph properties are by and large inherited from chimera_graph + G = chimera_graph(m=m, n=n, t=t, node_list=None, edge_list=None, data=True, coordinates=True) + if n is None: + n = G.graph['columns'] + if t is None: + t = G.graph['tile'] + + + # With modification of the boundary condition + if m>2: + # Wrapped around row external-coupler edges: + additional_edges = [((m - 1, j, 0, k), (0, j, 0, k)) + for j in range(n) + for k in range(t)] + else: + additional_edges = [] + + if n>2: + # Wrapped around columns external-coupler edges: + additional_edges += [((i, n - 1, 1, k), (i, 0, 1, k)) + for i in range(m) + for k in range(t)] + + if len(additional_edges)>0: + G.add_edges_from(additional_edges) + + _add_compatible_terms(G, node_list, edge_list) + + G.graph['boundary_condition'] = 'torus' + + return G diff --git a/dwave_networkx/generators/common.py b/dwave_networkx/generators/common.py index 1e9209f2..5855a575 100644 --- a/dwave_networkx/generators/common.py +++ b/dwave_networkx/generators/common.py @@ -4,10 +4,26 @@ def _add_compatible_edges(G, edge_list): # Slow when edge_list is large, but clear (non-defaulted behaviour, so fine): if edge_list is not None: if not all(G.has_edge(*e) for e in edge_list): - raise ValueError("edge_list contains edges incompatible with a " - "fully yielded graph of the requested topology") + raise ValueError("edge_list contains edges incompatible with G") # Hard to check edge_list consistency owing to directedness, etc. Brute force G.remove_edges_from(list(G.edges)) G.add_edges_from(edge_list) if G.number_of_edges() < len(edge_list): raise ValueError('edge_list contains duplicates.') + +def _add_compatible_nodes(G, node_list): + if node_list is not None: + if not all(G.has_node(n) for n in node_list): + raise ValueError("node_list contains nodes incompatible with G") + nodes = set(node_list) + remove_nodes = set(G) - nodes + G.remove_nodes_from(remove_nodes) + if G.number_of_nodes() < len(node_list): + raise ValueError('node_list contains duplicates.') + +def _add_compatible_terms(G, node_list, edge_list): + _add_compatible_edges(G, edge_list) + _add_compatible_nodes(G, node_list) + #Check node deletion hasn't caused edge deletion: + if edge_list is not None and len(edge_list) != G.number_of_edges(): + raise ValueError('The edge_list contains nodes absent from the node_list') diff --git a/dwave_networkx/generators/pegasus.py b/dwave_networkx/generators/pegasus.py index 48241f20..16fbb009 100644 --- a/dwave_networkx/generators/pegasus.py +++ b/dwave_networkx/generators/pegasus.py @@ -24,11 +24,12 @@ from itertools import product from .chimera import _chimera_coordinates_cache -from .common import _add_compatible_edges +from .common import _add_compatible_edges, _add_compatible_nodes, _add_compatible_terms __all__ = ['pegasus_graph', 'pegasus_coordinates', 'pegasus_sublattice_mappings', + 'pegasus_torus', ] def pegasus_graph(m, create_using=None, node_list=None, edge_list=None, data=True, @@ -93,9 +94,10 @@ def pegasus_graph(m, create_using=None, node_list=None, edge_list=None, data=Tru check_node_list : bool (optional, default :code:`False`) If :code:`True`, the ``node_list`` elements are checked for compatibility with the graph topology and node labeling conventions, an error is thrown - if any node is incompatible or duplicates exist. - In other words, only node_lists that specify subgraphs of the default - (full yield) graph are permitted. + if any node is incompatible or duplicates exist. + In other words, only node lists that specify subgraphs of the default + (full yield) graph are permitted. An exception is allowed if + ``check_edge_list=False``, in which case any node in ``edge_list`` is treated as valid. check_edge_list : bool (optional, default :code:`False`) If :code:`True`, the edge_list elements are checked for compatibility with the graph topology and node labeling conventions, an error is thrown @@ -235,6 +237,11 @@ def label(u, w, k, z): max_size = m * (m - 1) * 24 # max number of nodes G can have + if edge_list is None: + check_edge_list = False + if node_list is None: + check_node_list = False + if edge_list is None or check_edge_list is True: if nice_coordinates: fabric_start = 4,8 @@ -274,18 +281,19 @@ def efilter(e): return qfilter(*e[0]) and qfilter(*e[1]) if edge_list is not None: _add_compatible_edges(G, edge_list) else: + if check_node_list or node_list is None: + G.add_nodes_from(label(u, w, k, z) for u in range(2) + for w in range(m) + for k in range(12) + for z in range(m-1)) G.add_edges_from(edge_list) if node_list is not None: - nodes = set(node_list) - G.remove_nodes_from(set(G) - nodes) if check_node_list: - if G.number_of_nodes() != len(node_list): - raise ValueError("node_list contains nodes incompatible with " - "the specified topology and node-labeling " - "convention, or duplicates") - + _add_compatible_nodes(G, node_list) else: + nodes = set(node_list) + G.remove_nodes_from(set(G) - nodes) G.add_nodes_from(nodes) # for singleton nodes if data: @@ -366,9 +374,9 @@ def fragment_tuple(pegasus_coords): offset = offset[k] # Find the base (i.e. zeroth) Chimera fragment of this pegasus coordinate - fz0 = (z*12 + offset) // 2 #first fragment's z-coordinate - fw = (w*12 + k) // 2 #fragment w-coordinate - fk = k&1 #fragment k-index + fz0 = (z*12 + offset) // 2 # first fragment's z-coordinate + fw = (w*12 + k) // 2 # fragment w-coordinate + fk = k&1 # fragment k-index base = [fw, 0, u, fk] if u else [0, fw, u, fk] # Generate the six fragments associated with this pegasus coordinate @@ -478,13 +486,13 @@ def fragmented_edges(pegasus_graph): else: coords = lambda z: z - #first, we generate the edges internal to the fragments corresponding to a node + # first, we generate the edges internal to the fragments corresponding to a node for q in pegasus_graph.nodes(): u, w, k, z = coords(q) - #copied from get_tuple_fragmentation_fn and slightly optimized + # copied from get_tuple_fragmentation_fn and slightly optimized offset = offsets[u, k] - fz0 = (z*12 + offset) // 2 #first fragment z-coordinate - fw = (w*12 + k) // 2 #fragment w-coordinate + fz0 = (z*12 + offset) // 2 # first fragment z-coordinate + fw = (w*12 + k) // 2 # fragment w-coordinate base = [fw, fz0, u, k&1] if u else [fz0, fw, u, k&1] prev = tuple(base) for fz in range(fz0+1, fz0+6): @@ -493,36 +501,36 @@ def fragmented_edges(pegasus_graph): yield (prev, curr) prev = curr - #now for the thinky part: for each Pegasus edge, generate the corresponding Chimera edge - #we skip the "odd-coupler" edges because they don't exist in Chimera + # now for the thinky part: for each Pegasus edge, generate the corresponding Chimera edge + # we skip the "odd-coupler" edges because they don't exist in Chimera for q0, q1 in pegasus_graph.edges(): u0, w0, k0, z0 = coords(q0) u1, w1, k1, z1 = coords(q1) if u0 == u1: if k0 == k1: - #this is an external edge -- we could probably do some hijinks to fold this in - #with the nodes loop, but cost/benefit doesn't support it right now + # this is an external edge -- we could probably do some hijinks to fold this in + # with the nodes loop, but cost/benefit doesn't support it right now offset = offsets[u0, k0] - fz = (min(z0, z1)*12 + offset) // 2 #first fragment z-coordinate in the pair - fw = (w0*12 + k0) // 2 #fragment w-coordinate for both qubits - fk = k0&1 #fragment k-index + fz = (min(z0, z1)*12 + offset) // 2 # first fragment z-coordinate in the pair + fw = (w0*12 + k0) // 2 # fragment w-coordinate for both qubits + fk = k0&1 # fragment k-index if u0: yield ((fw, fz+5, u0, fk), (fw, fz+6, u0, fk)) else: yield ((fz+5, fw, u0, fk), (fz+6, fw, u0, fk)) - #else: this is an odd edge; yield nothing + # else: this is an odd edge; yield nothing else: - #this may look a little magical -- we're looking for an edge of the form + # this may look a little magical -- we're looking for an edge of the form # (fy, fx, u0, fk0), (fy, fx, u1, fk1) - #where (fy, fx) are the first two coordinates of both the fragments of (u0, w0, k0,z0), + # where (fy, fx) are the first two coordinates of both the fragments of (u0, w0, k0,z0), # (fy, fx) in [(fz0+0, fw0), (fz0+1, fw0), ..., (fz0+5, fw0)] - #and also the the fragments of (u1, w1, k1, z1): + # and also the the fragments of (u1, w1, k1, z1): # (fy, fx) in [(fw1, fz1+0), (fw1, fz1+1), ..., (fw1, fz1+5)] - #(see get_tuple_fragmentation_fn to see the fragment generator) - #with the assumption that an intersection exists: it can only be located at (fw0, fw1) - #since those coordinates are constant in the respective intervals. Thus, we get to - #skip looking up the offsets. Magic? No, Math! + # (see get_tuple_fragmentation_fn to see the fragment generator) + # with the assumption that an intersection exists: it can only be located at (fw0, fw1) + # since those coordinates are constant in the respective intervals. Thus, we get to + # skip looking up the offsets. Magic? No, Math! fw0 = (w0*12 + k0) // 2 fw1 = (w1*12 + k1) // 2 if u0: @@ -1003,7 +1011,7 @@ 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 + # store the offset in the mapping, so the user can reconstruct it mapping.offset = offset return mapping @@ -1055,7 +1063,7 @@ def mapping(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 + # store the offset in the mapping, so the user can reconstruct it mapping.offset = offset return mapping @@ -1091,7 +1099,7 @@ def pegasus_sublattice_mappings(source, target, offset_list=None): 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 + addition to reflections and rotations of the grid of unit tiles 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 @@ -1178,3 +1186,93 @@ def source_to_inner(q): for offset in offset_list: yield make_mapping(source_to_inner, nice_to_target, offset) + +def pegasus_torus(m, node_list=None, edge_list=None, + offset_lists=None, offsets_index=None): + """ + Creates a Pegasus graph modified to allow for periodic boundary conditions and translational invariance. + + Parameters + ---------- + m : int + Size parameter for the Pegasus lattice. + Connectivity of all nodes is :math:`13 + min(m - 1, 2)` + node_list : iterable (optional, default None) + Iterable of nodes in the graph. If None, nodes are generated + for an undiluted torus calculated from ``m`` + as described below. The node list must describe a subset + of the torus nodes to be maintained in the graph + using the coordinate node labeling scheme. + edge_list : iterable (optional, default None) + Iterable of edges in the graph. If None, edges are generated + for an undiluted torus calculated from ``m`` + as described below. The edge list must describe + a subgraph of the torus, using the coordinate node labeling scheme. + offset_lists : pair of lists, optional (default None) + Directly controls the offsets. Each list in the pair must have length 12 + and contain even integers. If ``offset_lists`` is not None, the ``offsets_index`` + parameter must be None. + offsets_index : int, optional (default None) + A number between 0 and 7, inclusive, that selects a preconfigured + set of topological parameters. If both the ``offsets_index`` and + ``offset_lists`` parameters are None, the ``offsets_index`` parameters is set + to zero. At least one of these two parameters must be None. + + Returns + ------- + G : NetworkX Graph + A Pegasus torus for size parameter :math:`m` using the coordinate labeling system. + + + A Pegasus torus is a generalization of the standard Pegasus graph + whereby degree-fifteen connectivity is maintained, but the boundary + condition is modified to enforce an additional translational-invariance + symmetry [RH]_. Local connectivity in the Pegasus torus + is identical to connectivity for Pegasus graph nodes away from the boundary. + A tile consists of 24 nodes, and the torus has :math:`m - 1` by :math:`m - 1` tiles. + Tile displacement modulo :math:`m - 1` defines an automorphism. + + See :func:`.pegasus_graph` for additional information. + + Examples + ======== + >>> G = dnx.pegasus_torus(4) # a 3x3 tile pegasus torus (connectivity 15) + >>> len(G) # 3*3*24 + 216 + >>> any([len(list(G.neighbors(n))) != 15 for n in G.nodes]) + False + + """ + # It is useful to inherit properties, attributes and methods of G: + G = pegasus_graph(m=m, node_list=None, edge_list=None, data=True, + coordinates=True, + offset_lists=offset_lists, offsets_index=offsets_index) + if m<2: + raise ValueError("m>=2 to define a non-empty lattice") + # Create the graph minor by contraction of boundary variables + # (u, m - 1, k, z) to (u, 0, k, z) and match boundary coupling to the + # bulk with addition of supplementary external couplers + def relabel(u,w,k,z): + return (u, w%(m - 1), k, z) + + # Contract internal couplers spanning the boundary: + G.add_edges_from([(relabel(*edge[0]), relabel(*edge[1])) + for edge in G.edges() if edge[0][1]==m - 1 or edge[1][1]==m - 1]) + if m>3: + # Add missing external couplers (u, w, k, -1) and (u, w, k, 0). + G.add_edges_from([((u, w, k, m - 2), (u, w, k, 0)) + for u in range(2) + for w in range(m - 1) + for k in range(12)]) + else: + # 2-tile wide lattices do not allow for boundary spanning + # edges. + pass + # Delete variables contracted at the boundary: + G.remove_nodes_from([(u, m - 1, k, z) + for u in range(2) for k in range(12) for z in range(m - 1)]) + _add_compatible_terms(G, node_list, edge_list) + + G.graph['boundary_condition'] = 'torus' + + return G diff --git a/dwave_networkx/generators/zephyr.py b/dwave_networkx/generators/zephyr.py index 069c510c..0c0f2978 100644 --- a/dwave_networkx/generators/zephyr.py +++ b/dwave_networkx/generators/zephyr.py @@ -25,11 +25,12 @@ from .chimera import _chimera_coordinates_cache -from .common import _add_compatible_edges +from .common import _add_compatible_edges, _add_compatible_nodes, _add_compatible_terms __all__ = ['zephyr_graph', 'zephyr_coordinates', 'zephyr_sublattice_mappings', + 'zephyr_torus' ] def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, @@ -38,7 +39,7 @@ def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, """ Creates a Zephyr graph with grid parameter ``m`` and tile parameter ``t``. - The Zephyr topology is described in [brk]_. + The Zephyr topology is described in [BRK]_. Parameters ---------- @@ -74,7 +75,8 @@ def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, the graph topology and node labeling conventions, and an error is thrown if any node is incompatible or duplicates exist. In other words, ``node_lists`` must specify a subgraph of the default - (full yield) graph described below. + (full yield) graph described below. An exception is allowed if + ``check_edge_list=False``, any node in edge_list will also be treated as valid. check_edge_list : bool (optional, default :code:`False`) If :code:`True`, ``edge_list`` elements are checked for compatibility with the graph topology and node labeling conventions, and an error is thrown @@ -155,7 +157,7 @@ def zephyr_graph(m, t=4, create_using=None, node_list=None, edge_list=None, References ---------- - .. [brk] Boothby, Raymond, King, Zephyr Topology of D-Wave Quantum + .. [BRK] Boothby, Raymond, King, Zephyr Topology of D-Wave Quantum Processors, October 2021. https://dwavesys.com/media/fawfas04/14-1056a-a_zephyr_topology_of_d-wave_quantum_processors.pdf """ @@ -180,22 +182,27 @@ def label(u, w, k, j, z): ("tile", t), ("data", data), ("labels", labels)) G.graph.update(construction) - + + if edge_list is None: + check_edge_list = False + if node_list is None: + check_node_list = False + if edge_list is None or check_edge_list is True: - #external edges + # external edges G.add_edges_from((label(u, w, k, j, z), label(u, w, k, j, z + 1)) for u, w, k, j, z in product( (0, 1), range(M), range(t), (0, 1), range(m-1) )) - #odd edges + # odd edges G.add_edges_from((label(u, w, k, 0, z), label(u, w, k, 1, z-a)) for u, w, k, a in product( (0, 1), range(M), range(t), (0, 1) ) for z in range(a, m)) - #internal edges + # internal edges G.add_edges_from((label(0, 2*w+1+a*(2*i-1), k, j, z), label(1, 2*z+1+b*(2*j-1), h, i, w)) for w, z, h, k, i, j, a, b in product( range(m), range(m), range(t), range(t), (0, 1), (0, 1), (0, 1), (0, 1) @@ -203,19 +210,20 @@ def label(u, w, k, j, z): if edge_list is not None: _add_compatible_edges(G, edge_list) else: + if check_node_list or node_list is None: + G.add_nodes_from(label(u, w, k, j, z) for u in range(2) + for w in range(2*m+1) + for k in range(t) + for j in range(2) + for z in range(m)) G.add_edges_from(edge_list) if node_list is not None: - nodes = set(node_list) - G.remove_nodes_from(set(G) - nodes) if check_node_list: - if G.number_of_nodes() != len(node_list): - raise ValueError("node_list contains nodes incompatible with " - "the specified topology and node-labeling " - "convention, or duplicates") - - + _add_compatible_nodes(G, node_list) else: + nodes = set(node_list) + G.remove_nodes_from(set(G) - nodes) G.add_nodes_from(nodes) # for singleton nodes if data: @@ -446,7 +454,7 @@ def mapping(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 + # store the offset in the mapping, so the user can reconstruct it mapping.offset = offset return mapping @@ -502,7 +510,7 @@ def mapping(q): 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 + # store the offset in the mapping, so the user can reconstruct it mapping.offset = offset return mapping @@ -549,7 +557,7 @@ def mapping(q): 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 + # store the offset in the mapping, so the user can reconstruct it mapping.offset = offset return mapping @@ -584,7 +592,7 @@ def zephyr_sublattice_mappings(source, target, offset_list=None): 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 + addition to reflections and rotations of the grid of unit tiles 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 @@ -680,3 +688,88 @@ def source_to_inner(q): for offset in offset_list: yield make_mapping(source_to_inner, zephyr_to_target, offset) + +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. + + The graph matches the local connectivity properties of a standard Zephyr graph, + but with modified periodic boundary condition. Tiles of :math:`8t` nodes are arranged + on an :math:`m` by :math:`m` torus. + + Parameters + ---------- + m : int + Grid parameter for the Zephyr lattice. + Connectivity of all nodes is :math:`4t + min(2m - 1, 4)`. + t : int + Tile parameter for the Zephyr lattice. + node_list : iterable (optional, default None) + Iterable of nodes in the graph. If None, nodes are generated + for an undiluted torus calculated from ``m`` and ``t`` + as described below. The node list must describe a subset + of the torus nodes to be maintained in the graph + using the coordinate node labeling scheme. + edge_list : iterable (optional, default None) + Iterable of edges in the graph. If None, edges are generated + for an undiluted torus calculated from ``m`` and ``t`` + as described below. The edge list must describe + a subgraph of the torus, using the coordinate node labeling scheme. + + Returns + ------- + G : NetworkX Graph + A Zephyr torus with grid parameter ``m`` and tile parameter ``t``, + with Zephyr coordinate node labels. + + + A Zephyr torus is a generalization of the standard Zephyr graph + whereby degree-twenty connectivity is maintained, but the boundary + condition is modified to enforce an additional translational-invariance + symmetry [RH]_. Local connectivity in the Zephyr torus + is identical to connectivity for Zephyr graph nodes away from the boundary. + A tile consists of :math:`8t` nodes, and the torus has :math:`m` by :math:`m` tiles. + Tile displacement modulo :math:`m` defines an automorphism. + + See :func:`.zephyr_graph` for additional information. + + Examples + -------- + >>> G = dnx.zephyr_torus(3) # a 3x3 tile pegasus torus (connectivity 15) + >>> len(G) # 3*3*24 + 288 + >>> any([len(list(G.neighbors(n))) != 20 for n in G.nodes]) + False + + """ + G = zephyr_graph(m=m, t=t, node_list=None, edge_list=None, + data=True, coordinates=True) + + def relabel(u, w, k, j, z): + return (u, w%(2*m), k, j, z) + + # Contract internal couplers spanning the boundary: + G.add_edges_from([(relabel(*edge[0]), relabel(*edge[1])) + for edge in G.edges() if edge[0][1]==2*m or edge[1][1]==2*m]) + + if m>1: + # Add boundary spanning external couplers: + G.add_edges_from([((u, w, k, 1, m - 1), (u, w, k, 0, 0)) + for u in range(2) + for w in range(2*m) + for k in range(t)]) + G.add_edges_from([((u, w, k, j, m - 1), (u, w, k, j, 0)) + for u in range(2) + for w in range(2*m) + for k in range(t) + for j in range(2)]) + + # Delete variables contracted at the boundary: + G.remove_nodes_from([(u, 2*m, k, j, z) + for u in range(2) for k in range(t) for j in range(2) for z in range(m)]) + + _add_compatible_terms(G, node_list, edge_list) + + G.graph['boundary_condition'] = 'torus' + + return G diff --git a/tests/test_canonicalization.py b/tests/test_canonicalization.py index 10e95fa6..699cf1dc 100644 --- a/tests/test_canonicalization.py +++ b/tests/test_canonicalization.py @@ -104,7 +104,7 @@ def test_construction_string_labels(self): self.assertEqual(bqm, bqm2) def test_reversed(self): - C33 = nx.OrderedGraph() + C33 = nx.Graph() #Ordering is guaranteed Python>=3.7, OrderedGraph is deprecated C33.add_nodes_from(reversed(range(3*3*4))) C33.add_edges_from(dnx.chimera_graph(3, 3, 4).edges) coord = dnx.chimera_coordinates(3, 3, 4) diff --git a/tests/test_generator_chimera.py b/tests/test_generator_chimera.py index 755cba3f..4317ba7c 100644 --- a/tests/test_generator_chimera.py +++ b/tests/test_generator_chimera.py @@ -16,6 +16,7 @@ import networkx as nx import dwave_networkx as dnx +import numpy as np alpha_map = dict(enumerate('ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz')) @@ -289,18 +290,18 @@ def test_node_list(self): t = 2 N = m*n*t*2 G = dnx.chimera_graph(m,n,t) - #Valid (full) node_list + # Valid (full) node_list node_list = list(G.nodes) G = dnx.chimera_graph(m, n, t, node_list=node_list, check_node_list=True) self.assertEqual(G.number_of_nodes(), len(node_list)) - #Valid node_list in coordinate system + # Valid node_list in coordinate system node_list = [(0,0,0,0)] G = dnx.chimera_graph(m, n, t, node_list=node_list, check_node_list=True, coordinates=True) self.assertEqual(G.number_of_nodes(), len(node_list)) with self.assertRaises(ValueError): - #Invalid node_list + # Invalid node_list node_list = [0, N] G = dnx.chimera_graph(m, n, t, node_list=node_list, check_node_list=True) @@ -311,16 +312,33 @@ def test_node_list(self): check_node_list=True) with self.assertRaises(ValueError): - #node is valid, but not in the requested coordinate system + # Node is valid, but not in the requested coordinate system node_list = [0] G = dnx.chimera_graph(m, n, t, node_list=node_list, check_node_list=True, coordinates=True) - + edge_list = [(-1,0)] + node_list = [0] + # Edges are not checked, but node_list is, the edge is deleted: + G = dnx.chimera_graph(m, n, t, node_list=node_list, edge_list=edge_list, + check_node_list=True, coordinates=True) + self.assertEqual(G.number_of_edges(), 0) + self.assertEqual(G.number_of_nodes(), 1) + edge_list = [(-1,0)] + node_list = [-1,0] + # Edges are not checked, but node_list is, the invalid node (-1) is permitted + # because it is specified in edge_list: + G = dnx.chimera_graph(m, n, t, node_list=node_list, edge_list=edge_list, + check_node_list=True, coordinates=True) + self.assertEqual(G.number_of_edges(), 1) + self.assertEqual(G.number_of_nodes(), 2) + + def test_edge_list(self): m = 2 n = 3 t = 4 + num_var = m*n*t*2 G = dnx.chimera_graph(m, n, t) edge_list = list(G.edges) # Valid (full) edge_list @@ -332,6 +350,7 @@ def test_edge_list(self): G = dnx.chimera_graph(m, n, t, edge_list=edge_list, check_edge_list=True, coordinates=True) self.assertEqual(G.number_of_edges(),len(edge_list)) + self.assertEqual(G.number_of_nodes(),num_var) #No node deletions specified # Valid edge, but absent from node_list, hence dropped: edge_list = [(0,t)] @@ -351,3 +370,63 @@ def test_edge_list(self): edge_list = [(0, t), (0, t)] G = dnx.chimera_graph(m, edge_list=edge_list, check_edge_list=True) + +class TestChimeraTorus(unittest.TestCase): + def test(self): + for m in range(1,4): + for n in range(1,4): + for t in [1,4]: + conn_vert = min(2,m-1) + t + conn_horiz = min(2,n-1) + t + num_var = m*n*t*2 + num_edges = ((num_var//2)*(conn_vert + conn_horiz))//2 + g = dnx.chimera_torus(m=m,n=n,t=t) + + # Check bulk properties: + self.assertEqual(g.number_of_nodes(),num_var) # Number nodes + self.assertEqual(g.number_of_edges(),num_edges) # Number nodes + + # Check translational invariance: + if m > 1: + drow = 1+np.random.randint(m-1) + else: + drow = 0 + if n > 1: + dcol = 1+np.random.randint(n-1) + else: + dcol = 0 + relabel = lambda tup: ((tup[0]+drow)%m,(tup[1]+dcol)%n,tup[2],tup[3]) + g_translated = nx.relabel_nodes(g,relabel,copy=True) + #Check 1:1 correspondence of edges + g.remove_edges_from(g_translated.edges()) + self.assertEqual(g.number_of_edges(),0) + #Check 1:1 correspondence of nodes + g.remove_nodes_from(g_translated.nodes()) + self.assertEqual(g.number_of_nodes(),0) + + def tests_list(self): + #Test correct handling of nodes and edges: + m=3 + n=3 + t=2 + num_var = m*n*t*2 + to_coord = dnx.chimera_coordinates(m,n,t).linear_to_chimera + node_list_lin = [0, t-1, num_var] + node_list = [to_coord(i) for i in node_list_lin] + G = dnx.chimera_torus(m=m, n=n, t=t, node_list = [node_list[i] for i in range(2) ]) + self.assertEqual(G.number_of_edges(),0) + self.assertEqual(G.number_of_nodes(),2) + with self.assertRaises(ValueError): + # 1 invalid node + G = dnx.chimera_torus(m=m, n=n, t=t, node_list=node_list) + edge_list_lin = [(0, t), (0, t+1), (0, 1)] + edge_list = [(to_coord(n1), to_coord(n2)) for n1, n2 in edge_list_lin] + G = dnx.chimera_torus(m=m, n=n, t=t, edge_list = [edge_list[i] for i in range(2) ]) + + self.assertEqual(G.number_of_edges(),2) + self.assertEqual(G.number_of_nodes(),num_var) #No deletions + + with self.assertRaises(ValueError): + # 1 invalid edge + G = dnx.chimera_torus(m=m, n=n, t=t, edge_list = edge_list) + diff --git a/tests/test_generator_pegasus.py b/tests/test_generator_pegasus.py index 66ba741f..698d2df5 100644 --- a/tests/test_generator_pegasus.py +++ b/tests/test_generator_pegasus.py @@ -19,6 +19,7 @@ import networkx as nx import dwave_networkx as dnx +import numpy as np from dwave_networkx.generators.pegasus import ( fragmented_edges, @@ -49,6 +50,26 @@ def test_bad_args(self): self.assertLessEqual(len(w), 13) self.assertGreaterEqual(len(w), 12) +class TestPegasusTorus(unittest.TestCase): + def test(self): + for m in [4]: + g = dnx.pegasus_torus(m) + + num_var = 24*(m-1)*(m-1) + self.assertEqual(g.number_of_nodes(),num_var) + num_edges = (num_var*(13 + (m>2) + (m>3)))//2 + self.assertEqual(g.number_of_edges(),num_edges) + #(u,w,k,z) -> (u, [w + u dx + (1-u)dy]%(L-1)), k, [z + (1-u) dx + u dy]%(L-1)) + L=m-1 #Cell dimension + if L>1: + dx = 1 + np.random.randint(m-1) + dy = 1 + np.random.randint(m-1) + relabel = lambda tup: (tup[0],(tup[1] + tup[0]*dx + (1-tup[0])*dy)%L,tup[2],(tup[3] + tup[0]*dy + (1-tup[0])*dx)%L) + g_translated = nx.relabel_nodes(g,relabel,copy=True) + g.remove_edges_from(g_translated.edges()) + self.assertEqual(g.number_of_edges(),0) + g.remove_nodes_from(g_translated.nodes()) + self.assertEqual(g.number_of_nodes(),0) class TestPegasusCoordinates(unittest.TestCase): @@ -331,7 +352,22 @@ def test_node_list(self): node_list = [0] G = dnx.pegasus_graph(m, node_list=node_list, fabric_only=False, check_node_list=True, coordinates=True) - + # Edges are not checked, but node_list is, the edge is deleted: + edge_list = [(-1,0)] + node_list = [0] + G = dnx.pegasus_graph(m, node_list=node_list, edge_list=edge_list, fabric_only=False, + check_node_list=True, coordinates=True) + self.assertEqual(G.number_of_edges(), 0) + self.assertEqual(G.number_of_nodes(), 1) + # Edges are not checked, but node_list is, the invalid node (-1) is permitted + # because it is specified in edge_list: + edge_list = [(-1,0)] + node_list = [-1,0] + G = dnx.pegasus_graph(m, node_list=node_list, edge_list=edge_list, fabric_only=False, + check_node_list=True, coordinates=True) + self.assertEqual(G.number_of_edges(), 1) + self.assertEqual(G.number_of_nodes(), 2) + def test_edge_list(self): m = 4 G = dnx.pegasus_graph(m) @@ -525,3 +561,28 @@ def test_nice_coordinates(self): #couplers, which aren't included in the chimera graph -- odd couplers make a perfect #matching, so thats 1/2 an edge per node. self.assertEqual(p.number_of_edges() + 9 * p.number_of_nodes()//2, num_edges) + + def tests_list(self): + #Test correct handling of nodes and edges: + m=4 + num_var = (m-1)*(m-1)*24 + to_coord = dnx.pegasus_coordinates(m).linear_to_pegasus + node_list_lin = [0, 2, -1] + node_list = [to_coord(i) for i in node_list_lin] + G = dnx.pegasus_torus(m=m, node_list = [node_list[i] for i in range(2) ]) + self.assertEqual(G.number_of_edges(),1) + self.assertEqual(G.number_of_nodes(),2) + with self.assertRaises(ValueError): + #Invalid node + G = dnx.pegasus_torus(m=m, node_list=node_list) + edge_list_lin = [(0, 1), (m-1, m), (2, 3)] + edge_list = [(to_coord(n1), to_coord(n2)) for n1, n2 in edge_list_lin] + + G = dnx.pegasus_torus(m=m, edge_list = [edge_list[i] for i in range(2) ]) + + self.assertEqual(G.number_of_edges(),2) + self.assertEqual(G.number_of_nodes(),num_var) #No deletions + + with self.assertRaises(ValueError): + #2 invalid, 1 valid + G = dnx.pegasus_torus(m=m, edge_list = edge_list) diff --git a/tests/test_generator_zephyr.py b/tests/test_generator_zephyr.py index b4c66844..246f453f 100644 --- a/tests/test_generator_zephyr.py +++ b/tests/test_generator_zephyr.py @@ -16,6 +16,7 @@ import networkx as nx import dwave_networkx as dnx +import numpy as np class TestZephyrGraph(unittest.TestCase): def test_single_tile(self): @@ -219,8 +220,22 @@ def test_node_list(self): # Not in the requested coordinate system node_list = [0] G = dnx.zephyr_graph(m, t, node_list=node_list, - check_node_list=True, coordinates=True) - + check_node_list=True, coordinates=True) + # Edges are not checked, but node_list is, the edge is deleted: + edge_list = [(-1,0)] + node_list = [0] + G = dnx.zephyr_graph(m, t, node_list=node_list, edge_list=edge_list, + check_node_list=True, coordinates=True) + self.assertEqual(G.number_of_edges(), 0) + self.assertEqual(G.number_of_nodes(), 1) + # Edges are not checked, but node_list is, the invalid node (-1) is permitted + # because it is specified in edge_list: + edge_list = [(-1,0)] + node_list = [-1,0] + G = dnx.zephyr_graph(m, t, node_list=node_list, edge_list=edge_list, + check_node_list=True, coordinates=True) + self.assertEqual(G.number_of_edges(), 1) + self.assertEqual(G.number_of_nodes(), 2) def test_edge_list(self): m=2 @@ -258,3 +273,61 @@ def test_edge_list(self): edge_list = [(0, 1), (0, 1)] G = dnx.zephyr_graph(m, t, edge_list=edge_list, check_edge_list=True) + + +class TestZephyrTorus(unittest.TestCase): + def test(self): + for m in [2,3,4]: + for t in [1,4]: + G = dnx.zephyr_torus(m=m, t=t) + # Test bulk properties: + + num_nodes = (8*t)*m*m + self.assertEqual(G.number_of_nodes(), num_nodes) + if m==1: + conn = 1 + t*4; + self.assertEqual(G.number_of_edges(),(num_nodes*conn)//2) + elif m==2: + conn = 3 + t*4 + self.assertEqual(G.number_of_edges(),(num_nodes*conn)//2) + else: + conn = 4 + t*4; + self.assertEqual(G.number_of_edges(),(num_nodes*conn)//2) + + # Check translational invariance (identical edges, identical nodes): + # (u,w,k,j,z) -> (u, [w + u (2*dx) + (1-u)*(2*dy)]%(m-1)), k, j, [z + (1-u) dx + u dy]%(m-1)) + dx = 1 + np.random.randint(m-2) + dy = np.random.randint(m-1) + relabel = lambda tup: (tup[0],(tup[1] + tup[0]*(2*dx) + (1-tup[0])*(2*dy))%(2*m),tup[2],tup[3],(tup[4] + tup[0]*dy + (1-tup[0])*dx)%m) + G_translated = nx.relabel_nodes(G,relabel,copy=True) + G.remove_edges_from(G_translated.edges()) + self.assertEqual(G.number_of_edges(),0) #At t=1, m=2 (n=32), 8 left over edges. 8 edges collapsed to the same place? + G.remove_nodes_from(G_translated.nodes()) + self.assertEqual(G.number_of_nodes(),0) + + + def tests_list(self): + # Test correct handling of nodes and edges: + m=3 + t=4 + num_var = m*m*t*8 + to_coord = dnx.zephyr_coordinates(m,t).linear_to_zephyr + node_list_lin = [0, 1, -1] + node_list = [to_coord(i) for i in node_list_lin] + G = dnx.zephyr_torus(m=m, t=t, node_list = [node_list[i] for i in range(2) ]) + self.assertEqual(G.number_of_edges(),1) + self.assertEqual(G.number_of_nodes(),2) + with self.assertRaises(ValueError): + # 1 invalid node + G = dnx.zephyr_torus(m=m, t=t, node_list=node_list) + edge_list_lin = [(0, 1), (m, m+1), (0, m+1)] + edge_list = [(to_coord(n1), to_coord(n2)) for n1, n2 in edge_list_lin] + G = dnx.zephyr_torus(m=m, t=t, edge_list = [edge_list[i] for i in range(2) ]) + + self.assertEqual(G.number_of_edges(),2) + self.assertEqual(G.number_of_nodes(),num_var) # No deletions + + with self.assertRaises(ValueError): + # 1 invalid edge + G = dnx.zephyr_torus(m=m, t=t, edge_list = edge_list) +