import numpy as np
import mpmath as mp
from scipy.optimize import minimize, differential_evolution, NonlinearConstraint
from scipy.optimize._numdiff import approx_derivative
from scipy.stats import multivariate_normal
import math
from collections import Counter
import matplotlib.pyplot as plt
import matplotlib.transforms as transforms
from matplotlib.patches import Ellipse
from gwr_inversion import gwr
import json
from importlib import resources


def Jacobian(fun, x, bounds):
    """
    Approximates the Jacobian of 'fun' at point 'x' without
    evaluating the function outside of 'bounds'.
    """
    bounds = [[b[0] for b in bounds], [b[1] for b in bounds]]
    return approx_derivative(fun, x, bounds=bounds)


def Hessian(fun, x, bounds):
    """
    Approximates the Hessian of 'fun' at point 'x' without
    evaluating the function outside of 'bounds'.
    """
    bounds = [[b[0] for b in bounds], [b[1] for b in bounds]]
    return approx_derivative(
        lambda y: approx_derivative(fun, y, bounds=bounds), x, bounds=bounds
    )


def confidence_region(mean, cov, obs, se_type, xlabel, ylabel, export):
    """
    Determines and plots confidence regions for a multivariate normal
    distribution with expected value 'mean' and covariance 'cov'.
    """
    [Eval, Evec] = np.linalg.eig(cov)
    Evec = -np.hstack((Evec[:, 1].reshape((2, 1)), Evec[:, 0].reshape((2, 1))))
    xCenter = mean[0]
    yCenter = mean[1]
    theta = np.linspace(0, 2 * np.pi, 100)
    x_vec = np.array([1, 0])
    cosrotation = np.dot(x_vec, Evec[:, 1]) / (
        np.linalg.norm(x_vec) * np.linalg.norm(Evec[:, 1])
    )
    rotation = np.pi / 2 - np.arccos(cosrotation)

    R = np.array(
        [[np.sin(rotation), np.cos(rotation)], [-np.cos(rotation), np.sin(rotation)]]
    )

    chisq = [1.368, 3.2188, 5.991]

    x = np.empty((len(theta), len(chisq)))
    y = np.empty((len(theta), len(chisq)))
    xRadius = np.empty(len(chisq))
    yRadius = np.empty(len(chisq))

    x_plot = np.empty((len(theta), len(chisq)))
    y_plot = np.empty((len(theta), len(chisq)))

    rotated_Coords = np.empty((2, len(theta), len(chisq)))

    for i in range(len(chisq)):
        xRadius[i] = np.sqrt(chisq[i] * Eval[0])
        yRadius[i] = np.sqrt(chisq[i] * Eval[1])

        x[:, i] = xRadius[i] * np.cos(theta)
        y[:, i] = yRadius[i] * np.sin(theta)

        rotated_Coords[:, :, i] = np.matmul(R, np.row_stack((x[:, i], y[:, i])))

        x_plot[:, i] = rotated_Coords[0, :, i].T + xCenter
        y_plot[:, i] = rotated_Coords[1, :, i].T + yCenter

    fig, (ax) = plt.subplots(figsize=(7, 5))

    labels = ["$50\%$", "$80\%$", "$95\%$"]
    linestyles = [":", "--", "-"]

    for i in range(len(chisq)):
        ax.plot(
            x_plot[:, i],
            y_plot[:, i],
            label=labels[i],
            linestyle=linestyles[i],
            color="k",
        )
    ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
    ax.set_xlabel(xlabel)
    ax.set_ylabel(ylabel)
    ax.plot(mean[0], mean[1], "+k", label="Estimate")
    if obs is not None:
        ax.scatter(
            obs[:, 0], obs[:, 1], c="b", marker="^", s=5, label="Simulated\n samples"
        )
    if se_type == "asymptotic":
        ax.set_title("Asymptotic confidence region")
    elif se_type == "simulated":
        ax.set_title("Simulated confidence region")
    ax.legend(bbox_to_anchor=(1.05, 1), loc="upper left")
    fig.set_tight_layout({"pad": 0})
    if isinstance(export, str):
        import tikzplotlib

        tikzplotlib.save(export + ".tex")
    return


def data_sort(t_data, p_data):
    """
    Data sorter to improve the efficiency of the :func:`birdepy` package.
    Counts how many times each probability_ z0 -> zt occurs, where transitions
    occurring over different elapsed times are treated as non-equal.

    Parameters
    ----------
    t_data : array_like
        Observation times of birth-and-death process.
        If one trajectory is observed, then this is a list.
        If multiple trajectories are observed, then this is a list of lists
        where each list corresponds to a trajectory.

    p_data : array_like
        Observed populations of birth-and-death process at times in argument
        't_data'.
        If one trajectory is observed, then this is a list.
        If multiple trajectories are observed, then this is a list of lists
        where each list corresponds to a traj  ectory.

    Return
    -------
    data : collections.Counter object
        A dictionary with keys corresponding to observed transitions
        (z0, zt, t) and values corresponding to the number of times
        an observed transition occurs.

    """
    if type(t_data) is np.ndarray:
        t_data = t_data.tolist()
    if type(t_data[0]) is not list:
        t_data = [t_data]
    if type(p_data) == np.ndarray:
        p_data = p_data.tolist()
    if type(p_data[0]) is not list:
        p_data = [p_data]

    all_transitions = []
    for idx1 in range(len(p_data)):
        for idx2 in range(len(p_data[idx1]) - 1):
            all_transitions.append(
                (
                    p_data[idx1][idx2],
                    p_data[idx1][idx2 + 1],
                    t_data[idx1][idx2 + 1] - t_data[idx1][idx2],
                )
            )
    return Counter(all_transitions)


def data_sort_2(data):
    """
    Further sorts output of :func:`birdepy._utility.data_sort`. Separates data
    into sub-collections where all transitions in any given sub-collection
    occur in the same elapsed time (rounded to 8 decimal places). This is useful
    when computing transition probabilities using methods that compute all
    transition probabilities simultaneously for a specified time (e.g., the
    matrix exponential method).

    Parameters
    ----------
    data : collections.Counter object
        A dictionary with keys corresponding to observed transitions
        (z0, zt, t) and values corresponding to the number of times
        a probability_ is observed.

    Return
    -------
    sorted_data : dict
        A dictionary with keys corresponding to inter-observation times and
        values containing dict objects with keys corresponding to observed
        (z0, zt) pairs and values corresponding to the number of times a
        pair is observed for the relevant inter-observation time. E.g.,
        {t0 : {(a, b): 3, (c, d) : 4}, t1 : {(e, f) : 1, (g, h) : 10}}.

    """
    sorted_data = {}
    for i in data:
        if np.around(i[2], 8) not in sorted_data:
            sorted_data[np.around(i[2], 8)] = {(i[0], i[1]): data[i]}
        else:
            sorted_data[np.around(i[2], 8)][(i[0], i[1])] = data[i]
    return sorted_data


def higher_birth(model):
    """
    Returns a function that gives the birth rate as a function
    of population size and parameters.
    """
    if model == "Verhulst":
        return lambda z, p: ((p[2] * z) <= 1) * p[0] * (1 - p[2] * z) * z
    elif model == "Ricker":
        return lambda z, p: p[0] * np.exp(-((p[2] * z) ** p[3])) * z
    elif model == "Hassell":
        return lambda z, p: p[0] * z / (1 + p[2] * z) ** p[3]
    elif model == "MS-S":
        return lambda z, p: p[0] * z / (1 + (p[2] * z) ** p[3])
    elif model == "Moran":
        return lambda z, p: (z <= p[4]) * (
            (p[4] - z)
            * (p[0] * z * (1 - p[2]) / p[4] + p[1] * (p[4] - z) * p[3] / p[4])
            / p[4]
        )
    elif model == "pure-birth":
        return lambda z, p: p[0] * z
    elif model == "pure-death":
        return lambda z, p: 0
    elif model == "Poisson":
        return lambda z, p: p[0]
    elif model == "linear":
        return lambda z, p: p[0] * z
    elif model == "linear-migration":
        return lambda z, p: p[0] * z + p[2]
    elif model == "M/M/1":
        return lambda z, p: p[0]
    elif model == "M/M/inf":
        return lambda z, p: p[0]
    elif model == "loss-system":
        return lambda z, p: (z < p[2]) * p[0]
    else:
        raise TypeError("Argument 'model' has an unknown value.")


def higher_death(model):
    """
    Returns a function that gives the death rate as a function
    of population size and parameters.
    """
    if model == "Verhulst":
        return lambda z, p: p[1] * (1 + p[3] * z) * z
    elif model in [
        "Ricker",
        "Hassell",
        "MS-S",
        "loss-system",
        "linear",
        "linear-migration",
        "M/M/inf",
    ]:
        return lambda z, p: p[1] * z
    elif model == "Moran":
        return lambda z, p: (z <= p[4]) * (
            z * (p[2] * (p[4] - z) * (1 - p[3]) / p[4] + p[0] * z * p[2] / p[4]) / p[4]
        )
    elif model == "M/M/1":
        return lambda z, p: (z > 0) * p[1]
    elif model in ["pure-birth", "Poisson"]:
        return lambda z, p: 0
    elif model == "pure-death":
        return lambda z, p: p[0] * z
    else:
        raise TypeError("Argument 'model' has an unknown value.")


def higher_h_fun(model):
    """
    Returns a function that gives the derivative with respect
    to population size of the function given by the birth rate
    minus the death rate.
    """
    if model == "Verhulst":
        return lambda z, p: p[0] - p[1] - 2 * z * (p[0] * p[2] + p[1] * p[3])
    elif model == "Ricker":
        return (
            lambda z, p: p[0]
            * np.exp(-((p[2] * z) ** p[3]))
            * (1 - p[3] * (p[2] * z) ** p[3])
            - p[1]
        )
    elif model == "Hassell":
        return (
            lambda z, p: (p[0] * (1 + p[2] * z - p[3] * z)) / (1 + (p[2] * z)) ** p[3]
            - p[1]
        )
    elif model == "MS-S":
        return (
            lambda z, p: (p[0] * (1 + (p[2] * z) ** p[3] * (1 - p[3])))
            / (1 + (p[2] * z) ** p[3]) ** 2
        )
    elif model == "Moran":
        return (
            lambda z, p: (
                z * p[0] * (p[4] * (1 - p[2]) - z)
                + p[1] * (z - [4]) * (z - p[4] * p[3])
            )
            / p[4] ** 2
        )
    elif model == "linear":
        return lambda z, p: (p[0] - p[1]) * (z - z + 1)
    elif model == "linear-migration":
        return lambda z, p: (p[0] - p[1]) * (z - z + 1)
    elif model == "M/M/1":
        return lambda z, p: 0 * (z - z + 1)
    elif model == "M/M/inf":
        return lambda z, p: -p[1] * (z - z + 1)
    elif model == "pure-birth":
        return lambda z, p: p[0] * (z - z + 1)
    elif model == "pure-death":
        return lambda z, p: -p[0] * (z - z + 1)
    elif model == "Poisson":
        return lambda z, p: 0 * (z - z + 1)
    else:
        raise TypeError("Unknown model.")


def higher_zf_bld(model):
    """
    Returns the fixed points of the fluid model associated with the model.
    """
    if model == "Verhulst":
        return lambda p: [0, (p[0] - p[1]) / (p[2] * p[0] + p[1] * p[3])]
    elif model == "Ricker":
        return lambda p: [0, ((np.log(p[0] / p[1])) ** (1 / p[3])) / p[2]]
    elif model == "Hassell":
        return lambda p: [0, ((p[0] / p[1]) ** (1 / p[3]) - 1) / p[2]]
    elif model == "MS-S":
        return lambda p: [0, ((p[0] / p[1] - 1) ** (1 / p[3])) / p[2]]
    elif model == "Moran":
        return lambda p: [
            p[4]
            * (
                p[0] * (1 - p[2])
                - p[1] * (1 + p[3])
                + np.sqrt(
                    (p[2] - 1) ** 2 * p[0] ** 2
                    + 2 * p[0] * p[1] * (p[2] + p[3] + p[2] * p[3] - 1)
                    + p[1] ** 2 * (p[3] - 1) ** 2
                )
            )
            / (2 * (p[0] - p[1])),
            p[4]
            * (
                p[0] * (1 - p[2])
                - p[1] * (1 + p[3])
                - np.sqrt(
                    (p[2] - 1) ** 2 * p[0] ** 2
                    + 2 * p[0] * p[1] * (p[2] + p[3] + p[2] * p[3] - 1)
                    + p[1] ** 2 * (p[3] - 1) ** 2
                )
            )
            / (2 * (p[0] - p[1])),
        ]
    elif model == "linear":
        return lambda p: [0]
    elif model == "linear-migration":
        return lambda p: [-p[2] / (p[0] - p[1])]
    elif model == "Poisson":
        return lambda p: [np.nan]
    elif model == "M/M/1":
        return lambda p: [np.nan]
    elif model == "M/M/inf":
        return lambda p: [p[0] / p[1]]
    elif model == "pure-birth":
        return lambda p: [np.nan]
    elif model == "pure-death":
        return lambda p: [np.nan]
    elif model == "loss-system":
        return lambda p: [np.nan]
    else:
        raise TypeError("Unknown model.")


def q_mat_bld(z_min, z_max, p, b_rate, d_rate):
    """
    Builds the transition rate matrix for a
    population-size-dependent birth-and-death process.
    """
    num_states = int(z_max - z_min + 1)
    states = np.arange(z_min, z_max + 1, 1)
    q_mat = np.zeros((num_states, num_states))
    q_mat[-1, -1] = -d_rate(z_max, p)
    q_mat[-1, -2] = d_rate(z_max, p)
    q_mat[0, 0] = -b_rate(z_min, p)
    q_mat[0, 1] = b_rate(z_min, p)
    for z in np.arange(1, num_states - 1, 1):
        _state = states[z]
        q_mat[z, z] = -b_rate(_state, p) - d_rate(_state, p)
        q_mat[z, z + 1] = b_rate(_state, p)
        q_mat[z, z - 1] = d_rate(_state, p)
    return q_mat


def p_bld(p_prop, idx_known_p, known_p):
    """
    Augments parameters 'p_prop' with parameters 'known_p'
    to form a complete vector of parameters.
    """
    known_p = np.array(known_p)
    p_size = known_p.size + p_prop.size
    idx_known_p = list(idx_known_p)
    if len(known_p) != len(idx_known_p):
        raise TypeError(
            "Argument 'idx_known_p' and argument 'known_p' must" " be the same size."
        )
    idx_unknown_p = list(set(range(p_size)) - set(idx_known_p))
    p = np.empty(p_size)
    p[idx_known_p] = known_p
    p[idx_unknown_p] = p_prop
    return p


def laplace_invert_mexp(fun, t, max_fun_evals, method="cme"):
    """
    Numerical Laplace transform inversion using the method of reference [1],
    an Euler scheme or a Gaver scheme.

    Uses code downloaded from https://inverselaplace.org/ in June of 2021.

    [1] Horváth, G., Horváth, I., Almousa, S. A. D., & Telek, M. (2020).
    Numerical inverse Laplace transformation using concentrated matrix
    exponential distributions. Performance Evaluation, 137, 102067.
    """
    if method == "cme":
        # find the most steep CME satisfying max_fun_evals
        iltcme_params = get_iltcme()
        params = iltcme_params[0]
        for p in iltcme_params:
            if p["cv2"] < params["cv2"] and (p["n"] + 1) <= max_fun_evals:
                params = p
        eta = (
            np.concatenate(
                ([params["c"]], np.array(params["a"]) + 1j * np.array(params["b"]))
            )
            * params["mu1"]
        )
        # print('eta:', eta)
        beta = (
            np.concatenate(
                ([1], 1 + 1j * np.arange(1, params["n"] + 1) * params["omega"])
            )
            * params["mu1"]
        )
        # print('beta:', beta)
    elif method == "euler":
        n_euler = math.floor((max_fun_evals - 1) / 2)
        eta = np.concatenate(
            ([0.5], np.ones(n_euler), np.zeros(n_euler - 1), [2**-n_euler])
        )
        logsum = np.cumsum(np.log(np.arange(1, n_euler + 1)))
        for k in range(1, n_euler):
            eta[2 * n_euler - k] = eta[2 * n_euler - k + 1] + math.exp(
                logsum[n_euler - 1]
                - n_euler * math.log(2.0)
                - logsum[k - 1]
                - logsum[n_euler - k - 1]
            )
        k = np.arange(2 * n_euler + 1)
        beta = n_euler * math.log(10.0) / 3.0 + 1j * math.pi * k
        eta = (10 ** ((n_euler) / 3.0)) * (1 - (k % 2) * 2) * eta
    elif method == "gaver":
        if max_fun_evals % 2 == 1:
            max_fun_evals -= 1
        ndiv2 = int(max_fun_evals / 2)
        eta = np.zeros(max_fun_evals)
        beta = np.zeros(max_fun_evals)
        logsum = np.concatenate(
            ([0], np.cumsum(np.log(np.arange(1, max_fun_evals + 1))))
        )
        for k in range(1, max_fun_evals + 1):
            insisum = 0.0
            for j in range(math.floor((k + 1) / 2), min(k, ndiv2) + 1):
                insisum += math.exp(
                    (ndiv2 + 1) * math.log(j)
                    - logsum[ndiv2 - j]
                    + logsum[2 * j]
                    - 2 * logsum[j]
                    - logsum[k - j]
                    - logsum[2 * j - k]
                )
            eta[k - 1] = math.log(2.0) * (-1) ** (k + ndiv2) * insisum
            beta[k - 1] = k * math.log(2.0)
    else:
        raise TypeError("Unknown Laplace inversion method.")
    res = []
    for x in t:
        res.append(eta.dot([fun(b / x) for b in beta]).real / x)
    return res


def cme(fun, t, k):
    """
    Numerical Laplace transform inversion using the method of reference [1].

    Is based on code downloaded from https://inverselaplace.org/ in June of 2021.

    [1] Horváth, G., Horváth, I., Almousa, S. A. D., & Telek, M. (2020).
    Numerical inverse Laplace transformation using concentrated matrix
    exponential distributions. Performance Evaluation, 137, 102067.
    """
    if "cme_params" not in globals() or globals()["cme_k"] != k:
        global cme_params
        global cme_k
        global cme_eta
        global cme_beta
        cme_k = k
        iltcme_params = get_iltcme()
        cme_params = iltcme_params[0]
        for p in iltcme_params:
            if p["cv2"] < cme_params["cv2"] and (p["n"] + 1) <= cme_k:
                cme_params = p
        cme_eta = [
            cme_params["a"][idx] + 1j * cme_params["b"][idx]
            for idx in range(cme_params["n"])
        ]
        cme_eta.insert(0, cme_params["c"] + 1j * 0)
        cme_eta = mp.matrix(cme_eta).T * cme_params["mu1"]
        cme_beta = [
            1 + 1j * idx * cme_params["omega"] for idx in range(1, cme_params["n"] + 1)
        ]
        cme_beta.insert(0, 1)
        cme_beta = mp.matrix(cme_beta) * cme_params["mu1"]
    foo = mp.matrix([fun(mp.fdiv(b, t)) for b in cme_beta])
    foo = cme_eta * foo
    return float(mp.fdiv(mp.re(foo[0]), t))


def laplace_invert(fun, t, **options):
    """
    Numerical Laplace transform inversion using a variety of different methods.
    """
    if "laplace_method" in options.keys():
        laplace_method = options["laplace_method"]
    else:
        laplace_method = "cme-mp"
    if "k" in options.keys():
        k = options["k"]
    else:
        k = 25
    if laplace_method == "gwr":
        return float(gwr(fun, time=t, M=k))
    elif laplace_method in ["cme", "euler", "gaver"]:
        return laplace_invert_mexp(fun, [t], k, method=laplace_method)[0]
    elif laplace_method in ["talbot", "stehfest", "dehoog"]:
        return float(mp.invertlaplace(fun, t, method=laplace_method))
    elif laplace_method == "cme-mp":
        return cme(fun, t, k)
    elif laplace_method == "cme-talbot":
        if "f_bounds" in options.keys():
            f_min = options["f_bounds"][0]
            f_max = options["f_bounds"][1]
        else:
            raise TypeError(
                "When 'laplace_method' is 'cme-talbot', kwarg"
                "'f_bounds' must be given as a list [f_min, f_max]."
                "When method 'cme-mp' returns a value outside "
                "these bounds, method 'talbot' is used instead. "
            )
        output = cme(fun, t, k)
        if f_min <= output <= f_max:
            return output
        else:
            return float(mp.invertlaplace(fun, t, method="talbot"))
    else:
        raise TypeError("Unknown Laplace inversion method.")


def add_options(options):
    """
    Completes the 'options' dictionary for use in :func:`minimize_`.
    Is needed to enable BirDePy to specify default values for options
    in the SciPy optimization functions.
    """
    if "seed" not in options.keys():
        options["seed"] = np.random.default_rng()
    if "strategy" not in options.keys():
        options["strategy"] = "best1bin"
    if "maxiter" not in options.keys():
        options["maxiter"] = 1000
    if "popsize" not in options.keys():
        options["popsize"] = 15
    if "tol" not in options.keys():
        options["tol"] = 0.01
    if "mutation" not in options.keys():
        options["mutation"] = 0.5
    if "recombination" not in options.keys():
        options["recombination"] = 0.7
    if "callback" not in options.keys():
        options["callback"] = None
    if "disp" not in options.keys():
        options["disp"] = False
    if "polish" not in options.keys():
        options["polish"] = True
    if "init" not in options.keys():
        options["init"] = "latinhypercube"
    if "atol" not in options.keys():
        options["atol"] = 0
    if "updating" not in options.keys():
        options["updating"] = "immediate"
    if "jac" not in options.keys():
        options["jac"] = "2-point"
    if "maxcor" not in options.keys():
        options["maxcor"] = 10
    if "ftol" not in options.keys():
        options["ftol"] = 2.220446049250313e-09
    if "gtol" not in options.keys():
        options["gtol"] = 1e-05
    if "eps" not in options.keys():
        options["eps"] = None
    if "maxfun" not in options.keys():
        options["maxfun"] = 15000
    if "maxiter" not in options.keys():
        options["maxiter"] = 15000
    if "iprint" not in options.keys():
        options["iprint"] = -1
    if "callback" not in options.keys():
        options["callback"] = None
    if "maxls" not in options.keys():
        options["maxls"] = 20
    if "finite_diff_rel_step" not in options.keys():
        options["finite_diff_rel_step"] = None
    if "ftol" not in options.keys():
        options["ftol"] = 1e-06
    if "eps" not in options.keys():
        options["eps"] = 1.4901161193847656e-08
    if "disp" not in options.keys():
        options["disp"] = False
    if "maxiter" not in options.keys():
        options["maxiter"] = 100
    if "finite_diff_rel_step" not in options.keys():
        options["finite_diff_rel_step"] = None
    if "jac" not in options.keys():
        options["jac"] = None
    return options


def minimize_(error_fun, p0, p_bounds, con, opt_method, options):
    """
    Minimizes 'error_fun' using the method specified by 'opt_method'.
    """
    if opt_method == "differential-evolution":
        if con != ():
            old_con = con
            con = NonlinearConstraint(con["fun"], 0, np.inf)
        sol = differential_evolution(
            error_fun,
            p_bounds,
            args=(),
            strategy=options["strategy"],
            maxiter=options["maxiter"],
            popsize=options["popsize"],
            tol=options["tol"],
            mutation=options["mutation"],
            recombination=options["recombination"],
            seed=options["seed"],
            callback=options["callback"],
            disp=options["disp"],
            polish=False,
            init=options["init"],
            atol=options["atol"],
            updating=options["updating"],
            constraints=con,
        )
        if options["polish"]:
            if con != ():
                sol = minimize(
                    error_fun,
                    sol.x,
                    method="SLSQP",
                    bounds=p_bounds,
                    constraints=old_con,
                )
            else:
                sol = minimize(error_fun, sol.x, method="L-BFGS-B", bounds=p_bounds)
    elif opt_method == "L-BFGS-B":
        sol = minimize(
            error_fun,
            p0,
            method=opt_method,
            bounds=p_bounds,
            callback=options["callback"],
            jac=options["jac"],
            options={
                "maxcor": options["maxcor"],
                "ftol": options["ftol"],
                "gtol": options["gtol"],
                "eps": options["eps"],
                "maxfun": options["maxfun"],
                "maxiter": options["maxiter"],
                "iprint": options["iprint"],
                "maxls": options["maxls"],
                "finite_diff_rel_step": options["finite_diff_rel_step"],
            },
        )
    elif opt_method == "SLSQP":
        sol = minimize(
            error_fun,
            p0,
            method=opt_method,
            bounds=p_bounds,
            constraints=con,
            jac=options["jac"],
            options={
                "ftol": options["ftol"],
                "eps": options["eps"],
                "disp": options["disp"],
                "maxiter": options["maxiter"],
                "finite_diff_rel_step": options["finite_diff_rel_step"],
            },
        )
    else:
        sol = minimize(
            error_fun, p0, method=opt_method, bounds=p_bounds, constraints=con
        )
    return sol


def trap_int(y, x):
    """
    Numerical integration of sampled data using the trapezoidal rule.

    Parameters
    ----------
    y : array_like
        Function values.

    x : array_like
        Domain values.

    Return
    -------
    res : ndarray
        Cumulative integral of `y` at points in `x`.
    """
    x = np.array(x)
    y = np.array(y)
    res = np.hstack(
        (
            [0],
            np.cumsum(
                np.multiply(0.5, np.multiply(np.diff(x), np.add(y[1:], y[0:-1])))
            ),
        )
    )
    return res


def get_iltcme():
    with resources.open_text("birdepy.data_files", "iltcme.json") as data_file:
        data = json.load(data_file)
    return data
