Exemple #1
0
def optimuminterval_2dsmear(eventenergies,
                            masslist,
                            passagefraction,
                            exposure,
                            cov,
                            delta,
                            tm="Si",
                            cl=0.9,
                            nsig=3,
                            verbose=False,
                            npts=1000,
                            subtract_zero=False):
    """
    Function for running Steve Yellin's Optimum Interval code on an
    inputted spectrum, using the two-dimensional normal distribution
    defined by the inputted covariance matrix to model the trigger
    efficiency. This is a more complicated version of
    `rqpy.limit.optimuminterval`.

    Parameters
    ----------
    eventenergies : ndarray
        Array of all of the event energies (in keV) to use for
        calculating the sensitivity.
    masslist : ndarray
        List of candidate DM masses (in GeV/c^2) to calculate the upper
        limit at.
    passagefraction : float, functionType
        The passage fraction of the cuts being applied to the data.
        Excludes the trigger efficiency, since that is wrapped up in the
        2D smearing. If a float, then it should be a number between 0
        and 1, meaning that the passage fraction is energy independent.
        If a function, then the input should be in units of keV.
    exposure : float
        The total exposure of the detector (kg*days).
    cov : ndarray
        The covariance matrix relating the measured/reconstructed energy
        and the trigger energy (both in keV).
    delta : float
        The threshold value (in keV) for the trigger energy.
    tm : str, int, optional
        The target material of the detector. Must be passed as the atomic
        symbol. Can also pass a compound, but must be its chemical formula
        (e.g. sapphire is 'Al2O3'). Default value is 'Si'.
    cl : float, optional
        The confidence level desired for the upper limit. Default is
        0.9. Can be any value between 0.00001 and 0.99999. However, the
        algorithm requires less than 100 upper limit events when outside
        the range 0.8 to 0.995 in order to work, so an error may be
        raised.
    nsig : float
        The number of sigma outside of which the two-dimensional normal
        PDF defined by the inputted covariance matrix will be set to
        zero. This defines an elliptical confidence region. This is used
        to restrict the amount of smearing that is applied to the DM
        spectrum to avoid calculate artificially low upper limits.
    verbose : bool, optional
        If True, then the algorithm prints out which mass is currently
        being used in the calculation. If False, no information is
        printed. Default is False.
    npts : float, optional
        The number of energies at which to evaluate the smeared
        differential rate. Large values result in long computation
        times. Default is 1e3.
    subtract_zero : bool, optional
        Option to subtract out the zero-energy multivariate normal
        distribution in true energy for a more conservative estimate of
        the 2D Gaussian smeared limit. This will have only a small
        effect. Default is False.

    Returns
    -------
    sigma : ndarray
        The corresponding cross sections of the sensitivity curve (in
        cm^2).
    oi_energy0 : ndarray
        The energies in keV at which each optimum interval started.
    oi_energy1 : ndarray
        The energies in keV at which each optimum interval ended.

    Notes
    -----
    This function is a wrapper for Steve Yellin's Optimum Interval code.
    His code can be found here: titus.stanford.edu/Upper/

    Read more about the Optimum Interval code in these two papers:
        - https://arxiv.org/abs/physics/0203002
        - https://arxiv.org/abs/0709.2701

    """

    if np.isscalar(masslist):
        masslist = [masslist]

    eventenergies = np.sort(eventenergies)

    elow = max(0.001, min(eventenergies))
    ehigh = max(eventenergies)

    en_interp = np.logspace(
        np.log10(0.9 * elow),
        np.log10(1.1 * ehigh),
        npts,
    )

    delta_e = np.concatenate(([(en_interp[1] - en_interp[0]) / 2
                               ], (en_interp[2:] - en_interp[:-2]) / 2,
                              [(en_interp[-1] - en_interp[-2]) / 2]))

    sigma0 = 1e-41

    event_inds = rp.inrange(eventenergies, elow, ehigh)
    inlim = rp.inrange(en_interp, elow, ehigh)

    sigma = np.ones(len(masslist)) * np.inf
    oi_energy0 = np.zeros(len(masslist))
    oi_energy1 = np.zeros(len(masslist))

    for ii, mass in enumerate(masslist):
        if verbose:
            print(f"On mass {ii+1} of {len(masslist)}.")

        init_rate = drde_gauss_smear2d(
            en_interp,
            cov,
            delta,
            mass,
            sigma0,
            nsig=nsig,
            tm=tm,
            subtract_zero=subtract_zero,
        )

        if isinstance(passagefraction, types.FunctionType):
            rate = init_rate * exposure * passagefraction(en_interp)
        else:
            rate = init_rate * exposure * passagefraction

        integ_rate = integrate.cumtrapz(
            rate[inlim],
            x=en_interp[inlim],
            initial=0,
        )

        tot_rate = integ_rate[-1]

        x_val_fcn = interpolate.interp1d(
            en_interp[inlim],
            integ_rate,
            kind="linear",
            bounds_error=False,
            fill_value=(0, tot_rate),
        )

        x_vals = x_val_fcn(eventenergies[event_inds])

        if tot_rate != 0:
            fc = x_vals / tot_rate
            fc[fc > 1] = 1

            cdf_max = 1 - 1e-6
            possiblewimp = fc <= cdf_max
            fc = fc[possiblewimp]

            if len(fc) == 0:
                fc = np.asarray([0, 1])

            try:
                uloutput, endpoint0, endpoint1 = upper(fc, cl=cl)
                sigma[ii] = (sigma0 / tot_rate) * uloutput
                energies_used = eventenergies[event_inds][possiblewimp]
                oi_energy0[ii] = energies_used[endpoint0]

                if endpoint1 < len(fc):
                    oi_energy1[ii] = energies_used[endpoint1]
                else:
                    oi_energy1[ii] = energies_used[-1]
            except:
                pass

    return sigma, oi_energy0, oi_energy1
Exemple #2
0
def drde_gauss_smear2d(x,
                       cov,
                       delta,
                       m_dm,
                       sig0,
                       nsig=3,
                       tm="Si",
                       subtract_zero=False):
    """
    Function for smearing the differential rate for DM, given that we have a covariance matrix
    for two energy estimators, where we have set a trigger threshold on one and a measured energy
    for the other.

    Parameters
    ----------
    x : float, ndarray
        The measured energies at which to calculate the differential event rate.
    cov : ndarray
        The covariance matrix relating the measured energy and the trigger energy.
    delta : float
        The threshold value (in keV) for the trigger energy.
    m_dm : float
        The dark matter mass at which to calculate the expected differential
        event rate. Expected units are GeV.
    sig0 : float
        The dark matter cross section at which to calculate the expected differential
        event rate. Expected units are cm^2.
    nsig : float, optional
        The number of sigma outside of which the PDF will be set to zero. This defines
        an elliptical confidence region, whose shape comes from the covariance matrix.
    tm : str, int, optional
        The target material of the detector. Must be passed as the atomic
        symbol. Can also pass a compound, but must be its chemical formula
        (e.g. sapphire is 'Al2O3'). Default value is 'Si'.
    subtract_zero : bool, optional
        Option to subtract out the zero-energy multivariate normal distribution in true energy for
        a more conservative estimate of the 2D Gaussian smeared limit. This will have only a small
        effect. Default is False.

    Returns
    -------
    out : ndarray
        The expected dark matter differential rate for the inputted recoil energies,
        dark matter mass, and dark matter cross section, taking into account the smearing
        by a two-dimensional normal distribution. Units are events/keV/kg/day, or "DRU".

    """

    x = np.atleast_1d(x)

    sig = lambda n: stats.norm.cdf(n) - stats.norm.cdf(-n)
    conf = stats.chi2.ppf(sig(nsig), 2)

    cov_inv = np.linalg.inv(cov)

    a = cov_inv[0, 0]
    b = 2 * cov_inv[1, 0]
    c = cov_inv[1, 1]

    # get the deltas of the confidence ellipse for trigger and reconstructed energies
    et_top = np.sqrt(conf / (c - b**2 / (4 * a)))
    ep_top = np.sqrt(conf / (a - b**2 / (4 * c)))

    # get range of nonzero true energies in integral
    start = 0.0
    end = drde_max_q(m_dm)
    d2 = 0.001
    y = np.linspace(start, end, num=int((end - start) / d2) + 1)
    ydiff = np.diff(y).mean()

    out = np.zeros(len(x))

    for ii, val in enumerate(x):
        # define function that we will be integrating over
        func = lambda et, e0: _gauss2d_integrand(
            val,
            et,
            e0,
            delta,
            cov,
            nsig,
            m_dm,
            sig0,
            subtract_zero=subtract_zero,
            tm=tm,
        )

        # get x values inside ellipse for each y value
        etvals = []
        for en in y:
            if rp.inrange(val, en - ep_top, en + ep_top):
                ets = np.linspace(en - et_top, en + et_top, num=100)
                etvals.append([ets, en])

        # evaluate double integral
        if len(etvals) > 0:
            temp_out = 0
            for ets, en in etvals:
                temp_out += np.sum(func(ets, en)) * np.diff(ets).mean() * ydiff
            out[ii] = temp_out

    return out
Exemple #3
0
def optimuminterval(eventenergies,
                    effenergies,
                    effs,
                    masslist,
                    exposure,
                    tm="Si",
                    cl=0.9,
                    res=None,
                    gauss_width=10,
                    verbose=False,
                    drdefunction=None,
                    hard_threshold=0.0):
    """
    Function for running Steve Yellin's Optimum Interval code on an inputted spectrum and efficiency curve.

    Parameters
    ----------
    eventenergies : ndarray
        Array of all of the event energies (in keV) to use for calculating the sensitivity.
    effenergies : ndarray 
        Array of the energy values (in keV) of the efficiency curve.
    effs : ndarray
        Array of the efficiencies (unitless) corresponding to `effenergies`.
        If `drdefunction` argument is provided, the `effs` argument is ignored. It is kept as
        a positional argument for backward compatibility
    masslist : ndarray
        List of candidate DM masses (in GeV/c^2) to calculate the sensitivity at.
    exposure : float
        The total exposure of the detector (kg*days).
    tm : str, int, optional
        The target material of the detector. Must be passed as the atomic
        symbol. Can also pass a compound, but must be its chemical formula
        (e.g. sapphire is 'Al2O3'). Default value is 'Si'.
    cl : float, optional
        The confidence level desired for the upper limit. Default is 0.9. Can be any value
        between 0.00001 and 0.99999. However, the algorithm requires less than 100 upper
        limit events when outside the range 0.8 to 0.995 in order to work, so an error may
        be raised.
    res : float, NoneType, optional
        The detector resolution in units of keV. If passed, then the differential event
        rate of the dark matter is convoluted with a Gaussian with width `res`, which results
        in a smeared spectrum. If left as None, no smearing is performed.
        If `drdefunction` is provided, this argument is ignored
    gauss_width : float, optional
        If `res` is not None, this is the number of standard deviations of the Gaussian
        distribution that the smearing will go out to. Default is 10.
        If `drdefunction` is provided, this argument is ignored
    verbose : bool, optional
        If True, then the algorithm prints out which mass is currently being used in the calculation.
        If False, no information is printed. Default is False.
    drdefunction : list, optional
        List of callables of type float(float). Every element of the list represents the signal model
        rate as a function of reconstructed energy for the corresponding Dark Matter mass from the
        `masslist` and the cross section sigma=10^-41 cm^2. The experiment efficiency must be taken
        into account. The energy unit is keV, the rate unit is 1/keV/kg/day.
        By default (or if None is provided) the standard Lewin&Smith signal model is used with gaussian
        smearing of width `res`, truncated at `gauss_width` standard deviations.
    hard_threshold : float, optional
        The energy value (keV) below which the efficiency is zero.
        This argument is not required in a case of smooth efficiency curve, however it must be provided 
        in a case of step-function-like efficiency.

    Returns
    -------
    sigma : ndarray
        The corresponding cross sections of the sensitivity curve (in cm^2).
    oi_energy0 : ndarray
        The energies in keV at which each optimum interval started.
    oi_energy1 : ndarray
        The energies in keV at which each optimum interval ended.

    Notes
    -----
    This function is a wrapper for Steve Yellin's Optimum Interval code. His code can be found
    here: titus.stanford.edu/Upper/

    Read more about the Optimum Interval code in these two papers:
        - https://arxiv.org/abs/physics/0203002
        - https://arxiv.org/abs/0709.2701

    """

    if np.isscalar(masslist):
        masslist = [masslist]

    eventenergies = np.sort(eventenergies)

    elow = max(hard_threshold, min(effenergies))
    ehigh = max(effenergies)

    en_interp = np.logspace(np.log10(elow), np.log10(ehigh), int(1e5))

    sigma0 = 1e-41

    event_inds = rp.inrange(eventenergies, elow, ehigh)

    sigma = np.ones(len(masslist)) * np.inf
    oi_energy0 = np.zeros(len(masslist))
    oi_energy1 = np.zeros(len(masslist))

    for ii, mass in enumerate(masslist):
        if verbose:
            print(f"On mass {ii+1} of {len(masslist)}.")

        if drdefunction is None:
            exp = effs * exposure

            curr_exp = interpolate.interp1d(
                effenergies,
                exp,
                kind="linear",
                bounds_error=False,
                fill_value=(0, exp[-1]),
            )

            init_rate = drde(
                en_interp,
                mass,
                sigma0,
                tm=tm,
            )
            if res is not None:
                init_rate = gauss_smear(en_interp,
                                        init_rate,
                                        res,
                                        gauss_width=gauss_width)
            rate = init_rate * curr_exp(en_interp)
        else:
            rate = drdefunction[ii](en_interp) * exposure

        integ_rate = integrate.cumtrapz(rate, x=en_interp, initial=0)

        tot_rate = integ_rate[-1]

        x_val_fcn = interpolate.interp1d(
            en_interp,
            integ_rate,
            kind="linear",
            bounds_error=False,
            fill_value=(0, tot_rate),
        )

        x_vals = x_val_fcn(eventenergies[event_inds])

        if tot_rate != 0:
            fc = x_vals / tot_rate
            fc[fc > 1] = 1

            cdf_max = 1 - 1e-6
            possiblewimp = fc <= cdf_max
            fc = fc[possiblewimp]

            if len(fc) == 0:
                fc = np.asarray([0, 1])

            try:
                uloutput, endpoint0, endpoint1 = upper(fc, cl=cl)

                sigma[ii] = (sigma0 / tot_rate) * uloutput

                oi_energy0[ii] = eventenergies[event_inds][possiblewimp][
                    endpoint0 -
                    1] if endpoint0 > 0 else elow  # endpoint==0 means the start of the SM integration range
                oi_energy1[ii] = eventenergies[event_inds][possiblewimp][
                    endpoint1 - 1] if endpoint1 - 1 < len(fc) else ehigh
            except:
                pass

    return sigma, oi_energy0, oi_energy1
Exemple #4
0
def _plot_energy_res_vs_bias(r0s,
                             energy_res,
                             energy_res_err,
                             qets,
                             taus,
                             xlims=None,
                             ylims=None,
                             lgctau=False,
                             lgcoptimum=False,
                             figsavepath='',
                             lgcsave=False,
                             energyscale=None):
    """
    Helper function for the IVanalysis class to plot the expected
    energy resolution as a function of QET bias and TES resistance.

    Parameters
    ----------
    r0s : ndarray
        Array of r0 values (in Ohms).
    energy_res : ndarray
        Array of expected energy resolutions (in eV).
    energy_res_err : ndarray, NoneType
        Array of energy resolution error bounds in eV. must be of shape
        (2, #qet bias) where the first dims are the lower and upper
        bounds.
    qets : ndarray
        Array of QET bias values (in Amps).
    taus : ndarray
        Array of tau minus values (in seconds).
    xlims : NoneType, tuple, optional
        Limits to be passed to ax.set_xlim().
    ylims : NoneType, tuple, optional
        Limits to be passed to ax.set_ylim().
    lgctau : bool, optional
        If True, tau_minus is plotted as function of R0 and QETbias.
    lgcoptimum : bool, optional
        If True, the optimum energy res (and tau_minus if lgctau=True).
    figsavepath : str, optional
        Directory to save the figure.
    lgcsave : bool, optional
        If true, the figure is saved.
    energyscale : char, NoneType, optional
        The metric prefix for how the energy resolution should be
        scaled. Defaults to None, which will be base units [eV] Can be:
        n->nano, u->micro, m->milli, k->kilo, M->Mega, or G->Giga.

    Returns
    -------
    None

    """

    metric_prefixes = {
        'n': 1e9,
        'u': 1e6,
        'm': 1e3,
        'k': 1e-3,
        'M': 1e-6,
        'G': 1e-9,
    }
    if energyscale is None:
        scale = 1
        energyscale = ''
    elif energyscale not in metric_prefixes:
        raise ValueError(
            f'energyscale must be one of {metric_prefixes.keys()}')
    else:
        scale = metric_prefixes[energyscale]
    if energyscale == 'u':
        energyscale = r'$\mu$'
    fig, ax = plt.subplots(nrows=1, ncols=1, figsize=(9, 6))

    if xlims is None:
        xlims = (min(r0s), max(r0s))
    if ylims is None:
        ylims = (min(energy_res * scale), max(energy_res * scale))
    crangey = rp.inrange(energy_res, ylims[0], ylims[1])
    crangex = rp.inrange(r0s, xlims[0], xlims[1])

    r0s = r0s[crangey & crangex]
    energy_res = energy_res[crangey & crangex] * scale
    energy_res_err = energy_res_err[:, crangey & crangex] * scale
    qets = (qets[crangey & crangex] * 1e6).round().astype(int)
    taus = taus[crangey & crangex] * 1e6

    ax.plot(r0s, energy_res, linestyle=' ', marker='.', ms=10, c='g')
    ax.plot(
        r0s,
        energy_res,
        linestyle='-',
        marker=' ',
        linewidth=3,
        alpha=.5,
        c='g',
    )
    ax.fill_between(
        r0s,
        energy_res_err[0],
        energy_res_err[1],
        alpha=.5,
        color='g',
    )

    ax.grid(True, which='both', linestyle='--')
    ax.set_xlabel('$R_0/R_N$')
    ax.set_ylabel(r'$σ_E$' + f' [{energyscale}eV]', color='g')
    ax.tick_params('y', colors='g')
    ax.tick_params(which="both", direction="in", right=True, top=True)

    if lgcoptimum:
        plte = ax.axvline(
            r0s[np.argmin(energy_res)],
            linestyle='--',
            color='g',
            alpha=0.5,
            label=r"""Min $\sigma_E$: """
            f"""{np.min(energy_res):.3f} [{energyscale}eV]""",
        )

    ax2 = ax.twiny()
    ax2.spines['bottom'].set_position(('outward', 36))
    ax2.xaxis.set_ticks_position('bottom')
    ax2.xaxis.set_label_position('bottom')
    ax2.set_xticks(r0s)
    ax2.set_xticklabels(qets)
    ax2.set_xlabel(r'QET bias [$\mu$A]')

    if lgctau:
        ax3 = ax.twinx()
        ax3.plot(r0s, taus, linestyle=' ', marker='.', ms=10, c='b')
        ax3.plot(
            r0s,
            taus,
            linestyle='-',
            marker=' ',
            linewidth=3,
            alpha=.5,
            c='b',
        )
        ax3.tick_params(which="both", direction="in", right=True, top=True)
        ax3.tick_params('y', colors='b')
        ax3.set_ylabel(r'$\tau_{-} [μs]$', color='b')

        if lgcoptimum:
            plttau = ax3.axvline(
                r0s[np.argmin(taus)],
                linestyle='--',
                alpha=0.5,
                label=r"""Min $\tau_{-}$: """
                f"""{np.min(taus):.3f} [μs]""",
            )
    if xlims is not None:
        ax.set_xlim(xlims)
        ax2.set_xlim(xlims)
        if lgctau:
            ax3.set_xlim(xlims)
    if ylims is not None:
        ax.set_ylim(ylims)

    ax.set_title('Expected Energy Resolution vs QET bias and $R_0/R_N$')
    if lgcoptimum:
        ax.legend()
        if lgctau:
            ax.legend(loc='upper center', handles=[plte, plttau])

    if lgcsave:
        plt.savefig(f'{figsavepath}energy_res_vs_bias.png')
Exemple #5
0
def passageplot(arr,
                cuts,
                basecut=None,
                nbins=100,
                lgcequaldensitybins=False,
                xlims=None,
                ylims=(0, 1),
                lgceff=True,
                lgclegend=True,
                labeldict=None,
                ax=None,
                cmap="viridis",
                showerrorbar=False,
                nsigmaerrorbar=1):
    """
    Function to plot histogram of RQ data with multiple cuts.

    Parameters
    ----------
    arr : array_like
        Array of values to be binned and plotted
    cuts : list, optional
        List of masks of values to be plotted. The cuts will be applied in the order that
        they are listed, such that any number of cuts can be plotted.
    basecut : NoneType, array_like, optional
        The base cut for comparison of the first cut in `cuts`. If left as None, then the
        passage fraction is calculated using all of the inputted data for the first cut.
    nbins : int, str, optional
        This is the same as plt.hist() bins parameter. Defaults is 'sqrt'.
    lgcequaldensitybins : bool, optional
        If set to True, the bin widths are set such that each bin has the same number
        of data points within it. If left as False, then a constant bin width is used.
    xlims : list of float, optional
        The xlimits of the passage fraction plot.
    ylims : list of float, optional
        This is passed to the plot as the y limits. Set to (0, 1) by default.
    lgceff : bool, optional
        If True, the total cut efficiencies are printed in the legend.
    lgclegend : bool, optional
        If True, the legend is plotted.
    labeldict : dict, optional
        Dictionary to overwrite the labels of the plot. defaults are:
            labels = {'title' : 'Passage Fraction Plot',
                      'xlabel' : 'variable',
                      'ylabel' : 'Passage Fraction',
                      'cut0' : '1st',
                      'cut1' : '2nd',
                      ...}
        Ex: to change just the title, pass: labeldict = {'title' : 'new title'}, to hist()
    ax : axes.Axes object, optional
        Option to pass an existing Matplotlib Axes object to plot over, if it already exists.
    cmap : str, optional
        The colormap to use for plotting each cut. Default is 'viridis'.
    showerrorbar : bool, optional
        Boolean flag for also plotting the error bars for the passage fraction in each bin.
        Default is False.
    nsigmaerrorbar : float, optional
        The number of sigma to show for the error bars if `showerrorbar` is True. Default is 1.

    Returns
    -------
    fig : Figure
        Matplotlib Figure object. Set to None if ax is passed as a parameter.
    ax : axes.Axes object
        Matplotlib Axes object

    """

    if not isinstance(cuts, list):
        cuts = [cuts]

    labels = {
        'title': 'Passage Fraction Plot',
        'xlabel': 'variable',
        'ylabel': 'Passage Fraction',
    }

    for ii in range(len(cuts)):

        num_str = str(ii + 1)

        if num_str[-1] == '1':
            num_str += "st"
        elif num_str[-1] == '2':
            num_str += "nd"
        elif num_str[-1] == '3':
            num_str += "rd"
        else:
            num_str += "th"

        labels[f"cut{ii}"] = num_str

    if labeldict is not None:
        labels.update(labeldict)

    if ax is None:
        fig, ax = plt.subplots(figsize=(9, 6))
    else:
        fig = None

    ax.set_title(labels['title'])
    ax.set_xlabel(labels['xlabel'])
    ax.set_ylabel(labels['ylabel'])

    if basecut is None:
        basecut = np.ones(len(arr), dtype=bool)

    colors = plt.cm.get_cmap(cmap)(np.linspace(0.1, 0.9, len(cuts)))

    ctemp = np.ones(len(arr), dtype=bool) & basecut

    if xlims is None:
        xlimitcut = np.ones(len(arr), dtype=bool)
    else:
        xlimitcut = rp.inrange(arr, xlims[0], xlims[1])

    for ii, cut in enumerate(cuts):
        oldsum = ctemp.sum()

        if ii == 0:
            passage_output = rp.passage_fraction(
                arr,
                cut,
                basecut=ctemp & xlimitcut,
                nbins=nbins,
                lgcequaldensitybins=lgcequaldensitybins,
            )
        else:
            passage_output = rp.passage_fraction(
                arr,
                cut,
                basecut=ctemp & xlimitcut,
                nbins=x_binned,
            )

        x_binned = passage_output[0]
        passage_binned = passage_output[1]

        ctemp = ctemp & cut
        newsum = ctemp.sum()
        cuteff = newsum / oldsum * 100
        label = f"Data passing {labels[f'cut{ii}']} cut"

        if showerrorbar:
            label += f" $\pm$ {nsigmaerrorbar}$\sigma$"

        if lgceff:
            label += f", Total Passage: {cuteff:.1f}%"

        if xlims is None:
            xlims = (x_binned.min() * 0.9, x_binned.max() * 1.1)

        bin_centers = (x_binned[1:] + x_binned[:-1]) / 2

        ax.hist(
            bin_centers,
            bins=x_binned,
            weights=passage_binned,
            histtype='step',
            color=colors[ii],
            label=label,
            linewidth=2,
        )

        if showerrorbar:
            passage_binned_biased = passage_output[2]
            passage_binned_err = passage_output[3]

            err_top = passage_binned_biased + passage_binned_err * nsigmaerrorbar
            err_bottom = passage_binned_biased - passage_binned_err * nsigmaerrorbar

            err_top = np.pad(err_top, (0, 1),
                             mode='constant',
                             constant_values=(0, err_top[-1]))
            err_bottom = np.pad(err_bottom, (0, 1),
                                mode='constant',
                                constant_values=(0, err_bottom[-1]))

            ax.fill_between(
                x_binned,
                err_top,
                y2=err_bottom,
                step='post',
                linewidth=1,
                alpha=0.5,
                color=colors[ii],
            )

    ax.set_xlim(xlims)
    ax.set_ylim(ylims)
    ax.tick_params(which="both", direction="in", right=True, top=True)
    ax.grid(linestyle="dashed")

    if lgclegend:
        ax.legend(loc="best")

    return fig, ax
Exemple #6
0
def conf_ellipse(mu, cov, conf=0.683, ax=None, **kwargs):
    """
    Draw a 2-D confidence level ellipse based on a mean, covariance matrix,
    and specified confidence level.

    Parameters
    ----------
    mu : array_like
        The x and y values of the mean, where the ellipse will be centered.
    cov : ndarray
        A 2-by-2 covariance matrix describing the relation of the x and y variables.
    conf : float
        The confidence level at which to draw the ellipse. Should be a value between
        0 and 1. Default is 0.683. See Notes for more information
    ax : axes.Axes object, NoneType, optional
        Option to pass an existing Matplotlib Axes object to plot over, if it already exists.
    **kwargs
        Keyword arguments to pass to `Ellipse`. See Notes for more information.

    Returns
    -------
    fig : Figure, NoneType
        Matplotlib Figure object. Set to None if ax is passed as a parameter.
    ax : axes.Axes object
        Matplotlib Axes object

    Raises
    ------
    ValueError
        If `conf` is not in the range [0, 1]

    Notes
    -----
    When deciding the value for `conf`, the standard frequentist statement about what this contour means is:

        "If the experiment is repeated many times with the same statistical analysis, then the
        contour (which will in general be different for each realization of the experiment) will
        define a region which contains the true value in 68.3% of the experiments."

    Note that the 68.3% confidence level contour in 2 dimensions is not the same as 1-sigma contour. The
    1-sigma contour for 2 dimensions (i.e. the value by which the chi^2 value increases by 1) contains
    the true value in 39.3% of the experiments.

    More discussion on multi-parameter errors can be found here:
        http://seal.web.cern.ch/seal/documents/minuit/mnerror.pdf

    The valid keyword arguments are below (taken from the Ellipse docstring). In this function, `fill` is
    defaulted to False and 'zorder' is defaulted so that the ellipse is be on top of previous plots.
        agg_filter: a filter function, which takes a (m, n, 3) float array and a dpi value, and returns a (m, n, 3) array
        alpha: float or None
        animated: bool
        antialiased: unknown
        capstyle: {'butt', 'round', 'projecting'}
        clip_box: `.Bbox`
        clip_on: bool
        clip_path: [(`~matplotlib.path.Path`, `.Transform`) | `.Patch` | None]
        color: color
        contains: callable
        edgecolor: color or None or 'auto'
        facecolor: color or None
        figure: `.Figure`
        fill: bool
        gid: str
        hatch: {'/', '\\', '|', '-', '+', 'x', 'o', 'O', '.', '*'}
        in_layout: bool
        joinstyle: {'miter', 'round', 'bevel'}
        label: object
        linestyle: {'-', '--', '-.', ':', '', (offset, on-off-seq), ...}
        linewidth: float or None for default
        path_effects: `.AbstractPathEffect`
        picker: None or bool or float or callable
        rasterized: bool or None
        sketch_params: (scale: float, length: float, randomness: float)
        snap: bool or None
        transform: `.Transform`
        url: str
        visible: bool
        zorder: float

    """

    if isinstance(mu, np.ndarray):
        mu = mu.tolist()

    if not rp.inrange(conf, 0, 1):
        raise ValueError("conf should be in the range [0, 1]")

    if ax is None:
        fig, ax = plt.subplots(figsize=(9, 6))
        ax.grid()
        ax.grid(which="minor", axis="both", linestyle="dotted")
        ax.tick_params(which="both", direction="in", right=True, top=True)
        autoscale_axes = True
    else:
        fig = None
        autoscale_axes = False

    if 'fill' not in kwargs:
        kwargs['fill'] = False

    if 'zorder' not in kwargs and len(ax.lines + ax.collections) > 0:
        kwargs['zorder'] = max(lc.get_zorder()
                               for lc in ax.lines + ax.collections) + 0.1

    a, v = np.linalg.eig(cov)
    v0 = v[:, 0]
    v1 = v[:, 1]

    theta = np.arctan2(v1[1], v1[0])

    quantile = stats.chi2.ppf(conf, 2)

    ell = Ellipse(
        mu,
        2 * (quantile * a[1])**0.5,
        2 * (quantile * a[0])**0.5,
        angle=theta * 180 / np.pi,
        **kwargs,
    )

    ax_ell = ax.add_patch(ell)

    if autoscale_axes:
        ax.autoscale()

    return fig, ax