Source code for desilike.samplers.qmc

"""Module implementing quasi-random sequences with reduced variance."""

import warnings

import numpy as np
from scipy.stats import qmc
from scipy.stats.qmc import Sobol, Halton, LatinHypercube

from .base import StaticSampler
from desilike.parameter import ParameterPriorError


class KroneckerSequence(qmc.QMCEngine):
    """A quasi-random sequence based on the inverse golden ratio."""

    def __init__(self, d, seed=0.5):
        """Initialize the sequence.

        Parameters
        ----------
        d : int
            Dimensionality.
        seed : float, optional
            Starting sample for the sequence in each dimension. Default is 0.5.

        """
        super().__init__(d=d)
        self.seed = float(seed)
        phi = 1.0
        # Use Newton's method to solve phi**(d+1) - phi - 1 = 0.
        while np.abs(phi**(self.d + 1) - phi - 1) > 1e-12:
            phi -= (phi**(self.d + 1) - phi - 1) / (
                (self.d + 1) * phi**self.d - 1)
        self.alpha = np.array([phi**(-(1 + d)) for d in range(self.d)])

    def _random(self, n=1, *, workers=1):
        """Get samples.

        Parameters
        ----------
        n : int
            Number of samples.
        workers : optional
            Ignored.

        Returns
        -------
        numpy.ndarray of shape (n_samples, n_dim)
            Samples.

        """
        i = np.arange(self.num_generated + 1, self.num_generated + n + 1)
        samples = (self.seed + np.outer(i, self.alpha)) % 1.
        self.num_generated += n
        if self.num_generated < np.amax(1 / self.alpha):
            warnings.warn(f"Kronecker sequence does not fill space with less "
                          f"{int(np.amax(1 / self.alpha))} samples.")
        return samples


ENGINES = dict(sobol=Sobol, halton=Halton, lhs=LatinHypercube,
               kronecker=KroneckerSequence)


[docs] class QMCSampler(StaticSampler): """Sampler based on Quasi Monte-Carlo (QMC) sequences. In addition to sequences in :mod:`scipy.qmc`, this module also implements Kronecker sequences. .. rubric:: References - `scipy QMC docs <https://docs.scipy.org/doc/scipy/reference/\ stats.qmc.html>`_ """
[docs] def get_samples(self, size=1000, engine='kronecker', **kwargs): """Get samples from a QMC sequence. Parameters ---------- size : dict or int, optional Size of the sequence. Default is 1000. engine : str, optional Engine to use. Choices are 'sobol', 'halton', 'lhs', 'kronecker'. Default is 'kronecker'. **kwargs Extra keyword arguments passed to the engine. Returns ------- numpy.ndarray of shape (n_samples, n_dim) Grid to be evaluated. """ lower, upper = [], [] for param in self.likelihood.varied_params: if param.limits is None: raise ParameterPriorError( f"Provide a limit for {param.name}.") lower.append(param.limits[0]) upper.append(param.limits[1]) if engine not in ENGINES: raise ValueError(f"'engine' must be in {list(ENGINES.keys())}. " f"Received {engine}.") self.engine = ENGINES[engine](d=len(self.varied_params), **kwargs) return qmc.scale(self.engine.random(n=size), lower, upper)