dimod icon indicating copy to clipboard operation
dimod copied to clipboard

`DQM.from_bqm` or similar

Open arcondello opened this issue 4 years ago • 1 comments

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.

arcondello avatar Aug 09 '21 23:08 arcondello

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

arcondello avatar Aug 10 '21 00:08 arcondello