# Source code for Arms.Gaussian

# -*- coding: utf-8 -*-
""" Gaussian distributed arm.

Example of creating an arm:

>>> import random; import numpy as np
>>> random.seed(0); np.random.seed(0)
>>> Gauss03 = Gaussian(0.3, 0.05)  # small variance
>>> Gauss03
N(0.3, 0.05)
>>> Gauss03.mean
0.3

Examples of sampling from an arm:

>>> Gauss03.draw()  # doctest: +ELLIPSIS
0.3470...
>>> Gauss03.draw_nparray(20)  # doctest: +ELLIPSIS,+NORMALIZE_WHITESPACE
array([0.388..., 0.320..., 0.348... , 0.412..., 0.393... ,
0.251..., 0.347..., 0.292..., 0.294..., 0.320...,
0.307..., 0.372..., 0.338..., 0.306..., 0.322...,
0.316..., 0.374..., 0.289..., 0.315..., 0.257...])
"""
from __future__ import division, print_function  # Python 2 compatibility

__author__ = "Olivier Cappé, Aurélien Garivier, Lilian Besson"
__version__ = "0.9"

from random import gauss
from numpy.random import standard_normal
import numpy as np
from scipy.special import erf

# Local imports
try:
from .Arm import Arm
from .kullback import klGauss
except ImportError:
from Arm import Arm
from kullback import klGauss

oo = float('+inf')  # Nice way to write +infinity

#: Default value for the variance of a [0, 1] Gaussian arm
VARIANCE = 0.05

[docs]class Gaussian(Arm):
""" Gaussian distributed arm, possibly truncated.

- Default is to truncate into [0, 1] (so Gaussian.draw() is in [0, 1]).
"""

[docs]    def __init__(self, mu, sigma=VARIANCE, mini=0, maxi=1):
"""New arm."""
self.mu = self.mean = mu  #: Mean of Gaussian arm
assert sigma > 0, "Error, the parameter 'sigma' for Gaussian arm has to be > 0."
self.sigma = sigma  #: Variance of Gaussian arm
assert mini <= maxi, "Error, the parameter 'mini' for Gaussian arm has to < 'maxi'."  # DEBUG
self.min = mini  #: Lower value of rewards
self.max = maxi  #: Higher value of rewards
# XXX if needed, compute the true mean : Cf. https://en.wikipedia.org/wiki/Truncated_normal_distribution#Moments
# real_mean = mu + sigma * (phi(mini) - phi(maxi)) / (Phi(maxi) - Phi(mini))

# --- Random samples

[docs]    def draw(self, t=None):
""" Draw one random sample. The parameter t is ignored in this Arm."""
return min(max(gauss(self.mu, self.sigma), self.min), self.max)

[docs]    def draw_nparray(self, shape=(1,)):
""" Draw a numpy array of random samples, of a certain shape."""
return np.minimum(np.maximum(self.mu + self.sigma * standard_normal(shape), self.min), self.max)

[docs]    def set_mean_param(self, mean):
self.mu = self.mean = mean

# --- Printing

# This decorator @property makes this method an attribute, cf. https://docs.python.org/3/library/functions.html#property
@property
def lower_amplitude(self):
"""(lower, amplitude)"""
return self.min, self.max - self.min

[docs]    def __str__(self):
return "Gaussian"

[docs]    def __repr__(self):
return "N({:.3g}, {:.3g})".format(self.mu, self.sigma)

# --- Lower bound

[docs]    def kl(self, x, y):
""" The kl(x, y) to use for this arm."""
return klGauss(x, y, self.sigma)

[docs]    def oneLR(self, mumax, mu):
""" One term of the Lai & Robbins lower bound for Gaussian arms: (mumax - mu) / KL(mu, mumax). """
return (mumax - mu) / klGauss(mu, mumax, self.sigma)

[docs]    def oneHOI(self, mumax, mu):
""" One term for the HOI factor for this arm."""
return 1 - (mumax - mu) / self.max

[docs]class Gaussian_0_1(Gaussian):
""" Gaussian distributed arm, truncated to [0, 1]."""
[docs]    def __init__(self, mu, sigma=0.05, mini=0, maxi=1):
super(Gaussian_0_1, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_0_2(Gaussian):
""" Gaussian distributed arm, truncated to [0, 2]."""
[docs]    def __init__(self, mu, sigma=0.1, mini=0, maxi=2):
super(Gaussian_0_2, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_0_5(Gaussian):
""" Gaussian distributed arm, truncated to [0, 5]."""
[docs]    def __init__(self, mu, sigma=0.5, mini=0, maxi=5):
super(Gaussian_0_5, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_0_10(Gaussian):
""" Gaussian distributed arm, truncated to [0, 10]."""
[docs]    def __init__(self, mu, sigma=1, mini=0, maxi=10):
super(Gaussian_0_10, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_0_100(Gaussian):
""" Gaussian distributed arm, truncated to [0, 100]."""
[docs]    def __init__(self, mu, sigma=5, mini=0, maxi=100):
super(Gaussian_0_100, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_m1_1(Gaussian):
""" Gaussian distributed arm, truncated to [-1, 1]."""
[docs]    def __init__(self, mu, sigma=0.1, mini=-1, maxi=1):
super(Gaussian_m1_1, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_m2_2(Gaussian):
""" Gaussian distributed arm, truncated to [-2, 2]."""
[docs]    def __init__(self, mu, sigma=0.25, mini=-2, maxi=2):
super(Gaussian_m2_2, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_m5_5(Gaussian):
""" Gaussian distributed arm, truncated to [-5, 5]."""
[docs]    def __init__(self, mu, sigma=1, mini=-5, maxi=5):
super(Gaussian_m5_5, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_m10_10(Gaussian):
""" Gaussian distributed arm, truncated to [-10, 10]."""
[docs]    def __init__(self, mu, sigma=2, mini=-10, maxi=10):
super(Gaussian_m10_10, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

[docs]class Gaussian_m100_100(Gaussian):
""" Gaussian distributed arm, truncated to [-100, 100]."""
[docs]    def __init__(self, mu, sigma=10, mini=-100, maxi=100):
super(Gaussian_m100_100, self).__init__(mu, sigma=sigma, mini=mini, maxi=maxi)

#: Default value for the variance of an unbounded Gaussian arm
UNBOUNDED_VARIANCE = 1

[docs]class UnboundedGaussian(Gaussian):
""" Gaussian distributed arm, not truncated, ie. supported in (-oo,  oo)."""

[docs]    def __init__(self, mu, sigma=UNBOUNDED_VARIANCE):
"""New arm."""
super(UnboundedGaussian, self).__init__(mu, sigma=sigma, mini=-oo, maxi=oo)

# def __str__(self):
#     return "UnboundedGaussian"

# --- Random samples

[docs]    def draw(self, t=None):
""" Draw one random sample. The parameter t is ignored in this Arm."""
return gauss(self.mu, self.sigma)

[docs]    def draw_nparray(self, shape=(1,)):
""" Draw a numpy array of random samples, of a certain shape."""
return self.mu + self.sigma * standard_normal(shape)

[docs]    def __repr__(self):
return "N({:.3g}, {:.3g})".format(self.mu, self.sigma)

def phi(xi):
r"""The :math:\phi(\xi) function, defined by:

.. math:: \phi(\xi) := \frac{1}{\sqrt{2 \pi}} \exp\left(- \frac12 \xi^2 \right)

It is the probability density function of the standard normal distribution, see https://en.wikipedia.org/wiki/Standard_normal_distribution.
"""
return np.exp(- 0.5 * xi**2) / np.sqrt(2. * np.pi)

def Phi(x):
r"""The :math:\Phi(x) function, defined by:

.. math:: \Phi(x) := \frac{1}{2} \left(1 + \mathrm{erf}\left( \frac{x}{\sqrt{2}} \right) \right).

It is the probability density function of the standard normal distribution, see https://en.wikipedia.org/wiki/Cumulative_distribution_function
"""
return (1. + erf(x / np.sqrt(2.))) / 2.

# Only export and expose the classes defined here
__all__ = [
"Gaussian",
"Gaussian_0_1",
"Gaussian_0_2",
"Gaussian_0_5",
"Gaussian_0_10",
"Gaussian_0_100",
"Gaussian_m1_1",
"Gaussian_m2_2",
"Gaussian_m5_5",
"Gaussian_m10_10",
"Gaussian_m100_100",
"UnboundedGaussian"
]

# --- Debugging

if __name__ == "__main__":
# Code for debugging purposes.
from doctest import testmod
print("\nTesting automatically all the docstring written in each functions of this module :")
testmod(verbose=True)