dimod
dimod copied to clipboard
`DQM.from_bqm` or similar
Something like
import warnings
from typing import Collection, Dict, Hashable, Mapping, Optional, Tuple
import dimod
try:
from dimod.typing import Variable
except ImportError:
# dimod < 0.10
Variable = Hashable
def bqm_to_dqm(bqm: dimod.BinaryQuadraticModel,
one_hots: Optional[Collection[Collection[Variable]]] = None,
) -> Tuple[dimod.DiscreteQuadraticModel, Dict[Variable, Tuple[Variable, int]]]:
"""Convert a BQM into a DQM.
Args:
bqm: A binary quadratic model.
one_hots: a collection of one-hot constraints. Each one-hot constraint
should cover a set of BQM variables. The one-hot constraints
cannot overlap
Returns:
A 2-tuple:
dqm: A discrete quadratic model
mapping: A mapping from the variable of the bqm to the `(variable, case)`
in the dqm.
"""
if bqm.vartype is dimod.SPIN:
raise ValueError("given bqm must be binary")
dqm = dimod.DiscreteQuadraticModel()
mapping: Dict[Variable, Tuple[Variable, int]] = dict()
if one_hots is not None:
for c, constraint in enumerate(one_hots):
if len(constraint) == 1:
# this is just binary
continue
dqm.add_variable(num_cases=len(constraint), label=c)
for case, v in enumerate(constraint):
if v in mapping:
raise ValueError("one-hot constraints must be disjoint")
mapping[v] = (c, case)
for v in bqm.variables:
if v not in mapping:
# variables that are not included in a one-hot constraint are
# added as two-case variables
dqm.add_variable(2, label=v)
mapping[v] = (v, 1) # track the case that indicates v is true
dqm.set_linear_case(*mapping[v], bqm.get_linear(v))
for u, v, bias in bqm.iter_quadratic():
if mapping[u][0] == mapping[v][0]:
# we can ignore interactions within a one-hot constraint
continue
dqm.set_quadratic_case(*mapping[u], *mapping[v], bias)
try:
# dimod 0.10 supports offsets, just ignore it for older versions
dqm.offset += bqm.offset
except AttributeError:
if bqm.offset:
warnings.warn('bqm has an offset that is ignored', stacklevel=2)
return dqm, mapping
def dqm_sample_to_bqm_sample(sample: Mapping[Variable, int],
mapping: Mapping[Variable, Tuple[Variable, int]],
) -> Dict[Variable, int]:
"""Convert a DQM sample into a BQM sample according to the given mapping
from BQM variables to DQM variables."""
return dict((bqm_v, int(sample[dqm_v] == case)) for bqm_v, (dqm_v, case) in mapping.items())
some basic unittests
import unittest
class Test(unittest.TestCase):
def test_no_onehot(self):
bqm = dimod.generators.gnp_random_bqm('abcdefghijkl', .25, 'SPIN')
bqm.change_vartype('BINARY', inplace=True)
bqm.offset = 0 # zero the offset out since not all dimod versions support it in DQM
dqm, mapping = bqm_to_dqm(bqm)
bqm_sampleset = dimod.ExactSolver().sample(bqm)
dqm_sampleset = dimod.ExactDQMSolver().sample_dqm(dqm)
# this assumes that the lowest is unique. todo: fix
self.assertEqual(dqm_sampleset.first.sample,
dqm_sample_to_bqm_sample(dqm_sampleset.first.sample, mapping))
def test_with_onehot(self):
bqm = dimod.generators.gnp_random_bqm('abcdefghijkl', .25, 'SPIN')
bqm.change_vartype('BINARY', inplace=True)
bqm.offset = 0 # zero the offset out since not all dimod versions support it in DQM
one_hots = [['a', 'b', 'c', 'd'], ['k', 'f']]
dqm, mapping = bqm_to_dqm(bqm, one_hots)
bqm_sampleset = dimod.ExactSolver().sample(bqm)
dqm_sampleset = dimod.ExactDQMSolver().sample_dqm(dqm)
# find the lowest-energy bqm sample that satisfies the one-hot
for sample in bqm_sampleset.samples():
if all(sum(sample[v] for v in const) == 1 for const in one_hots):
# this assumes that the lowest is unique. todo: fix
self.assertEqual(sample,
dqm_sample_to_bqm_sample(dqm_sampleset.first.sample, mapping))
break
the first snippet should work with dimod 0.9 but the tests require 0.10.
Additional Context
For CaseLabelDQM, we would not need to return the mapping.
Example usage (tested with dwave-ocean-sdk==3.4.1):
import dimod
import numpy as np
from dwave.system import LeapHybridDQMSampler
# make simple tsp
num_cities = 5
distances = np.triu(np.random.uniform(size=(num_cities, num_cities)), 1)
penalty_strength = 2
bqm = dimod.BQM('BINARY') # variables are (city, time)
# enforce the "visit each city once" constraint with one-hots
one_hots = [[(c, t) for t in range(num_cities)] for c in range(num_cities)]
# bqm_to_dqm does not allow one-hots to overlap, so we enforce "one city at a
# time" with penalty models
# in dimod 0.10 we could use:
# bqm.add_linear_equality_constraint([((c, t), 1) for c in range(num_cities)], penalty_strength, 1)
for t in range(num_cities):
bqm.update(dimod.generators.combinations([(c, t) for c in range(num_cities)], 1, penalty_strength))
# costs of travelling between cities
for c0 in range(num_cities):
for c1 in range(c0+1, num_cities):
cost = distances[c0, c1]
for t0 in range(num_cities):
t1 = (t0 + 1) % num_cities
bqm.set_quadratic((c0, t0), (c1, t1), cost)
bqm.set_quadratic((c1, t0), (c0, t1), cost)
# construct the DQM, using the one-hots from above
dqm, mapping = bqm_to_dqm(bqm, one_hots)
# solve on LeapHybridDQMSampler
sampleset = LeapHybridDQMSampler().sample_dqm(dqm)
# convert the lowest-energy returned solution back to bqm
print(dqm_sample_to_bqm_sample(sampleset.first.sample, mapping))