def estimate_min_excess(self, dataset): """Estimate minimum excess to reach the given significance. Parameters ---------- dataset : `SpectrumDataset` Spectrum dataset Returns ------- excess : `RegionNDMap` Minimal excess """ n_off = dataset.counts_off.data stat = WStatCountsStatistic( n_on=np.ones_like(n_off), n_off=n_off, alpha=dataset.alpha.data) excess_counts =stat.excess_matching_significance(self.sigma) is_gamma_limited = excess_counts < self.gamma_min excess_counts[is_gamma_limited] = self.gamma_min excess = dataset.background.copy() excess.data = excess_counts return excess
def test_wstat_errors(n_on, n_off, alpha, result): stat = WStatCountsStatistic(n_on, n_off, alpha) errn = stat.compute_errn() errp = stat.compute_errp() assert_allclose(errn, result[0], atol=1e-5) assert_allclose(errp, result[1], atol=1e-5)
def estimate_min_excess(self, dataset): """Estimate minimum excess to reach the given significance. Parameters ---------- dataset : `SpectrumDataset` Spectrum dataset Returns ------- excess : `RegionNDMap` Minimal excess """ n_off = dataset.counts_off.data stat = WStatCountsStatistic( n_on=dataset.alpha.data * n_off, n_off=n_off, alpha=dataset.alpha.data ) excess_counts = stat.n_sig_matching_significance(self.n_sigma) is_gamma_limited = excess_counts < self.gamma_min excess_counts[is_gamma_limited] = self.gamma_min bkg_syst_limited = ( excess_counts < self.bkg_syst_fraction * dataset.background.data ) excess_counts[bkg_syst_limited] = ( self.bkg_syst_fraction * dataset.background.data[bkg_syst_limited] ) excess = Map.from_geom(geom=dataset._geom, data=excess_counts) return excess
def convolved_map_dataset_counts_statistics(dataset, kernel): """Return CountsDataset objects containing smoothed maps from the MapDataset""" # Kernel is modified later make a copy here kernel = copy.deepcopy(kernel) kernel.normalize("peak") # fft convolution adds numerical noise, to ensure integer results we call # np.rint n_on_conv = np.rint(dataset.counts.convolve(kernel.array).data) if isinstance(dataset, MapDatasetOnOff): background = dataset.background background.data[dataset.acceptance_off.data == 0] = 0.0 background_conv = background.convolve(kernel.array).data n_off_conv = dataset.counts_off.convolve(kernel.array).data with np.errstate(invalid="ignore", divide="ignore"): alpha_conv = background_conv / n_off_conv return WStatCountsStatistic(n_on_conv.data, n_off_conv.data, alpha_conv.data) else: background_conv = dataset.npred().convolve(kernel.array).data return CashCountsStatistic(n_on_conv.data, background_conv.data)
def convolved_map_dataset_counts_statistics(dataset, kernel, mask, correlate_off): """Return CountsDataset objects containing smoothed maps from the MapDataset""" # Kernel is modified later make a copy here kernel = copy.deepcopy(kernel) kernel.normalize("peak") # fft convolution adds numerical noise, to ensure integer results we call # np.rint n_on = dataset.counts * mask n_on_conv = np.rint(n_on.convolve(kernel.array).data) if isinstance(dataset, MapDatasetOnOff): n_off = dataset.counts_off * mask npred_sig = dataset.npred_signal() * mask acceptance_on = dataset.acceptance * mask acceptance_off = dataset.acceptance_off * mask npred_sig_convolve = npred_sig.convolve(kernel.array) acceptance_on_convolve = acceptance_on.convolve(kernel.array) if correlate_off: n_off = n_off.convolve(kernel.array) acceptance_off = acceptance_off.convolve(kernel.array) with np.errstate(invalid="ignore", divide="ignore"): alpha = acceptance_on_convolve / acceptance_off return WStatCountsStatistic(n_on_conv.data, n_off.data, alpha.data, npred_sig_convolve.data) else: npred = dataset.npred() * mask background_conv = npred.convolve(kernel.array) return CashCountsStatistic(n_on_conv.data, background_conv.data)
def info_dict(self, in_safe_energy_range=True, **kwargs): """Info dict with summary statistics, summed over energy Parameters ---------- in_safe_energy_range : bool Whether to sum only in the safe energy range Returns ------- info_dict : dict Dictionary with summary info. """ info = super().info_dict(in_safe_energy_range) mask = self.mask_safe.data if in_safe_energy_range else slice(None) # TODO: handle energy dependent a_on / a_off info["a_on"] = self.acceptance.data[0, 0, 0].copy() if self.counts_off is not None: info["n_off"] = self.counts_off.data[mask].sum() info["a_off"] = self.acceptance_off.data[0, 0, 0].copy() else: info["n_off"] = 0 info["a_off"] = 1 info["alpha"] = self.alpha.data[0, 0, 0].copy() info["significance"] = WStatCountsStatistic( self.counts.data[mask].sum(), self.counts_off.data[mask].sum(), self.alpha.data[0, 0, 0], ).significance return info
def calculate_sensitivity_lima(n_on_events, n_background, alpha, n_bins_energy, n_bins_gammaness, n_bins_theta2): """ Sensitivity calculation using the Li & Ma formula eq. 17 of Li & Ma (1983). https://ui.adsabs.harvard.edu/abs/1983ApJ...272..317L/abstract We calculate the sensitivity in bins of energy, gammaness and theta2 Parameters --------- n_on_events: `numpy.ndarray` number of ON events in the signal region n_background: `numpy.ndarray` number of events in the background region alpha: `float` inverse of the number of off positions n_bins_energy: `int` number of bins in energy n_bins_gammaness: `int` number of bins in gammaness n_bins_theta2: `int` number of bins in theta2 Returns --------- sensitivity: `numpy.ndarray` sensitivity in percentage of Crab units n_excesses_5sigma: `numpy.ndarray` number of excesses corresponding to a 5 sigma significance """ stat = WStatCountsStatistic(n_on=n_on_events, n_off=n_background, alpha=alpha) n_excesses_5sigma = stat.excess_matching_significance(5) for i in range(0, n_bins_energy): for j in range(0, n_bins_gammaness): for k in range(0, n_bins_theta2): if n_excesses_5sigma[i][j][k] < 10: n_excesses_5sigma[i][j][k] = 10 if n_excesses_5sigma[i, j, k] < 0.05 * n_background[i][j][k] / 5: n_excesses_5sigma[i, j, k] = 0.05 * n_background[i][j][k] / 5 sensitivity = n_excesses_5sigma / n_on_events * 100 # percentage of Crab return n_excesses_5sigma, sensitivity
def calculate_sensitivity_lima_ebin(n_excesses, n_background, alpha, n_bins_energy): """ Sensitivity calculation using the Li & Ma formula eq. 17 of Li & Ma (1983). https://ui.adsabs.harvard.edu/abs/1983ApJ...272..317L/abstract Parameters --------- n_excesses: `numpy.ndarray` number of excess events in the signal region n_background: `numpy.ndarray` number of events in the background region alpha: `float` inverse of the number of off positions n_bins_energy:`int` number of bins in energy Returns --------- sensitivity: `numpy.ndarray` sensitivity in percentage of Crab units n_excesses_5sigma: `numpy.ndarray` number of excesses corresponding to a 5 sigma significance """ if any(len(a) != n_bins_energy for a in (n_excesses, n_background, alpha)): raise ValueError( 'Excess, background and alpha arrays must have the same length') stat = WStatCountsStatistic( n_on=np.ones_like(n_background), n_off=n_background, alpha=alpha) n_excesses_5sigma = stat.excess_matching_significance(5) for i in range(0, n_bins_energy): # If the excess needed to get 5 sigma is less than 10, # we force it to be at least 10 if n_excesses_5sigma[i] < 10: n_excesses_5sigma[i] = 10 # If the excess needed to get 5 sigma is less than 5% # of the background, we force it to be at least 5% of # the background if n_excesses_5sigma[i] < 0.05 * n_background[i] * alpha[i]: n_excesses_5sigma[i] = 0.05 * n_background[i] * alpha[i] sensitivity = n_excesses_5sigma / n_excesses * 100 # percentage of Crab return n_excesses_5sigma, sensitivity
def test_wstat_basic(n_on, n_off, alpha, result): stat = WStatCountsStatistic(n_on, n_off, alpha) excess = stat.excess significance = stat.significance p_value = stat.p_value assert_allclose(excess, result[0]) assert_allclose(significance, result[1], atol=1e-5) assert_allclose(p_value, result[2], atol=1e-5)
def test_wstat_basic(n_on, n_off, alpha, result): stat = WStatCountsStatistic(n_on, n_off, alpha) excess = stat.n_sig sqrt_ts = stat.sqrt_ts p_value = stat.p_value assert_allclose(excess, result[0], rtol=1e-4) assert_allclose(sqrt_ts, result[1], rtol=1e-4) assert_allclose(p_value, result[2], rtol=1e-4)
def test_lima_gammapy(): pytest.importorskip('gammapy') from gammapy.stats import WStatCountsStatistic from pyirf.statistics import li_ma_significance n_ons = [100, 50, 10] n_offs = [10, 20, 30] alphas = [2, 1, 0.2] for n_on, n_off, alpha in zip(n_ons, n_offs, alphas): sig_gammapy = WStatCountsStatistic(n_on, n_off, alpha).sqrt_ts assert np.isclose(li_ma_significance(n_on, n_off, alpha), sig_gammapy)
def test_wstat_with_musig(n_on, n_off, alpha, mu_sig, result): stat = WStatCountsStatistic(n_on, n_off, alpha, mu_sig) excess = stat.excess significance = stat.significance p_value = stat.p_value del_ts = stat.delta_ts assert_allclose(excess, result[0], rtol=1e-4) assert_allclose(significance, result[1], rtol=1e-4) assert_allclose(p_value, result[2], rtol=1e-4) assert_allclose(del_ts, result[3], rtol=1e-4)
def test_wstat_with_musig(n_on, n_off, alpha, mu_sig, result): stat = WStatCountsStatistic(n_on, n_off, alpha, mu_sig) excess = stat.n_sig sqrt_ts = stat.sqrt_ts p_value = stat.p_value del_ts = stat.ts assert_allclose(excess, result[0], rtol=1e-4) assert_allclose(sqrt_ts, result[1], rtol=1e-4) assert_allclose(p_value, result[2], rtol=1e-4) assert_allclose(del_ts, result[3], rtol=1e-4)
def info_dict(self, in_safe_data_range=True): """Info dict with summary statistics, summed over energy Parameters ---------- in_safe_data_range : bool Whether to sum only in the safe energy range Returns ------- info_dict : dict Dictionary with summary info. """ info = super().info_dict(in_safe_data_range) if self.mask_safe and in_safe_data_range: mask = self.mask_safe.data.astype(bool) else: mask = slice(None) counts_off = np.nan if self.counts_off is not None: counts_off = self.counts_off.data[mask].sum() info["counts_off"] = counts_off acceptance = 1 if self.acceptance: # TODO: handle energy dependent a_on / a_off acceptance = self.acceptance.data[mask].sum() info["acceptance"] = acceptance acceptance_off = np.nan if self.acceptance_off: acceptance_off = acceptance * counts_off / info["background"] info["acceptance_off"] = acceptance_off alpha = np.nan if self.acceptance_off and self.acceptance: alpha = np.mean(self.alpha.data[mask]) info["alpha"] = alpha info["sqrt_ts"] = WStatCountsStatistic( info["counts"], info["counts_off"], acceptance / acceptance_off, ).sqrt_ts info["stat_sum"] = self.stat_sum() return info
def calculate_sensitivity_lima(n_signal, n_background, alpha): """ Sensitivity calculation using the Li & Ma formula eq. 17 of Li & Ma (1983). https://ui.adsabs.harvard.edu/abs/1983ApJ...272..317L/abstract We calculate the sensitivity in bins of energy and theta2 Parameters ---------- n_on_events: `numpy.ndarray` number of ON events in the signal region n_background: `numpy.ndarray` number of events in the background region alpha: `float` inverse of the number of off positions n_bins_energy: `int` number of bins in energy n_bins_theta2: `int` number of bins in theta2 Returns ------- sensitivity: `numpy.ndarray` sensitivity in percentage of Crab units n_excesses_5sigma: `numpy.ndarray` number of excesses corresponding to a 5 sigma significance """ stat = WStatCountsStatistic(n_on=n_signal + alpha * n_background, n_off=n_background, alpha=alpha) n_excesses_5sigma = stat.n_sig_matching_significance(5) n_excesses_5sigma[n_excesses_5sigma < 10] = 10 bkg_5percent = 0.05 * n_background * alpha n_excesses_5sigma[n_excesses_5sigma < bkg_5percent] = bkg_5percent[ n_excesses_5sigma < bkg_5percent] sensitivity = n_excesses_5sigma / (n_signal) * 100 # percentage of Crab return n_excesses_5sigma, sensitivity
def convolved_map_dataset_counts_statistics(dataset, kernel, apply_mask_fit=False): """Return CountsDataset objects containing smoothed maps from the MapDataset""" # Kernel is modified later make a copy here kernel = copy.deepcopy(kernel) kernel.normalize("peak") mask = np.ones(dataset.data_shape, dtype=bool) if dataset.mask_safe: mask *= dataset.mask_safe if apply_mask_fit: mask *= dataset.mask_fit # fft convolution adds numerical noise, to ensure integer results we call # np.rint n_on = dataset.counts * mask n_on = n_on.sum_over_axes(keepdims=True) n_on_conv = np.rint(n_on.convolve(kernel.array).data) if isinstance(dataset, MapDatasetOnOff): background = dataset.counts_off_normalised * mask background.data[dataset.acceptance_off.data == 0] = 0.0 n_off = dataset.counts_off * mask background = background.sum_over_axes(keepdims=True) n_off = n_off.sum_over_axes(keepdims=True) background_conv = background.convolve(kernel.array) n_off_conv = n_off.convolve(kernel.array) npred_sig = dataset.npred_sig() * mask npred_sig = npred_sig.sum_over_axes(keepdims=True) mu_sig = npred_sig.convolve(kernel.array) with np.errstate(invalid="ignore", divide="ignore"): alpha_conv = background_conv / n_off_conv return WStatCountsStatistic(n_on_conv.data, n_off_conv.data, alpha_conv.data, mu_sig.data) else: npred = dataset.npred() * mask npred = npred.sum_over_axes(keepdims=True) background_conv = npred.convolve(kernel.array) return CashCountsStatistic(n_on_conv.data, background_conv.data)
def sigmax(dsets, stacked=False): """ Compute the max significance from the cumulated counts in a fluctuated dataset. Note: the value can change from depending on the random generation (this is not comparable to the mean maximal siginificance from a full simulation). Parameters ---------- dsets : list of Dataset objects Input datasets Returns ------- sig : float Significance """ import mcsim_config as mcf from gammapy.stats import WStatCountsStatistic non = 0 noff = 0 sigmax = -999 for i, ds in enumerate(dsets): if (stacked): # Data have already been masked ns = ds.excess.data.flatten().sum() nb = ds.background.data.flatten().sum() else: # Data should be masked ns = ds.excess.data[ds.mask_safe].flatten().sum() nb = ds.background.data[ds.mask_safe].flatten().sum() # ns = ds.npred_signal().data.sum() # don't use this for merged ds !!!! # if stacked: # mask applied, and existing mask if for 1st element # nb = ds.npred_background().data.sum() # else: # nb = ds.npred_background().data[ds.mask_safe].sum() on = (ns + nb) off = (nb / mcf.alpha) non += on noff += off wstat = WStatCountsStatistic(n_on=non, n_off=noff, alpha=mcf.alpha) if wstat.sqrt_ts > sigmax: sigmax = wstat.sqrt_ts return sigmax
"""Example plot showing the profile of the Wstat statistic and its connection to significance.""" import numpy as np import matplotlib.pyplot as plt from gammapy.stats import WStatCountsStatistic count_statistic = WStatCountsStatistic(n_on=13, n_off=11, alpha=0.5) excess = count_statistic.excess # We compute the WStat statistic profile mu_signal = np.linspace(-2.5, 26, 100) stat_values = count_statistic._stat_fcn(mu_signal) xmin, xmax = -2.5, 26 ymin, ymax = 0, 15 plt.figure(figsize=(5, 5)) plt.plot(mu_signal, stat_values, color="k") plt.xlim(xmin, xmax) plt.ylim(ymin, ymax) plt.xlabel(r"Number of expected signal event, $\mu_{sig}$") plt.ylabel(r"WStat value, TS ") plt.vlines( excess, ymin=ymin, ymax=count_statistic.stat_max, linestyle="dashed", color="k", label="Best fit", ) plt.hlines(count_statistic.stat_max, xmin=xmin,
def make_prof(self, sp_datasets): """ Utility to make the profile in each region Parameters ---------- sp_datasets : `~gammapy.datasets.MapDatasets` of `~gammapy.datasets.SpectrumDataset` or \ `~gammapy.datasets.SpectrumDatasetOnOff` the dataset to use for profile extraction Returns -------- results : list of dictionary the list of results (list of keys: x_min, x_ref, x_max, alpha, counts, background, excess, ts, sqrt_ts, \ err, errn, errp, ul, exposure, solid_angle) """ results = [] distance = self._get_projected_distance() for index, spds in enumerate(sp_datasets): old_model = None if spds.models is not None: old_model = spds.models spds.models = SkyModel(spectral_model=self.spectrum) e_reco = spds.counts.geom.axes["energy"].edges # ToDo: When the function to_spectrum_dataset will manage the masks, use the following line # mask = spds.mask if spds.mask is not None else slice(None) mask = slice(None) if isinstance(spds, SpectrumDatasetOnOff): stats = WStatCountsStatistic( spds.counts.data[mask][:, 0, 0], spds.counts_off.data[mask][:, 0, 0], spds.alpha.data[mask][:, 0, 0], ) else: stats = CashCountsStatistic( spds.counts.data[mask][:, 0, 0], spds.npred_background().data[mask][:, 0, 0], ) result = { "x_min": distance.edges[index], "x_max": distance.edges[index + 1], "x_ref": distance.center[index], "energy_edge": e_reco, } if isinstance(spds, SpectrumDatasetOnOff): result["alpha"] = stats.alpha result.update( { "counts": stats.n_on, "background": stats.mu_bkg, "excess": stats.n_sig, } ) result["ts"] = stats.ts result["sqrt_ts"] = stats.sqrt_ts result["err"] = stats.error * self.n_sigma if "errn-errp" in self.selection_optional: result["errn"] = stats.compute_errn(self.n_sigma) result["errp"] = stats.compute_errp(self.n_sigma) if "ul" in self.selection_optional: result["ul"] = stats.compute_upper_limit(self.n_sigma_ul) npred = spds.npred().data[mask][:, 0, 0] e_reco_lo = e_reco[:-1] e_reco_hi = e_reco[1:] flux = ( stats.n_sig / npred * spds.models[0].spectral_model.integral(e_reco_lo, e_reco_hi).value ) result["flux"] = flux result["flux_err"] = stats.error / stats.n_sig * flux if "errn-errp" in self.selection_optional: result["flux_errn"] = np.abs(result["errn"]) / stats.n_sig * flux result["flux_errp"] = result["errp"] / stats.n_sig * flux if "ul" in self.selection_optional: result["flux_ul"] = result["ul"] / stats.n_sig * flux solid_angle = spds.counts.geom.solid_angle() result["solid_angle"] = ( np.full(result["counts"].shape, solid_angle.to_value("sr")) * u.sr ) results.append(result) if old_model is not None: spds.models = old_model return results
def test_wstat_excess_matching_significance(n_off, alpha, significance, result): stat = WStatCountsStatistic(1, n_off, alpha) excess = stat.excess_matching_significance(significance) assert_allclose(excess, result, atol=1e-3)
def test_wstat_ul(n_on, n_off, alpha, result): stat = WStatCountsStatistic(n_on, n_off, alpha) ul = stat.compute_upper_limit() assert_allclose(ul, result[0], atol=1e-5)
def analyze_on_off(config): """ Extracts the theta2 plot of a dataset taken with ON/OFF observations Parameters ---------- config_file """ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 8)) LOGGER.info("Running ON/OFF analysis") LOGGER.info("ON data runs: %s", config['analysis']['runs_on']) observation_time_on, data_on = merge_dl2_runs( config['input']['data_tag'], config['analysis']['runs_on'], config['input']['columns_to_read'], 4) LOGGER.info("ON observation time: %s", observation_time_on) LOGGER.info("OFF data runs: %s", config['analysis']['runs_off']) observation_time_off, data_off = merge_dl2_runs( config['input']['data_tag'], config['analysis']['runs_off'], config['input']['columns_to_read'], 4) LOGGER.info("OFF observation time: %s", observation_time_off) # observation_time_ratio = observation_time_on / observation_time_off # LOGGER.info('Observation time ratio %s', observation_time_ratio) selected_data_on = filter_events(data_on, config['preselection']) selected_data_off = filter_events(data_off, config['preselection']) theta2_on = np.array(compute_theta2(selected_data_on, (0, 0))) theta2_off = np.array(compute_theta2(selected_data_off, (0, 0))) theta2_cut = config['analysis']['selection']['theta2'][0] n_on = np.sum(theta2_on < theta2_cut) n_off = np.sum(theta2_off < theta2_cut) LOGGER.info('Number of observed ON and OFF events are:\n %s, %s', n_on, n_off) theta2_norm_min = config['analysis']['selection']['theta2'][1] theta2_norm_max = config['analysis']['selection']['theta2'][2] n_norm_on = np.sum((theta2_on > theta2_norm_min) & (theta2_on < theta2_norm_max)) n_norm_off = np.sum((theta2_off > theta2_norm_min) & (theta2_off < theta2_norm_max)) lima_norm = n_norm_on / n_norm_off stat = WStatCountsStatistic(n_on, n_off, lima_norm) lima_significance = stat.sqrt_ts.item() lima_excess = stat.n_sig LOGGER.info('Excess is %s', lima_excess) LOGGER.info('Excess significance is %s', lima_significance) plotting.plot_1d_excess( [('ON data', theta2_on, 1), (f'OFF data X {lima_norm:.2f}', theta2_off, lima_norm)], lima_significance, r'$\theta^2$ [deg$^2$]', theta2_cut, ax1) # alpha analysis LOGGER.info('Perform alpha analysis') alpha_on = np.array(compute_alpha(selected_data_on)) alpha_off = np.array(compute_alpha(selected_data_off)) alpha_cut = config['analysis']['selection']['alpha'][0] n_on = np.sum(alpha_on < alpha_cut) n_off = np.sum(alpha_off < alpha_cut) LOGGER.info('Number of observed ON and OFFevents are:\n %s, %s', n_on, n_off) alpha_norm_min = config['analysis']['selection']['alpha'][1] alpha_norm_max = config['analysis']['selection']['alpha'][2] n_norm_on = np.sum((alpha_on > alpha_norm_min) & (alpha_on < alpha_norm_max)) n_norm_off = np.sum((alpha_off > alpha_norm_min) & (alpha_off < alpha_norm_max)) lima_norm = n_norm_on / n_norm_off stat = WStatCountsStatistic(n_on, n_off, lima_norm) lima_significance = stat.sqrt_ts.item() lima_excess = stat.n_sig LOGGER.info('Excess is %s', lima_excess) LOGGER.info('Excess significance is %s', lima_significance) plotting.plot_1d_excess( [('ON data', alpha_on, 1), (f'OFF data X {lima_norm:.2f}', alpha_off, lima_norm)], lima_significance, r'$\alpha$ [deg]', alpha_cut, ax2, 0, 90, 90) if config['output']['interactive'] is True: LOGGER.info( 'Interactive mode ON, plots will be only shown, but not saved') plt.show() else: LOGGER.info('Interactive mode OFF, no plots will be displayed') plt.ioff() plt.savefig(f"{config['output']['directory']}/on_off.png") plt.close()
"""Example plot showing the profile of the WStat statistic and its connection to excess errors.""" import numpy as np import matplotlib.pyplot as plt from gammapy.stats import WStatCountsStatistic count_statistic = WStatCountsStatistic(n_on=13, n_off=11, alpha=0.5) excess = count_statistic.excess errn = count_statistic.compute_errn(1.0) errp = count_statistic.compute_errp(1.0) errn_2sigma = count_statistic.compute_errn(2.0) errp_2sigma = count_statistic.compute_errp(2.0) # We compute the WStat statistic profile mu_signal = np.linspace(-2.5, 26, 100) stat_values = count_statistic._stat_fcn(mu_signal) xmin, xmax = -2.5, 26 ymin, ymax = 0, 15 plt.figure(figsize=(5, 5)) plt.plot(mu_signal, stat_values, color="k") plt.xlim(xmin, xmax) plt.ylim(ymin, ymax) plt.xlabel(r"Number of expected signal event, $\mu_{sig}$") plt.ylabel(r"WStat value, TS ") plt.hlines( count_statistic.stat_max + 1, xmin=excess + errn,
def analyze_wobble(config): """ Extracts the theta2 plot of a dataset taken with wobble observations Parameters ---------- config_file """ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(20, 8)) n_points = config['analysis']['parameters']['n_points'] theta2_cut = config['analysis']['selection']['theta2'][0] LOGGER.info( "Running wobble analysis with %s off-source observation points", n_points) LOGGER.info("Analyzing runs %s", config['analysis']['runs']) observation_time, data = merge_dl2_runs(config['input']['data_tag'], config['analysis']['runs'], config['input']['columns_to_read']) LOGGER.debug('\nPreselection:\n%s', config['preselection']) for key, value in config['preselection'].items(): LOGGER.debug('\nParameter: %s, range: %s, value type: %s', key, value, type(value)) selected_data = filter_events(data, filters=config['preselection']) # Add theta2 to selected data true_source_position = extract_source_position( selected_data, config['input']['observed_source']) plotting.plot_wobble(true_source_position, n_points, ax1) named_datasets = [] named_datasets.append( ('ON data', np.array(compute_theta2(selected_data, true_source_position)), 1)) n_on = np.sum(named_datasets[0][1] < theta2_cut) n_off = 0 rotation_angle = 360. / n_points origin_x = selected_data['reco_src_x'] origin_y = selected_data['reco_src_y'] for off_point in range(1, n_points): t_off_data = selected_data.copy() off_xy = rotate(tuple(zip(origin_x, origin_y)), rotation_angle * off_point) t_off_data['reco_src_x'] = [xy[0] for xy in off_xy] t_off_data['reco_src_y'] = [xy[1] for xy in off_xy] named_datasets.append( (f'OFF {rotation_angle * off_point}', np.array(compute_theta2(t_off_data, true_source_position)), 1)) n_off += np.sum(named_datasets[-1][1] < theta2_cut) stat = WStatCountsStatistic(n_on, n_off, 1. / (n_points - 1)) # API change for attributes significance and excess in the new gammapy version: https://docs.gammapy.org/dev/api/gammapy.stats.WStatCountsStatistic.html lima_significance = stat.sqrt_ts.item() lima_excess = stat.n_sig LOGGER.info('Observation time %s', observation_time) LOGGER.info('Number of "ON" events %s', n_on) LOGGER.info('Number of "OFF" events %s', n_off) LOGGER.info('ON/OFF observation time ratio %s', 1. / (n_points - 1)) LOGGER.info('Excess is %s', lima_excess) LOGGER.info('Li&Ma significance %s', lima_significance) plotting.plot_1d_excess(named_datasets, lima_significance, r'$\theta^2$ [deg$^2$]', theta2_cut, ax2) if config['output']['interactive'] is True: LOGGER.info( 'Interactive mode ON, plots will be only shown, but not saved') plt.show() else: LOGGER.info('Interactive mode OFF, no plots will be displayed') plt.ioff() plt.savefig(f"{config['output']['directory']}/wobble.png") plt.close()
def make_theta_squared_table(observations, theta_squared_axis, position, position_off=None): """Make theta squared distribution in the same FoV for a list of `Observation` objects. The ON theta2 profile is computed from a given distribution, on_position. By default, the OFF theta2 profile is extracted from a mirror position radially symmetric in the FOV to pos_on. The ON and OFF regions are assumed to be of the same size, so the normalisation factor between both region alpha = 1. Parameters ---------- observations: `~gammapy.data.Observations` List of observations theta_squared_axis : `~gammapy.maps.geom.MapAxis` Axis of edges of the theta2 bin used to compute the distribution position : `~astropy.coordinates.SkyCoord` Position from which the on theta^2 distribution is computed position_off : `astropy.coordinates.SkyCoord` Position from which the OFF theta^2 distribution is computed. Default: reflected position w.r.t. to the pointing position Returns ------- table : `~astropy.table.Table` Table containing the on counts, the off counts, acceptance, off acceptance and alpha for each theta squared bin. """ if not theta_squared_axis.edges.unit.is_equivalent("deg2"): raise ValueError("The theta2 axis should be equivalent to deg2") table = Table() table["theta2_min"] = theta_squared_axis.edges[:-1] table["theta2_max"] = theta_squared_axis.edges[1:] table["counts"] = 0 table["counts_off"] = 0 table["acceptance"] = 0.0 table["acceptance_off"] = 0.0 alpha_tot = np.zeros(len(table)) livetime_tot = 0 for observation in observations: separation = position.separation(observation.events.radec) counts, _ = np.histogram(separation**2, theta_squared_axis.edges) table["counts"] += counts if not position_off: # Estimate the position of the mirror position pos_angle = observation.pointing_radec.position_angle(position) sep_angle = observation.pointing_radec.separation(position) position_off = observation.pointing_radec.directional_offset_by( pos_angle + Angle(np.pi, "rad"), sep_angle) # Angular distance of the events from the mirror position separation_off = position_off.separation(observation.events.radec) # Extract the ON and OFF theta2 distribution from the two positions. counts_off, _ = np.histogram(separation_off**2, theta_squared_axis.edges) table["counts_off"] += counts_off # Normalisation between ON and OFF is one acceptance = np.ones(theta_squared_axis.nbin) acceptance_off = np.ones(theta_squared_axis.nbin) table["acceptance"] += acceptance table["acceptance_off"] += acceptance_off alpha = acceptance / acceptance_off alpha_tot += alpha * observation.observation_live_time_duration.to_value( "s") livetime_tot += observation.observation_live_time_duration.to_value( "s") alpha_tot /= livetime_tot table["alpha"] = alpha_tot stat = WStatCountsStatistic(table["counts"], table["counts_off"], table["alpha"]) table["excess"] = stat.n_sig table["sqrt_ts"] = stat.sqrt_ts table["excess_errn"] = stat.compute_errn() table["excess_errp"] = stat.compute_errp() table.meta["ON_RA"] = position.icrs.ra table.meta["ON_DEC"] = position.icrs.dec return table
def aperture_photometry(self): """ Perform an aperture photometry simulation and analysis. The counts are summed-up over enegy from the SpectrumDataset object list (The SpectrumDatasetOnOff objects are not created). The siginificance is computed under the assmption of a measured background, i.e. with possble fluctuation. Note that WStatCountsStatistic returns a significance with the sign of the excess (negative excess give negative significance). If the count number is not fluctuated, the excess can only be positive since it is obtained from a physical flux. Excess at zero gives a significance at zeo. More on the various statistics here : https://docs.gammapy.org/0.17/stats/fit_statistics.html Parameters ---------- Returns ------- sigma : numpy array, float One significance per time slice nxs : numpy array, float One excess count per time slice nbck : numy array, float One background count per time slice """ sigma_vs_time = [] # nxs = [] # nbck = [] non_vs_time = [] noff_vs_time = [] non = 0 noff = 0 sig = 0 ns = 0 nb = 0 header = True # for debuging # cumulate on and off counts along slices for ds_site in self.dset_list: # Cumulate on and off counts from potentially several sites for ds in ds_site: ns = ds.npred_signal().data[ds.mask_safe].sum() nb = ds.npred_background().data[ds.mask_safe].sum() if self.nosignal: ns = 0 non += (ns+nb) noff += (nb/mcf.alpha) if (self.dbg>2): if header: print() header = check_dataset(ds, deeper = (True if self.dbg>3 else False), masked = True, show_header = header) # Fluctuate (summed site if required) if (self.fluctuate): non = self.rnd_state.poisson(non) noff = self.rnd_state.poisson(noff) # Compute significance and background/excess counts wstat = WStatCountsStatistic(n_on = non, n_off = noff, alpha = mcf.alpha) sig = wstat.sqrt_ts # ? check # Much faster to append list than arrays # Array appending create a new object sigma_vs_time.append(sig) non_vs_time.append(non) noff_vs_time.append(noff) # nxs.append(ns) # nbck.append(nb) # End of loop over slices / datasets # Access to arrays is much faster than access to lists sigma_vs_time = np.array(sigma_vs_time) non_vs_time = np.array(non_vs_time) noff_vs_time = np.array(noff_vs_time) return (sigma_vs_time, non_vs_time, noff_vs_time)