Source code for dimod.core.sampler

"""
The :class:`.Sampler` abstract base class (`ABC <https://docs.python.org/3.6/library/abc.html#module-abc>`_\ )
helps you create new dimod samplers.

Any new dimod sampler must define a subclass of :class:`.Sampler` that implements
abstract properties :attr:`~.Sampler.parameters` and :attr:`~.Sampler.properties`
and one of the abstract methods :meth:`~.Sampler.sample`, :meth:`~.Sampler.sample_ising`,
or :meth:`~.Sampler.sample_qubo`. The :class:`.Sampler` class provides the complementary
methods as mixins and ensures consistent responses.

For example, the following steps show how to easily create a dimod sampler. It is
sufficient to implement a single method (in this example the :meth:`sample_ising` method)
to create a dimod sampler with the :class:`.Sampler` class.

.. code-block:: python

    class LinearIsingSampler(dimod.Sampler):

        def sample_ising(self, h, J):
            sample = linear_ising(h, J)
            energy = dimod.ising_energy(sample, h, J)
            return dimod.Response.from_dicts([sample], {'energy': [energy]})

        @property
        def properties(self):
            return dict()

        @property
        def parameters(self):
            return dict()

For this example, the implemented sampler :meth:`~.Sampler.sample_ising` can be based on
a simple placeholder function, which returns a sample that minimizes the linear terms:

.. code-block:: python

    def linear_ising(h, J):
        sample = {}
        for v in h:
            if h[v] < 0:
                sample[v] = +1
            else:
                sample[v] = -1
        return sample


The :class:`.Sampler` ABC provides the other sample methods "for free"
as mixins.

.. code-block:: python

    sampler = LinearIsingSampler()
    response = sampler.sample_ising({'a': -1}, {})  # Implemented by class LinearIsingSampler
    response = sampler.sample_qubo({('a', 'a'): 1})  # Mixin provided by Sampler class
    response = sampler.sample(BinaryQuadraticModel.from_ising({'a': -1}, {}))  # Mixin provided by Sampler class

Below is a more complex version of the same sampler, where the :attr:`properties` and
:attr:`parameters` properties return non-empty dicts.

.. code-block:: python

    class FancyLinearIsingSampler(dimod.Sampler):
        def __init__(self):
            self._properties = {'description': 'a simple sampler that only considers the linear terms'}
            self._parameters = {'verbose': []}

        def sample_ising(self, h, J, verbose=False):
            sample = linear_ising(h, J)
            energy = dimod.ising_energy(sample, h, J)
            if verbose:
                print(sample)
            return dimod.Response.from_dicts([sample], {'energy': [energy]})

        @property
        def properties(self):
            return self._properties

        @property
        def parameters(self):
            return self._parameters


"""
from six import add_metaclass

from dimod.binary_quadratic_model import BinaryQuadraticModel
from dimod.exceptions import InvalidSampler
from dimod.vartypes import Vartype

import dimod.abc as abc

__all__ = ['Sampler']


@add_metaclass(abc.SamplerABCMeta)
class Sampler:
    """Abstract base class for dimod samplers.

    Provides all methods :meth:`~.Sampler.sample`, :meth:`~.Sampler.sample_ising`,
    :meth:`~.Sampler.sample_qubo` assuming at least one is implemented.

    """

    @abc.abstractproperty  # for python2 compatibility
    def parameters(self):
        """dict: A dict where keys are the keyword parameters accepted by the sampler
        methods and values are lists of the properties relevent to each parameter.

        Examples:
            This example adds a `verbose` parameter to an Ising sampler.

            .. code-block:: python

                class IsingSampler(dimod.Sampler):
                    def __init__(self):
                        self._parameters = {'verbose': []}

                    def sample_ising(self, h, J, verbose=False):
                        sample = linear_ising(h, J) # Implemented elsewhere
                        energy = dimod.ising_energy(sample, h, J)
                        if verbose:
                            print(sample)
                        return dimod.Response.from_dicts([sample], {'energy': [energy]})

                    @property
                    def properties(self):
                        return dict()

                    @property
                    def parameters(self):
                        return self._parameters

        """
        pass

    @abc.abstractproperty  # for python2 compatibility
    def properties(self):
        """dict: A dict containing any additional information about the sampler.

        Examples:
            This example adds a `description` property to an Ising sampler.

            .. code-block:: python

                class IsingSampler(dimod.Sampler):
                    def __init__(self):
                        self._properties = {'description': 'an example Ising sampler'}

                    def sample_ising(self, h, J):
                        sample = linear_ising(h, J) # Implemented elsewhere
                        energy = dimod.ising_energy(sample, h, J)
                        return dimod.Response.from_dicts([sample], {'energy': [energy]})

                    @property
                    def properties(self):
                        return self._properties

                    @property
                    def parameters(self):
                        return dict()

        """
        pass

    @abc.samplemixinmethod
    def sample(self, bqm, **parameters):
        """Samples from a binary quadratic model using an implemented sample method.

        Examples:
            This example implements a placeholder Ising sampler and samples using
            the mixin binary quadratic model sampler.

            >>> import dimod
            >>> class ImplementIsingSampler(dimod.Sampler):
            ...     def sample_ising(self, h, J):
            ...         return dimod.Response.from_dicts([{1: -1, 2: +1}], {'energy': [-1.0]}) # Placeholder
            ...     @property
            ...     def properties(self):
            ...         return self._properties
            ...     @property
            ...     def parameters(self):
            ...         return dict()
            ...
            >>> sampler = ImplementIsingSampler()
            >>> model = dimod.BinaryQuadraticModel({0: 1, 1: -1, 2: .5},
            ...                                    {(0, 1): .5, (1, 2): 1.5},
            ...                                    1.4,
            ...                                    dimod.SPIN)
            >>> response = sampler.sample(model)
            >>> print(response)
            [[-1  1]]

        """
        if bqm.vartype is Vartype.SPIN:
            Q, offset = bqm.to_qubo()
            response = self.sample_qubo(Q, **parameters)
            response.change_vartype(Vartype.SPIN, data_vector_offsets={'energy': offset})
            return response
        elif bqm.vartype is Vartype.BINARY:
            h, J, offset = bqm.to_ising()
            response = self.sample_ising(h, J, **parameters)
            response.change_vartype(Vartype.BINARY, data_vector_offsets={'energy': offset})
            return response
        else:
            raise RuntimeError("binary quadratic model has an unknown vartype")

    @abc.samplemixinmethod
    def sample_ising(self, h, J, **parameters):
        """Samples from an Ising model using an implemented sample method.

        Examples:
            This example implements a placeholder QUBO sampler and samples using
            the mixin Ising sampler.

            >>> import dimod
            >>> class ImplementQuboSampler(dimod.Sampler):
            ...     def sample_qubo(self, Q):
            ...         return dimod.Response.from_dicts([{1: -1, 2: +1}], {'energy': [-1.0]}) # Placeholder
            ...     @property
            ...     def properties(self):
            ...         return self._properties
            ...     @property
            ...     def parameters(self):
            ...         return dict()
            ...
            >>> sampler = ImplementQuboSampler()
            >>> h = {1: 0.5, 2: -1, 3: -0.75}
            >>> J = {}
            >>> response = sampler.sample_ising(h, J)
            >>> print(response)
            [[-1  1]]

        """
        bqm = BinaryQuadraticModel.from_ising(h, J)
        response = self.sample(bqm, **parameters)
        return response

    @abc.samplemixinmethod
    def sample_qubo(self, Q, **parameters):
        """Samples from a QUBO using an implemented sample method.

        Examples:
            This example implements a placeholder Ising sampler and samples using
            the mixin QUBO sampler.

            >>> import dimod
            >>> class ImplementIsingSampler(dimod.Sampler):
            ...     def sample_ising(self, h, J):
            ...         return dimod.Response.from_dicts([{1: -1, 2: +1}], {'energy': [-1.0]}) # Placeholder
            ...     @property
            ...     def properties(self):
            ...         return self._properties
            ...     @property
            ...     def parameters(self):
            ...         return dict()
            ...
            >>> sampler = ImplementIsingSampler()
            >>> Q = {(0, 0): -0.5, (0, 1): 1, (1, 1): -0.75}
            >>> response = sampler.sample_qubo(Q)
            >>> print(response)
            [[0 1]]


        """
        bqm = BinaryQuadraticModel.from_qubo(Q)
        response = self.sample(bqm, **parameters)
        return response