
"""
Examples that might be useful at the APS using BlueSky

.. autosummary::
   
   ~SynPseudoVoigt

"""

# Copyright (c) 2017-2018, UChicago Argonne, LLC.  See LICENSE file.

import logging
import ophyd.sim
import numpy as np


logger = logging.getLogger(__name__).addHandler(logging.NullHandler())


class SynPseudoVoigt(ophyd.sim.SynSignal):
    """
    Evaluate a point on a pseudo-Voigt based on the value of a motor.
    
    Provides a signal to be measured.
    Acts like a detector.
    
    :see: https://en.wikipedia.org/wiki/Voigt_profile

    PARAMETERS
    
    name : str
        name of detector signal
    motor : `Mover`
        The independent coordinate
    motor_field : str
        name of `Mover` field
    center : float, optional
        location of maximum value, default=0
    eta : float, optional
        0 <= eta < 1.0: Lorentzian fraction, default=0.5
    scale : float, optional
        scale >= 1 : scale factor, default=1
    sigma : float, optional
        sigma > 0 : width, default=1
    bkg : float, optional
        bkg >= 0 : constant background, default=0
    noise : {'poisson', 'uniform', None}
        Add noise to the result.
    noise_multiplier : float
        Only relevant for 'uniform' noise. Multiply the random amount of
        noise by 'noise_multiplier'

    EXAMPLE
    
    ::

        from APS_BlueSky_tools.examples import SynPseudoVoigt
        motor = Mover('motor', {'motor': lambda x: x}, {'x': 0})
        det = SynPseudoVoigt('det', motor, 'motor', 
            center=0, eta=0.5, scale=1, sigma=1, bkg=0)

    EXAMPLE
    
    ::

        import numpy as np
        from APS_BlueSky_tools.examples import SynPseudoVoigt
        synthetic_pseudovoigt = SynPseudoVoigt(
            'synthetic_pseudovoigt', m1, 'm1', 
            center=-1.5 + 0.5*np.random.uniform(), 
            eta=0.2 + 0.5*np.random.uniform(), 
            sigma=0.001 + 0.05*np.random.uniform(), 
            scale=1e5,
            bkg=0.01*np.random.uniform())

        #  RE(bp.scan([synthetic_pseudovoigt], m1, -2, 0, 219))

    """

    def __init__(self, name, motor, motor_field, center=0, 
                eta=0.5, scale=1, sigma=1, bkg=0, 
                noise=None, noise_multiplier=1,
                **kwargs):
        if eta < 0.0 or eta > 1.0:
            raise ValueError("eta={} must be between 0 and 1".format(eta))
        if scale < 1.0:
            raise ValueError("scale must be >= 1")
        if sigma <= 0.0:
            raise ValueError("sigma must be > 0")
        if bkg < 0.0:
            raise ValueError("bkg must be >= 0")
        
        # remember these terms for later access by user
        self.name = name
        self.motor = motor
        self.center = center
        self.eta = eta
        self.scale = scale
        self.sigma = sigma
        self.bkg = bkg
        self.noise = noise
        self.noise_multiplier = noise_multiplier

        def f_lorentzian(x, gamma):
            #return gamma / np.pi / (x**2 + gamma**2)
            return 1 / np.pi / gamma / (1 + (x/gamma)**2)

        def f_gaussian(x, sigma):
            numerator = np.exp(-0.5 * (x / sigma) ** 2)
            denominator = sigma * np.sqrt(2 * np.pi)
            return numerator / denominator

        def pvoigt():
            m = motor.read()[motor_field]['value']
            g_max = f_gaussian(0, sigma)    # peak normalization
            l_max = f_lorentzian(0, sigma)
            v = bkg
            if eta > 0:
                v += eta * f_lorentzian(m - center, sigma) / l_max
            if eta < 1:
                v += (1-eta) * f_gaussian(m - center, sigma) / g_max
            v *= scale
            if noise == 'poisson':
                v = int(np.random.poisson(np.round(v), 1))
            elif noise == 'uniform':
                v += np.random.uniform(-1, 1) * noise_multiplier
            return v

        super().__init__(name=name, func=pvoigt, **kwargs)
