From 88fc02adbc1025db2281f2c333eaca1d1eca91e8 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 1 May 2026 00:00:49 +0200 Subject: [PATCH 01/27] Import problem generators from dwave_networkx/algorithms into dimod Squashed 'dwave_networkx/algorithms/' content from commit ae33535c git-subtree-dir: dwave_networkx/algorithms git-subtree-split: ae33535c091242a4bccca5dbfc7fb13131570ce6 Full history: https://github.com/dwavesystems/dwave-graphs/tree/legacy/dwave-networkx/dwave_networkx/algorithms Co-authored-by: Aidan Roy Co-authored-by: Alexander Condello Co-authored-by: Alexander Condello Co-authored-by: Arafat Co-authored-by: arafatk Co-authored-by: Bradley Ellert Co-authored-by: Heidi Tong Co-authored-by: Jack Raymond <10591246+jackraymond@users.noreply.github.com> Co-authored-by: Jack Raymond Co-authored-by: James Cortese Co-authored-by: Jason Necaise Co-authored-by: Jeremy Fan Co-authored-by: Joel M. Gottlieb Co-authored-by: Joel Pasvolsky Co-authored-by: John McFarland Co-authored-by: Kelly Boothby Co-authored-by: Maximilian Schmidt Co-authored-by: Radomir Stevanovic Co-authored-by: vgoliber Co-authored-by: Victoria Goliber <38987205+vgoliber@users.noreply.github.com> Co-authored-by: Victoria Goliber --- dwave_networkx/algorithms/__init__.py | 26 + dwave_networkx/algorithms/canonicalization.py | 163 +++ dwave_networkx/algorithms/clique.py | 187 ++++ dwave_networkx/algorithms/coloring.py | 403 ++++++++ dwave_networkx/algorithms/cover.py | 191 ++++ .../algorithms/elimination_ordering.py | 957 ++++++++++++++++++ dwave_networkx/algorithms/independent_set.py | 265 +++++ dwave_networkx/algorithms/markov.py | 213 ++++ dwave_networkx/algorithms/matching.py | 351 +++++++ dwave_networkx/algorithms/max_cut.py | 143 +++ dwave_networkx/algorithms/partition.py | 156 +++ dwave_networkx/algorithms/social.py | 185 ++++ dwave_networkx/algorithms/tsp.py | 241 +++++ 13 files changed, 3481 insertions(+) create mode 100644 dwave_networkx/algorithms/__init__.py create mode 100644 dwave_networkx/algorithms/canonicalization.py create mode 100644 dwave_networkx/algorithms/clique.py create mode 100644 dwave_networkx/algorithms/coloring.py create mode 100644 dwave_networkx/algorithms/cover.py create mode 100644 dwave_networkx/algorithms/elimination_ordering.py create mode 100644 dwave_networkx/algorithms/independent_set.py create mode 100644 dwave_networkx/algorithms/markov.py create mode 100644 dwave_networkx/algorithms/matching.py create mode 100644 dwave_networkx/algorithms/max_cut.py create mode 100644 dwave_networkx/algorithms/partition.py create mode 100644 dwave_networkx/algorithms/social.py create mode 100644 dwave_networkx/algorithms/tsp.py diff --git a/dwave_networkx/algorithms/__init__.py b/dwave_networkx/algorithms/__init__.py new file mode 100644 index 000000000..d588b2472 --- /dev/null +++ b/dwave_networkx/algorithms/__init__.py @@ -0,0 +1,26 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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 dwave_networkx.algorithms.independent_set import * +from dwave_networkx.algorithms.canonicalization import * +from dwave_networkx.algorithms.clique import * +from dwave_networkx.algorithms.cover import * +from dwave_networkx.algorithms.matching import * +from dwave_networkx.algorithms.social import * +from dwave_networkx.algorithms.elimination_ordering import * +from dwave_networkx.algorithms.coloring import * +from dwave_networkx.algorithms.max_cut import * +from dwave_networkx.algorithms.markov import * +from dwave_networkx.algorithms.tsp import * +from dwave_networkx.algorithms.partition import * diff --git a/dwave_networkx/algorithms/canonicalization.py b/dwave_networkx/algorithms/canonicalization.py new file mode 100644 index 000000000..af4f90c03 --- /dev/null +++ b/dwave_networkx/algorithms/canonicalization.py @@ -0,0 +1,163 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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. + +import math + +from dwave_networkx.generators.chimera import chimera_coordinates + +__all__ = ['canonical_chimera_labeling'] + + +def canonical_chimera_labeling(G, t=None): + """Returns a mapping from the labels of G to chimera-indexed labeling. + + Parameters + ---------- + G : NetworkX graph + A Chimera-structured graph. + t : int (optional, default 4) + Size of the shore within each Chimera tile. + + Returns + ------- + chimera_indices: dict + A mapping from the current labels to a 4-tuple of Chimera indices. + + """ + adj = G.adj + + if t is None: + if hasattr(G, 'edges'): + num_edges = len(G.edges) + else: + num_edges = len(G.quadratic) + t = _chimera_shore_size(adj, num_edges) + + chimera_indices = {} + + row = col = 0 + + # need to find a node in a corner + root_edge = min(((u, v) for u in adj for v in adj[u]), + key=lambda edge: len(adj[edge[0]]) + len(adj[edge[1]])) + root, _ = root_edge + + horiz, verti = rooted_tile(adj, root, t) + while len(chimera_indices) < len(adj): + + new_indices = {} + + if row == 0: + # if we're in the 0th row, we can assign the horizontal randomly + for si, v in enumerate(horiz): + new_indices[v] = (row, col, 0, si) + else: + # we need to match the row above + for v in horiz: + north = [u for u in adj[v] if u in chimera_indices] + assert len(north) == 1 + i, j, u, si = chimera_indices[north[0]] + assert i == row - 1 and j == col and u == 0 + new_indices[v] = (row, col, 0, si) + + if col == 0: + # if we're in the 0th col, we can assign the vertical randomly + for si, v in enumerate(verti): + new_indices[v] = (row, col, 1, si) + else: + # we need to match the column to the east + for v in verti: + east = [u for u in adj[v] if u in chimera_indices] + assert len(east) == 1 + i, j, u, si = chimera_indices[east[0]] + assert i == row and j == col - 1 and u == 1 + new_indices[v] = (row, col, 1, si) + + chimera_indices.update(new_indices) + + # get the next root + root_neighbours = [v for v in adj[root] if v not in chimera_indices] + if len(root_neighbours) == 1: + # we can increment the row + root = root_neighbours[0] + horiz, verti = rooted_tile(adj, root, t) + + row += 1 + else: + # need to go back to row 0, and increment the column + assert not root_neighbours # should be empty + + # we want (0, col, 1, 0), we could cache this, but for now let's just go look for it + # the slow way + vert_root = [v for v in chimera_indices if chimera_indices[v] == (0, col, 1, 0)][0] + + vert_root_neighbours = [v for v in adj[vert_root] if v not in chimera_indices] + + if vert_root_neighbours: + + verti, horiz = rooted_tile(adj, vert_root_neighbours[0], t) + root = next(iter(horiz)) + + row = 0 + col += 1 + + return chimera_indices + + +def rooted_tile(adj, n, t): + horiz = {n} + vert = set() + + # get all of the nodes that are two steps away from n + two_steps = {v for u in adj[n] for v in adj[u] if v != n} + + # find the subset of two_steps that share exactly t neighbours + for v in two_steps: + shared = set(adj[n]).intersection(adj[v]) + + if len(shared) == t: + assert v not in horiz + horiz.add(v) + vert |= shared + + assert len(vert) == t + return horiz, vert + + +def _chimera_shore_size(adj, num_edges): + # we know |E| = m*n*t*t + (2*m*n-m-n)*t + + num_nodes = len(adj) + + max_degree = max(len(adj[v]) for v in adj) + + if num_nodes == 2 * max_degree: + return max_degree + + def a(t): + return -2*t + + def b(t): + return (t + 2) * num_nodes - 2 * num_edges + + def c(t): + return -num_nodes + + t = max_degree - 1 + m = (-b(t) + math.sqrt(b(t)**2 - 4*a(t)*c(t))) / (2 * a(t)) + + if m.is_integer(): + return t + + return max_degree - 2 diff --git a/dwave_networkx/algorithms/clique.py b/dwave_networkx/algorithms/clique.py new file mode 100644 index 000000000..0b65f0858 --- /dev/null +++ b/dwave_networkx/algorithms/clique.py @@ -0,0 +1,187 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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. + +import networkx as nx +import dwave_networkx as dnx + +from dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ["maximum_clique", "clique_number", "is_clique"] + +@binary_quadratic_model_sampler(1) +def maximum_clique(G, sampler=None, lagrange=2.0, **sampler_args): + r"""Returns an approximate maximum clique. + + A clique in an undirected graph, G = (V, E), is a subset of the vertex set + :math:`C \subseteq V` such that for every two vertices in C there exists an edge + connecting the two. This is equivalent to saying that the subgraph + induced by C is complete (in some cases, the term clique may also refer + to the subgraph). A maximum clique is a clique of the largest + possible size in a given graph. + + This function works by finding the maximum independent set of the compliment + graph of the given graph G which is equivalent to finding maximum clique. + It defines a QUBO with ground states corresponding + to a maximum weighted independent set and uses the sampler to sample from it. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a maximum clique. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + lagrange : optional (default 2) + Lagrange parameter to weight constraints (no edges within set) + versus objective (largest set possible). + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + clique_nodes : list + List of nodes that form a maximum clique, as + determined by the given sampler. + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + + `Maximum Clique on Wikipedia `_ + + `Independent Set on Wikipedia `_ + + `QUBO on Wikipedia `_ + + Lucas, A. (2014). Ising formulations of many NP problems. + Frontiers in Physics, Volume 2, Article 5. + """ + if G is None: + raise ValueError("Expected NetworkX graph!") + + # finding the maximum clique in a graph is equivalent to finding + # the independent set in the complementary graph + complement_G = nx.complement(G) + return dnx.maximum_independent_set(complement_G, sampler, lagrange, **sampler_args) + + +@binary_quadratic_model_sampler(1) +def clique_number(G, sampler=None, lagrange=2.0, **sampler_args): + r"""Returns the number of vertices in the maximum clique of a graph. + + A maximum clique is a clique of the largest possible size in a given graph. + The clique number math:`\omega(G)` of a graph G is the number of + vertices in a maximum clique in G. The intersection number of + G is the smallest number of cliques that together cover all edges of G. + + This function works by finding the maximum independent set of the compliment + graph of the given graph G which is equivalent to finding maximum clique. + It defines a QUBO with ground states corresponding + to a maximum weighted independent set and uses the sampler to sample from it. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a maximum clique. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + lagrange : optional (default 2) + Lagrange parameter to weight constraints (no edges within set) + versus objective (largest set possible). + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + clique_nodes : list + List of nodes that form a maximum clique, as + determined by the given sampler. + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + + `Maximum Clique on Wikipedia `_ + """ + return len(maximum_clique(G, sampler, lagrange, **sampler_args)) + +def is_clique(G, clique_nodes): + """Determines whether the given nodes form a clique. + + A clique is a subset of nodes of an undirected graph such that every two + distinct nodes in the clique are adjacent. + + Parameters + ---------- + G : NetworkX graph + The graph on which to check the clique nodes. + + clique_nodes : list + List of nodes that form a clique, as + determined by the given sampler. + + Returns + ------- + is_clique : bool + True if clique_nodes forms a clique. + + Example + ------- + This example checks two sets of nodes, both derived from a + single Chimera unit cell, for an independent set. The first set is + the horizontal tile's nodes; the second has nodes from the horizontal and + verical tiles. + + >>> import dwave_networkx as dnx + >>> G = dnx.chimera_graph(1, 1, 4) + >>> dnx.is_clique(G, [0, 1, 2, 3]) + False + >>> dnx.is_clique(G, [0, 4]) + True + """ + for x in clique_nodes: + for y in clique_nodes: + if x != y: + if not(G.has_edge(x,y)): + return False + return True diff --git a/dwave_networkx/algorithms/coloring.py b/dwave_networkx/algorithms/coloring.py new file mode 100644 index 000000000..3ce60432a --- /dev/null +++ b/dwave_networkx/algorithms/coloring.py @@ -0,0 +1,403 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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. + +import math +import itertools + +import networkx as nx + +from dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ["is_vertex_coloring", + "is_cycle", + "min_vertex_color", + "min_vertex_coloring", # alias for min_vertex_color + "min_vertex_color_qubo", + "vertex_color", + "vertex_color_qubo", + ] + + +@nx.utils.decorators.nodes_or_number(1) +def vertex_color_qubo(G, colors): + """Return the QUBO with ground states corresponding to a vertex coloring. + + If `V` is the set of nodes, `E` is the set of edges and `C` is the set of + colors the resulting qubo will have: + + * :math:`|V|*|C|` variables/nodes + * :math:`|V|*|C|*(|C| - 1) / 2 + |E|*|C|` interactions/edges + + The QUBO has ground energy :math:`-|V|` and an infeasible gap of 1. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum vertex coloring. + + colors : int/sequence + The colors. If an int, the colors are labelled `[0, n)`. The number of + colors must be greater or equal to the chromatic number of the graph. + + Returns + ------- + QUBO : dict + The QUBO with ground states corresponding to valid colorings of the + graph. The QUBO variables are labelled `(v, c)` where `v` is a node + in `G` and `c` is a color. In the ground state of the QUBO, a variable + `(v, c)` has value 1 if `v` should be colored `c` in a valid coloring. + + + """ + _, colors = colors + + Q = {} + + # enforce that each variable in G has at most one color + for v in G.nodes: + # 1 in k constraint + for c in colors: + Q[(v, c), (v, c)] = -1 + + for c0, c1 in itertools.combinations(colors, 2): + Q[(v, c0), (v, c1)] = 2 + + # enforce that adjacent nodes do not have the same color + for u, v in G.edges: + # NAND constraint + for c in colors: + Q[(u, c), (v, c)] = 1 + + return Q + + +@binary_quadratic_model_sampler(2) +def vertex_color(G, colors, sampler=None, **sampler_args): + """Returns an approximate vertex coloring. + + Vertex coloring is the problem of assigning a color to the + vertices of a graph in a way that no adjacent vertices have the + same color. + + Defines a QUBO [Dah2013]_ with ground states corresponding to valid + vertex colorings and uses the sampler to sample from it. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum vertex coloring. + + colors : int/sequence + The colors. If an int, the colors are labelled `[0, n)`. The number of + colors must be greater or equal to the chromatic number of the graph. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + coloring : dict + A coloring for each vertex in G such that no adjacent nodes + share the same color. A dict of the form {node: color, ...} + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + """ + Q = vertex_color_qubo(G, colors) + + # get the lowest energy sample + sample = sampler.sample_qubo(Q, **sampler_args).first.sample + + return {v: c for (v, c), val in sample.items() if val} + + +def _chromatic_number_upper_bound(G): + # tries to determine an upper bound on the chromatic number of G + # Assumes G is not complete + + if not nx.is_connected(G): + return max((_chromatic_number_upper_bound(G.subgraph(c)) + for c in nx.connected_components(G))) + + n_nodes = len(G.nodes) + n_edges = len(G.edges) + + # chi * (chi - 1) <= 2 * |E| + quad_bound = math.ceil((1 + math.sqrt(1 + 8 * n_edges)) / 2) + + if n_nodes % 2 == 1 and is_cycle(G): + # odd cycle graphs need three colors + bound = 3 + elif n_nodes > 2: + try: + import numpy as np + except ImportError: + # chi <= max degree, unless it is complete or a cycle graph of odd length, + # in which case chi <= max degree + 1 (Brook's Theorem) + bound = max(G.degree(node) for node in G) + else: + # Let A be the adj matrix of G (symmetric, 0 on diag). Let theta_1 + # 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 + # 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 + + return min(quad_bound, bound) + + +def _chromatic_number_lower_bound(G): + # find a random maximal clique and use that to determine a lower bound + v = max(G, key=G.degree) + + clique = {v} + for u in G[v]: + if all(w in G[u] for w in clique): + clique.add(u) + + return len(clique) + + +def min_vertex_color_qubo(G, chromatic_lb=None, chromatic_ub=None): + """Return a QUBO with ground states corresponding to a minimum vertex + coloring. + + Vertex coloring is the problem of assigning a color to the + vertices of a graph in a way that no adjacent vertices have the + same color. A minimum vertex coloring is the problem of solving + the vertex coloring problem using the smallest number of colors. + + Defines a QUBO [Dah2013]_ with ground states corresponding to minimum + vertex colorings and uses the sampler to sample from it. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum vertex coloring. + + chromatic_lb : int, optional + A lower bound on the chromatic number. If one is not provided, a + bound is calulcated. + + chromatic_ub : int, optional + An upper bound on the chromatic number. If one is not provided, a bound + is calculated. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + QUBO : dict + The QUBO with ground states corresponding to minimum colorings of the + graph. The QUBO variables are labelled `(v, c)` where `v` is a node + in `G` and `c` is a color. In the ground state of the QUBO, a variable + `(v, c)` has value 1 if `v` should be colored `c` in a valid coloring. + + """ + + chi_ub = _chromatic_number_upper_bound(G) + chromatic_ub = chi_ub if chromatic_ub is None else min(chi_ub, chromatic_ub) + + chib_lb = _chromatic_number_lower_bound(G) + chromatic_lb = chib_lb if chromatic_lb is None else max(chib_lb, chromatic_lb) + + if chromatic_lb > chromatic_ub: + raise RuntimeError("something went wrong when calculating the " + "chromatic number bounds") + + # our base QUBO is one with as many colors as we might need, so we use the + # upper bound + Q = vertex_color_qubo(G, int(chromatic_ub)) + + if chromatic_lb != chromatic_ub: + # we want to penalize the colors that we aren't sure that we need + # we might need to use some of the colors, so we want to penalize + # them in increasing amounts, linearly. + + num_penalized = chromatic_ub - chromatic_lb + + # we want evenly spaced penalties in (0, 1) without the endpoints + weights = [p / (num_penalized + 1) for p in range(1, num_penalized + 1)] + + for p, c in zip(weights, range(chromatic_lb, chromatic_ub)): + for v in G: + Q[(v, c), (v, c)] += p + + return Q + + +@binary_quadratic_model_sampler(1) +def min_vertex_color(G, sampler=None, chromatic_lb=None, chromatic_ub=None, + **sampler_args): + """Returns an approximate minimum vertex coloring. + + Vertex coloring is the problem of assigning a color to the + vertices of a graph in a way that no adjacent vertices have the + same color. A minimum vertex coloring is the problem of solving + the vertex coloring problem using the smallest number of colors. + + Defines a QUBO [Dah2013]_ with ground states corresponding to minimum + vertex colorings and uses the sampler to sample from it. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum vertex coloring. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + chromatic_lb : int, optional + A lower bound on the chromatic number. If one is not provided, a + bound is calulcated. + + chromatic_ub : int, optional + An upper bound on the chromatic number. If one is not provided, a bound + is calculated. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + coloring : dict + A coloring for each vertex in G such that no adjacent nodes + share the same color. A dict of the form {node: color, ...} + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + """ + + Q = min_vertex_color_qubo(G, chromatic_lb=chromatic_lb, + chromatic_ub=chromatic_ub) + + # get the lowest energy sample + sample = sampler.sample_qubo(Q, **sampler_args).first.sample + + return {v: c for (v, c), val in sample.items() if val} + + +# legacy name, alias +min_vertex_coloring = min_vertex_color + + +def is_cycle(G): + """Determines whether the given graph is a cycle or circle graph. + + A cycle graph or circular graph is a graph that consists of a single cycle. + + https://en.wikipedia.org/wiki/Cycle_graph + + Parameters + ---------- + G : NetworkX graph + + Returns + ------- + is_cycle : bool + True if the graph consists of a single cycle. + + """ + if len(G) <= 2: + return False + + trailing, leading = next(iter(G.edges)) + start_node = trailing + + # travel around the graph, checking that each node has degree exactly two + # also track how many nodes were visited + n_visited = 1 + while leading != start_node: + neighbors = G[leading] + + if len(neighbors) != 2: + return False + + node1, node2 = neighbors + + if node1 == trailing: + trailing, leading = leading, node2 + else: + trailing, leading = leading, node1 + + n_visited += 1 + + # if we haven't visited all of the nodes, then it is not a connected cycle + return n_visited == len(G) + + +def is_vertex_coloring(G, coloring): + """Determines whether the given coloring is a vertex coloring of graph G. + + Parameters + ---------- + G : NetworkX graph + The graph on which the vertex coloring is applied. + + coloring : dict + A coloring of the nodes of G. Should be a dict of the form + {node: color, ...}. + + Returns + ------- + is_vertex_coloring : bool + True if the given coloring defines a vertex coloring; that is, no + two adjacent vertices share a color. + + Example + ------- + This example colors checks two colorings for a graph, G, of a single Chimera + unit cell. The first uses one color (0) for the four horizontal qubits + and another (1) for the four vertical qubits, in which case there are + no adjacencies; the second coloring swaps the color of one node. + + >>> G = dnx.chimera_graph(1,1,4) + >>> colors = {0: 0, 1: 0, 2: 0, 3: 0, 4: 1, 5: 1, 6: 1, 7: 1} + >>> dnx.is_vertex_coloring(G, colors) + True + >>> colors[4]=0 + >>> dnx.is_vertex_coloring(G, colors) + False + + """ + return all(coloring[u] != coloring[v] for u, v in G.edges) diff --git a/dwave_networkx/algorithms/cover.py b/dwave_networkx/algorithms/cover.py new file mode 100644 index 000000000..6abfa6c7e --- /dev/null +++ b/dwave_networkx/algorithms/cover.py @@ -0,0 +1,191 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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 dwave_networkx.algorithms.independent_set import maximum_weighted_independent_set +from dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ['min_weighted_vertex_cover', 'min_vertex_cover', 'is_vertex_cover'] + + +@binary_quadratic_model_sampler(2) +def min_weighted_vertex_cover(G, weight=None, sampler=None, lagrange=2.0, **sampler_args): + """Returns an approximate minimum weighted vertex cover. + + Defines a QUBO with ground states corresponding to a minimum weighted + vertex cover and uses the sampler to sample from it. + + A vertex cover is a set of vertices such that each edge of the graph + is incident with at least one vertex in the set. A minimum weighted + vertex cover is the vertex cover of minimum total node weight. + + Parameters + ---------- + G : NetworkX graph + + weight : string, optional (default None) + If None, every node has equal weight. If a string, use this node + attribute as the node weight. A node without this attribute is + assumed to have max weight. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + lagrange : optional (default 2) + Lagrange parameter to weight constraints versus objective. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + vertex_cover : list + List of nodes that the form a the minimum weighted vertex cover, as + determined by the given sampler. + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + https://en.wikipedia.org/wiki/Vertex_cover + + https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization + + References + ---------- + Based on the formulation presented in [Luc2014]_. + + """ + indep_nodes = set(maximum_weighted_independent_set(G, weight, sampler, lagrange, **sampler_args)) + return [v for v in G if v not in indep_nodes] + + +@binary_quadratic_model_sampler(1) +def min_vertex_cover(G, sampler=None, lagrange=2.0, **sampler_args): + """Returns an approximate minimum vertex cover. + + Defines a QUBO with ground states corresponding to a minimum + vertex cover and uses the sampler to sample from it. + + A vertex cover is a set of vertices such that each edge of the graph + is incident with at least one vertex in the set. A minimum vertex cover + is the vertex cover of smallest size. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum vertex cover. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + lagrange : optional (default 2) + Lagrange parameter to weight constraints versus objective. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + vertex_cover : list + List of nodes that form a minimum vertex cover, as + determined by the given sampler. + + Examples + -------- + This example uses a sampler from + `dimod `_ to find a minimum vertex + cover for a Chimera unit cell. Both the horizontal (vertices 0,1,2,3) and + vertical (vertices 4,5,6,7) tiles connect to all 16 edges, so repeated + executions can return either set. + + >>> import dwave_networkx as dnx + >>> import dimod + >>> sampler = dimod.ExactSolver() # small testing sampler + >>> G = dnx.chimera_graph(1, 1, 4) + >>> G.remove_node(7) # to give a unique solution + >>> dnx.min_vertex_cover(G, sampler, lagrange=2.0) + [4, 5, 6] + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + https://en.wikipedia.org/wiki/Vertex_cover + + https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization + + Lucas, A. (2014). Ising formulations of many NP problems. + Frontiers in Physics, Volume 2, Article 5. + + """ + return min_weighted_vertex_cover(G, None, sampler, lagrange, **sampler_args) + + +def is_vertex_cover(G, vertex_cover): + """Determines whether the given set of vertices is a vertex cover of graph G. + + A vertex cover is a set of vertices such that each edge of the graph + is incident with at least one vertex in the set. + + Parameters + ---------- + G : NetworkX graph + The graph on which to check the vertex cover. + + vertex_cover : + Iterable of nodes. + + Returns + ------- + is_cover : bool + True if the given iterable forms a vertex cover. + + Examples + -------- + This example checks two covers for a graph, G, of a single Chimera + unit cell. The first uses the set of the four horizontal qubits, which + do constitute a cover; the second set removes one node. + + >>> import dwave_networkx as dnx + >>> G = dnx.chimera_graph(1, 1, 4) + >>> cover = [0, 1, 2, 3] + >>> dnx.is_vertex_cover(G,cover) + True + >>> cover = [0, 1, 2] + >>> dnx.is_vertex_cover(G,cover) + False + + """ + cover = set(vertex_cover) + return all(u in cover or v in cover for u, v in G.edges) diff --git a/dwave_networkx/algorithms/elimination_ordering.py b/dwave_networkx/algorithms/elimination_ordering.py new file mode 100644 index 000000000..c47675e23 --- /dev/null +++ b/dwave_networkx/algorithms/elimination_ordering.py @@ -0,0 +1,957 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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. + +import itertools + +from random import random, sample + +import networkx as nx + +from dwave_networkx.generators.pegasus import pegasus_coordinates +from dwave_networkx.generators.zephyr import zephyr_coordinates +from dwave_networkx.generators.chimera import chimera_coordinates + +__all__ = ['is_almost_simplicial', + 'is_simplicial', + 'chimera_elimination_order', + 'pegasus_elimination_order', + 'zephyr_elimination_order', + 'max_cardinality_heuristic', + 'min_fill_heuristic', + 'min_width_heuristic', + 'treewidth_branch_and_bound', + 'minor_min_width', + 'elimination_order_width', + ] + + +def is_simplicial(G, n): + """Determines whether a node n in G is simplicial. + + Parameters + ---------- + G : NetworkX graph + The graph on which to check whether node n is simplicial. + n : node + A node in graph G. + + Returns + ------- + is_simplicial : bool + True if its neighbors form a clique. + + Examples + -------- + This example checks whether node 0 is simplicial for two graphs: G, a + single Chimera unit cell, which is bipartite, and K_5, the :math:`K_5` + complete graph. + + >>> G = dnx.chimera_graph(1, 1, 4) + >>> K_5 = nx.complete_graph(5) + >>> dnx.is_simplicial(G, 0) + False + >>> dnx.is_simplicial(K_5, 0) + True + + """ + return all(u in G[v] for u, v in itertools.combinations(G[n], 2)) + + +def is_almost_simplicial(G, n): + """Determines whether a node n in G is almost simplicial. + + Parameters + ---------- + G : NetworkX graph + The graph on which to check whether node n is almost simplicial. + n : node + A node in graph G. + + Returns + ------- + is_almost_simplicial : bool + True if all but one of its neighbors induce a clique + + Examples + -------- + This example checks whether node 0 is simplicial or almost simplicial for + a :math:`K_5` complete graph with one edge removed. + + >>> K_5 = nx.complete_graph(5) + >>> K_5.remove_edge(1,3) + >>> dnx.is_simplicial(K_5, 0) + False + >>> dnx.is_almost_simplicial(K_5, 0) + True + + """ + for w in G[n]: + if all(u in G[v] for u, v in itertools.combinations(G[n], 2) if u != w and v != w): + return True + return False + + +def minor_min_width(G): + """Computes a lower bound for the treewidth of graph G. + + Parameters + ---------- + G : NetworkX graph + The graph on which to compute a lower bound on the treewidth. + + Returns + ------- + lb : int + A lower bound on the treewidth. + + Examples + -------- + This example computes a lower bound for the treewidth of the :math:`K_7` + complete graph. + + >>> K_7 = nx.complete_graph(7) + >>> dnx.minor_min_width(K_7) + 6 + + References + ---------- + Based on the algorithm presented in [Gog2004]_. + + """ + # we need only deal with the adjacency structure of G. We will also + # be manipulating it directly so let's go ahead and make a new one + adj = {v: set(u for u in G[v] if u != v) for v in G} + + lb = 0 # lower bound on treewidth + while len(adj) > 1: + + # get the node with the smallest degree + v = min(adj, key=lambda v: len(adj[v])) + + # find the vertex u such that the degree of u is minimal in the neighborhood of v + neighbors = adj[v] + + if not neighbors: + # if v is a singleton, then we can just delete it + del adj[v] + continue + + def neighborhood_degree(u): + Gu = adj[u] + return sum(w in Gu for w in neighbors) + + u = min(neighbors, key=neighborhood_degree) + + # update the lower bound + new_lb = len(adj[v]) + if new_lb > lb: + lb = new_lb + + # contract the edge between u, v + adj[v] = adj[v].union(n for n in adj[u] if n != v) + for n in adj[v]: + adj[n].add(v) + for n in adj[u]: + adj[n].discard(u) + del adj[u] + + return lb + + +def min_fill_heuristic(G): + """Computes an upper bound on the treewidth of graph G based on + the min-fill heuristic for the elimination ordering. + + Parameters + ---------- + G : NetworkX graph + The graph on which to compute an upper bound for the treewidth. + + Returns + ------- + treewidth_upper_bound : int + An upper bound on the treewidth of the graph G. + + order : list + An elimination order that induces the treewidth. + + Examples + -------- + This example computes an upper bound for the treewidth of the :math:`K_4` + complete graph. + + >>> K_4 = nx.complete_graph(4) + >>> tw, order = dnx.min_fill_heuristic(K_4) + + References + ---------- + Based on the algorithm presented in [Gog2004]_. + + """ + # we need only deal with the adjacency structure of G. We will also + # be manipulating it directly so let's go ahead and make a new one + adj = {v: set(u for u in G[v] if u != v) for v in G} + + num_nodes = len(adj) + + # preallocate the return values + order = [0] * num_nodes + upper_bound = 0 + + for i in range(num_nodes): + # get the node that adds the fewest number of edges when eliminated from the graph + v = min(adj, key=lambda x: _min_fill_needed_edges(adj, x)) + + # if the number of neighbours of v is higher than upper_bound, update + dv = len(adj[v]) + if dv > upper_bound: + upper_bound = dv + + # make v simplicial by making its neighborhood a clique then remove the + # node + _elim_adj(adj, v) + order[i] = v + + return upper_bound, order + + +def _min_fill_needed_edges(adj, n): + # determines how many edges would needed to be added to G in order + # to make node n simplicial. + e = 0 # number of edges needed + for u, v in itertools.combinations(adj[n], 2): + if u not in adj[v]: + e += 1 + # We add random() which picks a value in the range [0., 1.). This is ok because the + # e are all integers. By adding a small random value, we randomize which node is + # chosen without affecting correctness. + return e + random() + + +def min_width_heuristic(G): + """Computes an upper bound on the treewidth of graph G based on + the min-width heuristic for the elimination ordering. + + Parameters + ---------- + G : NetworkX graph + The graph on which to compute an upper bound for the treewidth. + + Returns + ------- + treewidth_upper_bound : int + An upper bound on the treewidth of the graph G. + + order : list + An elimination order that induces the treewidth. + + Examples + -------- + This example computes an upper bound for the treewidth of the :math:`K_4` + complete graph. + + >>> K_4 = nx.complete_graph(4) + >>> tw, order = dnx.min_width_heuristic(K_4) + + References + ---------- + Based on the algorithm presented in [Gog2004]_. + + """ + # we need only deal with the adjacency structure of G. We will also + # be manipulating it directly so let's go ahead and make a new one + adj = {v: set(u for u in G[v] if u != v) for v in G} + + num_nodes = len(adj) + + # preallocate the return values + order = [0] * num_nodes + upper_bound = 0 + + for i in range(num_nodes): + # get the node with the smallest degree. We add random() which picks a value + # in the range [0., 1.). This is ok because the lens are all integers. By + # adding a small random value, we randomize which node is chosen without affecting + # correctness. + v = min(adj, key=lambda u: len(adj[u]) + random()) + + # if the number of neighbours of v is higher than upper_bound, update + dv = len(adj[v]) + if dv > upper_bound: + upper_bound = dv + + # make v simplicial by making its neighborhood a clique then remove the + # node + _elim_adj(adj, v) + order[i] = v + + return upper_bound, order + + +def max_cardinality_heuristic(G): + """Computes an upper bound on the treewidth of graph G based on + the max-cardinality heuristic for the elimination ordering. + + Parameters + ---------- + G : NetworkX graph + The graph on which to compute an upper bound for the treewidth. + + Returns + ------- + treewidth_upper_bound : int + An upper bound on the treewidth of the graph G. + + order : list + An elimination order that induces the treewidth. + + Examples + -------- + This example computes an upper bound for the treewidth of the :math:`K_4` + complete graph. + + >>> K_4 = nx.complete_graph(4) + >>> tw, order = dnx.max_cardinality_heuristic(K_4) + + References + ---------- + Based on the algorithm presented in [Gog2004]_. + + """ + # we need only deal with the adjacency structure of G. We will also + # be manipulating it directly so let's go ahead and make a new one + adj = {v: set(u for u in G[v] if u != v) for v in G} + + num_nodes = len(adj) + + # preallocate the return values + order = [0] * num_nodes + upper_bound = 0 + + # we will need to track the nodes and how many labelled neighbors + # each node has + labelled_neighbors = {v: 0 for v in adj} + + # working backwards + for i in range(num_nodes): + # pick the node with the most labelled neighbors + v = max(labelled_neighbors, key=lambda u: labelled_neighbors[u] + random()) + del labelled_neighbors[v] + + # increment all of its neighbors + for u in adj[v]: + if u in labelled_neighbors: + labelled_neighbors[u] += 1 + + order[-(i + 1)] = v + + for v in order: + # if the number of neighbours of v is higher than upper_bound, update + dv = len(adj[v]) + if dv > upper_bound: + upper_bound = dv + + # make v simplicial by making its neighborhood a clique then remove the node + # add v to order + _elim_adj(adj, v) + + return upper_bound, order + + +def _elim_adj(adj, n): + """eliminates a variable, acting on the adj matrix of G, + returning set of edges that were added. + + Parameters + ---------- + adj: dict + A dict of the form {v: neighbors, ...} where v are + vertices in a graph and neighbors is a set. + + Returns + ---------- + new_edges: set of edges that were added by eliminating v. + + """ + neighbors = adj[n] + new_edges = set() + for u, v in itertools.combinations(neighbors, 2): + if v not in adj[u]: + adj[u].add(v) + adj[v].add(u) + new_edges.add((u, v)) + new_edges.add((v, u)) + for v in neighbors: + adj[v].discard(n) + del adj[n] + return new_edges + + +def elimination_order_width(G, order): + """Calculates the width of the tree decomposition induced by a + variable elimination order. + + Parameters + ---------- + G : NetworkX graph + The graph on which to compute the width of the tree decomposition. + + order : list + The elimination order. Must be a list of all of the variables + in G. + + Returns + ------- + treewidth : int + The width of the tree decomposition induced by order. + + Examples + -------- + This example computes the width of the tree decomposition for the :math:`K_4` + complete graph induced by an elimination order found through the min-width + heuristic. + + >>> K_4 = nx.complete_graph(4) + >>> tw, order = dnx.min_width_heuristic(K_4) + >>> print(tw) + 3 + >>> dnx.elimination_order_width(K_4, order) + 3 + + + """ + # we need only deal with the adjacency structure of G. We will also + # be manipulating it directly so let's go ahead and make a new one + adj = {v: set(u for u in G[v] if u != v) for v in G} + + treewidth = 0 + + for v in order: + + # get the degree of the eliminated variable + try: + dv = len(adj[v]) + except KeyError: + raise ValueError('{} is in order but not in G'.format(v)) + + # the treewidth is the max of the current treewidth and the degree + if dv > treewidth: + treewidth = dv + + # eliminate v by making it simplicial (acts on adj in place) + _elim_adj(adj, v) + + # if adj is not empty, then order did not include all of the nodes in G. + if adj: + raise ValueError('not all nodes in G were in order') + + return treewidth + + +def treewidth_branch_and_bound(G, elimination_order=None, treewidth_upperbound=None): + """Computes the treewidth of graph G and a corresponding perfect elimination ordering. + + Algorithm based on [Gog2004]_. + + Parameters + ---------- + G : NetworkX graph + The graph on which to compute the treewidth and perfect elimination ordering. + + elimination_order: list (optional, Default None) + An elimination order used as an initial best-known order. If a good + order is provided, it may speed up computation. If not provided, the + initial order is generated using the min-fill heuristic. + + treewidth_upperbound : int (optional, Default None) + An upper bound on the treewidth. Note that using + this parameter can result in no returned order. + + Returns + ------- + treewidth : int + The treewidth of graph G. + order : list + An elimination order that induces the treewidth. + + Examples + -------- + This example computes the treewidth for the :math:`K_7` + complete graph using an optionally provided elimination order (a sequential + ordering of the nodes, arbitrally chosen). + + >>> K_7 = nx.complete_graph(7) + >>> dnx.treewidth_branch_and_bound(K_7, [0, 1, 2, 3, 4, 5, 6]) + (6, [0, 1, 2, 3, 4, 5, 6]) + + References + ---------- + Based on the algorithm presented in [Gog2004]_. + """ + # empty graphs have treewidth 0 and the nodes can be eliminated in + # any order + if not any(G[v] for v in G): + return 0, list(G) + + # variable names are chosen to match the paper + + # our order will be stored in vector x, named to be consistent with + # the paper + x = [] # the partial order + + f = minor_min_width(G) # our current lower bound guess, f(s) in the paper + g = 0 # g(s) in the paper + + # we need the best current update we can find. + ub, order = min_fill_heuristic(G) + + # if the user has provided an upperbound or an elimination order, check those against + # our current best guess + if elimination_order is not None: + upperbound = elimination_order_width(G, elimination_order) + if upperbound <= ub: + ub, order = upperbound, elimination_order + + if treewidth_upperbound is not None and treewidth_upperbound < ub: + # in this case the order might never be found + ub, order = treewidth_upperbound, [] + + # best found encodes the ub and the order + best_found = ub, order + + # if our upper bound is the same as f, then we are done! Otherwise begin the + # algorithm. + if f < ub: + # we need only deal with the adjacency structure of G. We will also + # be manipulating it directly so let's go ahead and make a new one + adj = {v: set(u for u in G[v] if u != v) for v in G} + + best_found = _branch_and_bound(adj, x, g, f, best_found) + elif f > ub and treewidth_upperbound is None: + raise RuntimeError("logic error") + + return best_found + + +def _branch_and_bound(adj, x, g, f, best_found, skipable=set(), theorem6p2=None): + """ Recursive branch and bound for computing treewidth of a subgraph. + adj: adjacency list + x: partial elimination order + g: width of x so far + f: lower bound on width of any elimination order starting with x + best_found = ub,order: best upper bound on the treewidth found so far, and its elimination order + skipable: vertices that can be skipped according to Lemma 5.3 + theorem6p2: terms that have been explored/can be pruned according to Theorem 6.2 + """ + + # theorem6p2 checks for branches that can be pruned using Theorem 6.2 + if theorem6p2 is None: + theorem6p2 = _theorem6p2() + prune6p2, explored6p2, finished6p2 = theorem6p2 + # current6p2 is the list of prunable terms created during this instantiation of _branch_and_bound. + # These terms will only be use during this call and its successors, + # so they are removed before the function terminates. + current6p2 = list() + + # theorem6p4 checks for branches that can be pruned using Theorem 6.4. + # These terms do not need to be passed to successive calls to _branch_and_bound, + # so they are simply created and deleted during this call. + prune6p4, explored6p4 = _theorem6p4() + + # Note: theorem6p1 and theorem6p3 are a pruning strategies that are currently disabled + # # as they does not appear to be invoked regularly, + # and invoking it can require large memory allocations. + # This can be fixed in the future if there is evidence that it's useful. + # To add them in, define _branch_and_bound as follows: + # def _branch_and_bound(adj, x, g, f, best_found, skipable=set(), theorem6p1=None, + # theorem6p2=None, theorem6p3=None): + + # if theorem6p1 is None: + # theorem6p1 = _theorem6p1() + # prune6p1, explored6p1 = theorem6p1 + + # if theorem6p3 is None: + # theorem6p3 = _theorem6p3() + # prune6p3, explored6p3 = theorem6p3 + + # we'll need to know our current upper bound in several places + ub, order = best_found + + # ok, take care of the base case first + if len(adj) < 2: + # check if our current branch is better than the best we've already + # found and if so update our best solution accordingly. + if f < ub: + return (f, x + list(adj)) + elif f == ub and not order: + return (f, x + list(adj)) + else: + return best_found + + # so we have not yet reached the base case + # Note: theorem 6.4 gives a heuristic for choosing order of n in adj. + # Quick_bb suggests using a min-fill or random order. + # We don't need to consider the neighbors of the last vertex eliminated + sorted_adj = sorted((n for n in adj if n not in skipable), key=lambda x: _min_fill_needed_edges(adj, x)) + for n in sorted_adj: + + g_s = max(g, len(adj[n])) + + # according to Lemma 5.3, we can skip all of the neighbors of the last + # variable eliniated when choosing the next variable + # this does not get altered so we don't need a copy + next_skipable = adj[n] + + if prune6p2(x, n, next_skipable): + continue + + # update the state by eliminating n and adding it to the partial ordering + adj_s = {v: adj[v].copy() for v in adj} # create a new object + edges_n = _elim_adj(adj_s, n) + x_s = x + [n] # new partial ordering + + # pruning (disabled): + # if prune6p1(x_s): + # continue + + if prune6p4(edges_n): + continue + + # By Theorem 5.4, if any two vertices have ub + 1 common neighbors then + # we can add an edge between them + _theorem5p4(adj_s, ub) + + # ok, let's update our values + f_s = max(g_s, minor_min_width(adj_s)) + + g_s, f_s, as_list = _graph_reduction(adj_s, x_s, g_s, f_s) + + # pruning (disabled): + # if prune6p3(x, as_list, n): + # continue + + if f_s < ub: + best_found = _branch_and_bound(adj_s, x_s, g_s, f_s, best_found, + next_skipable, theorem6p2=theorem6p2) + # if theorem6p1, theorem6p3 are enabled, this should be called as: + # best_found = _branch_and_bound(adj_s, x_s, g_s, f_s, best_found, + # next_skipable, theorem6p1=theorem6p1, + # theorem6p2=theorem6p2,theorem6p3=theorem6p3) + ub, __ = best_found + + # store some information for pruning (disabled): + # explored6p3(x, n, as_list) + + prunable = explored6p2(x, n, next_skipable) + current6p2.append(prunable) + + explored6p4(edges_n) + + # store some information for pruning (disabled): + # explored6p1(x) + + for prunable in current6p2: + finished6p2(prunable) + + return best_found + + +def _graph_reduction(adj, x, g, f): + """we can go ahead and remove any simplicial or almost-simplicial vertices from adj. + """ + as_list = set() + as_nodes = {v for v in adj if len(adj[v]) <= f and is_almost_simplicial(adj, v)} + while as_nodes: + as_list.union(as_nodes) + for n in as_nodes: + + # update g and f + dv = len(adj[n]) + if dv > g: + g = dv + if g > f: + f = g + + # eliminate v + x.append(n) + _elim_adj(adj, n) + + # see if we have any more simplicial nodes + as_nodes = {v for v in adj if len(adj[v]) <= f and is_almost_simplicial(adj, v)} + + return g, f, as_list + + +def _theorem5p4(adj, ub): + """By Theorem 5.4, if any two vertices have ub + 1 common neighbors + then we can add an edge between them. + """ + new_edges = set() + for u, v in itertools.combinations(adj, 2): + if u in adj[v]: + # already an edge + continue + + if len(adj[u].intersection(adj[v])) > ub: + new_edges.add((u, v)) + + while new_edges: + for u, v in new_edges: + adj[u].add(v) + adj[v].add(u) + + new_edges = set() + for u, v in itertools.combinations(adj, 2): + if u in adj[v]: + continue + + if len(adj[u].intersection(adj[v])) > ub: + new_edges.add((u, v)) + + +def _theorem6p1(): + """See Theorem 6.1 in paper.""" + + pruning_set = set() + + def _prune(x): + if len(x) <= 2: + return False + # this is faster than tuple(x[-3:]) + key = (tuple(x[:-2]), x[-2], x[-1]) + return key in pruning_set + + def _explored(x): + if len(x) >= 3: + prunable = (tuple(x[:-2]), x[-1], x[-2]) + pruning_set.add(prunable) + + return _prune, _explored + + +def _theorem6p2(): + """See Theorem 6.2 in paper. + Prunes (x,...,a) when (x,a) is explored and a has the same neighbour set in both graphs. + """ + pruning_set2 = set() + + def _prune2(x, a, nbrs_a): + frozen_nbrs_a = frozenset(nbrs_a) + for i in range(len(x)): + key = (tuple(x[0:i]), a, frozen_nbrs_a) + if key in pruning_set2: + return True + return False + + def _explored2(x, a, nbrs_a): + prunable = (tuple(x), a, frozenset(nbrs_a)) # (s,a,N(a)) + pruning_set2.add(prunable) + return prunable + + def _finished2(prunable): + pruning_set2.remove(prunable) + + return _prune2, _explored2, _finished2 + + +def _theorem6p3(): + """See Theorem 6.3 in paper. + Prunes (s,b) when (s,a) is explored, b (almost) simplicial in (s,a), and a (almost) simplicial in (s,b) + """ + pruning_set3 = set() + + def _prune3(x, as_list, b): + for a in as_list: + key = (tuple(x), a, b) # (s,a,b) with (s,a) explored + if key in pruning_set3: + return True + return False + + def _explored3(x, a, as_list): + for b in as_list: + prunable = (tuple(x), a, b) # (s,a,b) with (s,a) explored + pruning_set3.add(prunable) + + return _prune3, _explored3 + + +def _theorem6p4(): + """See Theorem 6.4 in paper. + Let E(x) denote the edges added when eliminating x. (edges_x below). + Prunes (s,b) when (s,a) is explored and E(a) is a subset of E(b). + For this theorem we only record E(a) rather than (s,E(a)) + because we only need to check for pruning in the same s context + (i.e the same level of recursion). + """ + pruning_set4 = list() + + def _prune4(edges_b): + for edges_a in pruning_set4: + if edges_a.issubset(edges_b): + return True + return False + + def _explored4(edges_a): + pruning_set4.append(edges_a) # (s,E_a) with (s,a) explored + + return _prune4, _explored4 + + +def chimera_elimination_order(m, n=None, t=4, coordinates=False): + """Provides a variable elimination order for a Chimera graph. + + A graph defined by ``chimera_graph(m,n,t)`` has treewidth :math:`max(m,n)*t`. + This function outputs a variable elimination order inducing a tree + decomposition of that width. + + Parameters + ---------- + m : int + Number of rows in the Chimera lattice. + n : int (optional, default m) + Number of columns in the Chimera lattice. + t : int (optional, default 4) + Size of the shore within each Chimera tile. + coordinates bool (optional, default False): + If True, the elimination order is given in terms of 4-term Chimera + coordinates, otherwise given in linear indices. + + Returns + ------- + order : list + An elimination order that induces the treewidth of chimera_graph(m,n,t). + + Examples + -------- + + >>> G = dnx.chimera_elimination_order(1, 1, 4) # a single Chimera tile + + """ + if n is None: + n = m + + index_flip = m > n + if index_flip: + m, n = n, m + + def chimeraI(m0, n0, k0, l0): + if index_flip: + return m*2*t*n0 + 2*t*m0 + t*(1-k0) + l0 + else: + return n*2*t*m0 + 2*t*n0 + t*k0 + l0 + + order = [] + + for n_i in range(n): + for t_i in range(t): + for m_i in range(m): + order.append(chimeraI(m_i, n_i, 0, t_i)) + + for n_i in range(n): + for m_i in range(m): + for t_i in range(t): + order.append(chimeraI(m_i, n_i, 1, t_i)) + + if coordinates: + return list(chimera_coordinates(m,n,t).iter_linear_to_chimera(order)) + else: + return order + + +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` [Boo2019]_. + + Simple pegasus variable elimination order rules: + + - eliminate vertical qubits, one column at a time + - eliminate horizontal qubits in each column once their adjacent vertical + qubits have been eliminated + + Args + ---- + n : int + The size parameter for the Pegasus lattice. + + coordinates : bool, optional (default False) + If True, the elimination order is given in terms of 4-term Pegasus + coordinates, otherwise given in linear indices. + + Returns + ------- + order : list + An elimination order that provides an upper bound on the treewidth. + + """ + m = n + l = 12 + + # ordering for horizontal qubits in each tile, from east to west: + h_order = [4, 5, 6, 7, 0, 1, 2, 3, 8, 9, 10, 11] + order = [] + for n_i in range(n): # for each tile offset + # eliminate vertical qubits: + for l_i in range(0, l, 2): + for l_v in range(l_i, l_i + 2): + for m_i in range(m - 1): # for each column + order.append((0, n_i, l_v, m_i)) + # eliminate horizontal qubits: + if n_i > 0 and not(l_i % 4): + # a new set of horizontal qubits have had all their neighbouring vertical qubits eliminated. + for m_i in range(m): + for l_h in range(h_order[l_i], h_order[l_i] + 4): + order.append((1, m_i, l_h, n_i - 1)) + + if coordinates: + return order + else: + return list(pegasus_coordinates(n).iter_pegasus_to_linear(order)) + + +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` [Boo2021]_. + + Simple zephyr variable elimination rules: + - eliminate vertical qubits, one column at a time + - eliminate horizontal qubits in each column from top to bottom + + Args + ---- + m : int + Grid parameter for the Zephyr lattice. + t : int + Tile parameter for the Zephyr lattice. + coordinates : bool, optional (default False) + If True, the elimination order is given in terms of 4-term Zephyr + coordinates, otherwise given in linear indices. + + Returns + ------- + order : list + An elimination order that achieves an upper bound on the treewidth. + + """ + 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)]) + + if coordinates: + return order + else: + return list(zephyr_coordinates(m).iter_zephyr_to_linear(order)) + diff --git a/dwave_networkx/algorithms/independent_set.py b/dwave_networkx/algorithms/independent_set.py new file mode 100644 index 000000000..798437ceb --- /dev/null +++ b/dwave_networkx/algorithms/independent_set.py @@ -0,0 +1,265 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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 dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ["maximum_weighted_independent_set", + "maximum_weighted_independent_set_qubo", + "maximum_independent_set", + "is_independent_set", + ] + + +@binary_quadratic_model_sampler(2) +def maximum_weighted_independent_set(G, weight=None, sampler=None, lagrange=2.0, **sampler_args): + """Returns an approximate maximum weighted independent set. + + Defines a QUBO with ground states corresponding to a + maximum weighted independent set and uses the sampler to sample + from it. + + An independent set is a set of nodes such that the subgraph + of G induced by these nodes contains no edges. A maximum + independent set is an independent set of maximum total node weight. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a maximum cut weighted independent set. + + weight : string, optional (default None) + If None, every node has equal weight. If a string, use this node + attribute as the node weight. A node without this attribute is + assumed to have max weight. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + lagrange : optional (default 2) + Lagrange parameter to weight constraints (no edges within set) + versus objective (largest set possible). + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + indep_nodes : list + List of nodes that form a maximum weighted independent set, as + determined by the given sampler. + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + + `Independent Set on Wikipedia `_ + + `QUBO on Wikipedia `_ + + Lucas, A. (2014). Ising formulations of many NP problems. + Frontiers in Physics, Volume 2, Article 5. + + """ + # Get a QUBO representation of the problem + Q = maximum_weighted_independent_set_qubo(G, weight, lagrange) + + # use the sampler to find low energy states + response = sampler.sample_qubo(Q, **sampler_args) + + # we want the lowest energy sample + sample = next(iter(response)) + + # nodes that are spin up or true are exactly the ones in S. + return [node for node in sample if sample[node] > 0] + + +@binary_quadratic_model_sampler(1) +def maximum_independent_set(G, sampler=None, lagrange=2.0, **sampler_args): + """Returns an approximate maximum independent set. + + Defines a QUBO with ground states corresponding to a + maximum independent set and uses the sampler to sample from + it. + + An independent set is a set of nodes such that the subgraph + of G induced by these nodes contains no edges. A maximum + independent set is an independent set of largest possible size. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a maximum cut independent set. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + lagrange : optional (default 2) + Lagrange parameter to weight constraints (no edges within set) + versus objective (largest set possible). + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + indep_nodes : list + List of nodes that form a maximum independent set, as + determined by the given sampler. + + Example + ------- + This example uses a sampler from + `dimod `_ to find a maximum + independent set for a graph of a Chimera unit cell created using the + `chimera_graph()` function. + + >>> import dimod + >>> sampler = dimod.SimulatedAnnealingSampler() + >>> G = dnx.chimera_graph(1, 1, 4) + >>> indep_nodes = dnx.maximum_independent_set(G, sampler) + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + + `Independent Set on Wikipedia `_ + + `QUBO on Wikipedia `_ + + Lucas, A. (2014). Ising formulations of many NP problems. + Frontiers in Physics, Volume 2, Article 5. + + """ + return maximum_weighted_independent_set(G, None, sampler, lagrange, **sampler_args) + + +def is_independent_set(G, indep_nodes): + """Determines whether the given nodes form an independent set. + + An independent set is a set of nodes such that the subgraph + of G induced by these nodes contains no edges. + + Parameters + ---------- + G : NetworkX graph + The graph on which to check the independent set. + + indep_nodes : list + List of nodes that form a maximum independent set, as + determined by the given sampler. + + Returns + ------- + is_independent : bool + True if indep_nodes form an independent set. + + Example + ------- + This example checks two sets of nodes, both derived from a + single Chimera unit cell, for an independent set. The first set is + the horizontal tile's nodes; the second has nodes from the horizontal and + verical tiles. + + >>> import dwave_networkx as dnx + >>> G = dnx.chimera_graph(1, 1, 4) + >>> dnx.is_independent_set(G, [0, 1, 2, 3]) + True + >>> dnx.is_independent_set(G, [0, 4]) + False + + """ + return len(G.subgraph(indep_nodes).edges) == 0 + + +def maximum_weighted_independent_set_qubo(G, weight=None, lagrange=2.0): + """Return the QUBO with ground states corresponding to a maximum weighted independent set. + + Parameters + ---------- + G : NetworkX graph + + weight : string, optional (default None) + If None, every node has equal weight. If a string, use this node + attribute as the node weight. A node without this attribute is + assumed to have max weight. + + lagrange : optional (default 2) + Lagrange parameter to weight constraints (no edges within set) + versus objective (largest set possible). + + Returns + ------- + QUBO : dict + The QUBO with ground states corresponding to a maximum weighted independent set. + + Examples + -------- + + >>> from dwave_networkx.algorithms.independent_set import maximum_weighted_independent_set_qubo + ... + >>> G = nx.path_graph(3) + >>> Q = maximum_weighted_independent_set_qubo(G, weight='weight', lagrange=2.0) + >>> Q[(0, 0)] + -1.0 + >>> Q[(1, 1)] + -1.0 + >>> Q[(0, 1)] + 2.0 + + """ + + # empty QUBO for an empty graph + if not G: + return {} + + # We assume that the sampler can handle an unstructured QUBO problem, so let's set one up. + # Let us define the largest independent set to be S. + # For each node n in the graph, we assign a boolean variable v_n, where v_n = 1 when n + # is in S and v_n = 0 otherwise. + # We call the matrix defining our QUBO problem Q. + # On the diagnonal, we assign the linear bias for each node to be the negative of its weight. + # This means that each node is biased towards being in S. Weights are scaled to a maximum of 1. + # Negative weights are considered 0. + # On the off diagnonal, we assign the off-diagonal terms of Q to be 2. Thus, if both + # nodes are in S, the overall energy is increased by 2. + cost = dict(G.nodes(data=weight, default=1)) + scale = max(cost.values()) + Q = {(node, node): min(-cost[node] / scale, 0.0) for node in G} + Q.update({edge: lagrange for edge in G.edges}) + + return Q diff --git a/dwave_networkx/algorithms/markov.py b/dwave_networkx/algorithms/markov.py new file mode 100644 index 000000000..22a7d970e --- /dev/null +++ b/dwave_networkx/algorithms/markov.py @@ -0,0 +1,213 @@ +# Copyright 2019 D-Wave Systems Inc. +# +# 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. + +import dimod + +from dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ['sample_markov_network', 'markov_network_bqm'] + + +############################################################################### +# The following code is partially based on https://github.com/tbabej/gibbs +# +# MIT License +# =========== +# +# Copyright 2017 Tomas Babej +# https://github.com/tbabej/gibbs +# +# This software is released under MIT licence. +# +# Permission is hereby granted, free of charge, to any person obtaining +# a copy of this software and associated documentation files (the +# "Software"), to deal in the Software without restriction, including +# without limitation the rights to use, copy, modify, merge, publish, +# distribute, sublicense, and/or sell copies of the Software, and to +# permit persons to whom the Software is furnished to do so, subject to +# the following conditions: +# +# The above copyright notice and this permission notice shall be +# included in all copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. +# + + +@binary_quadratic_model_sampler(1) +def sample_markov_network(MN, sampler=None, fixed_variables=None, + return_sampleset=False, + **sampler_args): + """Samples from a markov network using the provided sampler. + + Parameters + ---------- + G : NetworkX graph + A Markov Network as returned by :func:`.markov_network` + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + fixed_variables : dict + A dictionary of variable assignments to be fixed in the markov network. + + return_sampleset : bool (optional, default=False) + If True, returns a :obj:`dimod.SampleSet` rather than a list of samples. + + **sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + samples : list[dict]/:obj:`dimod.SampleSet` + A list of samples ordered from low-to-high energy or a sample set. + + Examples + -------- + + >>> import dimod + ... + >>> potentials = {('a', 'b'): {(0, 0): -1, + ... (0, 1): .5, + ... (1, 0): .5, + ... (1, 1): 2}} + >>> MN = dnx.markov_network(potentials) + >>> sampler = dimod.ExactSolver() + >>> samples = dnx.sample_markov_network(MN, sampler) + >>> samples[0] # doctest: +SKIP + {'a': 0, 'b': 0} + + >>> import dimod + ... + >>> potentials = {('a', 'b'): {(0, 0): -1, + ... (0, 1): .5, + ... (1, 0): .5, + ... (1, 1): 2}} + >>> MN = dnx.markov_network(potentials) + >>> sampler = dimod.ExactSolver() + >>> samples = dnx.sample_markov_network(MN, sampler, return_sampleset=True) + >>> samples.first # doctest: +SKIP + Sample(sample={'a': 0, 'b': 0}, energy=-1.0, num_occurrences=1) + + >>> import dimod + ... + >>> potentials = {('a', 'b'): {(0, 0): -1, + ... (0, 1): .5, + ... (1, 0): .5, + ... (1, 1): 2}, + ... ('b', 'c'): {(0, 0): -9, + ... (0, 1): 1.2, + ... (1, 0): 7.2, + ... (1, 1): 5}} + >>> MN = dnx.markov_network(potentials) + >>> sampler = dimod.ExactSolver() + >>> samples = dnx.sample_markov_network(MN, sampler, fixed_variables={'b': 0}) + >>> samples[0] # doctest: +SKIP + {'a': 0, 'c': 0, 'b': 0} + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + """ + + bqm = markov_network_bqm(MN) + + if fixed_variables: + # we can modify in-place since we just made it + bqm.fix_variables(fixed_variables) + + sampleset = sampler.sample(bqm, **sampler_args) + + if fixed_variables: + # add the variables back in + sampleset = dimod.append_variables(sampleset, fixed_variables) + + if return_sampleset: + return sampleset + else: + return list(map(dict, sampleset.samples())) + + +def markov_network_bqm(MN): + """Construct a binary quadratic model for a markov network. + + + Parameters + ---------- + G : NetworkX graph + A Markov Network as returned by :func:`.markov_network` + + Returns + ------- + bqm : :obj:`dimod.BinaryQuadraticModel` + A binary quadratic model. + + """ + + bqm = dimod.BinaryQuadraticModel.empty(dimod.BINARY) + + # the variable potentials + for v, ddict in MN.nodes(data=True, default=None): + potential = ddict.get('potential', None) + + if potential is None: + continue + + # for single nodes we don't need to worry about order + + phi0 = potential[(0,)] + phi1 = potential[(1,)] + + bqm.add_variable(v, phi1 - phi0) + bqm.offset += phi0 + + # the interaction potentials + for u, v, ddict in MN.edges(data=True, default=None): + potential = ddict.get('potential', None) + + if potential is None: + continue + + # in python<=3.5 the edge order might not be consistent so we use the + # one that was stored + order = ddict['order'] + u, v = order + + phi00 = potential[(0, 0)] + phi01 = potential[(0, 1)] + phi10 = potential[(1, 0)] + phi11 = potential[(1, 1)] + + bqm.add_variable(u, phi10 - phi00) + bqm.add_variable(v, phi01 - phi00) + bqm.add_interaction(u, v, phi11 - phi10 - phi01 + phi00) + bqm.offset += phi00 + + return bqm diff --git a/dwave_networkx/algorithms/matching.py b/dwave_networkx/algorithms/matching.py new file mode 100644 index 000000000..4b32ce85a --- /dev/null +++ b/dwave_networkx/algorithms/matching.py @@ -0,0 +1,351 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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. + +import itertools +import numbers +import warnings + +import dimod +import networkx as nx + +from dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ['is_matching', + 'is_maximal_matching', + 'matching_bqm', + 'maximal_matching_bqm', + 'min_maximal_matching', + 'min_maximal_matching_bqm', + ] + + +def matching_bqm(G): + """Find a binary quadratic model for the graph's matchings. + + A matching is a subset of edges in which no node occurs more than + once. This function returns a binary quadratic model (BQM) with ground + states corresponding to the possible matchings of G. + + Finding valid matchings can be done in polynomial time, so finding matching + with BQMs is generally inefficient. + This BQM may be useful when combined with other constraints and objectives. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a matching. + + Returns + ------- + bqm : :class:`dimod.BinaryQuadraticModel` + A binary quadratic model with ground states corresponding to a + matching. The variables of the BQM are the edges of `G` as frozensets. + The BQM's ground state energy is 0 by construction. + The energy of the first excited state is 1. + + """ + bqm = dimod.BinaryQuadraticModel.empty('BINARY') + + # add the edges of G as variables + for edge in G.edges: + bqm.add_variable(frozenset(edge), 0) + + for node in G: + for edge0, edge1 in itertools.combinations(G.edges(node), 2): + u = frozenset(edge0) + v = frozenset(edge1) + bqm.add_interaction(u, v, 1) + + return bqm + + +def maximal_matching_bqm(G, lagrange=None): + """Find a binary quadratic model for the graph's maximal matchings. + + A matching is a subset of edges in which no node occurs more than + once. A maximal matching is one in which no edges from G can be + added without violating the matching rule. + This function returns a binary quadratic model (BQM) with ground + states corresponding to the possible maximal matchings of G. + + Finding maximal matchings can be done in polynomial time, so finding + maximal matching with BQMs is generally inefficient. + This BQM may be useful when combined with other constraints and objectives. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a maximal matching. + + lagrange : float (optional) + The Lagrange multiplier for the matching constraint. Should be positive + and greater than `max_degree - 2`. + Defaults to `1.25 * (max_degree - 2)`. + + Returns + ------- + bqm : :class:`dimod.BinaryQuadraticModel` + A binary quadratic model with ground states corresponding to a maximal + matching. The variables of the BQM are the edges of `G` as frozensets. + The BQM's ground state energy is 0 by construction. + + """ + bqm = matching_bqm(G) + + if lagrange is None: + delta = max((G.degree[v] for v in G), default=0) + lagrange = max(1.25 * (delta - 2), 1) + + bqm.scale(lagrange) + + for node0, node1 in G.edges: + # (1 - y_v - y_u + y_v*y_u) <- see paper + + bqm.offset += 1 + + for edge in G.edges(node0): + bqm.linear[frozenset(edge)] -= 1 + + for edge in G.edges(node1): + bqm.linear[frozenset(edge)] -= 1 + + for edge0 in G.edges(node0): + u = frozenset(edge0) + for edge1 in G.edges(node1): + v = frozenset(edge1) + if u == v: + bqm.linear[u] += 1 + else: + bqm.add_interaction(u, v, 1) + + return bqm + + +def min_maximal_matching_bqm(G, maximal_lagrange=2, matching_lagrange=None): + """Find a binary quadratic model for the graph's minimum maximal matchings. + + A matching is a subset of edges in which no node occurs more than + once. A maximal matching is one in which no edges from G can be + added without violating the matching rule. A minimum maximal matching + is a maximal matching that contains the smallest possible number of edges. + This function returns a binary quadratic model (BQM) with ground + states corresponding to the possible maximal matchings of G. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum maximal matching. + + maximal_lagrange : float (optional, default=2) + The Lagrange multiplier for the maximal constraint. Should be greater + than 1. + + matching_lagrange : float (optional) + The Lagrange multiplier for the matching constraint. Should be positive + and greater than `maximal_lagrange * max_degree - 2`. + Defaults to `1.25 * (maximal_lagrange * max_degree - 2)`. + + Returns + ------- + bqm : :class:`dimod.BinaryQuadraticModel` + A binary quadratic model with ground states corresponding to a + minimum maximal matching. The variables of the BQM are the edges + of `G` as frozensets. + + """ + + if matching_lagrange is not None: + # we're going to scale the bqm by maximal_matching so undo that + # for maximal_lagrange + matching_lagrange /= maximal_lagrange + bqm = maximal_matching_bqm(G, lagrange=matching_lagrange) + bqm.scale(maximal_lagrange) + + for v in bqm.variables: + bqm.linear[v] += 1 + + return bqm + + +@binary_quadratic_model_sampler(1) +def maximal_matching(G, sampler=None, **sampler_args): + """Finds an approximate maximal matching. + + Defines a QUBO with ground states corresponding to a maximal + matching and uses the sampler to sample from it. + + A matching is a subset of edges in which no node occurs more than + once. A maximal matching is one in which no edges from G can be + added without violating the matching rule. + + Finding maximal matchings can be done is polynomial time, so this method + is only useful pedagogically. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a maximal matching. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + matching : set + A maximal matching of the graph. + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + + `Matching on Wikipedia `_ + + `QUBO on Wikipedia `_ + + Based on the formulation presented in [Luc2014]_. + + """ + if not G.edges: + return set() + + bqm = maximal_matching_bqm(G) + sampleset = sampler.sample(bqm, **sampler_args) + sample = sampleset.first.sample + + # the matching are the edges that are 1 in the sample + return set(tuple(edge) for edge, val in sample.items() if val > 0) + + +@binary_quadratic_model_sampler(1) +def min_maximal_matching(G, sampler=None, **sampler_args): + """Returns an approximate minimum maximal matching. + + Defines a QUBO with ground states corresponding to a minimum + maximal matching and uses the sampler to sample from it. + + A matching is a subset of edges in which no node occurs more than + once. A maximal matching is one in which no edges from G can be + added without violating the matching rule. A minimum maximal + matching is the smallest maximal matching for G. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum maximal matching. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + matching : set + A minimum maximal matching of the graph. + + Example + ------- + This example uses a sampler from + `dimod `_ to find a minimum maximal + matching for a Chimera unit cell. + + >>> import dimod + >>> sampler = dimod.ExactSolver() + >>> G = dnx.chimera_graph(1, 1, 4) + >>> matching = dnx.min_maximal_matching(G, sampler) + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + + `Matching on Wikipedia `_ + + `QUBO on Wikipedia `_ + + Lucas, A. (2014). Ising formulations of many NP problems. + Frontiers in Physics, Volume 2, Article 5. + + """ + if not G.edges: + return set() + + bqm = min_maximal_matching_bqm(G) + sampleset = sampler.sample(bqm, **sampler_args) + sample = sampleset.first.sample + + # the matching are the edges that are 1 in the sample + return set(tuple(edge) for edge, val in sample.items() if val > 0) + + +def is_matching(edges): + """Determine whether the given set of edges is a matching. + + Deprecated in favour of :func:`networkx.is_matching`. + """ + warnings.warn("This method is deprecated, please use NetworkX's" + "nx.is_matching(G, edges) rather than dwave-networkx's " + "dnx.is_matching(edges)", DeprecationWarning, stacklevel=2) + return len(set().union(*edges)) == len(edges) * 2 + + +def is_maximal_matching(G, matching): + """Determine whether the given set of edges is a maximal matching. + + Deprecated in favour of :func:`networkx.is_matching`. + """ + warnings.warn("This method is deprecated, please use NetworkX's" + "nx.is_maximal_matching(G, edges) rather than " + "dwave-networkx's dnx.is_maximal_matching(G, edges)", + DeprecationWarning, stacklevel=2) + touched_nodes = set().union(*matching) + + # first check if a matching + if len(touched_nodes) != len(matching) * 2: + return False + + # now for each edge, check that at least one of its variables is + # already in the matching + for (u, v) in G.edges: + if u not in touched_nodes and v not in touched_nodes: + return False + + return True diff --git a/dwave_networkx/algorithms/max_cut.py b/dwave_networkx/algorithms/max_cut.py new file mode 100644 index 000000000..6c7767628 --- /dev/null +++ b/dwave_networkx/algorithms/max_cut.py @@ -0,0 +1,143 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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 dwave_networkx.exceptions import DWaveNetworkXException +from dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ["maximum_cut", "weighted_maximum_cut"] + + +@binary_quadratic_model_sampler(1) +def maximum_cut(G, sampler=None, **sampler_args): + """Returns an approximate maximum cut. + + Defines an Ising problem with ground states corresponding to + a maximum cut and uses the sampler to sample from it. + + A maximum cut is a subset S of the vertices of G such that + the number of edges between S and the complementary subset + is as large as possible. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a maximum cut. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + S : set + A maximum cut of G. + + Example + ------- + This example uses a sampler from + `dimod `_ to find a maximum cut + for a graph of a Chimera unit cell created using the `chimera_graph()` + function. + + >>> import dimod + ... + >>> sampler = dimod.SimulatedAnnealingSampler() + >>> G = dnx.chimera_graph(1, 1, 4) + >>> cut = dnx.maximum_cut(G, sampler) + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + """ + # In order to form the Ising problem, we want to increase the + # energy by 1 for each edge between two nodes of the same color. + # The linear biases can all be 0. + h = {v: 0. for v in G} + J = {(u, v): 1 for u, v in G.edges} + + # draw the lowest energy sample from the sampler + response = sampler.sample_ising(h, J, **sampler_args) + sample = next(iter(response)) + + return set(v for v in G if sample[v] >= 0) + + +@binary_quadratic_model_sampler(1) +def weighted_maximum_cut(G, sampler=None, **sampler_args): + """Returns an approximate weighted maximum cut. + + Defines an Ising problem with ground states corresponding to + a weighted maximum cut and uses the sampler to sample from it. + + A weighted maximum cut is a subset S of the vertices of G that + maximizes the sum of the edge weights between S and its + complementary subset. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a weighted maximum cut. Each edge in G should + have a numeric `weight` attribute. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + S : set + A maximum cut of G. + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + """ + # In order to form the Ising problem, we want to increase the + # energy by 1 for each edge between two nodes of the same color. + # The linear biases can all be 0. + h = {v: 0. for v in G} + try: + J = {(u, v): G[u][v]['weight'] for u, v in G.edges} + except KeyError: + raise DWaveNetworkXException("edges must have 'weight' attribute") + + # draw the lowest energy sample from the sampler + response = sampler.sample_ising(h, J, **sampler_args) + sample = next(iter(response)) + + return set(v for v in G if sample[v] >= 0) diff --git a/dwave_networkx/algorithms/partition.py b/dwave_networkx/algorithms/partition.py new file mode 100644 index 000000000..42812bcc0 --- /dev/null +++ b/dwave_networkx/algorithms/partition.py @@ -0,0 +1,156 @@ +# Copyright 2021 D-Wave Systems Inc. +# +# 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. + +import itertools +import math + +import dimod + +__all__ = ["partition", + "graph_partition_cqm", + ] + + +def partition(G, num_partitions=2, sampler=None, **sampler_args): + """Returns an approximate k-partition of G. + + Defines an CQM with ground states corresponding to a + balanced k-partition of G and uses the sampler to sample from it. + A k-partition is a collection of k subsets of the vertices + of G such that each vertex is in exactly one subset, and + the number of edges between vertices in different subsets + is as small as possible. If G is a weighted graph, the sum + of weights over those edges are minimized. + + Parameters + ---------- + G : NetworkX graph + The graph to partition. + num_partitions : int, optional (default 2) + The number of subsets in the desired partition. + sampler : + A constrained quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Model, with or without constraints. The sampler + is expected to have a 'sample_cqm' method. A sampler is expected to + return an iterable of samples, in order of increasing energy. + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + node_partition : dict + The partition as a dictionary mapping each node to subsets labelled + as integers 0, 1, 2, ... num_partitions. + + Example + ------- + This example uses a sampler from + `dimod `_ to find a 2-partition + for a graph of a Chimera unit cell created using the `chimera_graph()` + function. + + >>> import dimod + >>> sampler = dimod.ExactCQMSolver() + >>> G = dnx.chimera_graph(1, 1, 4) + >>> partitions = dnx.partition(G, sampler=sampler) + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + """ + if not len(G.nodes): + return {} + + cqm = graph_partition_cqm(G, num_partitions) + + # Solve the problem using the CQM solver + response = sampler.sample_cqm(cqm, **sampler_args) + + # Consider only results satisfying all constraints + possible_partitions = response.filter(lambda d: d.is_feasible) + + if not possible_partitions: + raise RuntimeError("No feasible solution could be found for this problem instance.") + + # Reinterpret result as partition assignment over nodes + indicators = (key for key, value in possible_partitions.first.sample.items() if math.isclose(value, 1.)) + node_partition = {key[0]: key[1] for key in indicators} + + return node_partition + + +def graph_partition_cqm(G, num_partitions): + """Find a constrained quadratic model for the graph's partitions. + + Defines an CQM with ground states corresponding to a + balanced k-partition of G and uses the sampler to sample from it. + A k-partition is a collection of k subsets of the vertices + of G such that each vertex is in exactly one subset, and + the number of edges between vertices in different subsets + is as small as possible. If G is a weighted graph, the sum + of weights over those edges are minimized. + + Parameters + ---------- + G : NetworkX graph + The graph to partition. + num_partitions : int + The number of subsets in the desired partition. + + Returns + ------- + cqm : :class:`dimod.ConstrainedQuadraticModel` + A constrained quadratic model with ground states corresponding to a + partition problem. The nodes of `G` are discrete logical variables + of the CQM, where the cases are the different partitions the node + can be assigned to. The objective is given as the number of edges + connecting nodes in different partitions. + + """ + partition_size = G.number_of_nodes()/num_partitions + partitions = range(num_partitions) + cqm = dimod.ConstrainedQuadraticModel() + + # Variables will be added using the discrete method in CQM + x = {vk: dimod.Binary(vk) for vk in itertools.product(G.nodes, partitions)} + + for v in G.nodes: + cqm.add_discrete(((v, k) for k in partitions), label=v) + + + if not math.isclose(partition_size, int(partition_size)): + # if number of nodes don't divide into num_partitions, + # accept partitions of size ceil() or floor() + floor, ceil = int(partition_size), int(partition_size+1) + for k in partitions: + cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) >= floor, label='equal_partition_low_%s' %k) + cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) <= ceil, label='equal_partition_high_%s' %k) + else: + # each partition must have partition_size elements + for k in partitions: + cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) == int(partition_size), label='equal_partition_%s' %k) + + cuts = 0 + for (u, v, d) in G.edges(data=True): + for k in partitions: + w = d.get('weight',1) + cuts += w * x[u,k] * x[v,k] + + if cuts: + cqm.set_objective(-cuts) + + return cqm \ No newline at end of file diff --git a/dwave_networkx/algorithms/social.py b/dwave_networkx/algorithms/social.py new file mode 100644 index 000000000..a861832d1 --- /dev/null +++ b/dwave_networkx/algorithms/social.py @@ -0,0 +1,185 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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 dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ["structural_imbalance"] + + +@binary_quadratic_model_sampler(1) +def structural_imbalance(S, sampler=None, **sampler_args): + """Returns an approximate set of frustrated edges and a bicoloring. + + A signed social network graph is a graph whose signed edges + represent friendly/hostile interactions between nodes. A + signed social network is considered balanced if it can be cleanly + divided into two factions, where all relations within a faction are + friendly, and all relations between factions are hostile. The measure + of imbalance or frustration is the minimum number of edges that + violate this rule. + + Parameters + ---------- + S : NetworkX graph + A social graph on which each edge has a 'sign' + attribute with a numeric value. + + sampler + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrainted Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + sampler_args + Additional keyword parameters are passed to the sampler. + + Returns + ------- + frustrated_edges : dict + A dictionary of the edges that violate the edge sign. The imbalance + of the network is the length of frustrated_edges. + + colors: dict + A bicoloring of the nodes into two factions. + + Raises + ------ + ValueError + If any edge does not have a 'sign' attribute. + + Examples + -------- + >>> import dimod + >>> sampler = dimod.ExactSolver() + >>> S = nx.Graph() + >>> S.add_edge('Alice', 'Bob', sign=1) # Alice and Bob are friendly + >>> S.add_edge('Alice', 'Eve', sign=-1) # Alice and Eve are hostile + >>> S.add_edge('Bob', 'Eve', sign=-1) # Bob and Eve are hostile + >>> frustrated_edges, colors = dnx.structural_imbalance(S, sampler) + >>> print(frustrated_edges) + {} + >>> print(colors) # doctest: +SKIP + {'Alice': 0, 'Bob': 0, 'Eve': 1} + >>> S.add_edge('Ted', 'Bob', sign=1) # Ted is friendly with all + >>> S.add_edge('Ted', 'Alice', sign=1) + >>> S.add_edge('Ted', 'Eve', sign=1) + >>> frustrated_edges, colors = dnx.structural_imbalance(S, sampler) + >>> print(frustrated_edges) # doctest: +SKIP + {('Ted', 'Eve'): {'sign': 1}} + >>> print(colors) # doctest: +SKIP + {'Bob': 1, 'Ted': 1, 'Alice': 1, 'Eve': 0} + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + References + ---------- + + `Ising model on Wikipedia `_ + + Facchetti, G., Iacono G., and Altafini C. (2011). + Computing global structural balance in large-scale signed social networks. + PNAS, 108, no. 52, 20953-20958 + + """ + h, J = structural_imbalance_ising(S) + + # use the sampler to find low energy states + response = sampler.sample_ising(h, J, **sampler_args) + + # we want the lowest energy sample + sample = next(iter(response)) + + # spins determine the color + colors = {v: (spin + 1) // 2 for v, spin in sample.items()} + + # frustrated edges are the ones that are violated + frustrated_edges = {} + for u, v, data in S.edges(data=True): + sign = data['sign'] + + if sign > 0 and colors[u] != colors[v]: + frustrated_edges[(u, v)] = data + elif sign < 0 and colors[u] == colors[v]: + frustrated_edges[(u, v)] = data + # else: not frustrated or sign == 0, no relation to violate + + return frustrated_edges, colors + + +def structural_imbalance_ising(S): + """Construct the Ising problem to calculate the structural imbalance of a signed social network. + + A signed social network graph is a graph whose signed edges + represent friendly/hostile interactions between nodes. A + signed social network is considered balanced if it can be cleanly + divided into two factions, where all relations within a faction are + friendly, and all relations between factions are hostile. The measure + of imbalance or frustration is the minimum number of edges that + violate this rule. + + Parameters + ---------- + S : NetworkX graph + A social graph on which each edge has a 'sign' attribute with a numeric value. + + Returns + ------- + h : dict + The linear biases of the Ising problem. Each variable in the Ising problem represent + a node in the signed social network. The solution that minimized the Ising problem + will assign each variable a value, either -1 or 1. This bi-coloring defines the factions. + + J : dict + The quadratic biases of the Ising problem. + + Raises + ------ + ValueError + If any edge does not have a 'sign' attribute. + + Examples + -------- + >>> import dimod + >>> from dwave_networkx.algorithms.social import structural_imbalance_ising + ... + >>> S = nx.Graph() + >>> S.add_edge('Alice', 'Bob', sign=1) # Alice and Bob are friendly + >>> S.add_edge('Alice', 'Eve', sign=-1) # Alice and Eve are hostile + >>> S.add_edge('Bob', 'Eve', sign=-1) # Bob and Eve are hostile + ... + >>> h, J = structural_imbalance_ising(S) + >>> h # doctest: +SKIP + {'Alice': 0.0, 'Bob': 0.0, 'Eve': 0.0} + >>> J # doctest: +SKIP + {('Alice', 'Bob'): -1.0, ('Alice', 'Eve'): 1.0, ('Bob', 'Eve'): 1.0} + + """ + h = {v: 0.0 for v in S} + J = {} + for u, v, data in S.edges(data=True): + try: + J[(u, v)] = -1. * data['sign'] + except KeyError: + raise ValueError(("graph should be a signed social graph," + "each edge should have a 'sign' attr")) + + return h, J diff --git a/dwave_networkx/algorithms/tsp.py b/dwave_networkx/algorithms/tsp.py new file mode 100644 index 000000000..576b66656 --- /dev/null +++ b/dwave_networkx/algorithms/tsp.py @@ -0,0 +1,241 @@ +# Copyright 2018 D-Wave Systems Inc. +# +# 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. + +import itertools + +from collections import defaultdict + +from dwave_networkx.utils import binary_quadratic_model_sampler + +__all__ = ["traveling_salesperson", + "traveling_salesperson_qubo", + "traveling_salesman", + "traveling_salesman_qubo", + "is_hamiltonian_path", + ] + + +@binary_quadratic_model_sampler(1) +def traveling_salesperson(G, sampler=None, lagrange=None, weight='weight', + start=None, **sampler_args): + """Returns an approximate minimum traveling salesperson route. + + Defines a QUBO with ground states corresponding to the + minimum routes and uses the sampler to sample + from it. + + A route is a cycle in the graph that reaches each node exactly once. + A minimum route is a route with the smallest total edge weight. + + Parameters + ---------- + G : NetworkX graph + The graph on which to find a minimum traveling salesperson route. + This should be a complete graph with non-zero weights on every edge. + + sampler : + A binary quadratic model sampler. A sampler is a process that + samples from low energy states in models defined by an Ising + equation or a Quadratic Unconstrained Binary Optimization + Problem (QUBO). A sampler is expected to have a 'sample_qubo' + and 'sample_ising' method. A sampler is expected to return an + iterable of samples, in order of increasing energy. If no + sampler is provided, one must be provided using the + `set_default_sampler` function. + + lagrange : number, optional (default None) + Lagrange parameter to weight constraints (visit every city once) + versus objective (shortest distance route). + + weight : optional (default 'weight') + The name of the edge attribute containing the weight. + + start : node, optional + If provided, the route will begin at `start`. + + sampler_args : + Additional keyword parameters are passed to the sampler. + + Returns + ------- + route : list + List of nodes in order to be visited on a route + + Examples + -------- + + >>> import dimod + ... + >>> G = nx.Graph() + >>> G.add_weighted_edges_from({(0, 1, .1), (0, 2, .5), (0, 3, .1), (1, 2, .1), + ... (1, 3, .5), (2, 3, .1)}) + >>> dnx.traveling_salesperson(G, dimod.ExactSolver(), start=0) # doctest: +SKIP + [0, 1, 2, 3] + + Notes + ----- + Samplers by their nature may not return the optimal solution. This + function does not attempt to confirm the quality of the returned + sample. + + """ + # Get a QUBO representation of the problem + Q = traveling_salesperson_qubo(G, lagrange, weight) + + # use the sampler to find low energy states + response = sampler.sample_qubo(Q, **sampler_args) + + sample = response.first.sample + + route = [None]*len(G) + for (city, time), val in sample.items(): + if val: + route[time] = city + + if start is not None and route[0] != start: + # rotate to put the start in front + idx = route.index(start) + route = route[idx:] + route[:idx] + + return route + + +traveling_salesman = traveling_salesperson + + +def traveling_salesperson_qubo(G, lagrange=None, weight='weight', missing_edge_weight=None): + """Return the QUBO with ground states corresponding to a minimum TSP route. + + If :math:`|G|` is the number of nodes in the graph, the resulting qubo will have: + + * :math:`|G|^2` variables/nodes + * :math:`2 |G|^2 (|G| - 1)` interactions/edges + + Parameters + ---------- + G : NetworkX graph + A complete graph in which each edge has a attribute giving its weight. + + lagrange : number, optional (default None) + Lagrange parameter to weight constraints (no edges within set) + versus objective (largest set possible). + + weight : optional (default 'weight') + The name of the edge attribute containing the weight. + + missing_edge_weight : number, optional (default None) + For bi-directional graphs, the weight given to missing edges. + If None is given (the default), missing edges will be set to + the sum of all weights. + + Returns + ------- + QUBO : dict + The QUBO with ground states corresponding to a minimum travelling + salesperson route. The QUBO variables are labelled `(c, t)` where `c` + is a node in `G` and `t` is the time index. For instance, if `('a', 0)` + is 1 in the ground state, that means the node 'a' is visted first. + + """ + N = G.number_of_nodes() + + if lagrange is None: + # If no lagrange parameter provided, set to 'average' tour length. + # Usually a good estimate for a lagrange parameter is between 75-150% + # of the objective function value, so we come up with an estimate for + # tour length and use that. + if G.number_of_edges()>0: + lagrange = G.size(weight=weight)*G.number_of_nodes()/G.number_of_edges() + else: + lagrange = 2 + + # calculate default missing_edge_weight if required + if missing_edge_weight is None: + # networkx method to calculate sum of all weights + missing_edge_weight = G.size(weight=weight) + + # some input checking + if N in (1, 2): + msg = "graph must have at least 3 nodes or be empty" + raise ValueError(msg) + + # Creating the QUBO + Q = defaultdict(float) + + # Constraint that each row has exactly one 1 + for node in G: + for pos_1 in range(N): + Q[((node, pos_1), (node, pos_1))] -= lagrange + for pos_2 in range(pos_1+1, N): + Q[((node, pos_1), (node, pos_2))] += 2.0*lagrange + + # Constraint that each col has exactly one 1 + for pos in range(N): + for node_1 in G: + Q[((node_1, pos), (node_1, pos))] -= lagrange + for node_2 in set(G)-{node_1}: + # QUBO coefficient is 2*lagrange, but we are placing this value + # above *and* below the diagonal, so we put half in each position. + Q[((node_1, pos), (node_2, pos))] += lagrange + + # Objective that minimizes distance + for u, v in itertools.combinations(G.nodes, 2): + for pos in range(N): + nextpos = (pos + 1) % N + + # going from u -> v + try: + value = G[u][v][weight] + except KeyError: + value = missing_edge_weight + + Q[((u, pos), (v, nextpos))] += value + + # going from v -> u + try: + value = G[v][u][weight] + except KeyError: + value = missing_edge_weight + + Q[((v, pos), (u, nextpos))] += value + + return Q + + +traveling_salesman_qubo = traveling_salesperson_qubo + + +def is_hamiltonian_path(G, route): + """Determines whether the given list forms a valid TSP route. + + A travelling salesperson route must visit each city exactly once. + + Parameters + ---------- + G : NetworkX graph + + The graph on which to check the route. + + route : list + + List of nodes in the order that they are visited. + + Returns + ------- + is_valid : bool + True if route forms a valid travelling salesperson route. + + """ + + return (set(route) == set(G)) From 8da580ebf93479435992c098249b7769aff9c34c Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 1 May 2026 00:53:01 +0200 Subject: [PATCH 02/27] Move dnx problem generators under dimod.generators namespace --- {dwave_networkx/algorithms => dimod/generators}/coloring.py | 0 .../algorithms => dimod/generators}/independent_set.py | 0 {dwave_networkx/algorithms => dimod/generators}/markov.py | 0 {dwave_networkx/algorithms => dimod/generators}/matching.py | 0 {dwave_networkx/algorithms => dimod/generators}/partition.py | 0 {dwave_networkx/algorithms => dimod/generators}/social.py | 0 {dwave_networkx/algorithms => dimod/generators}/tsp.py | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename {dwave_networkx/algorithms => dimod/generators}/coloring.py (100%) rename {dwave_networkx/algorithms => dimod/generators}/independent_set.py (100%) rename {dwave_networkx/algorithms => dimod/generators}/markov.py (100%) rename {dwave_networkx/algorithms => dimod/generators}/matching.py (100%) rename {dwave_networkx/algorithms => dimod/generators}/partition.py (100%) rename {dwave_networkx/algorithms => dimod/generators}/social.py (100%) rename {dwave_networkx/algorithms => dimod/generators}/tsp.py (100%) diff --git a/dwave_networkx/algorithms/coloring.py b/dimod/generators/coloring.py similarity index 100% rename from dwave_networkx/algorithms/coloring.py rename to dimod/generators/coloring.py diff --git a/dwave_networkx/algorithms/independent_set.py b/dimod/generators/independent_set.py similarity index 100% rename from dwave_networkx/algorithms/independent_set.py rename to dimod/generators/independent_set.py diff --git a/dwave_networkx/algorithms/markov.py b/dimod/generators/markov.py similarity index 100% rename from dwave_networkx/algorithms/markov.py rename to dimod/generators/markov.py diff --git a/dwave_networkx/algorithms/matching.py b/dimod/generators/matching.py similarity index 100% rename from dwave_networkx/algorithms/matching.py rename to dimod/generators/matching.py diff --git a/dwave_networkx/algorithms/partition.py b/dimod/generators/partition.py similarity index 100% rename from dwave_networkx/algorithms/partition.py rename to dimod/generators/partition.py diff --git a/dwave_networkx/algorithms/social.py b/dimod/generators/social.py similarity index 100% rename from dwave_networkx/algorithms/social.py rename to dimod/generators/social.py diff --git a/dwave_networkx/algorithms/tsp.py b/dimod/generators/tsp.py similarity index 100% rename from dwave_networkx/algorithms/tsp.py rename to dimod/generators/tsp.py From 0e681727853afe3b2c89905bbff0eae1c9df2a17 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 1 May 2026 01:14:52 +0200 Subject: [PATCH 03/27] Clean-up dnx.algorithms to keep only problem generators --- dimod/generators/coloring.py | 166 +-- dimod/generators/markov.py | 109 +- dimod/generators/matching.py | 183 +--- dimod/generators/partition.py | 74 +- dimod/generators/social.py | 113 +-- dimod/generators/tsp.py | 120 +-- dwave_networkx/algorithms/__init__.py | 26 - dwave_networkx/algorithms/canonicalization.py | 163 --- dwave_networkx/algorithms/clique.py | 187 ---- dwave_networkx/algorithms/cover.py | 191 ---- .../algorithms/elimination_ordering.py | 957 ------------------ dwave_networkx/algorithms/max_cut.py | 143 --- 12 files changed, 8 insertions(+), 2424 deletions(-) delete mode 100644 dwave_networkx/algorithms/__init__.py delete mode 100644 dwave_networkx/algorithms/canonicalization.py delete mode 100644 dwave_networkx/algorithms/clique.py delete mode 100644 dwave_networkx/algorithms/cover.py delete mode 100644 dwave_networkx/algorithms/elimination_ordering.py delete mode 100644 dwave_networkx/algorithms/max_cut.py diff --git a/dimod/generators/coloring.py b/dimod/generators/coloring.py index 3ce60432a..b2c756691 100644 --- a/dimod/generators/coloring.py +++ b/dimod/generators/coloring.py @@ -17,14 +17,7 @@ import networkx as nx -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ["is_vertex_coloring", - "is_cycle", - "min_vertex_color", - "min_vertex_coloring", # alias for min_vertex_color - "min_vertex_color_qubo", - "vertex_color", +__all__ = ["min_vertex_color_qubo", "vertex_color_qubo", ] @@ -82,60 +75,6 @@ def vertex_color_qubo(G, colors): return Q -@binary_quadratic_model_sampler(2) -def vertex_color(G, colors, sampler=None, **sampler_args): - """Returns an approximate vertex coloring. - - Vertex coloring is the problem of assigning a color to the - vertices of a graph in a way that no adjacent vertices have the - same color. - - Defines a QUBO [Dah2013]_ with ground states corresponding to valid - vertex colorings and uses the sampler to sample from it. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum vertex coloring. - - colors : int/sequence - The colors. If an int, the colors are labelled `[0, n)`. The number of - colors must be greater or equal to the chromatic number of the graph. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - coloring : dict - A coloring for each vertex in G such that no adjacent nodes - share the same color. A dict of the form {node: color, ...} - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - """ - Q = vertex_color_qubo(G, colors) - - # get the lowest energy sample - sample = sampler.sample_qubo(Q, **sampler_args).first.sample - - return {v: c for (v, c), val in sample.items() if val} - - def _chromatic_number_upper_bound(G): # tries to determine an upper bound on the chromatic number of G # Assumes G is not complete @@ -255,72 +194,6 @@ def min_vertex_color_qubo(G, chromatic_lb=None, chromatic_ub=None): return Q -@binary_quadratic_model_sampler(1) -def min_vertex_color(G, sampler=None, chromatic_lb=None, chromatic_ub=None, - **sampler_args): - """Returns an approximate minimum vertex coloring. - - Vertex coloring is the problem of assigning a color to the - vertices of a graph in a way that no adjacent vertices have the - same color. A minimum vertex coloring is the problem of solving - the vertex coloring problem using the smallest number of colors. - - Defines a QUBO [Dah2013]_ with ground states corresponding to minimum - vertex colorings and uses the sampler to sample from it. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum vertex coloring. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - chromatic_lb : int, optional - A lower bound on the chromatic number. If one is not provided, a - bound is calulcated. - - chromatic_ub : int, optional - An upper bound on the chromatic number. If one is not provided, a bound - is calculated. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - coloring : dict - A coloring for each vertex in G such that no adjacent nodes - share the same color. A dict of the form {node: color, ...} - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - """ - - Q = min_vertex_color_qubo(G, chromatic_lb=chromatic_lb, - chromatic_ub=chromatic_ub) - - # get the lowest energy sample - sample = sampler.sample_qubo(Q, **sampler_args).first.sample - - return {v: c for (v, c), val in sample.items() if val} - - -# legacy name, alias -min_vertex_coloring = min_vertex_color - - def is_cycle(G): """Determines whether the given graph is a cycle or circle graph. @@ -364,40 +237,3 @@ def is_cycle(G): # if we haven't visited all of the nodes, then it is not a connected cycle return n_visited == len(G) - - -def is_vertex_coloring(G, coloring): - """Determines whether the given coloring is a vertex coloring of graph G. - - Parameters - ---------- - G : NetworkX graph - The graph on which the vertex coloring is applied. - - coloring : dict - A coloring of the nodes of G. Should be a dict of the form - {node: color, ...}. - - Returns - ------- - is_vertex_coloring : bool - True if the given coloring defines a vertex coloring; that is, no - two adjacent vertices share a color. - - Example - ------- - This example colors checks two colorings for a graph, G, of a single Chimera - unit cell. The first uses one color (0) for the four horizontal qubits - and another (1) for the four vertical qubits, in which case there are - no adjacencies; the second coloring swaps the color of one node. - - >>> G = dnx.chimera_graph(1,1,4) - >>> colors = {0: 0, 1: 0, 2: 0, 3: 0, 4: 1, 5: 1, 6: 1, 7: 1} - >>> dnx.is_vertex_coloring(G, colors) - True - >>> colors[4]=0 - >>> dnx.is_vertex_coloring(G, colors) - False - - """ - return all(coloring[u] != coloring[v] for u, v in G.edges) diff --git a/dimod/generators/markov.py b/dimod/generators/markov.py index 22a7d970e..ffe89aed7 100644 --- a/dimod/generators/markov.py +++ b/dimod/generators/markov.py @@ -14,9 +14,8 @@ import dimod -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ['sample_markov_network', 'markov_network_bqm'] +__all__ = ['markov_network_bqm', + ] ############################################################################### @@ -51,110 +50,6 @@ # -@binary_quadratic_model_sampler(1) -def sample_markov_network(MN, sampler=None, fixed_variables=None, - return_sampleset=False, - **sampler_args): - """Samples from a markov network using the provided sampler. - - Parameters - ---------- - G : NetworkX graph - A Markov Network as returned by :func:`.markov_network` - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - fixed_variables : dict - A dictionary of variable assignments to be fixed in the markov network. - - return_sampleset : bool (optional, default=False) - If True, returns a :obj:`dimod.SampleSet` rather than a list of samples. - - **sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - samples : list[dict]/:obj:`dimod.SampleSet` - A list of samples ordered from low-to-high energy or a sample set. - - Examples - -------- - - >>> import dimod - ... - >>> potentials = {('a', 'b'): {(0, 0): -1, - ... (0, 1): .5, - ... (1, 0): .5, - ... (1, 1): 2}} - >>> MN = dnx.markov_network(potentials) - >>> sampler = dimod.ExactSolver() - >>> samples = dnx.sample_markov_network(MN, sampler) - >>> samples[0] # doctest: +SKIP - {'a': 0, 'b': 0} - - >>> import dimod - ... - >>> potentials = {('a', 'b'): {(0, 0): -1, - ... (0, 1): .5, - ... (1, 0): .5, - ... (1, 1): 2}} - >>> MN = dnx.markov_network(potentials) - >>> sampler = dimod.ExactSolver() - >>> samples = dnx.sample_markov_network(MN, sampler, return_sampleset=True) - >>> samples.first # doctest: +SKIP - Sample(sample={'a': 0, 'b': 0}, energy=-1.0, num_occurrences=1) - - >>> import dimod - ... - >>> potentials = {('a', 'b'): {(0, 0): -1, - ... (0, 1): .5, - ... (1, 0): .5, - ... (1, 1): 2}, - ... ('b', 'c'): {(0, 0): -9, - ... (0, 1): 1.2, - ... (1, 0): 7.2, - ... (1, 1): 5}} - >>> MN = dnx.markov_network(potentials) - >>> sampler = dimod.ExactSolver() - >>> samples = dnx.sample_markov_network(MN, sampler, fixed_variables={'b': 0}) - >>> samples[0] # doctest: +SKIP - {'a': 0, 'c': 0, 'b': 0} - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - """ - - bqm = markov_network_bqm(MN) - - if fixed_variables: - # we can modify in-place since we just made it - bqm.fix_variables(fixed_variables) - - sampleset = sampler.sample(bqm, **sampler_args) - - if fixed_variables: - # add the variables back in - sampleset = dimod.append_variables(sampleset, fixed_variables) - - if return_sampleset: - return sampleset - else: - return list(map(dict, sampleset.samples())) - - def markov_network_bqm(MN): """Construct a binary quadratic model for a markov network. diff --git a/dimod/generators/matching.py b/dimod/generators/matching.py index 4b32ce85a..bf663eb72 100644 --- a/dimod/generators/matching.py +++ b/dimod/generators/matching.py @@ -13,19 +13,11 @@ # limitations under the License. import itertools -import numbers -import warnings import dimod -import networkx as nx -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ['is_matching', - 'is_maximal_matching', - 'matching_bqm', +__all__ = ['matching_bqm', 'maximal_matching_bqm', - 'min_maximal_matching', 'min_maximal_matching_bqm', ] @@ -176,176 +168,3 @@ def min_maximal_matching_bqm(G, maximal_lagrange=2, matching_lagrange=None): bqm.linear[v] += 1 return bqm - - -@binary_quadratic_model_sampler(1) -def maximal_matching(G, sampler=None, **sampler_args): - """Finds an approximate maximal matching. - - Defines a QUBO with ground states corresponding to a maximal - matching and uses the sampler to sample from it. - - A matching is a subset of edges in which no node occurs more than - once. A maximal matching is one in which no edges from G can be - added without violating the matching rule. - - Finding maximal matchings can be done is polynomial time, so this method - is only useful pedagogically. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a maximal matching. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - matching : set - A maximal matching of the graph. - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - - `Matching on Wikipedia `_ - - `QUBO on Wikipedia `_ - - Based on the formulation presented in [Luc2014]_. - - """ - if not G.edges: - return set() - - bqm = maximal_matching_bqm(G) - sampleset = sampler.sample(bqm, **sampler_args) - sample = sampleset.first.sample - - # the matching are the edges that are 1 in the sample - return set(tuple(edge) for edge, val in sample.items() if val > 0) - - -@binary_quadratic_model_sampler(1) -def min_maximal_matching(G, sampler=None, **sampler_args): - """Returns an approximate minimum maximal matching. - - Defines a QUBO with ground states corresponding to a minimum - maximal matching and uses the sampler to sample from it. - - A matching is a subset of edges in which no node occurs more than - once. A maximal matching is one in which no edges from G can be - added without violating the matching rule. A minimum maximal - matching is the smallest maximal matching for G. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum maximal matching. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - matching : set - A minimum maximal matching of the graph. - - Example - ------- - This example uses a sampler from - `dimod `_ to find a minimum maximal - matching for a Chimera unit cell. - - >>> import dimod - >>> sampler = dimod.ExactSolver() - >>> G = dnx.chimera_graph(1, 1, 4) - >>> matching = dnx.min_maximal_matching(G, sampler) - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - - `Matching on Wikipedia `_ - - `QUBO on Wikipedia `_ - - Lucas, A. (2014). Ising formulations of many NP problems. - Frontiers in Physics, Volume 2, Article 5. - - """ - if not G.edges: - return set() - - bqm = min_maximal_matching_bqm(G) - sampleset = sampler.sample(bqm, **sampler_args) - sample = sampleset.first.sample - - # the matching are the edges that are 1 in the sample - return set(tuple(edge) for edge, val in sample.items() if val > 0) - - -def is_matching(edges): - """Determine whether the given set of edges is a matching. - - Deprecated in favour of :func:`networkx.is_matching`. - """ - warnings.warn("This method is deprecated, please use NetworkX's" - "nx.is_matching(G, edges) rather than dwave-networkx's " - "dnx.is_matching(edges)", DeprecationWarning, stacklevel=2) - return len(set().union(*edges)) == len(edges) * 2 - - -def is_maximal_matching(G, matching): - """Determine whether the given set of edges is a maximal matching. - - Deprecated in favour of :func:`networkx.is_matching`. - """ - warnings.warn("This method is deprecated, please use NetworkX's" - "nx.is_maximal_matching(G, edges) rather than " - "dwave-networkx's dnx.is_maximal_matching(G, edges)", - DeprecationWarning, stacklevel=2) - touched_nodes = set().union(*matching) - - # first check if a matching - if len(touched_nodes) != len(matching) * 2: - return False - - # now for each edge, check that at least one of its variables is - # already in the matching - for (u, v) in G.edges: - if u not in touched_nodes and v not in touched_nodes: - return False - - return True diff --git a/dimod/generators/partition.py b/dimod/generators/partition.py index 42812bcc0..7fb218208 100644 --- a/dimod/generators/partition.py +++ b/dimod/generators/partition.py @@ -17,82 +17,10 @@ import dimod -__all__ = ["partition", - "graph_partition_cqm", +__all__ = ["graph_partition_cqm", ] -def partition(G, num_partitions=2, sampler=None, **sampler_args): - """Returns an approximate k-partition of G. - - Defines an CQM with ground states corresponding to a - balanced k-partition of G and uses the sampler to sample from it. - A k-partition is a collection of k subsets of the vertices - of G such that each vertex is in exactly one subset, and - the number of edges between vertices in different subsets - is as small as possible. If G is a weighted graph, the sum - of weights over those edges are minimized. - - Parameters - ---------- - G : NetworkX graph - The graph to partition. - num_partitions : int, optional (default 2) - The number of subsets in the desired partition. - sampler : - A constrained quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Model, with or without constraints. The sampler - is expected to have a 'sample_cqm' method. A sampler is expected to - return an iterable of samples, in order of increasing energy. - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - node_partition : dict - The partition as a dictionary mapping each node to subsets labelled - as integers 0, 1, 2, ... num_partitions. - - Example - ------- - This example uses a sampler from - `dimod `_ to find a 2-partition - for a graph of a Chimera unit cell created using the `chimera_graph()` - function. - - >>> import dimod - >>> sampler = dimod.ExactCQMSolver() - >>> G = dnx.chimera_graph(1, 1, 4) - >>> partitions = dnx.partition(G, sampler=sampler) - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - """ - if not len(G.nodes): - return {} - - cqm = graph_partition_cqm(G, num_partitions) - - # Solve the problem using the CQM solver - response = sampler.sample_cqm(cqm, **sampler_args) - - # Consider only results satisfying all constraints - possible_partitions = response.filter(lambda d: d.is_feasible) - - if not possible_partitions: - raise RuntimeError("No feasible solution could be found for this problem instance.") - - # Reinterpret result as partition assignment over nodes - indicators = (key for key, value in possible_partitions.first.sample.items() if math.isclose(value, 1.)) - node_partition = {key[0]: key[1] for key in indicators} - - return node_partition - - def graph_partition_cqm(G, num_partitions): """Find a constrained quadratic model for the graph's partitions. diff --git a/dimod/generators/social.py b/dimod/generators/social.py index a861832d1..ce754303c 100644 --- a/dimod/generators/social.py +++ b/dimod/generators/social.py @@ -12,117 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ["structural_imbalance"] - - -@binary_quadratic_model_sampler(1) -def structural_imbalance(S, sampler=None, **sampler_args): - """Returns an approximate set of frustrated edges and a bicoloring. - - A signed social network graph is a graph whose signed edges - represent friendly/hostile interactions between nodes. A - signed social network is considered balanced if it can be cleanly - divided into two factions, where all relations within a faction are - friendly, and all relations between factions are hostile. The measure - of imbalance or frustration is the minimum number of edges that - violate this rule. - - Parameters - ---------- - S : NetworkX graph - A social graph on which each edge has a 'sign' - attribute with a numeric value. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrainted Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - frustrated_edges : dict - A dictionary of the edges that violate the edge sign. The imbalance - of the network is the length of frustrated_edges. - - colors: dict - A bicoloring of the nodes into two factions. - - Raises - ------ - ValueError - If any edge does not have a 'sign' attribute. - - Examples - -------- - >>> import dimod - >>> sampler = dimod.ExactSolver() - >>> S = nx.Graph() - >>> S.add_edge('Alice', 'Bob', sign=1) # Alice and Bob are friendly - >>> S.add_edge('Alice', 'Eve', sign=-1) # Alice and Eve are hostile - >>> S.add_edge('Bob', 'Eve', sign=-1) # Bob and Eve are hostile - >>> frustrated_edges, colors = dnx.structural_imbalance(S, sampler) - >>> print(frustrated_edges) - {} - >>> print(colors) # doctest: +SKIP - {'Alice': 0, 'Bob': 0, 'Eve': 1} - >>> S.add_edge('Ted', 'Bob', sign=1) # Ted is friendly with all - >>> S.add_edge('Ted', 'Alice', sign=1) - >>> S.add_edge('Ted', 'Eve', sign=1) - >>> frustrated_edges, colors = dnx.structural_imbalance(S, sampler) - >>> print(frustrated_edges) # doctest: +SKIP - {('Ted', 'Eve'): {'sign': 1}} - >>> print(colors) # doctest: +SKIP - {'Bob': 1, 'Ted': 1, 'Alice': 1, 'Eve': 0} - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - - `Ising model on Wikipedia `_ - - Facchetti, G., Iacono G., and Altafini C. (2011). - Computing global structural balance in large-scale signed social networks. - PNAS, 108, no. 52, 20953-20958 - - """ - h, J = structural_imbalance_ising(S) - - # use the sampler to find low energy states - response = sampler.sample_ising(h, J, **sampler_args) - - # we want the lowest energy sample - sample = next(iter(response)) - - # spins determine the color - colors = {v: (spin + 1) // 2 for v, spin in sample.items()} - - # frustrated edges are the ones that are violated - frustrated_edges = {} - for u, v, data in S.edges(data=True): - sign = data['sign'] - - if sign > 0 and colors[u] != colors[v]: - frustrated_edges[(u, v)] = data - elif sign < 0 and colors[u] == colors[v]: - frustrated_edges[(u, v)] = data - # else: not frustrated or sign == 0, no relation to violate - - return frustrated_edges, colors +__all__ = ["structural_imbalance_ising", + ] def structural_imbalance_ising(S): diff --git a/dimod/generators/tsp.py b/dimod/generators/tsp.py index 576b66656..12ea0e659 100644 --- a/dimod/generators/tsp.py +++ b/dimod/generators/tsp.py @@ -16,104 +16,11 @@ from collections import defaultdict -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ["traveling_salesperson", - "traveling_salesperson_qubo", - "traveling_salesman", +__all__ = ["traveling_salesperson_qubo", "traveling_salesman_qubo", - "is_hamiltonian_path", ] -@binary_quadratic_model_sampler(1) -def traveling_salesperson(G, sampler=None, lagrange=None, weight='weight', - start=None, **sampler_args): - """Returns an approximate minimum traveling salesperson route. - - Defines a QUBO with ground states corresponding to the - minimum routes and uses the sampler to sample - from it. - - A route is a cycle in the graph that reaches each node exactly once. - A minimum route is a route with the smallest total edge weight. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum traveling salesperson route. - This should be a complete graph with non-zero weights on every edge. - - sampler : - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - lagrange : number, optional (default None) - Lagrange parameter to weight constraints (visit every city once) - versus objective (shortest distance route). - - weight : optional (default 'weight') - The name of the edge attribute containing the weight. - - start : node, optional - If provided, the route will begin at `start`. - - sampler_args : - Additional keyword parameters are passed to the sampler. - - Returns - ------- - route : list - List of nodes in order to be visited on a route - - Examples - -------- - - >>> import dimod - ... - >>> G = nx.Graph() - >>> G.add_weighted_edges_from({(0, 1, .1), (0, 2, .5), (0, 3, .1), (1, 2, .1), - ... (1, 3, .5), (2, 3, .1)}) - >>> dnx.traveling_salesperson(G, dimod.ExactSolver(), start=0) # doctest: +SKIP - [0, 1, 2, 3] - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - """ - # Get a QUBO representation of the problem - Q = traveling_salesperson_qubo(G, lagrange, weight) - - # use the sampler to find low energy states - response = sampler.sample_qubo(Q, **sampler_args) - - sample = response.first.sample - - route = [None]*len(G) - for (city, time), val in sample.items(): - if val: - route[time] = city - - if start is not None and route[0] != start: - # rotate to put the start in front - idx = route.index(start) - route = route[idx:] + route[:idx] - - return route - - -traveling_salesman = traveling_salesperson - - def traveling_salesperson_qubo(G, lagrange=None, weight='weight', missing_edge_weight=None): """Return the QUBO with ground states corresponding to a minimum TSP route. @@ -214,28 +121,3 @@ def traveling_salesperson_qubo(G, lagrange=None, weight='weight', missing_edge_w traveling_salesman_qubo = traveling_salesperson_qubo - - -def is_hamiltonian_path(G, route): - """Determines whether the given list forms a valid TSP route. - - A travelling salesperson route must visit each city exactly once. - - Parameters - ---------- - G : NetworkX graph - - The graph on which to check the route. - - route : list - - List of nodes in the order that they are visited. - - Returns - ------- - is_valid : bool - True if route forms a valid travelling salesperson route. - - """ - - return (set(route) == set(G)) diff --git a/dwave_networkx/algorithms/__init__.py b/dwave_networkx/algorithms/__init__.py deleted file mode 100644 index d588b2472..000000000 --- a/dwave_networkx/algorithms/__init__.py +++ /dev/null @@ -1,26 +0,0 @@ -# Copyright 2018 D-Wave Systems Inc. -# -# 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 dwave_networkx.algorithms.independent_set import * -from dwave_networkx.algorithms.canonicalization import * -from dwave_networkx.algorithms.clique import * -from dwave_networkx.algorithms.cover import * -from dwave_networkx.algorithms.matching import * -from dwave_networkx.algorithms.social import * -from dwave_networkx.algorithms.elimination_ordering import * -from dwave_networkx.algorithms.coloring import * -from dwave_networkx.algorithms.max_cut import * -from dwave_networkx.algorithms.markov import * -from dwave_networkx.algorithms.tsp import * -from dwave_networkx.algorithms.partition import * diff --git a/dwave_networkx/algorithms/canonicalization.py b/dwave_networkx/algorithms/canonicalization.py deleted file mode 100644 index af4f90c03..000000000 --- a/dwave_networkx/algorithms/canonicalization.py +++ /dev/null @@ -1,163 +0,0 @@ -# Copyright 2018 D-Wave Systems Inc. -# -# 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. - -import math - -from dwave_networkx.generators.chimera import chimera_coordinates - -__all__ = ['canonical_chimera_labeling'] - - -def canonical_chimera_labeling(G, t=None): - """Returns a mapping from the labels of G to chimera-indexed labeling. - - Parameters - ---------- - G : NetworkX graph - A Chimera-structured graph. - t : int (optional, default 4) - Size of the shore within each Chimera tile. - - Returns - ------- - chimera_indices: dict - A mapping from the current labels to a 4-tuple of Chimera indices. - - """ - adj = G.adj - - if t is None: - if hasattr(G, 'edges'): - num_edges = len(G.edges) - else: - num_edges = len(G.quadratic) - t = _chimera_shore_size(adj, num_edges) - - chimera_indices = {} - - row = col = 0 - - # need to find a node in a corner - root_edge = min(((u, v) for u in adj for v in adj[u]), - key=lambda edge: len(adj[edge[0]]) + len(adj[edge[1]])) - root, _ = root_edge - - horiz, verti = rooted_tile(adj, root, t) - while len(chimera_indices) < len(adj): - - new_indices = {} - - if row == 0: - # if we're in the 0th row, we can assign the horizontal randomly - for si, v in enumerate(horiz): - new_indices[v] = (row, col, 0, si) - else: - # we need to match the row above - for v in horiz: - north = [u for u in adj[v] if u in chimera_indices] - assert len(north) == 1 - i, j, u, si = chimera_indices[north[0]] - assert i == row - 1 and j == col and u == 0 - new_indices[v] = (row, col, 0, si) - - if col == 0: - # if we're in the 0th col, we can assign the vertical randomly - for si, v in enumerate(verti): - new_indices[v] = (row, col, 1, si) - else: - # we need to match the column to the east - for v in verti: - east = [u for u in adj[v] if u in chimera_indices] - assert len(east) == 1 - i, j, u, si = chimera_indices[east[0]] - assert i == row and j == col - 1 and u == 1 - new_indices[v] = (row, col, 1, si) - - chimera_indices.update(new_indices) - - # get the next root - root_neighbours = [v for v in adj[root] if v not in chimera_indices] - if len(root_neighbours) == 1: - # we can increment the row - root = root_neighbours[0] - horiz, verti = rooted_tile(adj, root, t) - - row += 1 - else: - # need to go back to row 0, and increment the column - assert not root_neighbours # should be empty - - # we want (0, col, 1, 0), we could cache this, but for now let's just go look for it - # the slow way - vert_root = [v for v in chimera_indices if chimera_indices[v] == (0, col, 1, 0)][0] - - vert_root_neighbours = [v for v in adj[vert_root] if v not in chimera_indices] - - if vert_root_neighbours: - - verti, horiz = rooted_tile(adj, vert_root_neighbours[0], t) - root = next(iter(horiz)) - - row = 0 - col += 1 - - return chimera_indices - - -def rooted_tile(adj, n, t): - horiz = {n} - vert = set() - - # get all of the nodes that are two steps away from n - two_steps = {v for u in adj[n] for v in adj[u] if v != n} - - # find the subset of two_steps that share exactly t neighbours - for v in two_steps: - shared = set(adj[n]).intersection(adj[v]) - - if len(shared) == t: - assert v not in horiz - horiz.add(v) - vert |= shared - - assert len(vert) == t - return horiz, vert - - -def _chimera_shore_size(adj, num_edges): - # we know |E| = m*n*t*t + (2*m*n-m-n)*t - - num_nodes = len(adj) - - max_degree = max(len(adj[v]) for v in adj) - - if num_nodes == 2 * max_degree: - return max_degree - - def a(t): - return -2*t - - def b(t): - return (t + 2) * num_nodes - 2 * num_edges - - def c(t): - return -num_nodes - - t = max_degree - 1 - m = (-b(t) + math.sqrt(b(t)**2 - 4*a(t)*c(t))) / (2 * a(t)) - - if m.is_integer(): - return t - - return max_degree - 2 diff --git a/dwave_networkx/algorithms/clique.py b/dwave_networkx/algorithms/clique.py deleted file mode 100644 index 0b65f0858..000000000 --- a/dwave_networkx/algorithms/clique.py +++ /dev/null @@ -1,187 +0,0 @@ -# Copyright 2018 D-Wave Systems Inc. -# -# 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. - -import networkx as nx -import dwave_networkx as dnx - -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ["maximum_clique", "clique_number", "is_clique"] - -@binary_quadratic_model_sampler(1) -def maximum_clique(G, sampler=None, lagrange=2.0, **sampler_args): - r"""Returns an approximate maximum clique. - - A clique in an undirected graph, G = (V, E), is a subset of the vertex set - :math:`C \subseteq V` such that for every two vertices in C there exists an edge - connecting the two. This is equivalent to saying that the subgraph - induced by C is complete (in some cases, the term clique may also refer - to the subgraph). A maximum clique is a clique of the largest - possible size in a given graph. - - This function works by finding the maximum independent set of the compliment - graph of the given graph G which is equivalent to finding maximum clique. - It defines a QUBO with ground states corresponding - to a maximum weighted independent set and uses the sampler to sample from it. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a maximum clique. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - lagrange : optional (default 2) - Lagrange parameter to weight constraints (no edges within set) - versus objective (largest set possible). - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - clique_nodes : list - List of nodes that form a maximum clique, as - determined by the given sampler. - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - - `Maximum Clique on Wikipedia `_ - - `Independent Set on Wikipedia `_ - - `QUBO on Wikipedia `_ - - Lucas, A. (2014). Ising formulations of many NP problems. - Frontiers in Physics, Volume 2, Article 5. - """ - if G is None: - raise ValueError("Expected NetworkX graph!") - - # finding the maximum clique in a graph is equivalent to finding - # the independent set in the complementary graph - complement_G = nx.complement(G) - return dnx.maximum_independent_set(complement_G, sampler, lagrange, **sampler_args) - - -@binary_quadratic_model_sampler(1) -def clique_number(G, sampler=None, lagrange=2.0, **sampler_args): - r"""Returns the number of vertices in the maximum clique of a graph. - - A maximum clique is a clique of the largest possible size in a given graph. - The clique number math:`\omega(G)` of a graph G is the number of - vertices in a maximum clique in G. The intersection number of - G is the smallest number of cliques that together cover all edges of G. - - This function works by finding the maximum independent set of the compliment - graph of the given graph G which is equivalent to finding maximum clique. - It defines a QUBO with ground states corresponding - to a maximum weighted independent set and uses the sampler to sample from it. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a maximum clique. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - lagrange : optional (default 2) - Lagrange parameter to weight constraints (no edges within set) - versus objective (largest set possible). - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - clique_nodes : list - List of nodes that form a maximum clique, as - determined by the given sampler. - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - - `Maximum Clique on Wikipedia `_ - """ - return len(maximum_clique(G, sampler, lagrange, **sampler_args)) - -def is_clique(G, clique_nodes): - """Determines whether the given nodes form a clique. - - A clique is a subset of nodes of an undirected graph such that every two - distinct nodes in the clique are adjacent. - - Parameters - ---------- - G : NetworkX graph - The graph on which to check the clique nodes. - - clique_nodes : list - List of nodes that form a clique, as - determined by the given sampler. - - Returns - ------- - is_clique : bool - True if clique_nodes forms a clique. - - Example - ------- - This example checks two sets of nodes, both derived from a - single Chimera unit cell, for an independent set. The first set is - the horizontal tile's nodes; the second has nodes from the horizontal and - verical tiles. - - >>> import dwave_networkx as dnx - >>> G = dnx.chimera_graph(1, 1, 4) - >>> dnx.is_clique(G, [0, 1, 2, 3]) - False - >>> dnx.is_clique(G, [0, 4]) - True - """ - for x in clique_nodes: - for y in clique_nodes: - if x != y: - if not(G.has_edge(x,y)): - return False - return True diff --git a/dwave_networkx/algorithms/cover.py b/dwave_networkx/algorithms/cover.py deleted file mode 100644 index 6abfa6c7e..000000000 --- a/dwave_networkx/algorithms/cover.py +++ /dev/null @@ -1,191 +0,0 @@ -# Copyright 2018 D-Wave Systems Inc. -# -# 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 dwave_networkx.algorithms.independent_set import maximum_weighted_independent_set -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ['min_weighted_vertex_cover', 'min_vertex_cover', 'is_vertex_cover'] - - -@binary_quadratic_model_sampler(2) -def min_weighted_vertex_cover(G, weight=None, sampler=None, lagrange=2.0, **sampler_args): - """Returns an approximate minimum weighted vertex cover. - - Defines a QUBO with ground states corresponding to a minimum weighted - vertex cover and uses the sampler to sample from it. - - A vertex cover is a set of vertices such that each edge of the graph - is incident with at least one vertex in the set. A minimum weighted - vertex cover is the vertex cover of minimum total node weight. - - Parameters - ---------- - G : NetworkX graph - - weight : string, optional (default None) - If None, every node has equal weight. If a string, use this node - attribute as the node weight. A node without this attribute is - assumed to have max weight. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - lagrange : optional (default 2) - Lagrange parameter to weight constraints versus objective. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - vertex_cover : list - List of nodes that the form a the minimum weighted vertex cover, as - determined by the given sampler. - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - https://en.wikipedia.org/wiki/Vertex_cover - - https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization - - References - ---------- - Based on the formulation presented in [Luc2014]_. - - """ - indep_nodes = set(maximum_weighted_independent_set(G, weight, sampler, lagrange, **sampler_args)) - return [v for v in G if v not in indep_nodes] - - -@binary_quadratic_model_sampler(1) -def min_vertex_cover(G, sampler=None, lagrange=2.0, **sampler_args): - """Returns an approximate minimum vertex cover. - - Defines a QUBO with ground states corresponding to a minimum - vertex cover and uses the sampler to sample from it. - - A vertex cover is a set of vertices such that each edge of the graph - is incident with at least one vertex in the set. A minimum vertex cover - is the vertex cover of smallest size. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum vertex cover. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - lagrange : optional (default 2) - Lagrange parameter to weight constraints versus objective. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - vertex_cover : list - List of nodes that form a minimum vertex cover, as - determined by the given sampler. - - Examples - -------- - This example uses a sampler from - `dimod `_ to find a minimum vertex - cover for a Chimera unit cell. Both the horizontal (vertices 0,1,2,3) and - vertical (vertices 4,5,6,7) tiles connect to all 16 edges, so repeated - executions can return either set. - - >>> import dwave_networkx as dnx - >>> import dimod - >>> sampler = dimod.ExactSolver() # small testing sampler - >>> G = dnx.chimera_graph(1, 1, 4) - >>> G.remove_node(7) # to give a unique solution - >>> dnx.min_vertex_cover(G, sampler, lagrange=2.0) - [4, 5, 6] - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - https://en.wikipedia.org/wiki/Vertex_cover - - https://en.wikipedia.org/wiki/Quadratic_unconstrained_binary_optimization - - Lucas, A. (2014). Ising formulations of many NP problems. - Frontiers in Physics, Volume 2, Article 5. - - """ - return min_weighted_vertex_cover(G, None, sampler, lagrange, **sampler_args) - - -def is_vertex_cover(G, vertex_cover): - """Determines whether the given set of vertices is a vertex cover of graph G. - - A vertex cover is a set of vertices such that each edge of the graph - is incident with at least one vertex in the set. - - Parameters - ---------- - G : NetworkX graph - The graph on which to check the vertex cover. - - vertex_cover : - Iterable of nodes. - - Returns - ------- - is_cover : bool - True if the given iterable forms a vertex cover. - - Examples - -------- - This example checks two covers for a graph, G, of a single Chimera - unit cell. The first uses the set of the four horizontal qubits, which - do constitute a cover; the second set removes one node. - - >>> import dwave_networkx as dnx - >>> G = dnx.chimera_graph(1, 1, 4) - >>> cover = [0, 1, 2, 3] - >>> dnx.is_vertex_cover(G,cover) - True - >>> cover = [0, 1, 2] - >>> dnx.is_vertex_cover(G,cover) - False - - """ - cover = set(vertex_cover) - return all(u in cover or v in cover for u, v in G.edges) diff --git a/dwave_networkx/algorithms/elimination_ordering.py b/dwave_networkx/algorithms/elimination_ordering.py deleted file mode 100644 index c47675e23..000000000 --- a/dwave_networkx/algorithms/elimination_ordering.py +++ /dev/null @@ -1,957 +0,0 @@ -# Copyright 2018 D-Wave Systems Inc. -# -# 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. - -import itertools - -from random import random, sample - -import networkx as nx - -from dwave_networkx.generators.pegasus import pegasus_coordinates -from dwave_networkx.generators.zephyr import zephyr_coordinates -from dwave_networkx.generators.chimera import chimera_coordinates - -__all__ = ['is_almost_simplicial', - 'is_simplicial', - 'chimera_elimination_order', - 'pegasus_elimination_order', - 'zephyr_elimination_order', - 'max_cardinality_heuristic', - 'min_fill_heuristic', - 'min_width_heuristic', - 'treewidth_branch_and_bound', - 'minor_min_width', - 'elimination_order_width', - ] - - -def is_simplicial(G, n): - """Determines whether a node n in G is simplicial. - - Parameters - ---------- - G : NetworkX graph - The graph on which to check whether node n is simplicial. - n : node - A node in graph G. - - Returns - ------- - is_simplicial : bool - True if its neighbors form a clique. - - Examples - -------- - This example checks whether node 0 is simplicial for two graphs: G, a - single Chimera unit cell, which is bipartite, and K_5, the :math:`K_5` - complete graph. - - >>> G = dnx.chimera_graph(1, 1, 4) - >>> K_5 = nx.complete_graph(5) - >>> dnx.is_simplicial(G, 0) - False - >>> dnx.is_simplicial(K_5, 0) - True - - """ - return all(u in G[v] for u, v in itertools.combinations(G[n], 2)) - - -def is_almost_simplicial(G, n): - """Determines whether a node n in G is almost simplicial. - - Parameters - ---------- - G : NetworkX graph - The graph on which to check whether node n is almost simplicial. - n : node - A node in graph G. - - Returns - ------- - is_almost_simplicial : bool - True if all but one of its neighbors induce a clique - - Examples - -------- - This example checks whether node 0 is simplicial or almost simplicial for - a :math:`K_5` complete graph with one edge removed. - - >>> K_5 = nx.complete_graph(5) - >>> K_5.remove_edge(1,3) - >>> dnx.is_simplicial(K_5, 0) - False - >>> dnx.is_almost_simplicial(K_5, 0) - True - - """ - for w in G[n]: - if all(u in G[v] for u, v in itertools.combinations(G[n], 2) if u != w and v != w): - return True - return False - - -def minor_min_width(G): - """Computes a lower bound for the treewidth of graph G. - - Parameters - ---------- - G : NetworkX graph - The graph on which to compute a lower bound on the treewidth. - - Returns - ------- - lb : int - A lower bound on the treewidth. - - Examples - -------- - This example computes a lower bound for the treewidth of the :math:`K_7` - complete graph. - - >>> K_7 = nx.complete_graph(7) - >>> dnx.minor_min_width(K_7) - 6 - - References - ---------- - Based on the algorithm presented in [Gog2004]_. - - """ - # we need only deal with the adjacency structure of G. We will also - # be manipulating it directly so let's go ahead and make a new one - adj = {v: set(u for u in G[v] if u != v) for v in G} - - lb = 0 # lower bound on treewidth - while len(adj) > 1: - - # get the node with the smallest degree - v = min(adj, key=lambda v: len(adj[v])) - - # find the vertex u such that the degree of u is minimal in the neighborhood of v - neighbors = adj[v] - - if not neighbors: - # if v is a singleton, then we can just delete it - del adj[v] - continue - - def neighborhood_degree(u): - Gu = adj[u] - return sum(w in Gu for w in neighbors) - - u = min(neighbors, key=neighborhood_degree) - - # update the lower bound - new_lb = len(adj[v]) - if new_lb > lb: - lb = new_lb - - # contract the edge between u, v - adj[v] = adj[v].union(n for n in adj[u] if n != v) - for n in adj[v]: - adj[n].add(v) - for n in adj[u]: - adj[n].discard(u) - del adj[u] - - return lb - - -def min_fill_heuristic(G): - """Computes an upper bound on the treewidth of graph G based on - the min-fill heuristic for the elimination ordering. - - Parameters - ---------- - G : NetworkX graph - The graph on which to compute an upper bound for the treewidth. - - Returns - ------- - treewidth_upper_bound : int - An upper bound on the treewidth of the graph G. - - order : list - An elimination order that induces the treewidth. - - Examples - -------- - This example computes an upper bound for the treewidth of the :math:`K_4` - complete graph. - - >>> K_4 = nx.complete_graph(4) - >>> tw, order = dnx.min_fill_heuristic(K_4) - - References - ---------- - Based on the algorithm presented in [Gog2004]_. - - """ - # we need only deal with the adjacency structure of G. We will also - # be manipulating it directly so let's go ahead and make a new one - adj = {v: set(u for u in G[v] if u != v) for v in G} - - num_nodes = len(adj) - - # preallocate the return values - order = [0] * num_nodes - upper_bound = 0 - - for i in range(num_nodes): - # get the node that adds the fewest number of edges when eliminated from the graph - v = min(adj, key=lambda x: _min_fill_needed_edges(adj, x)) - - # if the number of neighbours of v is higher than upper_bound, update - dv = len(adj[v]) - if dv > upper_bound: - upper_bound = dv - - # make v simplicial by making its neighborhood a clique then remove the - # node - _elim_adj(adj, v) - order[i] = v - - return upper_bound, order - - -def _min_fill_needed_edges(adj, n): - # determines how many edges would needed to be added to G in order - # to make node n simplicial. - e = 0 # number of edges needed - for u, v in itertools.combinations(adj[n], 2): - if u not in adj[v]: - e += 1 - # We add random() which picks a value in the range [0., 1.). This is ok because the - # e are all integers. By adding a small random value, we randomize which node is - # chosen without affecting correctness. - return e + random() - - -def min_width_heuristic(G): - """Computes an upper bound on the treewidth of graph G based on - the min-width heuristic for the elimination ordering. - - Parameters - ---------- - G : NetworkX graph - The graph on which to compute an upper bound for the treewidth. - - Returns - ------- - treewidth_upper_bound : int - An upper bound on the treewidth of the graph G. - - order : list - An elimination order that induces the treewidth. - - Examples - -------- - This example computes an upper bound for the treewidth of the :math:`K_4` - complete graph. - - >>> K_4 = nx.complete_graph(4) - >>> tw, order = dnx.min_width_heuristic(K_4) - - References - ---------- - Based on the algorithm presented in [Gog2004]_. - - """ - # we need only deal with the adjacency structure of G. We will also - # be manipulating it directly so let's go ahead and make a new one - adj = {v: set(u for u in G[v] if u != v) for v in G} - - num_nodes = len(adj) - - # preallocate the return values - order = [0] * num_nodes - upper_bound = 0 - - for i in range(num_nodes): - # get the node with the smallest degree. We add random() which picks a value - # in the range [0., 1.). This is ok because the lens are all integers. By - # adding a small random value, we randomize which node is chosen without affecting - # correctness. - v = min(adj, key=lambda u: len(adj[u]) + random()) - - # if the number of neighbours of v is higher than upper_bound, update - dv = len(adj[v]) - if dv > upper_bound: - upper_bound = dv - - # make v simplicial by making its neighborhood a clique then remove the - # node - _elim_adj(adj, v) - order[i] = v - - return upper_bound, order - - -def max_cardinality_heuristic(G): - """Computes an upper bound on the treewidth of graph G based on - the max-cardinality heuristic for the elimination ordering. - - Parameters - ---------- - G : NetworkX graph - The graph on which to compute an upper bound for the treewidth. - - Returns - ------- - treewidth_upper_bound : int - An upper bound on the treewidth of the graph G. - - order : list - An elimination order that induces the treewidth. - - Examples - -------- - This example computes an upper bound for the treewidth of the :math:`K_4` - complete graph. - - >>> K_4 = nx.complete_graph(4) - >>> tw, order = dnx.max_cardinality_heuristic(K_4) - - References - ---------- - Based on the algorithm presented in [Gog2004]_. - - """ - # we need only deal with the adjacency structure of G. We will also - # be manipulating it directly so let's go ahead and make a new one - adj = {v: set(u for u in G[v] if u != v) for v in G} - - num_nodes = len(adj) - - # preallocate the return values - order = [0] * num_nodes - upper_bound = 0 - - # we will need to track the nodes and how many labelled neighbors - # each node has - labelled_neighbors = {v: 0 for v in adj} - - # working backwards - for i in range(num_nodes): - # pick the node with the most labelled neighbors - v = max(labelled_neighbors, key=lambda u: labelled_neighbors[u] + random()) - del labelled_neighbors[v] - - # increment all of its neighbors - for u in adj[v]: - if u in labelled_neighbors: - labelled_neighbors[u] += 1 - - order[-(i + 1)] = v - - for v in order: - # if the number of neighbours of v is higher than upper_bound, update - dv = len(adj[v]) - if dv > upper_bound: - upper_bound = dv - - # make v simplicial by making its neighborhood a clique then remove the node - # add v to order - _elim_adj(adj, v) - - return upper_bound, order - - -def _elim_adj(adj, n): - """eliminates a variable, acting on the adj matrix of G, - returning set of edges that were added. - - Parameters - ---------- - adj: dict - A dict of the form {v: neighbors, ...} where v are - vertices in a graph and neighbors is a set. - - Returns - ---------- - new_edges: set of edges that were added by eliminating v. - - """ - neighbors = adj[n] - new_edges = set() - for u, v in itertools.combinations(neighbors, 2): - if v not in adj[u]: - adj[u].add(v) - adj[v].add(u) - new_edges.add((u, v)) - new_edges.add((v, u)) - for v in neighbors: - adj[v].discard(n) - del adj[n] - return new_edges - - -def elimination_order_width(G, order): - """Calculates the width of the tree decomposition induced by a - variable elimination order. - - Parameters - ---------- - G : NetworkX graph - The graph on which to compute the width of the tree decomposition. - - order : list - The elimination order. Must be a list of all of the variables - in G. - - Returns - ------- - treewidth : int - The width of the tree decomposition induced by order. - - Examples - -------- - This example computes the width of the tree decomposition for the :math:`K_4` - complete graph induced by an elimination order found through the min-width - heuristic. - - >>> K_4 = nx.complete_graph(4) - >>> tw, order = dnx.min_width_heuristic(K_4) - >>> print(tw) - 3 - >>> dnx.elimination_order_width(K_4, order) - 3 - - - """ - # we need only deal with the adjacency structure of G. We will also - # be manipulating it directly so let's go ahead and make a new one - adj = {v: set(u for u in G[v] if u != v) for v in G} - - treewidth = 0 - - for v in order: - - # get the degree of the eliminated variable - try: - dv = len(adj[v]) - except KeyError: - raise ValueError('{} is in order but not in G'.format(v)) - - # the treewidth is the max of the current treewidth and the degree - if dv > treewidth: - treewidth = dv - - # eliminate v by making it simplicial (acts on adj in place) - _elim_adj(adj, v) - - # if adj is not empty, then order did not include all of the nodes in G. - if adj: - raise ValueError('not all nodes in G were in order') - - return treewidth - - -def treewidth_branch_and_bound(G, elimination_order=None, treewidth_upperbound=None): - """Computes the treewidth of graph G and a corresponding perfect elimination ordering. - - Algorithm based on [Gog2004]_. - - Parameters - ---------- - G : NetworkX graph - The graph on which to compute the treewidth and perfect elimination ordering. - - elimination_order: list (optional, Default None) - An elimination order used as an initial best-known order. If a good - order is provided, it may speed up computation. If not provided, the - initial order is generated using the min-fill heuristic. - - treewidth_upperbound : int (optional, Default None) - An upper bound on the treewidth. Note that using - this parameter can result in no returned order. - - Returns - ------- - treewidth : int - The treewidth of graph G. - order : list - An elimination order that induces the treewidth. - - Examples - -------- - This example computes the treewidth for the :math:`K_7` - complete graph using an optionally provided elimination order (a sequential - ordering of the nodes, arbitrally chosen). - - >>> K_7 = nx.complete_graph(7) - >>> dnx.treewidth_branch_and_bound(K_7, [0, 1, 2, 3, 4, 5, 6]) - (6, [0, 1, 2, 3, 4, 5, 6]) - - References - ---------- - Based on the algorithm presented in [Gog2004]_. - """ - # empty graphs have treewidth 0 and the nodes can be eliminated in - # any order - if not any(G[v] for v in G): - return 0, list(G) - - # variable names are chosen to match the paper - - # our order will be stored in vector x, named to be consistent with - # the paper - x = [] # the partial order - - f = minor_min_width(G) # our current lower bound guess, f(s) in the paper - g = 0 # g(s) in the paper - - # we need the best current update we can find. - ub, order = min_fill_heuristic(G) - - # if the user has provided an upperbound or an elimination order, check those against - # our current best guess - if elimination_order is not None: - upperbound = elimination_order_width(G, elimination_order) - if upperbound <= ub: - ub, order = upperbound, elimination_order - - if treewidth_upperbound is not None and treewidth_upperbound < ub: - # in this case the order might never be found - ub, order = treewidth_upperbound, [] - - # best found encodes the ub and the order - best_found = ub, order - - # if our upper bound is the same as f, then we are done! Otherwise begin the - # algorithm. - if f < ub: - # we need only deal with the adjacency structure of G. We will also - # be manipulating it directly so let's go ahead and make a new one - adj = {v: set(u for u in G[v] if u != v) for v in G} - - best_found = _branch_and_bound(adj, x, g, f, best_found) - elif f > ub and treewidth_upperbound is None: - raise RuntimeError("logic error") - - return best_found - - -def _branch_and_bound(adj, x, g, f, best_found, skipable=set(), theorem6p2=None): - """ Recursive branch and bound for computing treewidth of a subgraph. - adj: adjacency list - x: partial elimination order - g: width of x so far - f: lower bound on width of any elimination order starting with x - best_found = ub,order: best upper bound on the treewidth found so far, and its elimination order - skipable: vertices that can be skipped according to Lemma 5.3 - theorem6p2: terms that have been explored/can be pruned according to Theorem 6.2 - """ - - # theorem6p2 checks for branches that can be pruned using Theorem 6.2 - if theorem6p2 is None: - theorem6p2 = _theorem6p2() - prune6p2, explored6p2, finished6p2 = theorem6p2 - # current6p2 is the list of prunable terms created during this instantiation of _branch_and_bound. - # These terms will only be use during this call and its successors, - # so they are removed before the function terminates. - current6p2 = list() - - # theorem6p4 checks for branches that can be pruned using Theorem 6.4. - # These terms do not need to be passed to successive calls to _branch_and_bound, - # so they are simply created and deleted during this call. - prune6p4, explored6p4 = _theorem6p4() - - # Note: theorem6p1 and theorem6p3 are a pruning strategies that are currently disabled - # # as they does not appear to be invoked regularly, - # and invoking it can require large memory allocations. - # This can be fixed in the future if there is evidence that it's useful. - # To add them in, define _branch_and_bound as follows: - # def _branch_and_bound(adj, x, g, f, best_found, skipable=set(), theorem6p1=None, - # theorem6p2=None, theorem6p3=None): - - # if theorem6p1 is None: - # theorem6p1 = _theorem6p1() - # prune6p1, explored6p1 = theorem6p1 - - # if theorem6p3 is None: - # theorem6p3 = _theorem6p3() - # prune6p3, explored6p3 = theorem6p3 - - # we'll need to know our current upper bound in several places - ub, order = best_found - - # ok, take care of the base case first - if len(adj) < 2: - # check if our current branch is better than the best we've already - # found and if so update our best solution accordingly. - if f < ub: - return (f, x + list(adj)) - elif f == ub and not order: - return (f, x + list(adj)) - else: - return best_found - - # so we have not yet reached the base case - # Note: theorem 6.4 gives a heuristic for choosing order of n in adj. - # Quick_bb suggests using a min-fill or random order. - # We don't need to consider the neighbors of the last vertex eliminated - sorted_adj = sorted((n for n in adj if n not in skipable), key=lambda x: _min_fill_needed_edges(adj, x)) - for n in sorted_adj: - - g_s = max(g, len(adj[n])) - - # according to Lemma 5.3, we can skip all of the neighbors of the last - # variable eliniated when choosing the next variable - # this does not get altered so we don't need a copy - next_skipable = adj[n] - - if prune6p2(x, n, next_skipable): - continue - - # update the state by eliminating n and adding it to the partial ordering - adj_s = {v: adj[v].copy() for v in adj} # create a new object - edges_n = _elim_adj(adj_s, n) - x_s = x + [n] # new partial ordering - - # pruning (disabled): - # if prune6p1(x_s): - # continue - - if prune6p4(edges_n): - continue - - # By Theorem 5.4, if any two vertices have ub + 1 common neighbors then - # we can add an edge between them - _theorem5p4(adj_s, ub) - - # ok, let's update our values - f_s = max(g_s, minor_min_width(adj_s)) - - g_s, f_s, as_list = _graph_reduction(adj_s, x_s, g_s, f_s) - - # pruning (disabled): - # if prune6p3(x, as_list, n): - # continue - - if f_s < ub: - best_found = _branch_and_bound(adj_s, x_s, g_s, f_s, best_found, - next_skipable, theorem6p2=theorem6p2) - # if theorem6p1, theorem6p3 are enabled, this should be called as: - # best_found = _branch_and_bound(adj_s, x_s, g_s, f_s, best_found, - # next_skipable, theorem6p1=theorem6p1, - # theorem6p2=theorem6p2,theorem6p3=theorem6p3) - ub, __ = best_found - - # store some information for pruning (disabled): - # explored6p3(x, n, as_list) - - prunable = explored6p2(x, n, next_skipable) - current6p2.append(prunable) - - explored6p4(edges_n) - - # store some information for pruning (disabled): - # explored6p1(x) - - for prunable in current6p2: - finished6p2(prunable) - - return best_found - - -def _graph_reduction(adj, x, g, f): - """we can go ahead and remove any simplicial or almost-simplicial vertices from adj. - """ - as_list = set() - as_nodes = {v for v in adj if len(adj[v]) <= f and is_almost_simplicial(adj, v)} - while as_nodes: - as_list.union(as_nodes) - for n in as_nodes: - - # update g and f - dv = len(adj[n]) - if dv > g: - g = dv - if g > f: - f = g - - # eliminate v - x.append(n) - _elim_adj(adj, n) - - # see if we have any more simplicial nodes - as_nodes = {v for v in adj if len(adj[v]) <= f and is_almost_simplicial(adj, v)} - - return g, f, as_list - - -def _theorem5p4(adj, ub): - """By Theorem 5.4, if any two vertices have ub + 1 common neighbors - then we can add an edge between them. - """ - new_edges = set() - for u, v in itertools.combinations(adj, 2): - if u in adj[v]: - # already an edge - continue - - if len(adj[u].intersection(adj[v])) > ub: - new_edges.add((u, v)) - - while new_edges: - for u, v in new_edges: - adj[u].add(v) - adj[v].add(u) - - new_edges = set() - for u, v in itertools.combinations(adj, 2): - if u in adj[v]: - continue - - if len(adj[u].intersection(adj[v])) > ub: - new_edges.add((u, v)) - - -def _theorem6p1(): - """See Theorem 6.1 in paper.""" - - pruning_set = set() - - def _prune(x): - if len(x) <= 2: - return False - # this is faster than tuple(x[-3:]) - key = (tuple(x[:-2]), x[-2], x[-1]) - return key in pruning_set - - def _explored(x): - if len(x) >= 3: - prunable = (tuple(x[:-2]), x[-1], x[-2]) - pruning_set.add(prunable) - - return _prune, _explored - - -def _theorem6p2(): - """See Theorem 6.2 in paper. - Prunes (x,...,a) when (x,a) is explored and a has the same neighbour set in both graphs. - """ - pruning_set2 = set() - - def _prune2(x, a, nbrs_a): - frozen_nbrs_a = frozenset(nbrs_a) - for i in range(len(x)): - key = (tuple(x[0:i]), a, frozen_nbrs_a) - if key in pruning_set2: - return True - return False - - def _explored2(x, a, nbrs_a): - prunable = (tuple(x), a, frozenset(nbrs_a)) # (s,a,N(a)) - pruning_set2.add(prunable) - return prunable - - def _finished2(prunable): - pruning_set2.remove(prunable) - - return _prune2, _explored2, _finished2 - - -def _theorem6p3(): - """See Theorem 6.3 in paper. - Prunes (s,b) when (s,a) is explored, b (almost) simplicial in (s,a), and a (almost) simplicial in (s,b) - """ - pruning_set3 = set() - - def _prune3(x, as_list, b): - for a in as_list: - key = (tuple(x), a, b) # (s,a,b) with (s,a) explored - if key in pruning_set3: - return True - return False - - def _explored3(x, a, as_list): - for b in as_list: - prunable = (tuple(x), a, b) # (s,a,b) with (s,a) explored - pruning_set3.add(prunable) - - return _prune3, _explored3 - - -def _theorem6p4(): - """See Theorem 6.4 in paper. - Let E(x) denote the edges added when eliminating x. (edges_x below). - Prunes (s,b) when (s,a) is explored and E(a) is a subset of E(b). - For this theorem we only record E(a) rather than (s,E(a)) - because we only need to check for pruning in the same s context - (i.e the same level of recursion). - """ - pruning_set4 = list() - - def _prune4(edges_b): - for edges_a in pruning_set4: - if edges_a.issubset(edges_b): - return True - return False - - def _explored4(edges_a): - pruning_set4.append(edges_a) # (s,E_a) with (s,a) explored - - return _prune4, _explored4 - - -def chimera_elimination_order(m, n=None, t=4, coordinates=False): - """Provides a variable elimination order for a Chimera graph. - - A graph defined by ``chimera_graph(m,n,t)`` has treewidth :math:`max(m,n)*t`. - This function outputs a variable elimination order inducing a tree - decomposition of that width. - - Parameters - ---------- - m : int - Number of rows in the Chimera lattice. - n : int (optional, default m) - Number of columns in the Chimera lattice. - t : int (optional, default 4) - Size of the shore within each Chimera tile. - coordinates bool (optional, default False): - If True, the elimination order is given in terms of 4-term Chimera - coordinates, otherwise given in linear indices. - - Returns - ------- - order : list - An elimination order that induces the treewidth of chimera_graph(m,n,t). - - Examples - -------- - - >>> G = dnx.chimera_elimination_order(1, 1, 4) # a single Chimera tile - - """ - if n is None: - n = m - - index_flip = m > n - if index_flip: - m, n = n, m - - def chimeraI(m0, n0, k0, l0): - if index_flip: - return m*2*t*n0 + 2*t*m0 + t*(1-k0) + l0 - else: - return n*2*t*m0 + 2*t*n0 + t*k0 + l0 - - order = [] - - for n_i in range(n): - for t_i in range(t): - for m_i in range(m): - order.append(chimeraI(m_i, n_i, 0, t_i)) - - for n_i in range(n): - for m_i in range(m): - for t_i in range(t): - order.append(chimeraI(m_i, n_i, 1, t_i)) - - if coordinates: - return list(chimera_coordinates(m,n,t).iter_linear_to_chimera(order)) - else: - return order - - -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` [Boo2019]_. - - Simple pegasus variable elimination order rules: - - - eliminate vertical qubits, one column at a time - - eliminate horizontal qubits in each column once their adjacent vertical - qubits have been eliminated - - Args - ---- - n : int - The size parameter for the Pegasus lattice. - - coordinates : bool, optional (default False) - If True, the elimination order is given in terms of 4-term Pegasus - coordinates, otherwise given in linear indices. - - Returns - ------- - order : list - An elimination order that provides an upper bound on the treewidth. - - """ - m = n - l = 12 - - # ordering for horizontal qubits in each tile, from east to west: - h_order = [4, 5, 6, 7, 0, 1, 2, 3, 8, 9, 10, 11] - order = [] - for n_i in range(n): # for each tile offset - # eliminate vertical qubits: - for l_i in range(0, l, 2): - for l_v in range(l_i, l_i + 2): - for m_i in range(m - 1): # for each column - order.append((0, n_i, l_v, m_i)) - # eliminate horizontal qubits: - if n_i > 0 and not(l_i % 4): - # a new set of horizontal qubits have had all their neighbouring vertical qubits eliminated. - for m_i in range(m): - for l_h in range(h_order[l_i], h_order[l_i] + 4): - order.append((1, m_i, l_h, n_i - 1)) - - if coordinates: - return order - else: - return list(pegasus_coordinates(n).iter_pegasus_to_linear(order)) - - -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` [Boo2021]_. - - Simple zephyr variable elimination rules: - - eliminate vertical qubits, one column at a time - - eliminate horizontal qubits in each column from top to bottom - - Args - ---- - m : int - Grid parameter for the Zephyr lattice. - t : int - Tile parameter for the Zephyr lattice. - coordinates : bool, optional (default False) - If True, the elimination order is given in terms of 4-term Zephyr - coordinates, otherwise given in linear indices. - - Returns - ------- - order : list - An elimination order that achieves an upper bound on the treewidth. - - """ - 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)]) - - if coordinates: - return order - else: - return list(zephyr_coordinates(m).iter_zephyr_to_linear(order)) - diff --git a/dwave_networkx/algorithms/max_cut.py b/dwave_networkx/algorithms/max_cut.py deleted file mode 100644 index 6c7767628..000000000 --- a/dwave_networkx/algorithms/max_cut.py +++ /dev/null @@ -1,143 +0,0 @@ -# Copyright 2018 D-Wave Systems Inc. -# -# 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 dwave_networkx.exceptions import DWaveNetworkXException -from dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ["maximum_cut", "weighted_maximum_cut"] - - -@binary_quadratic_model_sampler(1) -def maximum_cut(G, sampler=None, **sampler_args): - """Returns an approximate maximum cut. - - Defines an Ising problem with ground states corresponding to - a maximum cut and uses the sampler to sample from it. - - A maximum cut is a subset S of the vertices of G such that - the number of edges between S and the complementary subset - is as large as possible. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a maximum cut. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - S : set - A maximum cut of G. - - Example - ------- - This example uses a sampler from - `dimod `_ to find a maximum cut - for a graph of a Chimera unit cell created using the `chimera_graph()` - function. - - >>> import dimod - ... - >>> sampler = dimod.SimulatedAnnealingSampler() - >>> G = dnx.chimera_graph(1, 1, 4) - >>> cut = dnx.maximum_cut(G, sampler) - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - """ - # In order to form the Ising problem, we want to increase the - # energy by 1 for each edge between two nodes of the same color. - # The linear biases can all be 0. - h = {v: 0. for v in G} - J = {(u, v): 1 for u, v in G.edges} - - # draw the lowest energy sample from the sampler - response = sampler.sample_ising(h, J, **sampler_args) - sample = next(iter(response)) - - return set(v for v in G if sample[v] >= 0) - - -@binary_quadratic_model_sampler(1) -def weighted_maximum_cut(G, sampler=None, **sampler_args): - """Returns an approximate weighted maximum cut. - - Defines an Ising problem with ground states corresponding to - a weighted maximum cut and uses the sampler to sample from it. - - A weighted maximum cut is a subset S of the vertices of G that - maximizes the sum of the edge weights between S and its - complementary subset. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a weighted maximum cut. Each edge in G should - have a numeric `weight` attribute. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - S : set - A maximum cut of G. - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - """ - # In order to form the Ising problem, we want to increase the - # energy by 1 for each edge between two nodes of the same color. - # The linear biases can all be 0. - h = {v: 0. for v in G} - try: - J = {(u, v): G[u][v]['weight'] for u, v in G.edges} - except KeyError: - raise DWaveNetworkXException("edges must have 'weight' attribute") - - # draw the lowest energy sample from the sampler - response = sampler.sample_ising(h, J, **sampler_args) - sample = next(iter(response)) - - return set(v for v in G if sample[v] >= 0) From 1583c0bd9c3286a81f3cda2599774bac38042cc7 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 1 May 2026 01:21:17 +0200 Subject: [PATCH 04/27] Remove duplicate generators --- dimod/generators/independent_set.py | 265 ---------------------------- 1 file changed, 265 deletions(-) delete mode 100644 dimod/generators/independent_set.py diff --git a/dimod/generators/independent_set.py b/dimod/generators/independent_set.py deleted file mode 100644 index 798437ceb..000000000 --- a/dimod/generators/independent_set.py +++ /dev/null @@ -1,265 +0,0 @@ -# Copyright 2018 D-Wave Systems Inc. -# -# 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 dwave_networkx.utils import binary_quadratic_model_sampler - -__all__ = ["maximum_weighted_independent_set", - "maximum_weighted_independent_set_qubo", - "maximum_independent_set", - "is_independent_set", - ] - - -@binary_quadratic_model_sampler(2) -def maximum_weighted_independent_set(G, weight=None, sampler=None, lagrange=2.0, **sampler_args): - """Returns an approximate maximum weighted independent set. - - Defines a QUBO with ground states corresponding to a - maximum weighted independent set and uses the sampler to sample - from it. - - An independent set is a set of nodes such that the subgraph - of G induced by these nodes contains no edges. A maximum - independent set is an independent set of maximum total node weight. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a maximum cut weighted independent set. - - weight : string, optional (default None) - If None, every node has equal weight. If a string, use this node - attribute as the node weight. A node without this attribute is - assumed to have max weight. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - lagrange : optional (default 2) - Lagrange parameter to weight constraints (no edges within set) - versus objective (largest set possible). - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - indep_nodes : list - List of nodes that form a maximum weighted independent set, as - determined by the given sampler. - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - - `Independent Set on Wikipedia `_ - - `QUBO on Wikipedia `_ - - Lucas, A. (2014). Ising formulations of many NP problems. - Frontiers in Physics, Volume 2, Article 5. - - """ - # Get a QUBO representation of the problem - Q = maximum_weighted_independent_set_qubo(G, weight, lagrange) - - # use the sampler to find low energy states - response = sampler.sample_qubo(Q, **sampler_args) - - # we want the lowest energy sample - sample = next(iter(response)) - - # nodes that are spin up or true are exactly the ones in S. - return [node for node in sample if sample[node] > 0] - - -@binary_quadratic_model_sampler(1) -def maximum_independent_set(G, sampler=None, lagrange=2.0, **sampler_args): - """Returns an approximate maximum independent set. - - Defines a QUBO with ground states corresponding to a - maximum independent set and uses the sampler to sample from - it. - - An independent set is a set of nodes such that the subgraph - of G induced by these nodes contains no edges. A maximum - independent set is an independent set of largest possible size. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a maximum cut independent set. - - sampler - A binary quadratic model sampler. A sampler is a process that - samples from low energy states in models defined by an Ising - equation or a Quadratic Unconstrained Binary Optimization - Problem (QUBO). A sampler is expected to have a 'sample_qubo' - and 'sample_ising' method. A sampler is expected to return an - iterable of samples, in order of increasing energy. If no - sampler is provided, one must be provided using the - `set_default_sampler` function. - - lagrange : optional (default 2) - Lagrange parameter to weight constraints (no edges within set) - versus objective (largest set possible). - - sampler_args - Additional keyword parameters are passed to the sampler. - - Returns - ------- - indep_nodes : list - List of nodes that form a maximum independent set, as - determined by the given sampler. - - Example - ------- - This example uses a sampler from - `dimod `_ to find a maximum - independent set for a graph of a Chimera unit cell created using the - `chimera_graph()` function. - - >>> import dimod - >>> sampler = dimod.SimulatedAnnealingSampler() - >>> G = dnx.chimera_graph(1, 1, 4) - >>> indep_nodes = dnx.maximum_independent_set(G, sampler) - - Notes - ----- - Samplers by their nature may not return the optimal solution. This - function does not attempt to confirm the quality of the returned - sample. - - References - ---------- - - `Independent Set on Wikipedia `_ - - `QUBO on Wikipedia `_ - - Lucas, A. (2014). Ising formulations of many NP problems. - Frontiers in Physics, Volume 2, Article 5. - - """ - return maximum_weighted_independent_set(G, None, sampler, lagrange, **sampler_args) - - -def is_independent_set(G, indep_nodes): - """Determines whether the given nodes form an independent set. - - An independent set is a set of nodes such that the subgraph - of G induced by these nodes contains no edges. - - Parameters - ---------- - G : NetworkX graph - The graph on which to check the independent set. - - indep_nodes : list - List of nodes that form a maximum independent set, as - determined by the given sampler. - - Returns - ------- - is_independent : bool - True if indep_nodes form an independent set. - - Example - ------- - This example checks two sets of nodes, both derived from a - single Chimera unit cell, for an independent set. The first set is - the horizontal tile's nodes; the second has nodes from the horizontal and - verical tiles. - - >>> import dwave_networkx as dnx - >>> G = dnx.chimera_graph(1, 1, 4) - >>> dnx.is_independent_set(G, [0, 1, 2, 3]) - True - >>> dnx.is_independent_set(G, [0, 4]) - False - - """ - return len(G.subgraph(indep_nodes).edges) == 0 - - -def maximum_weighted_independent_set_qubo(G, weight=None, lagrange=2.0): - """Return the QUBO with ground states corresponding to a maximum weighted independent set. - - Parameters - ---------- - G : NetworkX graph - - weight : string, optional (default None) - If None, every node has equal weight. If a string, use this node - attribute as the node weight. A node without this attribute is - assumed to have max weight. - - lagrange : optional (default 2) - Lagrange parameter to weight constraints (no edges within set) - versus objective (largest set possible). - - Returns - ------- - QUBO : dict - The QUBO with ground states corresponding to a maximum weighted independent set. - - Examples - -------- - - >>> from dwave_networkx.algorithms.independent_set import maximum_weighted_independent_set_qubo - ... - >>> G = nx.path_graph(3) - >>> Q = maximum_weighted_independent_set_qubo(G, weight='weight', lagrange=2.0) - >>> Q[(0, 0)] - -1.0 - >>> Q[(1, 1)] - -1.0 - >>> Q[(0, 1)] - 2.0 - - """ - - # empty QUBO for an empty graph - if not G: - return {} - - # We assume that the sampler can handle an unstructured QUBO problem, so let's set one up. - # Let us define the largest independent set to be S. - # For each node n in the graph, we assign a boolean variable v_n, where v_n = 1 when n - # is in S and v_n = 0 otherwise. - # We call the matrix defining our QUBO problem Q. - # On the diagnonal, we assign the linear bias for each node to be the negative of its weight. - # This means that each node is biased towards being in S. Weights are scaled to a maximum of 1. - # Negative weights are considered 0. - # On the off diagnonal, we assign the off-diagonal terms of Q to be 2. Thus, if both - # nodes are in S, the overall energy is increased by 2. - cost = dict(G.nodes(data=weight, default=1)) - scale = max(cost.values()) - Q = {(node, node): min(-cost[node] / scale, 0.0) for node in G} - Q.update({edge: lagrange for edge in G.edges}) - - return Q From a75e281046930d7db67d2bdd1f6bcdf70453b902 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Mon, 4 May 2026 20:48:43 +0200 Subject: [PATCH 05/27] Make the new dnx generators available in dimod.generators ns --- dimod/generators/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/dimod/generators/__init__.py b/dimod/generators/__init__.py index d6e9ac06a..bf258a029 100644 --- a/dimod/generators/__init__.py +++ b/dimod/generators/__init__.py @@ -16,6 +16,7 @@ from dimod.generators.binpacking import * from dimod.generators.bpsp import * from dimod.generators.chimera import * +from dimod.generators.coloring import * from dimod.generators.constraints import * from dimod.generators.fcl import * from dimod.generators.gates import * @@ -23,8 +24,13 @@ from dimod.generators.integer import * from dimod.generators.knapsack import * from dimod.generators.magic_square import * +from dimod.generators.markov import * +from dimod.generators.matching import * from dimod.generators.multi_knapsack import * from dimod.generators.quadratic_assignment import * +from dimod.generators.partition import * from dimod.generators.random import * from dimod.generators.satisfiability import * +from dimod.generators.social import * +from dimod.generators.tsp import * from dimod.generators.wireless import * \ No newline at end of file From 470ab565da2c8437ca0c2b5a5850de56af29fcb8 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Tue, 5 May 2026 01:31:41 +0200 Subject: [PATCH 06/27] Import relevant problem generator tests from dwave_networkx Squashed QUBO tests from: tests/test_coloring.py tests/test_markov.py tests/test_matching.py tests/test_tsp.py Full history: https://github.com/dwavesystems/dwave-graphs/tree/legacy/dwave-networkx/tests Co-authored-by: Alexander Condello Co-authored-by: Alexander Condello Co-authored-by: Heidi Tong Co-authored-by: James Cortese Co-authored-by: Jeremy Fan Co-authored-by: Joel Pasvolsky Co-authored-by: Kelly Boothby Co-authored-by: Maximilian Schmidt Co-authored-by: Victoria Goliber <38987205+vgoliber@users.noreply.github.com> Co-authored-by: Victoria Goliber --- tests/test_generators.py | 433 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 433 insertions(+) diff --git a/tests/test_generators.py b/tests/test_generators.py index 94f5be40e..8d6393e6a 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -19,6 +19,7 @@ import itertools import numpy as np +import parameterized try: import networkx as nx @@ -1875,3 +1876,435 @@ def test_feasible(self): x = {f'x_{i}_{j}': 1 if i==j else 0 for i in range(num_locations)} lhs = cqm.constraints[f'facility_constraint_{j}'].lhs.energy(x) self.assertEqual(lhs, 0) + + +class TestMinVertexColorQubo(unittest.TestCase): + def test_chromatic_number(self): + G = nx.cycle_graph('abcd') + + # when the chromatic number is fixed this is exactly vertex_color + self.assertEqual(dnx.min_vertex_color_qubo(G, chromatic_lb=2, + chromatic_ub=2), + dnx.vertex_color_qubo(G, 2)) + + +class TestVertexColorQUBO(unittest.TestCase): + def test_single_node(self): + G = nx.Graph() + G.add_node('a') + + # a single color + Q = dnx.vertex_color_qubo(G, ['red']) + + self.assertEqual(Q, {(('a', 'red'), ('a', 'red')): -1}) + + def test_4cycle(self): + G = nx.cycle_graph('abcd') + + Q = dnx.vertex_color_qubo(G, 2) + + sampleset = dimod.ExactSolver().sample_qubo(Q) + + # check that the ground state is a valid coloring + ground_energy = sampleset.first.energy + + colorings = [] + for sample, en in sampleset.data(['sample', 'energy']): + if en > ground_energy: + break + + coloring = {} + for (v, c), val in sample.items(): + if val: + coloring[v] = c + + self.assertTrue(dnx.is_vertex_coloring(G, coloring)) + + colorings.append(coloring) + + # there are two valid colorings + self.assertEqual(len(colorings), 2) + + self.assertEqual(ground_energy, -len(G)) + + def test_num_variables(self): + G = nx.Graph() + G.add_nodes_from(range(15)) + + Q = dnx.vertex_color_qubo(G, 7) + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2) + + # add one edge + G.add_edge(0, 1) + Q = dnx.vertex_color_qubo(G, 7) + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2 + 7) + + def test_docstring_stats(self): + # get a complex-ish graph + G = nx.karate_club_graph() + + colors = range(10) + + Q = dnx.vertex_color_qubo(G, colors) + + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + + self.assertEqual(len(bqm), len(G)*len(colors)) + self.assertEqual(len(bqm.quadratic), len(G)*len(colors)*(len(colors)-1)/2 + + len(G.edges)*len(colors)) + + +class Test_sample_markov_network_bqm(unittest.TestCase): + def test_one_node(self): + potentials = {'a': {(0,): 1.2, (1,): .4}} + + bqm = dnx.markov_network_bqm(dnx.markov_network(potentials)) + + for edge, potential in potentials.items(): + for config, energy in potential.items(): + sample = dict(zip(edge, config)) + self.assertAlmostEqual(bqm.energy(sample), energy) + + def test_one_edge(self): + potentials = {'ab': {(0, 0): 1.2, (1, 0): .4, + (0, 1): 1.3, (1, 1): -4}} + + bqm = dnx.markov_network_bqm(dnx.markov_network(potentials)) + + for edge, potential in potentials.items(): + for config, energy in potential.items(): + sample = dict(zip(edge, config)) + self.assertAlmostEqual(bqm.energy(sample), energy) + + def test_typical(self): + potentials = {'a': {(0,): 1.5, (1,): -.5}, + 'ab': {(0, 0): 1.2, (1, 0): .4, + (0, 1): 1.3, (1, 1): -4}, + 'bc': {(0, 0): 1.7, (1, 0): .4, + (0, 1): -1, (1, 1): -4}, + 'd': {(0,): -.5, (1,): 1.6}} + + bqm = dnx.markov_network_bqm(dnx.markov_network(potentials)) + + samples = dimod.ExactSolver().sample(bqm) + + for sample, energy in samples.data(['sample', 'energy']): + + en = 0 + for interaction, potential in potentials.items(): + config = tuple(sample[v] for v in interaction) + en += potential[config] + + self.assertAlmostEqual(en, energy) + + +@parameterized.parameterized_class( + 'graph', + [[nx.Graph()], + [nx.path_graph(10)], + [nx.complete_graph(4)], + [nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4), (3, 5)])], + [nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (1, 5)])], + [nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4)])], + [nx.Graph({0: [], 1: [6], 2: [5], 3: [4], 4: [3], 2: [5], 6: [1]})] + # [nx.Graph([(0, 3), (0, 5), (0, 6), (1, 2), (1, 3), (1, 4), (1, 7), + # (2, 4), (2, 6), (3, 6), (3, 7), (4, 7), (6, 7)])], # slow + ] + ) +class TestMatching(unittest.TestCase): + def test_matching_bqm(self): + bqm = dnx.matching_bqm(self.graph) + + # the ground states should be exactly the matchings of G + sampleset = dimod.ExactSolver().sample(bqm) + + for sample, energy in sampleset.data(['sample', 'energy']): + edges = [v for v, val in sample.items() if val > 0] + self.assertEqual(nx.is_matching(self.graph, edges), energy == 0) + self.assertTrue(energy == 0 or energy >= 1) + + def test_maximal_matching(self): + matching = dnx.algorithms.matching.maximal_matching( + self.graph, dimod.ExactSolver()) + self.assertTrue(nx.is_maximal_matching(self.graph, matching)) + + def test_maximal_matching_bqm(self): + bqm = dnx.maximal_matching_bqm(self.graph) + + # the ground states should be exactly the maximal matchings of G + sampleset = dimod.ExactSolver().sample(bqm) + + for sample, energy in sampleset.data(['sample', 'energy']): + edges = set(v for v, val in sample.items() if val > 0) + self.assertEqual(nx.is_maximal_matching(self.graph, edges), + energy == 0) + self.assertGreaterEqual(energy, 0) + + def test_min_maximal_matching(self): + matching = dnx.min_maximal_matching(self.graph, dimod.ExactSolver()) + self.assertTrue(nx.is_maximal_matching(self.graph, matching)) + + def test_min_maximal_matching_bqm(self): + bqm = dnx.min_maximal_matching_bqm(self.graph) + + if len(self.graph) == 0: + self.assertEqual(len(bqm.linear), 0) + return + + # the ground states should be exactly the minimum maximal matchings of + # G + sampleset = dimod.ExactSolver().sample(bqm) + + # we'd like to use sampleset.lowest() but it didn't exist in dimod + # 0.8.0 + ground_energy = sampleset.first.energy + cardinalities = set() + for sample, energy in sampleset.data(['sample', 'energy']): + if energy > ground_energy: + continue + edges = set(v for v, val in sample.items() if val > 0) + self.assertTrue(nx.is_maximal_matching(self.graph, edges)) + cardinalities.add(len(edges)) + + # all ground have the same cardinality (or it's empty) + self.assertEqual(len(cardinalities), 1) + cardinality, = cardinalities + + # everything that's not ground has a higher energy + for sample, energy in sampleset.data(['sample', 'energy']): + edges = set(v for v, val in sample.items() if val > 0) + if energy != sampleset.first.energy: + if nx.is_maximal_matching(self.graph, edges): + self.assertGreater(len(edges), cardinality) + + +class TestTSPQUBO(unittest.TestCase): + def test_empty(self): + Q = dnx.traveling_salesperson_qubo(nx.Graph()) + self.assertEqual(Q, {}) + + def test_k3(self): + # 3cycle so all paths are equally good + G = nx.Graph() + G.add_weighted_edges_from([('a', 'b', 0.5), + ('b', 'c', 1.0), + ('a', 'c', 2.0)]) + + Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + + # all routes are min weight + min_routes = list(itertools.permutations(G.nodes)) + + # get the min energy of the qubo + sampleset = dimod.ExactSolver().sample(bqm) + ground_energy = sampleset.first.energy + + # all possible routes are equally good + for route in min_routes: + sample = {v: 0 for v in bqm.variables} + for idx, city in enumerate(route): + sample[(city, idx)] = 1 + self.assertAlmostEqual(bqm.energy(sample), ground_energy) + + # all min-energy solutions are valid routes + ground_count = 0 + for sample, energy in sampleset.data(['sample', 'energy']): + if abs(energy - ground_energy) > .001: + break + ground_count += 1 + + self.assertEqual(ground_count, len(min_routes)) + + def test_k3_bidirectional(self): + G = nx.DiGraph() + G.add_weighted_edges_from([('a', 'b', 0.5), + ('b', 'a', 0.5), + ('b', 'c', 1.0), + ('c', 'b', 1.0), + ('a', 'c', 2.0), + ('c', 'a', 2.0)]) + + Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + + # all routes are min weight + min_routes = list(itertools.permutations(G.nodes)) + + # get the min energy of the qubo + sampleset = dimod.ExactSolver().sample(bqm) + ground_energy = sampleset.first.energy + + # all possible routes are equally good + for route in min_routes: + sample = {v: 0 for v in bqm.variables} + for idx, city in enumerate(route): + sample[(city, idx)] = 1 + self.assertAlmostEqual(bqm.energy(sample), ground_energy) + + # all min-energy solutions are valid routes + ground_count = 0 + for sample, energy in sampleset.data(['sample', 'energy']): + if abs(energy - ground_energy) > .001: + break + ground_count += 1 + + self.assertEqual(ground_count, len(min_routes)) + + def test_graph_missing_edges(self): + G1 = nx.Graph() + G1.add_weighted_edges_from([ + ('a', 'b', 0.5), + ('b', 'c', 1.0), + ('a', 'c', 2.0), + ]) + Q1 = dnx.traveling_salesperson_qubo(G1, lagrange=10) + + G2 = nx.Graph() + G2.add_weighted_edges_from([ + ('a', 'b', 0.5), + ('a', 'c', 2.0), + ]) + # make sure that missing_edge_weight gets applied correctly + Q2 = dnx.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) + + self.assertDictEqual(Q1, Q2) + + def test_digraph_missing_edges(self): + G1 = nx.DiGraph() + G1.add_weighted_edges_from([ + ('a', 'b', 0.5), + ('b', 'a', 0.8), + ('b', 'c', 1.0), + ('c', 'b', 0.7), + ('a', 'c', 2.0), + ('c', 'a', 2.0), + ]) + Q1 = dnx.traveling_salesperson_qubo(G1, lagrange=10) + + G2 = nx.DiGraph() + G2.add_weighted_edges_from([ + ('a', 'b', 0.5), + ('b', 'a', 0.8), + ('c', 'b', 0.7), + ('a', 'c', 2.0), + ('c', 'a', 2.0), + ]) + + # make sure that missing_edge_weight gets applied correctly + Q2 = dnx.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) + + self.assertDictEqual(Q1, Q2) + + def test_k4_equal_weights(self): + # k5 with all equal weights so all paths are equally good + G = nx.Graph() + G.add_weighted_edges_from((u, v, .5) + for u, v in itertools.combinations(range(4), 2)) + + Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + + # all routes are min weight + min_routes = list(itertools.permutations(G.nodes)) + + # get the min energy of the qubo + sampleset = dimod.ExactSolver().sample(bqm) + ground_energy = sampleset.first.energy + + # all possible routes are equally good + for route in min_routes: + sample = {v: 0 for v in bqm.variables} + for idx, city in enumerate(route): + sample[(city, idx)] = 1 + self.assertAlmostEqual(bqm.energy(sample), ground_energy) + + # all min-energy solutions are valid routes + ground_count = 0 + for sample, energy in sampleset.data(['sample', 'energy']): + if abs(energy - ground_energy) > .001: + break + ground_count += 1 + + self.assertEqual(ground_count, len(min_routes)) + + def test_k4(self): + # good routes are 0,1,2,3 or 3,2,1,0 (and their rotations) + G = nx.Graph() + G.add_weighted_edges_from([(0, 1, 1), + (1, 2, 1), + (2, 3, 1), + (3, 0, 1), + (0, 2, 2), + (1, 3, 2)]) + + Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + + # good routes won't have 0<->2 or 1<->3 + min_routes = [(0, 1, 2, 3), + (1, 2, 3, 0), + (2, 3, 0, 1), + (1, 2, 3, 0), + (3, 2, 1, 0), + (2, 1, 0, 3), + (1, 0, 3, 2), + (0, 3, 2, 1)] + + # get the min energy of the qubo + sampleset = dimod.ExactSolver().sample(bqm) + ground_energy = sampleset.first.energy + + # all possible routes are equally good + for route in min_routes: + sample = {v: 0 for v in bqm.variables} + for idx, city in enumerate(route): + sample[(city, idx)] = 1 + self.assertAlmostEqual(bqm.energy(sample), ground_energy) + + # all min-energy solutions are valid routes + ground_count = 0 + for sample, energy in sampleset.data(['sample', 'energy']): + if abs(energy - ground_energy) > .001: + break + ground_count += 1 + + self.assertEqual(ground_count, len(min_routes)) + + def test_weighted_complete_graph(self): + G = nx.Graph() + G.add_weighted_edges_from({(0, 1, 1), (0, 2, 100), + (0, 3, 1), (1, 2, 1), + (1, 3, 100), (2, 3, 1)}) + + lagrange = 5.0 + + Q = dnx.traveling_salesperson_qubo(G, lagrange, 'weight') + + N = G.number_of_nodes() + correct_sum = G.size('weight')*2*N-2*N*N*lagrange+2*N*N*(N-1)*lagrange + + actual_sum = sum(Q.values()) + + self.assertEqual(correct_sum, actual_sum) + + def test_exceptions(self): + G = nx.Graph([(0, 1)]) + with self.assertRaises(ValueError): + dnx.traveling_salesperson_qubo(G) + + def test_docstring_size(self): + # in the docstring we state the size of the resulting BQM, this checks + # that + for n in range(3, 20): + G = nx.Graph() + G.add_weighted_edges_from((u, v, .5) + for u, v + in itertools.combinations(range(n), 2)) + Q = dnx.traveling_salesperson_qubo(G) + bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + + self.assertEqual(len(bqm), n**2) + self.assertEqual(len(bqm.quadratic), 2*n*n*(n - 1)) From 292cdecde3001dac9bcd3404cea611d6c6de0ece Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Tue, 5 May 2026 01:53:03 +0200 Subject: [PATCH 07/27] Update imported dnx tests --- tests/requirements.txt | 3 ++ tests/test_generators.py | 79 ++++++++++++++++++++-------------------- 2 files changed, 43 insertions(+), 39 deletions(-) diff --git a/tests/requirements.txt b/tests/requirements.txt index 5505b0629..fd0528377 100644 --- a/tests/requirements.txt +++ b/tests/requirements.txt @@ -4,3 +4,6 @@ pandas~=2.3.0; python_version < '3.14' pandas~=3.0.0; python_version >= '3.14' networkx==2.6.3 pymongo~=4.17.0 + +# TODO: update to dwave-graphs once released +dwave_networkx diff --git a/tests/test_generators.py b/tests/test_generators.py index 8d6393e6a..8ed4e019f 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -28,6 +28,15 @@ else: _networkx = True +# TODO: update to dwave-graphs once released +try: + import dwave_networkx as dnx +except ImportError: + _dnx = False +else: + _dnx = True + + class TestRandomGNMRandomBQM(unittest.TestCase): def test_bias_generator(self): def gen(n): @@ -1883,9 +1892,10 @@ def test_chromatic_number(self): G = nx.cycle_graph('abcd') # when the chromatic number is fixed this is exactly vertex_color - self.assertEqual(dnx.min_vertex_color_qubo(G, chromatic_lb=2, - chromatic_ub=2), - dnx.vertex_color_qubo(G, 2)) + self.assertEqual( + dimod.generators.coloring.min_vertex_color_qubo(G, chromatic_lb=2, chromatic_ub=2), + dimod.generators.coloring.vertex_color_qubo(G, 2) + ) class TestVertexColorQUBO(unittest.TestCase): @@ -1894,14 +1904,14 @@ def test_single_node(self): G.add_node('a') # a single color - Q = dnx.vertex_color_qubo(G, ['red']) + Q = dimod.generators.coloring.vertex_color_qubo(G, ['red']) self.assertEqual(Q, {(('a', 'red'), ('a', 'red')): -1}) def test_4cycle(self): G = nx.cycle_graph('abcd') - Q = dnx.vertex_color_qubo(G, 2) + Q = dimod.generators.coloring.vertex_color_qubo(G, 2) sampleset = dimod.ExactSolver().sample_qubo(Q) @@ -1918,7 +1928,8 @@ def test_4cycle(self): if val: coloring[v] = c - self.assertTrue(dnx.is_vertex_coloring(G, coloring)) + is_vertex_coloring = all(coloring[u] != coloring[v] for u, v in G.edges) + self.assertTrue(is_vertex_coloring) colorings.append(coloring) @@ -1931,13 +1942,13 @@ def test_num_variables(self): G = nx.Graph() G.add_nodes_from(range(15)) - Q = dnx.vertex_color_qubo(G, 7) + Q = dimod.generators.coloring.vertex_color_qubo(G, 7) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2) # add one edge G.add_edge(0, 1) - Q = dnx.vertex_color_qubo(G, 7) + Q = dimod.generators.coloring.vertex_color_qubo(G, 7) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2 + 7) @@ -1947,7 +1958,7 @@ def test_docstring_stats(self): colors = range(10) - Q = dnx.vertex_color_qubo(G, colors) + Q = dimod.generators.coloring.vertex_color_qubo(G, colors) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) @@ -1956,11 +1967,12 @@ def test_docstring_stats(self): + len(G.edges)*len(colors)) +@unittest.skipUnless(_dnx, "no dwave-graphs installed") class Test_sample_markov_network_bqm(unittest.TestCase): def test_one_node(self): potentials = {'a': {(0,): 1.2, (1,): .4}} - bqm = dnx.markov_network_bqm(dnx.markov_network(potentials)) + bqm = dimod.generators.markov.markov_network_bqm(dnx.markov_network(potentials)) for edge, potential in potentials.items(): for config, energy in potential.items(): @@ -1971,7 +1983,7 @@ def test_one_edge(self): potentials = {'ab': {(0, 0): 1.2, (1, 0): .4, (0, 1): 1.3, (1, 1): -4}} - bqm = dnx.markov_network_bqm(dnx.markov_network(potentials)) + bqm = dimod.generators.markov.markov_network_bqm(dnx.markov_network(potentials)) for edge, potential in potentials.items(): for config, energy in potential.items(): @@ -1986,7 +1998,7 @@ def test_typical(self): (0, 1): -1, (1, 1): -4}, 'd': {(0,): -.5, (1,): 1.6}} - bqm = dnx.markov_network_bqm(dnx.markov_network(potentials)) + bqm = dimod.generators.markov.markov_network_bqm(dnx.markov_network(potentials)) samples = dimod.ExactSolver().sample(bqm) @@ -2008,14 +2020,12 @@ def test_typical(self): [nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4), (3, 5)])], [nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (1, 4), (1, 5)])], [nx.Graph([(0, 1), (0, 2), (1, 2), (1, 3), (2, 4), (3, 4)])], - [nx.Graph({0: [], 1: [6], 2: [5], 3: [4], 4: [3], 2: [5], 6: [1]})] - # [nx.Graph([(0, 3), (0, 5), (0, 6), (1, 2), (1, 3), (1, 4), (1, 7), - # (2, 4), (2, 6), (3, 6), (3, 7), (4, 7), (6, 7)])], # slow + [nx.Graph({0: [], 1: [6], 2: [5], 3: [4], 4: [3], 2: [5], 6: [1]})], ] ) class TestMatching(unittest.TestCase): def test_matching_bqm(self): - bqm = dnx.matching_bqm(self.graph) + bqm = dimod.generators.matching.matching_bqm(self.graph) # the ground states should be exactly the matchings of G sampleset = dimod.ExactSolver().sample(bqm) @@ -2025,13 +2035,8 @@ def test_matching_bqm(self): self.assertEqual(nx.is_matching(self.graph, edges), energy == 0) self.assertTrue(energy == 0 or energy >= 1) - def test_maximal_matching(self): - matching = dnx.algorithms.matching.maximal_matching( - self.graph, dimod.ExactSolver()) - self.assertTrue(nx.is_maximal_matching(self.graph, matching)) - def test_maximal_matching_bqm(self): - bqm = dnx.maximal_matching_bqm(self.graph) + bqm = dimod.generators.matching.maximal_matching_bqm(self.graph) # the ground states should be exactly the maximal matchings of G sampleset = dimod.ExactSolver().sample(bqm) @@ -2042,12 +2047,8 @@ def test_maximal_matching_bqm(self): energy == 0) self.assertGreaterEqual(energy, 0) - def test_min_maximal_matching(self): - matching = dnx.min_maximal_matching(self.graph, dimod.ExactSolver()) - self.assertTrue(nx.is_maximal_matching(self.graph, matching)) - def test_min_maximal_matching_bqm(self): - bqm = dnx.min_maximal_matching_bqm(self.graph) + bqm = dimod.generators.matching.min_maximal_matching_bqm(self.graph) if len(self.graph) == 0: self.assertEqual(len(bqm.linear), 0) @@ -2082,7 +2083,7 @@ def test_min_maximal_matching_bqm(self): class TestTSPQUBO(unittest.TestCase): def test_empty(self): - Q = dnx.traveling_salesperson_qubo(nx.Graph()) + Q = dimod.generators.tsp.traveling_salesperson_qubo(nx.Graph()) self.assertEqual(Q, {}) def test_k3(self): @@ -2092,7 +2093,7 @@ def test_k3(self): ('b', 'c', 1.0), ('a', 'c', 2.0)]) - Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) # all routes are min weight @@ -2127,7 +2128,7 @@ def test_k3_bidirectional(self): ('a', 'c', 2.0), ('c', 'a', 2.0)]) - Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) # all routes are min weight @@ -2160,7 +2161,7 @@ def test_graph_missing_edges(self): ('b', 'c', 1.0), ('a', 'c', 2.0), ]) - Q1 = dnx.traveling_salesperson_qubo(G1, lagrange=10) + Q1 = dimod.generators.tsp.traveling_salesperson_qubo(G1, lagrange=10) G2 = nx.Graph() G2.add_weighted_edges_from([ @@ -2168,7 +2169,7 @@ def test_graph_missing_edges(self): ('a', 'c', 2.0), ]) # make sure that missing_edge_weight gets applied correctly - Q2 = dnx.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) + Q2 = dimod.generators.tsp.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) self.assertDictEqual(Q1, Q2) @@ -2182,7 +2183,7 @@ def test_digraph_missing_edges(self): ('a', 'c', 2.0), ('c', 'a', 2.0), ]) - Q1 = dnx.traveling_salesperson_qubo(G1, lagrange=10) + Q1 = dimod.generators.tsp.traveling_salesperson_qubo(G1, lagrange=10) G2 = nx.DiGraph() G2.add_weighted_edges_from([ @@ -2194,7 +2195,7 @@ def test_digraph_missing_edges(self): ]) # make sure that missing_edge_weight gets applied correctly - Q2 = dnx.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) + Q2 = dimod.generators.tsp.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) self.assertDictEqual(Q1, Q2) @@ -2204,7 +2205,7 @@ def test_k4_equal_weights(self): G.add_weighted_edges_from((u, v, .5) for u, v in itertools.combinations(range(4), 2)) - Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) # all routes are min weight @@ -2240,7 +2241,7 @@ def test_k4(self): (0, 2, 2), (1, 3, 2)]) - Q = dnx.traveling_salesperson_qubo(G, lagrange=10) + Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) # good routes won't have 0<->2 or 1<->3 @@ -2281,7 +2282,7 @@ def test_weighted_complete_graph(self): lagrange = 5.0 - Q = dnx.traveling_salesperson_qubo(G, lagrange, 'weight') + Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange, 'weight') N = G.number_of_nodes() correct_sum = G.size('weight')*2*N-2*N*N*lagrange+2*N*N*(N-1)*lagrange @@ -2293,7 +2294,7 @@ def test_weighted_complete_graph(self): def test_exceptions(self): G = nx.Graph([(0, 1)]) with self.assertRaises(ValueError): - dnx.traveling_salesperson_qubo(G) + dimod.generators.tsp.traveling_salesperson_qubo(G) def test_docstring_size(self): # in the docstring we state the size of the resulting BQM, this checks @@ -2303,7 +2304,7 @@ def test_docstring_size(self): G.add_weighted_edges_from((u, v, .5) for u, v in itertools.combinations(range(n), 2)) - Q = dnx.traveling_salesperson_qubo(G) + Q = dimod.generators.tsp.traveling_salesperson_qubo(G) bqm = dimod.BinaryQuadraticModel.from_qubo(Q) self.assertEqual(len(bqm), n**2) From 417cc62c268021130595ebb89704793dab7cfaff Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Tue, 5 May 2026 21:20:27 +0200 Subject: [PATCH 08/27] Ensure the correct version of dimod is used for cibw tests --- pyproject.toml | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index e053eeb69..2092c0004 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -21,7 +21,11 @@ exclude_lines = [ [tool.cibuildwheel] build-verbosity = "1" skip = "pp* *musllinux*" -before-test = "pip install -r {project}/tests/requirements.txt" +before-test = [ + # Install local dimod first to make sure test dependencies use the version under test + "pip install {package}", + "pip install -r {project}/tests/requirements.txt", +] test-command = "python -m unittest discover {project}/tests/" [tool.cibuildwheel.linux] From e8c02631f247f231f2ea6c264fdec87d47c5ac26 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Wed, 6 May 2026 21:20:24 +0200 Subject: [PATCH 09/27] Refactor vertex coloring generator to return a BQM; add type hints --- dimod/generators/coloring.py | 72 ++++++++++++++++++++---------------- tests/test_generators.py | 20 ++++------ 2 files changed, 48 insertions(+), 44 deletions(-) diff --git a/dimod/generators/coloring.py b/dimod/generators/coloring.py index b2c756691..204c719bb 100644 --- a/dimod/generators/coloring.py +++ b/dimod/generators/coloring.py @@ -12,67 +12,78 @@ # See the License for the specific language governing permissions and # limitations under the License. -import math import itertools +import math +from collections.abc import Sequence + +from dimod.binary_quadratic_model import BinaryQuadraticModel +from dimod.decorators import graph_argument +from dimod.typing import GraphLike +from dimod.vartypes import Vartype import networkx as nx __all__ = ["min_vertex_color_qubo", - "vertex_color_qubo", + "vertex_coloring", ] @nx.utils.decorators.nodes_or_number(1) -def vertex_color_qubo(G, colors): - """Return the QUBO with ground states corresponding to a vertex coloring. +@graph_argument('graph') +def vertex_coloring(graph: GraphLike, + colors: int | Sequence, + ) -> BinaryQuadraticModel: + """Generate a binary quadratic model (BQM) with ground states corresponding + to a vertex coloring. If `V` is the set of nodes, `E` is the set of edges and `C` is the set of - colors the resulting qubo will have: + colors the resulting BQM will have: * :math:`|V|*|C|` variables/nodes * :math:`|V|*|C|*(|C| - 1) / 2 + |E|*|C|` interactions/edges - The QUBO has ground energy :math:`-|V|` and an infeasible gap of 1. - - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum vertex coloring. + The BQM has ground energy :math:`-|V|` and an infeasible gap of 1. - colors : int/sequence - The colors. If an int, the colors are labelled `[0, n)`. The number of - colors must be greater or equal to the chromatic number of the graph. + Args: + graph: + The graph on which to find a vertex coloring. Either an integer `n`, + interpreted as a complete graph of size `n`, a nodes/edges + pair, a list of edges or a NetworkX graph. - Returns - ------- - QUBO : dict - The QUBO with ground states corresponding to valid colorings of the - graph. The QUBO variables are labelled `(v, c)` where `v` is a node - in `G` and `c` is a color. In the ground state of the QUBO, a variable - `(v, c)` has value 1 if `v` should be colored `c` in a valid coloring. + colors: + The colors. If an int, the colors are labelled `[0, n)`. The number + of colors must be greater or equal to the chromatic number of the + graph. + Returns: + A binary quadratic model with ground states corresponding to valid + colorings of the graph. The BQM variables are labelled `(v, c)` where + `v` is a node in `G` and `c` is a color. In the ground state of the BQM, + a variable `(v, c)` has value 1 if `v` should be colored `c` in a valid + coloring. """ + variables, edges = graph _, colors = colors - Q = {} + bqm = BinaryQuadraticModel(Vartype.BINARY) # enforce that each variable in G has at most one color - for v in G.nodes: + for v in variables: # 1 in k constraint for c in colors: - Q[(v, c), (v, c)] = -1 + bqm.add_linear((v, c), -1) for c0, c1 in itertools.combinations(colors, 2): - Q[(v, c0), (v, c1)] = 2 + bqm.add_quadratic((v, c0), (v, c1), 2) # enforce that adjacent nodes do not have the same color - for u, v in G.edges: + for u, v in edges: # NAND constraint for c in colors: - Q[(u, c), (v, c)] = 1 + bqm.add_quadratic((u, c), (v, c), 1) - return Q + return bqm def _chromatic_number_upper_bound(G): @@ -144,15 +155,12 @@ def min_vertex_color_qubo(G, chromatic_lb=None, chromatic_ub=None): chromatic_lb : int, optional A lower bound on the chromatic number. If one is not provided, a - bound is calulcated. + bound is calculated. chromatic_ub : int, optional An upper bound on the chromatic number. If one is not provided, a bound is calculated. - sampler_args - Additional keyword parameters are passed to the sampler. - Returns ------- QUBO : dict diff --git a/tests/test_generators.py b/tests/test_generators.py index 8ed4e019f..bb28913ed 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1898,22 +1898,22 @@ def test_chromatic_number(self): ) -class TestVertexColorQUBO(unittest.TestCase): +class TestVertexColoring(unittest.TestCase): def test_single_node(self): G = nx.Graph() G.add_node('a') # a single color - Q = dimod.generators.coloring.vertex_color_qubo(G, ['red']) + bqm = dimod.generators.coloring.vertex_coloring(G, ['red']) - self.assertEqual(Q, {(('a', 'red'), ('a', 'red')): -1}) + self.assertEqual(bqm, dimod.BQM.from_qubo({(('a', 'red'), ('a', 'red')): -1})) def test_4cycle(self): G = nx.cycle_graph('abcd') - Q = dimod.generators.coloring.vertex_color_qubo(G, 2) + bqm = dimod.generators.coloring.vertex_coloring(G, 2) - sampleset = dimod.ExactSolver().sample_qubo(Q) + sampleset = dimod.ExactSolver().sample(bqm) # check that the ground state is a valid coloring ground_energy = sampleset.first.energy @@ -1942,14 +1942,12 @@ def test_num_variables(self): G = nx.Graph() G.add_nodes_from(range(15)) - Q = dimod.generators.coloring.vertex_color_qubo(G, 7) - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.coloring.vertex_coloring(G, 7) self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2) # add one edge G.add_edge(0, 1) - Q = dimod.generators.coloring.vertex_color_qubo(G, 7) - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.coloring.vertex_coloring(G, 7) self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2 + 7) def test_docstring_stats(self): @@ -1958,9 +1956,7 @@ def test_docstring_stats(self): colors = range(10) - Q = dimod.generators.coloring.vertex_color_qubo(G, colors) - - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.coloring.vertex_coloring(G, colors) self.assertEqual(len(bqm), len(G)*len(colors)) self.assertEqual(len(bqm.quadratic), len(G)*len(colors)*(len(colors)-1)/2 From e2a06071ac769a65fd36421648355e2de47a2a00 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 8 May 2026 21:40:16 +0200 Subject: [PATCH 10/27] Refactor min vertex coloring generator to return a BQM --- dimod/generators/coloring.py | 101 ++++++++++++++++++----------------- tests/test_generators.py | 6 +-- 2 files changed, 54 insertions(+), 53 deletions(-) diff --git a/dimod/generators/coloring.py b/dimod/generators/coloring.py index 204c719bb..a9ce0a445 100644 --- a/dimod/generators/coloring.py +++ b/dimod/generators/coloring.py @@ -23,7 +23,7 @@ import networkx as nx -__all__ = ["min_vertex_color_qubo", +__all__ = ["min_vertex_coloring", "vertex_coloring", ] @@ -36,8 +36,8 @@ def vertex_coloring(graph: GraphLike, """Generate a binary quadratic model (BQM) with ground states corresponding to a vertex coloring. - If `V` is the set of nodes, `E` is the set of edges and `C` is the set of - colors the resulting BQM will have: + If :math:`V` is the set of nodes, :math:`E` is the set of edges and + :math:`C` is the set of colors the resulting BQM will have: * :math:`|V|*|C|` variables/nodes * :math:`|V|*|C|*(|C| - 1) / 2 + |E|*|C|` interactions/edges @@ -46,21 +46,21 @@ def vertex_coloring(graph: GraphLike, Args: graph: - The graph on which to find a vertex coloring. Either an integer `n`, - interpreted as a complete graph of size `n`, a nodes/edges + The graph on which to find a vertex coloring. Either an integer + ``n``, interpreted as a complete graph of size ``n``, a nodes/edges pair, a list of edges or a NetworkX graph. colors: - The colors. If an int, the colors are labelled `[0, n)`. The number - of colors must be greater or equal to the chromatic number of the - graph. + The colors. If an int, the colors are labelled ``[0, n)``. The + number of colors must be greater or equal to the chromatic number of + the graph. Returns: A binary quadratic model with ground states corresponding to valid - colorings of the graph. The BQM variables are labelled `(v, c)` where - `v` is a node in `G` and `c` is a color. In the ground state of the BQM, - a variable `(v, c)` has value 1 if `v` should be colored `c` in a valid - coloring. + colorings of the graph. The BQM variables are labelled ``(v, c)`` where + ``v`` is a node in ``graph`` and ``c`` is a color. In the ground state + of the BQM, a variable ``(v, c)`` has value 1 if ``v`` should be colored + ``c`` in a valid coloring. """ variables, edges = graph @@ -86,7 +86,7 @@ def vertex_coloring(graph: GraphLike, return bqm -def _chromatic_number_upper_bound(G): +def _chromatic_number_upper_bound(G: 'nx.Graph') -> int: # tries to determine an upper bound on the chromatic number of G # Assumes G is not complete @@ -124,7 +124,7 @@ def _chromatic_number_upper_bound(G): return min(quad_bound, bound) -def _chromatic_number_lower_bound(G): +def _chromatic_number_lower_bound(G: 'nx.Graph') -> int: # find a random maximal clique and use that to determine a lower bound v = max(G, key=G.degree) @@ -136,54 +136,58 @@ def _chromatic_number_lower_bound(G): return len(clique) -def min_vertex_color_qubo(G, chromatic_lb=None, chromatic_ub=None): - """Return a QUBO with ground states corresponding to a minimum vertex - coloring. +@graph_argument('graph', as_networkx=True) +def min_vertex_coloring(graph: GraphLike, + chromatic_lb: int | None = None, + chromatic_ub: int | None = None, + ) -> BinaryQuadraticModel: + """Generate a binary quadratic model (BQM) with ground states corresponding + to a minimum vertex coloring. Vertex coloring is the problem of assigning a color to the vertices of a graph in a way that no adjacent vertices have the same color. A minimum vertex coloring is the problem of solving the vertex coloring problem using the smallest number of colors. - Defines a QUBO [Dah2013]_ with ground states corresponding to minimum - vertex colorings and uses the sampler to sample from it. + After defining a BQM/QUBO [Dah2013]_ with ground states corresponding to + minimum vertex colorings, use a sampler to sample from it. - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum vertex coloring. + Args: + graph: + The graph on which to find a vertex coloring. Either an integer + ``n``, interpreted as a complete graph of size ``n``, a nodes/edges + pair, a list of edges or a NetworkX graph. - chromatic_lb : int, optional - A lower bound on the chromatic number. If one is not provided, a - bound is calculated. + chromatic_lb: + A lower bound on the chromatic number. If one is not provided, a + bound is calculated. - chromatic_ub : int, optional - An upper bound on the chromatic number. If one is not provided, a bound - is calculated. + chromatic_ub: + An upper bound on the chromatic number. If one is not provided, a + bound is calculated. - Returns - ------- - QUBO : dict - The QUBO with ground states corresponding to minimum colorings of the - graph. The QUBO variables are labelled `(v, c)` where `v` is a node - in `G` and `c` is a color. In the ground state of the QUBO, a variable - `(v, c)` has value 1 if `v` should be colored `c` in a valid coloring. + Returns: + A binary quadratic model with ground states corresponding to minimum + colorings of the graph. The BQM variables are labelled ``(v, c)`` where + ``v`` is a node in ``graph`` and ``c`` is a color. In the ground state + of the BQM, a variable ``(v, c)`` has value 1 if ``v`` should be colored + ``c`` in a valid coloring. """ - chi_ub = _chromatic_number_upper_bound(G) + chi_ub = _chromatic_number_upper_bound(graph) chromatic_ub = chi_ub if chromatic_ub is None else min(chi_ub, chromatic_ub) - chib_lb = _chromatic_number_lower_bound(G) + chib_lb = _chromatic_number_lower_bound(graph) chromatic_lb = chib_lb if chromatic_lb is None else max(chib_lb, chromatic_lb) if chromatic_lb > chromatic_ub: raise RuntimeError("something went wrong when calculating the " "chromatic number bounds") - # our base QUBO is one with as many colors as we might need, so we use the + # our base BQM is one with as many colors as we might need, so we use the # upper bound - Q = vertex_color_qubo(G, int(chromatic_ub)) + bqm = vertex_coloring(graph, colors=int(chromatic_ub)) if chromatic_lb != chromatic_ub: # we want to penalize the colors that we aren't sure that we need @@ -196,26 +200,23 @@ def min_vertex_color_qubo(G, chromatic_lb=None, chromatic_ub=None): weights = [p / (num_penalized + 1) for p in range(1, num_penalized + 1)] for p, c in zip(weights, range(chromatic_lb, chromatic_ub)): - for v in G: - Q[(v, c), (v, c)] += p + for v in bqm.variables: + bqm.linear[(v, c)] += p - return Q + return bqm -def is_cycle(G): +def is_cycle(G: 'nx.Graph') -> bool: """Determines whether the given graph is a cycle or circle graph. A cycle graph or circular graph is a graph that consists of a single cycle. https://en.wikipedia.org/wiki/Cycle_graph - Parameters - ---------- - G : NetworkX graph + Args: + G: A NetworkX graph. - Returns - ------- - is_cycle : bool + Returns: True if the graph consists of a single cycle. """ diff --git a/tests/test_generators.py b/tests/test_generators.py index bb28913ed..e1c03975f 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1887,14 +1887,14 @@ def test_feasible(self): self.assertEqual(lhs, 0) -class TestMinVertexColorQubo(unittest.TestCase): +class TestMinVertexColoring(unittest.TestCase): def test_chromatic_number(self): G = nx.cycle_graph('abcd') # when the chromatic number is fixed this is exactly vertex_color self.assertEqual( - dimod.generators.coloring.min_vertex_color_qubo(G, chromatic_lb=2, chromatic_ub=2), - dimod.generators.coloring.vertex_color_qubo(G, 2) + dimod.generators.coloring.min_vertex_coloring(G, chromatic_lb=2, chromatic_ub=2), + dimod.generators.coloring.vertex_coloring(G, 2) ) From 560f76baa1f2496a3026aac32004c0933b7e8c03 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Mon, 11 May 2026 19:54:45 +0200 Subject: [PATCH 11/27] Remove dependency on networkx of the imported dnx generators --- dimod/generators/coloring.py | 18 +++++++++--------- tests/test_generators.py | 8 ++++---- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/dimod/generators/coloring.py b/dimod/generators/coloring.py index a9ce0a445..2d2b16b90 100644 --- a/dimod/generators/coloring.py +++ b/dimod/generators/coloring.py @@ -21,17 +21,14 @@ from dimod.typing import GraphLike from dimod.vartypes import Vartype -import networkx as nx - __all__ = ["min_vertex_coloring", "vertex_coloring", ] -@nx.utils.decorators.nodes_or_number(1) @graph_argument('graph') def vertex_coloring(graph: GraphLike, - colors: int | Sequence, + colors: Sequence, ) -> BinaryQuadraticModel: """Generate a binary quadratic model (BQM) with ground states corresponding to a vertex coloring. @@ -51,9 +48,8 @@ def vertex_coloring(graph: GraphLike, pair, a list of edges or a NetworkX graph. colors: - The colors. If an int, the colors are labelled ``[0, n)``. The - number of colors must be greater or equal to the chromatic number of - the graph. + Color labels to use. The number of colors must be greater or equal + to the chromatic number of the graph. Returns: A binary quadratic model with ground states corresponding to valid @@ -64,7 +60,6 @@ def vertex_coloring(graph: GraphLike, """ variables, edges = graph - _, colors = colors bqm = BinaryQuadraticModel(Vartype.BINARY) @@ -86,10 +81,15 @@ def vertex_coloring(graph: GraphLike, return bqm +@graph_argument('G', as_networkx=True) def _chromatic_number_upper_bound(G: 'nx.Graph') -> int: # tries to determine an upper bound on the chromatic number of G # Assumes G is not complete + # nx is an optional dependency, so we defer import + # it's safe to import here, as `@graph_argument` would fail otherwise + import networkx as nx + if not nx.is_connected(G): return max((_chromatic_number_upper_bound(G.subgraph(c)) for c in nx.connected_components(G))) @@ -187,7 +187,7 @@ def min_vertex_coloring(graph: GraphLike, # our base BQM is one with as many colors as we might need, so we use the # upper bound - bqm = vertex_coloring(graph, colors=int(chromatic_ub)) + bqm = vertex_coloring(graph, colors=range(int(chromatic_ub))) if chromatic_lb != chromatic_ub: # we want to penalize the colors that we aren't sure that we need diff --git a/tests/test_generators.py b/tests/test_generators.py index e1c03975f..48e7ae6ae 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1894,7 +1894,7 @@ def test_chromatic_number(self): # when the chromatic number is fixed this is exactly vertex_color self.assertEqual( dimod.generators.coloring.min_vertex_coloring(G, chromatic_lb=2, chromatic_ub=2), - dimod.generators.coloring.vertex_coloring(G, 2) + dimod.generators.coloring.vertex_coloring(G, range(2)) ) @@ -1911,7 +1911,7 @@ def test_single_node(self): def test_4cycle(self): G = nx.cycle_graph('abcd') - bqm = dimod.generators.coloring.vertex_coloring(G, 2) + bqm = dimod.generators.coloring.vertex_coloring(G, range(2)) sampleset = dimod.ExactSolver().sample(bqm) @@ -1942,12 +1942,12 @@ def test_num_variables(self): G = nx.Graph() G.add_nodes_from(range(15)) - bqm = dimod.generators.coloring.vertex_coloring(G, 7) + bqm = dimod.generators.coloring.vertex_coloring(G, range(7)) self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2) # add one edge G.add_edge(0, 1) - bqm = dimod.generators.coloring.vertex_coloring(G, 7) + bqm = dimod.generators.coloring.vertex_coloring(G, range(7)) self.assertEqual(len(bqm.quadratic), len(G)*7*(7-1)/2 + 7) def test_docstring_stats(self): From ea46d31e45c1d65aae62acfa40324c5be9822a74 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Mon, 11 May 2026 21:29:01 +0200 Subject: [PATCH 12/27] Refresh markov_network generator for dimod context Add type hints, update docs. --- dimod/generators/markov.py | 27 +++++++++++++-------------- tests/test_generators.py | 12 ++++++++---- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/dimod/generators/markov.py b/dimod/generators/markov.py index ffe89aed7..4ba828371 100644 --- a/dimod/generators/markov.py +++ b/dimod/generators/markov.py @@ -14,7 +14,7 @@ import dimod -__all__ = ['markov_network_bqm', +__all__ = ['markov_network', ] @@ -50,26 +50,25 @@ # -def markov_network_bqm(MN): - """Construct a binary quadratic model for a markov network. +def markov_network(graph: 'nx.Graph') -> dimod.BinaryQuadraticModel: + """Construct a binary quadratic model for a Markov network. + Args: + graph: + A Markov network (in a NetworkX graph) as returned by + :func:`dwave.graphs.generators.markov.markov_network`. - Parameters - ---------- - G : NetworkX graph - A Markov Network as returned by :func:`.markov_network` - - Returns - ------- - bqm : :obj:`dimod.BinaryQuadraticModel` + Returns: A binary quadratic model. - """ + if not hasattr(graph, 'nodes') or not hasattr(graph, 'edges'): + raise ValueError("A NetworkX graph with potentials data required") + bqm = dimod.BinaryQuadraticModel.empty(dimod.BINARY) # the variable potentials - for v, ddict in MN.nodes(data=True, default=None): + for v, ddict in graph.nodes(data=True, default=None): potential = ddict.get('potential', None) if potential is None: @@ -84,7 +83,7 @@ def markov_network_bqm(MN): bqm.offset += phi0 # the interaction potentials - for u, v, ddict in MN.edges(data=True, default=None): + for u, v, ddict in graph.edges(data=True, default=None): potential = ddict.get('potential', None) if potential is None: diff --git a/tests/test_generators.py b/tests/test_generators.py index 48e7ae6ae..ebbcf74e1 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1964,11 +1964,15 @@ def test_docstring_stats(self): @unittest.skipUnless(_dnx, "no dwave-graphs installed") -class Test_sample_markov_network_bqm(unittest.TestCase): +class TestMarkovNetwork(unittest.TestCase): + def test_input_validation(self): + with self.assertRaises(ValueError): + dimod.generators.markov.markov_network(dict()) + def test_one_node(self): potentials = {'a': {(0,): 1.2, (1,): .4}} - bqm = dimod.generators.markov.markov_network_bqm(dnx.markov_network(potentials)) + bqm = dimod.generators.markov.markov_network(dnx.markov_network(potentials)) for edge, potential in potentials.items(): for config, energy in potential.items(): @@ -1979,7 +1983,7 @@ def test_one_edge(self): potentials = {'ab': {(0, 0): 1.2, (1, 0): .4, (0, 1): 1.3, (1, 1): -4}} - bqm = dimod.generators.markov.markov_network_bqm(dnx.markov_network(potentials)) + bqm = dimod.generators.markov.markov_network(dnx.markov_network(potentials)) for edge, potential in potentials.items(): for config, energy in potential.items(): @@ -1994,7 +1998,7 @@ def test_typical(self): (0, 1): -1, (1, 1): -4}, 'd': {(0,): -.5, (1,): 1.6}} - bqm = dimod.generators.markov.markov_network_bqm(dnx.markov_network(potentials)) + bqm = dimod.generators.markov.markov_network(dnx.markov_network(potentials)) samples = dimod.ExactSolver().sample(bqm) From 2a2f33b79fa799d8b28ded35240194cbe3c96214 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 28 May 2026 14:12:35 -0700 Subject: [PATCH 13/27] Convert dimod implicit type aliases to explicit Use `typing.TypeAlias` (PEP 613), available since py310. --- dimod/typing.py | 20 ++++++++++---------- dimod/vartypes.py | 2 +- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/dimod/typing.py b/dimod/typing.py index 28f4d73ab..70bd715ed 100644 --- a/dimod/typing.py +++ b/dimod/typing.py @@ -26,14 +26,14 @@ from numpy.typing import ArrayLike, DTypeLike except ImportError: # support numpy < 1.20 - ArrayLike = collections.abc.Sequence - DTypeLike = typing.Any + ArrayLike: typing.TypeAlias = collections.abc.Sequence + DTypeLike: typing.TypeAlias = typing.Any try: from numpy.typing import NDArray except ImportError: # support numpy < 1.21 - NDArray = collections.abc.Sequence + NDArray: typing.TypeAlias = collections.abc.Sequence __all__ = ['Bias', @@ -47,7 +47,7 @@ # use float for python types, https://www.python.org/dev/peps/pep-0484/#the-numeric-tower # exclude np.complexfloating from numpy types -Bias = typing.Union[float, np.floating, np.integer] +Bias: typing.TypeAlias = typing.Union[float, np.floating, np.integer] """A :obj:`~typing.Union` representing objects that can be used as biases. This includes: @@ -62,7 +62,7 @@ # a typing class is a pain accross our supported Python versions. In the future # we should handle it -Variable = collections.abc.Hashable +Variable: typing.TypeAlias = collections.abc.Hashable """Objects that can be used as variable labels. .. note:: @@ -74,7 +74,7 @@ """ -GraphLike = typing.Union[ +GraphLike: typing.TypeAlias = typing.Union[ int, # number of nodes tuple[ collections.abc.Collection[Variable], @@ -98,9 +98,9 @@ except ImportError: pass else: - GraphLike = typing.Union[GraphLike, nx.Graph] + GraphLike: typing.TypeAlias = typing.Union[GraphLike, nx.Graph] -Polynomial = collections.abc.Mapping[collections.abc.Sequence[Variable], Bias] +Polynomial: typing.TypeAlias = collections.abc.Mapping[collections.abc.Sequence[Variable], Bias] """A polynomial represented by a mapping.""" @@ -131,7 +131,7 @@ class DQMVectors(typing.NamedTuple): offset: Bias -SampleLike = typing.Union[ +SampleLike: typing.TypeAlias = typing.Union[ collections.abc.Sequence[Bias], collections.abc.Mapping[Variable, Bias], tuple[collections.abc.Sequence[Bias], collections.abc.Sequence[Variable]], @@ -155,7 +155,7 @@ class DQMVectors(typing.NamedTuple): """ -SamplesLike = typing.Union[ +SamplesLike: typing.TypeAlias = typing.Union[ SampleLike, collections.abc.Sequence[collections.abc.Sequence[Bias]], # 2d array tuple[collections.abc.Sequence[collections.abc.Sequence[Bias]], collections.abc.Sequence[Variable]], diff --git a/dimod/vartypes.py b/dimod/vartypes.py index cfddc3aa5..bb8d27e8c 100644 --- a/dimod/vartypes.py +++ b/dimod/vartypes.py @@ -146,7 +146,7 @@ def __call__(self, v: typing.Optional[dimod.typing.Variable] = None) -> Vartype: # There are other types we allow (e.g. frozenset((-1, +1)))) but they are very # rarely used over the years so IMO more succinct typing is more useful. # Also, Literal doesn't accept frozensets in its list so we'd have to do a Union. -VartypeLike = typing.Literal[ +VartypeLike: typing.TypeAlias = typing.Literal[ Vartype.SPIN, "SPIN", Vartype.BINARY, "BINARY", Vartype.INTEGER, "INTEGER", From bcfc8a91225351bc80a012a77e624d78ba2d213f Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 28 May 2026 15:58:57 -0700 Subject: [PATCH 14/27] Refactor matching BQM generators to not require networkx Add type hints, update docs to dimod style. --- dimod/generators/matching.py | 145 ++++++++++++++++++++--------------- tests/test_generators.py | 13 ++-- 2 files changed, 93 insertions(+), 65 deletions(-) diff --git a/dimod/generators/matching.py b/dimod/generators/matching.py index bf663eb72..3eb821489 100644 --- a/dimod/generators/matching.py +++ b/dimod/generators/matching.py @@ -12,49 +12,56 @@ # See the License for the specific language governing permissions and # limitations under the License. +import collections import itertools +from typing import Any import dimod +from dimod.decorators import graph_argument +from dimod.typing import GraphLike -__all__ = ['matching_bqm', - 'maximal_matching_bqm', - 'min_maximal_matching_bqm', +__all__ = ['matching', + 'maximal_matching', + 'min_maximal_matching', ] -def matching_bqm(G): +@graph_argument('graph') +def matching(graph: GraphLike) -> dimod.BinaryQuadraticModel: """Find a binary quadratic model for the graph's matchings. A matching is a subset of edges in which no node occurs more than once. This function returns a binary quadratic model (BQM) with ground - states corresponding to the possible matchings of G. + states corresponding to the possible matchings of ``graph``. Finding valid matchings can be done in polynomial time, so finding matching with BQMs is generally inefficient. This BQM may be useful when combined with other constraints and objectives. - Parameters - ---------- - G : NetworkX graph - The graph on which to find a matching. + Args: + graph: + The graph on which to find a matching. Either an integer + ``n``, interpreted as a complete graph of size ``n``, a nodes/edges + pair, a list of edges or a NetworkX graph. - Returns - ------- - bqm : :class:`dimod.BinaryQuadraticModel` + Returns: A binary quadratic model with ground states corresponding to a - matching. The variables of the BQM are the edges of `G` as frozensets. + matching. The variables of the BQM are the edges of ``graph`` as frozensets. The BQM's ground state energy is 0 by construction. The energy of the first excited state is 1. """ + nodes, edges = graph + hood = _neighborhood(edges) + bqm = dimod.BinaryQuadraticModel.empty('BINARY') - # add the edges of G as variables - for edge in G.edges: + # add the edges of `graph` as variables + for edge in edges: bqm.add_variable(frozenset(edge), 0) - for node in G: - for edge0, edge1 in itertools.combinations(G.edges(node), 2): + for node in nodes: + for edge0, edge1 in itertools.combinations(hood[node], 2): u = frozenset(edge0) v = frozenset(edge1) bqm.add_interaction(u, v, 1) @@ -62,59 +69,73 @@ def matching_bqm(G): return bqm -def maximal_matching_bqm(G, lagrange=None): +def _neighborhood(edges: list[tuple[Any, Any]]) -> dict[Any, list[tuple[Any, Any]]]: + # return neighborhood dict from the list of edges + hood = collections.defaultdict(list) + for edge in edges: + for node in edge: + hood[node].append(edge) + return hood + + +@graph_argument('graph') +def maximal_matching(graph: GraphLike, + lagrange: float | None = None, + ) -> dimod.BinaryQuadraticModel: """Find a binary quadratic model for the graph's maximal matchings. A matching is a subset of edges in which no node occurs more than - once. A maximal matching is one in which no edges from G can be + once. A maximal matching is one in which no edges from ``graph`` can be added without violating the matching rule. This function returns a binary quadratic model (BQM) with ground - states corresponding to the possible maximal matchings of G. + states corresponding to the possible maximal matchings of ``graph`. Finding maximal matchings can be done in polynomial time, so finding maximal matching with BQMs is generally inefficient. This BQM may be useful when combined with other constraints and objectives. - Parameters - ---------- - G : NetworkX graph - The graph on which to find a maximal matching. + Args: + graph: + The graph on which to find a maximal matching. Either an integer + ``n``, interpreted as a complete graph of size ``n``, a nodes/edges + pair, a list of edges or a NetworkX graph. - lagrange : float (optional) - The Lagrange multiplier for the matching constraint. Should be positive - and greater than `max_degree - 2`. - Defaults to `1.25 * (max_degree - 2)`. + lagrange: + The Lagrange multiplier for the matching constraint. Should be + positive and greater than ``max_degree - 2``. + Defaults to :math:`1.25 * (max_degree - 2)`. - Returns - ------- - bqm : :class:`dimod.BinaryQuadraticModel` + Returns: A binary quadratic model with ground states corresponding to a maximal - matching. The variables of the BQM are the edges of `G` as frozensets. - The BQM's ground state energy is 0 by construction. + matching. The variables of the BQM are the edges of ``graph`` as + frozensets. The BQM's ground state energy is 0 by construction. """ - bqm = matching_bqm(G) + nodes, edges = graph + hood = _neighborhood(edges) + + bqm = matching(graph) if lagrange is None: - delta = max((G.degree[v] for v in G), default=0) - lagrange = max(1.25 * (delta - 2), 1) + max_degree = len(max(hood.values(), key=len, default=[])) + lagrange = max(1.25 * (max_degree - 2), 1) bqm.scale(lagrange) - for node0, node1 in G.edges: + for node0, node1 in edges: # (1 - y_v - y_u + y_v*y_u) <- see paper bqm.offset += 1 - for edge in G.edges(node0): + for edge in hood[node0]: bqm.linear[frozenset(edge)] -= 1 - for edge in G.edges(node1): + for edge in hood[node1]: bqm.linear[frozenset(edge)] -= 1 - for edge0 in G.edges(node0): + for edge0 in hood[node0]: u = frozenset(edge0) - for edge1 in G.edges(node1): + for edge1 in hood[node1]: v = frozenset(edge1) if u == v: bqm.linear[u] += 1 @@ -124,36 +145,39 @@ def maximal_matching_bqm(G, lagrange=None): return bqm -def min_maximal_matching_bqm(G, maximal_lagrange=2, matching_lagrange=None): +@graph_argument('graph') +def min_maximal_matching(graph: GraphLike, + maximal_lagrange: float = 2, + matching_lagrange: float | None = None + ) -> dimod.BinaryQuadraticModel: """Find a binary quadratic model for the graph's minimum maximal matchings. A matching is a subset of edges in which no node occurs more than - once. A maximal matching is one in which no edges from G can be + once. A maximal matching is one in which no edges from ``graph`` can be added without violating the matching rule. A minimum maximal matching is a maximal matching that contains the smallest possible number of edges. This function returns a binary quadratic model (BQM) with ground - states corresponding to the possible maximal matchings of G. + states corresponding to the possible maximal matchings of ``graph``. - Parameters - ---------- - G : NetworkX graph - The graph on which to find a minimum maximal matching. + Args: + graph: + The graph on which to find a minimum maximal matching. Either an + integer ``n``, interpreted as a complete graph of size ``n``, a + nodes/edges pair, a list of edges or a NetworkX graph. - maximal_lagrange : float (optional, default=2) - The Lagrange multiplier for the maximal constraint. Should be greater - than 1. + maximal_lagrange: + The Lagrange multiplier for the maximal constraint. Should be + greater than 1. Defaults to 2. - matching_lagrange : float (optional) - The Lagrange multiplier for the matching constraint. Should be positive - and greater than `maximal_lagrange * max_degree - 2`. - Defaults to `1.25 * (maximal_lagrange * max_degree - 2)`. + matching_lagrange: + The Lagrange multiplier for the matching constraint. Should be + positive and greater than ``maximal_lagrange * max_degree - 2``. + Defaults to ``1.25 * (maximal_lagrange * max_degree - 2)``. - Returns - ------- - bqm : :class:`dimod.BinaryQuadraticModel` + Returns: A binary quadratic model with ground states corresponding to a minimum maximal matching. The variables of the BQM are the edges - of `G` as frozensets. + of ``graph`` as frozensets. """ @@ -161,7 +185,8 @@ def min_maximal_matching_bqm(G, maximal_lagrange=2, matching_lagrange=None): # we're going to scale the bqm by maximal_matching so undo that # for maximal_lagrange matching_lagrange /= maximal_lagrange - bqm = maximal_matching_bqm(G, lagrange=matching_lagrange) + + bqm = maximal_matching(graph, lagrange=matching_lagrange) bqm.scale(maximal_lagrange) for v in bqm.variables: diff --git a/tests/test_generators.py b/tests/test_generators.py index ebbcf74e1..46716b00b 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1887,6 +1887,7 @@ def test_feasible(self): self.assertEqual(lhs, 0) +@unittest.skipUnless(_networkx, "no networkx installed") class TestMinVertexColoring(unittest.TestCase): def test_chromatic_number(self): G = nx.cycle_graph('abcd') @@ -1898,6 +1899,7 @@ def test_chromatic_number(self): ) +@unittest.skipUnless(_networkx, "no networkx installed") class TestVertexColoring(unittest.TestCase): def test_single_node(self): G = nx.Graph() @@ -2012,6 +2014,7 @@ def test_typical(self): self.assertAlmostEqual(en, energy) +@unittest.skipUnless(_networkx, "no networkx installed") @parameterized.parameterized_class( 'graph', [[nx.Graph()], @@ -2024,8 +2027,8 @@ def test_typical(self): ] ) class TestMatching(unittest.TestCase): - def test_matching_bqm(self): - bqm = dimod.generators.matching.matching_bqm(self.graph) + def test_matching(self): + bqm = dimod.generators.matching(self.graph) # the ground states should be exactly the matchings of G sampleset = dimod.ExactSolver().sample(bqm) @@ -2035,8 +2038,8 @@ def test_matching_bqm(self): self.assertEqual(nx.is_matching(self.graph, edges), energy == 0) self.assertTrue(energy == 0 or energy >= 1) - def test_maximal_matching_bqm(self): - bqm = dimod.generators.matching.maximal_matching_bqm(self.graph) + def test_maximal_matching(self): + bqm = dimod.generators.maximal_matching(self.graph) # the ground states should be exactly the maximal matchings of G sampleset = dimod.ExactSolver().sample(bqm) @@ -2048,7 +2051,7 @@ def test_maximal_matching_bqm(self): self.assertGreaterEqual(energy, 0) def test_min_maximal_matching_bqm(self): - bqm = dimod.generators.matching.min_maximal_matching_bqm(self.graph) + bqm = dimod.generators.min_maximal_matching(self.graph) if len(self.graph) == 0: self.assertEqual(len(bqm.linear), 0) From b65983e5e378ceb162e2da89a3d30dcf9806dd8a Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 28 May 2026 17:40:37 -0700 Subject: [PATCH 15/27] Fix whitespaces (dos2unix newlines) in partition cqm generator --- dimod/generators/partition.py | 166 +++++++++++++++++----------------- 1 file changed, 83 insertions(+), 83 deletions(-) diff --git a/dimod/generators/partition.py b/dimod/generators/partition.py index 7fb218208..fa3600b2a 100644 --- a/dimod/generators/partition.py +++ b/dimod/generators/partition.py @@ -1,84 +1,84 @@ -# Copyright 2021 D-Wave Systems Inc. -# -# 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. - -import itertools -import math - -import dimod - -__all__ = ["graph_partition_cqm", - ] - - -def graph_partition_cqm(G, num_partitions): - """Find a constrained quadratic model for the graph's partitions. - - Defines an CQM with ground states corresponding to a - balanced k-partition of G and uses the sampler to sample from it. - A k-partition is a collection of k subsets of the vertices - of G such that each vertex is in exactly one subset, and - the number of edges between vertices in different subsets - is as small as possible. If G is a weighted graph, the sum - of weights over those edges are minimized. - - Parameters - ---------- - G : NetworkX graph - The graph to partition. - num_partitions : int - The number of subsets in the desired partition. - - Returns - ------- - cqm : :class:`dimod.ConstrainedQuadraticModel` - A constrained quadratic model with ground states corresponding to a - partition problem. The nodes of `G` are discrete logical variables - of the CQM, where the cases are the different partitions the node - can be assigned to. The objective is given as the number of edges - connecting nodes in different partitions. - - """ - partition_size = G.number_of_nodes()/num_partitions - partitions = range(num_partitions) - cqm = dimod.ConstrainedQuadraticModel() - - # Variables will be added using the discrete method in CQM - x = {vk: dimod.Binary(vk) for vk in itertools.product(G.nodes, partitions)} - - for v in G.nodes: - cqm.add_discrete(((v, k) for k in partitions), label=v) - - - if not math.isclose(partition_size, int(partition_size)): - # if number of nodes don't divide into num_partitions, - # accept partitions of size ceil() or floor() - floor, ceil = int(partition_size), int(partition_size+1) - for k in partitions: - cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) >= floor, label='equal_partition_low_%s' %k) - cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) <= ceil, label='equal_partition_high_%s' %k) - else: - # each partition must have partition_size elements - for k in partitions: - cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) == int(partition_size), label='equal_partition_%s' %k) - - cuts = 0 - for (u, v, d) in G.edges(data=True): - for k in partitions: - w = d.get('weight',1) - cuts += w * x[u,k] * x[v,k] - - if cuts: - cqm.set_objective(-cuts) - +# Copyright 2021 D-Wave Systems Inc. +# +# 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. + +import itertools +import math + +import dimod + +__all__ = ["graph_partition_cqm", + ] + + +def graph_partition_cqm(G, num_partitions): + """Find a constrained quadratic model for the graph's partitions. + + Defines an CQM with ground states corresponding to a + balanced k-partition of G and uses the sampler to sample from it. + A k-partition is a collection of k subsets of the vertices + of G such that each vertex is in exactly one subset, and + the number of edges between vertices in different subsets + is as small as possible. If G is a weighted graph, the sum + of weights over those edges are minimized. + + Parameters + ---------- + G : NetworkX graph + The graph to partition. + num_partitions : int + The number of subsets in the desired partition. + + Returns + ------- + cqm : :class:`dimod.ConstrainedQuadraticModel` + A constrained quadratic model with ground states corresponding to a + partition problem. The nodes of `G` are discrete logical variables + of the CQM, where the cases are the different partitions the node + can be assigned to. The objective is given as the number of edges + connecting nodes in different partitions. + + """ + partition_size = G.number_of_nodes()/num_partitions + partitions = range(num_partitions) + cqm = dimod.ConstrainedQuadraticModel() + + # Variables will be added using the discrete method in CQM + x = {vk: dimod.Binary(vk) for vk in itertools.product(G.nodes, partitions)} + + for v in G.nodes: + cqm.add_discrete(((v, k) for k in partitions), label=v) + + + if not math.isclose(partition_size, int(partition_size)): + # if number of nodes don't divide into num_partitions, + # accept partitions of size ceil() or floor() + floor, ceil = int(partition_size), int(partition_size+1) + for k in partitions: + cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) >= floor, label='equal_partition_low_%s' %k) + cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) <= ceil, label='equal_partition_high_%s' %k) + else: + # each partition must have partition_size elements + for k in partitions: + cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) == int(partition_size), label='equal_partition_%s' %k) + + cuts = 0 + for (u, v, d) in G.edges(data=True): + for k in partitions: + w = d.get('weight',1) + cuts += w * x[u,k] * x[v,k] + + if cuts: + cqm.set_objective(-cuts) + return cqm \ No newline at end of file From 6997942faee038a3467e52d70d4a7c7ff335ad16 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Thu, 28 May 2026 17:46:24 -0700 Subject: [PATCH 16/27] Update partition CQM generator: add type hints, refresh docs --- dimod/generators/partition.py | 76 ++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 33 deletions(-) diff --git a/dimod/generators/partition.py b/dimod/generators/partition.py index fa3600b2a..45a84513c 100644 --- a/dimod/generators/partition.py +++ b/dimod/generators/partition.py @@ -16,69 +16,79 @@ import math import dimod +from dimod.typing import GraphLike +from dimod.decorators import graph_argument -__all__ = ["graph_partition_cqm", +__all__ = ["graph_partition", ] -def graph_partition_cqm(G, num_partitions): +@graph_argument('graph', as_networkx=True) +def graph_partition(graph: GraphLike, + num_partitions: int, + ) -> dimod.ConstrainedQuadraticModel: """Find a constrained quadratic model for the graph's partitions. - Defines an CQM with ground states corresponding to a - balanced k-partition of G and uses the sampler to sample from it. - A k-partition is a collection of k subsets of the vertices - of G such that each vertex is in exactly one subset, and - the number of edges between vertices in different subsets - is as small as possible. If G is a weighted graph, the sum - of weights over those edges are minimized. - - Parameters - ---------- - G : NetworkX graph - The graph to partition. - num_partitions : int - The number of subsets in the desired partition. - - Returns - ------- - cqm : :class:`dimod.ConstrainedQuadraticModel` + Defines a CQM with ground states corresponding to a balanced k-partition + of ``graph``. A k-partition is a collection of k subsets of the vertices + of ``graph`` such that each vertex is in exactly one subset, and the number + of edges between vertices in different subsets is as small as possible. + If ``graph`` is a weighted graph, the sum of weights over those edges are + minimized. + + Args: + graph: + The graph to partition. Either an integer ``n``, interpreted as a + complete graph of size ``n``, a nodes/edges pair, a list of edges or + a NetworkX graph. When NetworkX graph is provided, optional edge + weights can be provided in the ``weight`` attribute. + + num_partitions: + The number of subsets in the desired partition. + + Returns: A constrained quadratic model with ground states corresponding to a - partition problem. The nodes of `G` are discrete logical variables + partition problem. The nodes of ``graph`` are discrete logical variables of the CQM, where the cases are the different partitions the node can be assigned to. The objective is given as the number of edges connecting nodes in different partitions. """ - partition_size = G.number_of_nodes()/num_partitions + partition_size = graph.number_of_nodes() / num_partitions partitions = range(num_partitions) cqm = dimod.ConstrainedQuadraticModel() # Variables will be added using the discrete method in CQM - x = {vk: dimod.Binary(vk) for vk in itertools.product(G.nodes, partitions)} - - for v in G.nodes: + x = {vk: dimod.Binary(vk) for vk in itertools.product(graph.nodes, partitions)} + + for v in graph.nodes: cqm.add_discrete(((v, k) for k in partitions), label=v) - - + if not math.isclose(partition_size, int(partition_size)): # if number of nodes don't divide into num_partitions, # accept partitions of size ceil() or floor() floor, ceil = int(partition_size), int(partition_size+1) for k in partitions: - cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) >= floor, label='equal_partition_low_%s' %k) - cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) <= ceil, label='equal_partition_high_%s' %k) + cqm.add_constraint( + dimod.quicksum((x[u, k] for u in graph.nodes)) >= floor, + label=f'equal_partition_low_{k}') + cqm.add_constraint( + dimod.quicksum((x[u, k] for u in graph.nodes)) <= ceil, + label=f'equal_partition_high_{k}') else: # each partition must have partition_size elements for k in partitions: - cqm.add_constraint(dimod.quicksum((x[u, k] for u in G.nodes)) == int(partition_size), label='equal_partition_%s' %k) + cqm.add_constraint( + dimod.quicksum((x[u, k] for u in graph.nodes)) == int(partition_size), + label=f'equal_partition_{k}') cuts = 0 - for (u, v, d) in G.edges(data=True): + for (u, v, d) in graph.edges(data=True): for k in partitions: w = d.get('weight',1) cuts += w * x[u,k] * x[v,k] - + if cuts: cqm.set_objective(-cuts) - return cqm \ No newline at end of file + return cqm From 942358562dcfc9ba8fc1328bdac578942b4e58ba Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 29 May 2026 11:14:07 -0700 Subject: [PATCH 17/27] Remove networkx checks in tests - always assume nx is present for tests --- tests/test_decorators.py | 11 +---------- tests/test_generators.py | 24 ++---------------------- 2 files changed, 3 insertions(+), 32 deletions(-) diff --git a/tests/test_decorators.py b/tests/test_decorators.py index 9d649e5f1..745552fc9 100644 --- a/tests/test_decorators.py +++ b/tests/test_decorators.py @@ -16,19 +16,12 @@ import typing import unittest import itertools - from collections.abc import Sequence from operator import itemgetter -try: - import networkx as nx -except ImportError: - _networkx = False -else: - _networkx = True +import networkx as nx import dimod - from dimod.vartypes import SPIN, BINARY from dimod.decorators import vartype_argument, graph_argument @@ -183,7 +176,6 @@ def f(vartype='SPIN'): class TestGraphArgument(unittest.TestCase): - @unittest.skipUnless(_networkx, "no networkx installed") def test_networkx_graph(self): @graph_argument('G') def f(G): @@ -311,7 +303,6 @@ def test_other_kwarg(self): def f(G): pass - @unittest.skipUnless(_networkx, "no networkx installed") def test_as_networkx_graph(self): from networkx.utils import graphs_equal diff --git a/tests/test_generators.py b/tests/test_generators.py index 46716b00b..98fb94097 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -13,21 +13,14 @@ # limitations under the License. import unittest -import unittest.mock import dimod import itertools +import networkx as nx import numpy as np import parameterized -try: - import networkx as nx -except ImportError: - _networkx = False -else: - _networkx = True - # TODO: update to dwave-graphs once released try: import dwave_networkx as dnx @@ -248,7 +241,6 @@ def test_singletile(self): self.assertIn(j, bqm.adj[i]) self.assertIn(bqm.adj[i][j], (-1, 1)) - @unittest.skipUnless(_networkx, "no networkx installed") def test_multitile(self): bqm = dimod.generators.chimera_anticluster(2, multiplier=4) @@ -322,7 +314,6 @@ def test_deprecation(self): bqm = dimod.generators.chimera_anticluster(0, cls=6) -@unittest.skipUnless(_networkx, "no networkx installed") class TestFCL(unittest.TestCase): def setUp(self): @@ -865,7 +856,6 @@ def test_edges(self): self.assertEqual(bqm.offset, 0) self.assertIs(bqm.vartype, dimod.BINARY) - @unittest.skipUnless(_networkx, "no networkx installed") def test_edges_networkx(self): G = nx.complete_graph(3) bqm = dimod.generators.independent_set(G.edges) @@ -881,7 +871,6 @@ def test_edges_and_nodes(self): self.assertEqual(bqm.offset, 0) self.assertIs(bqm.vartype, dimod.BINARY) - @unittest.skipUnless(_networkx, "no networkx installed") def test_edges_and_nodes_networkx(self): G = nx.complete_graph(3) G.add_node(3) @@ -895,7 +884,6 @@ def test_empty(self): self.assertEqual(dimod.generators.independent_set([]).shape, (0, 0)) self.assertEqual(dimod.generators.independent_set([], []).shape, (0, 0)) - @unittest.skipUnless(_networkx, "no networkx installed") def test_empty_networkx(self): G = nx.Graph() self.assertEqual(dimod.generators.independent_set(G.edges).shape, (0, 0)) @@ -930,7 +918,6 @@ def test_edges(self): self.assertEqual(bqm.offset, 0) self.assertIs(bqm.vartype, dimod.BINARY) - @unittest.skipUnless(_networkx, "no networkx installed") def test_edges_networkx(self): G = nx.complete_graph(3) bqm = dimod.generators.maximum_independent_set(G.edges) @@ -946,7 +933,6 @@ def test_edges_and_nodes(self): self.assertEqual(bqm.offset, 0) self.assertIs(bqm.vartype, dimod.BINARY) - @unittest.skipUnless(_networkx, "no networkx installed") def test_edges_and_nodes_networkx(self): G = nx.complete_graph(3) G.add_node(3) @@ -960,7 +946,6 @@ def test_empty(self): self.assertEqual(dimod.generators.maximum_independent_set([]).shape, (0, 0)) self.assertEqual(dimod.generators.maximum_independent_set([], []).shape, (0, 0)) - @unittest.skipUnless(_networkx, "no networkx installed") def test_empty_networkx(self): G = nx.Graph() self.assertEqual(dimod.generators.maximum_independent_set(G.edges).shape, (0, 0)) @@ -1012,7 +997,6 @@ def test_functional(self): configs = {tuple(sample[v] for v in range(3)) for sample in sampleset.lowest().samples()} self.assertEqual(configs, {(0, 1, 0), (1, 0, 1)}) - @unittest.skipUnless(_networkx, "no networkx installed") def test_functional_networkx(self): G = nx.complete_graph(3) G.add_nodes_from([0, 2], weight=.5) @@ -1029,7 +1013,6 @@ def test_empty(self): self.assertEqual(dimod.generators.maximum_weight_independent_set([]).shape, (0, 0)) self.assertEqual(dimod.generators.maximum_weight_independent_set([], []).shape, (0, 0)) - @unittest.skipUnless(_networkx, "no networkx installed") def test_empty_networkx(self): G = nx.Graph() self.assertEqual(dimod.generators.maximum_weight_independent_set(G.edges).shape, (0, 0)) @@ -1208,7 +1191,7 @@ def test_constraints_squares(self): else: self.assertEqual(term, 24) -@unittest.skipUnless(_networkx, "no networkx installed") + class TestMIMO(unittest.TestCase): def setUp(self): @@ -1887,7 +1870,6 @@ def test_feasible(self): self.assertEqual(lhs, 0) -@unittest.skipUnless(_networkx, "no networkx installed") class TestMinVertexColoring(unittest.TestCase): def test_chromatic_number(self): G = nx.cycle_graph('abcd') @@ -1899,7 +1881,6 @@ def test_chromatic_number(self): ) -@unittest.skipUnless(_networkx, "no networkx installed") class TestVertexColoring(unittest.TestCase): def test_single_node(self): G = nx.Graph() @@ -2014,7 +1995,6 @@ def test_typical(self): self.assertAlmostEqual(en, energy) -@unittest.skipUnless(_networkx, "no networkx installed") @parameterized.parameterized_class( 'graph', [[nx.Graph()], From 86b29df5f3e726289c4ab439d0ad104ec7621475 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 29 May 2026 11:41:46 -0700 Subject: [PATCH 18/27] Adapt graph_partition generator tests from dwave-graphs --- tests/test_generators.py | 69 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 66 insertions(+), 3 deletions(-) diff --git a/tests/test_generators.py b/tests/test_generators.py index 98fb94097..92eaa8eca 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -12,15 +12,16 @@ # See the License for the specific language governing permissions and # limitations under the License. -import unittest - -import dimod import itertools +import math +import unittest import networkx as nx import numpy as np import parameterized +import dimod + # TODO: update to dwave-graphs once released try: import dwave_networkx as dnx @@ -2064,6 +2065,68 @@ def test_min_maximal_matching_bqm(self): self.assertGreater(len(edges), cardinality) +class TestPartitioning(unittest.TestCase): + def get_partitions(self, cqm): + # copied from: :meth:`dwave.graphs.algorithms.partition.partition`. + + sampler = dimod.ExactCQMSolver() + response = sampler.sample_cqm(cqm) + possible_partitions = response.filter(lambda d: d.is_feasible) + + if not possible_partitions: + return {} + + indicators = (key for key, value in possible_partitions.first.sample.items() if math.isclose(value, 1.)) + node_partition = {key[0]: key[1] for key in indicators} + return node_partition + + def test_edge_cases(self): + G = nx.Graph() + cqm = dimod.generators.partition.graph_partition(G, num_partitions=2) + node_partitions = self.get_partitions(cqm) + self.assertTrue(node_partitions == {}) + + def test_typical_cases(self): + G = nx.complete_graph(8) + cqm = dimod.generators.partition.graph_partition(G, num_partitions=4) + node_partitions = self.get_partitions(cqm) + for i in range(4): + self.assertEqual(sum(x == i for x in node_partitions.values()), 2) # 4 equally sized subsets + + cqm = dimod.generators.partition.graph_partition(8, num_partitions=4) + node_partitions2 = self.get_partitions(cqm) + self.assertEqual(node_partitions, node_partitions2) + + G = nx.complete_graph(10) + cqm = dimod.generators.partition.graph_partition(G, num_partitions=2) + node_partitions = self.get_partitions(cqm) + self.assertEqual(sum(x == 0 for x in node_partitions.values()), 5) # half of the nodes in subset '0' + + nx.set_edge_attributes(G, 1, 'weight') + cqm = dimod.generators.partition.graph_partition(G, num_partitions=2) + node_partitions = self.get_partitions(cqm) + self.assertEqual(sum(x == 0 for x in node_partitions.values()), 5) # half of the nodes in subset '0' + + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 2), (1, 3), (3, 4), (2, 4)]) + cqm = dimod.generators.partition.graph_partition(G, num_partitions=2) + node_partitions = self.get_partitions(cqm) + self.assertIn(sum(x == 0 for x in node_partitions.values()), (2, 3)) # either 2 or 3 nodes in subset '0' (ditto '1') + + G = nx.Graph() + G.add_edges_from([(0, 1), (0, 2), (1, 2), (2, 3), (3, 4), (3, 5), (4, 5)]) + cqm = dimod.generators.partition.graph_partition(G, num_partitions=2) + node_partitions = self.get_partitions(cqm) + self.assertTrue(node_partitions[0] == node_partitions[1] == node_partitions[2]) + self.assertTrue(node_partitions[3] == node_partitions[4] == node_partitions[5]) + + nx.set_edge_attributes(G, values=1, name='weight') + nx.set_edge_attributes(G, values={(2, 3): 100}, name='weight') + cqm = dimod.generators.partition.graph_partition(G, num_partitions=2) + node_partitions = self.get_partitions(cqm) + self.assertEqual(node_partitions[2], node_partitions[3]) # weight edges are respected + + class TestTSPQUBO(unittest.TestCase): def test_empty(self): Q = dimod.generators.tsp.traveling_salesperson_qubo(nx.Graph()) From 7fd00b2862f339882127e4b6206cb9d3e0a94fe4 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 29 May 2026 15:17:19 -0700 Subject: [PATCH 19/27] Rewrite structural imbalance BQM generator Return BQM instead of ising, add type hints, refresh docs. --- dimod/generators/social.py | 99 +++++++++++++++++++------------------- 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/dimod/generators/social.py b/dimod/generators/social.py index ce754303c..d9bccb8a4 100644 --- a/dimod/generators/social.py +++ b/dimod/generators/social.py @@ -12,65 +12,66 @@ # See the License for the specific language governing permissions and # limitations under the License. -__all__ = ["structural_imbalance_ising", - ] +import dimod +__all__ = ["structural_imbalance", + ] -def structural_imbalance_ising(S): - """Construct the Ising problem to calculate the structural imbalance of a signed social network. - A signed social network graph is a graph whose signed edges - represent friendly/hostile interactions between nodes. A - signed social network is considered balanced if it can be cleanly - divided into two factions, where all relations within a faction are - friendly, and all relations between factions are hostile. The measure - of imbalance or frustration is the minimum number of edges that - violate this rule. +def structural_imbalance(graph: 'nx.Graph') -> dimod.BinaryQuadraticModel: + """Construct a binary quadratic model (BQM) to calculate the structural + imbalance of a signed social network. - Parameters - ---------- - S : NetworkX graph - A social graph on which each edge has a 'sign' attribute with a numeric value. + A signed social network graph is a graph whose signed edges represent + friendly/hostile interactions between nodes. A signed social network is + considered balanced if it can be cleanly divided into two factions, where + all relations within a faction are friendly, and all relations between + factions are hostile. The measure of imbalance or frustration is the minimum + number of edges that violate this rule. - Returns - ------- - h : dict - The linear biases of the Ising problem. Each variable in the Ising problem represent - a node in the signed social network. The solution that minimized the Ising problem - will assign each variable a value, either -1 or 1. This bi-coloring defines the factions. + Args: + graph: + A social graph (in a NetworkX graph) on which each edge has a 'sign' + attribute with a numeric value. - J : dict - The quadratic biases of the Ising problem. + Returns: + A binary quadratic model. Each variable in the model represents a node + in the signed social network. The solution that minimized the BQM will + assign each variable a value, either -1 or 1. This bi-coloring defines + the factions. - Raises - ------ - ValueError - If any edge does not have a 'sign' attribute. + Raises: + ValueError: If any edge does not have a 'sign' attribute. - Examples - -------- - >>> import dimod - >>> from dwave_networkx.algorithms.social import structural_imbalance_ising - ... - >>> S = nx.Graph() - >>> S.add_edge('Alice', 'Bob', sign=1) # Alice and Bob are friendly - >>> S.add_edge('Alice', 'Eve', sign=-1) # Alice and Eve are hostile - >>> S.add_edge('Bob', 'Eve', sign=-1) # Bob and Eve are hostile - ... - >>> h, J = structural_imbalance_ising(S) - >>> h # doctest: +SKIP - {'Alice': 0.0, 'Bob': 0.0, 'Eve': 0.0} - >>> J # doctest: +SKIP - {('Alice', 'Bob'): -1.0, ('Alice', 'Eve'): 1.0, ('Bob', 'Eve'): 1.0} + Examples: + >>> import dimod + >>> import networkx as nx + ... + >>> S = nx.Graph() + >>> S.add_edge('Alice', 'Bob', sign=1) # Alice and Bob are friendly + >>> S.add_edge('Alice', 'Eve', sign=-1) # Alice and Eve are hostile + >>> S.add_edge('Bob', 'Eve', sign=-1) # Bob and Eve are hostile + ... + >>> bqm = dimod.generators.structural_imbalance(S) + >>> bqm.linear # doctest: +SKIP + {'Alice': 0.0, 'Bob': 0.0, 'Eve': 0.0} + >>> bqm.quadratic # doctest: +SKIP + {('Alice', 'Bob'): -1.0, ('Alice', 'Eve'): 1.0, ('Bob', 'Eve'): 1.0} """ - h = {v: 0.0 for v in S} - J = {} - for u, v, data in S.edges(data=True): + if not hasattr(graph, 'nodes') or not hasattr(graph, 'edges'): + raise ValueError("Signed social network graph in NetworkX format required") + + bqm = dimod.BinaryQuadraticModel.empty('SPIN') + + for v in graph: + bqm.add_linear(v, 0.0) + + for u, v, data in graph.edges(data=True): try: - J[(u, v)] = -1. * data['sign'] + bqm.add_quadratic(u, v, -1. * data['sign']) except KeyError: - raise ValueError(("graph should be a signed social graph," - "each edge should have a 'sign' attr")) + raise ValueError("graph should be a signed social graph, " + "each edge should have a 'sign' attr") - return h, J + return bqm From 4bec7e542a24d83e6da96a0f82cd8922e7c7349e Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 29 May 2026 16:27:37 -0700 Subject: [PATCH 20/27] Adapt structural imbalance generator tests from dwave-graphs --- tests/test_generators.py | 77 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 77 insertions(+) diff --git a/tests/test_generators.py b/tests/test_generators.py index 92eaa8eca..5e48cbfd7 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -14,6 +14,7 @@ import itertools import math +import operator import unittest import networkx as nx @@ -2127,6 +2128,82 @@ def test_typical_cases(self): self.assertEqual(node_partitions[2], node_partitions[3]) # weight edges are respected +class TestSocial(unittest.TestCase): + def structural_imbalance(self, graph): + # see: :meth:`dwave.graphs.social.structural_imbalance`. + + bqm = dimod.generators.social.structural_imbalance(graph) + + # use the exact solver to find low energy states + sampler = dimod.ExactSolver() + response = sampler.sample(bqm) + + # we want the lowest energy sample + sample = response.first.sample + + # spins determine the color + colors = {v: (spin + 1) // 2 for v, spin in sample.items()} + return colors + + def check_bicolor(self, colors): + # colors should be ints and either 0 or 1 + for c in colors.values(): + self.assertIn(c, (0, 1)) + + def all_eq(self, l): + return all(itertools.starmap(operator.eq, zip(l, l[1:]))) + + def test_structural_imbalance_basic(self): + blueteam = ['Alice', 'Bob', 'Carol'] + redteam0 = ['Eve'] + redteam1 = ['Mallory', 'Trudy'] + + S = nx.Graph() + for p0, p1 in itertools.combinations(blueteam, 2): + S.add_edge(p0, p1, sign=1) + + S.add_edge(*redteam1, sign=1) + for p0 in blueteam: + for p1 in redteam0: + S.add_edge(p0, p1, sign=-1) + for p1 in redteam1: + S.add_edge(p0, p1, sign=-1) + + colors = self.structural_imbalance(S) + self.check_bicolor(colors) + + # blue team is one color, and red team another + self.assertTrue(self.all_eq([colors[n] for n in blueteam])) + self.assertTrue(self.all_eq([colors[n] for n in (redteam0 + redteam1)])) + self.assertNotEqual(colors[blueteam[0]], colors[redteam0[0]]) + + greenteam = ['Ted'] + for p0 in set(S.nodes): + for p1 in greenteam: + S.add_edge(p0, p1, sign=1) + + colors = self.structural_imbalance(S) + self.check_bicolor(colors) + + def test_non_nx_graph(self): + with self.assertRaises(ValueError): + dimod.generators.social.structural_imbalance({'a': 1}) + + def test_invalid_graph(self): + S = nx.Graph() + S.add_edge('Alice', 'Bob') + + with self.assertRaises(ValueError): + dimod.generators.social.structural_imbalance(S) + + def test_frustrated_hostile_edge(self): + S = nx.florentine_families_graph() + nx.set_edge_attributes(S, -1, 'sign') + + colors = self.structural_imbalance(S) + self.check_bicolor(colors) + + class TestTSPQUBO(unittest.TestCase): def test_empty(self): Q = dimod.generators.tsp.traveling_salesperson_qubo(nx.Graph()) From 7f7b51e4c3f7a39d821a4374f9246260ea0e9719 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 29 May 2026 17:27:28 -0700 Subject: [PATCH 21/27] Refresh the TSP generator: return BQM, add types, reformat docs --- dimod/generators/tsp.py | 113 +++++++++++++++++++++------------------ tests/test_generators.py | 39 ++++++-------- 2 files changed, 79 insertions(+), 73 deletions(-) diff --git a/dimod/generators/tsp.py b/dimod/generators/tsp.py index 12ea0e659..ff831080a 100644 --- a/dimod/generators/tsp.py +++ b/dimod/generators/tsp.py @@ -14,110 +14,121 @@ import itertools -from collections import defaultdict +import dimod -__all__ = ["traveling_salesperson_qubo", - "traveling_salesman_qubo", +__all__ = ["traveling_salesperson", + "traveling_salesman", ] -def traveling_salesperson_qubo(G, lagrange=None, weight='weight', missing_edge_weight=None): - """Return the QUBO with ground states corresponding to a minimum TSP route. +def traveling_salesperson(graph: 'nx.Graph', + lagrange: float | None = None, + weight: str = 'weight', + missing_edge_weight: float | None = None + ) -> dimod.BinaryQuadraticModel: + """Return a binary quadratic model (BQM) with ground states corresponding + to a minimum TSP route. - If :math:`|G|` is the number of nodes in the graph, the resulting qubo will have: + If :math:`|graph|` is the number of nodes in the graph, the resulting qubo + will have: - * :math:`|G|^2` variables/nodes - * :math:`2 |G|^2 (|G| - 1)` interactions/edges + * :math:`|graph|^2` variables/nodes + * :math:`2 |graph|^2 (|graph| - 1)` interactions/edges - Parameters - ---------- - G : NetworkX graph - A complete graph in which each edge has a attribute giving its weight. + Args: + graph: + A complete graph in which each edge has an attribute giving its + weight, given as a NetworkX graph. - lagrange : number, optional (default None) - Lagrange parameter to weight constraints (no edges within set) - versus objective (largest set possible). + lagrange: + Lagrange parameter to weight constraints (no edges within set) + versus objective (largest set possible). - weight : optional (default 'weight') - The name of the edge attribute containing the weight. + weight: + The name of the edge attribute containing the weight, defaults to + "weight". - missing_edge_weight : number, optional (default None) - For bi-directional graphs, the weight given to missing edges. - If None is given (the default), missing edges will be set to - the sum of all weights. - - Returns - ------- - QUBO : dict - The QUBO with ground states corresponding to a minimum travelling - salesperson route. The QUBO variables are labelled `(c, t)` where `c` - is a node in `G` and `t` is the time index. For instance, if `('a', 0)` - is 1 in the ground state, that means the node 'a' is visted first. + missing_edge_weight: + For bi-directional graphs, the weight given to missing edges. + If None is given (the default), missing edges will be set to + the sum of all weights. + + Returns: + A binary quadratic model (BQM) with ground states corresponding to a + minimum traveling salesperson route. The BQM variables are labelled + ``(c, t)`` where ``c`` is a node in ``graph`` and ``t`` is the time + index. For instance, if ``('a', 0)`` is 1 in the ground state, that means + the node 'a' is visited first. """ - N = G.number_of_nodes() + if not hasattr(graph, 'nodes') or not hasattr(graph, 'edges'): + raise ValueError("A NetworkX graph with weights data required") + + N = graph.number_of_nodes() if lagrange is None: # If no lagrange parameter provided, set to 'average' tour length. # Usually a good estimate for a lagrange parameter is between 75-150% # of the objective function value, so we come up with an estimate for # tour length and use that. - if G.number_of_edges()>0: - lagrange = G.size(weight=weight)*G.number_of_nodes()/G.number_of_edges() + if graph.number_of_edges() > 0: + lagrange = (graph.size(weight=weight) + * graph.number_of_nodes() + / graph.number_of_edges()) else: lagrange = 2 - + # calculate default missing_edge_weight if required if missing_edge_weight is None: # networkx method to calculate sum of all weights - missing_edge_weight = G.size(weight=weight) + missing_edge_weight = graph.size(weight=weight) # some input checking if N in (1, 2): msg = "graph must have at least 3 nodes or be empty" raise ValueError(msg) - # Creating the QUBO - Q = defaultdict(float) + # Creating the BQM + bqm = dimod.BinaryQuadraticModel.empty(dimod.BINARY) # Constraint that each row has exactly one 1 - for node in G: + for node in graph: for pos_1 in range(N): - Q[((node, pos_1), (node, pos_1))] -= lagrange + bqm.add_linear((node, pos_1), -lagrange) for pos_2 in range(pos_1+1, N): - Q[((node, pos_1), (node, pos_2))] += 2.0*lagrange + bqm.add_quadratic((node, pos_1), (node, pos_2), 2.0*lagrange) # Constraint that each col has exactly one 1 for pos in range(N): - for node_1 in G: - Q[((node_1, pos), (node_1, pos))] -= lagrange - for node_2 in set(G)-{node_1}: - # QUBO coefficient is 2*lagrange, but we are placing this value + for node_1 in graph: + bqm.add_linear((node_1, pos), -lagrange) + for node_2 in set(graph)-{node_1}: + # quadratic coefficient is 2*lagrange, but we are placing this value # above *and* below the diagonal, so we put half in each position. - Q[((node_1, pos), (node_2, pos))] += lagrange + bqm.add_quadratic((node_1, pos), (node_2, pos), lagrange) # Objective that minimizes distance - for u, v in itertools.combinations(G.nodes, 2): + for u, v in itertools.combinations(graph.nodes, 2): for pos in range(N): nextpos = (pos + 1) % N # going from u -> v try: - value = G[u][v][weight] + value = graph[u][v][weight] except KeyError: value = missing_edge_weight - Q[((u, pos), (v, nextpos))] += value + bqm.add_quadratic((u, pos), (v, nextpos), value) # going from v -> u try: - value = G[v][u][weight] + value = graph[v][u][weight] except KeyError: value = missing_edge_weight - Q[((v, pos), (u, nextpos))] += value + bqm.add_quadratic((v, pos), (u, nextpos), value) - return Q + return bqm -traveling_salesman_qubo = traveling_salesperson_qubo +traveling_salesman = traveling_salesperson diff --git a/tests/test_generators.py b/tests/test_generators.py index 5e48cbfd7..cd62e06e4 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2204,10 +2204,10 @@ def test_frustrated_hostile_edge(self): self.check_bicolor(colors) -class TestTSPQUBO(unittest.TestCase): +class TestTSP(unittest.TestCase): def test_empty(self): - Q = dimod.generators.tsp.traveling_salesperson_qubo(nx.Graph()) - self.assertEqual(Q, {}) + bqm = dimod.generators.tsp.traveling_salesperson(nx.Graph()) + self.assertEqual(bqm.to_qubo(), ({}, 0)) def test_k3(self): # 3cycle so all paths are equally good @@ -2216,8 +2216,7 @@ def test_k3(self): ('b', 'c', 1.0), ('a', 'c', 2.0)]) - Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.tsp.traveling_salesperson(G, lagrange=10) # all routes are min weight min_routes = list(itertools.permutations(G.nodes)) @@ -2251,8 +2250,7 @@ def test_k3_bidirectional(self): ('a', 'c', 2.0), ('c', 'a', 2.0)]) - Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.tsp.traveling_salesperson(G, lagrange=10) # all routes are min weight min_routes = list(itertools.permutations(G.nodes)) @@ -2284,7 +2282,7 @@ def test_graph_missing_edges(self): ('b', 'c', 1.0), ('a', 'c', 2.0), ]) - Q1 = dimod.generators.tsp.traveling_salesperson_qubo(G1, lagrange=10) + bqm1 = dimod.generators.tsp.traveling_salesperson(G1, lagrange=10) G2 = nx.Graph() G2.add_weighted_edges_from([ @@ -2292,9 +2290,9 @@ def test_graph_missing_edges(self): ('a', 'c', 2.0), ]) # make sure that missing_edge_weight gets applied correctly - Q2 = dimod.generators.tsp.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) + bqm2 = dimod.generators.tsp.traveling_salesperson(G2, lagrange=10, missing_edge_weight=1.0) - self.assertDictEqual(Q1, Q2) + self.assertEqual(bqm1, bqm2) def test_digraph_missing_edges(self): G1 = nx.DiGraph() @@ -2306,7 +2304,7 @@ def test_digraph_missing_edges(self): ('a', 'c', 2.0), ('c', 'a', 2.0), ]) - Q1 = dimod.generators.tsp.traveling_salesperson_qubo(G1, lagrange=10) + bqm1 = dimod.generators.tsp.traveling_salesperson(G1, lagrange=10) G2 = nx.DiGraph() G2.add_weighted_edges_from([ @@ -2318,9 +2316,9 @@ def test_digraph_missing_edges(self): ]) # make sure that missing_edge_weight gets applied correctly - Q2 = dimod.generators.tsp.traveling_salesperson_qubo(G2, lagrange=10, missing_edge_weight=1.0) + bqm2 = dimod.generators.tsp.traveling_salesperson(G2, lagrange=10, missing_edge_weight=1.0) - self.assertDictEqual(Q1, Q2) + self.assertEqual(bqm1, bqm2) def test_k4_equal_weights(self): # k5 with all equal weights so all paths are equally good @@ -2328,8 +2326,7 @@ def test_k4_equal_weights(self): G.add_weighted_edges_from((u, v, .5) for u, v in itertools.combinations(range(4), 2)) - Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.tsp.traveling_salesperson(G, lagrange=10) # all routes are min weight min_routes = list(itertools.permutations(G.nodes)) @@ -2364,8 +2361,7 @@ def test_k4(self): (0, 2, 2), (1, 3, 2)]) - Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange=10) - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.tsp.traveling_salesperson(G, lagrange=10) # good routes won't have 0<->2 or 1<->3 min_routes = [(0, 1, 2, 3), @@ -2405,19 +2401,19 @@ def test_weighted_complete_graph(self): lagrange = 5.0 - Q = dimod.generators.tsp.traveling_salesperson_qubo(G, lagrange, 'weight') + bqm = dimod.generators.tsp.traveling_salesperson(G, lagrange, 'weight') N = G.number_of_nodes() correct_sum = G.size('weight')*2*N-2*N*N*lagrange+2*N*N*(N-1)*lagrange - actual_sum = sum(Q.values()) + actual_sum = sum(bqm.linear.values()) + sum(bqm.quadratic.values()) self.assertEqual(correct_sum, actual_sum) def test_exceptions(self): G = nx.Graph([(0, 1)]) with self.assertRaises(ValueError): - dimod.generators.tsp.traveling_salesperson_qubo(G) + dimod.generators.tsp.traveling_salesperson(G) def test_docstring_size(self): # in the docstring we state the size of the resulting BQM, this checks @@ -2427,8 +2423,7 @@ def test_docstring_size(self): G.add_weighted_edges_from((u, v, .5) for u, v in itertools.combinations(range(n), 2)) - Q = dimod.generators.tsp.traveling_salesperson_qubo(G) - bqm = dimod.BinaryQuadraticModel.from_qubo(Q) + bqm = dimod.generators.tsp.traveling_salesperson(G) self.assertEqual(len(bqm), n**2) self.assertEqual(len(bqm.quadratic), 2*n*n*(n - 1)) From 7136a2bc625a8bb79528ef8886f373685b1b17bd Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Fri, 29 May 2026 17:35:57 -0700 Subject: [PATCH 22/27] Simplify TSP tests --- tests/test_generators.py | 87 +++++++++++----------------------------- 1 file changed, 23 insertions(+), 64 deletions(-) diff --git a/tests/test_generators.py b/tests/test_generators.py index cd62e06e4..a017d058f 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -2205,6 +2205,25 @@ def test_frustrated_hostile_edge(self): class TestTSP(unittest.TestCase): + def check_routes(self, bqm, min_routes, sampleset): + ground_energy = sampleset.first.energy + + # all possible routes are equally good + for route in min_routes: + sample = {v: 0 for v in bqm.variables} + for idx, city in enumerate(route): + sample[(city, idx)] = 1 + self.assertAlmostEqual(bqm.energy(sample), ground_energy) + + # all min-energy solutions are valid routes + ground_count = 0 + for sample, energy in sampleset.data(['sample', 'energy']): + if abs(energy - ground_energy) > .001: + break + ground_count += 1 + + self.assertEqual(ground_count, len(min_routes)) + def test_empty(self): bqm = dimod.generators.tsp.traveling_salesperson(nx.Graph()) self.assertEqual(bqm.to_qubo(), ({}, 0)) @@ -2223,23 +2242,8 @@ def test_k3(self): # get the min energy of the qubo sampleset = dimod.ExactSolver().sample(bqm) - ground_energy = sampleset.first.energy - # all possible routes are equally good - for route in min_routes: - sample = {v: 0 for v in bqm.variables} - for idx, city in enumerate(route): - sample[(city, idx)] = 1 - self.assertAlmostEqual(bqm.energy(sample), ground_energy) - - # all min-energy solutions are valid routes - ground_count = 0 - for sample, energy in sampleset.data(['sample', 'energy']): - if abs(energy - ground_energy) > .001: - break - ground_count += 1 - - self.assertEqual(ground_count, len(min_routes)) + self.check_routes(bqm, min_routes, sampleset) def test_k3_bidirectional(self): G = nx.DiGraph() @@ -2257,23 +2261,8 @@ def test_k3_bidirectional(self): # get the min energy of the qubo sampleset = dimod.ExactSolver().sample(bqm) - ground_energy = sampleset.first.energy - - # all possible routes are equally good - for route in min_routes: - sample = {v: 0 for v in bqm.variables} - for idx, city in enumerate(route): - sample[(city, idx)] = 1 - self.assertAlmostEqual(bqm.energy(sample), ground_energy) - # all min-energy solutions are valid routes - ground_count = 0 - for sample, energy in sampleset.data(['sample', 'energy']): - if abs(energy - ground_energy) > .001: - break - ground_count += 1 - - self.assertEqual(ground_count, len(min_routes)) + self.check_routes(bqm, min_routes, sampleset) def test_graph_missing_edges(self): G1 = nx.Graph() @@ -2333,23 +2322,8 @@ def test_k4_equal_weights(self): # get the min energy of the qubo sampleset = dimod.ExactSolver().sample(bqm) - ground_energy = sampleset.first.energy - # all possible routes are equally good - for route in min_routes: - sample = {v: 0 for v in bqm.variables} - for idx, city in enumerate(route): - sample[(city, idx)] = 1 - self.assertAlmostEqual(bqm.energy(sample), ground_energy) - - # all min-energy solutions are valid routes - ground_count = 0 - for sample, energy in sampleset.data(['sample', 'energy']): - if abs(energy - ground_energy) > .001: - break - ground_count += 1 - - self.assertEqual(ground_count, len(min_routes)) + self.check_routes(bqm, min_routes, sampleset) def test_k4(self): # good routes are 0,1,2,3 or 3,2,1,0 (and their rotations) @@ -2375,23 +2349,8 @@ def test_k4(self): # get the min energy of the qubo sampleset = dimod.ExactSolver().sample(bqm) - ground_energy = sampleset.first.energy - - # all possible routes are equally good - for route in min_routes: - sample = {v: 0 for v in bqm.variables} - for idx, city in enumerate(route): - sample[(city, idx)] = 1 - self.assertAlmostEqual(bqm.energy(sample), ground_energy) - - # all min-energy solutions are valid routes - ground_count = 0 - for sample, energy in sampleset.data(['sample', 'energy']): - if abs(energy - ground_energy) > .001: - break - ground_count += 1 - self.assertEqual(ground_count, len(min_routes)) + self.check_routes(bqm, min_routes, sampleset) def test_weighted_complete_graph(self): G = nx.Graph() From 8fcb9dfa426d07d9e733ef9eff0dbb94225fc093 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Mon, 1 Jun 2026 10:32:35 -0700 Subject: [PATCH 23/27] Add release notes for moved dnx generators --- ...-generators-from-dnx-db2b3873ef912599.yaml | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 releasenotes/notes/add-generators-from-dnx-db2b3873ef912599.yaml diff --git a/releasenotes/notes/add-generators-from-dnx-db2b3873ef912599.yaml b/releasenotes/notes/add-generators-from-dnx-db2b3873ef912599.yaml new file mode 100644 index 000000000..f5e974371 --- /dev/null +++ b/releasenotes/notes/add-generators-from-dnx-db2b3873ef912599.yaml @@ -0,0 +1,28 @@ +--- +prelude: > + Binary and constrained quadratic model generators that used to reside in + ``dwave-networkx`` (now ``dwave-graphs``) have been moved to + ``dimod.generators``. +features: + - | + Add generators for binary quadratic models with ground states corresponding + to a vertex coloring (``dimod.generators.vertex_coloring``) and a minimum + vertex coloring (``dimod.generators.min_vertex_coloring``). + - | + Add a binary quadratic model generator for a Markov network + (``dimod.generators.markov_network``). + - | + Add generators binary quadratic models with ground states corresponding to + a graph matching (``dimod.generators.matching``), graphs's maximal matching + (``dimod.generators.maximal_matching``) and graph's minimum maximal matching + (``dimod.generators.min_maximal_matching``). + - | + Add a generator for a constrained quadratic model with ground states + corresponding to a balanced k-partition of graph + (``dimod.generators.graph_partition``). + - | + Add a binary quadratic model generator to calculate the structural imbalance + of a signed social network (``dimod.generators.structural_imbalance``). + - | + Add a generator for a binary quadratic model with ground states corresponding + to a minimum TSP route (``dimod.generators.traveling_salesperson``). From 09ad7964ca626ee2949382602c52bf905d6b522f Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Mon, 1 Jun 2026 11:54:56 -0700 Subject: [PATCH 24/27] Update docs to include the dnx problem generators --- docs/generators.rst | 9 +++++++++ docs/requirements.txt | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/docs/generators.rst b/docs/generators.rst index 621d84e70..751995b45 100644 --- a/docs/generators.rst +++ b/docs/generators.rst @@ -32,6 +32,7 @@ Constraints binary_encoding combinations fulladder_gate + graph_partition halfadder_gate multiplication_circuit or_gate @@ -47,9 +48,13 @@ Optimization coordinated_multipoint independent_set knapsack + matching + maximal_matching maximum_independent_set maximum_weight_independent_set mimo + min_maximal_matching + min_vertex_coloring multi_knapsack quadratic_assignment quadratic_knapsack @@ -57,6 +62,9 @@ Optimization random_bin_packing random_knapsack random_multi_knapsack + structural_imbalance + traveling_salesperson + vertex_coloring Random ====== @@ -67,6 +75,7 @@ Random doped gnm_random_bqm gnp_random_bqm + markov_network randint random_2in4sat random_bin_packing diff --git a/docs/requirements.txt b/docs/requirements.txt index fd436e07c..e31439b7c 100644 --- a/docs/requirements.txt +++ b/docs/requirements.txt @@ -4,4 +4,4 @@ sphinx==7.3.7 breathe notebook==6.4.11 -networkx==2.5 \ No newline at end of file +networkx From 9751c3ebbb0170e73df02ace53fe7e4dc6eff787 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Mon, 1 Jun 2026 15:37:37 -0700 Subject: [PATCH 25/27] Add min_vertex_coloring smoke tests --- dimod/generators/coloring.py | 2 +- tests/test_generators.py | 8 ++++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/dimod/generators/coloring.py b/dimod/generators/coloring.py index 2d2b16b90..d74f0e790 100644 --- a/dimod/generators/coloring.py +++ b/dimod/generators/coloring.py @@ -200,7 +200,7 @@ def min_vertex_coloring(graph: GraphLike, weights = [p / (num_penalized + 1) for p in range(1, num_penalized + 1)] for p, c in zip(weights, range(chromatic_lb, chromatic_ub)): - for v in bqm.variables: + for v in graph.nodes: bqm.linear[(v, c)] += p return bqm diff --git a/tests/test_generators.py b/tests/test_generators.py index a017d058f..128eef428 100644 --- a/tests/test_generators.py +++ b/tests/test_generators.py @@ -1882,6 +1882,14 @@ def test_chromatic_number(self): dimod.generators.coloring.vertex_coloring(G, range(2)) ) + @parameterized.parameterized.expand([ + (nx.path_graph(5), ), + (nx.cycle_graph(5), ), + (nx.complete_graph(3), ), + ]) + def test_smoke(self, G): + dimod.generators.min_vertex_coloring(G) + class TestVertexColoring(unittest.TestCase): def test_single_node(self): From 3f56bd735348732c825c5d62148c36e75c25b2a1 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Tue, 2 Jun 2026 14:44:03 -0700 Subject: [PATCH 26/27] Update license year per code review --- dimod/generators/coloring.py | 2 +- dimod/generators/markov.py | 2 +- dimod/generators/matching.py | 2 +- dimod/generators/partition.py | 2 +- dimod/generators/social.py | 2 +- dimod/generators/tsp.py | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/dimod/generators/coloring.py b/dimod/generators/coloring.py index d74f0e790..21bd2aa76 100644 --- a/dimod/generators/coloring.py +++ b/dimod/generators/coloring.py @@ -1,4 +1,4 @@ -# Copyright 2018 D-Wave Systems Inc. +# Copyright 2026 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimod/generators/markov.py b/dimod/generators/markov.py index 4ba828371..fe057eeb1 100644 --- a/dimod/generators/markov.py +++ b/dimod/generators/markov.py @@ -1,4 +1,4 @@ -# Copyright 2019 D-Wave Systems Inc. +# Copyright 2026 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimod/generators/matching.py b/dimod/generators/matching.py index 3eb821489..145a1cb59 100644 --- a/dimod/generators/matching.py +++ b/dimod/generators/matching.py @@ -1,4 +1,4 @@ -# Copyright 2018 D-Wave Systems Inc. +# Copyright 2026 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimod/generators/partition.py b/dimod/generators/partition.py index 45a84513c..3f74c7b9c 100644 --- a/dimod/generators/partition.py +++ b/dimod/generators/partition.py @@ -1,4 +1,4 @@ -# Copyright 2021 D-Wave Systems Inc. +# Copyright 2026 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimod/generators/social.py b/dimod/generators/social.py index d9bccb8a4..9865b9db4 100644 --- a/dimod/generators/social.py +++ b/dimod/generators/social.py @@ -1,4 +1,4 @@ -# Copyright 2018 D-Wave Systems Inc. +# Copyright 2026 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. diff --git a/dimod/generators/tsp.py b/dimod/generators/tsp.py index ff831080a..fabe853ed 100644 --- a/dimod/generators/tsp.py +++ b/dimod/generators/tsp.py @@ -1,4 +1,4 @@ -# Copyright 2018 D-Wave Systems Inc. +# Copyright 2026 D-Wave # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. From 038a3182e2ae9bd2e0ff5dfd2ebfcd30a6f80ac0 Mon Sep 17 00:00:00 2001 From: Radomir Stevanovic Date: Tue, 2 Jun 2026 16:57:57 -0700 Subject: [PATCH 27/27] Apply suggestions from code review Co-authored-by: Theodor Isacsson --- dimod/generators/coloring.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dimod/generators/coloring.py b/dimod/generators/coloring.py index 21bd2aa76..93245bbab 100644 --- a/dimod/generators/coloring.py +++ b/dimod/generators/coloring.py @@ -206,7 +206,8 @@ def min_vertex_coloring(graph: GraphLike, return bqm -def is_cycle(G: 'nx.Graph') -> bool: +@graph_argument('G', as_networkx=True) +def is_cycle(G: GraphLike) -> bool: """Determines whether the given graph is a cycle or circle graph. A cycle graph or circular graph is a graph that consists of a single cycle.