Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
27 commits
Select commit Hold shift + click to select a range
88fc02a
Import problem generators from dwave_networkx/algorithms into dimod
randomir Apr 30, 2026
8da580e
Move dnx problem generators under dimod.generators namespace
randomir Apr 30, 2026
0e68172
Clean-up dnx.algorithms to keep only problem generators
randomir Apr 30, 2026
1583c0b
Remove duplicate generators
randomir Apr 30, 2026
a75e281
Make the new dnx generators available in dimod.generators ns
randomir May 4, 2026
470ab56
Import relevant problem generator tests from dwave_networkx
randomir May 4, 2026
292cdec
Update imported dnx tests
randomir May 4, 2026
417cc62
Ensure the correct version of dimod is used for cibw tests
randomir May 5, 2026
e8c0263
Refactor vertex coloring generator to return a BQM; add type hints
randomir May 6, 2026
e2a0607
Refactor min vertex coloring generator to return a BQM
randomir May 8, 2026
560f76b
Remove dependency on networkx of the imported dnx generators
randomir May 11, 2026
ea46d31
Refresh markov_network generator for dimod context
randomir May 11, 2026
2a2f33b
Convert dimod implicit type aliases to explicit
randomir May 28, 2026
bcfc8a9
Refactor matching BQM generators to not require networkx
randomir May 28, 2026
b65983e
Fix whitespaces (dos2unix newlines) in partition cqm generator
randomir May 29, 2026
6997942
Update partition CQM generator: add type hints, refresh docs
randomir May 29, 2026
9423585
Remove networkx checks in tests - always assume nx is present for tests
randomir May 29, 2026
86b29df
Adapt graph_partition generator tests from dwave-graphs
randomir May 29, 2026
7fd00b2
Rewrite structural imbalance BQM generator
randomir May 29, 2026
4bec7e5
Adapt structural imbalance generator tests from dwave-graphs
randomir May 29, 2026
7f7b51e
Refresh the TSP generator: return BQM, add types, reformat docs
randomir May 30, 2026
7136a2b
Simplify TSP tests
randomir May 30, 2026
8fcb9df
Add release notes for moved dnx generators
randomir Jun 1, 2026
09ad796
Update docs to include the dnx problem generators
randomir Jun 1, 2026
9751c3e
Add min_vertex_coloring smoke tests
randomir Jun 1, 2026
3f56bd7
Update license year per code review
randomir Jun 2, 2026
038a318
Apply suggestions from code review
randomir Jun 2, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions dimod/generators/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,15 +16,21 @@
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 *
from dimod.generators.graph import *
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 *
249 changes: 249 additions & 0 deletions dimod/generators/coloring.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,249 @@
# 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.
# 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
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

__all__ = ["min_vertex_coloring",
"vertex_coloring",
]


@graph_argument('graph')
def vertex_coloring(graph: GraphLike,
colors: Sequence,
) -> BinaryQuadraticModel:
"""Generate a binary quadratic model (BQM) with ground states corresponding
to a vertex coloring.

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

The BQM has ground energy :math:`-|V|` and an infeasible gap of 1.

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.

colors:
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
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

bqm = BinaryQuadraticModel(Vartype.BINARY)

# enforce that each variable in G has at most one color
for v in variables:
# 1 in k constraint
for c in colors:
bqm.add_linear((v, c), -1)

for c0, c1 in itertools.combinations(colors, 2):
bqm.add_quadratic((v, c0), (v, c1), 2)

# enforce that adjacent nodes do not have the same color
for u, v in edges:
# NAND constraint
for c in colors:
bqm.add_quadratic((u, c), (v, c), 1)

return bqm


@graph_argument('G', as_networkx=True)
def _chromatic_number_upper_bound(G: 'nx.Graph') -> int:
Comment on lines +84 to +85
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

What's the reason for using @graph_argument here since we're only allowing G to be a nx.Graph? Is it simply for the nx dependency check?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Yes, in case as_networkx=True, input nx graph is simply passed through.

# 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
Comment on lines +89 to +90
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Does @graph_argument expect networkx or will it raise a reasonable exception if called without?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fails gracefully. See #1421.

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)))

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: 'nx.Graph') -> int:
# 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)


@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.

After defining a BQM/QUBO [Dah2013]_ with ground states corresponding to
minimum vertex colorings, use a sampler to sample from it.

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:
A lower 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:
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(graph)
chromatic_ub = chi_ub if chromatic_ub is None else min(chi_ub, chromatic_ub)

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 BQM is one with as many colors as we might need, so we use the
# upper bound
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
# 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 graph.nodes:
bqm.linear[(v, c)] += p

return bqm


@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.

https://en.wikipedia.org/wiki/Cycle_graph

Args:
G: A NetworkX graph.

Returns:
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)
107 changes: 107 additions & 0 deletions dimod/generators/markov.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
# 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.
# 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

__all__ = ['markov_network',
]
Comment on lines +17 to +18
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Suggested change
__all__ = ['markov_network',
]
__all__ = ['markov_network']

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I agree, it's not my favorite indentation style, but it's (local) dimod's style.



###############################################################################
# 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.
#


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`.

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 graph.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 graph.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
Loading