#!/usr/bin/env python
# encoding: utf-8
"""
expfitting.py
Provide single or double exponential fits to data.
"""

import lmfit
import numpy as np
import scipy.optimize


class ExpFitting:
    """
    Parameters
    ----------
    nexp : int
        1 or 2 for single or double exponential fit
    initpars : dict
        dict of initial parameters. For example: {'dc': 0., 
        'a1': 1., 't1': 3, 'a2' : 0.5, 'delta': 3.}, where
        delta determines the ratio between the time constants.
    bounds : dict
        dictionary of bounds for each parameter, with a list of lower and upper values.
    
    """

    def __init__(self, nexp=1, initpars=None, bounds=None):
        self.fitpars = lmfit.Parameters()
        if nexp == 1:
            # (Name,  Value,  Vary,   Min,  Max,  Expr)
            self.fitpars.add_many(
                ("dc", 0, True, -100.0, 0.0, None),
                ("a1", 1.0, True, -25.0, 25.0, None),
                ("t1", 10.0, True, 0.1, 50, None),
            )
            self.efunc = self.exp1_err
        elif nexp == 2:
            self.fitpars.add_many(
                ("dc", 0, True, -100.0, 0.0, None),
                ("a1", 1.0, True, 0.0, 25.0, None),
                ("t1", 10.0, True, 0.1, 50, None),
                ("a2", 1.0, True, 0.0, 25.0, None),
                ("delta", 3.0, True, 3.0, 100.0, None),
            )
            if initpars is not None:
                assert len(initpars) == 5
                for k, v in initpars.iteritems():
                    self.fitpars[k].value = v
            if bounds is not None:
                assert len(bounds) == 5
                for k, v in bounds.iteritems():
                    self.fitpars[k].min = v[0]
                    self.fitpars[k].max = v[1]

            self.efunc = self.exp2_err
        else:
            raise ValueError

    def fit(self, x, y, p, verbose=False):

        kws = {"maxfev": 5000}
        mim = lmfit.minimize(
            self.efunc, p, method="least_squares", args=(x, y)
        )  # , kws=kws)
        if verbose:
            lmfit.printfuncs.report_fit(mim.params)
        fitpars = mim.params
        return fitpars

    @staticmethod
    def exp1(x, dc, t1, a1):
        return dc + a1 * np.exp(-x / t1)

    def exp1_err(self, p, x, y):
        return np.fabs(y - self.exp1(x, **dict([(k, p.value) for k, p in p.items()])))

    @staticmethod
    def exp2(x, dc, t1, a1, a2, delta):
        return dc + a1 * np.exp(-x / t1) + a2 * np.exp(-x / (t1 * delta))

    def exp2_err(self, p, x, y):
        return np.fabs(y - self.exp2(x, **dict([(k, p.value) for k, p in p.items()])))
