Source code for dimod.samplers.template_sampler

"""
dimod Samplers
==============

The sampler template provides an API that different samplers can use.
The crux of the API are two methods and a property.

Methods:

- sample_ising
- sample_qubo

Property:

- structure

Some samplers will only function on binary quadratic models with specific
structures. These samplers should have information in the `structure`
property. See Structured Samplers section below.


Examples
--------
Define a sampler that operates on QUBO problems:

>>> class MyLinearSampler(dimod.TemplateSampler):
...     def __init__(self):
...         dimod.TemplateSampler.__init__(self)
...         self.structure = 'linear'
...
...     @dimod.decorators.qubo(1)
...     def sample_qubo(self, Q):
...         response = dimod.BinaryResponse()
...
...         sample = {}
...         for (u, v) in Q:
...             if u != v:
...                 raise ValueError()
...
...             if Q[(u, v)] > 0:
...                 val = 0
...             else:
...                 val = 1
...
...             sample[v] = val
...
...         response.add_sample(sample, Q=Q)
...         return response

This will now behave as expected

>>> Q = {(0, 0): 1, (1, 1): 0}
>>> response = MyLinearSampler().sample_qubo(Q)
>>> list(response.samples())
[{0: 0, 1: 1}]

Also, by implementing one of the methods, we now can use the others.

>>> h = {0: -1, 1: 2}
>>> J = {}
>>> response = MyLinearSampler().sample_ising(h, J)
>>> list(response.samples())
[{0: 1, 1: -1}]

"""
# we could do a solver that requires a complete graph as an example above...
import inspect

from dimod import _PY2
from dimod.decorators import ising, qubo
from dimod.keyword_arguments import SamplerKeywordArg
from dimod.utilities import qubo_to_ising, ising_to_qubo

__all__ = ['TemplateSampler']


class TemplateSampler(object):
    """Serves as a template for samplers. Not intended to be used directly.

    The methods as provided are self-referential, trying to invoke them
    directly will lead to an infinite recursion. This is done so that users
    need only implement the methods that make sense.

    See module documentation for examples.

    """
    def __init__(self):
        self.structure = None

    @qubo(1)
    def sample_qubo(self, Q, **kwargs):
        """Converts the given QUBO into an Ising problem, then invokes the
        sample_ising method.

        See sample_ising documentation for more information.

        Args:
            Q (dict): A dictionary defining the QUBO. Should be of the form
                {(u, v): bias} where u, v are variables and bias is numeric.
            **kwargs: Any keyword arguments are passed directly to
                sample_ising.

        Returns:
            :obj:`BinaryResponse`:
                A `BinaryResponse`, converted from the `SpinResponse` return
                from sample_ising.

        Note:
            This method is inherited from the :obj:`TemplateSampler` base class.

        """
        h, J, offset = qubo_to_ising(Q)
        spin_response = self.sample_ising(h, J, **kwargs)
        return spin_response.as_binary(offset)

    @ising(1, 2)
    def sample_ising(self, h, J, **kwargs):
        """Converts the given Ising problem into a QUBO, then invokes the
        sample_qubo method.

        See sample_qubo documentation for more information.

        Args:
            h (dict/list): The linear terms in the Ising problem. If a
                dict, should be of the form {v: bias, ...} where v is
                a variable in the Ising problem, and bias is the linear
                bias associated with v. If a list, should be of the form
                [bias, ...] where the indices of the biases are the
                variables in the Ising problem.
            J (dict): A dictionary of the quadratic terms in the Ising
                problem. Should be of the form {(u, v): bias} where u,
                v are variables in the Ising problem and bias is the
                quadratic bias associated with u, v.
            **kwargs: Any keyword arguments are passed directly to
                sample_qubo.

        Returns:
            :obj:`SpinResponse`:
                A `SpinResponse`, converted from the `BinaryResponse`
                return from sample_ising.

        Note:
            This method is inherited from the :obj:`TemplateSampler` base class.

        """
        Q, offset = ising_to_qubo(h, J)
        binary_response = self.sample_qubo(Q, **kwargs)
        return binary_response.as_spin(offset)

    @property
    def structure(self):
        """Structure for the sampler. None for unstructured samplers."""
        return self._structure

    @structure.setter
    def structure(self, struct):
        self._structure = struct

    @property
    def accepted_kwargs(self):
        """dict[str: :class:`.SamplerKeywordArg`]: The keyword arguments
        accepted by the `sample_ising` and `sample_qubo` methods for this
        sampler.
        """
        return self.my_kwargs()

    def my_kwargs(self):
        """The keyword arguments accepted by the sampler or the highest
        composite layer.

        Returns:
            dict[str: :class:`.SamplerKeywordArg`]: The keyword arguments
            accepted by the `sample_ising` and `sample_qubo` methods for this
            sampler or the top-level composite layer. For all accepted keyword
            arguments see `accepted_kwargs`.

        Note:
            This method is inherited from the :obj:`.TemplateSampler` base class.

        """
        if _PY2:
            # we need to use getargspec in python 2
            ising_spec = inspect.getargspec(self.sample_ising)
            qubo_spec = inspect.getargspec(self.sample_qubo)

            kwargs = {}
            for name in ising_spec.args:
                if name == 'self':
                    continue
                elif name == 'h':
                    kwargs[name] = SamplerKeywordArg(name, 'dict[hashable, numeric]', dict)
                elif name == 'J':
                    kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
                else:
                    kwargs[name] = SamplerKeywordArg(name)
            for name in qubo_spec.args:
                if name == 'self':
                    continue
                elif name == 'Q':
                    kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
                else:
                    kwargs[name] = SamplerKeywordArg(name)
        else:
            # in python 3 we can use signature
            ising_sig = inspect.signature(self.sample_ising)
            qubo_sig = inspect.signature(self.sample_qubo)

            kwargs = {}
            for name, param in qubo_sig.parameters.items():
                if param.kind == param.POSITIONAL_OR_KEYWORD:
                    if name == 'Q':
                        kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
                    else:
                        kwargs[name] = SamplerKeywordArg(name)
            for name, param in ising_sig.parameters.items():
                if param.kind == param.POSITIONAL_OR_KEYWORD:
                    if name == 'h':
                        kwargs[name] = SamplerKeywordArg(name, 'dict[hashable, numeric]', dict)
                    elif name == 'J':
                        kwargs[name] = SamplerKeywordArg(name, 'dict[(hashable, hashable), numeric]', dict)
                    else:
                        kwargs[name] = SamplerKeywordArg(name)
        return kwargs