Example #1
0
    def add_fit(self, data, **kwargs):
        """
            Add an individual FitResult. By default, the part of the fit that
            contributed to the fitting is drawn solid, the remaining range
            is dashed. Note that it is not possible
            to add the same data twice, instead it will be redrawn with
            the new arguments/style options provided.

            Parameters
            ----------
            data : FitResult
                Added to the list of plotted elements.

            kwargs
                Keyword arguments passed to
                :obj:`matplotlib.axes.Axes.plot`. Use to customise the
                plots. If a `label` is set via `kwargs`, it will be added
                as a note in the meta data. If `linestyle` is set, the
                dashed plot of the region not contributing to the fit is
                omitted.
        """
        if not isinstance(data, FitResult):
            log.exception("'data' needs to be of type FitResult")
            raise ValueError
        if not (self.type is None or self.type == 'correlation'):
            log.exception(
                "It is not possible to 'add_fit()' to " +
                "an OutputHandler containing a time series\n" +
                "\tHave you previously called 'add_ts()' on this handler?")
            raise ValueError
        self.type = 'correlation'

        if self.xdata is None:
            self.dt = data.dt
            self.dtunit = data.dtunit
            self.ax.set_xlabel('k [{}{}]'.format(data.dt, data.dtunit))
            self.ax.set_ylabel('$r_{k}$')
            self.ax.set_title('Correlation')
        inds = self.set_xdata(data.steps, dt=data.dt, dtunit=data.dtunit)

        # description for fallback
        desc = str(data.desc)

        # plot legend label
        if 'label' in kwargs:
            label = kwargs.get('label')
            if label == '':
                label = None
            else:
                # user wants custom label not intended to hide the legend
                label = str(label)
        else:
            # user has not set anything, copy from desc if set
            label = 'Fit ' + ut.math_from_doc(data.fitfunc, 0)
            if desc != '':
                label = desc + ' ' + label

        # we dont support adding duplicates
        oldcurves = []
        if data in self.fits:
            indfit = self.fits.index(data)
            log.warning(
                'Fit was already added ({})\n'.format(self.fitlabels[indfit]) +
                '\tOverwriting with new style')
            del self.fits[indfit]
            del self.fitlabels[indfit]
            oldcurves = self.fitcurves[indfit]
            del self.fitcurves[indfit]
            del self.fitkwargs[indfit]

        self.fits.append(data)
        self.fitlabels.append(label)
        self.fitcurves.append(oldcurves)
        self.fitkwargs.append(kwargs)

        # refresh coefficients
        for r in self.rks:
            self._render_coefficients(r)

        # refresh fits
        for f in self.fits:
            self._render_fit(f)
Example #2
0
def overview(src, rks, fits, **kwargs):
    """
        creates an A4 overview panel and returns the matplotlib figure element.
        No Argument checks are done
    """

    # ratios = np.ones(4)*.75
    # ratios[3] = 0.25
    ratios = None
    # A4 in inches, should check rc params in the future
    # matplotlib changes the figure size when modifying subplots
    topshift = 0.925
    fig, axes = plt.subplots(nrows=4,
                             figsize=(8.27, 11.69 * topshift),
                             gridspec_kw={"height_ratios": ratios})

    # avoid huge file size for many trials due to separate layers.
    # everything below 0 gets rastered to the same layer.
    axes[0].set_rasterization_zorder(0)

    # ------------------------------------------------------------------ #
    # Time Series
    # ------------------------------------------------------------------ #

    tsout = OutputHandler(ax=axes[0])
    tsout.add_ts(src, label='Trials')
    if (src.shape[0] > 1):
        try:
            prevclr = plt.rcParams["axes.prop_cycle"].by_key()["color"][0]
        except Exception:
            prevclr = 'navy'
            log.debug('Exception getting color cycle', exc_info=True)
        tsout.add_ts(np.mean(src, axis=0), color=prevclr, label='Average')
    else:
        tsout.ax.legend().set_visible(False)

    tsout.ax.set_title('Time Series (Input Data)')
    tsout.ax.set_xlabel('t [{}{}]'.format(ut._printeger(rks[0].dt),
                                          rks[0].dtunit))

    # ------------------------------------------------------------------ #
    # Mean Trial Activity
    # ------------------------------------------------------------------ #

    if (src.shape[0] > 1):
        # average trial activites as function of trial number
        taout = OutputHandler(rks[0].trialactivities, ax=axes[1])
        try:
            err1 = rks[0].trialactivities - np.sqrt(rks[0].trialvariances)
            err2 = rks[0].trialactivities + np.sqrt(rks[0].trialvariances)
            prevclr = plt.rcParams["axes.prop_cycle"].by_key()["color"][0]
            taout.ax.fill_between(np.arange(1, rks[0].numtrials + 1),
                                  err1,
                                  err2,
                                  color=prevclr,
                                  alpha=0.2)
        except Exception as e:
            log.debug('Exception adding std deviation to plot', exc_info=True)
        taout.ax.set_title('Mean Trial Activity and Std. Deviation')
        taout.ax.set_xlabel('Trial i')
        taout.ax.set_ylabel('$\\bar{A}_i$')
    else:
        # running average over the one trial to see if stays stationary
        numsegs = kwargs.get(numsegs) if 'numsegs' in kwargs else 50
        ravg = np.zeros(numsegs)
        err1 = np.zeros(numsegs)
        err2 = np.zeros(numsegs)
        seglen = int(src.shape[1] / numsegs)
        for s in range(numsegs):
            temp = np.mean(src[0][s * seglen:(s + 1) * seglen])
            ravg[s] = temp
            stddev = np.sqrt(np.var(src[0][s * seglen:(s + 1) * seglen]))
            err1[s] = temp - stddev
            err2[s] = temp + stddev

        taout = OutputHandler(ravg, ax=axes[1])
        try:
            prevclr = plt.rcParams["axes.prop_cycle"].by_key()["color"][0]
            taout.ax.fill_between(np.arange(1, numsegs + 1),
                                  err1,
                                  err2,
                                  color=prevclr,
                                  alpha=0.2)
        except Exception as e:
            log.debug('Exception adding std deviation to plot', exc_info=True)
        taout.ax.set_title(
            'Average Activity and Stddev for {} Intervals'.format(numsegs))
        taout.ax.set_xlabel('Interval i')
        taout.ax.set_ylabel('$\\bar{A}_i$')

    # ------------------------------------------------------------------ #
    # Coefficients and Fit results
    # ------------------------------------------------------------------ #

    cout = OutputHandler(rks + fits, ax=axes[2])

    fitcurves = []
    fitlabels = []
    for i, f in enumerate(cout.fits):
        fitcurves.append(cout.fitcurves[i][0])
        label = ut.math_from_doc(f.fitfunc, 5)
        label += '\n\n$\\tau={:.2f}${}\n'.format(f.tau, f.dtunit)
        if f.tauquantiles is not None:
            label += '$[{:.2f}:{:.2f}]$\n\n' \
                .format(f.tauquantiles[0], f.tauquantiles[-1])
        else:
            label += '\n\n'
        label += '$m={:.5f}$\n'.format(f.mre)
        if f.mrequantiles is not None:
            label +='$[{:.5f}:{:.5f}]$' \
                .format(f.mrequantiles[0], f.mrequantiles[-1])
        else:
            label += '\n'
        fitlabels.append(label)

    tempkwargs = {
        # 'title': 'Fitresults',
        'ncol': len(fitlabels),
        'loc': 'upper center',
        'mode': 'expand',
        'frameon': True,
        'markerfirst': True,
        'fancybox': False,
        # 'framealpha': 1,
        'borderaxespad': 0,
        'edgecolor': 'black',
        # hide handles
        'handlelength': 0,
        'handletextpad': 0,
    }
    try:
        axes[3].legend(fitcurves, fitlabels, **tempkwargs)
    except Exception:
        log.debug('Exception passed', exc_info=True)
        del tempkwargs['edgecolor']
        axes[3].legend(fitcurves, fitlabels, **tempkwargs)

    # hide handles
    for handle in axes[3].get_legend().legendHandles:
        handle.set_visible(False)

    # center text
    for t in axes[3].get_legend().texts:
        t.set_multialignment('center')

    # apply stile and fill legend
    axes[3].get_legend().get_frame().set_linewidth(0.5)
    axes[3].axis('off')
    axes[3].set_title('Fitresults\n[$12.5\\%$:$87.5\\%$]')
    for a in axes:
        a.xaxis.set_tick_params(width=0.5)
        a.yaxis.set_tick_params(width=0.5)
        for s in a.spines:
            a.spines[s].set_linewidth(0.5)

    fig.tight_layout(h_pad=2.0)
    plt.subplots_adjust(top=topshift)
    title = kwargs.get('title') if 'title' in kwargs else None
    if (title is not None and title != ''):
        fig.suptitle(title + '\n', fontsize=14)

    if 'warning' in kwargs and kwargs.get('warning') is not None:
        s = u'\u26A0 {}'.format(kwargs.get('warning'))
        fig.text(.5,
                 .01,
                 s,
                 fontsize=13,
                 horizontalalignment='center',
                 color='red')

    fig.text(.995,
             .005,
             'v{}'.format(__version__),
             fontsize=8,
             horizontalalignment='right',
             color='silver')
    return fig
Example #3
0
    def save_meta(self, fname=''):
        """
            Saves only the details/source used to create the plot. It is
            recommended to call this manually, if you decide to save
            the plots yourself or when you want only the fit results.

            Parameters
            ----------
            fname : str, optional
                Path where to save, without file extension. Defaults to "./mre"
        """
        if not isinstance(fname, str): fname = str(fname)
        if fname == '': fname = './mre'

        # try creating enclosing dir if not existing
        tempdir = os.path.abspath(os.path.expanduser(fname + "/../"))
        os.makedirs(tempdir, exist_ok=True)

        fname = os.path.expanduser(fname)

        log.info('Saving meta to {}.tsv'.format(fname))
        # fits
        hdr = 'Mr. Estimator v{}\n'.format(__version__)
        try:
            for fdx, fit in enumerate(self.fits):
                hdr += '{}\n'.format('-' * 72)
                hdr += 'legendlabel: ' + str(self.fitlabels[fdx]) + '\n'
                hdr += '{}\n'.format('-' * 72)
                if fit.desc != '':
                    hdr += 'description: ' + str(fit.desc) + '\n'
                hdr += 'm = {}\ntau = {} [{}]\n' \
                    .format(fit.mre, fit.tau, fit.dtunit)
                if fit.quantiles is not None:
                    hdr += 'quantiles | tau [{}] | m:\n'.format(fit.dtunit)
                    for i, q in enumerate(fit.quantiles):
                        hdr += '{:6.3f} | '.format(fit.quantiles[i])
                        hdr += '{:8.3f} | '.format(fit.tauquantiles[i])
                        hdr += '{:8.8f}\n'.format(fit.mrequantiles[i])
                    hdr += '\n'
                hdr += 'fitrange: {} <= k <= {} [{}{}]\n'.format(
                    fit.steps[0], fit.steps[-1], ut._printeger(fit.dt),
                    fit.dtunit)
                hdr += 'function: ' + ut.math_from_doc(fit.fitfunc) + '\n'
                # hdr += '\twith parameters:\n'
                parname = list(inspect.signature(fit.fitfunc).parameters)[1:]
                parlen = len(max(parname, key=len))
                for pdx, par in enumerate(self.fits[fdx].popt):
                    unit = ''
                    if parname[pdx] == 'nu':
                        unit += '[1/{}]'.format(fit.dtunit)
                    elif parname[pdx].find('tau') != -1:
                        unit += '[{}]'.format(fit.dtunit)
                    hdr += '\t{: <{width}}'.format(parname[pdx] + ' ' + unit,
                                                   width=parlen + 5 +
                                                   len(fit.dtunit))
                    hdr += ' = {}\n'.format(par)
                hdr += '\n'
        except Exception as e:
            log.debug('Exception passed', exc_info=True)

        # rks / ts
        labels = ''
        dat = []
        if self.ydata is not None and len(self.ydata) != 0:
            hdr += '{}\n'.format('-' * 72)
            hdr += 'Data\n'
            hdr += '{}\n'.format('-' * 72)
            labels += '1_' + self.xlabel
            for ldx, label in enumerate(self.ylabels):
                labels += '\t' + str(ldx + 2) + '_' + label
            labels = labels.replace(' ', '_')
            dat = np.vstack((self.xdata, np.asarray(self.ydata)))
        np.savetxt(fname + '.tsv',
                   np.transpose(dat),
                   delimiter='\t',
                   header=hdr + labels)
Example #4
0
def overview(src, rks, fits, **kwargs):
    """
        creates an A4 overview panel and returns the matplotlib figure element.
        No Argument checks are done
    """

    ratios = np.ones(5)
    ratios[4] = 0.0001
    # ratios=None
    # A5 in inches, should check rc params in the future
    # matplotlib changes the figure size when modifying subplots
    fig, axes = plt.subplots(nrows=5,
                             figsize=(5.8, 8.3),
                             gridspec_kw={"height_ratios": ratios})

    # avoid huge file size for many trials due to separate layers.
    # everything below 0 gets rastered to the same layer.
    axes[0].set_rasterization_zorder(0)

    # ------------------------------------------------------------------ #
    # Time Series
    # ------------------------------------------------------------------ #

    tsout = OutputHandler(ax=axes[0])
    tsout.add_ts(src, label="Trials")
    if src.shape[0] > 1:
        try:
            prevclr = plt.rcParams["axes.prop_cycle"].by_key()["color"][0]
        except Exception:
            prevclr = "navy"
            log.debug("Exception getting color cycle", exc_info=True)
        tsout.add_ts(np.mean(src, axis=0), color=prevclr, label="Average")
    else:
        tsout.ax.legend().set_visible(False)

    tsout.ax.set_title("Time Series", fontweight="bold", loc="center")
    tsout.ax.set_title("(Input Data)",
                       fontsize="medium",
                       color="#646464",
                       loc="right")
    tsout.ax.set_xlabel("t [{}{}]".format(
        ut._printeger(rks[0].dt) + " " if rks[0].dt != 1 else "",
        rks[0].dtunit))

    # ------------------------------------------------------------------ #
    # Mean Trial Activity
    # ------------------------------------------------------------------ #

    if src.shape[0] > 1:
        # average trial activites as function of trial number
        taout = OutputHandler(rks[0].trialactivities, ax=axes[1])
        try:
            err1 = rks[0].trialactivities - np.sqrt(rks[0].trialvariances)
            err2 = rks[0].trialactivities + np.sqrt(rks[0].trialvariances)
            prevclr = plt.rcParams["axes.prop_cycle"].by_key()["color"][0]
            taout.ax.fill_between(np.arange(1, rks[0].numtrials + 1),
                                  err1,
                                  err2,
                                  color=prevclr,
                                  alpha=0.2)
        except Exception as e:
            log.debug("Exception adding std deviation to plot", exc_info=True)
        taout.ax.set_title("Mean Trial Activity and Std. Deviation",
                           fontweight="bold")
        taout.ax.set_xlabel("Trial i")
        taout.ax.set_ylabel("$\\bar{A}_i$")
    else:
        # running average over the one trial to see if stays stationary
        numsegs = kwargs.get(numsegs) if "numsegs" in kwargs else 50
        ravg = np.zeros(numsegs)
        err1 = np.zeros(numsegs)
        err2 = np.zeros(numsegs)
        seglen = int(src.shape[1] / numsegs)
        for s in range(numsegs):
            temp = np.mean(src[0][s * seglen:(s + 1) * seglen])
            ravg[s] = temp
            stddev = np.sqrt(np.var(src[0][s * seglen:(s + 1) * seglen]))
            err1[s] = temp - stddev
            err2[s] = temp + stddev

        taout = OutputHandler(ravg, ax=axes[1])
        try:
            prevclr = plt.rcParams["axes.prop_cycle"].by_key()["color"][0]
            taout.ax.fill_between(np.arange(1, numsegs + 1),
                                  err1,
                                  err2,
                                  color=prevclr,
                                  alpha=0.2)
        except Exception as e:
            log.debug("Exception adding std deviation to plot", exc_info=True)
        taout.ax.set_title(
            "Average Activity and Stddev for {} Intervals".format(numsegs),
            fontweight="bold",
        )
        taout.ax.set_xlabel("Interval i")
        taout.ax.set_ylabel("$\\bar{A}_i$")

    # ------------------------------------------------------------------ #
    # Coefficients and Fit results
    # ------------------------------------------------------------------ #

    cout = OutputHandler(rks + fits, ax=axes[2])

    fitcurves = []
    fitlabels = []
    for i, f in enumerate(cout.fits):
        fitcurves.append(cout.fitcurves[i][0])
        label = ut.math_from_doc(f.fitfunc, 5)
        label += "\n\n$\\tau={:.2f}${}\n".format(f.tau, f.dtunit)
        if f.tauquantiles is not None:
            label += "$[{:.2f}:{:.2f}]$\n\n".format(f.tauquantiles[0],
                                                    f.tauquantiles[-1])
        else:
            label += "\n\n"
        label += "$m={:.5f}$\n".format(f.mre)
        if f.mrequantiles is not None:
            label += "$[{:.5f}:{:.5f}]$".format(f.mrequantiles[0],
                                                f.mrequantiles[-1])
        else:
            label += "\n"
        fitlabels.append(label)

    tempkwargs = {
        # 'title': 'Fitresults',
        "ncol": len(fitlabels),
        "loc": "upper center",
        "mode": "expand",
        "frameon": True,
        "markerfirst": True,
        "fancybox": False,
        # 'framealpha': 1,
        "borderaxespad": 0,
        "edgecolor": "black",
        # hide handles
        "handlelength": 0,
        "handletextpad": 0,
    }
    try:
        axes[3].legend(fitcurves, fitlabels, **tempkwargs)
    except Exception:
        log.debug("Exception passed", exc_info=True)
        del tempkwargs["edgecolor"]
        axes[3].legend(fitcurves, fitlabels, **tempkwargs)

    # hide handles
    for handle in axes[3].get_legend().legendHandles:
        handle.set_visible(False)

    # center text
    for t in axes[3].get_legend().texts:
        t.set_multialignment("center")

    # apply stile and fill legend
    axes[3].get_legend().get_frame().set_linewidth(0.5)
    axes[3].axis("off")
    axes[3].set_title(
        "Fitresults",
        fontweight="bold",
        loc="center",
    )
    axes[3].set_title(
        " (with CI: [$12.5\\%:87.5\\%$])",
        color="#646464",
        fontsize="medium",
        loc="right",
    )
    for a in axes:
        a.xaxis.set_tick_params(width=0.5)
        a.yaxis.set_tick_params(width=0.5)
        for s in a.spines:
            a.spines[s].set_linewidth(0.5)

    # dummy axes for version and warnings
    axes[4].axis("off")

    fig.tight_layout()
    plt.subplots_adjust(hspace=0.8, top=0.95, bottom=0.0, left=0.1, right=0.99)

    title = kwargs.get("title") if "title" in kwargs else None
    if title is not None and title != "":
        fig.suptitle(title, fontsize=14)
        plt.subplots_adjust(top=0.91)

    if "warning" in kwargs and kwargs.get("warning") is not None:
        s = "\u26A0 {}".format(kwargs.get("warning"))
        fig.text(0.5,
                 0.01,
                 s,
                 fontsize=13,
                 horizontalalignment="center",
                 color="red")

    fig.text(
        0.995,
        0.005,
        "v{}".format(__version__),
        fontsize="small",
        horizontalalignment="right",
        color="#646464",
    )

    return fig
Example #5
0
    def save_meta(self, fname=""):
        """
            Saves only the details/source used to create the plot. It is
            recommended to call this manually, if you decide to save
            the plots yourself or when you want only the fit results.

            Parameters
            ----------
            fname : str, optional
                Path where to save, without file extension. Defaults to "./mre"
        """
        if not isinstance(fname, str):
            fname = str(fname)
        if fname == "":
            fname = "./mre"

        # try creating enclosing dir if not existing
        tempdir = os.path.abspath(os.path.expanduser(fname + "/../"))
        os.makedirs(tempdir, exist_ok=True)

        fname = os.path.expanduser(fname)

        log.info("Saving meta to {}.tsv".format(fname))
        # fits
        hdr = "Mr. Estimator v{}\n".format(__version__)
        try:
            for fdx, fit in enumerate(self.fits):
                hdr += "{}\n".format("-" * 72)
                hdr += "legendlabel: " + str(self.fitlabels[fdx]) + "\n"
                hdr += "{}\n".format("-" * 72)
                if fit.desc != "":
                    hdr += "description: " + str(fit.desc) + "\n"
                hdr += "m = {}\ntau = {} [{}]\n".format(
                    fit.mre, fit.tau, fit.dtunit)
                if fit.quantiles is not None:
                    hdr += "quantiles | tau [{}] | m:\n".format(fit.dtunit)
                    for i, q in enumerate(fit.quantiles):
                        hdr += "{:6.3f} | ".format(fit.quantiles[i])
                        hdr += "{:8.3f} | ".format(fit.tauquantiles[i])
                        hdr += "{:8.8f}\n".format(fit.mrequantiles[i])
                    hdr += "\n"
                hdr += "fitrange: {} <= k <= {} [{} {}]\n".format(
                    fit.steps[0], fit.steps[-1], ut._printeger(fit.dt),
                    fit.dtunit)
                hdr += "function: " + ut.math_from_doc(fit.fitfunc) + "\n"
                # hdr += '\twith parameters:\n'
                parname = list(inspect.signature(fit.fitfunc).parameters)[1:]
                parlen = len(max(parname, key=len))
                for pdx, par in enumerate(self.fits[fdx].popt):
                    unit = ""
                    if parname[pdx] == "nu":
                        unit += "[1/{}]".format(fit.dtunit)
                    elif parname[pdx].find("tau") != -1:
                        unit += "[{}]".format(fit.dtunit)
                    hdr += "\t{: <{width}}".format(parname[pdx] + " " + unit,
                                                   width=parlen + 5 +
                                                   len(fit.dtunit))
                    hdr += " = {}\n".format(par)
                hdr += "\n"
        except Exception as e:
            log.debug("Exception passed", exc_info=True)

        # rks / ts
        labels = ""
        dat = []
        if self.ydata is not None and len(self.ydata) != 0:
            hdr += "{}\n".format("-" * 72)
            hdr += "Data\n"
            hdr += "{}\n".format("-" * 72)
            labels += "1_" + self.xlabel
            for ldx, label in enumerate(self.ylabels):
                labels += "\t" + str(ldx + 2) + "_" + label
            labels = labels.replace(" ", "_")
            dat = np.vstack((self.xdata, np.asarray(self.ydata)))
        np.savetxt(fname + ".tsv",
                   np.transpose(dat),
                   delimiter="\t",
                   header=hdr + labels)
Example #6
0
def fit(data,
        fitfunc=f_exponential_offset,
        steps=None,
        fitpars=None,
        fitbnds=None,
        maxfev=None,
        ignoreweights=True,
        numboot=0,
        quantiles=None,
        seed=101,
        desc=None,
        description=None):
    """
        Estimate the Multistep Regression Estimator by fitting the provided
        correlation coefficients :math:`r_k`. The fit is performed using
        :func:`scipy.optimize.curve_fit` and can optionally be provided with
        (multiple) starting fitparameters and bounds.

        Parameters
        ----------
        data: CoefficientResult or ~numpy.array
            Correlation coefficients to fit. Ideally, provide this as
            :class:`CoefficientResult` as obtained from
            :func:`coefficients`. If arrays are provided,
            the function tries to match the data.

        fitfunc : callable, optional
            The model function, f(x, …).
            Directly passed to `curve_fit()`:
            It must take the independent variable as
            the first argument and the parameters to fit as separate remaining
            arguments.
            Default is :obj:`f_exponential_offset`.
            Other builtin options are :obj:`f_exponential` and
            :obj:`f_complex`.

        steps : ~numpy.array, optional
            Specify the steps :math:`k` for which to fit (think fitrange).
            If an array of length two is provided, e.g.
            ``steps=(minstep, maxstep)``, all enclosed values present in the
            provdied `data`, including `minstep` and `maxstep` will be used.
            Arrays larger than two are assumed to contain a manual choice of
            steps and those that are also present in `data` will be used.
            Strides other than one are possible.
            Ignored if `data` is not passed as CoefficientResult.
            Default: all values given in `data` are included in the fit.

        Other Parameters
        ----------------
        fitpars : ~numpy.ndarray, optional
            The starting parameters for the fit. If the provided array is two
            dimensional, multiple fits are performed and the one with the
            smallest sum of squares of residuals is returned.

        fitbounds : ~numpy.ndarray, optional
            Lower and upper bounds for each parameter handed to the fitting
            routine. Provide as numpy array of the form
            ``[[lowpar1, lowpar2, ...], [uppar1, uppar2, ...]]``

        numboot : int, optional
            Number of bootstrap samples to compute errors from. Default is 0

        seed : int, None or 'random', optional
            If `numboot` is not zero, provide a seed for the random number
            generator. If ``seed=None``, seeding will be skipped.
            Per default, the rng is (re)seeded everytime `fit()` is called so
            that every repeated call returns the same error estimates.

        quantiles: list, optional
            If `numboot` is not zero, provide the quantiles to return
            (between 0 and 1). See :obj:`numpy.quantile`.
            Defaults are ``[.125, .25, .4, .5, .6, .75, .875]``

        maxfev : int, optional
            Maximum iterations for the fit.

        description : str, optional
            Provide a custom description.

        Returns
        -------
        : :class:`FitResult`
            The output is grouped and can be accessed
            using its attributes (listed below).
    """
    # ------------------------------------------------------------------ #
    # Check arguments and prepare
    # ------------------------------------------------------------------ #

    log.debug('fit()')
    if (ut._log_locals):
        log.debug('Locals: {}'.format(locals()))

    fitfunc = fitfunc_check(fitfunc)

    # check input data type
    if isinstance(data, CoefficientResult):
        log.debug('Coefficients given in default format')
        src = data
        srcerrs = data.stderrs
        dt = data.dt
        dtunit = data.dtunit
    else:
        try:
            log.info("Given data is no CoefficientResult. Guessing format")
            dt = 1
            dtunit = 'ms'
            srcerrs = None
            data = np.asarray(data)
            if len(data.shape) == 1:
                log.debug('1d array, assuming this to be coefficients')
                if steps is not None and len(steps) == len(data):
                    log.debug("using steps provided in 'steps'")
                    tempsteps = np.copy(steps)
                else:
                    log.debug("using linear steps starting at 1")
                    tempsteps = np.arange(1, len(data) + 1)
                src = CoefficientResult(coefficients=data, steps=tempsteps)
            elif len(data.shape) == 2:
                if data.shape[0] > data.shape[1]: data = np.transpose(data)
                if data.shape[0] == 1:
                    log.debug('nested 1d array, assuming coefficients')
                    if steps is not None and len(steps) == len(data[0]):
                        log.debug("using steps provided in 'steps'")
                        tempsteps = np.copy(steps)
                    else:
                        log.debug("using steps linear steps starting at 1")
                        tempsteps = np.arange(1, len(data[0]) + 1)
                    src = CoefficientResult(coefficients=data[0],
                                            steps=tempsteps)
                elif data.shape[0] == 2:
                    log.debug('2d array, assuming this to be ' +
                              'steps and coefficients')
                    tempsteps = data[0]
                    src = CoefficientResult(coefficients=data[1],
                                            steps=tempsteps)
            else:
                raise TypeError
        except Exception as e:
            log.exception('Provided data has no compatible format')
            raise

    # check that input coefficients do not contain nans or infs
    if not np.isfinite(src.coefficients).all():
        log.exception(
            "Provided coefficients contain elements that are not finite. " +
            "Fits would not converge.\n" +
            "One can use `np.isfinite(data.coefficients)` to find problematic elements."
        )
        raise ValueError

    # check steps
    if steps is None:
        steps = (None, None)
    try:
        steps = np.array(steps)
        assert len(steps.shape) == 1
    except Exception as e:
        log.exception('Please provide steps as ' +
                      'steps=(minstep, maxstep) or as one dimensional numpy ' +
                      'array containing all desired step values')
        raise ValueError from e
    if len(steps) == 2:
        minstep = src.steps[0]  # default: use what is in the given data
        maxstep = src.steps[-1]
        if steps[0] is not None:
            minstep = steps[0]
        if steps[1] is not None:
            maxstep = steps[1]
        if minstep > maxstep or minstep < 1:
            log.debug('minstep={} is invalid, setting to 1'.format(minstep))
            minstep = 1
        if maxstep > src.steps[-1] or maxstep < minstep:
            log.debug('maxstep={} is invalid'.format(maxstep))
            maxstep = src.steps[-1]
            log.debug('Adjusting maxstep to {}'.format(maxstep))

        steps = np.arange(minstep, maxstep + 1, dtype=int)
        log.debug('Checking steps between {} and {}'.format(minstep, maxstep))
    else:
        if (steps < 1).any():
            log.exception('All provided steps must be >= 1')
            raise ValueError
        steps = np.asarray(steps, dtype=int)
        log.debug('Using provided custom steps')

    # make sure this is data, no pointer, so we dont overwrite anything
    stepinds, _ = ut._intersecting_index(src.steps, steps)
    srcsteps = np.copy(src.steps[stepinds])

    if desc is not None and description is None:
        description = str(desc)
    if description is None:
        try:
            # this only works when data is a coefficient result
            description = data.description
        except Exception as e:
            log.debug('Exception passed', exc_info=True)
    else:
        description = str(description)

    # ignoreweights, new default
    if ignoreweights:
        srcerrs = None
    else:
        # make sure srcerrs are not all equal and select right indices
        try:
            srcerrs = srcerrs[stepinds]
            if (srcerrs == srcerrs[0]).all():
                srcerrs = None
        except:
            srcerrs = None

    if fitfunc not in [f_exponential, f_exponential_offset, f_complex]:
        log.info('Custom fitfunction specified {}'.format(fitfunc))

    fitpars = fitpars_check(fitpars, fitfunc)

    # should implement fitbnds_check(bnds, fitfunc)
    if fitbnds is None: fitbnds = default_fitbnds(fitfunc)

    # logging this should not cause an actual exception. ugly, needs rework
    try:
        if fitbnds is None:
            bnds = np.array([-np.inf, np.inf])
            log.info('Unbound fit to {}'.format(ut.math_from_doc(fitfunc)))
            log.debug('kmin = {}, kmax = {}'.format(srcsteps[0], srcsteps[-1]))
            ic = list(inspect.signature(fitfunc).parameters)[1:]
            ic = ('{} = {:.3f}'.format(a, b) for a, b in zip(ic, fitpars[0]))
            log.debug('Starting parameters: ' + ', '.join(ic))
        else:
            bnds = fitbnds
            log.info('Bounded fit to {}'.format(ut.math_from_doc(fitfunc)))
            log.debug('kmin = {}, kmax = {}'.format(srcsteps[0], srcsteps[-1]))
            ic = list(inspect.signature(fitfunc).parameters)[1:]
            ic = ('{0:<6} = {1:8.3f} in ({2:9.4f}, {3:9.4f})'.format(
                a, b, c, d) for a, b, c, d in zip(ic, fitpars[0], fitbnds[
                    0, :], fitbnds[1, :]))
            log.debug('First parameters:\n' + '\n'.join(ic))
    except Exception as e:
        log.debug('Exception when logging fitpars', exc_info=True)

    if (fitpars.shape[0] > 1):
        log.debug('Repeating fit with {} sets of initial parameters:'.format(
            fitpars.shape[0]))

    # ------------------------------------------------------------------ #
    # Fit via scipy.curve_fit
    # ------------------------------------------------------------------ #

    # fitpars: 2d ndarray
    # fitbnds: matching scipy.curve_fit: [lowerbndslist, upperbndslist]
    maxfev = 100 * (len(fitpars[0]) + 1) if maxfev is None else int(maxfev)

    def fitloop(ftcoefficients, ftmaxfev, fitlog=True):
        ssresmin = np.inf
        fulpopt = None
        fulpcov = None

        if len(fitpars) != 1 and fitlog:
            log.info('Fitting with {} different start values'.format(
                len(fitpars)))

        for idx, pars in enumerate(tqdm(fitpars, disable=(not fitlog))):

            try:
                popt, pcov = scipy.optimize.curve_fit(fitfunc,
                                                      srcsteps * dt,
                                                      ftcoefficients,
                                                      p0=pars,
                                                      bounds=bnds,
                                                      maxfev=ftmaxfev,
                                                      sigma=srcerrs)

                residuals = ftcoefficients - fitfunc(srcsteps * dt, *popt)
                ssres = np.sum(residuals**2)

            except Exception as e:
                ssres = np.inf
                popt = None
                pcov = None
                if fitlog:
                    log.debug('Fit %d did not converge. Ignoring this fit',
                              idx + 1)
                    log.debug('Exception passed', exc_info=True)

            if ssres < ssresmin:
                ssresmin = ssres
                fulpopt = popt
                fulpcov = pcov

        if fitlog:
            pass
            # log.info('Finished %d fit(s)', len(fitpars))

        return fulpopt, fulpcov, ssresmin

    fulpopt, fulpcov, ssresmin = fitloop(src.coefficients[stepinds],
                                         int(maxfev))

    if fulpopt is None:
        if maxfev > 10000:
            pass
        else:
            log.warning('No fit converged after {} '.format(maxfev) +
                        'iterations. Increasing to 10000')
            maxfev = 10000
            fulpopt, fulpcov, ssresmin = fitloop(src.coefficients[stepinds],
                                                 int(maxfev))

    # avoid crashing scripts if no fit converged, return np.nan result
    if fulpopt is None:
        log.exception('No fit converged afer %d iterations', maxfev)
        try:
            if description is None:
                description = '(fit failed)'
            else:
                description = str(description) + ' (fit failed)'
        except Exception as e:
            log.debug('Exception passed', exc_info=True)
        return FitResult(tau=np.nan,
                         mre=np.nan,
                         fitfunc=fitfunc,
                         steps=steps,
                         dt=dt,
                         dtunit=dtunit,
                         description=description)

    try:
        rsquared = 0.0
        sstot = np.sum((src.coefficients[stepinds] -
                        np.mean(src.coefficients[stepinds]))**2)
        rsquared = 1.0 - (ssresmin / sstot)

        # adjusted rsquared to consider parameter number
        rsquared = 1.0 - (1.0 - rsquared) * \
            (len(stepinds) -1)/(len(stepinds) -1 - len(fulpopt))
    except Exception as e:
        log.debug('Exception passed when estimating rsquared', exc_info=True)

    # ------------------------------------------------------------------ #
    # Bootstrapping
    # ------------------------------------------------------------------ #
    taustderr = None
    mrestderr = None
    tauquantiles = None
    mrequantiles = None
    if src.numboot <= 1:
        log.debug('Fitting of bootstrapsamples can only be done if ' +
                  "coefficients() was called with sufficient trials and " +
                  "bootstrapsamples were created by specifying 'numboot'")
    elif fitfunc == f_linear:
        log.warning('Bootstrap is not suppored for the f_linear fitfunction')
    elif src.numboot > 1:
        if numboot > src.numboot:
            log.debug(
                "The provided data does not contain enough " +
                "bootstrapsamples (%d) to do the requested " +
                "'numboot=%d' fits.\nCall 'coefficeints()' and 'fit()' " +
                "with the same 'numboot' argument to avoid this.", src.numboot,
                numboot)
            numboot = src.numboot
        if numboot == 0:
            log.debug("'numboot=0' skipping bootstrapping")
        else:
            log.info('Bootstrapping {} replicas ({} fits each)'.format(
                numboot, len(fitpars)))

            log.debug('fit() seeding to {}'.format(seed))
            if seed is None:
                pass
            elif seed == 'random':
                np.random.seed(None)
            else:
                np.random.seed(seed)

            bstau = np.full(numboot + 1, np.nan)
            bsmre = np.full(numboot + 1, np.nan)

            # use scipy default maxfev for errors
            maxfev = 100 * (len(fitpars[0]) + 1)

            for tdx in tqdm(range(numboot)):
                bspopt, bspcov, bsres = fitloop(
                    src.bootstrapcrs[tdx].coefficients[stepinds], int(maxfev),
                    False)
                try:
                    bstau[tdx] = bspopt[0]
                    bsmre[tdx] = np.exp(-1 * dt / bspopt[0])
                except TypeError:
                    log.debug('Exception passed', exc_info=True)
                    bstau[tdx] = np.nan
                    bsmre[tdx] = np.nan

            # log.info('{} Bootstrap replicas done'.format(numboot))

            # add source sample?
            bstau[-1] = fulpopt[0]
            bsmre[-1] = np.exp(-1 * dt / fulpopt[0])

            taustderr = np.sqrt(np.nanvar(bstau, ddof=1))
            mrestderr = np.sqrt(np.nanvar(bsmre, ddof=1))
            if quantiles is None:
                quantiles = np.array([.125, .25, .4, .5, .6, .75, .875])
            else:
                quantiles = np.array(quantiles)
            tauquantiles = np.nanpercentile(bstau, quantiles * 100.)
            mrequantiles = np.nanpercentile(bsmre, quantiles * 100.)

    tau = fulpopt[0]
    mre = np.exp(-1 * dt / fulpopt[0])

    if fitfunc == f_linear:
        tau = None
        mre = None

    fulres = FitResult(tau=tau,
                       mre=mre,
                       fitfunc=fitfunc,
                       taustderr=taustderr,
                       mrestderr=mrestderr,
                       tauquantiles=tauquantiles,
                       mrequantiles=mrequantiles,
                       quantiles=quantiles,
                       popt=fulpopt,
                       pcov=fulpcov,
                       ssres=ssresmin,
                       rsquared=rsquared,
                       steps=steps,
                       dt=dt,
                       dtunit=dtunit,
                       description=description)

    # ------------------------------------------------------------------ #
    # consistency
    # ------------------------------------------------------------------ #

    log.info(
        'Finished fitting ' +
        '{} to {},\nmre = {}, tau = {}{}, ssres = {:.5f}'.format(
            'the data' if description is None else "'" + description +
            "'", fitfunc.__name__, ut._prerror(fulres.mre, fulres.mrestderr),
            ut._prerror(fulres.tau, fulres.taustderr, 2,
                        2), fulres.dtunit, fulres.ssres))

    if fulres.tau is None:
        return fulres

    try:
        if src.method == 'trialseparated':
            if fulres.tau > 0.1 * (src.triallen * dt):
                log.warning(
                    "The obtained autocorrelationtime " +
                    "(tau~{:.0f}{}) ".format(fulres.tau, dtunit) +
                    "is larger than 10% of the trial length " +
                    "({:.0f}{}).".format(src.triallen*dt) +
                    ("\nThe 'stationarymean' method might be more suitable." if \
                        src.numtrials > 1 else "")
                )
    except:
        log.debug('Exception passed', exc_info=True)

    try:
        if src.method == 'stationarymean':
            if fulres.tau > (src.triallen * dt):
                log.warning("The obtained autocorrelationtime " +
                            "(tau~{:.0f}{}) ".format(fulres.tau, dtunit) +
                            "is larger than the trial length " +
                            "({:.0f}{}).".format(src.triallen * dt, dtunit) +
                            "\nDon't trust this estimate!")
    except:
        log.debug('Exception passed', exc_info=True)

    # this was really just some back of the envelope suggestion.
    # if fulres.tau >= 0.75*(steps[-1]*dt):
    #     log.warning('The obtained autocorrelationtime is large compared '+
    #         'to the fitrange:\n' +
    #         "tmin~{:.0f}{}, tmax~{:.0f}{}, tau~{:.0f}{}\n"
    #         .format(steps[0]*dt, dtunit, steps[-1]*dt, dtunit, fulres.tau, dtunit) +
    #         'Consider fitting with a larger \'maxstep\'')

    # if fulres.tau <= 0.05*(steps[-1]*dt) or fulres.tau <= steps[0]*dt:
    #     log.warning('The obtained autocorrelationtime is small compared '+
    #         "to the fitrange:\n" +
    #         "tmin~{:.0f}{}, tmax~{:.0f}{}, tau~{:.0f}{}\n"
    #         .format(steps[0]*dt, dtunit, steps[-1]*dt, dtunit, fulres.tau, dtunit) +
    #         "Consider fitting with smaller 'minstep' and 'maxstep'")

    if fitfunc is f_complex:
        # check for amplitudes A>B, A>C, A>O
        # tau, A, O, tauosc, B, gamma, nu, taugs, C
        try:
            if fulpopt[1] <= fulpopt[4] or fulpopt[1] <= fulpopt[8]:
                log.warning(
                    'The amplitude of the exponential decay is ' +
                    'smaller than corrections: A=%f B=%f C=%f', fulpopt[1],
                    fulpopt[4], fulpopt[8])
        except:
            log.debug('Exception passed', exc_info=True)

    return fulres