diff --git a/README.rst b/README.rst index 78d0ced..7a354e1 100644 --- a/README.rst +++ b/README.rst @@ -32,9 +32,8 @@ solving binary quadratic models (BQM). import dwave.preprocessing -Currently, this package contains an implementation of roof duality, an algorithm -used for finding minimizing assignments of a polynomial's variables. For details -on the algorithm and how to use it, see the package's +Currently, this package contains several preprocessing composites. For details on +underlying algorithms and usage, see the package's `Reference Documentation `_. .. index-end-marker diff --git a/docs/reference/composites.rst b/docs/reference/composites.rst new file mode 100644 index 0000000..23ea28b --- /dev/null +++ b/docs/reference/composites.rst @@ -0,0 +1,164 @@ +.. _preprocessing_composites: + +========== +Composites +========== + +The `dwave-preprocessing` package includes several composites: + +.. automodule:: dwave.preprocessing.composites + +.... + +Connected Components Composite +------------------------------ + +Class +~~~~~ + +.. autoclass:: ConnectedComponentsComposite + +Properties +~~~~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~ConnectedComponentsComposite.child + ~ConnectedComponentsComposite.children + ~ConnectedComponentsComposite.parameters + ~ConnectedComponentsComposite.properties + +Methods +~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~ConnectedComponentsComposite.sample + ~ConnectedComponentsComposite.sample_ising + ~ConnectedComponentsComposite.sample_qubo + +.... + +Clip Composite +-------------- + +Class +~~~~~ + +.. autoclass:: ClipComposite + +Properties +~~~~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~ClipComposite.child + ~ClipComposite.children + ~ClipComposite.parameters + ~ClipComposite.properties + +Methods +~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~ClipComposite.sample + ~ClipComposite.sample_ising + ~ClipComposite.sample_qubo + +.... + +Fix Variables Composite +------------------------ + +Class +~~~~~ + +.. autoclass:: FixVariablesComposite + +Properties +~~~~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~FixVariablesComposite.child + ~FixVariablesComposite.children + ~FixVariablesComposite.parameters + ~FixVariablesComposite.properties + +Methods +~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~FixVariablesComposite.sample + ~FixVariablesComposite.sample_ising + ~FixVariablesComposite.sample_qubo + +.... + +Scale Composite +--------------- + +Class +~~~~~ + +.. autoclass:: ScaleComposite + +Properties +~~~~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~ScaleComposite.child + ~ScaleComposite.children + ~ScaleComposite.parameters + ~ScaleComposite.properties + +Methods +~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~ScaleComposite.sample + ~ScaleComposite.sample_ising + ~ScaleComposite.sample_qubo + +.... + +Spin Reversal Transform Composite +--------------------------------- + +Class +~~~~~ + +.. autoclass:: SpinReversalTransformComposite + +Properties +~~~~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~SpinReversalTransformComposite.child + ~SpinReversalTransformComposite.children + ~SpinReversalTransformComposite.parameters + ~SpinReversalTransformComposite.properties + +Methods +~~~~~~~ + +.. autosummary:: + :toctree: generated/ + + ~SpinReversalTransformComposite.sample + ~SpinReversalTransformComposite.sample_ising + ~SpinReversalTransformComposite.sample_qubo diff --git a/docs/reference/index.rst b/docs/reference/index.rst index ebd933c..b4d4edc 100644 --- a/docs/reference/index.rst +++ b/docs/reference/index.rst @@ -5,6 +5,7 @@ Reference Documentation .. toctree:: :maxdepth: 2 - - roof_duality + + composites + lower_bounds cpp diff --git a/docs/reference/lower_bounds.rst b/docs/reference/lower_bounds.rst new file mode 100644 index 0000000..37d63ca --- /dev/null +++ b/docs/reference/lower_bounds.rst @@ -0,0 +1,26 @@ +.. _preprocessing_lower_bounds: + +============ +Lower Bounds +============ + +A common preprocessing method for binary quadratic models (BQM) is finding the +lower bound of their energy. + +Roof Duality +------------ + +`dwave-preprocessing` contains an implementation of roof duality, an +algorithm used for finding a lower bound for the minimum of a quadratic boolean +function, as well as minimizing assignments for some of the boolean variables; +these fixed variables take the same values in all, or some, optimal solutions +[#BHT]_ [#BH]_. + +.. [#BHT] Boros, E., P.L. Hammer, G. Tavares. Preprocessing of Unconstraint Quadratic Binary Optimization. Rutcor Research Report 10-2006, April, 2006. + +.. [#BH] Boros, E., P.L. Hammer. Pseudo-Boolean optimization. Discrete Applied Mathematics 123, (2002), pp. 155-225. + +.. autofunction:: dwave.preprocessing.lower_bounds.roof_duality + +The roof duality algorithm may also be accessed through the +:class:`~dwave.preprocessing.composites.FixVariablesComposite`. diff --git a/docs/reference/roof_duality.rst b/docs/reference/roof_duality.rst deleted file mode 100644 index 41d1752..0000000 --- a/docs/reference/roof_duality.rst +++ /dev/null @@ -1,48 +0,0 @@ -.. _preprocessing_roof_duality: - -============ -Roof Duality -============ - -.. currentmodule:: dwave.preprocessing.roof_duality - -Roof duality finds a lower bound for the minimum of a quadratic polynomial. It -can also find minimizing assignments for some of the polynomial's variables; -these fixed variables take the same values in all, or some, optimal solutions -[BHT]_ [BH]_. The problem size may then be reduced by fixing the variables of a -:term:`binary quadratic model` (BQM) before solving. - -The roof duality algorithm may be accessed through :func:`fix_variables` or the -:class:`RoofDualityComposite`. - -.. [BHT] Boros, E., P.L. Hammer, G. Tavares. Preprocessing of Unconstraint Quadratic Binary Optimization. Rutcor Research Report 10-2006, April, 2006. - -.. [BH] Boros, E., P.L. Hammer. Pseudo-Boolean optimization. Discrete Applied Mathematics 123, (2002), pp. 155-225. - -.. autofunction:: fix_variables - -Class ------ - -.. autoclass:: RoofDualityComposite - -Properties ----------- - -.. autosummary:: - :toctree: generated/ - - RoofDualityComposite.child - RoofDualityComposite.children - RoofDualityComposite.parameters - RoofDualityComposite.properties - -Methods -------- - -.. autosummary:: - :toctree: generated/ - - RoofDualityComposite.sample - RoofDualityComposite.sample_ising - RoofDualityComposite.sample_qubo diff --git a/dwave/preprocessing/composites/__init__.py b/dwave/preprocessing/composites/__init__.py new file mode 100644 index 0000000..30d3aa1 --- /dev/null +++ b/dwave/preprocessing/composites/__init__.py @@ -0,0 +1,19 @@ +# 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. + +from dwave.preprocessing.composites.clip import * +from dwave.preprocessing.composites.connected_components import * +from dwave.preprocessing.composites.fix_variables import * +from dwave.preprocessing.composites.scale import * +from dwave.preprocessing.composites.spin_reversal_transform import * diff --git a/dwave/preprocessing/composites/clip.py b/dwave/preprocessing/composites/clip.py new file mode 100644 index 0000000..d9c2a40 --- /dev/null +++ b/dwave/preprocessing/composites/clip.py @@ -0,0 +1,114 @@ +# 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. + +from dimod.core.composite import ComposedSampler +from dimod.sampleset import SampleSet + +__all__ = ['ClipComposite'] + +class ClipComposite(ComposedSampler): + """Composite to clip variables of a problem. + + Clips the variables of a binary quadratic model (BQM) and modifies linear + and quadratic terms accordingly. + + Args: + sampler (:class:`dimod.Sampler`): + A dimod sampler. + + Examples: + This example uses :class:`.ClipComposite` to instantiate a + composed sampler that submits a simple Ising problem to a sampler. + The composed sampler clips linear and quadratic biases as + indicated by options. + + >>> from dimod import ExactSolver + >>> from dwave.preprocessing.composites import ClipComposite + >>> h = {'a': -4.0, 'b': -4.0} + >>> J = {('a', 'b'): 3.2} + >>> sampler = ClipComposite(ExactSolver()) + >>> response = sampler.sample_ising(h, J, lower_bound=-2.0, upper_bound=2.0) + + """ + + def __init__(self, child_sampler): + self._children = [child_sampler] + + @property + def children(self): + return self._children + + @property + def parameters(self): + param = self.child.parameters.copy() + param.update({'lower_bound': [], 'upper_bound': []}) + return param + + @property + def properties(self): + return {'child_properties': self.child.properties.copy()} + + def sample(self, bqm, *, lower_bound=None, upper_bound=None, **parameters): + """Clip and sample from the provided binary quadratic model. + + If lower_bound and upper_bound are given variables with value above or below are clipped. + + Args: + bqm (:class:`dimod.BinaryQuadraticModel`): + Binary quadratic model to be sampled from. + + lower_bound (number): + Value by which to clip the variables from below. + + upper_bound (number): + Value by which to clip the variables from above. + + **parameters: + Parameters for the sampling method, specified by the child sampler. + + Returns: + :class:`dimod.SampleSet` + + """ + child = self.child + bqm_copy = _clip_bqm(bqm, lower_bound, upper_bound) + response = child.sample(bqm_copy, **parameters) + + return SampleSet.from_samples_bqm(response, bqm, info=response.info) + + +def _clip_bqm(bqm, lower_bound, upper_bound): + """Helper function for clipping a bqm.""" + + bqm_copy = bqm.copy() + if lower_bound is not None: + linear = bqm_copy.linear + for k, v in linear.items(): + if v < lower_bound: + linear[k] = lower_bound + quadratic = bqm_copy.quadratic + for k, v in quadratic.items(): + if v < lower_bound: + quadratic[k] = lower_bound + + if upper_bound is not None: + linear = bqm_copy.linear + for k, v in linear.items(): + if v > upper_bound: + linear[k] = upper_bound + quadratic = bqm_copy.quadratic + for k, v in quadratic.items(): + if v > upper_bound: + quadratic[k] = upper_bound + return bqm_copy diff --git a/dwave/preprocessing/composites/connected_components.py b/dwave/preprocessing/composites/connected_components.py new file mode 100644 index 0000000..6d89af7 --- /dev/null +++ b/dwave/preprocessing/composites/connected_components.py @@ -0,0 +1,116 @@ +# 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. + +from dimod.bqm import as_bqm, AdjVectorBQM, AdjDictBQM +from dimod.core.composite import ComposedSampler +from dimod.sampleset import SampleSet, append_variables +from dimod.traversal import connected_components + +__all__ = ['ConnectedComponentsComposite'] + +class ConnectedComponentsComposite(ComposedSampler): + """Composite to decompose a problem to the connected components + and solve each. + + Connected components of a binary quadratic model (BQM) graph are computed + (if not provided), and each subproblem is passed to the child sampler. + Returned samples from each child sampler are merged. Only the best solution + of each response is selected and merged with others (i.e. this composite + returns a single solution). + + Args: + sampler (:class:`dimod.Sampler`): + A dimod sampler + + Examples: + This example uses :class:`.ConnectedComponentsComposite` to solve a simple + Ising problem that can be separated into two components. This small example + uses :class:`dimod.ExactSolver` and is just illustrative. + + >>> from dimod import ExactSolver + >>> from dwave.preprocessing.composites import ConnectedComponentsComposite + >>> h = {} + >>> J1 = {(1, 2): -1.0, (2, 3): 2.0, (3, 4): 3.0} + >>> J2 = {(12, 13): 6} + >>> sampler = ExactSolver() + >>> sampler_ccc = ConnectedComponentsComposite(sampler) + >>> e1 = sampler.sample_ising(h, J1).first.energy + >>> e2 = sampler.sample_ising(h, J2).first.energy + >>> e_ccc = sampler_ccc.sample_ising(h, {**J1, **J2}).first.energy + >>> e_ccc == e1 + e2 + True + + """ + + def __init__(self, child_sampler): + self._children = [child_sampler] + + @property + def children(self): + return self._children + + @property + def parameters(self): + params = self.child.parameters.copy() + return params + + @property + def properties(self): + return {'child_properties': self.child.properties.copy()} + + def sample(self, bqm, *, components=None, **parameters): + """Sample from the provided binary quadratic model. + + Args: + bqm (:class:`dimod.BinaryQuadraticModel`): + Binary quadratic model to be sampled from. + + components (list(set)): + A list of disjoint set of variables that fully partition the variables + + **parameters: + Parameters for the sampling method, specified by the child sampler. + + Returns: + :class:`dimod.SampleSet` + + """ + # make sure the BQM is shapeable + bqm = as_bqm(bqm, cls=[AdjVectorBQM, AdjDictBQM]) + + # solve the problem on the child system + child = self.child + variables = bqm.variables + if components is None: + components = list(connected_components(bqm)) + if isinstance(components, set): + components = [components] + sampleset = None + fixed_value = min(bqm.vartype.value) + for component in components: + bqm_copy = bqm.copy() + bqm_copy.fix_variables({i: fixed_value for i in (variables - component)}) + if sampleset is None: + # here .truncate(1) is used to pick the best solution only. The other options + # for future development is to combine all sample with all. + # This way you'd get the same behaviour as the ExactSolver + sampleset = child.sample(bqm_copy, **parameters).truncate(1) + else: + sampleset = append_variables(sampleset.truncate(1), child.sample(bqm_copy, **parameters).truncate(1)) + + if sampleset is None: + return SampleSet.from_samples_bqm({}, bqm) + else: + return SampleSet.from_samples_bqm(sampleset, bqm) + diff --git a/dwave/preprocessing/composites/fix_variables.py b/dwave/preprocessing/composites/fix_variables.py new file mode 100644 index 0000000..3d3ca7e --- /dev/null +++ b/dwave/preprocessing/composites/fix_variables.py @@ -0,0 +1,153 @@ +# 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 numpy as np +import warnings + +from dimod.bqm import as_bqm, AdjVectorBQM, AdjDictBQM +from dimod.core.composite import ComposedSampler +from dimod.sampleset import SampleSet, append_variables + +from dwave.preprocessing.lower_bounds import roof_duality + +__all__ = ['FixVariablesComposite'] + +class FixVariablesComposite(ComposedSampler): + """Composite to fix variables of a problem to provided. + + Fixes variables of a binary quadratic model (BQM) and modifies linear and + quadratic terms accordingly. Returned samples include the fixed variable. + + Args: + child_sampler (:class:`dimod.Sampler`): + A dimod sampler + + algorithm (str, optional, default='explicit'): + Determines how ``fixed_variables`` are found. + + 'explicit': ``fixed_variables`` should be passed in a call to + `.sample()`. If not, no fixing occurs and the problem is directly + passed to the child sampler. + + 'roof_duality': Roof duality algorithm is used to find ``fixed_variables``. + ``strict`` may be passed in a call to `.sample()` to determine what + variables the algorithm will fix. For details, see + :func:`~dwave.preprocessing.lower_bounds.roof_duality`. + + Examples: + This example uses the :class:`.FixVariablesComposite` to instantiate a + composed sampler that submits a simple Ising problem to a sampler. + The composed sampler fixes a variable and modifies linear and quadratic + biases accordingly. + + >>> from dimod import ExactSolver + >>> from dwave.preprocessing.composites import FixVariablesComposite + >>> h = {1: -1.3, 4: -0.5} + >>> J = {(1, 4): -0.6} + >>> sampler = FixVariablesComposite(ExactSolver()) + >>> sampleset = sampler.sample_ising(h, J, fixed_variables={1: -1}) + + This next example involves the same problem but calculates ``fixed_variables`` + using the 'roof_duality' ``algorithm``. + + >>> sampler = FixVariablesComposite(ExactSolver(), algorithm='roof_duality') + >>> sampleset = sampler.sample_ising(h, J, strict=False) + + """ + + def __init__(self, child_sampler, *, algorithm='explicit'): + self._children = [child_sampler] + self.algorithm = algorithm + + self._parameters = self.child.parameters.copy() + + if self.algorithm == 'explicit': + self._parameters['fixed_variables'] = [] + elif self.algorithm == 'roof_duality': + self._parameters['strict'] = [] + else: + raise ValueError("Unknown algorithm: {}".format(algorithm)) + + @property + def children(self): + return self._children + + @property + def parameters(self): + return self._parameters + + @property + def properties(self): + return {'child_properties': self.child.properties.copy()} + + def sample(self, bqm, **parameters): + """Sample from the provided binary quadratic model. + + Args: + bqm (:class:`dimod.BinaryQuadraticModel`): + Binary quadratic model to be sampled from. + + fixed_variables (dict, optional, default=None): + A dictionary of variable assignments used when ``self.algorithm`` + is 'explicit'. + + strict (bool, optional, default=True): + Only used if ``self.algorithm`` is 'roof_duality'. If True, only + fixes variables for which assignments are true for all minimizing + points (strong persistency). If False, also fixes variables for + which the assignments are true for some but not all minimizing + points (weak persistency). + + **parameters: + Parameters for the sampling method, specified by the child sampler. + + Returns: + :class:`dimod.SampleSet` + + """ + + if self.algorithm == 'explicit': + fixed_variables = parameters.pop('fixed_variables', None) + if fixed_variables is None: + msg = ("No fixed_variables passed in when algorithm is 'explicit'. " + "Passing problem to child sampler without fixing.") + warnings.warn(msg) + return self.child.sample(bqm, **parameters) + elif self.algorithm == 'roof_duality': + fixed_variables = roof_duality(bqm, strict=parameters.pop('strict', True)) + + # make sure that we're shapeable and that we have a BQM we can mutate + bqm_copy = as_bqm(bqm, cls=[AdjVectorBQM, AdjDictBQM], copy=True) + + bqm_copy.fix_variables(fixed_variables) + + sampleset = self.child.sample(bqm_copy, **parameters) + + def _hook(sampleset): + # make RoofDualityComposite non-blocking + + if sampleset.variables: + if len(sampleset): + return append_variables(sampleset, fixed_variables) + else: + return sampleset.from_samples_bqm((np.empty((0, len(bqm))), + bqm.variables), bqm=bqm) + + # there are only fixed variables, make sure that the correct number + # of samples are returned + samples = [fixed_variables]*max(len(sampleset), 1) + + return sampleset.from_samples_bqm(samples, bqm=bqm) + + return SampleSet.from_future(sampleset, _hook) diff --git a/dwave/preprocessing/composites/scale.py b/dwave/preprocessing/composites/scale.py new file mode 100644 index 0000000..eadca95 --- /dev/null +++ b/dwave/preprocessing/composites/scale.py @@ -0,0 +1,144 @@ +# 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. + +from dimod.core.composite import ComposedSampler +from dimod.decorators import nonblocking_sample_method +from dimod.sampleset import SampleSet + +__all__ = ['ScaleComposite'] + +class ScaleComposite(ComposedSampler): + """Composite that scales variables of a problem. + + Scales the variables of a binary quadratic model (BQM) and modifies linear + and quadratic terms accordingly. + + Args: + sampler (:class:`dimod.Sampler`): + A dimod sampler. + + Examples: + This example uses :class:`.ScaleComposite` to instantiate a + composed sampler that submits a simple Ising problem to a sampler. + The composed sampler scales linear biases, quadratic biases, and + offset as indicated by options. + + >>> from dimod import ExactSolver + >>> from dwave.preprocessing.composites import ScaleComposite + >>> h = {'a': -4.0, 'b': -4.0} + >>> J = {('a', 'b'): 3.2} + >>> sampler = ScaleComposite(ExactSolver()) + >>> response = sampler.sample_ising(h, J, scalar=0.5, + ... ignored_interactions=[('a','b')]) + + """ + + def __init__(self, child_sampler): + self._children = [child_sampler] + + @property + def children(self): + return self._children + + @property + def parameters(self): + param = self.child.parameters.copy() + param.update({'scalar': [], + 'bias_range': [], + 'quadratic_range': [], + 'ignored_variables': [], + 'ignored_interactions': [], + 'ignore_offset': []}) + return param + + @property + def properties(self): + return {'child_properties': self.child.properties.copy()} + + @nonblocking_sample_method + def sample(self, bqm, *, scalar=None, bias_range=1, quadratic_range=None, + ignored_variables=None, ignored_interactions=None, + ignore_offset=False, **parameters): + """Scale and sample from the provided binary quadratic model. + + If ``scalar`` is not given, the problem is scaled based on bias and + quadratic ranges. See :meth:`.BinaryQuadraticModel.scale` and + :meth:`.BinaryQuadraticModel.normalize` + + Args: + bqm (:class:`dimod.BinaryQuadraticModel`): + Binary quadratic model to be sampled from. + + scalar (number): + Value by which to scale the energy range of the binary + quadratic model. Overrides `bias_range` and `quadratic_range`. + + bias_range (number/pair, default=1): + Value/range by which to normalize the all the biases, or if + `quadratic_range` is provided, just the linear biases. + Overridden by `scalar`. + + quadratic_range (number/pair): + Value/range by which to normalize the quadratic biases. + Overridden by `scalar`. + + ignored_variables (iterable, optional): + Biases associated with these variables are not scaled. + + ignored_interactions (iterable[tuple], optional): + As an iterable of 2-tuples. Biases associated with these + interactions are not scaled. + + ignore_offset (bool, default=False): + If True, the offset is not scaled. + + **parameters: + Parameters for the sampling method, specified by the child + sampler. + + Returns: + :class:`dimod.SampleSet` + + """ + original_bqm = bqm + bqm = bqm.copy() # we're going to be scaling + + if scalar is not None: + bqm.scale(scalar, + ignored_variables=ignored_variables, + ignored_interactions=ignored_interactions, + ignore_offset=ignore_offset) + else: + scalar = bqm.normalize(bias_range, quadratic_range, + ignored_variables=ignored_variables, + ignored_interactions=ignored_interactions, + ignore_offset=ignore_offset) + + if scalar == 0: + raise ValueError('scalar must be non-zero') + + sampleset = self.child.sample(bqm, **parameters) + + yield sampleset # so that SampleSet.done() works + + if not (ignored_variables or ignored_interactions or ignore_offset): + # we just need to scale back and don't need to worry about + # the stuff we ignored + sampleset.record.energy *= 1 / scalar + else: + sampleset.record.energy = original_bqm.energies(sampleset) + + sampleset.info.update(scalar=scalar) + + yield sampleset diff --git a/dwave/preprocessing/composites/spin_reversal_transform.py b/dwave/preprocessing/composites/spin_reversal_transform.py new file mode 100644 index 0000000..5ac96aa --- /dev/null +++ b/dwave/preprocessing/composites/spin_reversal_transform.py @@ -0,0 +1,120 @@ +# 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. + +from random import random + +from dimod.core.composite import Composite +from dimod.core.sampler import Sampler +from dimod.sampleset import SampleSet, concatenate +from dimod.vartypes import Vartype + +__all__ = ['SpinReversalTransformComposite'] + +class SpinReversalTransformComposite(Sampler, Composite): + """Composite for applying spin reversal transform preprocessing. + + Spin reversal transforms (or "gauge transformations") are applied + by flipping the spin of variables in the Ising problem. After + sampling the transformed Ising problem, the same bits are flipped in the + resulting sample [#km]_. + + Args: + sampler: A `dimod` sampler object. + + Examples: + This example composes a dimod ExactSolver sampler with spin transforms then + uses it to sample an Ising problem. + + >>> from dimod import ExactSolver + >>> from dwave.preprocessing.composites import SpinReversalTransformComposite + >>> base_sampler = ExactSolver() + >>> composed_sampler = SpinReversalTransformComposite(base_sampler) + ... # Sample an Ising problem + >>> response = composed_sampler.sample_ising({'a': -0.5, 'b': 1.0}, {('a', 'b'): -1}) + >>> response.first.sample + {'a': -1, 'b': -1} + + References + ---------- + .. [#km] Andrew D. King and Catherine C. McGeoch. Algorithm engineering + for a quantum annealing platform. https://arxiv.org/abs/1410.2628, + 2014. + + """ + children = None + parameters = None + properties = None + + def __init__(self, child): + self.children = [child] + + self.parameters = parameters = {'spin_reversal_variables': []} + parameters.update(child.parameters) + + self.properties = {'child_properties': child.properties} + + def sample(self, bqm, *, num_spin_reversal_transforms=2, **kwargs): + """Sample from the binary quadratic model. + + Args: + bqm (:class:`~dimod.BinaryQuadraticModel`): + Binary quadratic model to be sampled from. + + num_spin_reversal_transforms (integer, optional, default=2): + Number of spin reversal transform runs. + + Returns: + :class:`.SampleSet` + + Examples: + This example runs 100 spin reversals applied to one variable of a QUBO problem. + + >>> from dimod import ExactSolver + >>> from dwave.preprocessing.composites import SpinReversalTransformComposite + >>> base_sampler = ExactSolver() + >>> composed_sampler = SpinReversalTransformComposite(base_sampler) + ... + >>> Q = {('a', 'a'): -1, ('b', 'b'): -1, ('a', 'b'): 2} + >>> response = composed_sampler.sample_qubo(Q, + ... num_spin_reversal_transforms=100) + >>> len(response) + 400 + """ + + # make a main response + responses = [] + + flipped_bqm = bqm.copy() + transform = {v: False for v in bqm.variables} + + for ii in range(num_spin_reversal_transforms): + # flip each variable with a 50% chance + for v in bqm: + if random() > .5: + transform[v] = not transform[v] + flipped_bqm.flip_variable(v) + + flipped_response = self.child.sample(flipped_bqm, **kwargs) + + tf_idxs = [flipped_response.variables.index(v) + for v, flip in transform.items() if flip] + + if bqm.vartype is Vartype.SPIN: + flipped_response.record.sample[:, tf_idxs] = -1 * flipped_response.record.sample[:, tf_idxs] + else: + flipped_response.record.sample[:, tf_idxs] = 1 - flipped_response.record.sample[:, tf_idxs] + + responses.append(flipped_response) + + return concatenate(responses) diff --git a/dwave/preprocessing/roof_duality.py b/dwave/preprocessing/lower_bounds.py similarity index 53% rename from dwave/preprocessing/roof_duality.py rename to dwave/preprocessing/lower_bounds.py index c2a5f44..1344961 100644 --- a/dwave/preprocessing/roof_duality.py +++ b/dwave/preprocessing/lower_bounds.py @@ -13,13 +13,12 @@ # limitations under the License. from dimod.vartypes import Vartype -from dimod.reference.composites.fixedvariable import FixedVariableComposite from dwave.preprocessing.cyfix_variables import fix_variables_wrapper -def fix_variables(bqm, *, strict=True): +def roof_duality(bqm, *, strict=True): """Determine minimizing assignments for some variables of a binary quadratic - model using roof duality. + model using the roof duality algorithm. Args: bqm (:class:`.BinaryQuadraticModel`): @@ -39,9 +38,9 @@ def fix_variables(bqm, *, strict=True): and fixes the model's single variable to the minimizing assignment. >>> import dimod - >>> from dwave.preprocessing import roof_duality + >>> from dwave.preprocessing.lower_bounds import roof_duality >>> bqm = dimod.BinaryQuadraticModel.from_ising({'a': 1.0}, {}) - >>> roof_duality.fix_variables(bqm) + >>> roof_duality(bqm) {'a': -1} This example has two ground states, :math:`a=b=-1` and :math:`a=b=1`, with @@ -50,7 +49,7 @@ def fix_variables(bqm, *, strict=True): >>> bqm = dimod.BinaryQuadraticModel.empty(dimod.SPIN) >>> bqm.add_interaction('a', 'b', -1.0) - >>> roof_duality.fix_variables(bqm) # doctest: +SKIP + >>> roof_duality(bqm) # doctest: +SKIP {} This example sets ``strict`` to False, so variables are fixed to an assignment @@ -58,7 +57,7 @@ def fix_variables(bqm, *, strict=True): >>> bqm = dimod.BinaryQuadraticModel.empty(dimod.SPIN) >>> bqm.add_interaction('a', 'b', -1.0) - >>> roof_duality.fix_variables(bqm, strict=False) # doctest: +SKIP + >>> roof_duality(bqm, strict=False) # doctest: +SKIP {'a': 1, 'b': 1} """ @@ -87,62 +86,3 @@ def fix_variables(bqm, *, strict=True): return {v: 2*val - 1 for v, val in fixed.items()} else: return fixed - - -class RoofDualityComposite(FixedVariableComposite): - """A composite that uses the :func:`fix_variables` function to determine - variable assignments, then fixes them before calling its child sampler. - - Returned samples include the fixed variables. - - Args: - child_sampler (:class:`dimod.Sampler`): - A dimod sampler. Used to sample the binary quadratic model after - variables have been fixed. - - Examples: - This example uses the RoofDualityComposite to fix the variables of a - small Ising problem before solving with dimod's ExactSolver. - - >>> from dimod import ExactSolver - >>> from dwave.preprocessing.roof_duality import RoofDualityComposite - >>> sampler = RoofDualityComposite(ExactSolver()) - >>> sampleset = sampler.sample_ising({'a': 10}, {'ab': -1, 'bc': 1}) - >>> print(sampleset) - a b c energy num_oc. - 0 -1 -1 +1 -12.0 1 - ['SPIN', 1 rows, 1 samples, 3 variables] - - """ - @property - def parameters(self): - params = self.child.parameters.copy() - params['strict'] = [] - return params - - def sample(self, bqm, *, strict=True, **parameters): - """Sample from the provided binary quadratic model. - - Uses the :func:`roof_duality.fix_variables` function to determine - which variables to fix. - - Args: - bqm (:class:`dimod.BinaryQuadraticModel`): - Binary quadratic model to be sampled from. - - strict (bool, optional, default=True): - If True, only fixes variables for which assignments are true for - all minimizing points (strong persistency). If False, also fixes - variables for which the assignments are true for some but not all - minimizing points (weak persistency). - - **parameters: - Parameters for the child sampler. - - Returns: - :class:`dimod.SampleSet` - - """ - # use roof-duality to decide which variables to fix - parameters['fixed_variables'] = fix_variables(bqm, strict=strict) - return super(RoofDualityComposite, self).sample(bqm, **parameters) diff --git a/tests/test_clip.py b/tests/test_clip.py new file mode 100644 index 0000000..d23addc --- /dev/null +++ b/tests/test_clip.py @@ -0,0 +1,104 @@ +# 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 unittest + +import dimod +import dimod.testing as dtest +from dimod import BinaryQuadraticModel, ExactSolver, NullSampler + +from dwave.preprocessing.composites import ClipComposite + +@dtest.load_sampler_bqm_tests(ClipComposite(ExactSolver())) +@dtest.load_sampler_bqm_tests(ClipComposite(NullSampler())) +class TestClipCompositeClass(unittest.TestCase): + def test_instantiation_smoketest(self): + sampler = ClipComposite(NullSampler()) + dtest.assert_sampler_api(sampler) + + def test_no_bounds(self): + bqm = BinaryQuadraticModel({0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}, + {(0, 1): -2.0, (0, 2): -5.0, (0, 3): -2.0, + (0, 4): -2.0, (1, 2): -2.0, (1, 3): -2.0, (1, 4): 4.0, + (2, 3): -3.0, (2, 4): -5.0, (3, 4): -4.0}, 0, dimod.SPIN) + sampler = ClipComposite(ExactSolver()) + solver = ExactSolver() + response = sampler.sample(bqm) + response_exact = solver.sample(bqm) + self.assertEqual(response.first.sample, response_exact.first.sample) + self.assertAlmostEqual(response.first.energy, response_exact.first.energy) + + def test_lb_only(self): + bqm = BinaryQuadraticModel({0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}, + {(0, 1): -2.0, (0, 2): -5.0, (0, 3): -2.0, + (0, 4): -2.0, (1, 2): -2.0, (1, 3): -2.0, (1, 4): 4.0, + (2, 3): -3.0, (2, 4): -5.0, (3, 4): -4.0}, 0, dimod.SPIN) + sampler = ClipComposite(ExactSolver()) + solver = ExactSolver() + response = sampler.sample(bqm, lower_bound=-1) + response_exact = solver.sample(bqm) + self.assertEqual(response.first.sample, response_exact.first.sample) + self.assertAlmostEqual(response.first.energy, response_exact.first.energy) + + def test_ub_only(self): + bqm = BinaryQuadraticModel({0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}, + {(0, 1): -2.0, (0, 2): -5.0, (0, 3): -2.0, + (0, 4): -2.0, (1, 2): -2.0, (1, 3): -2.0, (1, 4): 4.0, + (2, 3): -3.0, (2, 4): -5.0, (3, 4): -4.0}, 0, dimod.SPIN) + sampler = ClipComposite(ExactSolver()) + solver = ExactSolver() + response = sampler.sample(bqm, upper_bound=1) + response_exact = solver.sample(bqm) + self.assertEqual(response.first.sample, response_exact.first.sample) + self.assertAlmostEqual(response.first.energy, response_exact.first.energy) + + def test_lb_and_ub(self): + bqm = BinaryQuadraticModel({0: 0.0, 1: 0.0, 2: 0.0, 3: 0.0, 4: 0.0}, + {(0, 1): -2.0, (0, 2): -5.0, (0, 3): -2.0, + (0, 4): -2.0, (1, 2): -2.0, (1, 3): -2.0, (1, 4): 4.0, + (2, 3): -3.0, (2, 4): -5.0, (3, 4): -4.0}, 0, dimod.SPIN) + sampler = ClipComposite(ExactSolver()) + solver = ExactSolver() + response = sampler.sample(bqm, lower_bound=-1, upper_bound=1) + response_exact = solver.sample(bqm) + self.assertEqual(response.first.sample, response_exact.first.sample) + self.assertAlmostEqual(response.first.energy, response_exact.first.energy) + + def test_with_labels(self): + bqm = BinaryQuadraticModel({'a': 0.0, 'b': 0.0, 'c': 0.0, 'd': 0.0, 'e': 0.0}, + {('a', 'b'): -2.0, ('a', 'c'): -5.0, ('a', 'd'): -2.0, + ('a', 'e'): -2.0, ('b', 'c'): -2.0, ('b', 'd'): -2.0, ('b', 'e'): 4.0, + ('c', 'd'): -3.0, ('c', 'e'): -5.0, ('d', 'e'): -4.0}, 0, dimod.SPIN) + sampler = ClipComposite(ExactSolver()) + solver = ExactSolver() + response = sampler.sample(bqm, lower_bound=-1, upper_bound=1) + response_exact = solver.sample(bqm) + self.assertEqual(response.first.sample, response_exact.first.sample) + self.assertAlmostEqual(response.first.energy, response_exact.first.energy) + + def test_empty_bqm(self): + bqm = BinaryQuadraticModel({}, {}, 0.0, dimod.SPIN) + sampler = ClipComposite(ExactSolver()) + sampler.sample(bqm, lower_bound=-1, upper_bound=1) + + def test_info_propagation(self): + bqm = BinaryQuadraticModel.from_ising({}, {}) + + class MySampler: + @staticmethod + def sample(bqm): + return dimod.SampleSet.from_samples_bqm([], bqm, info=dict(a=1)) + + sampleset = ClipComposite(MySampler).sample(bqm) + self.assertEqual(sampleset.info, {'a': 1}) diff --git a/tests/test_connected_components.py b/tests/test_connected_components.py new file mode 100644 index 0000000..fbd2dee --- /dev/null +++ b/tests/test_connected_components.py @@ -0,0 +1,83 @@ +# 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 unittest + +import dimod.testing as dtest +from dimod.vartypes import Vartype +from dimod import BinaryQuadraticModel +from dimod import ExactSolver, NullSampler +from dimod import SampleSet + +from dwave.preprocessing.composites import (ConnectedComponentsComposite, + FixVariablesComposite) + + +@dtest.load_sampler_bqm_tests(ConnectedComponentsComposite(ExactSolver())) +@dtest.load_sampler_bqm_tests(ConnectedComponentsComposite(NullSampler())) +class TestConnectedComponentsComposite(unittest.TestCase): + def test_instantiation_smoke(self): + sampler = ConnectedComponentsComposite(ExactSolver()) + + dtest.assert_sampler_api(sampler) + + def test_sample(self): + bqm = BinaryQuadraticModel({1: -1.3, 4: -0.5}, + {(1, 4): -0.6}, + 0, + vartype=Vartype.SPIN) + sampler = ConnectedComponentsComposite(ExactSolver()) + response = sampler.sample(bqm) + + self.assertEqual(response.first.sample, {4: 1, 1: 1}) + self.assertAlmostEqual(response.first.energy, -2.4) + + def test_empty_bqm(self): + bqm = BinaryQuadraticModel({1: -1.3, 4: -0.5}, + {(1, 4): -0.6}, + 0, + vartype=Vartype.SPIN) + + fixed_variables = {1: -1, 4: -1} + sampler = FixVariablesComposite(ConnectedComponentsComposite(ExactSolver())) + response = sampler.sample(bqm, fixed_variables=fixed_variables) + self.assertIsInstance(response, SampleSet) + + def test_sample_two_components(self): + bqm = BinaryQuadraticModel({0: 0.0, 1: 4.0, 2: -4.0, 3: 0.0}, {(0, 1): -4.0, (2, 3): 4.0}, 0.0, Vartype.BINARY) + + sampler = ConnectedComponentsComposite(ExactSolver()) + response = sampler.sample(bqm) + self.assertIsInstance(response, SampleSet) + self.assertEqual(response.first.sample, {0: 0, 1: 0, 2: 1, 3: 0}) + self.assertAlmostEqual(response.first.energy, bqm.energy({0: 0, 1: 0, 2: 1, 3: 0})) + + def test_sample_three_components(self): + bqm = BinaryQuadraticModel({0: 0.0, 1: 4.0, 2: -4.0, 3: 0.0, 4: 1.0, 5: -1.0}, + {(0, 1): -4.0, (2, 3): 4.0, (4, 5): -2.0}, 0.0, Vartype.BINARY) + + sampler = ConnectedComponentsComposite(ExactSolver()) + response = sampler.sample(bqm) + self.assertIsInstance(response, SampleSet) + self.assertEqual(response.first.sample, {0: 0, 1: 0, 2: 1, 3: 0, 4: 1, 5: 1}) + self.assertAlmostEqual(response.first.energy, bqm.energy({0: 0, 1: 0, 2: 1, 3: 0, 4: 1, 5: 1})) + + def test_sample_passcomponents(self): + bqm = BinaryQuadraticModel({0: 0.0, 1: 4.0, 2: -4.0, 3: 0.0}, {(0, 1): -4.0, (2, 3): 4.0}, 0.0, Vartype.BINARY) + + sampler = ConnectedComponentsComposite(ExactSolver()) + response = sampler.sample(bqm, components=[{0, 1}, {2, 3}]) + self.assertIsInstance(response, SampleSet) + self.assertEqual(response.first.sample, {0: 0, 1: 0, 2: 1, 3: 0}) + self.assertAlmostEqual(response.first.energy, bqm.energy({0: 0, 1: 0, 2: 1, 3: 0})) diff --git a/tests/test_fix_variables.py b/tests/test_fix_variables.py new file mode 100644 index 0000000..3a27a91 --- /dev/null +++ b/tests/test_fix_variables.py @@ -0,0 +1,100 @@ +# 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 unittest + +import dimod.testing as dtest +from dimod.vartypes import Vartype +from dimod import BinaryQuadraticModel +from dimod import ExactSolver, NullSampler +from dimod import SampleSet + +from dwave.preprocessing.composites import FixVariablesComposite + + +@dtest.load_sampler_bqm_tests(FixVariablesComposite(ExactSolver())) +@dtest.load_sampler_bqm_tests(FixVariablesComposite(NullSampler())) +class TestFixVariablesComposite(unittest.TestCase): + def test_instantiation_smoke(self): + sampler = FixVariablesComposite(ExactSolver()) + dtest.assert_sampler_api(sampler) + + def test_invalid_algorithm(self): + with self.assertRaises(ValueError): + sampler = FixVariablesComposite(ExactSolver(), algorithm="abc") + + def test_sample(self): + bqm = BinaryQuadraticModel({1: -1.3, 4: -0.5}, + {(1, 4): -0.6}, + 0, + vartype=Vartype.SPIN) + + fixed_variables = {1: -1} + sampler = FixVariablesComposite(ExactSolver()) + response = sampler.sample(bqm, fixed_variables=fixed_variables) + + self.assertEqual(response.first.sample, {4: -1, 1: -1}) + self.assertAlmostEqual(response.first.energy, 1.2) + + def test_empty_bqm(self): + bqm = BinaryQuadraticModel({1: -1.3, 4: -0.5}, + {(1, 4): -0.6}, + 0, + vartype=Vartype.SPIN) + + fixed_variables = {1: -1, 4: -1} + sampler = FixVariablesComposite(ExactSolver()) + response = sampler.sample(bqm, fixed_variables=fixed_variables) + self.assertIsInstance(response, SampleSet) + + def test_empty_fix(self): + linear = {1: -1.3, 4: -0.5} + quadratic = {(1, 4): -0.6} + + sampler = FixVariablesComposite(ExactSolver()) + response = sampler.sample_ising(linear, quadratic) + self.assertIsInstance(response, SampleSet) + + self.assertEqual(response.first.sample, {4: 1, 1: 1}) + self.assertAlmostEqual(response.first.energy, -2.4) + + def test_roof_duality_3path(self): + sampler = FixVariablesComposite(ExactSolver(), algorithm='roof_duality') + sampleset = sampler.sample_ising({'a': 10}, {'ab': -1, 'bc': 1}) + + # all should be fixed, so should just see one + self.assertEqual(len(sampleset), 1) + self.assertEqual(set(sampleset.variables), set('abc')) + + def test_roof_duality_triangle(self): + bqm = BinaryQuadraticModel.from_ising({}, {'ab': -1, 'bc': -1, 'ac': -1}) + + # two equally good solutions + sampler = FixVariablesComposite(ExactSolver(), algorithm='roof_duality') + sampleset = sampler.sample(bqm) + + self.assertEqual(set(sampleset.variables), set('abc')) + dtest.assert_response_energies(sampleset, bqm) + + def test_roof_duality_triangle_not_strict(self): + bqm = BinaryQuadraticModel.from_ising({}, {'ab': -1, 'bc': -1, 'ac': -1}) + + # two equally good solutions, but with strict=False, it will pick one + sampler = FixVariablesComposite(ExactSolver(), algorithm='roof_duality') + + sampleset = sampler.sample(bqm, strict=False) + + self.assertEqual(set(sampleset.variables), set('abc')) + self.assertEqual(len(sampleset), 1) # all should be fixed + dtest.assert_response_energies(sampleset, bqm) diff --git a/tests/test_roof_duality.py b/tests/test_roof_duality.py index 4251364..bce6e81 100644 --- a/tests/test_roof_duality.py +++ b/tests/test_roof_duality.py @@ -16,68 +16,30 @@ import dimod -from dwave.preprocessing.roof_duality import fix_variables, RoofDualityComposite +from dwave.preprocessing.lower_bounds import roof_duality -class TestFixVariables(unittest.TestCase): +class TestRoofDuality(unittest.TestCase): def test_empty(self): bqm = dimod.AdjVectorBQM('BINARY') - fixed = fix_variables(bqm, strict=True) + fixed = roof_duality(bqm, strict=True) self.assertEqual(fixed, {}) - fixed = fix_variables(bqm, strict=False) + fixed = roof_duality(bqm, strict=False) self.assertEqual(fixed, {}) def test_all_zero(self): num_vars = 3 bqm = dimod.AdjVectorBQM(num_vars, 'BINARY') - fixed = fix_variables(bqm, strict=True) + fixed = roof_duality(bqm, strict=True) self.assertEqual(fixed, {}) - fixed = fix_variables(bqm, strict=False) + fixed = roof_duality(bqm, strict=False) self.assertEqual(len(fixed), num_vars) for val in fixed.values(): self.assertEqual(val, 1) def test_3path(self): bqm = dimod.BinaryQuadraticModel.from_ising({'a': 10}, {'ab': -1, 'bc': 1}) - fixed = fix_variables(bqm) + fixed = roof_duality(bqm) self.assertEqual(fixed, {'a': -1, 'b': -1, 'c': 1}) - -@dimod.testing.load_sampler_bqm_tests(RoofDualityComposite(dimod.ExactSolver())) -@dimod.testing.load_sampler_bqm_tests(RoofDualityComposite(dimod.NullSampler())) -class TestRoofDualityComposite(unittest.TestCase): - def test_construction(self): - sampler = RoofDualityComposite(dimod.ExactSolver()) - dimod.testing.assert_sampler_api(sampler) - - def test_3path(self): - sampler = RoofDualityComposite(dimod.ExactSolver()) - sampleset = sampler.sample_ising({'a': 10}, {'ab': -1, 'bc': 1}) - - # all should be fixed, so should just see one - self.assertEqual(len(sampleset), 1) - self.assertEqual(set(sampleset.variables), set('abc')) - - def test_triangle(self): - sampler = RoofDualityComposite(dimod.ExactSolver()) - - bqm = dimod.BinaryQuadraticModel.from_ising({}, {'ab': -1, 'bc': -1, 'ac': -1}) - - # two equally good solutions - sampleset = sampler.sample(bqm) - - self.assertEqual(set(sampleset.variables), set('abc')) - dimod.testing.assert_response_energies(sampleset, bqm) - - def test_triangle_not_strict(self): - sampler = RoofDualityComposite(dimod.ExactSolver()) - - bqm = dimod.BinaryQuadraticModel.from_ising({}, {'ab': -1, 'bc': -1, 'ac': -1}) - - # two equally good solutions, but with strict=False, it will pick one - sampleset = sampler.sample(bqm, strict=False) - - self.assertEqual(set(sampleset.variables), set('abc')) - self.assertEqual(len(sampleset), 1) # all should be fixed - dimod.testing.assert_response_energies(sampleset, bqm) diff --git a/tests/test_scale.py b/tests/test_scale.py new file mode 100644 index 0000000..11f6fef --- /dev/null +++ b/tests/test_scale.py @@ -0,0 +1,119 @@ +# 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 unittest + +import dimod +import dimod.testing as dtest + +from dwave.preprocessing.composites import ScaleComposite + +@dtest.load_sampler_bqm_tests(ScaleComposite(dimod.ExactSolver())) +@dtest.load_sampler_bqm_tests(ScaleComposite(dimod.NullSampler())) +class TestScaleComposite(unittest.TestCase): + def test_api(self): + sampler = ScaleComposite(dimod.ExactSolver()) + dtest.assert_sampler_api(sampler) + + def test_bias_range(self): + bqm = dimod.BQM.from_ising({'a': -4.0, 'b': -4.0}, + {('a', 'b'): 3.2}, 1.5) + + sampler = ScaleComposite(dimod.TrackingComposite(dimod.ExactSolver())) + + sampleset = sampler.sample(bqm, bias_range=[-2, 2]) + + # check that everything was restored properly + dtest.assert_sampleset_energies(sampleset, bqm) + + self.assertEqual(sampler.child.input['bqm'], + dimod.BQM.from_ising({'a': -2.0, 'b': -2.0}, + {('a', 'b'): 1.6}, .75)) + + def test_bias_ranges(self): + bqm = dimod.BQM.from_ising({'a': -4.0, 'b': -4.0}, + {('a', 'b'): 4}, 1.5) + + sampler = ScaleComposite(dimod.TrackingComposite(dimod.ExactSolver())) + + sampleset = sampler.sample(bqm, bias_range=[-3, 3], + quadratic_range=[-2, 2]) + + # check that everything was restored properly + dtest.assert_sampleset_energies(sampleset, bqm) + + self.assertEqual(sampler.child.input['bqm'], + dimod.BQM.from_ising({'a': -2.0, 'b': -2.0}, + {('a', 'b'): 2}, .75)) + + def test_ignored_interactions(self): + bqm = dimod.BQM.from_ising({'a': -4.0, 'b': -4.0}, + {('a', 'b'): 3.2, ('b', 'c'): 1}, 1.5) + + sampler = ScaleComposite(dimod.TrackingComposite(dimod.ExactSolver())) + + sampleset = sampler.sample(bqm, scalar=.5, + ignored_interactions=[('b', 'c')]) + + # check that everything was restored properly + dtest.assert_sampleset_energies(sampleset, bqm) + + self.assertEqual(sampler.child.input['bqm'], + dimod.BQM.from_ising({'a': -2.0, 'b': -2.0}, + {'ab': 1.6, 'bc': 1}, .75)) + + def test_ignored_offset(self): + bqm = dimod.BQM.from_ising({'a': -4.0, 'b': -4.0}, + {('a', 'b'): 3.2}, 1.5) + + sampler = ScaleComposite(dimod.TrackingComposite(dimod.ExactSolver())) + + sampleset = sampler.sample(bqm, scalar=.5, ignore_offset=True) + + # check that everything was restored properly + dtest.assert_sampleset_energies(sampleset, bqm) + + self.assertEqual(sampler.child.input['bqm'], + dimod.BQM.from_ising({'a': -2.0, 'b': -2.0}, + {('a', 'b'): 1.6}, 1.5)) + + def test_ignored_variables(self): + bqm = dimod.BQM.from_ising({'a': -4.0, 'b': -4.0}, + {('a', 'b'): 3.2}, 1.5) + + sampler = ScaleComposite(dimod.TrackingComposite(dimod.ExactSolver())) + + sampleset = sampler.sample(bqm, scalar=.5, ignored_variables='a') + + # check that everything was restored properly + dtest.assert_sampleset_energies(sampleset, bqm) + + self.assertEqual(sampler.child.input['bqm'], + dimod.BQM.from_ising({'a': -4.0, 'b': -2.0}, + {('a', 'b'): 1.6}, .75)) + + def test_scalar(self): + bqm = dimod.BQM.from_ising({'a': -4.0, 'b': -4.0}, + {('a', 'b'): 3.2}, 1.5) + + sampler = ScaleComposite(dimod.TrackingComposite(dimod.ExactSolver())) + + sampleset = sampler.sample(bqm, scalar=.5) + + # check that everything was restored properly + dtest.assert_sampleset_energies(sampleset, bqm) + + self.assertEqual(sampler.child.input['bqm'], + dimod.BQM.from_ising({'a': -2.0, 'b': -2.0}, + {('a', 'b'): 1.6}, .75)) diff --git a/tests/test_spin_reversal_transform.py b/tests/test_spin_reversal_transform.py new file mode 100644 index 0000000..fb04921 --- /dev/null +++ b/tests/test_spin_reversal_transform.py @@ -0,0 +1,28 @@ +# 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 unittest + +from dimod import ExactSolver, RandomSampler, SimulatedAnnealingSampler +import dimod.testing as dtest + +from dwave.preprocessing.composites import SpinReversalTransformComposite + +class TestSpinTransformComposite(unittest.TestCase): + def test_instantiation(self): + for factory in [ExactSolver, RandomSampler, SimulatedAnnealingSampler]: + sampler = SpinReversalTransformComposite(factory()) + + dtest.assert_sampler_api(sampler) + dtest.assert_composite_api(sampler)