def test_energy_bias_resolution(): from pyirf.benchmarks import energy_bias_resolution_from_energy_dispersion from pyirf.binning import bin_center # create a toy energy dispersion n_migra_bins = 500 true_bias = np.array([ [0.5, 0.6], [0, 0.1], [-0.1, -0.2], ]) true_resolution = np.array([ [0.4, 0.5], [0.2, 0.3], [0.1, 0.15], ]) n_energy_bins, n_fov_bins = true_bias.shape energy_bins = np.geomspace(10, 1000, n_energy_bins + 1) energy_center = bin_center(energy_bins) migra_bins = np.geomspace(0.2, 5, n_migra_bins + 1) cdf = np.empty((n_energy_bins, n_migra_bins + 1, n_fov_bins)) for energy_bin, fov_bin in product(range(n_energy_bins), range(n_fov_bins)): energy = energy_center[energy_bin] mu = (1 + true_bias[energy_bin, fov_bin]) * energy sigma = true_resolution[energy_bin, fov_bin] * energy reco_energy = migra_bins * energy cdf[energy_bin, :, fov_bin] = norm.cdf(reco_energy, mu, sigma) edisp = cdf[:, 1:, :] - cdf[:, :-1, :] bias, resolution = energy_bias_resolution_from_energy_dispersion( edisp, migra_bins, ) assert np.allclose(bias, true_bias, atol=0.01) assert np.allclose(resolution, true_resolution, atol=0.01)
def test_calculate_sensitivity(): from pyirf.sensitivity import calculate_sensitivity from pyirf.binning import bin_center bins = [0.1, 1.0, 10] * u.TeV signal_hist = QTable({ 'reco_energy_low': bins[:-1], 'reco_energy_high': bins[1:], 'reco_energy_center': bin_center(bins), 'n': [100, 100], 'n_weighted': [100, 100], }) bg_hist = signal_hist.copy() bg_hist['n'] = 20 bg_hist['n_weighted'] = 50 sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) # too small sensitivity signal_hist['n_weighted'] = [10, 10] sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) assert len(sensitivity) == len(signal_hist) assert np.all(np.isnan(sensitivity['relative_sensitivity'])) assert np.all(sensitivity['failed_checks'] == 0b100) # not above 5 percent of remaining background signal_hist['n_weighted'] = 699 bg_hist['n_weighted'] = 70_000 sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) assert len(sensitivity) == len(signal_hist) assert np.all(np.isnan(sensitivity['relative_sensitivity'])) # too few signal assert np.all(sensitivity['failed_checks'] == 0b010) # less then 10 events signal_hist['n_weighted'] = [9, 9] bg_hist['n_weighted'] = [1, 1] sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) assert len(sensitivity) == len(signal_hist) assert np.all(np.isnan(sensitivity['relative_sensitivity'])) # too few signal assert np.all(sensitivity['failed_checks'] == 0b001)
def test_calculate_sensitivity(): from pyirf.sensitivity import calculate_sensitivity from pyirf.binning import bin_center bins = [0.1, 1.0, 10] * u.TeV signal_hist = QTable({ 'reco_energy_low': bins[:-1], 'reco_energy_high': bins[1:], 'reco_energy_center': bin_center(bins), 'n': [100, 100], 'n_weighted': [100, 100], }) bg_hist = signal_hist.copy() bg_hist['n'] = 20 bg_hist['n_weighted'] = 50 sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) # too small sensitivity signal_hist['n_weighted'] = [10, 10] sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) assert len(sensitivity) == len(signal_hist) assert np.all(np.isnan(sensitivity['relative_sensitivity'])) # not above 5 percent of remaining background signal_hist['n_weighted'] = 699 bg_hist['n_weighted'] = 70_000 sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) assert len(sensitivity) == len(signal_hist) # we scale up the signal until we meet the requirement, so # we must have more than 5 sigma assert np.all(sensitivity['significance'] >= 5) # less then 10 events signal_hist['n_weighted'] = [9, 9] bg_hist['n_weighted'] = [1, 1] sensitivity = calculate_sensitivity(signal_hist, bg_hist, alpha=0.2) # we scale up the signal until we meet the requirement, so # we must have more than 5 sigma assert np.all(sensitivity['significance'] >= 5)
def main(): logging.basicConfig(level=logging.INFO) logging.getLogger("pyirf").setLevel(logging.DEBUG) for particle_type, p in particles.items(): log.info(f"Simulated {particle_type.title()} Events:") p["events"], p["simulation_info"] = read_eventdisplay_fits(p["file"]) p["events"]["particle_type"] = particle_type p["simulated_spectrum"] = PowerLaw.from_simulation( p["simulation_info"], T_OBS) p["events"]["weight"] = calculate_event_weights( p["events"]["true_energy"], p["target_spectrum"], p["simulated_spectrum"]) for prefix in ('true', 'reco'): k = f"{prefix}_source_fov_offset" p["events"][k] = calculate_source_fov_offset(p["events"], prefix=prefix) # calculate theta / distance between reco and assuemd source positoin # we handle only ON observations here, so the assumed source pos # is the pointing position p["events"]["theta"] = calculate_theta( p["events"], assumed_source_az=p["events"]["pointing_az"], assumed_source_alt=p["events"]["pointing_alt"], ) log.info(p["simulation_info"]) log.info("") gammas = particles["gamma"]["events"] # background table composed of both electrons and protons background = table.vstack( [particles["proton"]["events"], particles["electron"]["events"]]) INITIAL_GH_CUT = np.quantile(gammas['gh_score'], (1 - INITIAL_GH_CUT_EFFICENCY)) log.info( f"Using fixed G/H cut of {INITIAL_GH_CUT} to calculate theta cuts") # event display uses much finer bins for the theta cut than # for the sensitivity theta_bins = add_overflow_bins( create_bins_per_decade(10**(-1.9) * u.TeV, 10**2.3005 * u.TeV, 50)) # same bins as event display uses sensitivity_bins = add_overflow_bins( create_bins_per_decade(10**-1.9 * u.TeV, 10**2.31 * u.TeV, bins_per_decade=5)) # theta cut is 68 percent containmente of the gammas # for now with a fixed global, unoptimized score cut # the cut is calculated in the same bins as the sensitivity, # but then interpolated to 10x the resolution. mask_theta_cuts = gammas["gh_score"] >= INITIAL_GH_CUT theta_cuts_coarse = calculate_percentile_cut( gammas["theta"][mask_theta_cuts], gammas["reco_energy"][mask_theta_cuts], bins=sensitivity_bins, min_value=0.05 * u.deg, fill_value=0.32 * u.deg, max_value=0.32 * u.deg, percentile=68, ) # interpolate to 50 bins per decade theta_center = bin_center(theta_bins) inter_center = bin_center(sensitivity_bins) theta_cuts = table.QTable({ "low": theta_bins[:-1], "high": theta_bins[1:], "center": theta_center, "cut": np.interp(np.log10(theta_center / u.TeV), np.log10(inter_center / u.TeV), theta_cuts_coarse['cut']), }) log.info("Optimizing G/H separation cut for best sensitivity") gh_cut_efficiencies = np.arange( GH_CUT_EFFICIENCY_STEP, MAX_GH_CUT_EFFICIENCY + GH_CUT_EFFICIENCY_STEP / 2, GH_CUT_EFFICIENCY_STEP) sensitivity, gh_cuts = optimize_gh_cut( gammas, background, reco_energy_bins=sensitivity_bins, gh_cut_efficiencies=gh_cut_efficiencies, op=operator.ge, theta_cuts=theta_cuts, alpha=ALPHA, background_radius=MAX_BG_RADIUS, ) # now that we have the optimized gh cuts, we recalculate the theta # cut as 68 percent containment on the events surviving these cuts. log.info('Recalculating theta cut for optimized GH Cuts') for tab in (gammas, background): tab["selected_gh"] = evaluate_binned_cut(tab["gh_score"], tab["reco_energy"], gh_cuts, operator.ge) gammas["selected_theta"] = evaluate_binned_cut(gammas["theta"], gammas["reco_energy"], theta_cuts, operator.le) gammas["selected"] = gammas["selected_theta"] & gammas["selected_gh"] # scale relative sensitivity by Crab flux to get the flux sensitivity spectrum = particles['gamma']['target_spectrum'] sensitivity["flux_sensitivity"] = ( sensitivity["relative_sensitivity"] * spectrum(sensitivity['reco_energy_center'])) log.info('Calculating IRFs') hdus = [ fits.PrimaryHDU(), fits.BinTableHDU(sensitivity, name="SENSITIVITY"), fits.BinTableHDU(theta_cuts, name="THETA_CUTS"), fits.BinTableHDU(gh_cuts, name="GH_CUTS"), ] masks = { "": gammas["selected"], "_NO_CUTS": slice(None), "_ONLY_GH": gammas["selected_gh"], "_ONLY_THETA": gammas["selected_theta"], } # binnings for the irfs true_energy_bins = add_overflow_bins( create_bins_per_decade(10**-1.9 * u.TeV, 10**2.31 * u.TeV, 10)) reco_energy_bins = add_overflow_bins( create_bins_per_decade(10**-1.9 * u.TeV, 10**2.31 * u.TeV, 5)) fov_offset_bins = [0, 0.5] * u.deg source_offset_bins = np.arange(0, 1 + 1e-4, 1e-3) * u.deg energy_migration_bins = np.geomspace(0.2, 5, 200) for label, mask in masks.items(): effective_area = effective_area_per_energy( gammas[mask], particles["gamma"]["simulation_info"], true_energy_bins=true_energy_bins, ) hdus.append( create_aeff2d_hdu( effective_area[..., np.newaxis], # add one dimension for FOV offset true_energy_bins, fov_offset_bins, extname="EFFECTIVE_AREA" + label, )) edisp = energy_dispersion( gammas[mask], true_energy_bins=true_energy_bins, fov_offset_bins=fov_offset_bins, migration_bins=energy_migration_bins, ) hdus.append( create_energy_dispersion_hdu( edisp, true_energy_bins=true_energy_bins, migration_bins=energy_migration_bins, fov_offset_bins=fov_offset_bins, extname="ENERGY_DISPERSION" + label, )) bias_resolution = energy_bias_resolution(gammas[gammas["selected"]], reco_energy_bins, energy_type="reco") ang_res = angular_resolution(gammas[gammas["selected_gh"]], reco_energy_bins, energy_type="reco") psf = psf_table( gammas[gammas["selected_gh"]], true_energy_bins, fov_offset_bins=fov_offset_bins, source_offset_bins=source_offset_bins, ) background_rate = background_2d( background[background['selected_gh']], reco_energy_bins, fov_offset_bins=np.arange(0, 11) * u.deg, t_obs=T_OBS, ) hdus.append( create_background_2d_hdu( background_rate, reco_energy_bins, fov_offset_bins=np.arange(0, 11) * u.deg, )) hdus.append( create_psf_table_hdu( psf, true_energy_bins, source_offset_bins, fov_offset_bins, )) hdus.append( create_rad_max_hdu(theta_cuts["cut"][:, np.newaxis], theta_bins, fov_offset_bins)) hdus.append(fits.BinTableHDU(ang_res, name="ANGULAR_RESOLUTION")) hdus.append( fits.BinTableHDU(bias_resolution, name="ENERGY_BIAS_RESOLUTION")) log.info('Writing outputfile') fits.HDUList(hdus).writeto("pyirf_eventdisplay.fits.gz", overwrite=True)