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
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
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
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')
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
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