import warnings
from abc import ABCMeta, abstractmethod
from collections import namedtuple
from functools import partial

import matplotlib.pyplot as plt
import numpy as np
import scipy.optimize as sp_optimize
from scipy import interpolate, linalg, special, stats
from scipy.integrate import cumulative_trapezoid, quad

try:
    from multiprocessing import cpu_count

    import numexpr as ne

    ne.set_num_threads(min(64, cpu_count()))
    NE = True
except ImportError:
    NE = False

import varwg
from varwg import helpers as my
from varwg.time_series_analysis import _kde as kde

# def _build_owens():
#     if sys.platform != "win32":
#         # import urllib.request, urllib.error, urllib.parse
#         from urllib import request, error
#         import socket
#         socket.setdefaulttimeout(10)
#         warn = False
#         url = "http://people.sc.fsu.edu/~jburkardt/f_src/owens/owens.f90"
#         try:
#             src_dir = os.path.dirname(__file__)
#         except NameError:
#             src_dir = os.path.abspath(".")
#         # if os.path.exists(os.path.join(src_dir, "owens.f90")):
#         #     warn = True
#         if not warn:
#             with my.chdir(src_dir):
#                 with open("owens.f90", "w") as owens_file:
#                     try:
#                         content_full = (request
#                                         .urlopen(url)
#                                         .read()
#                                         .decode("utf-8"))
#                         # under python 3: "character ( len = 9 )" causes
#                         # f2py to fail, so we cut out the subroutine
#                         # timestep, which is not needed for owens-t
#                         content = []
#                         keep_line = True
#                         for line in content_full.split("\n"):
#                             if line.startswith("subroutine timestamp"):
#                                 keep_line = False
#                             if keep_line:
#                                 content += [line]
#                             if not keep_line and line == "end":
#                                 keep_line = True
#                         content = os.linesep.join(content)
#                         owens_file.write(content + os.linesep)
#                     except error.URLError:
#                         warn = True
#                 try:
#                     subprocess.call("f2py -L$PREFIX$/lib -c -m owens owens.f90",
#                                     shell=True)
#                     from varwg.time_series_analysis import owens
#                     warn = False
#                 except Exception:
#                     warn = True
#         if warn:
#             warnings.warn("""Could not import owens t function.
#                 Try (if on linux):
#                 wget {url}
#                 f2py -c -m owens owens.f90
#                 cp owens.so {src_dir}/
#                 """.format(url=url, src_dir=src_dir))
#             return False
#         return True
#     else:
#         return False


# try:
#     from varwg.time_series_analysis import owens
# except ImportError:
#     owens = _build_owens()
# if owens:
#     # it might have been build freshly
#     from varwg.time_series_analysis import owens
#     owens_t = np.vectorize(lambda h, a: owens.t(h, a))

# some special functions vectorized to be able to handle arrays as input
gamma_func = np.vectorize(lambda x: special.gamma(x), otypes=[float])
gammaln = np.vectorize(lambda x: special.gammaln(x), otypes=[float])
gammainc = np.vectorize(lambda a, x: special.gammainc(a, x), otypes=[float])
gammaincinv = np.vectorize(lambda a, qq: special.gammaincinv(a, qq), otypes=[float])
digamma = np.vectorize(lambda a: special.digamma(a), otypes=[float])
hyp1f1 = np.vectorize(lambda a, b, x: special.hyp1f1(a, b, x), otypes=[float])
nctdtr = np.vectorize(lambda x1, x2, x3: special.nctdtr(x1, x2, x3), otypes=[float])
nctdtrit = np.vectorize(lambda x1, x2, x3: special.nctdtrit(x1, x2, x3), otypes=[float])
stdtr = np.vectorize(lambda df, x: special.stdtr(df, x), otypes=[float])
stdtrit = np.vectorize(lambda df, qq: special.stdtrit(df, qq), otypes=[float])

sqrt2 = np.sqrt(2)


def max_likelihood(
    density_func,
    x0,
    values=None,
    opt_func=None,
    disp=False,
    weights=None,
    method="BFGS",
    bounds=None,
    *args,
    **kwds,
):
    """Fits parameters of a density function to data, using the log-maximum-
    likelihood approach."""
    if opt_func is None:
        opt_func = sp_optimize.minimize
    if weights is None and values is not None:
        weights = np.ones_like(values)
    # i hereby define the unlikelihood function as the negative
    # log-likelihood function, so that i can maximize the likelihood
    # by minimizing the unlikelihood
    if values is None:
        args = ()

        def unlikelihood(params):
            return -np.sum(np.log(weights * density_func(params) + 1e-12))

    else:
        args = (values,)

        def unlikelihood(params, values):
            # we have to express fixed values in terms of kwds,
            # because there could be collisions otherwise...

            # first come, first serve: interpret the first n elements
            # of params as the first n variables in
            # dist.parameter_names
            p_kwds = {
                par_name: value
                for par_name, value in zip(
                    density_func.__self__.parameter_names, params
                )
                if par_name not in kwds
            }
            p_kwds.update(kwds)
            dens = density_func(values, **p_kwds)
            try:
                inside = weights * dens
            except FloatingPointError:
                inside = dens
            inside[~np.isfinite(inside)] = 1e-9
            inside[inside <= 0] = 1e-9
            return -np.sum(np.log(inside))

    return opt_func(
        unlikelihood,
        x0,
        args=args,
        # bounds=density_func._bounds,
        bounds=bounds,
        # method=method,
        # method=("L-BFGS-B" if density_func._bounds else method),
        method=("L-BFGS-B" if bounds else method),
        options={"maxiter": 1e4 * len(x0), "disp": disp},
    )


def min_ks(cdf, x0, values, opt_func=None, disp=False, *args, **kwds):
    """Fits parameters of a cumulative distribution function to data,
    minimizing the Kolmogorov-Smirnof test statistic."""
    if opt_func is None:
        opt_func = sp_optimize.fmin
    n = len(values)
    values_sorted = np.sort(values)
    ranks_plus = np.arange(0.0, n) / n
    ranks_minus = np.arange(1.0, n + 1) / n

    def ks(params):
        # we have to express fixed values in terms of kwds, because there
        # could be collisions otherwise...

        # first come, first serve: interpret the first n elements of params
        # as the first n variables in dist.parameter_names
        p_kwds = {
            par_name: value
            for par_name, value in zip(cdf.__self__.parameter_names, params)
            if par_name not in kwds
        }
        p_kwds.update(kwds)

        cdf_values = cdf(values_sorted, **p_kwds)
        cdf_values[np.isnan(cdf_values)] = np.inf
        dmin_plus = np.abs(cdf_values - ranks_plus).max()
        dmin_minus = np.abs(cdf_values - ranks_minus).max()
        return max(dmin_plus, dmin_minus)

    return opt_func(
        ks,
        x0,
        args=args,
        maxiter=1e4 * len(x0),
        maxfun=1e4 * len(x0),
        disp=disp,
    )


def min_fsum(cdf, x0, values, opt_func=None, disp=False, *args, **kwds):
    if opt_func is None:
        opt_func = sp_optimize.fmin
    n = len(values)
    values_sorted = np.sort(values)
    ranks_plus = np.arange(0.0, n) / n
    ranks_minus = np.arange(1.0, n + 1) / n

    def ks(params):
        # we have to express fixed values in terms of kwds, because there
        # could be collisions otherwise...

        # first come, first serve: interpret the first n elements of params
        # as the first n variables in dist.parameter_names
        p_kwds = {
            par_name: value
            for par_name, value in zip(cdf.__self__.parameter_names, params)
            if par_name not in kwds
        }
        p_kwds.update(kwds)

        cdf_values = cdf(values_sorted, **p_kwds)
        cdf_values[np.isnan(cdf_values)] = np.inf
        dmin_plus = ((cdf_values - ranks_plus) ** 2).sum()
        dmin_minus = ((cdf_values - ranks_minus) ** 2).sum()
        return dmin_plus + dmin_minus

    return opt_func(
        ks,
        x0,
        args=args,
        maxiter=1e4 * len(x0),
        maxfun=1e4 * len(x0),
        disp=disp,
    )


class DistMeta(ABCMeta):
    """Assure some class attributes are set, so that life is easier later on."""

    # pylint does not let me call the first parameter meta :(
    def __new__(cls, name, bases, cls_dict):
        new_cls = super(DistMeta, cls).__new__(cls, name, bases, cls_dict)
        new_cls.name = name.lower()
        # store the distribution parameter names as a class attribute
        # take the pdf as an example
        # could do some signature checks on cdf and ppf with that...
        code = cls_dict["_pdf"].__code__
        # first varname is self, second is x
        varnames = list(code.co_varnames[2 : code.co_argcount])
        # supplements are needed to call pdf, cdf and ppf, but are not
        # fitted (e.g. data for kernel density)
        # number of fittable parameters
        n_pars = len(varnames)
        if "supplements_names" in cls_dict:
            n_pars -= len(cls_dict["supplements_names"])
        else:
            new_cls.supplements_names = None
        if "_bounds" not in new_cls.__dict__:
            new_cls._bounds = None
        new_cls.parameter_names = tuple(varnames)
        new_cls.n_pars = n_pars
        return new_cls


class Dist(metaclass=DistMeta):
    """Mimics part of the interface of stats.rv_continuous. Comes with a few
    extra goodies."""

    # i would like to call the first item "solution", but in order to
    # have a nice compatibility to the optime.minimize result object,
    # it has to be "x"
    Result = namedtuple("Result", ("x", "supplements", "success"))
    isscipy = False

    @abstractmethod
    def _pdf(self):
        pass

    @abstractmethod
    def _cdf(self):
        pass

    @abstractmethod
    def _ppf(self):
        pass

    @abstractmethod
    def _fit(self):
        pass

    @property
    def scipy_(self):
        return False

    def _clean_kwds(self, kwds):
        """Return a copy with only the keywords that are also in
        self.parameter_names."""
        return {
            key: value
            for key, value in list(kwds.items())
            if key in self.parameter_names
        }

    def fit(self, *args, **kwds):
        return self._fit(*args, **self._clean_kwds(kwds))

    def sample(self, *args, **kwds):
        size = np.atleast_1d(args[0])
        qq = varwg.rng.random(size)
        return self.ppf(qq, *args[1:], **self._clean_kwds(kwds))

    @my.asscalar
    def pdf(self, *args, **kwds):
        densities = np.atleast_1d(self._pdf(*args, **self._clean_kwds(kwds)))
        if "x" in kwds:
            x = kwds.pop("x")
            invalid_x = self._invalid_x(x, *args, **kwds)
        else:
            invalid_x = self._invalid_x(args[0], *args[1:], **kwds)
        return np.where(invalid_x | np.isinf(densities), np.nan, densities)

    @my.asscalar
    def cdf(self, *args, **kwds):
        qq = np.atleast_1d(self._cdf(*args, **self._clean_kwds(kwds)))
        if "x" in kwds:
            x = kwds.pop("x")
            invalid_x = self._invalid_x(x, *args, **kwds)
        else:
            invalid_x = self._invalid_x(args[0], *args[1:], **kwds)
        return np.where(invalid_x | np.isinf(qq), np.nan, qq)

    @my.asscalar
    def ppf(self, *args, **kwds):
        if "qq" in kwds:
            qq = kwds.pop("qq")
        else:
            qq = args[0]
        qq = np.atleast_1d(qq)
        finite_mask = np.isfinite(qq)
        quantiles_finite = np.where(finite_mask, qq, -1)
        x = np.atleast_1d(
            self._ppf(quantiles_finite, *args[1:], **self._clean_kwds(kwds))
        )
        if quantiles_finite.size == 1:
            quantiles_finite = np.full_like(x, quantiles_finite)
        x = self._fix_x(x)
        x[(quantiles_finite < 0) | (quantiles_finite > 1)] = np.nan
        return x

    @my.asscalar
    def mean(self, *args, **kwds):
        """Crude estimation of the expected value. Heavy tails break this!"""
        epsilon = 1e-6
        x_min, x_max = self.ppf([epsilon, 1 - epsilon], *args, **kwds)
        result = quad(lambda x: x * self.pdf(x, *args, **kwds), x_min, x_max)
        return result[0]

    @my.asscalar
    def median(self, *args, **kwds):
        return self.ppf(0.5, *args, **kwds)

    def __call__(self, *params):
        return Frozen(self, *params)

    def fit_ml(self, values, x0=None, *args, **kwds):
        if x0 is None:
            x0 = (1,) * self.n_pars
        result = max_likelihood(
            self.pdf, x0, values, bounds=self._bounds, *args, **kwds
        )
        return self.Result(
            x=result.x, supplements=None, success=result.success
        )

    # def fit_ml(self, values, x0=None, *args, **kwds):
    #     if x0 is None:
    #         x0 = (1, ) * len(self.parameter_names)
    #     if self.supplements_names is not None:
    #         x0 = list(x0)
    #         for name in self.supplements_names:
    #             x0[self.parameter_names.index(name)] = None
    #             # x0.pop(self.parameter_names.index(name))
    #     params = max_likelihood(self.pdf, x0, values, *args, **kwds)
    #     if self.supplements_names is not None:
    #         supplements = {name: params[self.parameter_names.index(name)]
    #                        for name in self.supplements_names}
    #         for name in self.supplements_names:
    #             params.remove(name)
    #     else:
    #         supplements = self.supplements
    #     return self.Result(x=params, supplements=supplements)

    def fit_ks(
        self, values, opt_func=sp_optimize.fmin, x0=None, *args, **kwds
    ):
        if x0 is None:
            x0 = (1,) * len(self.parameter_names)
        return min_ks(self.cdf, x0, values, opt_func, *args, **kwds)

    def fit_fsum(
        self, values, opt_func=sp_optimize.fmin, x0=None, *args, **kwds
    ):
        if x0 is None:
            x0 = (1,) * len(self.parameter_names)
        return min_fsum(self.cdf, x0, values, opt_func, *args, **kwds)

    def _constraints(self, *args, **kwds):
        param = kwds[list(kwds.keys())[0]]
        return np.full_like(param, False, dtype=bool)

    def _invalid_x(self, x, *args, **kwds):
        """Returns a mask indicating where x < lower bound or x > upper bound."""
        x = np.atleast_1d(x)
        lower_key = (
            "lc"
            if "lc" in self.parameter_names
            else "l" if "l" in self.parameter_names else None
        )
        lower = kwds.get(lower_key, False)
        b_shape = np.broadcast(x, lower).shape
        mask = np.atleast_1d(np.full(b_shape, False, dtype=bool))
        if isinstance(lower, np.ndarray) or lower:
            if lower is None:
                lower_i = self.parameter_names.index(lower_key)
                lower = np.atleast_1d(args[lower_i])
            if x.size == 1 and isinstance(lower, np.ndarray):
                x = np.full_like(lower, x)
                mask = np.full_like(lower, mask)
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", RuntimeWarning)
                mask[x < lower] = True

        upper_key = (
            "uc"
            if "uc" in self.parameter_names
            else "u" if "u" in self.parameter_names else None
        )
        upper = kwds.get(upper_key, False)
        if isinstance(upper, np.ndarray) or upper:
            if upper is None:
                upper_i = self.parameter_names.index(upper_key)
                upper = np.atleast_1d(args[upper_i])
            if x.size == 1 and isinstance(upper, np.ndarray):
                x = np.full_like(upper, x)
                mask = np.full_like(upper, mask)
            with warnings.catch_warnings():
                warnings.simplefilter("ignore", RuntimeWarning)
                mask[x > upper] = True

        return np.squeeze(mask)

    def _fix_x(self, x, *args, **kwds):
        """The distributions might know what to replace invalid x-values with."""
        return x


class Frozen(object):
    # this idea is stolen from the scipy.stats-module and expanded
    def __init__(self, dist, *params):
        self.dist = dist
        self.params = params
        self.parameter_names = dist.parameter_names
        self.name = dist.name

    def pdf(self, x):
        return self.dist.pdf(x, **self.parameter_dict)

    def cdf(self, x):
        return self.dist.cdf(x, **self.parameter_dict)

    def ppf(self, qq):
        return self.dist.ppf(qq, **self.parameter_dict)

    def sample(self, size):
        return self.dist.sample(size, **self.parameter_dict)

    @property
    def mean(self):
        """Crude estimation of the expected value. Heavy tails break this!"""
        return self.dist.mean(**self.parameter_dict)

    @property
    def median(self):
        return self.ppf(0.5)

    @property
    def parameter_dict(self):
        return {
            name: value
            for name, value in zip(self.parameter_names, self.params)
        }

    def plot_fit(self, values=None, n_classes=30):
        """Display a combined plot of a histogram, fitted pdf, empirical cdf
        and fitted cdf."""
        if values is None and hasattr(self.dist, "x"):
            values = self.dist.x
        fig = plt.figure()
        ax1 = fig.add_subplot(111)

        # the histogram of the data
        bins = ax1.hist(
            values, n_classes, density=True, facecolor="green", alpha=0.75
        )[1]

        class_middles = 0.5 * (bins[1:] + bins[:-1])
        density = self.pdf(class_middles)
        ax1.plot(class_middles, density, "r--")

        # the quantile part
        ax2 = ax1.twinx()
        # empirical cdf
        values_sort = np.sort(values)
        ranks_emp = (0.5 + np.arange(len(values))) / len(values)
        ax2.plot(values_sort, ranks_emp)
        # theoretical cdf
        x = np.linspace(values.min(), values.max(), 5e2)
        ranks_theory = self.cdf(x)
        ax2.plot(x, ranks_theory, "r--")
        ax2.grid()
        plt.title(
            ", ".join(
                " %s: %.3f" % (par_name, par)
                for par_name, par in self.parameter_dict.items()
                if par_name != "kernel_data"
            )
        )
        fig.suptitle(self.dist.name)
        return fig, (ax1, ax2)

    def plot_qq(self, values, *args, **kwds):
        """A qq-plot. Scatters theoretic over empirical ranks."""
        ranks_emp = (stats.stats.rankdata(values) - 0.5) / len(values)
        ranks_the = self.cdf(values)
        fig = plt.figure()
        plt.scatter(ranks_emp, ranks_the, marker="o", *args, **kwds)
        plt.plot([0, 1], [0, 1])
        plt.xlim(0, 1)
        plt.ylim(0, 1)
        plt.xlabel("Empirical ranks")
        plt.ylabel("Fitted ranks")
        plt.grid()
        return fig


class Censored(Dist):
    def __init__(self, distribution, lc=-np.inf, uc=np.inf):
        self.dist = distribution
        self.lc = lc
        self.uc = uc
        # read "lc" as "lower cut", "uc" as "upper cut"
        # mind you: these are different than lower and upper bound in i.e. the
        # beta distribution. we can also truncate the beta distribution and
        # have upper bounds above the upper cut.
        if hasattr(distribution, "parameter_names"):
            self.parameter_names = tuple(
                list(distribution.parameter_names) + ["lc", "uc"]
            )
        self.name = "censored " + distribution.name

    def _pdf(self, x, *args, **kwds):
        x = np.atleast_1d(x)
        args = [np.asarray(arg) for arg in args]
        lc = self.lc if "lc" not in kwds else kwds.pop("lc")
        uc = self.uc if "uc" not in kwds else kwds.pop("uc")
        density = self.dist.pdf(x, *args, **kwds)
        qq = self.dist.cdf(x, *args, **kwds)
        lc_above_ii = np.atleast_1d(x > lc)
        if np.any(lc_above_ii):
            try:
                lc_index = np.argmin(x[lc_above_ii])
                density[lc_index] += qq[lc_index]
            except IndexError:
                for jj, lc_above in enumerate(lc_above_ii):
                    lc_index = np.argmin(x[0, lc_above])
                    density[jj, lc_index] += qq[jj, lc_index]
        uc_below_ii = np.atleast_1d(x < uc)
        if np.any(uc_below_ii):
            try:
                uc_index = np.argmax(x[uc_below_ii])
                density[uc_index] += 1 - qq[uc_index]
            except IndexError:
                for jj, uc_below in enumerate(uc_below_ii):
                    uc_index = np.argmax(x[0, uc_below])
                    density[jj, uc_index] += 1 - qq[jj, uc_index]
        density[(x < lc) | (x > uc)] = 0
        return density

    def _cdf(self, x, *args, **kwds):
        x = np.asarray(x)
        args = [np.asarray(arg) for arg in args]
        lc = self.lc if "lc" not in kwds else kwds.pop("lc")
        uc = self.uc if "uc" not in kwds else kwds.pop("uc")
        qq = np.atleast_1d(self.dist.cdf(x, *args, **kwds))
        below_lc_ii = np.atleast_1d(x <= lc)
        if np.any(below_lc_ii):
            lc_quantile = self.dist.cdf(lc, *args, **kwds)
            if np.isscalar(lc_quantile):
                lc_quantile = np.full_like(x, lc_quantile)
            qq[below_lc_ii] = lc_quantile[below_lc_ii]
        above_uc_ii = np.atleast_1d(x >= uc)
        if np.any(above_uc_ii):
            uc_quantile = self.dist.cdf(uc, *args, **kwds)
            if np.isscalar(uc_quantile):
                uc_quantile = np.full_like(x, uc_quantile)
            qq[above_uc_ii] = 1 - uc_quantile[above_uc_ii]
        qq[qq < 0] = 0
        qq[qq > 1] = 1
        return np.squeeze(qq)

    def _ppf(self, qq, *args, **kwds):
        qq = np.asarray(qq)
        args = [np.asarray(arg) for arg in args]
        lc = self.lc if "lc" not in kwds else kwds["lc"]  # kwds.pop("lc")
        uc = self.uc if "uc" not in kwds else kwds["uc"]  # kwds.pop("uc")
        x = self.dist.ppf(qq, *args, **kwds)
        lower_ii = x < lc
        if np.any(lower_ii):
            x[lower_ii] = (np.full_like(x, lc) if np.isscalar(lc) else lc)[
                lower_ii
            ]
        upper_ii = x > uc
        if np.any(upper_ii):
            x[upper_ii] = (np.full_like(x, uc) if np.isscalar(uc) else uc)[
                upper_ii
            ]
        return x

    def _fit(self, x, *args, **kwds):
        # try:
        #     x0 = self.dist._feasible_start
        # except AttributeError:
        #     x0 = self.dist.fit(x, *args, **kwds)
        # using the cdf to fit, because of the insanity inherent in the pdf
        # (i.e. no maximum likelihood)
        # return min_ks(self.cdf, x0, x, lc=self.lc, uc=self.uc, *args)
        # fit on valid x
        valid_x = x[~self._invalid_x(x, *args, **kwds)]
        return self.dist.fit(
            valid_x,
            *args,
            **{
                key: val
                for key, val in list(kwds.items())
                if key != "lc" and key != "uc"
            },
        )

    def constraints(self, x, *args, **kwds):
        return self.dist.constraints(x, *args, **kwds)


class Truncated(Dist):
    """Wraps around any other distribution found here to truncate its upper
    and/or lower tail."""

    def __init__(self, distribution, lc=-np.inf, uc=np.inf):
        self.dist = distribution
        self.lc = lc
        self.uc = uc
        # read "lc" as "lower cut", "uc" as "upper cut"
        # mind you: these are different than lower and upper bound in i.e. the
        # beta distribution. we can also truncate the beta distribution and
        # have upper bounds above the upper cut.
        self.parameter_names = tuple(
            list(distribution.parameter_names) + ["lc", "uc"]
        )
        self.name = "truncated " + distribution.name

    def _pdf(self, x, *args, **kwds):
        args = [np.asarray(arg) for arg in args]
        if "uc" in kwds:
            uc = kwds.pop("uc")
            if len(args) > 0:
                del args[-1]
        else:
            uc = self.uc
        if "lc" in kwds:
            lc = kwds.pop("lc")
            if len(args) > 0:
                del args[-1]
        else:
            lc = self.lc
        if len(args) > len(self.dist.parameter_names):
            args = args[: len(self.dist.parameter_names)]
        # still sober
        un_trunc = self.dist.pdf(x, *args, **kwds) / (
            self.dist.cdf(uc, *args, **kwds) - self.dist.cdf(lc, *args, **kwds)
        )
        un_trunc[(x < lc) | (x > uc)] = 1e-9
        # now totally drunkated
        return un_trunc

    def _cdf(self, x, *args, **kwds):
        args = [np.asarray(arg) for arg in args]
        if "uc" in kwds:
            uc = kwds.pop("uc")
            if len(args) > 0:
                del args[-1]
        else:
            uc = self.uc
        if "lc" in kwds:
            lc = kwds.pop("lc")
            if len(args) > 0:
                del args[-1]
        else:
            lc = self.lc
        if len(args) > len(self.dist.parameter_names):
            args = args[: len(self.dist.parameter_names)]
        qq = (
            self.dist.cdf(x, *args, **kwds) - self.dist.cdf(lc, *args, **kwds)
        ) / (
            self.dist.cdf(uc, *args, **kwds) - self.dist.cdf(lc, *args, **kwds)
        )
        qq[qq < 0] = 0
        qq[qq > 1] = 1
        return qq

    def _ppf(self, qq, *args, **kwds):
        args = [np.asarray(arg) for arg in args]
        if "uc" in kwds:
            uc = kwds.pop("uc")
            if len(args) > 0:
                del args[-1]
        else:
            uc = self.uc
        if "lc" in kwds:
            lc = kwds.pop("lc")
            if len(args) > 0:
                del args[-1]
        else:
            lc = self.lc
        if len(args) > len(self.dist.parameter_names):
            args = args[: len(self.dist.parameter_names)]
        x = self.dist.ppf(
            qq * self.dist.cdf(uc, *args, **kwds)
            + (1 - qq) * self.dist.cdf(lc, *args, **kwds),
            *args,
            **kwds,
        )
        lower_ii = x < lc
        if np.sum(lower_ii) > 0:
            x[lower_ii] = lc[lower_ii]
        upper_ii = x > uc
        if np.sum(upper_ii) > 1:
            x[upper_ii] = uc[upper_ii]
        return x

    def _fit(self, x, *args, **kwds):
        try:
            x0 = self.dist._feasible_start
        except AttributeError:
            x0 = self.dist.fit(x, *args)
        # using the cdf to fit, because of the insanity inherent in the pdf
        # (i.e. no maximum likelihood)
        if self.lc is None:
            x0 = tuple(list(x0) + [np.min(x)])
            kwds["lc"] = x0[-1]
        else:
            kwds["lc"] = self.lc
        if self.uc is None:
            x0 = tuple(list(x0) + [np.max(x)])
            kwds["uc"] = x0[-1]
        else:
            kwds["uc"] = self.uc
        solution = list(min_ks(self.cdf, x0, x, *args, **kwds))
        if self.uc is None:
            self.uc = solution.pop(-1)
        if self.lc is None:
            self.lc = solution.pop(-1)
        if "lc" in kwds:
            solution += [kwds["lc"]]
        if "uc" in kwds:
            solution += [kwds["uc"]]
        return solution

    def constraints(self, x, *args, **kwds):
        return self.dist.constraints(x, *args, **kwds)


class Normal(Dist):
    _feasible_start = (0, 2)

    def _pdf(self, x, mu=0, sigma=1):
        x, mu, sigma = np.atleast_1d(x, mu, sigma)
        if NE:
            _pi = np.pi
            dens = ne.evaluate(
                "(1. / (2 * _pi * sigma ** 2) ** .5 *"
                + "exp(-(x - mu) ** 2 / (2 * sigma ** 2)))"
            )
        else:
            dens = (
                1.0
                / (2 * np.pi * sigma**2) ** 0.5
                * np.exp(-((x - mu) ** 2) / (2 * sigma**2))
            )
        return dens

    def _cdf(self, x, mu=0, sigma=1):
        x, mu, sigma = np.atleast_1d(x, mu, sigma)
        qq = 0.5 * (1 + special.erf((x - mu) / (sigma * 2**0.5)))
        return qq

    def _ppf(self, qq, mu=0, sigma=1):
        qq, mu, sigma = np.atleast_1d(qq, mu, sigma)
        x = special.ndtri(qq) * sigma + mu
        return x

    def _fit(self, x, mu=None, sigma=None):
        x_noninf = x[np.isfinite(x)]
        if mu is None:
            mu = x_noninf.mean()
        if sigma is None:
            sigma = x_noninf.std()
        return mu, sigma


norm = Normal()

# class SkewNormal(Dist):
#     """This is taken and adapted from
#     http://promanthan.com/randomstuff/skewt-0.0.1.tgz
#     which might end up in scipy.stats"""
#     @staticmethod
#     def _fui(h, i):
#         return (h ** (2 * i)) / ((2 ** i) * gamma(i + 1))
#
#     @staticmethod
#     def _tInt(h, a, jmax, cutPoint):
#         seriesL = np.empty(0)
#         seriesH = np.empty(0)
#         i = np.arange(0, jmax + 1)
#         low = h <= cutPoint
#         hL = h[low]
#         hH = h[np.logical_not(low)]
#         L = hL.size
#         if L > 0:
#             b = SkewNormal._fui(hL[:, np.newaxis], i)
#             cumb = b.cumsum(axis=1)  # transposed compared to R code
#             b1 = np.exp(-0.5 * hL ** 2)[:, np.newaxis] * cumb
#             matr = np.ones((jmax + 1, L)) - b1.transpose()
#             jk = ([1.0, -1.0] * jmax)[0:jmax + 1] / (2 * i + 1)
#             matr = np.inner((jk[:, np.newaxis] * matr).transpose(),
#                             a ** (2 * i + 1.0))
#             seriesL = (np.arctan(a) - matr.flatten(1)) / (2 * np.pi)
#         if hH.size > 0:
#             seriesH = (np.arctan(a) *
#                        np.exp(-0.5 * (hH ** 2.0) * a / np.arctan(a))
#                         * (1 + 0.00868 * (hH ** 4.0) * a ** 4.0) /
#                         (2.0 * np.pi))
#         series = np.empty(h.size)
#         series[low] = seriesL
#         series[np.logical_not(low)] = seriesH
#         return series
#
#     @staticmethod
#     def _tOwen(h, a, jmax=50, cutPoint=6):
#         aa = np.abs(a)
#         ah = np.abs(h)
#         if np.isnan(aa):
#             raise ValueError("a is NaN")
#         if np.isposinf(aa):
#             return 0.5 * norm().cdf(-ah)
#         if aa == 0.0:
#             return np.zeros(h.size)
#         na = np.isnan(h)
#         inf = np.isposinf(ah)
#         ah[np.logical_or(na, inf)] = 0
#         if aa <= 1:
#             owen = SkewNormal._tInt(ah, aa, jmax, cutPoint)
#         else:
#             owen = (0.5 * norm().cdf(ah) + norm().cdf(aa * ah)
#                     * (0.5 - norm().cdf(ah)) -
#                     SkewNormal._tInt(aa * ah, (1.0 / aa), jmax, cutPoint))
#         owen[np.isposinf(owen)] = 0
#         return owen * np.sign(a)
#
#     def _pdf(self, x, zeta, omega, alpha):
#         x, zeta, omega, alpha = np.atleast_1d(x, zeta, omega, alpha)
#         return (1 / (omega * np.pi) *
#                 np.exp(-(x - zeta) ** 2 / (2 * omega ** 2))
#                 * norm.cdf(alpha * (x - zeta) / omega))
#
#     def _cdf(self, x, zeta, omega, alpha):
#         x, zeta, omega, alpha = np.atleast_1d(x, zeta, omega, alpha)
#         return (norm.cdf((x - zeta) / omega) -
#                 2 * SkewNormal._tOwen((x - zeta) / omega, alpha))
#
#     def _ppfInternal(self, qq, shape):
#         maxQ = np.sqrt(chi2.ppf(qq, 1))
#         minQ = -np.sqrt(chi2.ppf(1 - qq, 1))
#         if shape > 1e+5:
#             return maxQ
#         if shape < -1e+5:
#             return minQ
#         nan = np.isnan(qq) | (qq > 1) | (qq < 0)
#         zero = qq == 0
#         one = qq == 1
#         qq[nan | zero | one] = 0.5
#         cum = SkewNormal._cumulants(shape, 4)
#         g1 = cum[2] / cum[1] ** (3 / 2.0)
#         g2 = cum[3] / cum[1] ** 2
#         x = norm().ppf(qq)
#         x = (x + (x ** 2 - 1) * g1 / 6 + x * (x ** 2 - 3) * g2 / 24 -
#              x * (2 * x ** 2 - 5) * g1 ** 2 / 36)
#         x = cum[0] + np.sqrt(cum[1]) * x
#         tol = 1e-8
#         maxErr = 1
#         while maxErr > tol:
#             sn = skewnorm(shape)
#             x1 = x - (sn.cdf(x) - qq) / (sn.pdf(x))
#             x1 = np.minimum(x1, maxQ)
#             x1 = np.maximum(x1, minQ)
#             maxErr = np.amax(np.abs(x1 - x) / (1 + np.abs(x)))
#             x = x1
#         x[nan] = np.NaN
#         x[zero] = -np.Infinity
#         x[one] = np.Infinity
#         return x
#
#     def _ppf(self, qq, shape):
#         if np.all(shape == shape[0]):
#             return self._ppfInternal(qq, shape[0])
#         else:
#             vec = np.vectorize(lambda qq, shape:
#                                 self._ppfInternal(np.array([qq]), shape))
#             return vec(qq, shape)
#
#     def _ppf(self, qq, zeta, omega, alpha):
#         qq, zeta, omega, alpha = \
#             np.atleast_1d(qq, zeta, omega, alpha)
#         return self._ppf(qq, alpha) * omega + zeta


# class SkewNormal(Dist):
#     _feasible_start = (0, 1, .5)

#     def _pdf(self, x, zeta=0, omega=1, alpha=0):
#         x, zeta, omega, alpha = np.atleast_1d(x, zeta, omega, alpha)
#         t = (x - zeta) / omega
#         dens = 2. / omega * norm.pdf(t) * norm.cdf(t * alpha)
#         return dens

#     def _cdf(self, x, zeta=0, omega=1, alpha=0):
#         x, zeta, omega, alpha = np.atleast_1d(x, zeta, omega, alpha)
#         t = (x - zeta) / omega
#         qq = norm.cdf(t) - 2. * owens_t(t, alpha)
#         return qq

#     def _ppf(self, qq, zeta=0, omega=1, alpha=0):
#         qq, zeta, omega, alpha = \
#             np.atleast_1d(qq, zeta, omega, alpha)
#         if len(zeta) == 1:
#             zeta = np.full_like(qq, zeta)
#         if len(omega) == 1:
#             omega = np.full_like(qq, omega)
#         if len(alpha) == 1:
#             alpha = np.full_like(qq, alpha)

#         qq0 = norm.cdf(qq) * omega + zeta
#         x = np.empty_like(qq)
#         for i in range(len(qq0)):
#             q_exp = qq[i]

#             def error(x):
#                 q_act = self.cdf(x, zeta[i], omega[i], alpha[i])
#                 return (q_act - q_exp) ** 2

#             x[i] = sp_optimize.minimize(error, qq0[i],
#                                         method="Nelder-Mead"
#                                         )["x"]
#         return x

#     def _fit(self, x, skew_max=.9):
#         skew = stats.skew(x)
#         skew_23 = np.abs(skew)**(2. / 3)
#         if not (-skew_max < skew < skew_max):
#             delta = np.sqrt(np.pi / 2 * skew_max**(2. / 3) /
#                             (skew_max**(2. / 3) + (
#                                 (4 - np.pi) / 2)**(2. / 3)))
#             delta = np.copysign(delta, skew)

# #             warnings.warn("Sample skew %.2f not in feasible range ~(-1,1)!" %
# #                           skew)
#         else:
#             delta = np.sqrt(np.pi / 2 * skew_23 / (skew_23 + (
#                 (4 - np.pi) / 2)**(2. / 3)))
#         delta = np.copysign(delta, skew)
#         alpha = delta / np.sqrt(1 - delta**2)
#         omega = np.sqrt(np.var(x) / (1 - 2 * delta**2 / np.pi))
#         zeta = np.mean(x) - omega * np.sqrt(2 / np.pi) * delta
#         return self.fit_ml(x, x0=(zeta, omega, alpha)).x
#         if not (-skew_max < skew < skew_max):
#             zeta, omega, alpha = self.fit_ml(
#                 x, x0=(zeta, omega, alpha / 2))
#             delta = alpha / np.sqrt(1 + alpha**2)
#         return zeta, omega, alpha
# if owens:
#     skewnorm = SkewNormal()


# class ExGauss(Dist):
#     _feasible_start = (0, 1, 1)
#     _bounds = [(-np.inf, np.inf),
#                (1e-9, np.inf),
#                (1e-9, np.inf)]

#     def _pdf(self, x, mu, sigma, gamma):
#         x, mu, sigma, gamma = np.atleast_1d(x, mu, sigma, gamma)
#         gamma_sigma_sqr = gamma * sigma ** 2
#         return (gamma / 2 *
#            np.exp(gamma / 2 * (2 * mu + gamma_sigma_sqr - 2 * x)) *
#            special.erfc((mu + gamma_sigma_sqr - x) / (sqrt2 * sigma)))

#     def _cdf(self, x, mu, sigma, gamma):
#         x, mu, sigma, gamma = np.atleast_1d(x, mu, sigma, gamma)
#         return norm.cdf(x, mu, sigma) - self._pdf(x, mu, sigma, gamma) / gamma

#     def _ppf(self, q, mu, sigma, gamma):
#         q, mu, sigma, gamma = np.atleast_1d(q, mu, sigma, gamma)


class TruncatedNormal(Dist):
    _feasible_start = (0, 1, -1, 1)
    _additional_kwds = {"lc": -1, "uc": 1}
    _bounds = [(-np.inf, np.inf), (1e-9, np.inf)]

    def _pdf(self, x, mu=0, sigma=1, lc=-np.inf, uc=np.inf):
        x, mu, sigma, lc, uc = np.atleast_1d(x, mu, sigma, lc, uc)
        if (x.shape != lc.shape) or (x.shape != uc.shape):
            x, mu, sigma, lc, uc = np.broadcast_arrays(x, mu, sigma, lc, uc)
        un_trunc = np.atleast_1d(
            norm.pdf(x, mu, sigma)
            / (norm.cdf(uc, mu, sigma) - norm.cdf(lc, mu, sigma))
        )
        un_trunc[(x < lc) | (x > uc)] = 0
        sigma_neg_mask = sigma <= 0
        if np.any(sigma_neg_mask):
            if sigma.size == 1:
                un_trunc += sigma**2
            else:
                un_trunc[sigma_neg_mask] += sigma[sigma_neg_mask] ** 2
        return un_trunc

    def _cdf(self, x, mu=0, sigma=1, lc=-np.inf, uc=np.inf):
        x, mu, sigma, lc, uc = np.atleast_1d(x, mu, sigma, lc, uc)
        if (x.shape != lc.shape) or (x.shape != uc.shape):
            x, mu, sigma, lc, uc = np.broadcast_arrays(x, mu, sigma, lc, uc)
        qq = (norm.cdf(x, mu, sigma) - norm.cdf(lc, mu, sigma)) / (
            norm.cdf(uc, mu, sigma) - norm.cdf(lc, mu, sigma)
        )
        qq = np.atleast_1d(qq)
        with warnings.catch_warnings():
            warnings.simplefilter("ignore", RuntimeWarning)
            qq[qq < 0] = 0
            qq[qq > 1] = 1
        return qq

    def _ppf(self, qq, mu=0, sigma=1, lc=-np.inf, uc=np.inf):
        qq, mu, sigma, lc, uc = np.atleast_1d(qq, mu, sigma, lc, uc)
        if (qq.shape != lc.shape) or (qq.shape != uc.shape):
            qq, mu, sigma, lc, uc = np.broadcast_arrays(qq, mu, sigma, lc, uc)
        x = norm.ppf(
            qq * norm.cdf(uc, mu, sigma) + (1 - qq) * norm.cdf(lc, mu, sigma),
            mu,
            sigma,
        )
        x = np.atleast_1d(x)
        lower_ii = x < lc
        if np.sum(lower_ii) > 0:
            x[lower_ii] = lc[lower_ii]
        upper_ii = x > uc
        if np.sum(upper_ii) > 1:
            x[upper_ii] = uc[upper_ii]
        return x

    def _fit(self, x, lc=-np.inf, uc=np.inf):
        x0 = [np.nanmean(x), np.nanstd(x)]
        kwds = {}
        if lc is None:  # or np.isneginf(lc):
            x0 += [np.nanmin(x)]
        else:
            kwds["lc"] = lc
        if uc is None:  # or np.isinf(uc):
            x0 += [np.nanmax(x)]
        else:
            kwds["uc"] = uc
        return self.fit_ml(x, x0=x0, method="Nelder-Mead", **kwds).x

    def _constraints(self, x, mu, sigma, lc=-np.inf, uc=np.inf, **kwds):
        mask = (x > uc) | (x < lc) | (sigma <= 0)
        return mask


truncnorm = TruncatedNormal()


class LogNormal(Dist):
    _feasible_start = (2.0, 0.5)

    def _pdf(self, x, mu, sigma):
        return norm.pdf(np.log(x), mu, sigma) / x

    def _cdf(self, x, mu, sigma):
        return norm.cdf(np.log(x), mu, sigma)

    def _ppf(self, qq, mu, sigma):
        return np.exp(norm.ppf(qq, mu, sigma))

    def _fit(self, x):
        x_noninf = x[(x > 0) & np.isfinite(x)]
        mu = np.mean(np.log(x_noninf))
        sigma = np.std(np.log(x_noninf))
        return mu, sigma

    def _constraints(self, x, mu, sigma):
        mask = (x <= 0) | (sigma <= 0)
        return mask


lognormal = LogNormal()


class JohnsonSU(Dist):
    _feasible_start = (1, 1, 0, 1)

    def _pdf(self, x, a, b, loc, scale):
        x, a, b, loc, scale = np.atleast_1d(x, a, b, loc, scale)
        x = (x - loc) / scale
        x2 = x * x
        trm = norm.pdf(a + b * np.log(x + np.sqrt(x2 + 1)))
        dens = b * 1.0 / np.sqrt(x2 + 1.0) * trm / scale
        return dens

    def _cdf(self, x, a, b, loc, scale):
        x = np.atleast_1d(x - loc) / scale
        qq = np.atleast_1d(norm.cdf(a + b * np.log(x + np.sqrt(x * x + 1))))
        qq[np.isneginf(x)] = 0
        return qq

    def _ppf(self, qq, a, b, loc, scale):
        qq, a, b, loc, scale = np.atleast_1d(qq, a, b, loc, scale)
        z = np.sinh((norm.ppf(qq) - a) / b)
        x = z * scale + loc
        return x

    def _fit(self, x):
        x_noinf = x[np.isfinite(x)]
        loc, scale = x_noinf.mean(), x_noinf.std()
        return self.fit_fsum(x_noinf, x0=(1, 1, loc, scale))

    def _constraints(self, x, a, b, loc, scale):
        return b <= 0


johnsonsu = JohnsonSU()


class Cauchy(Dist):
    _feasible_start = (0, 1)

    def _pdf(self, x, x0, gamma):
        return 1 / (np.pi * gamma * (1 + ((x - x0) / gamma) ** 2))

    def _cdf(self, x, x0, gamma):
        return (1 / np.pi) * np.arctan2(x - x0, gamma) + 0.5

    def _ppf(self, qq, x0, gamma):
        return x0 + gamma * np.tan(np.pi * (qq - 0.5))

    def _fit(self, x):
        x0 = np.median(stats.trimboth(x, 0.38))
        inner_quart = stats.trimboth(x, 0.25)
        inner_range = inner_quart.max() - inner_quart.min()
        return self.fit_ml(x, x0=(x0, 0.5 * inner_range)).x

    def _constraints(self, x, x0, gamma):
        return gamma < 0


cauchy = Cauchy()


class StudentT(Dist):
    _feasible_start = (0, 1)

    def _pdf(self, x, mu=0, df=1):
        x, mu, df = np.atleast_1d(x, mu, df)
        Px = (
            np.exp(gammaln((df + 1) / 2.0) - gammaln(df / 2.0))
            / np.sqrt(df * np.pi)
            * (1 + (x - mu) ** 2 / df) ** (-(df + 1) / 2.0)
        )
        return Px

    def _cdf(self, x, mu=0, df=1):
        qq = stdtr(df, np.atleast_1d(x) - mu)
        return qq

    def _ppf(self, qq, mu=0, df=1):
        qq = np.atleast_1d(qq)
        x = stdtrit(df, qq) + mu
        x[qq == 0] = -np.inf
        x[qq == 1] = np.inf
        return x

    def _fit(self, x):
        x_noninf = x[np.isfinite(x)]
        sigma = np.var(x_noninf)
        df = 2 * sigma / (sigma - 1)
        if df > 6:
            df = 6
        return self.fit_ml(
            x_noninf,
            x0=(np.mean(x_noninf), df if df > 0 else 0.1),
            method="Nelder-Mead",
        ).x

    def _constraints(self, x, mu, df):
        mask = (df <= 0) | (df > 8)
        return mask


student_t = StudentT()


class NoncentralT(Dist):
    _feasible_start = (2, 3, 0)

    # stolen from scipy.stats.distributions.nct
    def _pdf(self, x, df, nc, mu=0):
        x, df, nc = (np.atleast_1d(var).astype(float) for var in (x, df, nc))
        pdf_x = lambda x: (
            (df / x)
            * (
                self.cdf(x * (1 + 2 / df) ** 0.5, df + 2, nc)
                - self.cdf(x, df, nc)
            )
        )
        dens = np.where(
            np.abs(x - mu) <= 0.5, self._pdf_old(x, df, nc, mu), pdf_x(x - mu)
        )
        dens[np.isinf(x)] = 0
        return dens

    def _pdf_old(self, x, df, nc, mu=0):
        n = df * 1.0
        nc = nc * 1.0
        x2 = (x - mu) ** 2
        ncx2 = nc * nc * x2
        fac1 = n + x2
        trm1 = (
            n / 2.0 * np.log(n)
            + gammaln(n + 1)
            - (
                n * np.log(2)
                + nc * nc / 2.0
                + (n / 2.0) * np.log(fac1)
                + gammaln(n / 2.0)
            )
        )
        Px = np.exp(trm1)
        valF = ncx2 / (2 * fac1)
        trm1 = (
            sqrt2
            * nc
            * (x - mu)
            * hyp1f1(n / 2 + 1, 1.5, valF)
            / (np.asarray(fac1 * gamma_func((n + 1) / 2)))
        )
        trm2 = hyp1f1((n + 1) / 2, 0.5, valF) / (
            np.asarray(np.sqrt(fac1) * gamma_func(n / 2 + 1))
        )
        Px *= trm1 + trm2
        return Px

    def _cdf(self, x, df, nc, mu=0):
        x, df, nc, mu = (
            np.atleast_1d(var).astype(float) for var in (x, df, nc, mu)
        )
        qq = nctdtr(df, nc, x - mu)
        return qq

    def _ppf(self, qq, df, nc, mu=0):
        qq, df, nc = (np.atleast_1d(var).astype(float) for var in (qq, df, nc))
        x = nctdtrit(df, nc, qq) + mu
        x[qq == 0] = -np.inf
        x[qq == 1] = np.inf
        return x

    def _fit(self, x):
        x_noninf = x[np.isfinite(x)]
        mode = stats.mode(np.round(x_noninf, 1)).mode
        x0 = (1, 2 * stats.skew(x_noninf), mode)
        df, nc, mu = self.fit_ml(x_noninf, x0=x0, method="Nelder-Mead").x
        #        if df >= 30:
        #            # then we are normal anyway
        #            df = 30
        return df, nc, mu

    def _constraints(self, x, df, nc, mu):
        mask = (df < 0.1) | (df > 8)
        return mask


noncentral_t = NoncentralT()

# TODO: implement ppf
# class LogitNormal(Dist):
#    def logit(self, x):
#        return np.log(x / (1 - x))
#
#    def _pdf(self, x, mu, sigma, l=0, u=1):
#        x_normed = (x - l) / (u - l)
#        logit = self.logit(x_normed)
#        return norm_pdf(logit, mu, sigma) / (x_normed * (1 - x_normed))
#
#    def _cdf(self, x, mu, sigma, l=0, u=1):
#        x_normed = (x - l) / (u - l)
#        logit = self.logit(x_normed)
#        return norm_cdf(logit, mu, sigma)
#
#    def _ppf(self, qq, mu, sigma, l=0, u=1):
#        raise NotImplementedError
#
#    def _fit(self, x, l=None, u=None):
#        l = x.min() - 1e-9 if l is None else l
#        u = x.max() + 1e-9 if u is None else u
#        x_normed = (x - l) / (u - l)
#        logit = self.logit(x_normed)
#        mu, sigma = norm_fit(logit)
#        return mu, sigma, l, u
#
#    def _constraints(self, x, mu, sigma, l=0, u=1):
#        x, mu, sigma, l, u = (np.asarray(var) for var in (x, mu, sigma, l, u))
#        if np.any(x < l):
#            return False
#        if np.any(x > u):
#            return False
#        return True
# logitnormal = LogitNormal()

# class Rayleigh(Dist):
#    def _pdf(self, x, sigma):
#        return (x / sigma ** 2) * np.exp(-x ** 2 / (2 * sigma ** 2))
#
#    def _cdf(self, x, sigma):
#        return 1 - np.exp(-x ** 2 / (2 * sigma ** 2))
#
#    def _fit(self, x):
#        return ((.5 * np.average(x ** 2)) ** .5,)
#
#    @abstractmethod
#    def _ppf(self):
#        raise NotImplementedError
#
#    def _constraints(self, x, sigma):
#        if np.any(sigma <= 0):
#            return False
#        return True
# rayleigh = Rayleigh()

# TODO: implement ppf
# class RayleighLU(Dist):
#    def _pdf(self, x, sigma, l, u):
#        x_normed = (x - l) / (u - l)
#        return (x_normed / sigma ** 2) * \
#            np.exp(-x_normed ** 2 / (2 * sigma ** 2))
#
#    def _cdf(self, x, sigma, l, u):
#        x_normed = (x - l) / (u - l)
#        return 1 - np.exp(-x_normed ** 2 / (2 * sigma ** 2))
#
#    def _fit(self, x, l=None, u=None):
#        l = x.min() if l is None else l
#        u = x.max() if u is None else u
#        x_normed = (x - l) / (u - l)
#        return (.5 * np.average(x_normed ** 2)) ** .5, l, u
#
#    def _ppf(self):
#        raise NotImplementedError
#
#    def _constraints(self, x, sigma, l, u):
#        if np.any(sigma <= 0):
#            return False
#        if np.any(l >= u):
#            return False
#        return True
# rayleighlu = RayleighLU()

# TODO: fit does not work well
# class LogNormalLU(Dist):
#    def _pdf(self, x, mu, sigma, l, u):
#        x_normed = (x - l) / (u - l)
#        return norm_pdf(np.log(x_normed + 1e-9), mu, sigma) / (u - l)
#
#    def _cdf(self, x, mu, sigma, l, u):
#        x_normed = (x - l) / (u - l)
#        return norm_cdf(np.log(x_normed + 1e-9), mu, sigma)
#
#    def _ppf(self, qq, mu, sigma, l, u):
#        return np.exp(norm_ppf(qq, mu, sigma)) * (u - l) + l
#
#    def _fit(self, x, l=None, u=None):
#        x_noninf = x[np.isfinite(x)]
#        l = x_noninf.min() if l is None else l
#        u = x_noninf.max() if u is None else u
#        x_normed = (x_noninf - l) / (u - l)
#        mu, sigma = norm_fit(np.log(x_normed + 1e-9))
#        return self.fit_ml(x_noninf, x0=(mu, sigma, l, u))
#
#    def _constraints(self, x, mu, sigma, l, u):
#        if np.any(l >= u):
#            return False
#        return True
# lognormallu = LogNormalLU()
# lognormallu._feasible_start = (0, 1, -1, 1)

# class WeibullLU(Dist):
#    def _pdf(self, x, alpha, beta, l=0, u=1):
#        x_normed = (x - l) / (u - l)
#        # alpha and beta should be positive
#        densities = alpha * beta * x_normed ** (beta - 1) * \
#            np.exp(-alpha * x_normed ** beta)
#        return densities / (u - l)
#
#    def _cdf(self, x, alpha, beta, l=0, u=1):
#        x_normed = (x - l) / (u - l)
#        return 1 - np.exp(-alpha * x_normed ** beta)
#
#    def _ppf(self, qq, alpha, beta, l=0, u=1):
#        x = (-np.log(1 - qq) / alpha) ** (1 / beta)
#        return x * (u - l) + l
#
#    def _fit(self, values, beta_start=10, *args, **kwds):
#        """Implements a semi-analytical method of moments. beta is found by
#        minimizing errors. After that, alpha can be estimated with the help of
#        beta.
#        See
#        http://interstat.statjournals.net/YEAR/2000/articles/0010001.norm_pdf
#        """
#        values_noninf = values[np.isfinite(values)]
#        l = values_noninf.min()
#        u = values_noninf.max()
#        values_noninf = (values_noninf - l) / (u - l)
#        # we need the coefficient of variation now and the mean later. so do
#        # not use stats.variation in order to not calculate the mean twice.
#        mean = np.mean(values_noninf)
#        cv = np.std(values_noninf) / mean
#
#        def cv_error(beta):
#            """Squared error between the theoretical and empirical coefficient
#            of variation."""
#            gamma1 = special.gamma(1 + 2 / beta)
#            gamma2 = special.gamma(1 + 1 / beta)
#            return (cv - (gamma1 - gamma2 ** 2) ** .5 / gamma2) ** 2
#        beta = sp_optimize.fmin(cv_error, [beta_start], disp=False)[0]
#        alpha = (mean / special.gamma(1 / beta + 1)) ** (-beta)
#        return alpha, beta, l, u
#
#    def _constraints(self, x, alpha, beta, l, u):
#        x, alpha, beta = (np.asarray(var) for var in (x, alpha, beta))
#        if np.any(alpha <= 0):
#            return False
#        if np.any(beta <= 0):
#            return False
#        if np.any(x < l):
#            return False
#        if np.any(x > u):
#            return False
#        return True
# weibulllu = WeibullLU()
# weibulllu._feasible_start = (1, 1, -1, 1)


class Weibull(Dist):
    _feasible_start = (0.5, 2.0)

    def _pdf(self, x, alpha, beta):
        # alpha and beta should be positive
        densities = alpha * beta * x ** (beta - 1) * np.exp(-alpha * x**beta)
        return densities

    def _cdf(self, x, alpha, beta):
        return 1 - np.exp(-alpha * x**beta)

    def _ppf(self, qq, alpha, beta):
        return (-np.log(1 - qq) / alpha) ** (1.0 / beta)

    def _fit(self, values, beta_start=10, *args, **kwds):
        """Implements a semi-analytical method of moments. beta is found by
        minimizing errors. After that, alpha can be estimated with the help of
        beta.
        See
        http://interstat.statjournals.net/YEAR/2000/articles/0010001.norm_pdf
        """
        # we need the coefficient of variation now and the mean later. so do
        # not use stats.variation in order to not calculate the mean twice.
        mean = np.nanmean(values)
        cv = np.nanstd(values) / mean

        def cv_error(beta):
            """Squared error between the theoretical and empirical coefficient
            of variation."""
            gamma1 = special.gamma(1 + 2 / beta)
            gamma2 = special.gamma(1 + 1 / beta)
            return (cv - (gamma1 - gamma2**2) ** 0.5 / gamma2) ** 2

        beta = sp_optimize.fminbound(cv_error, -1, 1e6, disp=False)
        alpha = (mean / special.gamma(1 / beta + 1)) ** (-beta)
        return alpha, beta

    def _constraints(self, x, alpha, beta):
        mask = (x < 0) | (alpha <= 0) | (beta <= 0)
        return mask


weibull = Weibull()


class Kumaraswamy(Dist):
    """Resembles the Beta distribution, but does not need a transcendental
    function."""

    _feasible_start = (2, 2, 0, 1)

    def _pdf(self, x, a, b, l=0, u=1):
        x = np.atleast_1d(x).astype(float)
        if NE:
            x = ne.evaluate("(x - l) / (u - l)")
            dens = ne.evaluate(
                "a * b * x ** (a - 1) * (1 - x ** a) ** " + "(b - 1) / (u - l)"
            )
        else:
            x = (x - l) / (u - l)
            dens = a * b * x ** (a - 1) * (1 - x**a) ** (b - 1) / (u - l)
        return dens

    def _cdf(self, x, a, b, l=0, u=1):
        x = np.atleast_1d(x).astype(float)
        if np.any((x < l) | (x > u)):
            warnings.warn("Some values below lower or above upper bounds.")
            x = np.where(x > u, u, x)
            x = np.where(x < l, l, x)
        return 1 - (1 - ((x - l) / (u - l)) ** a) ** b

    def _ppf(self, qq, a, b, l=0, u=1):
        qq, a, b, l, u = np.atleast_1d(qq, a, b, l, u)
        if NE:
            x = ne.evaluate(
                "((1 - (1 - qq) ** (1 / b)) ** " + "(1 / a)) * (u - l) + l"
            )
        else:
            x = ((1 - (1 - qq) ** (1.0 / b)) ** (1.0 / a)) * (u - l) + l
        return x

    def _fit(self, x, l=None, u=None):
        # return beta.fit(x, l, u)
        x0 = beta.fit(x, l, u)
        par_bounds = [(1e-9, np.inf), (1e-9, np.inf)]
        # x0 will have entries for l and u if those where None
        if l is None:
            par_bounds += [(-np.inf, x0[-2 if u is None else -1])]
        if u is None:
            par_bounds += [(x0[-1], np.inf)]
        return max_likelihood(self.pdf, x0, x, bounds=par_bounds).x

    def _constraints(self, x, a, b, l=0, u=1):
        mask = (a < 0) | (b < 0) | (x <= l) | (x > u)
        return mask


kumaraswamy = Kumaraswamy()


class Beta(Dist):
    """Beta distribution on the interval [l, u]."""

    _feasible_start = (1, 1, 0, 1)

    def _pdf(self, x, alpha, beta, l=0, u=1):
        x, alpha, beta, l, u = list(map(np.asarray, (x, alpha, beta, l, u)))
        # putting the values into [0, 1] according to [l, u]
        # we avoid the value of exactly 0 or 1 to not get numerical problems
        #        x_normed = (x - l) / (u - l)
        #        x = np.copy(x_normed)
        #        ll, uu = sys.float_info.min, 1 - sys.float_info.min
        #        x_normed = np.where(x_normed >= uu, uu, x_normed)
        #        x_normed = np.where(x_normed <= ll, ll, x_normed)
        if NE:
            x = ne.evaluate("(x - l) / (u - l)")
            return (
                1
                / special.beta(alpha, beta)
                * ne.evaluate(
                    "x ** (alpha - 1) * (1 - x) ** (beta - 1) / " + "(u - l)"
                )
            )
        else:
            x = (u - l) / (u - l)
            return (
                1
                / special.beta(alpha, beta)
                * x ** (alpha - 1)
                * (1 - x) ** (beta - 1)
                / (u - l)
            )

    def _cdf(self, x, alpha, beta, l=0, u=1):
        if np.any((x < l) | (x > u)):
            warnings.warn("Some values below lower or above upper bounds.")
        # note: the betainc function below really delivers the regularized
        # incomplete beta function
        return special.betainc(alpha, beta, (x - l) / (u - l))

    def _ppf(self, qq, alpha, beta, l=0, u=1):
        return (u - l) * special.betaincinv(alpha, beta, qq) + l

    def _fit(self, x, l=None, u=None):
        """stolen from
        http://en.wikipedia.org/wiki/Beta_distribution#Parameter_estimation."""
        # remember whether l and u where given. if they were, we do
        # not return them
        if l is None:
            l = x.min()
            return_l = True
        else:
            return_l = False
        if u is None:
            u = x.max()
            return_u = True
        else:
            return_u = False

        # have to make sure that the parameters all have the same lengths
        try:
            ll = np.atleast_1d(np.empty_like(u))
            ll[:] = l
            l = ll
        except (TypeError, ValueError):
            pass
        try:
            uu = np.empty_like(l)
            uu[:] = u
            u = uu
        except (TypeError, ValueError):
            pass

        xmean = (x.mean() - l) / (u - l)
        xvar = x.var()
        if xvar == 0:
            alpha = np.full_like(u, np.nan)
            beta = np.full_like(u, np.nan)
        else:
            xvar /= (u - l) ** 2
            alpha = xmean * (xmean * (1 - xmean) / xvar - 1)
            beta = (1 - xmean) * alpha / xmean

        return_list = [abs(alpha), abs(beta)]
        if return_l:
            return_list += [l]
        if return_u:
            return_list += [u]
        return np.squeeze(return_list)

    #        return self.fit_ml(x, x0=(alpha, beta, l, u))

    def _constraints(self, x, alpha, beta, l=0, u=1):
        mask = (alpha <= 0) | (beta <= 0) | (x < l) | (x > u)
        return mask


beta = Beta()


class Gamma(Dist):
    """Gamma distribution."""

    _feasible_start = (2, 2)

    def _pdf(self, x, k, theta):
        x = np.atleast_1d(x)
        dens = (
            x ** (k - 1)
            * np.exp(-x / float(theta))
            / (special.gamma(k) * theta**k)
        )
        return dens

    def _cdf(self, x, k, theta):
        x = np.atleast_1d(x)
        qq = gammainc(k, x / theta) / gamma_func(k)
        # qq[np.isinf(x)] = 1
        return qq

    def _ppf(self, qq, k, theta):
        qq = np.atleast_1d(qq)
        x = theta * gammaincinv(k, qq * gamma_func(k))
        # x[qq == 1] = np.inf
        return x

    def _fit(self, x, rel_change=1e-6):
        """A maximum-likelihood estimator. See
        http://en.wikipedia.org/wiki/Gamma_distribution#Parameter_estimation
        """

        def psi(k):
            if k >= 8:
                return np.log(k) - (
                    1.0
                    + (1.0 - (0.1 - 1.0 / (21.0 * k**2)) / k**2) / (6.0 * k)
                ) / (2.0 * k)
            else:
                return psi(k + 1) - 1.0 / k

        def psi_(k):
            if k >= 8:
                return (
                    1.0
                    + (
                        1.0
                        + (1.0 - (1.0 / 5.0 - 1.0 / (7.0 * k**2)) / k**2)
                        / (3.0 * k)
                    )
                    / (2.0 * k)
                ) / k
            else:
                return psi_(k + 1) + 1.0 / k**2

        xmean = x[np.isfinite(x)].mean()
        # we get problems here if there are 0's in x (log(x) = -inf, which
        # happens for wind)
        s = np.log(xmean) - np.mean(np.log(x[np.isfinite(x) & (x > 0)]))
        k_old = (3 - s + ((s - 3) ** 2 + 24 * s) ** 0.5) / (12 * s)
        k_rec = lambda k: k - (np.log(k) - psi(k) - s) / (1.0 / k - psi_(k))
        k_new = k_rec(k_old)
        while abs(k_old - k_new) / k_old > rel_change:
            k_old = k_new
            k_new = k_rec(k_old)
        theta = xmean / k_new
        return self.fit_fsum(x, x0=(k_new, theta))

    def _constraints(self, x, k, theta):
        mask = (k <= 0) | (theta <= 0) | (x < 0)
        return mask


gamma = Gamma()


class Gamma1(Dist):
    _feasible_start = (1, 0.5, 2)

    def _pdf(self, x, a, loc, scale):
        x, a, loc, scale = np.atleast_1d(x, a, loc, scale)
        z = (x - loc) / scale
        dens = (np.exp((a - 1) * np.log(z) - z - gammaln(a))) / scale

        return dens

    def _cdf(self, x, a, loc, scale):
        x, a, loc, scale = np.atleast_1d(x, a, loc, scale)
        z = (x - loc) / scale
        qq = gammainc(a, z)
        qq[np.isinf(x)] = 1
        return qq

    def _ppf(self, qq, a, loc, scale):
        qq, a, loc, scale = np.atleast_1d(qq, a, loc, scale)
        x = gammaincinv(a, qq) * scale + loc
        x[qq == 1] = np.inf
        return x

    def _fit(self, x):
        return stats.gamma.fit(x[np.isfinite(x)])
        # this is stolen from sp.stats.gamma-gen and not understood

    #        x_noninf = x[np.isfinite(x)]
    #        a = 4 / stats.skew(x_noninf) ** 2
    #        muhat = np.array(x_noninf).mean()
    #        mu2hat = np.array(x_noninf).var()
    #        Shat = (mu2hat / a) ** .5
    #        Lhat = muhat - Shat * a
    #        return self.fit_ml(x_noninf, x0=(a, Lhat, Shat))


gamma1 = Gamma1()


class Expon(Dist):
    _feasible_start = (1e-3, 1.0 / 5)

    def _pdf(self, x, x0, lambd):
        return np.where(x > x0, lambd * np.exp(-lambd * (x - x0)), 0)

    def _cdf(self, x, x0, lambd):
        return np.where(x > x0, 1 - np.exp(-lambd * (x - x0)), 0)

    def _ppf(self, qq, x0, lambd):
        return -np.log(1 - qq) / lambd + x0

    def _fit(self, x):
        x_noninf = x[np.isfinite(x)]
        x0 = np.min(x_noninf)
        lambd = 1 / np.mean(x_noninf)
        return x0, lambd

    def _constraints(self, x, x0, lambd):
        return x <= x0


expon = Expon()


class ExponTwo(Dist):
    _feasible_start = (0, 0.5, 1, 1)

    def _pdf(self, x, x0, q0, lambda1, lambda2):
        x = np.atleast_1d(x)
        dens = np.where(
            x <= x0,
            q0 * expon.pdf(x0 - x, 0, lambda1),
            (1 - q0) * expon.pdf(x, x0, lambda2),
        )
        return dens

    def _cdf(self, x, x0, q0, lambda1, lambda2):
        x = np.atleast_1d(x)
        qq = np.where(
            x <= x0,
            q0 * (1 - expon.cdf(x0 - x, 0, lambda1)),
            q0 + (1 - q0) * expon.cdf(x, x0, lambda2),
        )
        return qq

    def _ppf(self, qq, x0, q0, lambda1, lambda2):
        qq = np.atleast_1d(qq)
        x = np.where(
            qq <= q0,
            x0 + np.log(qq / q0) / lambda1,
            x0 - np.log(1 - (qq - q0) / (1 - q0)) / lambda2,
        )
        return x

    def _fit(self, x):
        x_noninf = x[np.isfinite(x)]
        x0 = stats.mode(np.round(x_noninf, 1)).mode
        lambda1 = -1 / np.mean(x_noninf[x_noninf < x0] - x0)
        lambda2 = 1 / np.mean(x_noninf[x_noninf > x0] - x0)
        q0 = (np.argmin(np.abs(np.sort(x_noninf) - x0)) + 0.5) / len(x)
        return self.fit_ml(
            x_noninf, x0=(x0, q0, lambda1, lambda2), method="Nelder-Mead"
        ).x

    def _constraints(self, x, x0, q0, lambda1, lambda2):
        mask = (lambda1 <= 0) | (lambda2 <= 0)
        return mask


expon_two = ExponTwo()


class NoncentralLaplace(Dist):
    _feasible_start = (1, 0.5, 0.5)

    def _pdf(self, x, x0, q0, lambd):
        lambda2 = lambd * q0 / (1 - q0)
        x = np.atleast_1d(x)
        dens = np.where(
            x <= x0,
            q0 * lambd * np.exp(-lambd * (x0 - x)),
            (1 - q0) * lambda2 * np.exp(-lambda2 * (x - x0)),
        )
        dens[np.isinf(x)] = 0
        return dens

    def _cdf(self, x, x0, q0, lambd):
        lambda2 = lambd * q0 / (1 - q0)
        x = np.atleast_1d(x)
        qq = np.where(
            x <= x0,
            q0 * (1 - expon.cdf(x0 - x, 0, lambd)),
            q0 + (1 - q0) * expon.cdf(x, x0, lambda2),
        )
        return qq

    def _ppf(self, qq, x0, q0, lambd):
        lambda2 = lambd * q0 / (1 - q0)
        qq = np.atleast_1d(qq)
        x = np.where(
            qq <= q0,
            x0 + np.log(qq / q0) / lambd,
            x0 - np.log(1 - (qq - q0) / (1 - q0)) / lambda2,
        )
        return x

    def _fit(self, x):
        x0 = stats.mode(np.round(x, 1)).mode
        q0 = (np.argmin(np.abs(np.sort(x) - x0)) + 0.5) / len(x)
        lambd = (1 - 2 * q0) / (q0 * np.mean(x[np.isfinite(x)]))
        if lambd < 0:
            # this actually means that x0 was not right, so provide a feasible
            # lambd and let the ml estimation figure the rest out
            lambd = -1.0 / np.mean(x[(x < x0) & np.isfinite(x)] - x0)
        return self.fit_ks(x, x0=(x0, q0, lambd))

    def _constraints(self, x, x0, q0, lambd):
        mask = (lambd <= 0) | (q0 < 0) | (q0 > 1)
        return mask


noncentral_laplace = NoncentralLaplace()


class MDFt(object):
    """Multiple degrees of freedom t distribution.

    The marginals have different degrees of freedom, therefore this is
    not a "true" Multivariate t distribution.
    See Serban et al 2007.
    """

    @staticmethod
    def _marginal_pdf(x, sigma, df):
        x = np.atleast_1d(np.asarray(x))
        dens = (
            gamma_func((df + 1) / 2.0)
            / (np.sqrt(df * np.pi) * gamma_func(df / 2.0))
            * (1 + x**2 / df) ** (-(df + 1) / 2.0)
        )
        return dens

    @staticmethod
    def loglikelihood(data, sigma, df):
        if np.any(~np.isfinite(sigma)):
            return np.inf
        sigma_inv_sqrt = np.linalg.inv(linalg.sqrtm(sigma))
        yt = np.array(
            [
                np.squeeze(np.asarray(sigma_inv_sqrt[i] @ data))
                / np.sqrt((dfi - 2) / dfi)
                for i, dfi in enumerate(df)
            ]
        )
        marginal_pdfs = [MDFt._marginal_pdf(yt, sigma, dfi) for dfi in df]
        llh = (
            np.sum(np.log(np.sqrt(df - 2) / df))
            + np.sum(list(map(np.log, marginal_pdfs)))
            # is that needed? it should not change the location of
            # the maximum?!
            - 0.5 * np.log(linalg.det(sigma))
        )
        return -llh

    @staticmethod
    def sample(size, sigma, df):
        K = sigma.shape[0]
        if isinstance(df, float):
            df = np.array(K * [df])
        yt = np.array([student_t.sample(size, df=dfi) for dfi in df])
        zt = np.sqrt((df - 2.0) / df)[:, None] * yt
        return np.dot(linalg.sqrtm(sigma), zt)

    @staticmethod
    def fit(data):
        K = data.shape[0]

        def unlikelihood(params):
            sigma = fill_lower(params[:-K])
            df = params[-K:]
            return -MDFt.loglikelihood(data, sigma, df)

        sigma = np.cov(data)
        sigma_upper = sigma[np.triu_indices_from(sigma)]
        df = np.array([student_t.fit(values) for values in data])
        x0 = np.concatenate((sigma_upper, df))
        bounds = [
            (0, None) if i == j else (None, None)
            for i in range(K)
            for j in range(i + 1)
        ]
        bounds += K * [(5, None)]
        result = sp_optimize.minimize(
            unlikelihood, x0=x0, bounds=bounds, options=dict(disp=True)
        )
        sigma = fill_lower(result.x[:-K])
        df = result.x[-K:]
        return sigma, df


def fill_lower(sequence):
    K = int(np.ceil(np.sqrt(len(sequence))))
    arr = np.empty((K, K))
    arr[np.triu_indices_from(arr)] = sequence
    upper_i = np.triu_indices_from(arr, k=1)
    lower_i = np.tril_indices_from(arr, k=-1)
    arr[lower_i] = arr[upper_i]
    return arr


class _Rain(Dist):
    def _mask_kwds(self, mask, kwds):
        return {
            name: (
                vals[mask]
                if (hasattr(vals, "shape") and vals.shape == mask.shape)
                else vals
            )
            for name, vals in kwds.items()
        }

    def _pdf(self, meta, *args, **kwds):
        pass

    def _cdf(self, meta, *args, **kwds):
        pass

    def _ppf(self, meta, *args, **kwds):
        pass


class _KDE(object):
    # these are not part of the solution and should not be tested on
    _cache_names = (
        "kernel_data",
        "x_eval",
        "q_kde_eval",
    )  # "kernel_width"

    def fit_kde(self, x, x0=None, bounds=None):
        if bounds is not None:
            bounds = np.squeeze(bounds)
        # return kde.optimal_kernel_width(x, x0=None, bounds=bounds)
        return kde.silvermans_rule(x)

    def _kde_integral(
        self, kernel_width, kernel_data, f_thresh, upper_eval, recalc=True
    ):
        lower_eval = max(0.95 * f_thresh, 1e-9)
        if upper_eval is None or np.any(upper_eval < kernel_data):
            if len(kernel_data):
                upper_eval = 1.75 * (kernel_data.max() + 1.75 * kernel_width)
            else:
                upper_eval = 1.025 * f_thresh
        x_eval_log = np.log(np.linspace(lower_eval, upper_eval, 500))
        if len(kernel_data) == 0:
            x_eval_log = np.concatenate((x_eval_log, [np.log(upper_eval)]))
            q_kde_eval = np.ones_like(x_eval_log)
            q_kde_eval[0] = 0
            return q_kde_eval, x_eval_log
        if kernel_width is None:
            kernel_width = kde.silvermans_rule(kernel_data)
        if np.isnan(kernel_width):
            kernel_data += (
                upper_eval * 1e-6 * varwg.rng.normal(size=len(kernel_data))
            )
            kernel_width = kde.silvermans_rule(kernel_data)
        dens_kde_eval = kde.kernel_density(
            kernel_width,
            np.log(kernel_data),
            eval_points=x_eval_log,
            recalc=recalc,
        )
        while np.isclose(dens_kde_eval.sum(), 0):
            # increase number of evaluation points
            n_eval_points = len(x_eval_log) * 2
            x_eval_log = np.log(
                np.linspace(lower_eval, upper_eval, n_eval_points)
            )
            dens_kde_eval = kde.kernel_density(
                kernel_width,
                np.log(kernel_data),
                eval_points=x_eval_log,
                recalc=recalc,
            )
        assert np.all(np.isfinite(dens_kde_eval))

        x_eval = np.exp(x_eval_log)
        dens_kde_eval /= x_eval
        q_kde_eval = cumulative_trapezoid(y=dens_kde_eval, x=x_eval, initial=0)
        assert np.all(np.isfinite(q_kde_eval))
        # this is ugly, but I cannot find a better solution (now)
        q_kde_eval /= q_kde_eval[-1]
        assert np.all(np.isfinite(q_kde_eval))
        # fill only the quantile range [q_thresh, 1]
        q_kde_eval = self.q_thresh + (1.0 - self.q_thresh) * q_kde_eval
        q_kde_eval = np.concatenate((q_kde_eval, [1.0]))
        assert np.all(np.isfinite(q_kde_eval))
        x_eval_log = np.concatenate((x_eval_log, [np.log(upper_eval)]))
        return q_kde_eval, x_eval_log

    def _kde_cdf(self, xx, q_kde_eval=None, x_eval=None):
        xx = np.atleast_1d(xx)
        qq_kde = np.empty_like(xx)
        if isinstance(q_kde_eval, list):
            q_kde_eval = np.asarray(q_kde_eval, dtype=object)
        if isinstance(x_eval, list):
            x_eval = np.asarray(x_eval, dtype=object)
        for i, x in enumerate(xx):
            if not (x_eval_ := x_eval[i]).ndim:
                x_eval_ = x_eval
            if x > x_eval_[-1]:
                qq_kde[i] = 1.0
                continue
            if x < x_eval_[0]:
                qq_kde[i] = 0.0
                continue
            if not (q_kde_eval_ := q_kde_eval[i]).ndim:
                q_kde_eval_ = q_kde_eval
            q_kde_interp = interpolate.interp1d(
                x_eval_, q_kde_eval_, kind="linear", assume_sorted=True
            )
            qq_kde[i] = q_kde_interp(x)
        return qq_kde

    def _kde_ppf(self, qq, q_kde_eval=None, x_eval=None):
        qq = np.atleast_1d(qq)
        # assert np.all(qq >= self.q_thresh)
        xx_kde = np.empty_like(qq)
        if isinstance(q_kde_eval, list):
            q_kde_eval = np.asarray(q_kde_eval, dtype=object)
        if isinstance(x_eval, list):
            x_eval = np.asarray(x_eval, dtype=object)
        for i, q in enumerate(qq):
            if not (x_eval_ := x_eval[i]).ndim:
                x_eval_ = x_eval
            if not (q_kde_eval_ := q_kde_eval[i]).ndim:
                q_kde_eval_ = q_kde_eval
            x_kde_interp = interpolate.interp1d(
                q_kde_eval_, x_eval_, kind="linear", assume_sorted=True
            )
            xx_kde[i] = x_kde_interp(q)
        return xx_kde


class RainMix(_KDE, _Rain):
    supplements_names = (
        "q_thresh",
        "f_thresh",
        "kernel_data",
        "q_kde_eval",
        "x_eval",
    )

    def __init__(
        self,
        distribution,
        threshold=0.0015,
        q_thresh_lower=0.6,
        q_thresh_upper=0.95,
    ):
        """Mixed Rain distribution with lower threshold and KDE above
        q_threshold.

        Requires self-generated sample data to be initialized because
        of the KDE.

        Parameters
        ----------
        distribution : Dist
        threshold : float, optional
            Threshold rain intensity
        q_threshold : float, optional
            Quantile above which to estimate the distribution via KDE.
            Not the quantile of the full distribution, but of the
            'wet' values.
        """
        self.dist = distribution
        self.thresh = threshold
        self.q_thresh_lower = q_thresh_lower
        self.q_thresh_upper = q_thresh_upper
        self.debug = False

        # this enables testing
        sample_data = self._gen_sample_data()
        self.sample_data = sample_data
        self._feasible_start = self.fit(sample_data)
        # parameter_names exists because of the DistMeta class
        self.parameter_names = (
            self.parameter_names + distribution.parameter_names
        )
        # n_pars is set in class construction time with the Dist metaclass
        self.n_pars += self.dist.n_pars
        self.name = "rainmix " + distribution.name

    def _gen_sample_data(self):
        # the following solves problems that arise during testing
        # as we have a mixed parametric, non-parametric distribution
        # here, supplying a feasible starting solution is non-trivial.
        # the data is part of the solution!
        sample_quantiles = np.linspace(0.001, 0.999, 1000)
        sample_data = self.dist.ppf(
            sample_quantiles, *self.dist._feasible_start
        )
        sample_data[-50:] *= 2
        sample_data = np.concatenate((np.zeros_like(sample_data), sample_data))
        return sample_data

    def _pdf(
        self,
        x,
        q_thresh,
        rain_prob,
        kernel_width,
        kernel_data,
        f_thresh,
        q_kde_eval=None,
        x_eval=None,
        *args,
        **kwds,
    ):
        (x, q_thresh, rain_prob, kernel_width, f_thresh) = np.atleast_1d(
            x, q_thresh, rain_prob, kernel_width, f_thresh
        )
        assert np.all(np.isfinite(rain_prob))
        if len(rain_prob) == 1:
            rain_prob = np.broadcast_to(rain_prob, x.shape)
        if len(kernel_width) == 1:
            kernel_width = np.broadcast_to(kernel_width, x.shape)
        if isinstance(kernel_data, np.ndarray):
            if kernel_data.ndim == 1 and kernel_data.dtype != np.dtype("O"):
                kernel_data = np.broadcast_to(
                    kernel_data, (x.size, kernel_data.size)
                )
        else:
            kernel_data = np.asarray(kernel_data, dtype=object)
        if len(q_thresh) == 1:
            q_thresh = np.broadcast_to(q_thresh, x.shape)
        if len(f_thresh) == 1:
            f_thresh = np.broadcast_to(f_thresh, x.shape)
        if x.ndim > 1 and rain_prob.ndim > 1:
            (
                x,
                rain_prob,
                f_thresh,
            ) = np.broadcast_arrays(x, rain_prob, f_thresh)

        rain_mask = x > self.thresh
        par_mask = (x < f_thresh) & rain_mask
        kde_mask = x >= f_thresh
        # the following tries to catch broadcast ambiguities, but might not
        # be the most parsimonious formulation in terms of memory
        dens = np.zeros_like(rain_prob + rain_mask)

        def par_scale(x):
            return (x - self.thresh) / (f_thresh[par_mask] - self.thresh)

        def par_rescale(dens):
            return dens / (f_thresh[par_mask] - self.thresh) * self.q_thresh

        # parametric part
        if np.any(par_mask):
            x_par = x[par_mask]
            x_par = par_scale(x_par)
            kwds_par = self._mask_kwds(par_mask, kwds)
            dens_par = self.dist.pdf(x_par, *args, **kwds_par)
            dens_par = par_rescale(dens_par)
            if dens.shape == par_mask.shape:
                try:
                    dens[par_mask] = np.atleast_1d(dens_par)
                except TypeError:
                    dens[par_mask] = np.atleast_1d(dens_par)[0]
            else:
                dens[:, par_mask[0]] = dens_par

        def kde_single(dens, x_kde, mask, kernel_width, kernel_data):
            if len(kernel_data.ravel()) == 0:
                dens[mask] = 0
                return
            dens_kde = kde.kernel_density(
                kernel_width, np.log(kernel_data), eval_points=np.log(x_kde)
            )
            # dividing by x_kde because of chain-rule differentiation
            # of ln(x)
            dens[mask] = (1.0 - self.q_thresh) * dens_kde / x_kde
            if kernel_data.size:
                dens[mask] /= kernel_data.size - 1

        # non-parametric (KDE) part
        if np.any(kde_mask):
            if kde_mask.ndim == 2:
                for tt, mask in enumerate(kde_mask):
                    kde_single(
                        dens[tt],
                        x[tt, mask],
                        mask,
                        kernel_width[tt],
                        kernel_data[tt],
                    )
            else:
                kde_single(
                    dens,
                    x[kde_mask],
                    kde_mask,
                    kernel_width[kde_mask],
                    kernel_data,
                )
        return dens

    def _cdf(
        self,
        x,
        q_thresh,
        rain_prob,
        kernel_width,
        kernel_data,
        f_thresh,
        q_kde_eval=None,
        x_eval=None,
        *args,
        **kwds,
    ):
        (x, q_thresh, rain_prob, kernel_width, f_thresh) = np.atleast_1d(
            x, q_thresh, rain_prob, kernel_width, f_thresh
        )
        # i want to manipulate kwds, but only here
        kwds = {k: v for k, v in kwds.items()}
        if len(rain_prob) == 1:
            rain_prob = np.broadcast_to(rain_prob, x.shape)
        if len(kernel_width) == 1:
            kernel_width = np.broadcast_to(kernel_width, x.shape)
        if isinstance(kernel_data, np.ndarray):
            if kernel_data.ndim == 1 and kernel_data.dtype != np.dtype("O"):
                kernel_data = np.broadcast_to(
                    kernel_data, (len(x), len(kernel_data))
                )
        else:
            kernel_data = np.asarray(kernel_data, dtype=object)
        if len(q_thresh) == 1:
            q_thresh = np.broadcast_to(q_thresh, x.shape)
        if len(f_thresh) == 1:
            f_thresh = np.broadcast_to(f_thresh, x.shape)
        upper_eval = np.atleast_1d(kwds.pop("u", [None]))
        if len(upper_eval) == 1:
            upper_eval = np.broadcast_to(upper_eval, x.shape)
        if isinstance(q_kde_eval, list):
            q_kde_eval = np.asarray(q_kde_eval, dtype=object)
        if isinstance(x_eval, list):
            x_eval = np.asarray(x_eval, dtype=object)

        with warnings.catch_warnings():
            warnings.simplefilter("ignore", category=RuntimeWarning)
            rain_mask = x > self.thresh
            kde_mask = x >= f_thresh
            par_mask = ~kde_mask & rain_mask
        # fill with lower part of south-of-p0 uniform random numbers
        qq = np.zeros_like(x, dtype=float)
        n_non_rain = (~rain_mask).sum()
        if n_non_rain > 0:
            qq[~rain_mask] = (1 - rain_prob[~rain_mask]) * varwg.rng.random(
                n_non_rain
            )

        if self.debug:
            fig, ax = plt.subplots(nrows=1, ncols=1)
            ax.hist(qq[~rain_mask], 40, histtype="step", label="norain")
        # parametric part
        if np.any(par_mask):
            x_par = x[par_mask]
            kwds_par = self._mask_kwds(par_mask, kwds)
            x_par = (x_par - self.thresh) / (f_thresh[par_mask] - self.thresh)
            if "l" in kwds_par:
                x_par = np.maximum(x_par, kwds_par["l"])
            if self.debug:
                assert np.all(x_par >= 0)
                assert np.all(x_par <= 1)
            if "l" not in kwds_par:
                kwds_par["l"] = 0.0
            if "u" not in kwds_par:
                kwds_par["u"] = 1.0
            q_par = self.dist.cdf(x_par, *args, **kwds_par)
            p0 = (1 - rain_prob)[par_mask]
            p1 = q_thresh[par_mask]
            q_par = p0 + (p1 - p0) * q_par
            qq[par_mask] = q_par
            assert np.all(np.isfinite(q_par))

        # non-parametric (KDE) part
        if np.any(kde_mask):
            assert np.all(kernel_width[kde_mask] > 0)
            if isinstance(q_kde_eval, list):
                q_kde_eval = [q_kde_eval[i] for i in np.where(kde_mask)[0]]
            elif q_kde_eval.ndim == 2:
                q_kde_eval = q_kde_eval[kde_mask]
            if isinstance(x_eval, list):
                x_eval = [x_eval[i] for i in np.where(kde_mask)[0]]
            elif x_eval.ndim == 2:
                x_eval = x_eval[kde_mask]
            q_kde = self._kde_cdf(x[kde_mask], q_kde_eval, x_eval)
            qq[kde_mask] = q_kde
            if self.debug:
                assert np.all(np.isfinite(q_kde))
            if self.debug:
                ax.hist(q_kde, 20, histtype="step", label="kde")
                ax.legend()
            assert np.all(np.isfinite(q_kde))
        return qq

    def _ppf(
        self,
        qq,
        q_thresh,
        rain_prob,
        kernel_width,
        kernel_data,
        f_thresh,
        q_kde_eval=None,
        x_eval=None,
        *args,
        **kwds,
    ):
        (qq, q_thresh, rain_prob, kernel_width, f_thresh) = np.atleast_1d(
            qq, q_thresh, rain_prob, kernel_width, f_thresh
        )
        assert np.all(np.isfinite(qq))
        # i want to manipulate kwds, but only here
        kwds = {k: v for k, v in kwds.items()}
        if len(rain_prob) == 1:
            rain_prob = np.broadcast_to(rain_prob, qq.shape)
        if len(kernel_width) == 1:
            kernel_width = np.broadcast_to(kernel_width, qq.shape)
        if isinstance(kernel_data, np.ndarray):
            if kernel_data.ndim == 1 and kernel_data.dtype != np.dtype("O"):
                kernel_data = np.broadcast_to(
                    kernel_data, (len(qq), len(kernel_data))
                )
        else:
            kernel_data = np.asarray(kernel_data, dtype=object)
        if len(q_thresh) == 1:
            q_thresh = np.broadcast_to(q_thresh, qq.shape)
        if len(f_thresh) == 1:
            f_thresh = np.broadcast_to(f_thresh, qq.shape)
        upper_eval = np.atleast_1d(kwds.pop("u", [None]))
        if len(upper_eval) == 1:
            upper_eval = np.broadcast_to(upper_eval, qq.shape)
        if isinstance(q_kde_eval, list):
            q_kde_eval = np.asarray(q_kde_eval, dtype=object)
        if isinstance(x_eval, list):
            x_eval = np.asarray(x_eval, dtype=object)

        # we want to have zero rain where probability of rain is lower
        # than rain_prob
        x = np.zeros_like(qq, dtype=float)
        rain_mask = qq > 1 - rain_prob
        kde_mask = qq >= q_thresh
        par_mask = rain_mask & ~kde_mask
        if np.all(~rain_mask):
            return x

        # parametric part
        if np.any(par_mask):
            kwds_par = self._mask_kwds(par_mask, kwds)
            q_par = qq[par_mask]
            p0 = 1 - rain_prob[par_mask]
            p1 = q_thresh[par_mask]
            q_par = (q_par - p0) / (p1 - p0)
            x_par = self.dist.ppf(q_par, *args, **kwds_par)
            if self.debug:
                assert np.all(q_par >= 0)
                assert np.all(q_par <= 1)
            x_par = (f_thresh[par_mask] - self.thresh) * x_par + self.thresh
            x[par_mask] = x_par
            assert np.all(np.isfinite(x_par))

        # non-parametric (KDE) part
        if np.any(kde_mask):
            if (q_kde_eval.ndim == 2) or (
                q_kde_eval.dtype == np.dtype("object")
            ):
                q_kde_eval = q_kde_eval[kde_mask]
            if (x_eval.ndim == 2) or (x_eval.dtype == np.dtype("object")):
                x_eval = x_eval[kde_mask]
            x_kde = self._kde_ppf(qq[kde_mask], q_kde_eval, x_eval)
            x[kde_mask] = x_kde
            assert np.all(np.isfinite(x_kde))

        # # make sure there is no rain when rain_prob is very close to 0
        # biblical_rain_mask = x[rain_prob < 1e-2] > 0
        # if np.sum(biblical_rain_mask):
        #     # print(f"Capping biblical rain: {np.sum(biblical_rain_mask)} steps"
        #     #       f"({100 * np.mean(biblical_rain_mask):.2f}%)")
        #     x[rain_prob < 1e-6] = 0

        return x

    def median(self, *args, **kwds):
        return self.ppf(0.5, *args, **kwds)

    def mean(self, *args, **kwds):
        f_thresh = kwds["f_thresh"]
        epsilon = 1e-6
        # integrate parametric and kde part separately
        result_par = quad(
            lambda x: x * self.pdf(x, *args, **kwds),
            self.thresh,
            f_thresh,
        )
        x_max = self.ppf(1 - epsilon, *args, **kwds)
        result_kde = quad(
            lambda x: x * self.pdf(x, *args, **kwds),
            f_thresh,
            x_max,
        )
        return 0.5 * (result_par[0] + result_kde[0])

    def _fit_qthresh(
        self, q_thresh, x, x0=None, kernel_bounds=None, *args, **kwds
    ):
        rain_mask = x > self.thresh
        rain_prob = np.mean(rain_mask)
        self.q_thresh = q_thresh
        f_thresh = max(np.percentile(x, 100 * self.q_thresh), self.thresh)
        kde_mask = x >= f_thresh
        par_mask = rain_mask & ~kde_mask
        if np.sum(par_mask) > 1:
            rain_kwds = self._mask_kwds(par_mask, kwds)
            x_par = x[rain_mask & ~kde_mask]
            x_par = (x_par - self.thresh) / (f_thresh - self.thresh)
            par_names = self.dist.parameter_names
            if "u" in par_names:
                # the supplied upper limit is not meant for the
                # parametric but for the kde part
                rain_kwds["u"] = 1.0
                # lower bound is not to be fitted
                rain_kwds["l"] = 0.0
            par_solution = list(self.dist.fit(x_par, **rain_kwds))
        else:
            par_solution = self.dist._feasible_start
        self.success = True  # haha!
        kernel_data = x[kde_mask]
        if np.any(par_mask):
            kernel_data = np.concatenate(([np.max(x[par_mask])], kernel_data))
        # if len(kernel_data) >= 5:
        if len(kernel_data) > 2:
            kernel_width = self.fit_kde(
                np.log(kernel_data),
                None if x0 is None else x0[2],
                bounds=kernel_bounds,
            )
            if np.isnan(kernel_width):
                kernel_width = None
        else:
            kernel_data = np.array([])
            kernel_width = None
        upper_eval = kwds.pop("u", None)
        q_kde_eval, x_eval_log = self._kde_integral(
            kernel_width, kernel_data, f_thresh, upper_eval
        )
        x_eval = np.exp(x_eval_log)
        if kernel_width:
            kernel_width = float(kernel_width)
        self.fitted_pars = (
            q_thresh,
            rain_prob,
            kernel_width,
            kernel_data,
            f_thresh,
            q_kde_eval,
            x_eval,
        ) + tuple(par_solution)
        self.q_thresh = q_thresh
        density = self.pdf(x, *self.fitted_pars)
        mask = (density > 0) & np.isfinite(density)
        lik = -np.nansum(np.log(density[mask]))
        return lik

    def _fit(self, x, x0=None, kernel_bounds=None, *args, **kwds):
        result = sp_optimize.minimize_scalar(
            partial(self._fit_qthresh, kernel_bounds=kernel_bounds, **kwds),
            args=(x, x0) + args,
            bounds=[self.q_thresh_lower, self.q_thresh_upper],
            method="bounded",
        )
        return self.fitted_pars

    def fit_ml(self, x, x0=None, bounds=None, *args, **kwds):
        """This is a lie, we don't fit rainmix with ML."""
        params = self._fit(x, x0, bounds=bounds, *args, **kwds)
        params = list(params)
        supplements = {
            name: params[self.parameter_names.index(name)]
            for name in self.supplements_names
        }
        fixed_names = [
            name for name in kwds.keys() if name in self.parameter_names
        ]
        params = [
            param
            for name, param in zip(self.parameter_names, params)
            if (name not in self.supplements_names)
            and (name not in fixed_names)
        ]
        return self.Result(
            x=params, supplements=supplements, success=self.success
        )

    def _constraints(
        self,
        x,
        q_thresh,
        rain_prob,
        kernel_width,
        kernel_data,
        f_thresh,
        q_kde_eval=None,
        x_eval=None,
        **kwds,
    ):
        # (rain_prob, kernel_width, kernel_data, f_thresh) = map(
        #     np.asarray, (rain_prob, kernel_width, kernel_data, f_thresh)
        # )
        (rain_prob, kernel_width, f_thresh) = map(
            np.asarray, (rain_prob, kernel_width, f_thresh)
        )
        if "kernel_bounds" in kwds:
            kwds.pop("kernel_bounds")
        mask = (
            (q_thresh < self.q_thresh_lower)
            | (q_thresh > self.q_thresh_upper)
            | (rain_prob < 0)
            | (rain_prob > 1)
            | (kernel_width <= 0)
            | (f_thresh < 0)
        )
        kde_mask = x >= f_thresh
        par_mask = (x > self.thresh) & ~kde_mask
        mask[par_mask] |= self.dist._constraints(
            x[par_mask],
            **{key: value[par_mask] for key, value in kwds.items()},
        )
        return mask

    def _invalid_x(self, x, *args, **kwds):
        return np.atleast_1d((np.full_like(x, False, dtype=bool)))

    def _fix_x(self, x):
        return np.where(x < 0, 0, x)


rainmix_kumaraswamy = RainMix(
    kumaraswamy, threshold=0.001, q_thresh_lower=0.95, q_thresh_upper=0.99
)
# rainmix_expon = RainMix(expon, q_threshold=q_threshold)
# rainmix_weibull = RainMix(weibull, q_threshold=q_threshold)
# rainmix_gamma = RainMix(gamma, q_threshold=q_threshold)
# rainmix_lognormal = RainMix(lognormal, q_threshold=q_threshold)
# rainmix_gamma1 = RainMix(gamma1)


class Rain(_Rain):
    def __init__(self, distribution, threshold=0.0001):
        self.dist = distribution
        self.thresh = threshold
        self.parameter_names = tuple(
            ["rain_prob"] + list(distribution.parameter_names)
        )
        self.n_pars = len(self.parameter_names)
        self._feasible_start = (0.98,) + self.dist._feasible_start
        self.name = "rain " + distribution.name

    def _pdf(self, x, rain_prob, *args, **kwds):
        x, rain_prob = np.atleast_1d(x, rain_prob)
        if len(rain_prob) == 1:
            rain_prob = np.broadcast_to(rain_prob, x.shape)
        if x.ndim > 1 and rain_prob.ndim > 1:
            x, rain_prob = np.broadcast_arrays(x, rain_prob)
        # the following tries to catch broadcast ambiguities, but might not
        # be the most parsimonious formulation in terms of memory
        rain_mask = x > self.thresh
        dens = np.empty_like(rain_prob + rain_mask, dtype=float)
        dens[~rain_mask] = (1 - rain_prob[~rain_mask]) / self.thresh
        kwds_rain = self._mask_kwds(rain_mask, kwds)
        dens_rain = (
            self.dist.pdf(x[rain_mask] - self.thresh, *args, **kwds_rain)
            * rain_prob[rain_mask]
        )
        if dens.shape == rain_mask.shape:
            try:
                dens[rain_mask] = np.atleast_1d(dens_rain)
            except TypeError:
                dens[rain_mask] = np.atleast_1d(dens_rain)[0]
        else:
            dens[:, rain_mask[0]] = dens_rain
        return dens

    def _cdf(self, x, rain_prob, *args, **kwds):
        x, rain_prob = np.atleast_1d(x, rain_prob)
        qq = np.zeros_like(x, dtype=float)
        if len(rain_prob) == 1:
            rain_prob = np.full_like(x, rain_prob)
        finite_mask = np.isfinite(x)
        x_finite = np.where(finite_mask, x, 0)
        rain_mask = x_finite > self.thresh
        non_rain_mask = ~rain_mask
        # subtracting self.thresh from x has the unwanted side-effect of
        # shifting x to possibly out-of-bound values. so this should not be
        # done with values where non_rain_mask.
        # as we mask x, we also have to mask the parameters given in **kwds
        kwds_rain = self._mask_kwds(rain_mask, kwds)
        p0 = 1 - rain_prob[rain_mask]
        qq[rain_mask] = p0 + rain_prob[rain_mask] * self.dist.cdf(
            x[rain_mask] - self.thresh, *args, **kwds_rain
        )
        qq[non_rain_mask] = varwg.rng.uniform(size=non_rain_mask.sum()) * (
            1 - rain_prob[non_rain_mask]
        )
        qq[~finite_mask] = np.nan
        return qq

    def _ppf(self, qq, rain_prob, *args, **kwds):
        qq, rain_prob = np.atleast_1d(qq, rain_prob)
        if len(rain_prob) == 1:
            rain_prob = np.full_like(qq, rain_prob)
        # we want to have zero rain, where probability of rain is lower than
        # rain_prob
        x = np.zeros_like(qq, dtype=float)
        rain_mask = qq > 1 - rain_prob
        if np.all(~rain_mask):
            return x
        # subtracting self.thresh from x has the unwanted side-effect of
        # shifting x to possibly out-of-bound values. so this should not be
        # done with values where ~rain_mask.
        # as we mask x, we also have to mask the parameters given in **kwds
        kwds_rain = self._mask_kwds(rain_mask, kwds)
        x[rain_mask] = self.thresh + self.dist.ppf(
            1 - (1 - qq[rain_mask]) / rain_prob[rain_mask], *args, **kwds_rain
        )
        return x

    def _fit(self, x, **kwds):
        rain_mask = x > self.thresh
        rain_prob = np.mean(rain_mask)
        if rain_prob:
            solution = self.dist.fit(x[rain_mask] - self.thresh, **kwds)
        else:
            solution = np.full(self.dist.n_pars - len(kwds), np.nan)
        return tuple([rain_prob] + list(solution))


rain_expon = Rain(expon, threshold=0.002)
rain_weibull = Rain(weibull, threshold=0.002)

if __name__ == "__main__":
    # import config_konstanz_disag as conf
    import config_konstanz as conf

    import varwg
    from varwg.core import base, plotting

    varwg.set_conf(conf)
    # times_hourly, met = varwg.read_met(varwg.conf.met_file)
    # rain, times = vg.my.sumup(met["R"], 24, times_hourly)
    met_vg = varwg.VG(
        ("R", "theta", "ILWR", "Qsw", "rh"), verbose=True, refit="R"
    )
    rain_dist, solution = met_vg.dist_sol["R"]
    met_vg.simulate()
    # rain = met_vg.data_raw[0]
    # rain_dist = rainmix_expon
    # rain_dist = vg.sd.SlidingDist(rain_dist, rain, met_vg.times, verbose=True)

    # solution = rain_dist.fit()
    # rain_dist.plot_seasonality_fit()
    # rain_dist.plot_monthly_params()
    rain_dist.plot_fourier_fit()
    rain_dist.plot_monthly_fit(
        solution,
        # dists_alt=(Rain(gamma, threshold=.015),
        #            expon)
    )
    met_vg.plot_monthly_hists("R")
    met_vg.plot_meteogramm_daily()
    # rain_dist.plot_monthly_params()
    quantiles = rain_dist.cdf(solution)
    my.hist(quantiles, 20)
    my.hist(met_vg.data_trans[0], 20, dist=norm)
    # rain_dist.scatter_pdf(solution)
    plt.show()
