From d6c1a2b2e4c58e306e019905fe66b9fc017dd26f Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 21 Mar 2022 12:05:13 +0100 Subject: [PATCH 001/274] Added the TextFileLoader class to load text data files. --- skyllh/core/storage.py | 196 ++++++++++++++++++++++++++++++++++++++++- 1 file changed, 194 insertions(+), 2 deletions(-) diff --git a/skyllh/core/storage.py b/skyllh/core/storage.py index 1179304b4e..c8b2cf629f 100644 --- a/skyllh/core/storage.py +++ b/skyllh/core/storage.py @@ -81,7 +81,8 @@ def create_FileLoader(pathfilenames, **kwargs): cls = _FILE_LOADER_REG[fmt] return cls(pathfilenames, **kwargs) - raise RuntimeError('No FileLoader class is suitable to load the data file "%s"!'%(pathfilenames[0])) + raise RuntimeError('No FileLoader class is suitable to load the data file ' + '"%s"!'%(pathfilenames[0])) def assert_file_exists(pathfilename): """Checks if the given file exists and raises a RuntimeError if it does @@ -92,7 +93,8 @@ def assert_file_exists(pathfilename): class FileLoader(object, metaclass=abc.ABCMeta): - + """Abstract base class for a FileLoader class. + """ def __init__(self, pathfilenames, **kwargs): """Initializes a new FileLoader instance. @@ -408,6 +410,195 @@ def load_data(self, **kwargs): return data +class TextFileLoader(FileLoader): + """The TextFileLoader class provides the data loading functionality for + data text files where values are stored in a comma, or whitespace, separated + format. It uses the numpy.loadtxt function to load the data. It reads the + first line of the text file for a table header. + """ + def __init__(self, pathfilenames, header_comment='#', header_separator=None, + **kwargs): + """Creates a new file loader instance for a text data file. + """ + super().__init__(pathfilenames, **kwargs) + + self.header_comment = header_comment + self.header_separator = header_separator + + @property + def header_comment(self): + """The character that defines a comment line in the text file. + """ + return self._header_comment + @header_comment.setter + def header_comment(self, s): + if(not isinstance(s, str)): + raise TypeError('The header_comment property must be of type str!') + self._header_comment = s + + @property + def header_separator(self): + """The separator of the header field names. If None, it assumes + whitespaces. + """ + return self._header_separator + @header_separator.setter + def header_separator(self, s): + if(s is not None): + if(not isinstance(s, str)): + raise TypeErr('The header_separator property must be None or ' + 'of type str!') + self._header_separator = s + + def _extract_column_names(self, line): + """Tries to extract the column names of the data table based on the + given line. + + Parameters + ---------- + line : str + The text line containing the column names. + + Returns + ------- + names : list of str | None + The column names. + It returns None, if the column names cannot be extracted. + """ + # Remove possible new-line character and leading white-spaces. + line = line.strip() + # Check if the line is a comment line. + if(line[0:len(self._header_comment)] != self._header_comment): + return None + # Remove the leading comment character(s). + line = line.strip(self._header_comment) + # Remove possible leading whitespace characters. + line = line.strip() + # Split the line into the column names. + names = line.split(self._header_separator) + # Remove possible whitespaces of column names. + names = [ n.strip() for n in names ] + + if(len(names) == 0): + return None + + return names + + def _load_file(self, pathfilename, keep_fields, dtype_convertions, + dtype_convertion_except_fields): + """Loads the given file. + + Returns + ------- + data : DataFieldRecordArray instance + The DataFieldRecordArray instance holding the loaded data. + """ + assert_file_exists(pathfilename) + + with open(pathfilename, 'r') as ifile: + line = ifile.readline() + column_names = self._extract_column_names(line) + if(column_names is None): + raise ValueError('The data text file "{}" does not contain a ' + 'readable table header as first line!'.format(pathfilename)) + usecols = None + dtype = [(n,np.float) for n in column_names] + if(keep_fields is not None): + # Select only the given columns. + usecols = [] + dtype = [] + for (idx,name) in enumerate(column_names): + if(name in keep_fields): + usecols.append(idx) + dtype.append((name,np.float)) + usecols = tuple(usecols) + + data_ndarray = np.loadtxt(ifile, + dtype=dtype, + comments=self._header_comment, + usecols=usecols) + + data = DataFieldRecordArray( + data_ndarray, + keep_fields=keep_fields, + dtype_convertions=dtype_convertions, + dtype_convertion_except_fields=dtype_convertion_except_fields, + copy=False) + + return data + + def load_data(self, keep_fields=None, dtype_convertions=None, + dtype_convertion_except_fields=None, **kwargs): + """Loads the data from the data files specified through their fully + qualified file names. + + Parameters + ---------- + keep_fields : str | sequence of str | None + Load the data into memory only for these data fields. If set to + ``None``, all in-file-present data fields are loaded into memory. + dtype_convertions : dict | None + If not None, this dictionary defines how data fields of specific + data types get converted into the specified data types. + This can be used to use less memory. + dtype_convertion_except_fields : str | sequence of str | None + The sequence of field names whose data type should not get + converted. + + Returns + ------- + data : DataFieldRecordArray + The DataFieldRecordArray holding the loaded data. + + Raises + ------ + RuntimeError + If a file does not exist. + ValueError + If the table header cannot be read. + """ + if(keep_fields is not None): + if(isinstance(keep_fields, str)): + keep_fields = [ keep_fields ] + elif(not issequenceof(keep_fields, str)): + raise TypeError('The keep_fields argument must be None, an ' + 'instance of type str, or a sequence of instances of ' + 'type str!') + + if(dtype_convertions is None): + dtype_convertions = dict() + elif(not isinstance(dtype_convertions, dict)): + raise TypeError('The dtype_convertions argument must be None, ' + 'or an instance of dict!') + + if(dtype_convertion_except_fields is None): + dtype_convertion_except_fields = [] + elif(isinstance(dtype_convertion_except_fields, str)): + dtype_convertion_except_fields = [ dtype_convertion_except_fields ] + elif(not issequenceof(dtype_convertion_except_fields, str)): + raise TypeError('The dtype_convertion_except_fields argument ' + 'must be a sequence of str instances.') + + # Load the first data file. + data = self._load_file( + self._pathfilename_list[0], + keep_fields=keep_fields, + dtype_convertions=dtype_convertions, + dtype_convertion_except_fields=dtype_convertion_except_fields + ) + + # Load possible subsequent data files by appending to the first data. + for i in range(1, len(self._pathfilename_list)): + data.append(self._load_file( + self._pathfilename_list[i], + keep_fields=keep_fields, + dtype_convertions=dtype_convertions, + dtype_convertion_except_fields=dtype_convertion_except_fields + )) + + return data + + class DataFieldRecordArray(object): """The DataFieldRecordArray class provides a data container similar to a numpy record ndarray. But the data fields are stored as individual numpy ndarray @@ -977,3 +1168,4 @@ def sort_by_field(self, name): register_FileLoader(['.npy'], NPYFileLoader) register_FileLoader(['.pkl'], PKLFileLoader) +register_FileLoader(['.csv'], TextFileLoader) From e601acdafddc8a780b889d7fb944287c7eeed8dd Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 21 Mar 2022 15:31:29 +0100 Subject: [PATCH 002/274] Added initial dataset definition for public ps data. --- skyllh/datasets/__init__.py | 0 skyllh/datasets/i3/PublicData_10y_ps.py | 267 ++++++++++++++++++++++++ skyllh/datasets/i3/__init__.py | 0 3 files changed, 267 insertions(+) create mode 100644 skyllh/datasets/__init__.py create mode 100644 skyllh/datasets/i3/PublicData_10y_ps.py create mode 100644 skyllh/datasets/i3/__init__.py diff --git a/skyllh/datasets/__init__.py b/skyllh/datasets/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py new file mode 100644 index 0000000000..2fbb4bafa3 --- /dev/null +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -0,0 +1,267 @@ +# -*- coding: utf-8 -*- +# Author: Dr. Martin Wolf + +import os.path +import numpy as np + +from skyllh.core.dataset import DatasetCollection +from skyllh.i3.dataset import I3Dataset + + +def create_dataset_collection(base_path=None, sub_path_fmt=None): + """Defines the dataset collection for IceCube's 10-year + point-source public data, which is available at + http://icecube.wisc.edu/data-releases/20210126_PS-IC40-IC86_VII.zip + + Parameters + ---------- + base_path : str | None + The base path of the data files. The actual path of a data file is + assumed to be of the structure //. + If None, use the default path CFG['repository']['base_path']. + sub_path_fmt : str | None + The sub path format of the data files of the public data sample. + If None, use the default sub path format + 'icecube_10year_ps'. + + Returns + ------- + dsc : DatasetCollection + The dataset collection containing all the seasons as individual + I3Dataset objects. + """ + # Define the version of the data sample (collection). + (version, verqualifiers) = (1, dict(p=0)) + + # Define the default sub path format. + default_sub_path_fmt = 'icecube_10year_ps' + + # We create a dataset collection that will hold the individual seasonal + # public data datasets (all of the same version!). + dsc = DatasetCollection('Public Data 10-year point-source') + + dsc.description = """ + The events contained in this release correspond to the IceCube's + time-integrated point source search with 10 years of data [2]. Please refer + to the description of the sample and known changes in the text at [1]. + + The data contained in this release of IceCube’s point source sample shows + evidence of a cumulative excess of events from four sources (NGC 1068, + TXS 0506+056, PKS 1424+240, and GB6 J1542+6129) from a catalogue of 110 + potential sources. NGC 1068 gives the largest excess and is coincidentally + the hottest spot in the full Northern sky search [1]. + + Data from IC86-2012 through IC86-2014 used in [2] use an updated selection + and reconstruction compared to the 7 year time-integrated search [3] and the + detection of the 2014-2015 neutrino flare from the direction of + TXS 0506+056 [4]. The 7 year and 10 year versions of the sample show + overlaps of between 80 and 90%. + + An a posteriori cross check of the updated sample has been performed on + TXS 0506+056 showing two previously-significant cascade-like events removed + in the newer sample. These two events occur near the blazar's position + during the TXS flare and give large reconstructed energies, but are likely + not well-modeled by the track-like reconstructions included in this + selection. While the events are unlikely to be track-like, their + contribution to previous results has been handled properly. + + While the significance of the 2014-2015 TXS 0505+56 flare has decreased from + p=7.0e-5 to 8.1e-3, the change is a result of changes to the sample and not + of increased data. No problems have been identified with the previously + published results and since we have no reason a priori to prefer the new + sample over the old sample, these results do not supercede those in [4]. + + This release contains data beginning in 2008 (IC40) until the spring of 2018 + (IC86-2017). This release duplicates and supplants previously released data + from 2012 and earlier. Events from this release cannot be combined with any + other releases + + ----------------------------------------- + # Experimental data events + ----------------------------------------- + The "events" folder contains the events observed in the 10 year sample of + IceCube's point source neutrino selection. Each file corresponds to a single + season of IceCube datataking, including roughly one year of data. For each + event, reconstructed particle information is included. + + - MJD: The MJD time (ut1) of the event interaction given to 1e-8 days, + corresponding to roughly millisecond precision. + + - log10(E/GeV): The reconstructed energy of a muon passing through the + detector. The reconstruction follows the prescription for unfolding the + given in Section 8 of [5]. + + - AngErr[deg]: The estimated angular uncertainty on the reconstructed + direction given in degrees. The angular uncertainty is assumed to be + symmetric in azimuth and zenith and is used to calculate the signal spatial + probabilities for each event following the procedure given in [6]. The + errors are calibrated using simulated events so that they provide correct + coverage for an E^{-2} power law flux. This sample assumes a lower limit on + the estimated angular uncertainty of 0.2 degrees. + + - RA[deg], Dec[deg]: The right ascension and declination (J2000) + corresponding to the particle's reconstructed origin. Given in degrees. + + - Azimuth[deg], Zenith[deg]: The local coordinates of the particle's + reconstructed origin. + + The local coordinates may be necessary when searching for transient + phenomena on timescales shorter than 1 day due to non-uniformity in the + detector's response as a function of azimuth. In these cases, we recommend + scrambling events in time, then using the local coordinates and time to + calculate new RA and Dec values. + + Note that during the preparation of this data release, one duplicated event + was discovered in the IC86-2015 season. This event has not contributed to + any significant excesses. + + ----------------------------------------- + # Detector uptime + ----------------------------------------- + In order to properly account for detector uptime, IceCube maintains + "good run lists". These contain information about "good runs", periods of + datataking useful for analysis. Data may be marked unusable for various + reasons, including major construction or upgrade work, calibration runs, or + other anomalies. The "uptime" folder contains lists of the good runs for + each season. + + - MJD_start[days], MJD_stop[days]: The start and end times for each good run + + ----------------------------------------- + # Instrument response functions + ----------------------------------------- + In order to best model the response of the IceCube detector to a given + signal, Monte Carlo simulations are produced for each detector + configuration. Events are sampled from these simulations to model the + response of point sources from an arbitrary source and spectrum. + + We provide several binned responses for the detector in the "irfs" folder + of this data release. + + ------------------ + # Effective Areas + ------------------ + The effective area is a property of the detector and selection which, when + convolved with a flux model, gives the expected rate of events in the + detector. Here we release the muon neutrino effective areas for each season + of data. + + The effective areas are averaged over bins using simulated muon neutrino + events ranging from 100 GeV to 100 PeV. Because the response varies widely + in both energy and declination, we provide the tabulated response in these + two dimensions. Due to IceCube's unique position at the south pole, the + effective area is uniform in right ascension for timescales longer than + 1 day. It varies by about 10% as a function of azimuth, an effect which may + be important for shorter timescales. While the azimuthal effective areas are + not included here, they are included in IceCube's internal analyses. + These may be made available upon request. + + Tabulated versions of the effective area are included in csv files in the + "irfs" folder. Plotted versions are included as pdf files in the same + location. Because the detector configuration and selection were unchanged + after the IC86-2012 season, the effective area for this season should be + used for IC86-2012 through IC86-2017. + + - log10(E_nu/GeV)_min, log10(E_nu/GeV)_max: The minimum and maximum of the + energy bin used to caclulate the average effective area. Note that this uses + the neutrino's true energy and not the reconstructed muon energy. + + - Dec_nu_min[deg], Dec_nu_max[deg]: The minimum and maximum of the + declination of the neutrino origin. Again, note that this is the true + direction of the neutrino and not the reconstructed muon direction. + + - A_Eff[cm^2]: The average effective area across a bin. + + ------------------ + # Smearing Matrices + ------------------ + IceCube has a nontrivial smearing matrix with correlations between the + directional uncertainty, the point spread function, and the reconstructed + muon energy. To provide the most complete set of information, we include + tables of these responses for each season from IC40 through IC86-2012. + Seasons after IC86-2012 reuse that season's response functions. + + The included smearing matrices take the form of 5D tables mapping a + (E_nu, Dec_nu) bin in effective area to a 3D matrix of (E, PSF, AngErr). + The contents of each 3D matrix bin give the fractional count of simulated + events within the bin relative to all events in the (E_nu, Dec_nu) bin. + + Fractional_Counts = [Events in (E_nu, Dec_nu, E, PSF, AngErr)] / + [Events in (E_nu, Dec_nu)] + + The simulations statistics, while large enough for direct sampling, are + limited when producing these tables, ranging from just 621,858 simulated + events for IC40 to 11,595,414 simulated events for IC86-2012. In order to + reduce statistical uncertainties in each 5D bin, bins are selected in each + (E_nu, Dec_nu) bin independently. The bin edges are given in the smearing + matrix files. All locations not given have a Fractional_Counts of 0. + + - log10(E_nu/GeV)_min, log10(E_nu/GeV)_max: The minimum and maximum of the + energy bin used to caclulate the average effective area. Note that this uses + the neutrino's true energy and not the reconstructed muon energy. + + - Dec_nu_min[deg], Dec_nu_max[deg]: The minimum and maximum of the + declination of the neutrino origin. Again, note that this is the true + direction of the neutrino and not the reconstructed muon direction. + + - log10(E/GeV): The reconstructed energy of a muon passing through the + detector. The reconstruction follows the prescription for unfolding the + given in Section 8 of [5]. + + - PSF_min[deg], PSF_max[deg]: The minimum and maximum of the true angle + between the neutrino origin and the reconstructed muon direction. + + - AngErr_min[deg], AngErr_max[deg]: The estimated angular uncertainty on the + reconstructed direction given in degrees. The angular uncertainty is assumed + to be symmetric in azimuth and zenith and is used to calculate the signal + spatial probabilities for each event following the procedure given in [6]. + The errors are calibrated so that they provide correct coverage for an + E^{-2} power law flux. This sample assumes a lower limit on the estimated + angular uncertainty of 0.2 degrees. + + - Fractional_Counts: The fraction of simulated events falling within each + 5D bin relative to all events in the (E_nu, Dec_nu) bin. + + ----------------------------------------- + # References + ----------------------------------------- + [1] IceCube Data for Neutrino Point-Source Searches: Years 2008-2018, + [[ArXiv link]] + [2] Time-integrated Neutrino Source Searches with 10 years of IceCube Data, + Phys. Rev. Lett. 124, 051103 (2020) + [3] All-sky search for time-integrated neutrino emission from astrophysical + sources with 7 years of IceCube data, + Astrophys. J., 835 (2017) no. 2, 151 + [4] Neutrino emission from the direction of the blazar TXS 0506+056 prior to + the IceCube-170922A alert, + Science 361, 147-151 (2018) + [5] Energy Reconstruction Methods in the IceCube Neutrino Telescope, + JINST 9 (2014), P03009 + [6] Methods for point source analysis in high energy neutrino telescopes, + Astropart.Phys.29:299-305,2008 + + ----------------------------------------- + # Last Update + ----------------------------------------- + 28 January 2021 + """ + + # Define the common keyword arguments for all data sets. + ds_kwargs = dict( + livetime = None, + version = version, + verqualifiers = verqualifiers, + base_path = base_path, + default_sub_path_fmt = default_sub_path_fmt, + sub_path_fmt = sub_path_fmt + ) + + IC40 = I3Dataset( + name = 'IC40', + exp_pathfilenames = 'events/IC40_exp.csv', + mc_pathfilenames = '', + **ds_kwargs + ) + + + return dsc diff --git a/skyllh/datasets/i3/__init__.py b/skyllh/datasets/i3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 From d152b68dcac0f1ed5d53c739c6839892d8fd38e7 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 21 Mar 2022 17:57:27 +0100 Subject: [PATCH 003/274] Added support for public data and GRL data preparation --- skyllh/core/dataset.py | 116 ++++++++++++++++++++++++----------------- skyllh/core/storage.py | 2 + skyllh/i3/dataset.py | 76 +++++++++++++++++---------- 3 files changed, 118 insertions(+), 76 deletions(-) diff --git a/skyllh/core/dataset.py b/skyllh/core/dataset.py index 868c4c21b8..6b99bde3f0 100644 --- a/skyllh/core/dataset.py +++ b/skyllh/core/dataset.py @@ -116,8 +116,9 @@ def __init__( exp_pathfilenames : str | sequence of str | None The file name(s), including paths, of the experimental data file(s). This can be None, if a MC-only study is performed. - mc_pathfilenames : str | sequence of str + mc_pathfilenames : str | sequence of str | None The file name(s), including paths, of the monte-carlo data file(s). + This can be None, if a MC-less analysis is performed. livetime : float | None The integrated live-time in days of the dataset. It can be None for cases where the live-time is retrieved directly from the data files @@ -223,6 +224,8 @@ def mc_pathfilename_list(self): return self._mc_pathfilename_list @mc_pathfilename_list.setter def mc_pathfilename_list(self, pathfilenames): + if(pathfilenames is None): + pathfilenames = [] if(isinstance(pathfilenames, str)): pathfilenames = [pathfilenames] if(not issequenceof(pathfilenames, str)): @@ -612,7 +615,9 @@ def update_version_qualifiers(self, verqualifiers): # number. if((q in self._verqualifiers) and (verqualifiers[q] <= self._verqualifiers[q])): - raise ValueError('The integer number (%d) of the version qualifier "%s" is not larger than the old integer number (%d)'%(verqualifiers[q], q, self._verqualifiers[q])) + raise ValueError('The integer number (%d) of the version ' + 'qualifier "%s" is not larger than the old integer number ' + '(%d)'%(verqualifiers[q], q, self._verqualifiers[q])) self._verqualifiers[q] = verqualifiers[q] def load_data( @@ -687,8 +692,9 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): # Load the experimental data if there is any. if(len(self._exp_pathfilename_list) > 0): - fileloader_exp = create_FileLoader(self.exp_abs_pathfilename_list) with TaskTimer(tl, 'Loading exp data from disk.'): + fileloader_exp = create_FileLoader( + self.exp_abs_pathfilename_list) # Create the list of field names that should get kept. keep_fields = list(set( _conv_new2orig_field_names( @@ -710,35 +716,36 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): else: data_exp = None - # Load the monte-carlo data. - with TaskTimer(tl, 'Loading mc data from disk.'): - fileloader_mc = create_FileLoader(self.mc_abs_pathfilename_list) - keep_fields = list(set( - _conv_new2orig_field_names( - CFG['dataset']['analysis_required_exp_field_names'] + - self._loading_extra_exp_field_name_list + - keep_fields, - self._exp_field_name_renaming_dict) + - _conv_new2orig_field_names( - CFG['dataset']['analysis_required_mc_field_names'] + - self._loading_extra_mc_field_name_list + - keep_fields, - self._mc_field_name_renaming_dict) - )) - data_mc = fileloader_mc.load_data( - keep_fields=keep_fields, - dtype_convertions=dtc_dict, - dtype_convertion_except_fields=_conv_new2orig_field_names( - dtc_except_fields, - self._mc_field_name_renaming_dict), - efficiency_mode=efficiency_mode) - data_mc.rename_fields(self._mc_field_name_renaming_dict) + # Load the monte-carlo data if there is any. + if(len(self._mc_pathfilename_list) > 0): + with TaskTimer(tl, 'Loading mc data from disk.'): + fileloader_mc = create_FileLoader( + self.mc_abs_pathfilename_list) + keep_fields = list(set( + _conv_new2orig_field_names( + CFG['dataset']['analysis_required_exp_field_names'] + + self._loading_extra_exp_field_name_list + + keep_fields, + self._exp_field_name_renaming_dict) + + _conv_new2orig_field_names( + CFG['dataset']['analysis_required_mc_field_names'] + + self._loading_extra_mc_field_name_list + + keep_fields, + self._mc_field_name_renaming_dict) + )) + data_mc = fileloader_mc.load_data( + keep_fields=keep_fields, + dtype_convertions=dtc_dict, + dtype_convertion_except_fields=_conv_new2orig_field_names( + dtc_except_fields, + self._mc_field_name_renaming_dict), + efficiency_mode=efficiency_mode) + data_mc.rename_fields(self._mc_field_name_renaming_dict) + else: + data_mc = None if(livetime is None): livetime = self.livetime - if(livetime is None): - raise ValueError('No livetime was provided for dataset ' - '"%s"!'%(self.name)) data = DatasetData(data_exp, data_mc, livetime) @@ -932,13 +939,15 @@ def load_and_prepare_data( keep_fields ) data.exp.tidy_up(keep_fields=keep_fields_exp) - with TaskTimer(tl, 'Cleaning MC data.'): - keep_fields_mc = ( - CFG['dataset']['analysis_required_exp_field_names'] + - CFG['dataset']['analysis_required_mc_field_names'] + - keep_fields - ) - data.mc.tidy_up(keep_fields=keep_fields_mc) + + if(data.mc is not None): + with TaskTimer(tl, 'Cleaning MC data.'): + keep_fields_mc = ( + CFG['dataset']['analysis_required_exp_field_names'] + + CFG['dataset']['analysis_required_mc_field_names'] + + keep_fields + ) + data.mc.tidy_up(keep_fields=keep_fields_mc) with TaskTimer(tl, 'Asserting data format.'): assert_data_format(self, data) @@ -1517,24 +1526,28 @@ def exp(self, data): @property def mc(self): """The DataFieldRecordArray instance holding the monte-carlo data. + This is None, if there is no monte-carlo data available. """ return self._mc @mc.setter def mc(self, data): - if(not isinstance(data, DataFieldRecordArray)): - raise TypeError('The mc property must be an instance of ' - 'DataFieldRecordArray!') + if(data is not None): + if(not isinstance(data, DataFieldRecordArray)): + raise TypeError('The mc property must be an instance of ' + 'DataFieldRecordArray!') self._mc = data @property def livetime(self): """The integrated livetime in days of the data. + This is None, if there is no live-time provided. """ return self._livetime @livetime.setter def livetime(self, lt): - lt = float_cast(lt, - 'The livetime property must be castable to type float!') + if(lt is not None): + lt = float_cast(lt, + 'The livetime property must be castable to type float!') self._livetime = lt @property @@ -1578,13 +1591,20 @@ def _get_missing_keys(keys, required_keys): 'experimental data of dataset "%s": '%(dataset.name)+ ', '.join(missing_exp_keys)) - # Check monte-carlo data keys. - missing_mc_keys = _get_missing_keys( - data.mc.field_name_list, - CFG['dataset']['analysis_required_exp_field_names'] + - CFG['dataset']['analysis_required_mc_field_names']) - if(len(missing_mc_keys) != 0): - raise KeyError('The following data fields are missing for the monte-carlo data of dataset "%s": '%(dataset.name)+', '.join(missing_mc_keys)) + if(data.mc is not None): + # Check monte-carlo data keys. + missing_mc_keys = _get_missing_keys( + data.mc.field_name_list, + CFG['dataset']['analysis_required_exp_field_names'] + + CFG['dataset']['analysis_required_mc_field_names']) + if(len(missing_mc_keys) != 0): + raise KeyError('The following data fields are missing for the ' + 'monte-carlo data of dataset "%s": '%(dataset.name)+ + ', '.join(missing_mc_keys)) + + if(data.livetime is None): + raise ValueError('No livetime was specified for dataset "{}"!'.format( + dataset.name)) def remove_events(data_exp, mjds): diff --git a/skyllh/core/storage.py b/skyllh/core/storage.py index c8b2cf629f..a6d0dbb1cb 100644 --- a/skyllh/core/storage.py +++ b/skyllh/core/storage.py @@ -512,6 +512,8 @@ def _load_file(self, pathfilename, keep_fields, dtype_convertions, usecols.append(idx) dtype.append((name,np.float)) usecols = tuple(usecols) + if(len(dtype) == 0): + raise ValueError('No data columns were selected to be loaded!') data_ndarray = np.loadtxt(ifile, dtype=dtype, diff --git a/skyllh/i3/dataset.py b/skyllh/i3/dataset.py index cf56df291e..038f64aac2 100644 --- a/skyllh/i3/dataset.py +++ b/skyllh/i3/dataset.py @@ -146,8 +146,8 @@ def __str__(self): return s def load_grl(self, efficiency_mode=None, tl=None): - """Loads the good-run-list and returns a structured numpy ndarray with - the following data fields: + """Loads the good-run-list and returns a DataFieldRecordArray instance + which should contain the following data fields: run : int The run number. @@ -248,41 +248,22 @@ def load_data( # Load the good-run-list (GRL) data if it is provided for this dataset, # and calculate the livetime based on the GRL. data_grl = None - lt = self.livetime if(len(self._grl_pathfilename_list) > 0): data_grl = self.load_grl( efficiency_mode=efficiency_mode, tl=tl) - if('livetime' not in data_grl.field_name_list): - raise KeyError('The GRL file(s) "%s" has no data field named ' - '"livetime"!'%(','.join(self._grl_pathfilename_list))) - lt = np.sum(data_grl['livetime']) - - # Override the livetime if there is a user defined livetime. - if(livetime is not None): - lt = livetime # Load all the defined data. data = I3DatasetData( super(I3Dataset, self).load_data( keep_fields=keep_fields, - livetime=lt, + livetime=livetime, dtc_dict=dtc_dict, dtc_except_fields=dtc_except_fields, efficiency_mode=efficiency_mode, tl=tl), data_grl) - # Select only the experimental data which fits the good-run-list for - # this dataset. - if(data_grl is not None): - task = 'Selected only the experimental data that matches the GRL '\ - 'for dataset "%s".'%(self.name) - with TaskTimer(tl, task): - runs = np.unique(data_grl['run']) - mask = np.isin(data.exp['run'], runs) - data.exp = data.exp[mask] - return data def prepare_data(self, data, tl=None): @@ -309,15 +290,43 @@ def prepare_data(self, data, tl=None): super(I3Dataset, self).prepare_data(data, tl=tl) if(data.exp is not None): + # Append sin(dec) data field to the experimental data. task = 'Appending IceCube-specific data fields to exp data.' with TaskTimer(tl, task): - data.exp.append_field('sin_dec', np.sin(data.exp['dec'])) + data.exp.append_field( + 'sin_dec', np.sin(data.exp['dec'])) - # Append sin(dec) and sin(true_dec) to the MC data. - task = 'Appending IceCube-specific data fields to MC data.' - with TaskTimer(tl, task): - data.mc.append_field('sin_dec', np.sin(data.mc['dec'])) - data.mc.append_field('sin_true_dec', np.sin(data.mc['true_dec'])) + if(data.mc is not None): + # Append sin(dec) and sin(true_dec) to the MC data. + task = 'Appending IceCube-specific data fields to MC data.' + with TaskTimer(tl, task): + data.mc.append_field( + 'sin_dec', np.sin(data.mc['dec'])) + data.mc.append_field( + 'sin_true_dec', np.sin(data.mc['true_dec'])) + + # Set the livetime of the dataset from the GRL data when no livetime + # was specified previously. + if(data.livetime is None and data.grl is not None): + if('start' not in data.grl): + raise KeyError('The GRL data for dataset "{}" has no data ' + 'field named "start"!'.format(self.name)) + if('stop' not in data.grl): + raise KeyError('The GRL data for dataset "{}" has no data ' + 'field named "stop"!'.format(self.name)) + data.livetime = np.sum(data.grl['stop'] - data.grl['start']) + + # Select only the experimental data which fits the good-run-list for + # this dataset. + if((data.grl is not None) and + ('run' in data.grl) and + ('run' in data.exp)): + task = 'Selected only the experimental data that matches the GRL '\ + 'for dataset "%s".'%(self.name) + with TaskTimer(tl, task): + runs = np.unique(data.grl['run']) + mask = np.isin(data.exp['run'], runs) + data.exp = data.exp[mask] class I3DatasetData(DatasetData): @@ -326,6 +335,17 @@ class I3DatasetData(DatasetData): holds the good-run-list (GRL) data. """ def __init__(self, data, data_grl): + """Constructs a new I3DatasetData instance. + + Parameters + ---------- + data : DatasetData instance + The DatasetData instance holding the experimental and monte-carlo + data. + data_grl : DataFieldRecordArray instance | None + The DataFieldRecordArray instance holding the good-run-list data + of the dataset. This can be None, if no GRL data is available. + """ super(I3DatasetData, self).__init__( data._exp, data._mc, data._livetime) From 0576ef10f70b9a993fb6ff1f585d0419e2afa5d9 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 21 Mar 2022 17:58:02 +0100 Subject: [PATCH 004/274] Defined all datasets of the public 10y ps data --- skyllh/datasets/i3/PublicData_10y_ps.py | 126 +++++++++++++++++++++++- 1 file changed, 125 insertions(+), 1 deletion(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 2fbb4bafa3..8ef25facc0 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -256,12 +256,136 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): sub_path_fmt = sub_path_fmt ) + grl_field_name_renaming_dict = { + 'MJD_start[days]': 'start', + 'MJD_stop[days]': 'stop' + } + IC40 = I3Dataset( name = 'IC40', exp_pathfilenames = 'events/IC40_exp.csv', - mc_pathfilenames = '', + grl_pathfilenames = 'uptime/IC40_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC40.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC59 = I3Dataset( + name = 'IC59', + exp_pathfilenames = 'events/IC59_exp.csv', + grl_pathfilenames = 'uptime/IC59_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC59.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC79 = I3Dataset( + name = 'IC79', + exp_pathfilenames = 'events/IC79_exp.csv', + grl_pathfilenames = 'uptime/IC79_exp.csv', + mc_pathfilenames = None, **ds_kwargs ) + IC79.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_I = I3Dataset( + name = 'IC86_I', + exp_pathfilenames = 'events/IC86_I_exp.csv', + grl_pathfilenames = 'uptime/IC86_I_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_I.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC86_II = I3Dataset( + name = 'IC86_II', + exp_pathfilenames = 'events/IC86_II_exp.csv', + grl_pathfilenames = 'uptime/IC86_II_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_II.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC86_III = I3Dataset( + name = 'IC86_III', + exp_pathfilenames = 'events/IC86_III_exp.csv', + grl_pathfilenames = 'uptime/IC86_III_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_III.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC86_IV = I3Dataset( + name = 'IC86_IV', + exp_pathfilenames = 'events/IC86_IV_exp.csv', + grl_pathfilenames = 'uptime/IC86_IV_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_IV.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC86_V = I3Dataset( + name = 'IC86_V', + exp_pathfilenames = 'events/IC86_V_exp.csv', + grl_pathfilenames = 'uptime/IC86_V_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_V.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC86_VI = I3Dataset( + name = 'IC86_VI', + exp_pathfilenames = 'events/IC86_VI_exp.csv', + grl_pathfilenames = 'uptime/IC86_VI_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_VI.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + IC86_VII = I3Dataset( + name = 'IC86_VII', + exp_pathfilenames = 'events/IC86_VII_exp.csv', + grl_pathfilenames = 'uptime/IC86_VII_exp.csv', + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + + dsc.add_datasets(( + IC40, + IC59, + IC79, + IC86_I, + IC86_II, + IC86_III, + IC86_IV, + IC86_V, + IC86_VI, + IC86_VII + )) + + dsc.set_exp_field_name_renaming_dict({ + 'MJD[days]': 'time', + 'log10(E/GeV)': 'log_energy', + 'AngErr[deg]': 'ang_err', + 'RA[deg]': 'ra', + 'Dec[deg]': 'dec', + 'Azimuth[deg]': 'azi', + 'Zenith[deg]': 'zen' + }) + + def add_run_number(data): + exp = data.exp + exp.append_field('run', np.repeat(0, len(exp))) + def convert_deg2rad(data): + exp = data.exp + exp['ang_err'] = np.deg2rad(exp['ang_err']) + exp['ra'] = np.deg2rad(exp['ra']) + exp['dec'] = np.deg2rad(exp['dec']) + exp['azi'] = np.deg2rad(exp['azi']) + exp['zen'] = np.deg2rad(exp['zen']) + + dsc.add_data_preparation(add_run_number) + dsc.add_data_preparation(convert_deg2rad) return dsc From aa14bacc61a5f25fe6d2ec898b29ac3596fced1c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 22 Mar 2022 14:08:53 +0100 Subject: [PATCH 005/274] Added initial analysis code for trad ps analysis for public data --- skyllh/analyses/__init__.py | 0 skyllh/analyses/i3/__init__.py | 0 skyllh/analyses/i3/trad_ps/__init__.py | 0 skyllh/analyses/i3/trad_ps/analysis.py | 329 +++++++++++++++++++++++++ skyllh/datasets/i3/__init__.py | 7 + 5 files changed, 336 insertions(+) create mode 100644 skyllh/analyses/__init__.py create mode 100644 skyllh/analyses/i3/__init__.py create mode 100644 skyllh/analyses/i3/trad_ps/__init__.py create mode 100644 skyllh/analyses/i3/trad_ps/analysis.py diff --git a/skyllh/analyses/__init__.py b/skyllh/analyses/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/skyllh/analyses/i3/__init__.py b/skyllh/analyses/i3/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/skyllh/analyses/i3/trad_ps/__init__.py b/skyllh/analyses/i3/trad_ps/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py new file mode 100644 index 0000000000..0719479a78 --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -0,0 +1,329 @@ +# -*- coding: utf-8 -*- + +"""The trad_ps analysis is a multi-dataset time-integrated single source +analysis with a two-component likelihood function using a spacial and an energy +event PDF. +""" + +import argparse +import logging +import numpy as np + +from skyllh.core.progressbar import ProgressBar + +# Classes to define the source hypothesis. +from skyllh.physics.source import PointLikeSource +from skyllh.physics.flux import PowerLawFlux +from skyllh.core.source_hypo_group import SourceHypoGroup +from skyllh.core.source_hypothesis import SourceHypoGroupManager + +# Classes to define the fit parameters. +from skyllh.core.parameters import ( + SingleSourceFitParameterMapper, + FitParameter +) + +# Classes for the minimizer. +from skyllh.core.minimizer import Minimizer, LBFGSMinimizerImpl + +# Classes for utility functionality. +from skyllh.core.config import CFG +from skyllh.core.random import RandomStateService +from skyllh.core.optimize import SpatialBoxEventSelectionMethod +from skyllh.core.smoothing import BlockSmoothingFilter +from skyllh.core.timing import TimeLord +from skyllh.core.trialdata import TrialDataManager + +# Classes for defining the analysis. +from skyllh.core.test_statistic import TestStatisticWilks +from skyllh.core.analysis import ( + TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis +) + +# Classes to define the background generation. +from skyllh.core.scrambling import DataScrambler, UniformRAScramblingMethod +from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod + +# Classes to define the detector signal yield tailored to the source hypothesis. +from skyllh.i3.detsigyield import PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod + +# Classes to define the signal and background PDFs. +from skyllh.core.signalpdf import GaussianPSFPointLikeSourceSignalSpatialPDF +from skyllh.i3.signalpdf import SignalI3EnergyPDFSet +from skyllh.i3.backgroundpdf import ( + DataBackgroundI3SpatialPDF, + DataBackgroundI3EnergyPDF +) +# Classes to define the spatial and energy PDF ratios. +from skyllh.core.pdfratio import ( + SpatialSigOverBkgPDFRatio, + Skylab2SkylabPDFRatioFillMethod +) +from skyllh.i3.pdfratio import I3EnergySigSetOverBkgPDFRatioSpline + +from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod + +# Analysis utilities. +from skyllh.core.analysis_utils import ( + pointlikesource_to_data_field_array +) + +# Logging setup utilities. +from skyllh.core.debugging import ( + setup_logger, + setup_console_handler, + setup_file_handler +) + +# The pre-defined data samples. +from skyllh.datasets.i3 import data_samples + +def TXS_location(): + src_ra = np.radians(77.358) + src_dec = np.radians(5.693) + return (src_ra, src_dec) + +def create_analysis( + datasets, + source, + refplflux_Phi0=1, + refplflux_E0=1e3, + refplflux_gamma=2, + ns_seed=10.0, + gamma_seed=3, + compress_data=False, + keep_data_fields=None, + optimize_delta_angle=10, + efficiency_mode=None, + tl=None, + ppbar=None +): + """Creates the Analysis instance for this particular analysis. + + Parameters: + ----------- + datasets : list of Dataset instances + The list of Dataset instances, which should be used in the + analysis. + source : PointLikeSource instance + The PointLikeSource instance defining the point source position. + refplflux_Phi0 : float + The flux normalization to use for the reference power law flux model. + refplflux_E0 : float + The reference energy to use for the reference power law flux model. + refplflux_gamma : float + The spectral index to use for the reference power law flux model. + ns_seed : float + Value to seed the minimizer with for the ns fit. + gamma_seed : float | None + Value to seed the minimizer with for the gamma fit. If set to None, + the refplflux_gamma value will be set as gamma_seed. + compress_data : bool + Flag if the data should get converted from float64 into float32. + keep_data_fields : list of str | None + List of additional data field names that should get kept when loading + the data. + optimize_delta_angle : float + The delta angle in degrees for the event selection optimization methods. + efficiency_mode : str | None + The efficiency mode the data should get loaded with. Possible values + are: + + - 'memory': + The data will be load in a memory efficient way. This will + require more time, because all data records of a file will + be loaded sequentially. + - 'time': + The data will be loaded in a time efficient way. This will + require more memory, because each data file gets loaded in + memory at once. + + The default value is ``'time'``. If set to ``None``, the default + value will be used. + tl : TimeLord instance | None + The TimeLord instance to use to time the creation of the analysis. + ppbar : ProgressBar instance | None + The instance of ProgressBar for the optional parent progress bar. + + Returns + ------- + analysis : SpatialEnergyTimeIntegratedMultiDatasetSingleSourceAnalysis + The Analysis instance for this analysis. + """ + # Define the flux model. + fluxmodel = PowerLawFlux( + Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) + + # Define the fit parameter ns. + fitparam_ns = FitParameter('ns', 0, 1e3, ns_seed) + + # Define the gamma fit parameter. + fitparam_gamma = FitParameter('gamma', valmin=1, valmax=4, initial=gamma_seed) + + # Define the detector signal efficiency implementation method for the + # IceCube detector and this source and fluxmodel. + # The sin(dec) binning will be taken by the implementation method + # automatically from the Dataset instance. + gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) + detsigyield_implmethod = PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( + gamma_grid) + + # Define the signal generation method. + sig_gen_method = PointLikeSourceI3SignalGenerationMethod() + + # Create a source hypothesis group manager. + src_hypo_group_manager = SourceHypoGroupManager( + SourceHypoGroup( + source, fluxmodel, detsigyield_implmethod, sig_gen_method)) + + # Create a source fit parameter mapper and define the fit parameters. + src_fitparam_mapper = SingleSourceFitParameterMapper() + src_fitparam_mapper.def_fit_parameter(fitparam_gamma) + + # Define the test statistic. + test_statistic = TestStatisticWilks() + + # Define the data scrambler with its data scrambling method, which is used + # for background generation. + data_scrambler = DataScrambler(UniformRAScramblingMethod()) + + # Create background generation method. + bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) + + # Create the minimizer instance. + minimizer = Minimizer(LBFGSMinimizerImpl()) + + # Create the Analysis instance. + analysis = Analysis( + src_hypo_group_manager, + src_fitparam_mapper, + fitparam_ns, + test_statistic, + bkg_gen_method + ) + + # Define the event selection method for pure optimization purposes. + # We will use the same method for all datasets. + event_selection_method = SpatialBoxEventSelectionMethod( + src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) + + # Add the data sets to the analysis. + pbar = ProgressBar(len(datasets), parent=ppbar).start() + for ds in datasets: + # Load the data of the data set. + data = ds.load_and_prepare_data( + keep_fields=keep_data_fields, + compress=compress_data, + efficiency_mode=efficiency_mode, + tl=tl) + + # Create a trial data manager and add the required data fields. + tdm = TrialDataManager() + tdm.add_source_data_field('src_array', pointlikesource_to_data_field_array) + + sin_dec_binning = ds.get_binning_definition('sin_dec') + log_energy_binning = ds.get_binning_definition('log_energy') + + # Create the spatial PDF ratio instance for this dataset. + spatial_sigpdf = GaussianPSFPointLikeSourceSignalSpatialPDF( + dec_range=np.arcsin(sin_dec_binning.range)) + spatial_bkgpdf = DataBackgroundI3SpatialPDF( + data.exp, sin_dec_binning) + spatial_pdfratio = SpatialSigOverBkgPDFRatio( + spatial_sigpdf, spatial_bkgpdf) + + # Create the energy PDF ratio instance for this dataset. + smoothing_filter = BlockSmoothingFilter(nbins=1) + energy_sigpdfset = SignalI3EnergyPDFSet( + data.mc, log_energy_binning, sin_dec_binning, fluxmodel, gamma_grid, + smoothing_filter, ppbar=pbar) + energy_bkgpdf = DataBackgroundI3EnergyPDF( + data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) + fillmethod = Skylab2SkylabPDFRatioFillMethod() + energy_pdfratio = I3EnergySigSetOverBkgPDFRatioSpline( + energy_sigpdfset, energy_bkgpdf, + fillmethod=fillmethod, + ppbar=pbar) + + pdfratios = [ spatial_pdfratio, energy_pdfratio ] + + analysis.add_dataset( + ds, data, pdfratios, tdm, event_selection_method) + + pbar.increment() + pbar.finish() + + analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) + + analysis.construct_signal_generator() + + return analysis + +if(__name__ == '__main__'): + p = argparse.ArgumentParser( + description = "Calculates TS for a given source location using 7-year " + "point source sample and 3-year GFU sample.", + formatter_class = argparse.RawTextHelpFormatter + ) + p.add_argument("--data_base_path", default=None, type=str, + help='The base path to the data samples (default=None)' + ) + p.add_argument("--ncpu", default=1, type=int, + help='The number of CPUs to utilize where parallelization is possible.' + ) + args = p.parse_args() + + # Setup `skyllh` package logging. + # To optimize logging set the logging level to the lowest handling level. + setup_logger('skyllh', logging.DEBUG) + log_format = '%(asctime)s %(processName)s %(name)s %(levelname)s: '\ + '%(message)s' + setup_console_handler('skyllh', logging.INFO, log_format) + setup_file_handler('skyllh', logging.DEBUG, log_format, 'debug.log') + + CFG['multiproc']['ncpu'] = args.ncpu + + sample_seasons = [ + ("PointSourceTracks", "IC40"), + ("PointSourceTracks", "IC59"), + ("PointSourceTracks", "IC79"), + ("PointSourceTracks", "IC86, 2011"), + ("PointSourceTracks", "IC86, 2012-2014"), + ("GFU", "IC86, 2015-2017") + ] + + datasets = [] + for (sample, season) in sample_seasons: + # Get the dataset from the correct dataset collection. + dsc = data_samples[sample].create_dataset_collection(args.data_base_path) + datasets.append(dsc.get_dataset(season)) + + rss_seed = 1 + # Define a random state service. + rss = RandomStateService(rss_seed) + + # Define the point source. + source = PointLikeSource(*TXS_location()) + + tl = TimeLord() + + with tl.task_timer('Creating analysis.'): + ana = create_analysis( + datasets, source, compress_data=False, tl=tl) + + with tl.task_timer('Unblinding data.'): + (TS, fitparam_dict, status) = ana.unblind(rss) + + #print('log_lambda_max: %g'%(log_lambda_max)) + print('TS = %g'%(TS)) + print('ns_fit = %g'%(fitparam_dict['ns'])) + print('gamma_fit = %g'%(fitparam_dict['gamma'])) + + # Generate some signal events. + with tl.task_timer('Generating signal events.'): + (n_sig, signal_events_dict) = ana.sig_generator.generate_signal_events(rss, 100) + + print('n_sig: %d', n_sig) + print('signal datasets: '+str(signal_events_dict.keys())) + + print(tl) diff --git a/skyllh/datasets/i3/__init__.py b/skyllh/datasets/i3/__init__.py index e69de29bb2..3cd88bf4e4 100644 --- a/skyllh/datasets/i3/__init__.py +++ b/skyllh/datasets/i3/__init__.py @@ -0,0 +1,7 @@ +# -*- coding: utf-8 -*- + +from skyllh.datasets.i3 import PublicData_10y_ps + +data_samples = { + 'PublicData_10y_ps': PublicData_10y_ps +} From 4a53d7c77a2bdd325329e82215c595d53899419b Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 23 Mar 2022 12:17:11 +0100 Subject: [PATCH 006/274] Add sin_dec and energy binning information to the public dataset. --- skyllh/datasets/i3/PublicData_10y_ps.py | 96 +++++++++++++++++++++++++ 1 file changed, 96 insertions(+) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 8ef25facc0..9f0a539dec 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -261,6 +261,12 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'MJD_stop[days]': 'stop' } + # Define the datasets for the different seasons. + # For the declination and energy binning we use the same binning as was + # used in the original point-source analysis using the PointSourceTracks + # dataset. + + # ---------- IC40 ---------------------------------------------------------- IC40 = I3Dataset( name = 'IC40', exp_pathfilenames = 'events/IC40_exp.csv', @@ -270,6 +276,17 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC40.grl_field_name_renaming_dict = grl_field_name_renaming_dict + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.25, 10 + 1), + np.linspace(-0.25, 0.0, 10 + 1), + np.linspace(0.0, 1., 10 + 1), + ])) + IC40.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9. + 0.01, 0.125) + IC40.define_binning('log_energy', energy_bins) + + # ---------- IC59 ---------------------------------------------------------- IC59 = I3Dataset( name = 'IC59', exp_pathfilenames = 'events/IC59_exp.csv', @@ -279,6 +296,18 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC59.grl_field_name_renaming_dict = grl_field_name_renaming_dict + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.95, 2 + 1), + np.linspace(-0.95, -0.25, 25 + 1), + np.linspace(-0.25, 0.05, 15 + 1), + np.linspace(0.05, 1., 10 + 1), + ])) + IC59.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) + IC59.define_binning('log_energy', energy_bins) + + # ---------- IC79 ---------------------------------------------------------- IC79 = I3Dataset( name = 'IC79', exp_pathfilenames = 'events/IC79_exp.csv', @@ -288,6 +317,17 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC79.grl_field_name_renaming_dict = grl_field_name_renaming_dict + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.75, 10 + 1), + np.linspace(-0.75, 0., 15 + 1), + np.linspace(0., 1., 20 + 1) + ])) + IC79.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9. + 0.01, 0.125) + IC79.define_binning('log_energy', energy_bins) + + # ---------- IC86-I -------------------------------------------------------- IC86_I = I3Dataset( name = 'IC86_I', exp_pathfilenames = 'events/IC86_I_exp.csv', @@ -297,6 +337,19 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC86_I.grl_field_name_renaming_dict = grl_field_name_renaming_dict + b = np.sin(np.radians(-5.)) # North/South transition boundary. + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.2, 10 + 1), + np.linspace(-0.2, b, 4 + 1), + np.linspace(b, 0.2, 5 + 1), + np.linspace(0.2, 1., 10), + ])) + IC86_I.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(1., 10. + 0.01, 0.125) + IC86_I.define_binning('log_energy', energy_bins) + + # ---------- IC86-II ------------------------------------------------------- IC86_II = I3Dataset( name = 'IC86_II', exp_pathfilenames = 'events/IC86_II_exp.csv', @@ -306,6 +359,18 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC86_II.grl_field_name_renaming_dict = grl_field_name_renaming_dict + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.93, 4 + 1), + np.linspace(-0.93, -0.3, 10 + 1), + np.linspace(-0.3, 0.05, 9 + 1), + np.linspace(0.05, 1., 18 + 1), + ])) + IC86_II.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + IC86_II.define_binning('log_energy', energy_bins) + + # ---------- IC86-III ------------------------------------------------------ IC86_III = I3Dataset( name = 'IC86_III', exp_pathfilenames = 'events/IC86_III_exp.csv', @@ -315,6 +380,12 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC86_III.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_III.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_III.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-IV ------------------------------------------------------- IC86_IV = I3Dataset( name = 'IC86_IV', exp_pathfilenames = 'events/IC86_IV_exp.csv', @@ -324,6 +395,12 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC86_IV.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_IV.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_IV.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-V -------------------------------------------------------- IC86_V = I3Dataset( name = 'IC86_V', exp_pathfilenames = 'events/IC86_V_exp.csv', @@ -333,6 +410,12 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC86_V.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_V.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_V.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-VI ------------------------------------------------------- IC86_VI = I3Dataset( name = 'IC86_VI', exp_pathfilenames = 'events/IC86_VI_exp.csv', @@ -342,6 +425,12 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC86_VI.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VI.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_VI.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-VII ------------------------------------------------------ IC86_VII = I3Dataset( name = 'IC86_VII', exp_pathfilenames = 'events/IC86_VII_exp.csv', @@ -351,6 +440,13 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ) IC86_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VII.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_VII.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + #--------------------------------------------------------------------------- + dsc.add_datasets(( IC40, IC59, From 3f8599410022c907dd8a6a1ac9238ae647383bc3 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 23 Mar 2022 15:06:10 +0100 Subject: [PATCH 007/274] Fix typo --- skyllh/core/storage.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/core/storage.py b/skyllh/core/storage.py index a6d0dbb1cb..a6422bad1c 100644 --- a/skyllh/core/storage.py +++ b/skyllh/core/storage.py @@ -446,7 +446,7 @@ def header_separator(self): def header_separator(self, s): if(s is not None): if(not isinstance(s, str)): - raise TypeErr('The header_separator property must be None or ' + raise TypeError('The header_separator property must be None or ' 'of type str!') self._header_separator = s From 1fec1e6bd0b590f645005b7bc571326f9493f71f Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 23 Mar 2022 15:26:54 +0100 Subject: [PATCH 008/274] Add integral form of the power law flux --- skyllh/physics/flux.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/skyllh/physics/flux.py b/skyllh/physics/flux.py index c7ae70f8d9..2a9ebaf85f 100644 --- a/skyllh/physics/flux.py +++ b/skyllh/physics/flux.py @@ -293,6 +293,30 @@ def __call__(self, E): flux = self.Phi0 * np.power(E / self.E0, -self.gamma) return flux + def get_integral(self, E_min, E_max): + """Returns the integral value of the flux between the given energy + range. + + Parameters + ---------- + E_min : float | 1d numpy ndarray of float + The lower energy bound of the integration. + E_max : float | 1d numpy ndarray of float + The upper energy bound of the integration. + + Returns + ------- + integral : float | 1d ndarray of float + The integral value(s) of the given integral range(s). + """ + gamma = self.gamma + + integral = (self.Phi0 / ((1.-gamma)*np.power(self.E0, -gamma)) * + (np.power(E_max, 1.-gamma) - np.power(E_min, 1.-gamma))) + + return integral + + class CutoffPowerLawFlux(PowerLawFlux): """Cut-off power law flux of the form From 07a9b233070cca7f8a0b94530b05a113b113db4c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 24 Mar 2022 15:42:57 +0100 Subject: [PATCH 009/274] Added effective area data file to dataset definition --- skyllh/datasets/i3/PublicData_10y_ps.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 9f0a539dec..b6f71245ac 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -275,6 +275,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC40.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC40.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC40_effectiveArea.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.25, 10 + 1), @@ -295,6 +297,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC59.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC59.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC59_effectiveArea.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.95, 2 + 1), @@ -316,6 +320,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC79.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC79.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC79_effectiveArea.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.75, 10 + 1), @@ -336,6 +342,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC86_I.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_I.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_I_effectiveArea.csv') b = np.sin(np.radians(-5.)) # North/South transition boundary. sin_dec_bins = np.unique(np.concatenate([ @@ -358,6 +366,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC86_II.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_II.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), @@ -379,6 +389,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC86_III.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_III.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_III.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -394,6 +406,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC86_IV.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_IV.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_IV.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -409,6 +423,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC86_V.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_V.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_V.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -424,6 +440,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC86_VI.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VI.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VI.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -439,6 +457,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): **ds_kwargs ) IC86_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VII.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VII.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) From eb0154d251bcadbe6affce904fb07b6f1337c0bd Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 25 Mar 2022 15:11:51 +0100 Subject: [PATCH 010/274] Add special case for gamma=1 to the integration method --- skyllh/physics/flux.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skyllh/physics/flux.py b/skyllh/physics/flux.py index 2a9ebaf85f..74d4ecbc22 100644 --- a/skyllh/physics/flux.py +++ b/skyllh/physics/flux.py @@ -311,6 +311,12 @@ def get_integral(self, E_min, E_max): """ gamma = self.gamma + # Handle special case for gamma = 1. + if(gamma == 1): + integral = self.Phi0 * self.E0 * ( + np.log(np.abs(E_max)) - np.log(np.abs(E_min))) + return integral + integral = (self.Phi0 / ((1.-gamma)*np.power(self.E0, -gamma)) * (np.power(E_max, 1.-gamma) - np.power(E_min, 1.-gamma))) From 08c268565fda7fd08d25af7636c2d7bb9fbcb118 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 25 Mar 2022 15:13:43 +0100 Subject: [PATCH 011/274] Added detector signal yield construction class for public data --- skyllh/analyses/i3/trad_ps/analysis.py | 14 +- skyllh/analyses/i3/trad_ps/detsigyield.py | 258 ++++++++++++++++++++++ 2 files changed, 268 insertions(+), 4 deletions(-) create mode 100644 skyllh/analyses/i3/trad_ps/detsigyield.py diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index 0719479a78..d02b527049 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -45,7 +45,10 @@ from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod # Classes to define the detector signal yield tailored to the source hypothesis. -from skyllh.i3.detsigyield import PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod +from skyllh.analyses.i3.trad_ps.detsigyield import ( + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod +) + # Classes to define the signal and background PDFs. from skyllh.core.signalpdf import GaussianPSFPointLikeSourceSignalSpatialPDF @@ -78,6 +81,7 @@ # The pre-defined data samples. from skyllh.datasets.i3 import data_samples + def TXS_location(): src_ra = np.radians(77.358) src_dec = np.radians(5.693) @@ -165,8 +169,9 @@ def create_analysis( # The sin(dec) binning will be taken by the implementation method # automatically from the Dataset instance. gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) - detsigyield_implmethod = PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( - gamma_grid) + detsigyield_implmethod = \ + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( + gamma_grid) # Define the signal generation method. sig_gen_method = PointLikeSourceI3SignalGenerationMethod() @@ -219,7 +224,8 @@ def create_analysis( # Create a trial data manager and add the required data fields. tdm = TrialDataManager() - tdm.add_source_data_field('src_array', pointlikesource_to_data_field_array) + tdm.add_source_data_field('src_array', + pointlikesource_to_data_field_array) sin_dec_binning = ds.get_binning_definition('sin_dec') log_energy_binning = ds.get_binning_definition('log_energy') diff --git a/skyllh/analyses/i3/trad_ps/detsigyield.py b/skyllh/analyses/i3/trad_ps/detsigyield.py new file mode 100644 index 0000000000..5b7568960b --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/detsigyield.py @@ -0,0 +1,258 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +import scipy.interpolate + +from skyllh.core import multiproc +from skyllh.core.binning import BinningDefinition +from skyllh.core.dataset import ( + Dataset, + DatasetData +) +from skyllh.core.livetime import Livetime +from skyllh.core.parameters import ParameterGrid +from skyllh.core.detsigyield import ( + get_integrated_livetime_in_days +) +from skyllh.core.storage import ( + create_FileLoader +) +from skyllh.physics.flux import ( + PowerLawFlux, + get_conversion_factor_to_internal_flux_unit +) +from skyllh.i3.detsigyield import ( + PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod, + PowerLawFluxPointLikeSourceI3DetSigYield +) + + +class PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( + PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod, + multiproc.IsParallelizable): + """Thus detector signal yield constructor class constructs a + detector signal yield instance for a varibale power law flux model, which + has the spectral index gama as fit parameter, assuming a point-like source. + It constructs a two-dimensional spline function in sin(dec) and gamma, using + a :class:`scipy.interpolate.RectBivariateSpline`. Hence, the detector signal + yield can vary with the declination and the spectral index, gamma, of the + source. + + This detector signal yield implementation method works with a + PowerLawFlux flux model. + + It is tailored to the IceCube detector at the South Pole, where the + effective area depends soley on the zenith angle, and hence on the + declination, of the source. + + It takes the effective area for the detector signal yield from the auxilary + detector effective area data file given by the public data. + """ + def __init__( + self, gamma_grid, spline_order_sinDec=2, spline_order_gamma=2, + ncpu=None): + """Creates a new IceCube detector signal yield constructor instance for + a power law flux model. It requires the effective area from the public + data, and a gamma parameter grid to compute the gamma dependency of the + detector signal yield. + + Parameters + ---------- + gamma_grid : ParameterGrid instance + The ParameterGrid instance which defines the grid of gamma values. + spline_order_sinDec : int + The order of the spline function for the logarithmic values of the + detector signal yield along the sin(dec) axis. + The default is 2. + spline_order_gamma : int + The order of the spline function for the logarithmic values of the + detector signal yield along the gamma axis. + The default is 2. + ncpu : int | None + The number of CPUs to utilize. Global setting will take place if + not specified, i.e. set to None. + """ + super().__init__( + gamma_grid=gamma_grid, + sin_dec_binning=None, + spline_order_sinDec=spline_order_sinDec, + spline_order_gamma=spline_order_gamma, + ncpu=ncpu) + + def construct_detsigyield( + self, dataset, data, fluxmodel, livetime, ppbar=None): + """Constructs a detector signal yield 2-dimensional log spline + function for the given power law flux model with varying gamma values. + + Parameters + ---------- + dataset : Dataset instance + The Dataset instance holding the sin(dec) binning definition. + data : DatasetData instance + The DatasetData instance holding the monte-carlo event data. + This implementation loads the effective area from the provided + public data and hence does not need monte-carlo data. + fluxmodel : FluxModel + The flux model instance. Must be an instance of PowerLawFlux. + livetime : float | Livetime instance + The live-time in days or an instance of Livetime to use for the + detector signal yield. + ppbar : ProgressBar instance | None + The instance of ProgressBar of the optional parent progress bar. + + Returns + ------- + detsigyield : PowerLawFluxPointLikeSourceI3DetSigYield instance + The DetSigYield instance for a point-like source with a power law + flux with variable gamma parameter. + """ + # Check for the correct data types of the input arguments. + if(not isinstance(dataset, Dataset)): + raise TypeError('The dataset argument must be an instance of ' + 'Dataset!') + if(not isinstance(data, DatasetData)): + raise TypeError('The data argument must be an instance of ' + 'DatasetData!') + if(not self.supports_fluxmodel(fluxmodel)): + raise TypeError('The DetSigYieldImplMethod "%s" does not support ' + 'the flux model "%s"!'%( + self.__class__.__name__, + fluxmodel.__class__.__name__)) + if((not isinstance(livetime, float)) and + (not isinstance(livetime, Livetime))): + raise TypeError('The livetime argument must be an instance of ' + 'float or Livetime!') + + # Get integrated live-time in days. + livetime_days = get_integrated_livetime_in_days(livetime) + + # Calculate conversion factor from the flux model unit into the internal + # flux unit GeV^-1 cm^-2 s^-1. + toGeVcm2s = get_conversion_factor_to_internal_flux_unit(fluxmodel) + + # Load the effective area data from the public dataset. + aeff_fnames = dataset.get_abs_pathfilename_list( + dataset.get_aux_data_definition('eff_area_datafile')) + floader = create_FileLoader(aeff_fnames) + aeff_data = floader.load_data() + aeff_data.rename_fields( + { + 'log10(E_nu/GeV)_min': 'log_true_energy_nu_min', + 'log10(E_nu/GeV)_max': 'log_true_energy_nu_max', + 'Dec_nu_min[deg]': 'true_sin_dec_nu_min', + 'Dec_nu_max[deg]': 'true_sin_dec_nu_max', + 'A_Eff[cm^2]': 'a_eff' + }, + must_exist=True) + # Convert the true neutrino declination from degrees to radians and into + # sin values. + aeff_data['true_sin_dec_nu_min'] = np.sin(np.deg2rad( + aeff_data['true_sin_dec_nu_min'])) + aeff_data['true_sin_dec_nu_max'] = np.sin(np.deg2rad( + aeff_data['true_sin_dec_nu_max'])) + + # Determine the binning for energy and declination. + log_energy_bin_edges_lower = np.unique( + aeff_data['log_true_energy_nu_min']) + log_energy_bin_edges_upper = np.unique( + aeff_data['log_true_energy_nu_max']) + + sin_dec_bin_edges_lower = np.unique(aeff_data['true_sin_dec_nu_min']) + sin_dec_bin_edges_upper = np.unique(aeff_data['true_sin_dec_nu_max']) + + if(len(log_energy_bin_edges_lower) != len(log_energy_bin_edges_upper)): + raise ValueError('Cannot extract the log10(E/GeV) binning of the ' + 'effective area for dataset "{}". The number of lower and ' + 'upper bin edges is not equal!'.format(dataset.name)) + if(len(sin_dec_bin_edges_lower) != len(sin_dec_bin_edges_upper)): + raise ValueError('Cannot extract the Dec_nu binning of the ' + 'effective area for dataset "{}". The number of lower and ' + 'upper bin edges is not equal!'.format(dataset.name)) + + n_bins_log_energy = len(log_energy_bin_edges_lower) + n_bins_sin_dec = len(sin_dec_bin_edges_lower) + + # Construct the 2d array for the effective area. + aeff_arr = np.zeros((n_bins_sin_dec, n_bins_log_energy), dtype=np.float) + + sin_dec_idx = np.digitize( + 0.5*(aeff_data['true_sin_dec_nu_min'] + + aeff_data['true_sin_dec_nu_max']), + sin_dec_bin_edges_lower) - 1 + log_e_idx = np.digitize( + 0.5*(aeff_data['log_true_energy_nu_min'] + + aeff_data['log_true_energy_nu_max']), + log_energy_bin_edges_lower) - 1 + + aeff_arr[sin_dec_idx,log_e_idx] = aeff_data['a_eff'] + + # Calculate the detector signal yield in sin_dec vs gamma. + def hist( + energy_bin_edges_lower, energy_bin_edges_upper, + aeff, fluxmodel): + """Creates a histogram of the detector signal yield for the given + sin(dec) binning. + + Parameters + ---------- + energy_bin_edges_lower : 1d ndarray + The array holding the lower bin edges in E_nu/GeV. + energy_bin_edges_upper : 1d ndarray + The array holding the upper bin edges in E_nu/GeV. + aeff : (n_bins_sin_dec, n_bins_log_energy)-shaped 2d ndarray + The effective area binned data array. + + Returns + ------- + h : (n_bins_sin_dec,)-shaped 1d ndarray + The numpy array containing the detector signal yield values for + the different sin_dec bins and the given flux model. + """ + # Create histogram for the number of neutrinos with each energy + # bin. + h_phi = fluxmodel.get_integral( + energy_bin_edges_lower, energy_bin_edges_upper) + + # Sum over the enegry bins for each sin_dec row. + h = np.sum(aeff*h_phi, axis=1) + + return h + + energy_bin_edges_lower = np.power(10, log_energy_bin_edges_lower) + energy_bin_edges_upper = np.power(10, log_energy_bin_edges_upper) + + # Make a copy of the gamma grid and extend the grid by one bin on each + # side. + gamma_grid = self._gamma_grid.copy() + gamma_grid.add_extra_lower_and_upper_bin() + + # Construct the arguments for the hist function to be used in the + # multiproc.parallelize function. + args_list = [ + ((energy_bin_edges_lower, + energy_bin_edges_upper, + aeff_arr, + fluxmodel.copy({'gamma':gamma})), {}) + for gamma in gamma_grid.grid + ] + h = np.vstack( + multiproc.parallelize( + hist, args_list, self.ncpu, ppbar=ppbar)).T + h *= toGeVcm2s * livetime_days * 86400. + + # Create a 2d spline in log of the detector signal yield. + sin_dec_bincenters = 0.5*( + sin_dec_bin_edges_lower + sin_dec_bin_edges_upper) + log_spl_sinDec_gamma = scipy.interpolate.RectBivariateSpline( + sin_dec_bincenters, gamma_grid.grid, np.log(h), + kx = self.spline_order_sinDec, ky = self.spline_order_gamma, s = 0) + + # Construct the detector signal yield instance with the created spline. + sin_dec_binedges = np.concatenate( + (sin_dec_bin_edges_lower, [sin_dec_bin_edges_upper[-1]])) + sin_dec_binning = BinningDefinition('sin_dec', sin_dec_binedges) + detsigyield = PowerLawFluxPointLikeSourceI3DetSigYield( + self, dataset, fluxmodel, livetime, sin_dec_binning, log_spl_sinDec_gamma) + + return detsigyield From 8179dd2c8bb0d0af5554dc772c25e4c82b1b6c21 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 29 Mar 2022 16:29:06 +0200 Subject: [PATCH 012/274] Added smearing data file info to dataset definition --- skyllh/datasets/i3/PublicData_10y_ps.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index b6f71245ac..046d2f4aca 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -277,6 +277,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC40.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC40.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC40_effectiveArea.csv') + IC40.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC40_smearing.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.25, 10 + 1), @@ -299,6 +301,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC59.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC59.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC59_effectiveArea.csv') + IC59.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC59_smearing.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.95, 2 + 1), @@ -322,6 +326,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC79.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC79.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC79_effectiveArea.csv') + IC79.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC79_smearing.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.75, 10 + 1), @@ -344,6 +350,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_I.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_I.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC86_I_effectiveArea.csv') + IC86_I.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_I_smearing.csv') b = np.sin(np.radians(-5.)) # North/South transition boundary. sin_dec_bins = np.unique(np.concatenate([ @@ -368,6 +376,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_II.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_II.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_II.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), @@ -391,6 +401,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_III.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_III.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_III.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_III.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -408,6 +420,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_IV.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_IV.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_IV.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_IV.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -425,6 +439,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_V.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_V.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_V.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_V.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -442,6 +458,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_VI.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_VI.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_VI.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_VI.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -459,6 +477,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_VII.add_aux_data_definition( 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_VII.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_VII.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) From dc04e92cde98f864829684183b72ef78b83d096c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 29 Mar 2022 18:46:02 +0200 Subject: [PATCH 013/274] Added inverse normalized cumulative distribution function --- skyllh/physics/flux.py | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/skyllh/physics/flux.py b/skyllh/physics/flux.py index 74d4ecbc22..e8ad94c3e8 100644 --- a/skyllh/physics/flux.py +++ b/skyllh/physics/flux.py @@ -322,6 +322,41 @@ def get_integral(self, E_min, E_max): return integral + def get_inv_normed_cdf(self, x, E_min, E_max): + """Calculates the inverse cumulative distribution function value for + each given value of x, which is a number between 0 and 1. + + Parameters + ---------- + x : float | 1d numpy ndarray of float + The argument value(s) of the inverse cumulative distribution + function. Must be between 0 and 1. + E_min : float + The lower energy edge of the flux to be considered. + E_max : float + The upper energy edge of the flux to be considered. + + Returns + ------- + inv_normed_cdf : float | 1d numpy ndarray + The energy value(s) from the inverse normed cumulative distribution + function. + """ + gamma = self.gamma + + if(gamma == 1): + inv_normed_cdf = E_max * np.exp(np.log(x/E_min) / + np.log(E_max/E_min)) + return inv_normed_cdf + + + N_0 = E_max ** (1. - gamma) - E_min ** (1. - gamma) + inv_normed_cdf = np.power( + x * N_0 + E_min**(1. - gamma), + (1. / (1. - gamma))) + + return inv_normed_cdf + class CutoffPowerLawFlux(PowerLawFlux): """Cut-off power law flux of the form From a3e0f1ea3027d4ac3029077559847307c1b75185 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 29 Mar 2022 19:25:01 +0200 Subject: [PATCH 014/274] Added util function to load the smearing matrix --- skyllh/analyses/i3/trad_ps/utils.py | 117 ++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) create mode 100644 skyllh/analyses/i3/trad_ps/utils.py diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py new file mode 100644 index 0000000000..627537a286 --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -0,0 +1,117 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +from skyllh.core.storage import create_FileLoader + + +def load_smearing_histogram(pathfilenames): + """Loads the 5D smearing histogram from the given data file. + + Parameters + ---------- + pathfilenames : list of str + The file name of the data file. + + Returns + ------- + histogram : 5d ndarray + The 5d histogram array holding the probability values of the smearing + matrix. + The axes are (true_e, true_dec, reco_e, psf, ang_err). + true_e_bin_edges : 1d ndarray + The ndarray holding the bin edges of the true energy axis. + true_dec_bin_edges : 1d ndarray + The ndarray holding the bin edges of the true declination axis. + reco_e_lower_edges : 3d ndarray + The 3d ndarray holding the lower bin edges of the reco energy axis. + For each pair of true_e and true_dec different reco energy bin edges + are provided. + reco_e_upper_edges : 3d ndarray + The 3d ndarray holding the upper bin edges of the reco energy axis. + For each pair of true_e and true_dec different reco energy bin edges + are provided. + """ + # Load the smearing data from the public dataset. + loader = create_FileLoader(pathfilenames=pathfilenames) + data = loader.load_data() + # Rename the data fields. + renaming_dict = { + 'log10(E_nu/GeV)_min': 'true_e_min', + 'log10(E_nu/GeV)_max': 'true_e_max', + 'Dec_nu_min[deg]': 'true_dec_min', + 'Dec_nu_max[deg]': 'true_dec_max', + 'log10(E/GeV)_min': 'e_min', + 'log10(E/GeV)_max': 'e_max', + 'PSF_min[deg]': 'psf_min', + 'PSF_max[deg]': 'psf_max', + 'AngErr_min[deg]': 'ang_err_min', + 'AngErr_max[deg]': 'ang_err_max', + 'Fractional_Counts': 'norm_counts' + } + data.rename_fields(renaming_dict) + + def _get_nbins_from_edges(lower_edges, upper_edges): + """Helper function to extract the number of bins from the data's + bin edges. + """ + n = 0 + # Select only valid rows. + mask = upper_edges - lower_edges > 0 + data = lower_edges[mask] + # Go through the valid rows and search for the number of increasing + # bin edge values. + v0 = None + for v in data: + if(v0 is not None and v < v0): + # Reached the end of the edges block. + break + if(v0 is None or v > v0): + v0 = v + n += 1 + return n + + true_e_bin_edges = np.union1d(data['true_e_min'], data['true_e_max']) + true_dec_bin_edges = np.union1d(data['true_dec_min'], data['true_dec_max']) + + n_true_e = len(true_e_bin_edges) - 1 + n_true_dec = len(true_dec_bin_edges) - 1 + + n_reco_e = _get_nbins_from_edges( + data['e_min'], data['e_max']) + n_psf = _get_nbins_from_edges( + data['psf_min'], data['psf_max']) + n_ang_err = _get_nbins_from_edges( + data['ang_err_min'], data['ang_err_max']) + + # Get reco energy bin_edges as a 3d array. + idxs = np.array( + range(len(data)) + ) % (n_psf * n_ang_err) == 0 + + reco_e_lower_edges = np.reshape( + data['e_min'][idxs], + (n_true_e, n_true_dec, n_reco_e) + ) + reco_e_upper_edges = np.reshape( + data['e_max'][idxs], + (n_true_e, n_true_dec, n_reco_e) + ) + + # Create 5D histogram for the probabilities. + histogram = np.reshape( + data['norm_counts'], + ( + n_true_e, + n_true_dec, + n_reco_e, + n_psf, + n_ang_err + ) + ) + + return (histogram, + true_e_bin_edges, + true_dec_bin_edges, + reco_e_lower_edges, + reco_e_upper_edges) From 6c3fa8e0485197bf17870c0113e0795b4cf4ddb6 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 31 Mar 2022 23:29:42 +0200 Subject: [PATCH 015/274] Added energy pdf set --- skyllh/analyses/i3/trad_ps/signalpdf.py | 413 ++++++++++++++++++++++++ 1 file changed, 413 insertions(+) create mode 100644 skyllh/analyses/i3/trad_ps/signalpdf.py diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py new file mode 100644 index 0000000000..9f56dce2ca --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -0,0 +1,413 @@ +# -*- coding: utf-8 -*- + +import numpy as np +from copy import deepcopy +from scipy.interpolate import UnivariateSpline + +from skyllh.core.binning import ( + BinningDefinition, + UsesBinning +) +from skyllh.core.pdf import ( + PDF, + PDFSet, + IsSignalPDF, + EnergyPDF +) +from skyllh.core.multiproc import ( + IsParallelizable, + parallelize +) +from skyllh.core.parameters import ( + ParameterGrid, + ParameterGridSet +) +from skyllh.i3.dataset import I3Dataset +from skyllh.physics.flux import FluxModel +from skyllh.analyses.i3.trad_ps.utils import load_smearing_histogram + + +class PublicDataSignalI3EnergyPDF(EnergyPDF, IsSignalPDF, UsesBinning): + """Class that implements the enegry signal PDF for a given flux model given + the public data. + """ + def __init__(self, ds, flux_model, data_dict=None): + """Constructs a new enegry PDF instance using the public IceCube data. + + Parameters + ---------- + ds : instance of I3Dataset + The I3Dataset instance holding the file name of the smearing data of + the public data. + flux_model : instance of FluxModel + The flux model that should be used to calculate the energy signal + pdf. + data_dict : dict | None + If not None, the histogram data and its bin edges can be provided. + The dictionary needs the following entries: + + - 'histogram' + - 'true_e_bin_edges' + - 'true_dec_bin_edges' + - 'reco_e_lower_edges' + - 'reco_e_upper_edges' + """ + super().__init__() + + if(not isinstance(ds, I3Dataset)): + raise TypeError( + 'The ds argument must be an instance of I3Dataset!') + if(not isinstance(flux_model, FluxModel)): + raise TypeError( + 'The flux_model argument must be an instance of FluxModel!') + + self._ds = ds + self._flux_model = flux_model + + if(data_dict is None): + (self.histogram, + true_e_bin_edges, + true_dec_bin_edges, + self.reco_e_lower_edges, + self.reco_e_upper_edges + ) = load_smearing_histogram( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) + else: + self.histogram = data_dict['histogram'] + true_e_bin_edges = data_dict['true_e_bin_edges'] + true_dec_bin_edges = data_dict['true_dec_bin_edges'] + self.reco_e_lower_edges = data_dict['reco_e_lower_edges'] + self.reco_e_upper_edges = data_dict['reco_e_upper_edges'] + + # Get the number of bins for each of the variables in the matrix. + # The number of bins for e_mu, psf, and ang_err for each true_e and + # true_dec bin are equal. + self.add_binning(BinningDefinition('true_e', true_e_bin_edges)) + self.add_binning(BinningDefinition('true_dec', true_dec_bin_edges)) + + # Marginalize over the PSF and angular error axes. + self.histogram = np.sum(self.histogram, axis=(3,4)) + + # Create a (prob vs E_reco) spline for each source declination bin. + n_true_dec = len(true_dec_bin_edges) - 1 + true_e_binning = self.get_binning('true_e') + self.spline_norm_list = [] + for true_dec_idx in range(n_true_dec): + (spl, norm) = self.get_total_weighted_energy_pdf( + true_dec_idx, true_e_binning) + self.spline_norm_list.append((spl, norm)) + + @property + def ds(self): + """(read-only) The I3Dataset instance for which this enegry signal PDF + was constructed. + """ + return self._ds + + @property + def flux_model(self): + """(read-only) The FluxModel instance for which this energy signal PDF + was constructed. + """ + return self._flux_model + + def _create_spline(self, bin_centers, values, order=1, smooth=0): + """Creates a :class:`scipy.interpolate.UnivariateSpline` with the + given order and smoothing factor. + """ + spline = UnivariateSpline( + bin_centers, values, k=order, s=smooth, ext='zeros' + ) + + return spline + + def get_weighted_energy_pdf_hist_for_true_energy_dec_bin( + self, true_e_idx, true_dec_idx, flux_model, log_e_min=2): + """Gets the reconstructed muon energy pdf histogram for a specific true + neutrino energy and declination bin weighted with the assumed flux + model. + + Parameters + ---------- + true_e_idx : int + The index of the true enegry bin. + true_dec_idx : int + The index of the true declination bin. + flux_model : instance of FluxModel + The FluxModel instance that represents the flux formula. + log_e_min : float + The minimal reconstructed energy in log10 to be considered for the + PDF. + + Returns + ------- + energy_pdf_hist : 1d ndarray | None + The enegry PDF values. + None is returned if all PDF values are zero. + bin_centers : 1d ndarray | None + The bin center values for the energy PDF values. + None is returned if all PDF values are zero. + """ + # Find the index of the true neutrino energy bin and the corresponding + # distribution for the reconstructed muon energy. + energy_pdf_hist = deepcopy(self.histogram[true_e_idx, true_dec_idx]) + + # Check whether there is no pdf in the table for this neutrino energy. + if(np.sum(energy_pdf_hist) == 0): + return (None, None) + + # Get the reco energy bin centers. + lower_binedges = self.reco_e_lower_edges[true_e_idx, true_dec_idx] + upper_binedges = self.reco_e_upper_edges[true_e_idx, true_dec_idx] + bin_centers = 0.5 * (lower_binedges + upper_binedges) + + # Convolve the reco energy pdf with the flux model. + energy_pdf_hist *= flux_model.get_integral( + np.power(10, lower_binedges), np.power(10, upper_binedges) + ) + + # Find where the reconstructed energy is below the minimal energy and + # mask those values. We don't have any reco energy below the minimal + # enegry in the data. + mask = bin_centers >= log_e_min + bin_centers = bin_centers[mask] + bin_widths = upper_binedges[mask] - lower_binedges[mask] + energy_pdf_hist = energy_pdf_hist[mask] + + # Re-normalize in case some bins were cut. + energy_pdf_hist /= np.sum(energy_pdf_hist * bin_widths) + + return (energy_pdf_hist, bin_centers) + + def get_total_weighted_energy_pdf( + self, true_dec_idx, true_e_binning, log_e_min=2, order=1, smooth=0): + """Gets the reconstructed muon energy distribution weighted with the + assumed flux model and marginalized over all possible true neutrino + energies for a given true declination bin. The function generates a + spline, and calculates its integral for later normalization. + + Parameters + ---------- + true_dec_idx : int + The index of the true declination bin. + true_e_binning : instance of BinningDefinition + The BinningDefinition instance holding the true energy binning + information. + log_e_min : float + The log10 value of the minimal energy to be considered. + order : int + The order of the spline. + smooth : int + The smooth strength of the spline. + + Returns + ------- + spline : instance of scipy.interpolate.UnivariateSpline + The enegry PDF spline. + norm : float + The integral of the enegry PDF spline. + """ + # Loop over the true energy bins and for each create a spline for the + # reconstructed muon energy pdf. + splines = [] + bin_centers = [] + for true_e_idx in range(true_e_binning.nbins): + (e_pdf, e_pdf_bin_centers) =\ + self.get_weighted_energy_pdf_hist_for_true_energy_dec_bin( + true_e_idx, true_dec_idx, self.flux_model + ) + if(e_pdf is None): + continue + splines.append( + self._create_spline(e_pdf_bin_centers, e_pdf) + ) + bin_centers.append(e_pdf_bin_centers) + + # Build a (non-normalized) spline for the total reconstructed muon + # energy pdf by summing the splines corresponding to each true energy. + # Take as x values for the spline all the bin centers of the single + # reconstructed muon energy pdfs. + spline_x_vals = np.sort( + np.unique( + np.concatenate(bin_centers) + ) + ) + + spline = self._create_spline( + spline_x_vals, + np.sum([spl(spline_x_vals) for spl in splines], axis=0), + order=order, + smooth=smooth + ) + norm = spline.integral( + np.min(spline_x_vals), np.max(spline_x_vals) + ) + + return (spline, norm) + + def get_prob(self, tdm, fitparams=None, tl=None): + """Calculates the energy probability (in log10(E)) of each event. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the data events for which the + probability should be calculated for. The following data fields must + exist: + + - 'log_energy' : float + The 10-base logarithm of the energy value of the event. + - 'src_array' : (n_sources,)-shaped record array with the follwing + data fields: + + - 'dec' : float + The declination of the source. + fitparams : None + Unused interface parameter. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + prob : 1D (N_events,) shaped ndarray + The array with the energy probability for each event. + """ + get_data = tdm.get_data + + src_array = get_data('src_array') + if(len(src_array) != 1): + raise NotImplementedError( + 'The PDF class "{}" is only implemneted for a single ' + 'source! {} sources were defined!'.format( + self.__class__.name, len(src_array))) + + src_dec = get_data('src_array')['dec'][0] + true_dec_binning = self.get_binning('true_dec') + + true_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) + + log_energy = get_data('log_energy') + + (spline, norm) = self.spline_norm_list[true_dec_idx] + with TaskTimer(tl, 'Evaluating logE spline.'): + prob = spline(log_energy) / norm + + return prob + + +class PublicDataSignalI3EnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): + """This is the signal energy PDF for IceCube using public data. + It creates a set of PublicDataI3EnergyPDF objects for a discrete set of + energy signal parameters. + """ + def __init__( + self, ds, flux_model, fitparam_grid_set, ncpu=None, ppbar=None): + """ + """ + if(isinstance(fitparam_grid_set, ParameterGrid)): + fitparam_grid_set = ParameterGridSet([fitparam_grid_set]) + if(not isinstance(fitparam_grid_set, ParameterGridSet)): + raise TypeError('The fitparam_grid_set argument must be an ' + 'instance of ParameterGrid or ParameterGridSet!') + + # We need to extend the fit parameter grids on the lower and upper end + # by one bin to allow for the calculation of the interpolation. But we + # will do this on a copy of the object. + fitparam_grid_set = fitparam_grid_set.copy() + fitparam_grid_set.add_extra_lower_and_upper_bin() + + super().__init__( + pdf_type=PublicDataSignalI3EnergyPDF, + fitparams_grid_set=fitparam_grid_set, + ncpu=ncpu) + + # Load the smearing data from file. + (histogram, + true_e_bin_edges, + true_dec_bin_edges, + reco_e_lower_edges, + reco_e_upper_edges + ) = load_smearing_histogram( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) + + + def create_PublicDataSignalI3EnergyPDF( + ds, data_dict, flux_model, gridfitparams): + # Create a copy of the FluxModel with the given flux parameters. + # The copy is needed to not interfer with other CPU processes. + my_flux_model = flux_model.copy(newprop=gridfitparams) + + epdf = PublicDataSignalI3EnergyPDF( + ds, my_flux_model, data_dict=data_dict) + + return epdf + + data_dict = { + 'histogram': histogram, + 'true_e_bin_edges': true_e_bin_edges, + 'true_dec_bin_edges': true_dec_bin_edges, + 'reco_e_lower_edges': reco_e_lower_edges, + 'reco_e_upper_edges': reco_e_upper_edges + } + args_list = [ + ((ds, data_dict, flux_model, gridfitparams), {}) + for gridfitparams in self.gridfitparams_list + ] + + epdf_list = parallelize( + create_PublicDataSignalI3EnergyPDF, + args_list, + self.ncpu, + ppbar=ppbar) + + # Save all the energy PDF objects in the PDFSet PDF registry with + # the hash of the individual parameters as key. + for (gridfitparams, epdf) in zip(self.gridfitparams_list, epdf_list): + self.add_pdf(epdf, gridfitparams) + + def assert_is_valid_for_exp_data(self, data_exp): + pass + + def get_prob(self, tdm, gridfitparams): + """Calculates the signal energy probability (in logE) of each event for + a given set of signal fit parameters on a grid. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the data events for which the + probability should be calculated for. The following data fields must + exist: + + - 'log_energy' : float + The logarithm of the energy value of the event. + - 'src_array' : 1d record array + The source record array containing the declination of the + sources. + + gridfitparams : dict + The dictionary holding the signal parameter values for which the + signal energy probability should be calculated. Note, that the + parameter values must match a set of parameter grid values for which + a PublicDataSignalI3EnergyPDF object has been created at + construction time of this PublicDataSignalI3EnergyPDFSet object. + There is no interpolation method defined + at this point to allow for arbitrary parameter values! + + Returns + ------- + prob : 1d ndarray + The array with the signal energy probability for each event. + + Raises + ------ + KeyError + If no energy PDF can be found for the given signal parameter values. + """ + epdf = self.get_pdf(gridfitparams) + + prob = epdf.get_prob(tdm) + return prob From f1fb81b906b449f3c24722b8fa58b0c55ce79d24 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 1 Apr 2022 10:56:27 +0200 Subject: [PATCH 016/274] Added psf and ang_err bin edges to return of the smearing histogram loading function --- skyllh/analyses/i3/trad_ps/signalpdf.py | 7 +++- skyllh/analyses/i3/trad_ps/utils.py | 55 ++++++++++++++++++++++--- 2 files changed, 55 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 9f56dce2ca..6230c9c1db 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -328,12 +328,15 @@ def __init__( true_e_bin_edges, true_dec_bin_edges, reco_e_lower_edges, - reco_e_upper_edges + reco_e_upper_edges, + psf_lower_edges, + psf_upper_edges, + ang_err_lower_edges, + ang_err_upper_edges ) = load_smearing_histogram( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - def create_PublicDataSignalI3EnergyPDF( ds, data_dict, flux_model, gridfitparams): # Create a copy of the FluxModel with the given flux parameters. diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 627537a286..1e2873d98d 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -27,10 +27,24 @@ def load_smearing_histogram(pathfilenames): The 3d ndarray holding the lower bin edges of the reco energy axis. For each pair of true_e and true_dec different reco energy bin edges are provided. + The shape is (n_true_e, n_true_dec, n_reco_e). reco_e_upper_edges : 3d ndarray The 3d ndarray holding the upper bin edges of the reco energy axis. For each pair of true_e and true_dec different reco energy bin edges are provided. + The shape is (n_true_e, n_true_dec, n_reco_e). + psf_lower_edges : 4d ndarray + The 4d ndarray holding the lower bin edges of the PSF axis. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psf). + psf_upper_edges : 4d ndarray + The 4d ndarray holding the upper bin edges of the PSF axis. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psf). + ang_err_lower_edges : 5d ndarray + The 5d ndarray holding the lower bin edges of the angular error axis. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err). + ang_err_upper_edges : 5d ndarray + The 5d ndarray holding the upper bin edges of the angular error axis. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err). """ # Load the smearing data from the public dataset. loader = create_FileLoader(pathfilenames=pathfilenames) @@ -98,6 +112,31 @@ def _get_nbins_from_edges(lower_edges, upper_edges): (n_true_e, n_true_dec, n_reco_e) ) + # Get psf bin_edges as a 4d array. + idxs = np.array( + range(len(data)) + ) % n_ang_err == 0 + + psf_lower_edges = np.reshape( + data['psf_min'][idxs], + (n_true_e, n_true_dec, n_reco_e, n_psf) + ) + psf_upper_edges = np.reshape( + data['psf_max'][idxs], + (n_true_e, n_true_dec, n_reco_e, n_psf) + ) + + # Get angular error bin_edges as a 5d array. + ang_err_lower_edges = np.reshape( + data['ang_err_min'], + (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err) + ) + ang_err_upper_edges = np.reshape( + data['ang_err_max'], + (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err) + ) + + # Create 5D histogram for the probabilities. histogram = np.reshape( data['norm_counts'], @@ -110,8 +149,14 @@ def _get_nbins_from_edges(lower_edges, upper_edges): ) ) - return (histogram, - true_e_bin_edges, - true_dec_bin_edges, - reco_e_lower_edges, - reco_e_upper_edges) + return ( + histogram, + true_e_bin_edges, + true_dec_bin_edges, + reco_e_lower_edges, + reco_e_upper_edges, + psf_lower_edges, + psf_upper_edges, + ang_err_lower_edges, + ang_err_upper_edges + ) From aeed1cea14a3bc42075e8f545e54b03b6607009e Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Sat, 2 Apr 2022 00:05:05 +0200 Subject: [PATCH 017/274] Updated the update_version_qualifiers method --- skyllh/core/dataset.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/skyllh/core/dataset.py b/skyllh/core/dataset.py index 6b99bde3f0..65d6198572 100644 --- a/skyllh/core/dataset.py +++ b/skyllh/core/dataset.py @@ -610,16 +610,29 @@ def update_version_qualifiers(self, verqualifiers): If the integer number of an existing version qualifier is not larger than the old one. """ + got_new_verqualifiers = False + verqualifiers_keys = verqualifiers.keys() + self_verqualifiers_keys = self._verqualifiers.keys() + if(len(verqualifiers_keys) > len(self_verqualifiers_keys)): + # New version qualifiers must be a subset of the old version + # qualifiers. + for q in self_verqualifiers_keys: + if(not q in verqualifiers_keys): + raise ValueError('The version qualifier {} has been ' + 'dropped!'.format(q)) + got_new_verqualifiers = True + + existing_verqualifiers_incremented = False for q in verqualifiers: - # If the qualifier already exist, it must have a larger integer - # number. if((q in self._verqualifiers) and - (verqualifiers[q] <= self._verqualifiers[q])): - raise ValueError('The integer number (%d) of the version ' - 'qualifier "%s" is not larger than the old integer number ' - '(%d)'%(verqualifiers[q], q, self._verqualifiers[q])) + (verqualifiers[q] > self._verqualifiers[q])): + existing_verqualifiers_incremented = True self._verqualifiers[q] = verqualifiers[q] + if(not (got_new_verqualifiers or existing_verqualifiers_incremented)): + raise ValueError('Version qualifier values did not increment and ' + 'no new version qualifiers were added!') + def load_data( self, keep_fields=None, livetime=None, dtc_dict=None, dtc_except_fields=None, efficiency_mode=None, tl=None): From c78593e056818e4dcf3285828ca2058c8bcf5559 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 4 Apr 2022 17:41:39 +0200 Subject: [PATCH 018/274] Added combined dataset for IC86_II-VII --- skyllh/datasets/i3/PublicData_10y_ps.py | 36 ++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 046d2f4aca..6eee094ef2 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -485,6 +485,39 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_VII.add_binning_definition( IC86_II.get_binning_definition('log_energy')) + # ---------- IC86-II-VII --------------------------------------------------- + IC86_II_VII = I3Dataset( + name = 'IC86_II-VII', + exp_pathfilenames = [ + 'events/IC86_II_exp.csv', + 'events/IC86_III_exp.csv', + 'events/IC86_IV_exp.csv', + 'events/IC86_V_exp.csv', + 'events/IC86_VI_exp.csv', + 'events/IC86_VII_exp.csv' + ], + grl_pathfilenames = [ + 'uptime/IC86_II_exp.csv', + 'uptime/IC86_III_exp.csv', + 'uptime/IC86_IV_exp.csv', + 'uptime/IC86_V_exp.csv', + 'uptime/IC86_VI_exp.csv', + 'uptime/IC86_VII_exp.csv' + ], + mc_pathfilenames = None, + **ds_kwargs + ) + IC86_II_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_II_VII.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_II_VII.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + + IC86_II_VII.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_II_VII.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + #--------------------------------------------------------------------------- dsc.add_datasets(( @@ -497,7 +530,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_IV, IC86_V, IC86_VI, - IC86_VII + IC86_VII, + IC86_II_VII )) dsc.set_exp_field_name_renaming_dict({ From db2988c54b96318d75c13006b2c39572219df7f0 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 4 Apr 2022 17:47:46 +0200 Subject: [PATCH 019/274] Added PDF ratio class for public data energy PDF --- skyllh/analyses/i3/trad_ps/analysis.py | 64 +++-- skyllh/analyses/i3/trad_ps/pdfratio.py | 330 ++++++++++++++++++++++++ skyllh/analyses/i3/trad_ps/signalpdf.py | 16 +- 3 files changed, 378 insertions(+), 32 deletions(-) create mode 100644 skyllh/analyses/i3/trad_ps/pdfratio.py diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index d02b527049..915f3924d0 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -44,12 +44,6 @@ from skyllh.core.scrambling import DataScrambler, UniformRAScramblingMethod from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod -# Classes to define the detector signal yield tailored to the source hypothesis. -from skyllh.analyses.i3.trad_ps.detsigyield import ( - PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod -) - - # Classes to define the signal and background PDFs. from skyllh.core.signalpdf import GaussianPSFPointLikeSourceSignalSpatialPDF from skyllh.i3.signalpdf import SignalI3EnergyPDFSet @@ -62,7 +56,6 @@ SpatialSigOverBkgPDFRatio, Skylab2SkylabPDFRatioFillMethod ) -from skyllh.i3.pdfratio import I3EnergySigSetOverBkgPDFRatioSpline from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod @@ -78,9 +71,19 @@ setup_file_handler ) -# The pre-defined data samples. +# Pre-defined IceCube data samples. from skyllh.datasets.i3 import data_samples +# Analysis specific classes for working with the public data. +from skyllh.analyses.i3.trad_ps.detsigyield import ( + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod +) +from skyllh.analyses.i3.trad_ps.signalpdf import ( + PublicDataSignalI3EnergyPDFSet +) +from skyllh.analyses.i3.trad_ps.pdfratio import ( + PublicDataI3EnergySigSetOverBkgPDFRatioSpline +) def TXS_location(): src_ra = np.radians(77.358) @@ -174,7 +177,8 @@ def create_analysis( gamma_grid) # Define the signal generation method. - sig_gen_method = PointLikeSourceI3SignalGenerationMethod() + #sig_gen_method = PointLikeSourceI3SignalGenerationMethod() + sig_gen_method = None # Create a source hypothesis group manager. src_hypo_group_manager = SourceHypoGroupManager( @@ -240,14 +244,15 @@ def create_analysis( # Create the energy PDF ratio instance for this dataset. smoothing_filter = BlockSmoothingFilter(nbins=1) - energy_sigpdfset = SignalI3EnergyPDFSet( - data.mc, log_energy_binning, sin_dec_binning, fluxmodel, gamma_grid, - smoothing_filter, ppbar=pbar) + + energy_sigpdfset = PublicDataSignalI3EnergyPDFSet( + ds, fluxmodel, gamma_grid, ppbar=pbar) energy_bkgpdf = DataBackgroundI3EnergyPDF( data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) fillmethod = Skylab2SkylabPDFRatioFillMethod() - energy_pdfratio = I3EnergySigSetOverBkgPDFRatioSpline( - energy_sigpdfset, energy_bkgpdf, + energy_pdfratio = PublicDataI3EnergySigSetOverBkgPDFRatioSpline( + energy_sigpdfset, + energy_bkgpdf, fillmethod=fillmethod, ppbar=pbar) @@ -261,17 +266,17 @@ def create_analysis( analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) - analysis.construct_signal_generator() + #analysis.construct_signal_generator() return analysis if(__name__ == '__main__'): p = argparse.ArgumentParser( - description = "Calculates TS for a given source location using 7-year " - "point source sample and 3-year GFU sample.", + description = 'Calculates TS for a given source location using the ' + '10-year public point source sample.', formatter_class = argparse.RawTextHelpFormatter ) - p.add_argument("--data_base_path", default=None, type=str, + p.add_argument('--data_base_path', default=None, type=str, help='The base path to the data samples (default=None)' ) p.add_argument("--ncpu", default=1, type=int, @@ -285,17 +290,18 @@ def create_analysis( log_format = '%(asctime)s %(processName)s %(name)s %(levelname)s: '\ '%(message)s' setup_console_handler('skyllh', logging.INFO, log_format) - setup_file_handler('skyllh', logging.DEBUG, log_format, 'debug.log') + setup_file_handler('skyllh', 'debug.log', + log_level=logging.DEBUG, + log_format=log_format) CFG['multiproc']['ncpu'] = args.ncpu sample_seasons = [ - ("PointSourceTracks", "IC40"), - ("PointSourceTracks", "IC59"), - ("PointSourceTracks", "IC79"), - ("PointSourceTracks", "IC86, 2011"), - ("PointSourceTracks", "IC86, 2012-2014"), - ("GFU", "IC86, 2015-2017") + ('PublicData_10y_ps', 'IC40'), + ('PublicData_10y_ps', 'IC59'), + ('PublicData_10y_ps', 'IC79'), + ('PublicData_10y_ps', 'IC86_I'), + ('PublicData_10y_ps', 'IC86_II-VII') ] datasets = [] @@ -315,21 +321,23 @@ def create_analysis( with tl.task_timer('Creating analysis.'): ana = create_analysis( - datasets, source, compress_data=False, tl=tl) + datasets, source, tl=tl) with tl.task_timer('Unblinding data.'): (TS, fitparam_dict, status) = ana.unblind(rss) - #print('log_lambda_max: %g'%(log_lambda_max)) print('TS = %g'%(TS)) print('ns_fit = %g'%(fitparam_dict['ns'])) print('gamma_fit = %g'%(fitparam_dict['gamma'])) + """ # Generate some signal events. with tl.task_timer('Generating signal events.'): - (n_sig, signal_events_dict) = ana.sig_generator.generate_signal_events(rss, 100) + (n_sig, signal_events_dict) =\ + ana.sig_generator.generate_signal_events(rss, 100) print('n_sig: %d', n_sig) print('signal datasets: '+str(signal_events_dict.keys())) + """ print(tl) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py new file mode 100644 index 0000000000..ee210d2c78 --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- + +from scipy.interpolate import RegularGridInterpolator + +from skyllh.core.pdf import EnergyPDF +from skyllh.core.pdfratio import ( + SigSetOverBkgPDFRatio, + PDFRatioFillMethod, + MostSignalLikePDFRatioFillMethod +) +from skyllh.core.multiproc import IsParallelizable + +from skyllh.analyses.i3.trad_ps.signalpdf import PublicDataSignalI3EnergyPDFSet + +class PublicDataI3EnergySigSetOverBkgPDFRatioSpline( + SigSetOverBkgPDFRatio, + IsParallelizable): + """This class implements a signal over background PDF ratio spline for a + signal PDF that is derived from PublicDataSignalI3EnergyPDFSet and a + background PDF that is derived from I3EnergyPDF. It creates a spline for the + ratio of the signal and background PDFs for a grid of different discrete + energy signal fit parameters, which are defined by the signal PDF set. + """ + def __init__( + self, signalpdfset, backgroundpdf, + fillmethod=None, interpolmethod=None, ncpu=None, ppbar=None): + """Creates a new IceCube signal-over-background energy PDF ratio object + specialized for the public data. + + Paramerers + ---------- + signalpdfset : class instance derived from PDFSet (for PDF type + EnergyPDF), IsSignalPDF, and UsesBinning + The PDF set, which provides signal energy PDFs for a set of + discrete signal parameters. + backgroundpdf : class instance derived from EnergyPDF, and + IsBackgroundPDF + The background energy PDF object. + fillmethod : instance of PDFRatioFillMethod | None + An instance of class derived from PDFRatioFillMethod that implements + the desired ratio fill method. + If set to None (default), the default ratio fill method + MostSignalLikePDFRatioFillMethod will be used. + interpolmethod : class of GridManifoldInterpolationMethod + The class implementing the fit parameter interpolation method for + the PDF ratio manifold grid. + ncpu : int | None + The number of CPUs to use to create the ratio splines for the + different sets of signal parameters. + ppbar : ProgressBar instance | None + The instance of ProgressBar of the optional parent progress bar. + """ + super().__init__( + pdf_type=EnergyPDF, + signalpdfset=signalpdfset, backgroundpdf=backgroundpdf, + interpolmethod=interpolmethod, + ncpu=ncpu) + + # Define the default ratio fill method. + if(fillmethod is None): + fillmethod = MostSignalLikePDFRatioFillMethod() + self.fillmethod = fillmethod + + def create_log_ratio_spline( + sigpdfset, bkgpdf, fillmethod, gridfitparams, src_dec_idx): + """Creates the signal/background ratio 2d spline for the given + signal parameters. + + Returns + ------- + log_ratio_spline : RegularGridInterpolator + The spline of the logarithmic PDF ratio values in the + (log10(E_reco),sin(dec_reco)) space. + """ + # Get the signal PDF for the given signal parameters. + sigpdf = sigpdfset.get_pdf(gridfitparams) + + bkg_log_e_bincenters = bkgpdf.get_binning('log_energy').bincenters + sigpdf_hist = sigpdf.calc_prob_for_true_dec_idx( + src_dec_idx, bkg_log_e_bincenters) + # Transform the (log10(E_reco),)-shaped 1d array into the + # (log10(E_reco),sin(dec_reco))-shaped 2d array. + sigpdf_hist = np.repeat( + [sigpdf_hist], bkgpdf.hist.shape[1], axis=0).T + + sig_mask_mc_covered = np.ones_like(sigpdf_hist, dtype=np.bool) + bkg_mask_mc_covered = np.ones_like(bkgpdf.hist, dtype=np.bool) + sig_mask_mc_covered_zero_physics = sigpdf_hist == 0 + bkg_mask_mc_covered_zero_physics = bkgpdf.hist == 0 + + # Create the ratio array with the same shape than the background pdf + # histogram. + ratio = np.ones_like(bkgpdf.hist, dtype=np.float) + + # Fill the ratio array. + ratio = fillmethod.fill_ratios(ratio, + sigpdf_hist, bkgpdf.hist, + sig_mask_mc_covered, + sig_mask_mc_covered_zero_physics, + bkg_mask_mc_covered, + bkg_mask_mc_covered_zero_physics) + + # Define the grid points for the spline. In general, we use the bin + # centers of the binning, but for the first and last point of each + # dimension we use the lower and upper bin edge, respectively, to + # ensure full coverage of the spline across the binning range. + points_list = [] + for binning in bkgpdf.binnings: + points = binning.bincenters + (points[0], points[-1]) = (binning.lower_edge, binning.upper_edge) + points_list.append(points) + + # Create the spline for the ratio values. + log_ratio_spline = RegularGridInterpolator( + tuple(points_list), + np.log(ratio), + method='linear', + bounds_error=False, + fill_value=0.) + + return log_ratio_spline + + # Get the list of fit parameter permutations on the grid for which we + # need to create PDF ratio arrays. + gridfitparams_list = signalpdfset.gridfitparams_list + + self._gridfitparams_hash_log_ratio_spline_dict_list = [] + for src_dec_idx in range(sigpdfset.true_dec_binning.nbins): + args_list = [ + ( + ( + signalpdfset, + backgroundpdf, + fillmethod, + gridfitparams, + src_dec_idx + ), + {} + ) + for gridfitparams in gridfitparams_list + ] + + log_ratio_spline_list = parallelize( + create_log_ratio_spline, args_list, self.ncpu, ppbar=ppbar) + + # Save all the log_ratio splines in a dictionary. + gridfitparams_hash_log_ratio_spline_dict = dict() + for (gridfitparams, log_ratio_spline) in zip( + gridfitparams_list, log_ratio_spline_list): + gridfitparams_hash = make_params_hash(gridfitparams) + gridfitparams_hash_log_ratio_spline_dict[ + gridfitparams_hash] = log_ratio_spline + self._gridfitparams_hash_log_ratio_spline_dict_list.append( + gridfitparams_hash_log_ratio_spline_dict) + + # Save the list of data field names. + self._data_field_names = [ + binning.name + for binning in self.backgroundpdf.binnings + ] + + # Construct the instance for the fit parameter interpolation method. + self._interpolmethod_instance = self.interpolmethod( + self._get_spline_value, + signalpdfset.fitparams_grid_set) + + # Create cache variables for the last ratio value and gradients in order + # to avoid the recalculation of the ratio value when the + # ``get_gradient`` method is called (usually after the ``get_ratio`` + # method was called). + self._cache_src_dec_idx = None + self._cache_fitparams_hash = None + self._cache_ratio = None + self._cache_gradients = None + + @property + def fillmethod(self): + """The PDFRatioFillMethod object, which should be used for filling the + PDF ratio bins. + """ + return self._fillmethod + @fillmethod.setter + def fillmethod(self, obj): + if(not isinstance(obj, PDFRatioFillMethod)): + raise TypeError('The fillmethod property must be an instance of ' + 'PDFRatioFillMethod!') + self._fillmethod = obj + + def _get_spline_value(self, tdm, gridfitparams, eventdata): + """Selects the spline object for the given fit parameter grid point and + evaluates the spline for all the given events. + """ + if(self._cache_src_dec_idx is None): + raise RuntimeError('There was no source declination bin index ' + 'pre-calculated!') + + # Get the spline object for the given fit parameter grid values. + gridfitparams_hash = make_params_hash(gridfitparams) + spline = self._gridfitparams_hash_log_ratio_spline_dict_list\ + [self._cache_src_dec_idx][gridfitparams_hash] + + # Evaluate the spline. + value = spline(eventdata) + + return value + + def _get_src_dec_idx_from_source_array(self, src_array): + """Determines the source declination index given the source array from + the trial data manager. For now only a single source is supported! + """ + if(len(src_array) != 1): + raise NotImplementedError( + 'The PDFRatio class "{}" is only implemneted for a single ' + 'source! But {} sources were defined!'.format( + self.__class__.name, len(src_array))) + src_dec = get_data('src_array')['dec'][0] + true_dec_binning = self.signalpdfset.true_dec_binning + src_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) + + return src_dec_idx + + def _is_cached(self, tdm, src_dec_idx, fitparams_hash): + """Checks if the ratio and gradients for the given set of fit parameters + are already cached. + """ + if((self._cache_src_dec_idx == src_dec_idx) and + (self._cache_fitparams_hash == fitparams_hash) and + (len(self._cache_ratio) == tdm.n_selected_events) + ): + return True + return False + + def _calculate_ratio_and_gradients( + self, tdm, src_dec_idx, fitparams, fitparams_hash): + """Calculates the ratio values and ratio gradients for all the events + given the fit parameters. It caches the results. + """ + get_data = tdm.get_data + + # The _get_spline_value method needs the cache source dec index for the + # current evaluation of the PDF ratio. + self._cache_src_dec_idx = src_dec_idx + + # Create a 2D event data array holding only the needed event data fields + # for the PDF ratio spline evaluation. + eventdata = np.vstack([get_data(fn) for fn in self._data_field_names]).T + + (ratio, gradients) = self._interpolmethod_instance.get_value_and_gradients( + tdm, eventdata, fitparams) + # The interpolation works on the logarithm of the ratio spline, hence + # we need to transform it using the exp function, and we need to account + # for the exp function in the gradients. + ratio = np.exp(ratio) + gradients = ratio * gradients + + # Cache the value and the gradients. + self._cache_fitparams_hash = fitparams_hash + self._cache_ratio = ratio + self._cache_gradients = gradients + + def get_ratio(self, tdm, fitparams, tl=None): + """Retrieves the PDF ratio values for each given trial event data, given + the given set of fit parameters. This method is called during the + likelihood maximization process. + For computational efficiency reasons, the gradients are calculated as + well and will be cached. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the trial event data for which + the PDF ratio values should get calculated. + fitparams : dict + The dictionary with the fit parameter values. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + ratio : 1d ndarray of float + The PDF ratio value for each given event. + """ + fitparams_hash = make_params_hash(fitparams) + + # Determine the source declination bin index. + src_array = get_data('src_array') + src_dec_idx = self._get_src_dec_idx_from_source_array(src_array) + + # Check if the ratio value is already cached. + if(self._is_cached(tdm, src_dec_idx, fitparams_hash)): + return self._cache_ratio + + self._calculate_ratio_and_gradients( + tdm, src_dec_idx, fitparams, fitparams_hash) + + return self._cache_ratio + + def get_gradient(self, tdm, fitparams, fitparam_name): + """Retrieves the PDF ratio gradient for the pidx'th fit parameter. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the trial event data for which + the PDF ratio gradient values should get calculated. + fitparams : dict + The dictionary with the fit parameter values. + fitparam_name : str + The name of the fit parameter for which the gradient should get + calculated. + """ + fitparams_hash = make_params_hash(fitparams) + + # Convert the fit parameter name into the local fit parameter index. + pidx = self.convert_signal_fitparam_name_into_index(fitparam_name) + + # Determine the source declination bin index. + src_array = get_data('src_array') + src_dec_idx = self._get_src_dec_idx_from_source_array(src_array) + + # Check if the gradients have been calculated already. + if(self._is_cached(tdm, src_dec_idx, fitparams_hash)): + return self._cache_gradients[pidx] + + # The gradients have not been calculated yet. + self._calculate_ratio_and_gradients( + tdm, src_dec_idx, fitparams, fitparams_hash) + + return self._cache_gradients[pidx] diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 6230c9c1db..4e2d996907 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -246,6 +246,15 @@ def get_total_weighted_energy_pdf( return (spline, norm) + def calc_prob_for_true_dec_idx(self, true_dec_idx, log_energy, tl=None): + """Calculates the PDF value for the given true declination bin and the + given log10(E_reco) energy values. + """ + (spline, norm) = self.spline_norm_list[true_dec_idx] + with TaskTimer(tl, 'Evaluating logE spline.'): + prob = spline(log_energy) / norm + return prob + def get_prob(self, tdm, fitparams=None, tl=None): """Calculates the energy probability (in log10(E)) of each event. @@ -285,14 +294,11 @@ def get_prob(self, tdm, fitparams=None, tl=None): src_dec = get_data('src_array')['dec'][0] true_dec_binning = self.get_binning('true_dec') - true_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) log_energy = get_data('log_energy') - (spline, norm) = self.spline_norm_list[true_dec_idx] - with TaskTimer(tl, 'Evaluating logE spline.'): - prob = spline(log_energy) / norm + prob = self.calc_prob_for_true_dec_idx(true_dec_idx, log_energy, tl=tl) return prob @@ -337,6 +343,8 @@ def __init__( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) + self.true_dec_binning = BinningDefinition(true_dec_bin_edges) + def create_PublicDataSignalI3EnergyPDF( ds, data_dict, flux_model, gridfitparams): # Create a copy of the FluxModel with the given flux parameters. From 14db75d255270b382046310223afef311c1289aa Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 5 Apr 2022 15:08:49 +0200 Subject: [PATCH 020/274] minor typo bugfixes --- skyllh/analyses/i3/trad_ps/pdfratio.py | 19 +++++++++++++------ skyllh/analyses/i3/trad_ps/signalpdf.py | 4 +++- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index ee210d2c78..4dd9349fba 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -1,17 +1,23 @@ # -*- coding: utf-8 -*- +import numpy as np from scipy.interpolate import RegularGridInterpolator +from skyllh.core.parameters import make_params_hash from skyllh.core.pdf import EnergyPDF from skyllh.core.pdfratio import ( SigSetOverBkgPDFRatio, PDFRatioFillMethod, MostSignalLikePDFRatioFillMethod ) -from skyllh.core.multiproc import IsParallelizable +from skyllh.core.multiproc import ( + IsParallelizable, + parallelize +) from skyllh.analyses.i3.trad_ps.signalpdf import PublicDataSignalI3EnergyPDFSet + class PublicDataI3EnergySigSetOverBkgPDFRatioSpline( SigSetOverBkgPDFRatio, IsParallelizable): @@ -86,7 +92,8 @@ def create_log_ratio_spline( sig_mask_mc_covered = np.ones_like(sigpdf_hist, dtype=np.bool) bkg_mask_mc_covered = np.ones_like(bkgpdf.hist, dtype=np.bool) sig_mask_mc_covered_zero_physics = sigpdf_hist == 0 - bkg_mask_mc_covered_zero_physics = bkgpdf.hist == 0 + bkg_mask_mc_covered_zero_physics = np.zeros_like( + bkgpdf.hist, dtype=np.bool) # Create the ratio array with the same shape than the background pdf # histogram. @@ -125,7 +132,7 @@ def create_log_ratio_spline( gridfitparams_list = signalpdfset.gridfitparams_list self._gridfitparams_hash_log_ratio_spline_dict_list = [] - for src_dec_idx in range(sigpdfset.true_dec_binning.nbins): + for src_dec_idx in range(signalpdfset.true_dec_binning.nbins): args_list = [ ( ( @@ -213,7 +220,7 @@ def _get_src_dec_idx_from_source_array(self, src_array): 'The PDFRatio class "{}" is only implemneted for a single ' 'source! But {} sources were defined!'.format( self.__class__.name, len(src_array))) - src_dec = get_data('src_array')['dec'][0] + src_dec = src_array['dec'][0] true_dec_binning = self.signalpdfset.true_dec_binning src_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) @@ -284,7 +291,7 @@ def get_ratio(self, tdm, fitparams, tl=None): fitparams_hash = make_params_hash(fitparams) # Determine the source declination bin index. - src_array = get_data('src_array') + src_array = tdm.get_data('src_array') src_dec_idx = self._get_src_dec_idx_from_source_array(src_array) # Check if the ratio value is already cached. @@ -316,7 +323,7 @@ def get_gradient(self, tdm, fitparams, fitparam_name): pidx = self.convert_signal_fitparam_name_into_index(fitparam_name) # Determine the source declination bin index. - src_array = get_data('src_array') + src_array = tdm.get_data('src_array') src_dec_idx = self._get_src_dec_idx_from_source_array(src_array) # Check if the gradients have been calculated already. diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 4e2d996907..4e46c38787 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -4,6 +4,7 @@ from copy import deepcopy from scipy.interpolate import UnivariateSpline +from skyllh.core.timing import TaskTimer from skyllh.core.binning import ( BinningDefinition, UsesBinning @@ -343,7 +344,8 @@ def __init__( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - self.true_dec_binning = BinningDefinition(true_dec_bin_edges) + self.true_dec_binning = BinningDefinition( + 'true_dec', true_dec_bin_edges) def create_PublicDataSignalI3EnergyPDF( ds, data_dict, flux_model, gridfitparams): From 6e2d0a942f4b3b89216fed49a97684f21bd726ba Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 11 Apr 2022 19:25:21 +0200 Subject: [PATCH 021/274] Added signal generator class for prove of concept --- .../analyses/i3/trad_ps/signal_generator.py | 560 ++++++++++++++++++ 1 file changed, 560 insertions(+) create mode 100644 skyllh/analyses/i3/trad_ps/signal_generator.py diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py new file mode 100644 index 0000000000..c918011c99 --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -0,0 +1,560 @@ +import numpy as np +from copy import deepcopy +import os.path +from matplotlib import pyplot + +from skyllh.core.storage import TextFileLoader +from skyllh.physics.flux import FluxModel +from skyllh.analyses.i3.trad_ps.utils import load_smearing_histogram +from skyllh.core.random import RandomStateService + + +class signal_injector(object): + r""" + """ + + def __init__( + self, + name: str, + declination: float, + right_ascension: float, + flux_model: FluxModel, + data_path="/home/mwolf/projects/publicdata_ps/icecube_10year_ps/irfs" + ): + r""" + Parameters + ---------- + - name : str + Dataset identifier. Must be one among: + ['IC40', 'IC59', 'IC79', 'IC86_I', 'IC86_II']. + + - declination : float + Source declination in degrees. + + - flux_model : FluxModel + Instance of the `FluxModel` class. + """ + + self.flux_model = flux_model + self.dec = declination + self.ra = right_ascension + + ( + self.histogram, + self.true_e_bin_edges, + self.true_dec_bin_edges, + self.reco_e_lower_edges, + self.reco_e_upper_edges, + self.psf_lower_edges, + self.psf_upper_edges, + self.ang_err_lower_edges, + self.ang_err_upper_edges + ) = load_smearing_histogram(os.path.join(data_path, f"{name}_smearing.csv")) + + # Find the declination bin + if(self.dec < self.true_dec_bin_edges[0] or self.dec > self.true_dec_bin_edges[-1]): + raise ValueError("NotImplemented") + self.dec_idx = np.digitize(self.dec, self.true_dec_bin_edges) - 1 + + + @staticmethod + def _get_bin_centers(low_edges, high_edges): + r"""Given an array of lower bin edges and an array of upper bin edges, + returns the corresponding bin centers. + """ + # bin_edges = np.union1d(low_edges, high_edges) + # bin_centers = 0.5 * (bin_edges[1:] + bin_edges[:-1]) + bin_centers = 0.5 * (low_edges + high_edges) + return bin_centers + + + def get_weighted_marginalized_pdf( + self, true_e_idx, reco_e_idx=None, psf_idx=None + ): + r"""Get the reconstructed muon energy pdf for a specific true neutrino + energy weighted with the assumed flux model. + The function returns both the bin center values and the pdf values, + which might be useful for plotting. + If no pdf is given for the assumed true neutrino energy, returns None. + """ + + if reco_e_idx is None: + # Get the marginalized distribution of the reconstructed energy + # for a given (true energy, true declination) bin. + pdf = deepcopy(self.histogram[true_e_idx, self.dec_idx, :]) + pdf = np.sum(pdf, axis=(-2,-1)) + label = "reco_e" + elif psf_idx is None: + # Get the marginalized distribution of the neutrino-muon opening + # angle for a given (true energy, true declination, reco energy) + # bin. + pdf = deepcopy( + self.histogram[true_e_idx, self.dec_idx, reco_e_idx, :] + ) + pdf = np.sum(pdf, axis=-1) + label = "psf" + else: + # Get the marginalized distribution of the neutrino-muon opening + # angle for a given + # (true energy, true declination, reco energy, psi) bin. + pdf = deepcopy( + self.histogram[true_e_idx, self.dec_idx, reco_e_idx, psf_idx, :] + ) + label = "ang_err" + + # Check whether there is no pdf in the table for this neutrino energy. + if np.sum(pdf) == 0: + return None, None, None, None, None + + if label == "reco_e": + # Get the reco energy bin centers. + lower_bin_edges = ( + self.reco_e_lower_edges[true_e_idx, self.dec_idx, :] + ) + upper_bin_edges = ( + self.reco_e_upper_edges[true_e_idx, self.dec_idx, :] + ) + bin_centers = self._get_bin_centers( + lower_bin_edges, upper_bin_edges + ) + + # pdf *= self.flux_model.get_integral( + # 10**lower_bin_edges, 10**upper_bin_edges + # ) + + # Find where the reconstructed energy is below 100GeV and mask those + # values. We don't have any reco energy below 100GeV in the data. + # mask = bin_centers >= 2 + # lower_bin_edges = lower_bin_edges[mask] + # upper_bin_edges = upper_bin_edges[mask] + # pdf = pdf[mask] + + elif label == "psf": + lower_bin_edges = ( + self.psf_lower_edges[true_e_idx, self.dec_idx, reco_e_idx, :] + ) + upper_bin_edges = ( + self.psf_upper_edges[true_e_idx, self.dec_idx, reco_e_idx, :] + ) + + elif label == "ang_err": + lower_bin_edges = ( + self.ang_err_lower_edges[ + true_e_idx, self.dec_idx, reco_e_idx, psf_idx, : + ] + ) + upper_bin_edges = ( + self.ang_err_upper_edges[ + true_e_idx, self.dec_idx, reco_e_idx, psf_idx, : + ] + ) + + bin_centers = self._get_bin_centers( + lower_bin_edges, upper_bin_edges + ) + bin_width = upper_bin_edges - lower_bin_edges + + # Re-normalize in case some bins were cut. + pdf /= (np.sum(pdf * bin_width)) + + return lower_bin_edges, upper_bin_edges, bin_centers, bin_width, pdf + + + def _get_reconstruction_from_histogram( + self, rs, idxs, value=None, bin_centers=None + ): + if value is not None: + if bin_centers is None: + raise RuntimeError("NotImplemented.") + value_idx = np.argmin(abs(value - bin_centers)) + idxs[idxs.index(None)] = value_idx + + (low_edges, up_edges, new_bin_centers, bin_width, hist) = ( + self.get_weighted_marginalized_pdf(idxs[0],idxs[1],idxs[2]) + ) + if low_edges is None: + return None, None, None, None + reco_bin = rs.choice(new_bin_centers, p=(hist * bin_width)) + reco_idx = np.argmin(abs(reco_bin - new_bin_centers)) + reco_value = np.random.uniform(low_edges[reco_idx], up_edges[reco_idx]) + + return reco_value, reco_bin, new_bin_centers, idxs + + + def circle_parametrization(self, rs, psf): + psf = np.atleast_1d(psf) + # Transform everything in radians and convert the source declination + # to source zenith angle + a = np.radians(psf) + b = np.radians(90 - self.dec) + c = np.radians(self.ra) + + # Random rotation angle for the 2D circle + t = rs.uniform(0, 2*np.pi, size=len(psf)) + + # Parametrize the circle + x = ( + (np.sin(a)*np.cos(b)*np.cos(c)) * np.cos(t) + \ + (np.sin(a)*np.sin(c)) * np.sin(t) - \ + (np.cos(a)*np.sin(b)*np.cos(c)) + ) + y = ( + -(np.sin(a)*np.cos(b)*np.sin(c)) * np.cos(t) + \ + (np.sin(a)*np.cos(c)) * np.sin(t) + \ + (np.cos(a)*np.sin(b)*np.sin(c)) + ) + z = ( + (np.sin(a)*np.sin(b)) * np.cos(t) + \ + (np.cos(a)*np.cos(b)) + ) + + # Convert back to right ascension and declination + # This is to distinguish between diametrically opposite directions. + zen = np.arccos(z) + azi = np.arctan2(y,x) + + return (np.degrees(np.pi - azi), np.degrees(np.pi/2 - zen)) + + def get_log_e_pdf(self, log_true_e_idx): + if log_true_e_idx == -1: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, self.dec_idx] + pdf = np.sum(pdf, axis=(-2,-1)) + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the reco energy bin edges and widths. + lower_bin_edges = self.reco_e_lower_edges[ + log_true_e_idx, self.dec_idx + ] + upper_bin_edges = self.reco_e_upper_edges[ + log_true_e_idx, self.dec_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf /= (np.sum(pdf * bin_widths)) + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def get_psi_pdf(self, log_true_e_idx, log_e_idx): + if log_true_e_idx == -1 or log_e_idx == -1: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, self.dec_idx, log_e_idx] + pdf = np.sum(pdf, axis=-1) + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the PSI bin edges and widths. + lower_bin_edges = self.psf_lower_edges[ + log_true_e_idx, self.dec_idx, log_e_idx + ] + upper_bin_edges = self.psf_upper_edges[ + log_true_e_idx, self.dec_idx, log_e_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf /= (np.sum(pdf * bin_widths)) + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def get_ang_err_pdf(self, log_true_e_idx, log_e_idx, psi_idx): + if log_true_e_idx == -1 or log_e_idx == -1 or psi_idx == -1: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, self.dec_idx, log_e_idx, psi_idx] + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the ang_err bin edges and widths. + lower_bin_edges = self.ang_err_lower_edges[ + log_true_e_idx, self.dec_idx, log_e_idx, psi_idx + ] + upper_bin_edges = self.ang_err_upper_edges[ + log_true_e_idx, self.dec_idx, log_e_idx, psi_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf = pdf / np.sum(pdf * bin_widths) + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def get_log_e_from_log_true_e_idxs(self, rs, log_true_e_idxs): + n_evt = len(log_true_e_idxs) + log_e_idx = np.empty((n_evt,), dtype=int) + log_e = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + b_size = np.count_nonzero(m) + (pdf, low_bin_edges, up_bin_edges, bin_widths) = self.get_log_e_pdf(b_log_true_e_idx) + if pdf is None: + log_e_idx[m] = -1 + log_e[m] = np.nan + continue + + b_log_e_idx = rs.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=b_size) + b_log_e = rs.uniform( + low_bin_edges[b_log_e_idx], + up_bin_edges[b_log_e_idx], + size=b_size) + + log_e_idx[m] = b_log_e_idx + log_e[m] = b_log_e + + return (log_e_idx, log_e) + + def get_psi_from_log_true_e_idxs_and_log_e_idxs( + self, rs, log_true_e_idxs, log_e_idxs): + if(len(log_true_e_idxs) != len(log_e_idxs)): + raise ValueError('The lengths of log_true_e_idxs ' + 'and log_e_idxs must be equal!') + + n_evt = len(log_true_e_idxs) + psi_idx = np.empty((n_evt,), dtype=int) + psi = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) + for bb_log_e_idx in bb_unique_log_e_idxs: + mm = m & (log_e_idxs == bb_log_e_idx) + bb_size = np.count_nonzero(mm) + (pdf, low_bin_edges, up_bin_edges, bin_widths) = ( + self.get_psi_pdf(b_log_true_e_idx, bb_log_e_idx) + ) + if pdf is None: + psi_idx[mm] = -1 + psi[mm] = np.nan + continue + + bb_psi_idx = rs.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=bb_size) + bb_psi = rs.uniform( + low_bin_edges[bb_psi_idx], + up_bin_edges[bb_psi_idx], + size=bb_size) + + psi_idx[mm] = bb_psi_idx + psi[mm] = bb_psi + + return (psi_idx, psi) + + def get_ang_err_from_log_true_e_idxs_and_log_e_idxs_and_psi_idxs( + self, rs, log_true_e_idxs, log_e_idxs, psi_idxs): + if (len(log_true_e_idxs) != len(log_e_idxs)) and\ + (len(log_e_idxs) != len(psi_idxs)): + raise ValueError('The lengths of log_true_e_idxs, ' + 'log_e_idxs, and psi_idxs must be equal!') + + n_evt = len(log_true_e_idxs) + ang_err = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) + for bb_log_e_idx in bb_unique_log_e_idxs: + mm = m & (log_e_idxs == bb_log_e_idx) + bbb_unique_psi_idxs = np.unique(psi_idxs[mm]) + for bbb_psi_idx in bbb_unique_psi_idxs: + mmm = mm & (psi_idxs == bbb_psi_idx) + bbb_size = np.count_nonzero(mmm) + (pdf, low_bin_edges, up_bin_edges, bin_widths) = ( + self.get_ang_err_pdf(b_log_true_e_idx, bb_log_e_idx, bbb_psi_idx) + ) + if pdf is None: + ang_err[mmm] = np.nan + continue + + bbb_ang_err_idx = rs.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=bbb_size) + bbb_ang_err = rs.uniform( + low_bin_edges[bbb_ang_err_idx], + up_bin_edges[bbb_ang_err_idx], + size=bbb_size) + + ang_err[mmm] = bbb_ang_err + + return ang_err + + def _generate_fast_n_events(self, rs, n_events): + # Initialize the output: + out_dtype = [ + ('log_true_e', float), + ('log_e', float), + ('psi', float), + ('ra', float), + ('dec', float), + ('ang_err', float) + ] + events = np.empty((n_events,), dtype=out_dtype) + + # First draw a true neutrino energy from the hypothesis spectrum. + log_true_e = np.log10(self.flux_model.get_inv_normed_cdf( + rs.uniform(size=n_events), + E_min=10**np.min(self.true_e_bin_edges), + E_max=10**np.max(self.true_e_bin_edges) + )) + events['log_true_e'] = log_true_e + + log_true_e_idxs = ( + np.digitize(log_true_e, bins=self.true_e_bin_edges) - 1 + ) + # Get reconstructed energy given true neutrino energy. + (log_e_idxs, log_e) = self.get_log_e_from_log_true_e_idxs( + rs, log_true_e_idxs) + events['log_e'] = log_e + + # Get reconstructed psi given true neutrino energy and reconstructed energy. + (psi_idxs, psi) = self.get_psi_from_log_true_e_idxs_and_log_e_idxs( + rs, log_true_e_idxs, log_e_idxs) + events['psi'] = psi + + # Get reconstructed ang_err given true neutrino energy, reconstructed energy, + # and psi. + ang_err = self.get_ang_err_from_log_true_e_idxs_and_log_e_idxs_and_psi_idxs( + rs, log_true_e_idxs, log_e_idxs, psi_idxs) + events['ang_err'] = ang_err + + # Convert the psf into a set of (r.a. and dec.) + (ra, dec) = self.circle_parametrization(rs, psi) + events['ra'] = ra + events['dec'] = dec + + return events + + def generate_fast( + self, n_events, seed=1): + rs = np.random.RandomState(seed) + + events = None + n_evt_generated = 0 + while n_evt_generated != n_events: + n_evt = n_events - n_evt_generated + + events_ = self._generate_fast_n_events(rs, n_evt) + + # Cut events that failed to be generated due to missing PDFs. + m = np.invert( + np.isnan(events_['log_e']) | + np.isnan(events_['psi']) | + np.isnan(events_['ang_err']) + ) + events_ = events_[m] + n_evt_generated += len(events_) + if events is None: + events = events_ + else: + events = np.concatenate((events, events_)) + + return events + + def _generate_n_events(self, rs, n_events): + + if not isinstance(n_events, int): + raise TypeError("The number of events must be an integer.") + + if n_events < 0: + raise ValueError("The number of events must be positive!") + + # Initialize the output: + out_dtype = [ + ('log_true_e', float), + ('log_e', float), + ('psi', float), + ('ra', float), + ('dec', float), + ('ang_err', float), + ] + + if n_events == 0: + print("Warning! Zero events are being generated") + return np.array([], dtype=out_dtype) + + events = np.empty((n_events, ), dtype=out_dtype) + + # First draw a true neutrino energy from the hypothesis spectrum. + true_energies = np.log10(self.flux_model.get_inv_normed_cdf( + rs.uniform(size=n_events), + E_min=10**np.min(self.true_e_bin_edges), + E_max=10**np.max(self.true_e_bin_edges) + )) + true_e_idx = ( + np.digitize(true_energies, bins=self.true_e_bin_edges) - 1 + ) + + for i in range(n_events): + # Get a reconstructed energy according to P(E_reco | E_true) + idxs = [true_e_idx[i], None, None] + + reco_energy, reco_e_bin, reco_e_bin_centers, idxs = ( + self._get_reconstruction_from_histogram(rs, idxs) + ) + if reco_energy is not None: + # Get an opening angle according to P(psf | E_true,E_reco). + psf, psf_bin, psf_bin_centers, idxs = ( + self._get_reconstruction_from_histogram( + rs, idxs, reco_e_bin, reco_e_bin_centers + ) + ) + + if psf is not None: + # Get an angular error according to P(ang_err | E_true,E_reco,psf). + ang_err, ang_err_bin, ang_err_bin_centers, idxs = ( + self._get_reconstruction_from_histogram( + rs, idxs, psf_bin, psf_bin_centers + ) + ) + + # Convert the psf set of (r.a. and dec.) + ra, dec = self.circle_parametrization(rs, psf) + + events[i] = (true_energies[i], reco_energy, psf, ra, dec, ang_err) + else: + events[i] = (true_energies[i], reco_energy, np.nan, np.nan, np.nan, np.nan) + else: + events[i] = (true_energies[i], np.nan, np.nan, np.nan, np.nan, np.nan) + + return events + + def generate(self, n_events, seed=1): + rs = np.random.RandomState(seed) + + events = None + n_evt_generated = 0 + while n_evt_generated != n_events: + n_evt = n_events - n_evt_generated + + events_ = self._generate_n_events(rs, n_evt) + + # Cut events that failed to be generated due to missing PDFs. + m = np.invert( + np.isnan(events_['log_e']) | + np.isnan(events_['psi']) | + np.isnan(events_['ang_err']) + ) + events_ = events_[m] + n_evt_generated += len(events_) + if events is None: + events = events_ + else: + events = np.concatenate((events, events_)) + + return events From 7ceea15d059f2a789b4848b987b8caae46e7079a Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 11 Apr 2022 20:12:05 +0200 Subject: [PATCH 022/274] Select the true energy range for which reco energy pdfs are available --- .../analyses/i3/trad_ps/signal_generator.py | 27 ++++++++++++++++--- 1 file changed, 23 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index c918011c99..c8dd59debf 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -406,12 +406,21 @@ def _generate_fast_n_events(self, rs, n_events): ] events = np.empty((n_events,), dtype=out_dtype) + # Determine the true energy range for which log_e PDFs are available. + m = np.sum( + (self.reco_e_upper_edges[:,self.dec_idx] - + self.reco_e_lower_edges[:,self.dec_idx] > 0), + axis=1) != 0 + min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) + max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) + # First draw a true neutrino energy from the hypothesis spectrum. log_true_e = np.log10(self.flux_model.get_inv_normed_cdf( rs.uniform(size=n_events), - E_min=10**np.min(self.true_e_bin_edges), - E_max=10**np.max(self.true_e_bin_edges) + E_min=10**min_log_true_e, + E_max=10**max_log_true_e )) + events['log_true_e'] = log_true_e log_true_e_idxs = ( @@ -458,6 +467,7 @@ def generate_fast( np.isnan(events_['ang_err']) ) events_ = events_[m] + n_evt_generated += len(events_) if events is None: events = events_ @@ -490,12 +500,21 @@ def _generate_n_events(self, rs, n_events): events = np.empty((n_events, ), dtype=out_dtype) + # Determine the true energy range for which log_e PDFs are available. + m = np.sum( + (self.reco_e_upper_edges[:,self.dec_idx] - + self.reco_e_lower_edges[:,self.dec_idx] > 0), + axis=1) != 0 + min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) + max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) + # First draw a true neutrino energy from the hypothesis spectrum. true_energies = np.log10(self.flux_model.get_inv_normed_cdf( rs.uniform(size=n_events), - E_min=10**np.min(self.true_e_bin_edges), - E_max=10**np.max(self.true_e_bin_edges) + E_min=10**min_log_true_e, + E_max=10**max_log_true_e )) + true_e_idx = ( np.digitize(true_energies, bins=self.true_e_bin_edges) - 1 ) From 9f776e048f898eb1db52b7e3790066d5ea3218d4 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 12 Apr 2022 14:28:10 +0200 Subject: [PATCH 023/274] Some cleaning. --- .../analyses/i3/trad_ps/signal_generator.py | 96 +++++++++---------- 1 file changed, 45 insertions(+), 51 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index c8dd59debf..9b26f2a12f 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -1,12 +1,9 @@ import numpy as np from copy import deepcopy import os.path -from matplotlib import pyplot -from skyllh.core.storage import TextFileLoader from skyllh.physics.flux import FluxModel from skyllh.analyses.i3.trad_ps.utils import load_smearing_histogram -from skyllh.core.random import RandomStateService class signal_injector(object): @@ -31,8 +28,14 @@ def __init__( - declination : float Source declination in degrees. + - right_ascension : float + Source right ascension in degrees. + - flux_model : FluxModel Instance of the `FluxModel` class. + + - data_path : str + Path to the smearing matrix data. """ self.flux_model = flux_model @@ -56,7 +59,6 @@ def __init__( raise ValueError("NotImplemented") self.dec_idx = np.digitize(self.dec, self.true_dec_bin_edges) - 1 - @staticmethod def _get_bin_centers(low_edges, high_edges): r"""Given an array of lower bin edges and an array of upper bin edges, @@ -67,7 +69,6 @@ def _get_bin_centers(low_edges, high_edges): bin_centers = 0.5 * (low_edges + high_edges) return bin_centers - def get_weighted_marginalized_pdf( self, true_e_idx, reco_e_idx=None, psf_idx=None ): @@ -82,7 +83,7 @@ def get_weighted_marginalized_pdf( # Get the marginalized distribution of the reconstructed energy # for a given (true energy, true declination) bin. pdf = deepcopy(self.histogram[true_e_idx, self.dec_idx, :]) - pdf = np.sum(pdf, axis=(-2,-1)) + pdf = np.sum(pdf, axis=(-2, -1)) label = "reco_e" elif psf_idx is None: # Get the marginalized distribution of the neutrino-muon opening @@ -98,7 +99,8 @@ def get_weighted_marginalized_pdf( # angle for a given # (true energy, true declination, reco energy, psi) bin. pdf = deepcopy( - self.histogram[true_e_idx, self.dec_idx, reco_e_idx, psf_idx, :] + self.histogram[true_e_idx, self.dec_idx, + reco_e_idx, psf_idx, :] ) label = "ang_err" @@ -118,17 +120,6 @@ def get_weighted_marginalized_pdf( lower_bin_edges, upper_bin_edges ) - # pdf *= self.flux_model.get_integral( - # 10**lower_bin_edges, 10**upper_bin_edges - # ) - - # Find where the reconstructed energy is below 100GeV and mask those - # values. We don't have any reco energy below 100GeV in the data. - # mask = bin_centers >= 2 - # lower_bin_edges = lower_bin_edges[mask] - # upper_bin_edges = upper_bin_edges[mask] - # pdf = pdf[mask] - elif label == "psf": lower_bin_edges = ( self.psf_lower_edges[true_e_idx, self.dec_idx, reco_e_idx, :] @@ -159,7 +150,6 @@ def get_weighted_marginalized_pdf( return lower_bin_edges, upper_bin_edges, bin_centers, bin_width, pdf - def _get_reconstruction_from_histogram( self, rs, idxs, value=None, bin_centers=None ): @@ -170,7 +160,7 @@ def _get_reconstruction_from_histogram( idxs[idxs.index(None)] = value_idx (low_edges, up_edges, new_bin_centers, bin_width, hist) = ( - self.get_weighted_marginalized_pdf(idxs[0],idxs[1],idxs[2]) + self.get_weighted_marginalized_pdf(idxs[0], idxs[1], idxs[2]) ) if low_edges is None: return None, None, None, None @@ -180,7 +170,6 @@ def _get_reconstruction_from_histogram( return reco_value, reco_bin, new_bin_centers, idxs - def circle_parametrization(self, rs, psf): psf = np.atleast_1d(psf) # Transform everything in radians and convert the source declination @@ -194,24 +183,24 @@ def circle_parametrization(self, rs, psf): # Parametrize the circle x = ( - (np.sin(a)*np.cos(b)*np.cos(c)) * np.cos(t) + \ - (np.sin(a)*np.sin(c)) * np.sin(t) - \ + (np.sin(a)*np.cos(b)*np.cos(c)) * np.cos(t) + + (np.sin(a)*np.sin(c)) * np.sin(t) - (np.cos(a)*np.sin(b)*np.cos(c)) ) y = ( - -(np.sin(a)*np.cos(b)*np.sin(c)) * np.cos(t) + \ - (np.sin(a)*np.cos(c)) * np.sin(t) + \ + -(np.sin(a)*np.cos(b)*np.sin(c)) * np.cos(t) + + (np.sin(a)*np.cos(c)) * np.sin(t) + (np.cos(a)*np.sin(b)*np.sin(c)) ) z = ( - (np.sin(a)*np.sin(b)) * np.cos(t) + \ + (np.sin(a)*np.sin(b)) * np.cos(t) + (np.cos(a)*np.cos(b)) ) # Convert back to right ascension and declination # This is to distinguish between diametrically opposite directions. zen = np.arccos(z) - azi = np.arctan2(y,x) + azi = np.arctan2(y, x) return (np.degrees(np.pi - azi), np.degrees(np.pi/2 - zen)) @@ -220,7 +209,7 @@ def get_log_e_pdf(self, log_true_e_idx): return (None, None, None, None) pdf = self.histogram[log_true_e_idx, self.dec_idx] - pdf = np.sum(pdf, axis=(-2,-1)) + pdf = np.sum(pdf, axis=(-2, -1)) if np.sum(pdf) == 0: return (None, None, None, None) @@ -295,7 +284,8 @@ def get_log_e_from_log_true_e_idxs(self, rs, log_true_e_idxs): for b_log_true_e_idx in unique_log_true_e_idxs: m = log_true_e_idxs == b_log_true_e_idx b_size = np.count_nonzero(m) - (pdf, low_bin_edges, up_bin_edges, bin_widths) = self.get_log_e_pdf(b_log_true_e_idx) + (pdf, low_bin_edges, up_bin_edges, + bin_widths) = self.get_log_e_pdf(b_log_true_e_idx) if pdf is None: log_e_idx[m] = -1 log_e[m] = np.nan @@ -319,7 +309,7 @@ def get_psi_from_log_true_e_idxs_and_log_e_idxs( self, rs, log_true_e_idxs, log_e_idxs): if(len(log_true_e_idxs) != len(log_e_idxs)): raise ValueError('The lengths of log_true_e_idxs ' - 'and log_e_idxs must be equal!') + 'and log_e_idxs must be equal!') n_evt = len(log_true_e_idxs) psi_idx = np.empty((n_evt,), dtype=int) @@ -359,7 +349,7 @@ def get_ang_err_from_log_true_e_idxs_and_log_e_idxs_and_psi_idxs( if (len(log_true_e_idxs) != len(log_e_idxs)) and\ (len(log_e_idxs) != len(psi_idxs)): raise ValueError('The lengths of log_true_e_idxs, ' - 'log_e_idxs, and psi_idxs must be equal!') + 'log_e_idxs, and psi_idxs must be equal!') n_evt = len(log_true_e_idxs) ang_err = np.empty((n_evt,), dtype=np.double) @@ -375,7 +365,8 @@ def get_ang_err_from_log_true_e_idxs_and_log_e_idxs_and_psi_idxs( mmm = mm & (psi_idxs == bbb_psi_idx) bbb_size = np.count_nonzero(mmm) (pdf, low_bin_edges, up_bin_edges, bin_widths) = ( - self.get_ang_err_pdf(b_log_true_e_idx, bb_log_e_idx, bbb_psi_idx) + self.get_ang_err_pdf( + b_log_true_e_idx, bb_log_e_idx, bbb_psi_idx) ) if pdf is None: ang_err[mmm] = np.nan @@ -397,19 +388,19 @@ def get_ang_err_from_log_true_e_idxs_and_log_e_idxs_and_psi_idxs( def _generate_fast_n_events(self, rs, n_events): # Initialize the output: out_dtype = [ - ('log_true_e', float), - ('log_e', float), - ('psi', float), - ('ra', float), - ('dec', float), - ('ang_err', float) + ('log_true_e', np.double), + ('log_e', np.double), + ('psi', np.double), + ('ra', np.double), + ('dec', np.double), + ('ang_err', np.double), ] events = np.empty((n_events,), dtype=out_dtype) # Determine the true energy range for which log_e PDFs are available. m = np.sum( - (self.reco_e_upper_edges[:,self.dec_idx] - - self.reco_e_lower_edges[:,self.dec_idx] > 0), + (self.reco_e_upper_edges[:, self.dec_idx] - + self.reco_e_lower_edges[:, self.dec_idx] > 0), axis=1) != 0 min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) @@ -486,12 +477,12 @@ def _generate_n_events(self, rs, n_events): # Initialize the output: out_dtype = [ - ('log_true_e', float), - ('log_e', float), - ('psi', float), - ('ra', float), - ('dec', float), - ('ang_err', float), + ('log_true_e', np.double), + ('log_e', np.double), + ('psi', np.double), + ('ra', np.double), + ('dec', np.double), + ('ang_err', np.double), ] if n_events == 0: @@ -502,8 +493,8 @@ def _generate_n_events(self, rs, n_events): # Determine the true energy range for which log_e PDFs are available. m = np.sum( - (self.reco_e_upper_edges[:,self.dec_idx] - - self.reco_e_lower_edges[:,self.dec_idx] > 0), + (self.reco_e_upper_edges[:, self.dec_idx] - + self.reco_e_lower_edges[:, self.dec_idx] > 0), axis=1) != 0 min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) @@ -545,11 +536,14 @@ def _generate_n_events(self, rs, n_events): # Convert the psf set of (r.a. and dec.) ra, dec = self.circle_parametrization(rs, psf) - events[i] = (true_energies[i], reco_energy, psf, ra, dec, ang_err) + events[i] = (true_energies[i], reco_energy, + psf, ra, dec, ang_err) else: - events[i] = (true_energies[i], reco_energy, np.nan, np.nan, np.nan, np.nan) + events[i] = (true_energies[i], reco_energy, + np.nan, np.nan, np.nan, np.nan) else: - events[i] = (true_energies[i], np.nan, np.nan, np.nan, np.nan, np.nan) + events[i] = (true_energies[i], np.nan, + np.nan, np.nan, np.nan, np.nan) return events From cdc9f800692638941e39af123b6cb5cdc27155d9 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 12 Apr 2022 14:59:44 +0200 Subject: [PATCH 024/274] Added utility function to generate dec,ra values given psi values. --- skyllh/analyses/i3/trad_ps/utils.py | 60 +++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 1e2873d98d..f2134e61c0 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -160,3 +160,63 @@ def _get_nbins_from_edges(lower_edges, upper_edges): ang_err_lower_edges, ang_err_upper_edges ) + +def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): + """Generates random declinations and right-ascension coordinates for the + given source location and opening angle `psi`. + + Parameters + ---------- + rss : instance of RandomStateService + The instance of RandomStateService to use for drawing random numbers. + src_dec : float + The declination of the source in radians. + src_ra : float + The right-ascension of the source in radians. + psi : 1d ndarray of float + The opening-angle values in radians. + + Returns + ------- + dec : 1d ndarray of float + The declination values. + ra : 1d ndarray of float + The right-ascension values. + """ + + psi = np.atleast_1d(psi) + + # Transform everything in radians and convert the source declination + # to source zenith angle + a = psi + b = np.pi/2 - src_dec + c = src_ra + + # Random rotation angle for the 2D circle + t = rss.random.uniform(0, 2*np.pi, size=len(psi)) + + # Parametrize the circle + x = ( + (np.sin(a)*np.cos(b)*np.cos(c)) * np.cos(t) + \ + (np.sin(a)*np.sin(c)) * np.sin(t) - \ + (np.cos(a)*np.sin(b)*np.cos(c)) + ) + y = ( + -(np.sin(a)*np.cos(b)*np.sin(c)) * np.cos(t) + \ + (np.sin(a)*np.cos(c)) * np.sin(t) + \ + (np.cos(a)*np.sin(b)*np.sin(c)) + ) + z = ( + (np.sin(a)*np.sin(b)) * np.cos(t) + \ + (np.cos(a)*np.cos(b)) + ) + + # Convert back to right-ascension and declination. + # This is to distinguish between diametrically opposite directions. + zen = np.arccos(z) + azi = np.arctan2(y,x) + + dec = np.pi/2 - zen + ra = np.pi - azi + + return (dec, ra) From 688f99337cae319c71ae15d1d8439397a6fbcd4a Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 12 Apr 2022 15:36:49 +0200 Subject: [PATCH 025/274] Fix power law inverse cdf calculation for gamma=1. --- skyllh/physics/flux.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/skyllh/physics/flux.py b/skyllh/physics/flux.py index e8ad94c3e8..c765af999f 100644 --- a/skyllh/physics/flux.py +++ b/skyllh/physics/flux.py @@ -345,15 +345,14 @@ def get_inv_normed_cdf(self, x, E_min, E_max): gamma = self.gamma if(gamma == 1): - inv_normed_cdf = E_max * np.exp(np.log(x/E_min) / - np.log(E_max/E_min)) - return inv_normed_cdf - - - N_0 = E_max ** (1. - gamma) - E_min ** (1. - gamma) - inv_normed_cdf = np.power( - x * N_0 + E_min**(1. - gamma), - (1. / (1. - gamma))) + N_0 = np.log(E_max / E_min) + inv_normed_cdf = E_min * np.exp(x * N_0) + + else: + N_0 = E_max ** (1. - gamma) - E_min ** (1. - gamma) + inv_normed_cdf = np.power( + x * N_0 + E_min**(1. - gamma), + (1. / (1. - gamma))) return inv_normed_cdf From ca5b9fb7c99164ce26f30c3309dce230b9285429 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 12 Apr 2022 17:34:01 +0200 Subject: [PATCH 026/274] Added the inverse CDF computation to the user_manual. --- doc/user_manual.pdf | Bin 280143 -> 289670 bytes doc/user_manual.tex | 78 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 78 insertions(+) diff --git a/doc/user_manual.pdf b/doc/user_manual.pdf index 29de440cbb360283115ce8a65f6781c1509dfdf5..fe2addda01f3f972107e3ad6e84013de09d2f2d6 100644 GIT binary patch delta 134916 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 dt3&l!IhUOu( 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
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "(h, be) = np.histogram(trials['ts'], bins=np.arange(0, np.max(trials['ts'])+0.1, 0.1))\n", + "plt.plot(0.5*(be[:-1]+be[1:]), h, drawstyle='steps-mid', label='background')\n", + "plt.vlines(ts, 1, np.max(h), label=f'TS(TXS 0506+056)={ts:.3f}')\n", + "plt.yscale('log')\n", + "plt.xlabel('TS')\n", + "plt.ylabel('#trials per bin')\n", + "plt.legend()\n", + "pass" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "We can see that the TS value of the unblinded data for TXS is rather large and 10k trials are not enough to calculate a reliable estimate for the p-value. Hence, we will generate a few more trials. SkyLLH provides also a helper function to extend the trial data file we just created. It is called ``extend_trial_data_file``: " + ] + }, + { + "cell_type": "code", + "execution_count": 21, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.core.analysis_utils import extend_trial_data_file" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function extend_trial_data_file in module skyllh.core.analysis_utils:\n", + "\n", + "extend_trial_data_file(ana, rss, n_trials, trial_data, mean_n_sig=0, mean_n_sig_null=0, mean_n_bkg_list=None, bkg_kwargs=None, sig_kwargs=None, pathfilename=None, **kwargs)\n", + " Appends to the trial data file `n_trials` generated trials for each\n", + " mean number of injected signal events up to `ns_max` for a given analysis.\n", + " \n", + " Parameters\n", + " ----------\n", + " ana : Analysis\n", + " The Analysis instance to use for sensitivity estimation.\n", + " rss : RandomStateService\n", + " The RandomStateService instance to use for generating random\n", + " numbers.\n", + " n_trials : int\n", + " The number of trials the trial data file needs to be extended by.\n", + " trial_data : structured numpy ndarray\n", + " The structured numpy ndarray holding the trials.\n", + " mean_n_sig : ndarray of float | float | 2- or 3-element sequence of float\n", + " The array of mean number of injected signal events (MNOISEs) for which\n", + " to generate trials. If this argument is not a ndarray, an array of\n", + " MNOISEs is generated based on this argument.\n", + " If a single float is given, only this given MNOISEs are injected.\n", + " If a 2-element sequence of floats is given, it specifies the range of\n", + " MNOISEs with a step size of one.\n", + " If a 3-element sequence of floats is given, it specifies the range plus\n", + " the step size of the MNOISEs.\n", + " mean_n_sig_null : ndarray of float | float | 2- or 3-element sequence of\n", + " float\n", + " The array of the fixed mean number of signal events (FMNOSEs) for the\n", + " null-hypothesis for which to generate trials. If this argument is not a\n", + " ndarray, an array of FMNOSEs is generated based on this argument.\n", + " If a single float is given, only this given FMNOSEs are used.\n", + " If a 2-element sequence of floats is given, it specifies the range of\n", + " FMNOSEs with a step size of one.\n", + " If a 3-element sequence of floats is given, it specifies the range plus\n", + " the step size of the FMNOSEs.\n", + " bkg_kwargs : dict | None\n", + " Additional keyword arguments for the `generate_events` method of the\n", + " background generation method class. An usual keyword argument is\n", + " `poisson`.\n", + " sig_kwargs : dict | None\n", + " Additional keyword arguments for the `generate_signal_events` method\n", + " of the `SignalGenerator` class. An usual keyword argument is\n", + " `poisson`.\n", + " pathfilename : string | None\n", + " Trial data file path including the filename.\n", + " \n", + " Additional keyword arguments\n", + " ----------------------------\n", + " Additional keyword arguments are passed-on to the ``create_trial_data_file``\n", + " function.\n", + " \n", + " Returns\n", + " -------\n", + " trial_data :\n", + " Trial data file extended by the required number of trials for each\n", + " mean number of injected signal events..\n", + "\n" + ] + } + ], + "source": [ + "help(extend_trial_data_file)" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[==========================================================] 100% ELT 0h:29m:35s\n" + ] + } + ], + "source": [ + "tl = TimeLord()\n", + "rss = RandomStateService(seed=2)\n", + "trials = extend_trial_data_file(\n", + " ana=ana,\n", + " rss=rss,\n", + " n_trials=4e4,\n", + " trial_data=trials,\n", + " pathfilename='/home/mwolf/projects/publicdata_ps/txs_bkg_trails.npy',\n", + " ncpu=8,\n", + " tl=tl)" + ] + }, + { + "cell_type": "code", + "execution_count": 26, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TimeLord: Executed tasks:\n", + "[Generating background events for data set 0.] 0.002 sec/iter (40000)\n", + "[Generating background events for data set 1.] 0.003 sec/iter (40000)\n", + "[Generating background events for data set 2.] 0.003 sec/iter (40000)\n", + "[Generating background events for data set 3.] 0.005 sec/iter (40000)\n", + "[Generating background events for data set 4.] 0.020 sec/iter (40000)\n", + "[Generating pseudo data. ] 0.028 sec/iter (40000)\n", + "[Initializing trial. ] 0.031 sec/iter (40000)\n", + "[Create fitparams dictionary. ] 1.0e-05 sec/iter (2375320)\n", + "[Calc fit param dep data fields. ] 3.1e-06 sec/iter (2375320)\n", + "[Get sig prob. ] 1.9e-04 sec/iter (2375320)\n", + "[Evaluating bkg log-spline. ] 2.7e-04 sec/iter (2375320)\n", + "[Get bkg prob. ] 3.3e-04 sec/iter (2375320)\n", + "[Calc PDF ratios. ] 6.6e-05 sec/iter (2375320)\n", + "[Calc pdfratio values. ] 8.5e-04 sec/iter (2375320)\n", + "[Calc pdfratio value product Ri ] 3.8e-05 sec/iter (2375320)\n", + "[Calc logLamds and grads ] 3.1e-04 sec/iter (2375320)\n", + "[Evaluate llh-ratio function. ] 0.005 sec/iter (475064)\n", + "[Minimize -llhratio function. ] 0.055 sec/iter (40000)\n", + "[Maximizing LLH ratio function. ] 0.055 sec/iter (40000)\n", + "[Calculating test statistic. ] 3.8e-05 sec/iter (40000)\n" + ] + } + ], + "source": [ + "print(tl)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The local p-value is defined as the fraction of background trials with TS value greater than the unblinded TS value of the source. " + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "-log10(p_local) = 2.93\n" + ] + } + ], + "source": [ + "minus_log10_pval = -np.log10(len(trials[trials['ts'] > ts]) / len(trials))\n", + "print(f'-log10(p_local) = {minus_log10_pval:.2f}')" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYUAAAEGCAYAAACKB4k+AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8li6FKAAAgAElEQVR4nO3deXzU9bX/8dcxiOCCyqaRgARBSwANmBqp3opeUVAR6A9BsI9KbQUXtO1tq5YuiF2ktbW91VqFiksVxNqLiHKtbZHiQoNgc2WTpUAxECGAgBv7+f0x+X47mUySSchkZjLv5+PBI5nvzPc7J+M4Zz7rMXdHREQE4KhUByAiIulDSUFEREJKCiIiElJSEBGRkJKCiIiEWqQ6gCPRvn1779q1a6rDEBHJKEuXLt3u7h3i3ZfRSaFr164sWbIk1WGIiGQUM/tXTfep+0hEREJKCiIiElJSEBGRUEaPKYg0lQMHDlBWVsbevXtTHYpIwlq1akVeXh5HH310wucoKYgkoKysjBNOOIGuXbtiZqkOR6RO7s6OHTsoKysjPz8/4fPUfSSSgL1799KuXTslBMkYZka7du3q3brNyKRgZkPMbOru3btTHYpkESUEyTQNec9mZFJw97nuPu7EE09s0PmT565g1COLmFGyqZEjExHJbBmZFBpDyYadzCndnOowRBK2ceNGevfuHfe+1atXs3r16jqvsWDBAq666qrGDq1RjB07lueeey7VYWS9rBxonjSkFyu37El1GCIZxd1xd446Kmu/S2YF/dcVySAHDx7k+uuv5+yzz2bEiBF88skn3HPPPYwYMYIhQ4Ywbtw4gmqK69at49JLL+Wcc86hX79+/POf/6xyrbfeeou+ffuyfv16KioqGDhwIP369WP8+PGcfvrpbN++nY0bN9KzZ09uueUW+vXrx3vvvcfMmTPp06cPvXv35s477wyvd/zxx4e/P/fcc4wdOxaItABuv/12Pve5z9GtW7ewNeDuTJgwgYKCAq688kq2bduW5FdPEpGVLQWRIzF57opGb2kWnNaGSUN61fm41atX8+ijj3LBBRdwww038NBDDzFhwgRGjRoFwI9+9CNefPFFhgwZwnXXXcddd93F8OHD2bt3L4cPH+a9994D4M033+S2225jzpw5dOnShQkTJnDJJZfwne98h5dffpmpU6dWec7HHnuMhx56iC1btnDnnXeydOlSTj75ZC677DKef/55hg0bVmvc5eXlvP7667z77rtcffXVjBgxgtmzZ7N69WqWLVvG1q1bKSgo4IYbbjiCV1Eag1oKIhmkc+fOXHDBBQB88Ytf5PXXX+fVV19l5MiRDBkyhPnz57NixQo+/PBDNm/ezPDhw4HIIqZjjz0WgFWrVjFu3Djmzp1Lly5dAHj99de59tprARg0aBAnn3xy+Jynn346559/PhBpXQwYMIAOHTrQokULrrvuOhYuXFhn3MOGDeOoo46ioKCArVu3ArBw4UJGjx5NTk4Op512GpdcckkjvUpyJNRSEKmnRL7RJ0vsFEMz45ZbbmHWrFnk5uYyc+ZM9u7dG3YhxZObm8vevXv5xz/+wWmnnQZQ6+OPO+648PfaHhcdW+zc+GOOOSbuNTTNN/2opSCSQTZt2sSiRYsAmDlzJhdeeCEAJ598Mh9//HHYX9+mTRvy8vJ4/vnnAdi3bx+ffPIJACeddBIvvfQSEydOZMGCBQBceOGFPPvsswC88sorfPDBB3Gfv7i4mL/97W9s376dQ4cOMXPmTC666CIATjnlFFatWsXhw4eZPXt2nX/L5z//eZ555hkOHTpEeXk5r776agNfFWlMaimIZJCePXvyxBNPMH78eHr06MHNN9/MBx98wNVXX02nTp347Gc/Gz7297//PePHj+cHP/gBRx99NH/4wx/C+0455RTmzp3L4MGDmT59OpMmTWL06NHMmjWLiy66iNzcXE444QQ++uijKs+fm5vLvffey8UXX4y7c8UVVzB06FAApkyZwlVXXUXnzp3p3bt3tXNjDR8+nPnz59OnTx/OPPPMMLlIalltzcF0V1RU5A0tsjPqkci3rVnj+zdmSNJMrVq1ip49e6Y6jBoFaxTOOuusBp2/b98+cnJyaNGiBYsWLeLmm2+mtLS0MUOUFIn33jWzpe5eFO/xadVSMLPjgIXAJHd/MdXxiGSLTZs2MXLkSA4fPkzLli2ZNm1aqkOSFElqUjCz6cBVwDZ37x11fBDw30AO8Dt3n1J5153As8mMSUSq69GjB//4xz9SHYakgWQPND8ODIo+YGY5wG+AwUABMNrMCszsUmAlsDXJMYmISA2S2lJw94Vm1jXm8HnAOndfD2BmzwBDgeOB44gkik/NbJ67H469ppmNA8YB4RxrERFpHKkYU+gEvBd1uwwodvcJAGY2FtgeLyEAuPtUYCpEBpqTG6qISHZJRVKIt1ol/HB398ebLhQREYmWisVrZUDnqNt5wJb6XEBFdkREkiMVSeEtoIeZ5ZtZS+Ba4IX6XOBIi+yIZJodO3ZQWFhIYWEhp556Kp06dQpvT548mauuuoqrr76awsJCSkpKwvNGjBjB+vXrKS4uprCwkC5dutChQ4fw3GXLlnHGGWewdu1aAA4cOECfPn3Ca/z4xz+mV69enH322dWuHdi5cycDBw6kR48eDBw4MFwNvXHjRlq3bh0+10033RSes3TpUvr06UP37t25/fbbq2x98eyzz1JQUECvXr0YM2ZMwq/Rhg0bKC4upkePHowaNYr9+/cDkRoSJ554YhjHPffcE56za9cuRowYwWc+8xl69uwZrhb/1re+xfz58xN+7nfffZf+/ftzzDHH8POf/zw8vnfvXs477zzOOeccevXqxaRJk+p1fuDQoUP07du3Si2Mu+++u8r7YN68eQnHW6tgj/Rk/ANmAuXAASIthK9UHr8CWAP8E/huA647BJjavXt3b6iRD7/pIx9+s8HnS3ZZuXJlqkMITZo0ye+77z53d3/zzTf9/PPP93feecffffddr6io8M2bN7u7+/Lly33YsGFVzn3sscf81ltvrXJs1qxZPnDgQHd3/8lPfuLjxo2rcu29e/e6u1e5drRvf/vbfu+997q7+7333ut33HGHu7tv2LDBe/XqFfdv+OxnP+tvvvmmHz582AcNGuTz5s1zd/c1a9Z4YWGh79y5093dt27dWu3cxx57zCdNmlTt+DXXXOMzZ850d/fx48f7Qw895O7ur776ql955ZVx4/jSl77k06ZNc3f3ffv2+QcffODu7hs3bgxfk0Rs3brVFy9e7BMnTgz/27i7Hz582D/88EN3d9+/f7+fd955vmjRooTPD/ziF7/w0aNHV/k7ot8HtYn33gWWeA2fr8mefTS6huPzgAanNXefC8wtKiq6saHXEDkSAwYMaNTrBXsQ1Vd5eTnt27enZcuWALRv3z687+mnnw63oKjNyJEjmT59Oj/72c94+OGHw/UKwbWDzeyirx1tzpw5YfzXX389AwYM4Kc//WmtMe/Zs4f+/SO7CXzpS1/i+eefZ/DgwUybNo1bb7013KW1Y8eOdcYPkS+38+fPZ8aMGWEcd999NzfffHON5+zZs4eFCxfy+OOPA9CyZcvwdTz99NPZsWMH77//Pqeeemqdz9+xY0c6duzISy+9VOW4mYV1Jg4cOMCBAwfibgJY0/kAZWVlvPTSS3z3u9/l/vvvrzOWI6UN8UQy2GWXXcZ7773H5ZdfzuTJk/nb3/4W3vfGG29w7rnnJnSdX/3qV9x5551873vfo23btlWufeaZZ3LLLbdUuXa0rVu3kpubC0T2RooulrNhwwb69u3LRRddxGuvvQbA5s2bycvLCx+Tl5fH5s2R0rhr1qxhzZo1XHDBBZx//vm8/PLLCcW/Y8cOTjrpJFq0aFHtmgCLFi3inHPOYfDgwaxYsQKA9evX06FDB7785S/Tt29fvvrVr/Lxxx+H5/Tr14833ngDgG984xthN030vylTplCXQ4cOUVhYSMeOHRk4cCDFxcUJ/U2Br3/96/zsZz+LW/HuwQcf5Oyzz+aGG26ocRPD+kqrbS4SZWZDgCHdu3dPdSiSpRr6zb6xHX/88SxdupSnn36akpISRo0axZQpUxg7dizl5eV06NAhoeu8/PLL5Obmsnz58mrXfu2113j11VerXDsRubm5bNq0iXbt2rF06VKGDRvGihUr4m6/HXx7PnjwIGvXrmXBggWUlZXxH//xHyxfvpxDhw7xn//5n0BkDGP//v3hDrC///3v436bD67Zr18//vWvf3H88cczb948hg0bxtq1azl48CBvv/02DzzwAMXFxXzta19jypQp/PCHPwQi3963bInMgfnlL3+Z0N8cT05ODqWlpezatYvhw4ezfPnyGmttx3rxxRfp2LEj5557brX33M0338z3v/99zIzvf//7fPOb32T69OkNjjOQkS0F10CzSCgnJ4fi4mJuv/12HnzwQf74xz8C0Lp162p1DeLZsmULv/71r1m8eDHz5s3jnXfeqXLtAQMGMHny5CrXjnbKKadQXl4ORLqGgi6fY445hnbt2gFw7rnncsYZZ7BmzRry8vIoKysLzy8rKwvrOuTl5TF06FCOPvpo8vPzOeuss1i7di3t2rWjtLSU0tJS7rnnHm666abwdp8+fWjfvj27du3i4MGD1a7Zpk2bsAvniiuu4MCBA2zfvp28vDzy8vLCb+4jRozg7bffDuPau3cvrVu3Bo6spRA46aSTGDBgQMKtH4i09l544QW6du3Ktddey/z58/niF78Yvu45OTkcddRR3HjjjSxevDjh69YmI5OCiESsXr06nDkEUFpayumnnw5Ettlet25dndf4xje+wcSJE8nLy+P+++/n1ltvxd1rvXa0q6++mieeeAKAJ554IhzHqKio4NChQ0Ckq2bt2rV069Yt3Jb773//O+7Ok08+GZ4zbNiwsK7C9u3bWbNmDd26davzbzAzLr744rCeRHQc77//ftg6Wbx4MYcPH6Zdu3aceuqpdO7cOdxh9q9//SsFBQXhNdesWRN+o//lL38ZJqHof3fddVetcVVUVLBr1y4APv30U/7yl7/wmc98ps6/J3DvvfdSVlbGxo0beeaZZ7jkkkt46qmnAMJEDDB79uyEWx91UfeRSAb76KOPuO2229i2bRs5OTn06tUrrK985ZVXsmDBAi699NIaz//zn//Mpk2b+MpXvgLAkCFDmDZtGk8++SS9e/fmtttuY9euXbRo0YLu3btXqd0cuOuuuxg5ciSPPvooXbp0Ces2LFy4kB/84Ae0aNGCnJwcHn744XC84re//S1jx47l008/ZfDgwQwePBiAyy+/nFdeeYWCggJycnK47777wtZGXX76059y7bXX8r3vfY++ffuGf9Nzzz3Hb3/7W1q0aEHr1q155plnwq6lBx54gOuuu479+/fTrVs3HnvsMSAyKLxu3TqKiuLuLl3N+++/T1FREXv27OGoo47iV7/6FStXrqS8vJzrr7+eQ4cOcfjwYUaOHBlOK3344YcBuOmmm2o8v02bNjU+5x133EFpaSlmRteuXXnkkUcSirUuqqegegqSgEysp/Dpp59y8cUX88Ybb5CTk5Oq0DLS7Nmzefvtt8PxhUxW33oK6j4SaaZat27N5MmTq8zCkcQcPHiQb37zm6kOIyXUfSSSIHfPuELzl19+eapDyEjXXHNNqkNoFA3pCcrIloJmH0lTa9WqFTt27GjQ/2QiqeDu7Nixg1atWtXrvIxsKYg0tWAaZUVFRapDiev9998H4PDhuDvOS5Zq1apVlYWCiVBSEElAMG8+XQXbOaTLojrJXBnZfSQiIsmRkUlB9RRERJIjI5OCBppFRJIjI5OCiIgkh5KCiIiElBRERCSkpCAiIqGMTAqafSQikhwZmRQ0+0hEJDkyMimIiEhyZHVSKNmwkxklm1IdhohI2sjapDC0sBMAc0q117yI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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "(h, be) = np.histogram(trials['ts'], bins=np.arange(0, np.max(trials['ts'])+0.1, 0.1))\n", + "plt.plot(0.5*(be[:-1]+be[1:]), h, drawstyle='steps-mid', label='background')\n", + "plt.vlines(ts, 1, np.max(h), label=f'TS(TXS 0506+056)={ts:.3f}')\n", + "plt.yscale('log')\n", + "plt.xlabel('TS')\n", + "plt.ylabel('#trials per bin')\n", + "plt.legend()\n", + "pass" + ] + }, + { + "cell_type": "code", + "execution_count": 33, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "array([ 1, 2, 491263, 111352301, 550290313,\n", + " 794921487, 1298508491, 1791095845, 1869695442, 1872583848,\n", + " 2360782358, 3093770124, 4000937544, 4005303368, 4070471979,\n", + " 4282876139])" + ] + }, + "execution_count": 33, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "np.unique(trials['seed'])" + ] + }, { "cell_type": "code", "execution_count": null, From cbad01214831aab9231596292f90077ec32a8d98 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 11 Oct 2022 16:13:32 +0200 Subject: [PATCH 160/274] Added section about flux normalization --- doc/sphinx/tutorials/publicdata_ps.ipynb | 182 ++++++++++++++--------- 1 file changed, 111 insertions(+), 71 deletions(-) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index 862104b348..3a31d568ac 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -257,12 +257,12 @@ "name": "stdout", "output_type": "stream", "text": [ - "[==========================================================] 100% ELT 0h:00m:15s[ ] 0% ELT 0h:00m:00s\n", "[==========================================================] 100% ELT 0h:00m:14s[ ] 0% ELT 0h:00m:00s\n", - "[==========================================================] 100% ELT 0h:00m:14s[ ] 0% ELT 0h:00m:00s\n", - "[==========================================================] 100% ELT 0h:00m:15s[ ] 0% ELT 0h:00m:00s\n", - "[==========================================================] 100% ELT 0h:00m:14s[ ] 0% ELT 0h:00m:00s\n", - "[==========================================================] 100% ELT 0h:01m:47s\n", + "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:01m:40s\n", "[==========================================================] 100% ELT 0h:00m:00s\n" ] } @@ -271,6 +271,13 @@ "ana = create_analysis(datasets=datasets, source=source)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Unblinding the data" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -364,6 +371,62 @@ "print(f'gamma = {x[\"gamma\"]:.2f}')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Calculating the corresponding flux normalization " + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "By default the analysis is created with a flux normalization of 1 GeV$^{-1}$s$^{-1}$cm$^{-2}$sr$^{-1}$ (see `refplflux_Phi0` argument of the `create_analysis` method). The analysis instance has the method `calculate_fluxmodel_scaling_factor` that calculates the scaling factor the reference flux normalization has to be multiplied with to represent a given analysis result, i.e. $n_{\\text{s}}$ and $\\gamma$ value. This function takes the detected mean $n_{\\text{s}}$ value as first argument and the list of source parameter values as second argument:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Flux scaling factor = 1.423e-15\n" + ] + } + ], + "source": [ + "scaling_factor = ana.calculate_fluxmodel_scaling_factor(x['ns'], [x['gamma']])\n", + "print(f'Flux scaling factor = {scaling_factor:.3e}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Hence, our result corresponds to a power-law flux of:" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "1.423e-15 (E/1000 GeV)^{-2.17} 1/(GeV s cm^2 sr)\n" + ] + } + ], + "source": [ + "print(f'{scaling_factor:.3e}'' (E/1000 GeV)^{-'f'{x[\"gamma\"]:.2f}'+'} 1/(GeV s cm^2 sr)')" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -381,7 +444,7 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": 18, "metadata": {}, "outputs": [], "source": [ @@ -390,7 +453,7 @@ }, { "cell_type": "code", - "execution_count": 17, + "execution_count": 19, "metadata": {}, "outputs": [ { @@ -485,7 +548,7 @@ }, { "cell_type": "code", - "execution_count": 18, + "execution_count": 20, "metadata": {}, "outputs": [], "source": [ @@ -495,35 +558,35 @@ }, { "cell_type": "code", - "execution_count": 19, + "execution_count": 21, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[==========================================================] 100% ELT 0h:08m:14s\n", + "[==========================================================] 100% ELT 0h:07m:31s\n", "TimeLord: Executed tasks:\n", "[Generating background events for data set 0.] 0.002 sec/iter (10000)\n", - "[Generating background events for data set 1.] 0.004 sec/iter (10000)\n", + "[Generating background events for data set 1.] 0.003 sec/iter (10000)\n", "[Generating background events for data set 2.] 0.003 sec/iter (10000)\n", - "[Generating background events for data set 3.] 0.006 sec/iter (10000)\n", - "[Generating background events for data set 4.] 0.028 sec/iter (10000)\n", - "[Generating pseudo data. ] 0.035 sec/iter (10000)\n", - "[Initializing trial. ] 0.034 sec/iter (10000)\n", - "[Create fitparams dictionary. ] 1.2e-05 sec/iter (593990)\n", - "[Calc fit param dep data fields. ] 3.5e-06 sec/iter (593990)\n", - "[Get sig prob. ] 2.1e-04 sec/iter (593990)\n", - "[Evaluating bkg log-spline. ] 2.9e-04 sec/iter (593990)\n", - "[Get bkg prob. ] 3.6e-04 sec/iter (593990)\n", - "[Calc PDF ratios. ] 7.4e-05 sec/iter (593990)\n", - "[Calc pdfratio values. ] 9.0e-04 sec/iter (593990)\n", - "[Calc pdfratio value product Ri ] 4.2e-05 sec/iter (593990)\n", - "[Calc logLamds and grads ] 3.4e-04 sec/iter (593990)\n", - "[Evaluate llh-ratio function. ] 0.005 sec/iter (118798)\n", - "[Minimize -llhratio function. ] 0.057 sec/iter (10000)\n", - "[Maximizing LLH ratio function. ] 0.057 sec/iter (10000)\n", - "[Calculating test statistic. ] 4.2e-05 sec/iter (10000)\n" + "[Generating background events for data set 3.] 0.005 sec/iter (10000)\n", + "[Generating background events for data set 4.] 0.024 sec/iter (10000)\n", + "[Generating pseudo data. ] 0.030 sec/iter (10000)\n", + "[Initializing trial. ] 0.030 sec/iter (10000)\n", + "[Create fitparams dictionary. ] 1.0e-05 sec/iter (593990)\n", + "[Calc fit param dep data fields. ] 2.9e-06 sec/iter (593990)\n", + "[Get sig prob. ] 1.8e-04 sec/iter (593990)\n", + "[Evaluating bkg log-spline. ] 2.6e-04 sec/iter (593990)\n", + "[Get bkg prob. ] 3.2e-04 sec/iter (593990)\n", + "[Calc PDF ratios. ] 6.2e-05 sec/iter (593990)\n", + "[Calc pdfratio values. ] 8.2e-04 sec/iter (593990)\n", + "[Calc pdfratio value product Ri ] 3.5e-05 sec/iter (593990)\n", + "[Calc logLamds and grads ] 2.9e-04 sec/iter (593990)\n", + "[Evaluate llh-ratio function. ] 0.004 sec/iter (118798)\n", + "[Minimize -llhratio function. ] 0.052 sec/iter (10000)\n", + "[Maximizing LLH ratio function. ] 0.052 sec/iter (10000)\n", + "[Calculating test statistic. ] 3.5e-05 sec/iter (10000)\n" ] } ], @@ -549,7 +612,7 @@ }, { "cell_type": "code", - "execution_count": 20, + "execution_count": 22, "metadata": {}, "outputs": [ { @@ -585,7 +648,7 @@ }, { "cell_type": "code", - "execution_count": 21, + "execution_count": 23, "metadata": {}, "outputs": [], "source": [ @@ -594,7 +657,7 @@ }, { "cell_type": "code", - "execution_count": 22, + "execution_count": 24, "metadata": {}, "outputs": [ { @@ -668,14 +731,14 @@ }, { "cell_type": "code", - "execution_count": 23, + "execution_count": 25, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ - "[==========================================================] 100% ELT 0h:29m:35s\n" + "[==========================================================] 100% ELT 0h:29m:56s\n" ] } ], @@ -706,22 +769,22 @@ "[Generating background events for data set 1.] 0.003 sec/iter (40000)\n", "[Generating background events for data set 2.] 0.003 sec/iter (40000)\n", "[Generating background events for data set 3.] 0.005 sec/iter (40000)\n", - "[Generating background events for data set 4.] 0.020 sec/iter (40000)\n", - "[Generating pseudo data. ] 0.028 sec/iter (40000)\n", - "[Initializing trial. ] 0.031 sec/iter (40000)\n", - "[Create fitparams dictionary. ] 1.0e-05 sec/iter (2375320)\n", - "[Calc fit param dep data fields. ] 3.1e-06 sec/iter (2375320)\n", - "[Get sig prob. ] 1.9e-04 sec/iter (2375320)\n", - "[Evaluating bkg log-spline. ] 2.7e-04 sec/iter (2375320)\n", - "[Get bkg prob. ] 3.3e-04 sec/iter (2375320)\n", - "[Calc PDF ratios. ] 6.6e-05 sec/iter (2375320)\n", + "[Generating background events for data set 4.] 0.019 sec/iter (40000)\n", + "[Generating pseudo data. ] 0.027 sec/iter (40000)\n", + "[Initializing trial. ] 0.032 sec/iter (40000)\n", + "[Create fitparams dictionary. ] 1.1e-05 sec/iter (2375320)\n", + "[Calc fit param dep data fields. ] 3.3e-06 sec/iter (2375320)\n", + "[Get sig prob. ] 2.0e-04 sec/iter (2375320)\n", + "[Evaluating bkg log-spline. ] 2.8e-04 sec/iter (2375320)\n", + "[Get bkg prob. ] 3.5e-04 sec/iter (2375320)\n", + "[Calc PDF ratios. ] 6.8e-05 sec/iter (2375320)\n", "[Calc pdfratio values. ] 8.5e-04 sec/iter (2375320)\n", - "[Calc pdfratio value product Ri ] 3.8e-05 sec/iter (2375320)\n", + "[Calc pdfratio value product Ri ] 3.9e-05 sec/iter (2375320)\n", "[Calc logLamds and grads ] 3.1e-04 sec/iter (2375320)\n", "[Evaluate llh-ratio function. ] 0.005 sec/iter (475064)\n", - "[Minimize -llhratio function. ] 0.055 sec/iter (40000)\n", - "[Maximizing LLH ratio function. ] 0.055 sec/iter (40000)\n", - "[Calculating test statistic. ] 3.8e-05 sec/iter (40000)\n" + "[Minimize -llhratio function. ] 0.054 sec/iter (40000)\n", + "[Maximizing LLH ratio function. ] 0.054 sec/iter (40000)\n", + "[Calculating test statistic. ] 3.7e-05 sec/iter (40000)\n" ] } ], @@ -738,7 +801,7 @@ }, { "cell_type": "code", - "execution_count": 31, + "execution_count": 27, "metadata": {}, "outputs": [ { @@ -756,7 +819,7 @@ }, { "cell_type": "code", - "execution_count": 32, + "execution_count": 28, "metadata": {}, "outputs": [ { @@ -783,29 +846,6 @@ "pass" ] }, - { - "cell_type": "code", - "execution_count": 33, - "metadata": {}, - "outputs": [ - { - "data": { - "text/plain": [ - "array([ 1, 2, 491263, 111352301, 550290313,\n", - " 794921487, 1298508491, 1791095845, 1869695442, 1872583848,\n", - " 2360782358, 3093770124, 4000937544, 4005303368, 4070471979,\n", - " 4282876139])" - ] - }, - "execution_count": 33, - "metadata": {}, - "output_type": "execute_result" - } - ], - "source": [ - "np.unique(trials['seed'])" - ] - }, { "cell_type": "code", "execution_count": null, From d272745ef74ea5d729704378c82182ea6aa14fb9 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 27 Oct 2022 18:24:14 +0200 Subject: [PATCH 161/274] Improve tutorial --- doc/sphinx/tutorials/publicdata_ps.ipynb | 285 ++++++++++++++++++++++- 1 file changed, 274 insertions(+), 11 deletions(-) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index 3a31d568ac..23c30b42ba 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -261,8 +261,8 @@ "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", - "[==========================================================] 100% ELT 0h:00m:13s[ ] 0% ELT 0h:00m:00s\n", - "[==========================================================] 100% ELT 0h:01m:40s\n", + "[==========================================================] 100% ELT 0h:00m:12s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:01m:36s\n", "[==========================================================] 100% ELT 0h:00m:00s\n" ] } @@ -271,6 +271,121 @@ "ana = create_analysis(datasets=datasets, source=source)" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Initializing a trial\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After the `Analysis` instance was created trials can be run. To do so the analysis needs to be initialized with some trial data. For instance we could initialize the analysis with the experimental data to \"unblind\" the analysis afterwards. Technically the `TrialDataManager` of each log-likelihood ratio function, i.e. dataset, is initialized with data.\n", + "\n", + "The `Analysis` class provides the method `initialize_trial` to initialize a trial with data. It takes a list of `DataFieldRecordArray` instances holding the events. If we want to initialize a trial with the experimental data, we can get that list from the `Analysis` instance itself:" + ] + }, + { + "cell_type": "code", + "execution_count": 32, + "metadata": {}, + "outputs": [], + "source": [ + "events_list = [ data.exp for data in ana.data_list ]\n", + "ana.initialize_trial(events_list)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Maximizing the log-likelihood ratio function\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After initializing a trial, we can maximize the LLH ratio function using the `maximize_llhratio` method of the `Analysis` class. This method requires a ``RandomStateService`` instance in case the minimizer does not succeed and a new set of initial values for the fit parameters need to get generated. The method returns a 4-element tuple. The first element is the set of fit parameters used in the maximization. The second element is the value of the LLH ration function at its maximum. The third element is the array of the fit parameter values at the maximum, and the forth element is the status dictionary of the minimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 39, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.core.random import RandomStateService\n", + "rss = RandomStateService(seed=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 40, + "metadata": {}, + "outputs": [], + "source": [ + "(fitparamset, log_lambda_max, fitparam_values, status) = ana.maximize_llhratio(rss)" + ] + }, + { + "cell_type": "code", + "execution_count": 44, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "log_lambda_max = 6.572529558548655\n", + "fitparam_values = [14.58039149 2.1685849 ]\n", + "status = {'grad': array([-2.09454353e-06, 2.13693588e-04]), 'task': b'CONVERGENCE: REL_REDUCTION_OF_F_<=_FACTR*EPSMCH', 'funcalls': 15, 'nit': 9, 'warnflag': 0, 'skyllh_minimizer_n_reps': 0, 'n_llhratio_func_calls': 15}\n" + ] + } + ], + "source": [ + "print(f'log_lambda_max = {log_lambda_max}')\n", + "print(f'fitparam_values = {fitparam_values}')\n", + "print(f'status = {status}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calculating the test-statistic\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the maximum of the LLH ratio function and the fit parameter values at the maximum we can calculate the test-statistic using the `calculate_test_statistic` method of the `Analysis` class:" + ] + }, + { + "cell_type": "code", + "execution_count": 48, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TS = 13.145\n" + ] + } + ], + "source": [ + "TS = ana.calculate_test_statistic(log_lambda_max, fitparam_values)\n", + "print(f'TS = {TS:.3f}')" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -282,12 +397,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "After creating the analysis instance we can unblind the data for the choosen source. Hence, we maximize the likelihood function for all given experimental data events. The analysis instance has the method ``unblind`` that can be used for that. This method requires a ``RandomStateService`` instance in case the minimizer does not succeed and a new set of initial values for the fit parameters need to get generated." + "After creating the analysis instance we can unblind the data for the choosen source. Hence, we initialize the analysis with a trial of the experimental data, maximize the log-likelihood ratio function for all given experimental data events, and calculate the test-statistic value. The analysis instance has the method ``unblind`` that can be used for that. This method requires a ``RandomStateService`` instance in case the minimizer does not succeed and a new set of initial values for the fit parameters need to get generated." ] }, { "cell_type": "code", - "execution_count": 12, + "execution_count": 35, "metadata": {}, "outputs": [], "source": [ @@ -427,6 +542,161 @@ "print(f'{scaling_factor:.3e}'' (E/1000 GeV)^{-'f'{x[\"gamma\"]:.2f}'+'} 1/(GeV s cm^2 sr)')" ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Evaluating the log-likelihood ratio function\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Sometimes it is useful to be able to evaluate the log-likelihood ratio function, e.g. for creating a likelihood contour plot. Because SkyLLH's structure is based on the mathematical structure of the likelihood function, the `Analysis` instance has the property `llhratio` which is the class instance of the used log-likelihood ratio function. This instance has the method `evaluate`. The method takes an array of the fit parameter values as argument at which the LLH ratio function will be evaluated. It returns the value of the LLH ratio function at the given point and its gradients w.r.t. the fit parameters.\n", + "\n", + "In our case this is the number of signal events, $n_{\\mathrm{s}}$ and the spectral index $\\gamma$. If we evaluate the LLH ratio function at the maximum, the gradients should be close to zero." + ] + }, + { + "cell_type": "code", + "execution_count": 31, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on method evaluate in module skyllh.core.llhratio:\n", + "\n", + "evaluate(fitparam_values, tl=None) method of skyllh.core.llhratio.MultiDatasetTCLLHRatio instance\n", + " Evaluates the composite log-likelihood-ratio function and returns its\n", + " value and global fit parameter gradients.\n", + " \n", + " Parameters\n", + " ----------\n", + " fitparam_values : (N_fitparams)-shaped numpy 1D ndarray\n", + " The ndarray holding the current values of the global fit parameters.\n", + " The first element of that array is, by definition, the number of\n", + " signal events, ns.\n", + " \n", + " Returns\n", + " -------\n", + " log_lambda : float\n", + " The calculated log-lambda value of the composite\n", + " log-likelihood-ratio function.\n", + " grads : (N_fitparams,)-shaped 1D ndarray\n", + " The ndarray holding the gradient value of the composite\n", + " log-likelihood-ratio function for ns and each global fit parameter.\n", + " By definition the first element is the gradient for ns.\n", + "\n" + ] + } + ], + "source": [ + "help(ana.llhratio.evaluate)" + ] + }, + { + "cell_type": "code", + "execution_count": 29, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "llhratio_value = 6.573\n", + "grad_ns = 0.001\n", + "grad_gamma = -0.027\n" + ] + } + ], + "source": [ + "(llhratio_value, (grad_ns, grad_gamma)) = ana.llhratio.evaluate([14.58, 2.17])\n", + "print(f'llhratio_value = {llhratio_value:.3f}')\n", + "print(f'grad_ns = {grad_ns:.3f}')\n", + "print(f'grad_gamma = {grad_gamma:.3f}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Using the `evalaute` method of the `LLHRatio` class we can scan the log-likelihood ratio space and create a contour plot showing the best fit and the 95% quantile." + ] + }, + { + "cell_type": "code", + "execution_count": 136, + "metadata": {}, + "outputs": [], + "source": [ + "(ns_min, ns_max, ns_step) = (0, 80, 0.5)\n", + "(gamma_min, gamma_max, gamma_step) = (1.5, 4.0, 0.1)\n", + "\n", + "ns_edges = np.linspace(ns_min, ns_max, int((ns_max-ns_min)/ns_step)+1)\n", + "ns_vals = 0.5*(ns_edges[1:] + ns_edges[:-1])\n", + "\n", + "gamma_edges = np.linspace(gamma_min, gamma_max, int((gamma_max-gamma_min)/gamma_step+1))\n", + "gamma_vals = 0.5*(gamma_edges[1:] + gamma_edges[:-1])\n", + "\n", + "log_lambda = np.empty((len(ns_vals), len(gamma_vals)), dtype=np.double)\n", + "for (ns_i, ns) in enumerate(ns_vals):\n", + " for (gamma_i, gamma) in enumerate(gamma_vals):\n", + " log_lambda[ns_i,gamma_i] = ana.llhratio.evaluate([ns, gamma])[0]\n", + "\n", + "# Determine the best fit ns and gamma values from the scan.\n", + "index_max = np.argmax(log_lambda)\n", + "ns_i_max = int(index_max / len(gamma_vals))\n", + "gamma_i_max = index_max % len(gamma_vals)\n", + "ns_best = ns_vals[ns_i_max]\n", + "gamma_best = gamma_vals[gamma_i_max]" + ] + }, + { + "cell_type": "code", + "execution_count": 137, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "(1.5, 4.0)" + ] + }, + "execution_count": 137, + "metadata": {}, + "output_type": "execute_result" + }, + { + "data": { + "image/png": "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\n", + "text/plain": [ + "
" + ] + }, + "metadata": { + "needs_background": "light" + }, + "output_type": "display_data" + } + ], + "source": [ + "from matplotlib.colors import LogNorm\n", + "plt.figure(figsize=(8,6))\n", + "plt.pcolormesh(gamma_edges, ns_edges, log_lambda, norm=LogNorm())\n", + "cbar = plt.colorbar()\n", + "cbar.set_label(r'$\\log(\\Lambda)$')\n", + "plt.contour(gamma_vals, ns_vals, log_lambda, [np.quantile(ts, 0.95)])\n", + "plt.plot(gamma_best, ns_best, marker='x', color='black', ms=10)\n", + "plt.xlabel(r'$\\gamma$')\n", + "plt.ylabel(r'$n_{\\mathrm{s}}$')\n", + "plt.ylim(ns_min, ns_max)\n", + "plt.xlim(gamma_min, gamma_max)" + ] + }, { "cell_type": "markdown", "metadata": {}, @@ -845,13 +1115,6 @@ "plt.legend()\n", "pass" ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [] } ], "metadata": { From 4d34155430713a64e488c4762838177c5c57cf5f Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 27 Oct 2022 18:25:50 +0200 Subject: [PATCH 162/274] bug fix --- doc/sphinx/tutorials/publicdata_ps.ipynb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index 23c30b42ba..8cef0d17a6 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -689,7 +689,7 @@ "plt.pcolormesh(gamma_edges, ns_edges, log_lambda, norm=LogNorm())\n", "cbar = plt.colorbar()\n", "cbar.set_label(r'$\\log(\\Lambda)$')\n", - "plt.contour(gamma_vals, ns_vals, log_lambda, [np.quantile(ts, 0.95)])\n", + "plt.contour(gamma_vals, ns_vals, log_lambda, [np.quantile(log_lambda, 0.95)])\n", "plt.plot(gamma_best, ns_best, marker='x', color='black', ms=10)\n", "plt.xlabel(r'$\\gamma$')\n", "plt.ylabel(r'$n_{\\mathrm{s}}$')\n", From 2e662e6c2bc1a95004633d8952114c4ae8df19b8 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 27 Oct 2022 18:45:55 +0200 Subject: [PATCH 163/274] Add disclaimer about the PS data release --- doc/sphinx/tutorials/publicdata_ps.ipynb | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index 8cef0d17a6..f8b5b4a726 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -15,6 +15,15 @@ "This tutorial shows how to use the IceCube public 10-year point-source data with SkyLLH." ] }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "**Disclamer**\n", + "\n", + "The released 10-year IceCube point-source data can reproduce the published results only within a certain amount of uncertainty due to a limited instrument response function binning provided in the data release. But the IceCube collaboration is able to reproduce the published results using detailed direct simulation data, as done for the publication." + ] + }, { "cell_type": "code", "execution_count": 1, From 4af81495c019942dffbf381eba89419e479dc6ac Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 27 Oct 2022 18:47:07 +0200 Subject: [PATCH 164/274] Fix format --- doc/sphinx/tutorials/publicdata_ps.ipynb | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index f8b5b4a726..a34ed32c1b 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -19,9 +19,12 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "**Disclamer**\n", + "**Disclaimer**\n", "\n", - "The released 10-year IceCube point-source data can reproduce the published results only within a certain amount of uncertainty due to a limited instrument response function binning provided in the data release. But the IceCube collaboration is able to reproduce the published results using detailed direct simulation data, as done for the publication." + " The released 10-year IceCube point-source data can reproduce the published results only within a certain\n", + " amount of uncertainty due to a limited instrument response function binning provided in the data release.\n", + " But the IceCube collaboration is able to reproduce the published results using detailed direct simulation\n", + " data, as done for the publication." ] }, { From b693e1311a95f179804d7613b6ddffbb815935aa Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 3 Nov 2022 18:38:06 +0100 Subject: [PATCH 165/274] Change text --- doc/sphinx/tutorials/publicdata_ps.ipynb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index a34ed32c1b..bac0f57156 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -22,8 +22,8 @@ "**Disclaimer**\n", "\n", " The released 10-year IceCube point-source data can reproduce the published results only within a certain\n", - " amount of uncertainty due to a limited instrument response function binning provided in the data release.\n", - " But the IceCube collaboration is able to reproduce the published results using detailed direct simulation\n", + " amount of uncertainty due to the limited instrument response function binning provided in the data release.\n", + " The IceCube collaboration is able to reproduce the published results using detailed direct simulation\n", " data, as done for the publication." ] }, From 8707f8e58e03d2c6d918fe14284040786a744fad Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 14 Nov 2022 17:04:41 +0100 Subject: [PATCH 166/274] Remove obsolete code for the signal energy pdf calculation. --- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 883 ------------------ 1 file changed, 883 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 8b01f5a532..48120077d5 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -629,489 +629,6 @@ def get_prob(self, tdm, gridfitparams): return prob -class PDSignalEnergyPDF_old(PDF, IsSignalPDF): - """This class provides a signal energy PDF for a spectrial index value. - """ - - def __init__( - self, f_e, log_e_edges, **kwargs): - """Creates a new signal energy PDF instance for a particular spectral - index value. - """ - super().__init__(**kwargs) - - self.f_e = f_e - - self.log_e_lower_edges = log_e_edges[:-1] - self.log_e_upper_edges = log_e_edges[1:] - - # Add the PDF axes. - self.add_axis(PDFAxis( - name='log_energy', - vmin=self.log_e_lower_edges[0], - vmax=self.log_e_upper_edges[-1]) - ) - - # Check integrity. - integral = np.sum(self.f_e * np.diff(log_e_edges)) - if not np.isclose(integral, 1): - raise ValueError( - 'The integral over log10_E of the energy term must be unity! ' - 'But it is {}!'.format(integral)) - - # Create a spline of the PDF. - self._create_spline(order=1, s=0) - - def _create_spline(self, order=1, s=0): - """Creates the spline representation of the energy PDF. - """ - log10_e_bincenters = 0.5*( - self.log_e_lower_edges + self.log_e_upper_edges) - self.spl_rep = interpolate.splrep( - log10_e_bincenters, self.f_e, - xb=self.log_e_lower_edges[0], - xe=self.log_e_upper_edges[-1], - k=order, - s=s - ) - self.spl_norm = integrate.quad( - self._eval_spline, - self.log_e_lower_edges[0], self.log_e_upper_edges[-1], - limit=200, full_output=1)[0] - - def _eval_spline(self, x): - return interpolate.splev(x, self.spl_rep, der=0) - - def assert_is_valid_for_trial_data(self, tdm): - pass - - def get_splined_pd_by_log10_e(self, log10_e, tl=None): - """Calculates the probability density for the given log10(E/GeV) - values using the spline representation of the PDF. - - - """ - # Select events that actually have a signal enegry PDF. - # All other events will get zero signal probability. - m = ( - (log10_e >= self.log_e_lower_edges[0]) & - (log10_e < self.log_e_upper_edges[-1]) - ) - - pd = np.zeros((len(log10_e),), dtype=np.double) - - pd[m] = self._eval_spline(log10_e[m]) / self.spl_norm - - return pd - - def get_pd_by_log10_e(self, log10_e, tl=None): - """Calculates the probability density for the given log10(E/GeV) - values. - - Parameters - ---------- - log10_e : (n_events,)-shaped 1D numpy ndarray - The numpy ndarray holding the log10(E/GeV) values. - tl : TimeLord | None - The optional TimeLord instance to measure code timing information. - """ - # Select events that actually have a signal enegry PDF. - # All other events will get zero signal probability. - m = ( - (log10_e >= self.log_e_lower_edges[0]) & - (log10_e < self.log_e_upper_edges[-1]) - ) - - log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.log_e_lower_edges, self.log_e_upper_edges, log10_e[m]) - - pd = np.zeros((len(log10_e),), dtype=np.double) - pd[m] = self.f_e[log_e_idxs] - - return pd - - def get_prob(self, tdm, params=None, tl=None): - """Calculates the probability density for the events given by the - TrialDataManager. - - Parameters - ---------- - tdm : TrialDataManager instance - The TrialDataManager instance holding the data events for which the - probability should be looked up. The following data fields are - required: - - 'log_energy' - The log10 of the reconstructed energy. - - 'psi' - The opening angle from the source to the event in radians. - - 'ang_err' - The angular error of the event in radians. - params : dict | None - The dictionary containing the parameter names and values for which - the probability should get calculated. - By definition this PDF does not depend on parameters. - tl : TimeLord instance | None - The optional TimeLord instance that should be used to measure - timing information. - - Returns - ------- - prob : (N_events,)-shaped numpy ndarray - The 1D numpy ndarray with the probability density for each event. - grads : (N_fitparams,N_events)-shaped ndarray | None - The 2D numpy ndarray holding the gradients of the PDF w.r.t. - each fit parameter for each event. The order of the gradients - is the same as the order of floating parameters specified through - the ``param_set`` property. - It is ``None``, if this PDF does not depend on any parameters. - """ - log10_e = tdm.get_data('log_energy') - - pd = self.get_pd_by_log10_e(log10_e, tl=tl) - - return (pd, None) - - -class PDSignalEnergyPDFSet_old(PDFSet, IsSignalPDF, IsParallelizable): - """This class provides a signal energy PDF set for the public data. - It creates a set of PDSignalEnergyPDF instances, one for each spectral - index value on a grid. - """ - - def __init__( - self, - ds, - src_dec, - flux_model, - fitparam_grid_set, - union_sm_arr_pathfilename=None, - smoothing=1, - ncpu=None, - ppbar=None, - **kwargs): - """Creates a new PDSignalEnergyPDFSet instance for the public data. - - Parameters - ---------- - ds : I3Dataset instance - The I3Dataset instance that defines the public data dataset. - src_dec : float - The declination of the source in radians. - flux_model : FluxModel instance - The FluxModel instance that defines the source's flux model. - fitparam_grid_set : ParameterGrid | ParameterGridSet instance - The parameter grid set defining the grids of the fit parameters. - union_sm_arr_pathfilename : str | None - The pathfilename of the unionized smearing matrix array file from - which the unionized smearing matrix array should get loaded from. - If None, the unionized smearing matrix array will be created. - smoothing : int - The number of bins to combine to create a smoother energy pdf. - Eight seems to produce good results. - """ - self._logger = get_logger(module_classname(self)) - - # Check for the correct types of the arguments. - if not isinstance(ds, I3Dataset): - raise TypeError( - 'The ds argument must be an instance of I3Dataset!') - - if not isinstance(flux_model, FluxModel): - raise TypeError( - 'The flux_model argument must be an instance of FluxModel!') - - if (not isinstance(fitparam_grid_set, ParameterGrid)) and\ - (not isinstance(fitparam_grid_set, ParameterGridSet)): - raise TypeError( - 'The fitparam_grid_set argument must be an instance of type ' - 'ParameterGrid or ParameterGridSet!') - - # Extend the fitparam_grid_set to allow for parameter interpolation - # values at the grid edges. - fitparam_grid_set = fitparam_grid_set.copy() - fitparam_grid_set.add_extra_lower_and_upper_bin() - - super().__init__( - pdf_type=PDF, - fitparams_grid_set=fitparam_grid_set, - ncpu=ncpu - ) - - # Load the unionized smearing matrix array or create it if no one was - # specified. - if ((union_sm_arr_pathfilename is not None) and - os.path.exists(union_sm_arr_pathfilename)): - self._logger.info( - 'Loading unionized smearing matrix from file "{}".'.format( - union_sm_arr_pathfilename)) - with open(union_sm_arr_pathfilename, 'rb') as f: - data = pickle.load(f) - else: - pathfilenames = ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('smearing_datafile')) - self._logger.info( - 'Creating unionized smearing matrix from smearing matrix file ' - '"{}".'.format( - pathfilenames)) - sm = PublicDataSmearingMatrix( - pathfilenames=pathfilenames) - data = create_unionized_smearing_matrix_array(sm, src_dec) - if union_sm_arr_pathfilename is not None: - self._logger.info( - 'Saving unionized smearing matrix to file "{}".'.format( - union_sm_arr_pathfilename)) - with open(union_sm_arr_pathfilename, 'wb') as f: - pickle.dump(data, f) - del(sm) - union_arr = data['union_arr'] - log10_true_e_binedges = data['log10_true_e_binedges'] - log10_reco_e_edges = data['log10_reco_e_binedges'] - psi_edges = data['psi_binedges'] - ang_err_edges = data['ang_err_binedges'] - del(data) - - # Merge small energy bins. - bw_th = 0.1 - max_bw = 0.2 - (union_arr, log10_reco_e_edges) = merge_reco_energy_bins( - union_arr, log10_reco_e_edges, bw_th, max_bw) - - true_e_binedges = np.power(10, log10_true_e_binedges) - nbins_true_e = len(true_e_binedges) - 1 - - # Calculate the neutrino enegry bin widths in GeV. - dE_nu = np.diff(true_e_binedges) - self._logger.debug( - 'dE_nu = {}'.format(dE_nu) - ) - - # Load the effective area. - aeff = PDAeff( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('eff_area_datafile'))) - - # Calculate the detector's neutrino energy detection probability to - # detect a neutrino of energy E_nu given a neutrino declination: - # p(E_nu|dec) - det_prob = np.empty((len(dE_nu),), dtype=np.double) - for i in range(len(dE_nu)): - det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( - sin_true_dec=np.sin(src_dec), - true_e_min=true_e_binedges[i], - true_e_max=true_e_binedges[i+1], - true_e_range_min=true_e_binedges[0], - true_e_range_max=true_e_binedges[-1] - ) - - self._logger.debug('det_prob = {}, sum = {}'.format( - det_prob, np.sum(det_prob))) - - if not np.isclose(np.sum(det_prob), 1): - self._logger.warn( - 'The sum of the detection probabilities is not unity! It is ' - '{}.'.format(np.sum(det_prob))) - - log10_reco_e_bw = np.diff(log10_reco_e_edges) - psi_edges_bw = np.diff(psi_edges) - ang_err_bw = np.diff(ang_err_edges) - - bin_volumes = ( - log10_reco_e_bw[:, np.newaxis, np.newaxis] * - psi_edges_bw[np.newaxis, :, np.newaxis] * - ang_err_bw[np.newaxis, np.newaxis, :]) - - # Create the energy pdf for different gamma values. - def create_energy_pdf(union_arr, flux_model, gridfitparams): - """Creates an energy pdf for a specific gamma value. - """ - # Create a copy of the FluxModel with the given flux parameters. - # The copy is needed to not interfer with other CPU processes. - my_flux_model = flux_model.copy(newprop=gridfitparams) - - E_nu_min = true_e_binedges[:-1] - E_nu_max = true_e_binedges[1:] - - self._logger.debug( - 'Generate signal energy PDF for parameters {} in {} E_nu ' - 'bins.'.format( - gridfitparams, nbins_true_e) - ) - - # Calculate the flux probability p(E_nu|gamma). - flux_prob = ( - my_flux_model.get_integral(E_nu_min, E_nu_max) / - my_flux_model.get_integral( - true_e_binedges[0], - true_e_binedges[-1] - ) - ) - if not np.isclose(np.sum(flux_prob), 1): - self._logger.warn( - 'The sum of the flux probabilities is not unity! It is ' - '{}.'.format(np.sum(flux_prob))) - - self._logger.debug( - 'flux_prob = {}, sum = {}'.format( - flux_prob, np.sum(flux_prob)) - ) - - p = flux_prob * det_prob - - true_e_prob = p / np.sum(p) - - self._logger.debug( - 'true_e_prob = {}'.format( - true_e_prob)) - - transfer = np.copy(union_arr) - for true_e_idx in range(nbins_true_e): - transfer[true_e_idx] *= true_e_prob[true_e_idx] - pdf_arr = np.sum(transfer, axis=0) - del(transfer) - - # Normalize the pdf, which is the probability per bin volume. - norm = np.sum(pdf_arr) - if norm == 0: - raise ValueError( - 'The signal PDF is empty for {}! This should ' - 'not happen. Check the parameter ranges!'.format( - str(gridfitparams))) - pdf_arr /= norm - pdf_arr /= bin_volumes - - # Create the enegry PDF f_e = P(log10_E_reco|dec) = - # \int dPsi dang_err P(E_reco,Psi,ang_err). - f_e = np.sum( - pdf_arr * psi_edges_bw[np.newaxis, :, np.newaxis] * - ang_err_bw[np.newaxis, np.newaxis, :], - axis=(1, 2)) - - del(pdf_arr) - - # Combine always step bins to smooth out the pdf. - step = smoothing - n = len(log10_reco_e_edges)-1 - n_new = int(np.ceil((len(log10_reco_e_edges)-1)/step,)) - f_e_new = np.zeros((n_new,), dtype=np.double) - log10_reco_e_edges_new = np.zeros( - (n_new+1), dtype=np.double) - start = 0 - k = 0 - while start <= n-1: - end = np.min([start+step, n]) - - v = np.sum(f_e[start:end]) / (end - start) - f_e_new[k] = v - log10_reco_e_edges_new[k] = log10_reco_e_edges[start] - - start += step - k += 1 - log10_reco_e_edges_new[-1] = log10_reco_e_edges[-1] - - # Re-normalize the PDF. - f_e_new = f_e_new / np.sum(f_e_new) / \ - np.diff(log10_reco_e_edges_new) - - pdf = PDSignalEnergyPDF(f_e_new, log10_reco_e_edges_new) - - return pdf - - args_list = [ - ((union_arr, flux_model, gridfitparams), {}) - for gridfitparams in self.gridfitparams_list - ] - - pdf_list = parallelize( - create_energy_pdf, - args_list, - ncpu=self.ncpu, - ppbar=ppbar) - - del(union_arr) - - # Save all the energy PDF objects in the PDFSet PDF registry with - # the hash of the individual parameters as key. - for (gridfitparams, pdf) in zip(self.gridfitparams_list, pdf_list): - self.add_pdf(pdf, gridfitparams) - - def get_prob(self, tdm, gridfitparams, tl=None): - """Calculates the signal probability density of each event for the - given set of signal fit parameters on a grid. - - Parameters - ---------- - tdm : instance of TrialDataManager - The TrialDataManager instance holding the data events for which the - probability should be calculated for. The following data fields must - exist: - - - 'log_energy' - The log10 of the reconstructed energy. - - 'psi' - The opening angle from the source to the event in radians. - - 'ang_err' - The angular error of the event in radians. - gridfitparams : dict - The dictionary holding the signal parameter values for which the - signal energy probability should be calculated. Note, that the - parameter values must match a set of parameter grid values for which - a PDSignalPDF object has been created at construction time of this - PDSignalPDFSet object. - tl : TimeLord instance | None - The optional TimeLord instance that should be used to measure time. - - Returns - ------- - prob : 1d ndarray - The array with the signal energy probability for each event. - grads : (N_fitparams,N_events)-shaped ndarray | None - The 2D numpy ndarray holding the gradients of the PDF w.r.t. - each fit parameter for each event. The order of the gradients - is the same as the order of floating parameters specified through - the ``param_set`` property. - It is ``None``, if this PDF does not depend on any parameters. - - Raises - ------ - KeyError - If no energy PDF can be found for the given signal parameter values. - """ - # print('Getting signal PDF for gridfitparams={}'.format( - # str(gridfitparams))) - pdf = self.get_pdf(gridfitparams) - - (prob, grads) = pdf.get_prob(tdm, tl=tl) - - return (prob, grads) - - -#def eval_spline(x, spl): - #values = spl(x) - #values = np.nan_to_num(values, nan=0) - #return values - - -#def create_spline(log10_e_bincenters, f_e, norm=False): - #"""Creates the spline representation of the energy PDF. - #""" - - #spline = interpolate.PchipInterpolator( - #log10_e_bincenters, f_e, extrapolate=False - #) - - #if norm: - #spl_norm = integrate.quad( - #eval_spline, - #log10_e_bincenters[0], log10_e_bincenters[-1], - #args=(spline,), - #limit=200, full_output=1)[0] - - #return spline, spl_norm - - #else: - #return spline - - class PDSignalEnergyPDF(PDF, IsSignalPDF): """This class provides a signal energy PDF for a spectrial index value. """ @@ -1512,403 +1029,3 @@ def get_prob(self, tdm, gridfitparams, tl=None): return (prob, grads) - -class PDSignalPDF_unionized_matrix(PDF, IsSignalPDF): - """This class provides a signal pdf for a given spectrial index value. - """ - - def __init__( - self, f_s, f_e, log_e_edges, psi_edges, ang_err_edges, - true_e_prob, **kwargs): - """Creates a new signal PDF for the public data. - - Parameters - ---------- - f_s : (n_e_reco, n_psi, n_ang_err)-shaped 3D numpy ndarray - The conditional PDF array P(Psi|E_reco,ang_err). - - """ - super().__init__(**kwargs) - - self.f_s = f_s - self.f_e = f_e - - self.log_e_lower_edges = log_e_edges[:-1] - self.log_e_upper_edges = log_e_edges[1:] - - self.psi_lower_edges = psi_edges[:-1] - self.psi_upper_edges = psi_edges[1:] - - self.ang_err_lower_edges = ang_err_edges[:-1] - self.ang_err_upper_edges = ang_err_edges[1:] - - self.true_e_prob = true_e_prob - - # Add the PDF axes. - self.add_axis(PDFAxis( - name='log_energy', - vmin=self.log_e_lower_edges[0], - vmax=self.log_e_upper_edges[-1]) - ) - self.add_axis(PDFAxis( - name='psi', - vmin=self.psi_lower_edges[0], - vmax=self.psi_lower_edges[-1]) - ) - self.add_axis(PDFAxis( - name='ang_err', - vmin=self.ang_err_lower_edges[0], - vmax=self.ang_err_upper_edges[-1]) - ) - - # Check integrity. - integral = np.sum( - # 1/(2*np.pi*np.sin(0.5*(psi_edges[None,1:,None]+ - # psi_edges[None,:-1,None]) - # )) * - self.f_s * np.diff(psi_edges)[None, :, None], axis=1) - if not np.all(np.isclose(integral[integral > 0], 1)): - raise ValueError( - 'The integral over Psi of the spatial term must be unity! ' - 'But it is {}!'.format(integral[integral > 0])) - integral = np.sum( - self.f_e * np.diff(log_e_edges) - ) - if not np.isclose(integral, 1): - raise ValueError( - 'The integral over log10_E of the energy term must be unity! ' - 'But it is {}!'.format(integral)) - - def assert_is_valid_for_trial_data(self, tdm): - pass - - def get_prob(self, tdm, params=None, tl=None): - """Calculates the probability density for the events given by the - TrialDataManager. - - Parameters - ---------- - tdm : TrialDataManager instance - The TrialDataManager instance holding the data events for which the - probability should be looked up. The following data fields are - required: - - 'log_energy' - The log10 of the reconstructed energy. - - 'psi' - The opening angle from the source to the event in radians. - - 'ang_err' - The angular error of the event in radians. - params : dict | None - The dictionary containing the parameter names and values for which - the probability should get calculated. - By definition this PDF does not depend on parameters. - tl : TimeLord instance | None - The optional TimeLord instance that should be used to measure - timing information. - - Returns - ------- - prob : (N_events,)-shaped numpy ndarray - The 1D numpy ndarray with the probability density for each event. - grads : (N_fitparams,N_events)-shaped ndarray | None - The 2D numpy ndarray holding the gradients of the PDF w.r.t. - each fit parameter for each event. The order of the gradients - is the same as the order of floating parameters specified through - the ``param_set`` property. - It is ``None``, if this PDF does not depend on any parameters. - """ - log_e = tdm.get_data('log_energy') - psi = tdm.get_data('psi') - ang_err = tdm.get_data('ang_err') - - # Select events that actually have a signal PDF. - # All other events will get zero signal probability. - m = ( - (log_e >= self.log_e_lower_edges[0]) & - (log_e < self.log_e_upper_edges[-1]) & - (psi >= self.psi_lower_edges[0]) & - (psi < self.psi_upper_edges[-1]) & - (ang_err >= self.ang_err_lower_edges[0]) & - (ang_err < self.ang_err_upper_edges[-1]) - ) - - log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.log_e_lower_edges, self.log_e_upper_edges, log_e[m]) - psi_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.psi_lower_edges, self.psi_upper_edges, psi[m]) - ang_err_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.ang_err_lower_edges, self.ang_err_upper_edges, ang_err[m]) - - pd_spatial = np.zeros((len(psi),), dtype=np.double) - pd_spatial[m] = ( - 1/(2*np.pi * np.sin(psi[m])) * - self.f_s[(log_e_idxs, psi_idxs, ang_err_idxs)] - ) - - pd_energy = np.zeros((len(log_e),), dtype=np.double) - pd_energy[m] = self.f_e[log_e_idxs] - - return (pd_spatial * pd_energy, None) - - -class PDSignalPDFSet_unionized_matrix(PDFSet, IsSignalPDF, IsParallelizable): - """This class provides a signal PDF set for the public data. - """ - - def __init__( - self, - ds, - src_dec, - flux_model, - fitparam_grid_set, - union_sm_arr_pathfilename=None, - ncpu=None, - ppbar=None, - **kwargs): - """Creates a new PDSignalPDFSet instance for the public data. - - Parameters - ---------- - ds : I3Dataset instance - The I3Dataset instance that defines the public data dataset. - src_dec : float - The declination of the source in radians. - flux_model : FluxModel instance - The FluxModel instance that defines the source's flux model. - """ - self._logger = get_logger(module_classname(self)) - - # Check for the correct types of the arguments. - if not isinstance(ds, I3Dataset): - raise TypeError( - 'The ds argument must be an instance of I3Dataset!') - - if not isinstance(flux_model, FluxModel): - raise TypeError( - 'The flux_model argument must be an instance of FluxModel!') - - # Extend the fitparam_grid_set to allow for parameter interpolation - # values at the grid edges. - fitparam_grid_set = fitparam_grid_set.copy() - fitparam_grid_set.add_extra_lower_and_upper_bin() - - super().__init__( - pdf_type=PDF, - fitparams_grid_set=fitparam_grid_set, - ncpu=ncpu - ) - - if(union_sm_arr_pathfilename is not None): - with open(union_sm_arr_pathfilename, 'rb') as f: - data = pickle.load(f) - else: - sm = PublicDataSmearingMatrix( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('smearing_datafile'))) - data = create_unionized_smearing_matrix_array(sm, src_dec) - del(sm) - union_arr = data['union_arr'] - log_true_e_binedges = data['log10_true_e_binedges'] - reco_e_edges = data['log10_reco_e_binedges'] - psi_edges = data['psi_binedges'] - ang_err_edges = data['ang_err_binedges'] - del(data) - - true_e_bincenters = np.power( - 10, - 0.5*(log_true_e_binedges[:-1] + log_true_e_binedges[1:])) - - true_e_binedges = np.power(10, log_true_e_binedges) - - # Calculate the neutrino enegry bin widths in GeV. - dE_nu = np.diff(true_e_binedges) - self._logger.debug( - 'dE_nu = {}'.format(dE_nu) - ) - - # Load the effective area. - aeff = PDAeff( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('eff_area_datafile'))) - - # Calculate the detector's neutrino energy detection probability to - # detect a neutrino of energy E_nu given a neutrino declination: - # p(E_nu|dec) - det_prob = np.empty((len(dE_nu),), dtype=np.double) - for i in range(len(dE_nu)): - det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( - sin_true_dec=np.sin(src_dec), - true_e_min=true_e_binedges[i], - true_e_max=true_e_binedges[i+1] - ) - - self._logger.debug('det_prob = {}, sum = {}'.format( - det_prob, np.sum(det_prob))) - - if not np.isclose(np.sum(det_prob), 1, rtol=0.06): - raise ValueError( - 'The sum of the detection probabilities is not unity! It is ' - '{}.'.format(np.sum(det_prob))) - - reco_e_bw = np.diff(reco_e_edges) - psi_edges_bw = np.diff(psi_edges) - ang_err_bw = np.diff(ang_err_edges) - - bin_volumes = ( - reco_e_bw[:, np.newaxis, np.newaxis] * - psi_edges_bw[np.newaxis, :, np.newaxis] * - ang_err_bw[np.newaxis, np.newaxis, :]) - - # Create the pdf in gamma for different gamma values. - - def create_pdf(union_arr, flux_model, gridfitparams): - """Creates a pdf for a specific gamma value. - """ - # Create a copy of the FluxModel with the given flux parameters. - # The copy is needed to not interfer with other CPU processes. - my_flux_model = flux_model.copy(newprop=gridfitparams) - - E_nu_min = np.power(10, log_true_e_binedges[:-1]) - E_nu_max = np.power(10, log_true_e_binedges[1:]) - - nbins_log_true_e = len(log_true_e_binedges) - 1 - - self._logger.debug( - 'Generate signal PDF for parameters {} in {} E_nu bins.'.format( - gridfitparams, nbins_log_true_e) - ) - - # Calculate the flux probability p(E_nu|gamma). - flux_prob = ( - my_flux_model.get_integral(E_nu_min, E_nu_max) / - my_flux_model.get_integral( - np.power(10, log_true_e_binedges[0]), - np.power(10, log_true_e_binedges[-1]) - ) - ) - if not np.isclose(np.sum(flux_prob), 1): - raise ValueError( - 'The sum of the flux probabilities is not unity!') - - self._logger.debug( - 'flux_prob = {}'.format(flux_prob) - ) - - p = flux_prob * det_prob - self._logger.debug( - 'p = {}, sum(p)={}'.format(p, sum(p)) - ) - - true_e_prob = p / np.sum(p) - - self._logger.debug( - f'true_e_prob = {true_e_prob}') - - transfer = np.copy(union_arr) - for true_e_idx in range(nbins_log_true_e): - transfer[true_e_idx] *= true_e_prob[true_e_idx] - pdf_arr = np.sum(transfer, axis=0) - del(transfer) - - # Normalize the pdf, which is the probability per bin volume. - norm = np.sum(pdf_arr) - if norm == 0: - raise ValueError( - 'The signal PDF is empty for {}! This should ' - 'not happen. Check the parameter ranges!'.format( - str(gridfitparams))) - pdf_arr /= norm - pdf_arr /= bin_volumes - - # Create the spatial PDF f_s = P(Psi|E_reco,ang_err) = - # P(E_reco,Psi,ang_err) / \int dPsi P(E_reco,Psi,ang_err). - marg_pdf = np.sum( - pdf_arr * psi_edges_bw[np.newaxis, :, np.newaxis], - axis=1, - keepdims=True - ) - f_s = pdf_arr / marg_pdf - f_s[np.isnan(f_s)] = 0 - - # Create the enegry PDF f_e = P(log10_E_reco|dec) = - # \int dPsi dang_err P(E_reco,Psi,ang_err). - f_e = np.sum( - pdf_arr * psi_edges_bw[np.newaxis, :, np.newaxis] * - ang_err_bw[np.newaxis, np.newaxis, :], - axis=(1, 2)) - - del(pdf_arr) - - pdf = PDSignalPDF_unionized_matrix( - f_s, f_e, reco_e_edges, psi_edges, ang_err_edges, - true_e_prob) - - return pdf - - args_list = [ - ((union_arr, flux_model, gridfitparams), {}) - for gridfitparams in self.gridfitparams_list - ] - - pdf_list = parallelize( - create_pdf, - args_list, - ncpu=self.ncpu, - ppbar=ppbar) - - del(union_arr) - - # Save all the energy PDF objects in the PDFSet PDF registry with - # the hash of the individual parameters as key. - for (gridfitparams, pdf) in zip(self.gridfitparams_list, pdf_list): - self.add_pdf(pdf, gridfitparams) - - def get_prob(self, tdm, gridfitparams, tl=None): - """Calculates the signal probability density of each event for the - given set of signal fit parameters on a grid. - - Parameters - ---------- - tdm : instance of TrialDataManager - The TrialDataManager instance holding the data events for which the - probability should be calculated for. The following data fields must - exist: - - - 'log_energy' - The log10 of the reconstructed energy. - - 'psi' - The opening angle from the source to the event in radians. - - 'ang_err' - The angular error of the event in radians. - gridfitparams : dict - The dictionary holding the signal parameter values for which the - signal energy probability should be calculated. Note, that the - parameter values must match a set of parameter grid values for which - a PDSignalPDF object has been created at construction time of this - PDSignalPDFSet object. - tl : TimeLord instance | None - The optional TimeLord instance that should be used to measure time. - - Returns - ------- - prob : 1d ndarray - The array with the signal energy probability for each event. - grads : (N_fitparams,N_events)-shaped ndarray | None - The 2D numpy ndarray holding the gradients of the PDF w.r.t. - each fit parameter for each event. The order of the gradients - is the same as the order of floating parameters specified through - the ``param_set`` property. - It is ``None``, if this PDF does not depend on any parameters. - - Raises - ------ - KeyError - If no energy PDF can be found for the given signal parameter values. - """ - print('Getting signal PDF for gridfitparams={}'.format( - str(gridfitparams))) - pdf = self.get_pdf(gridfitparams) - - (prob, grads) = pdf.get_prob(tdm, tl=tl) - - return (prob, grads) From 6d4057d98cce3a81bc96b08846fe182e6a7faf0c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 14 Nov 2022 17:10:54 +0100 Subject: [PATCH 167/274] Use correct argument name --- skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py index 9da17a7ac7..cf13f8b0a2 100644 --- a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py @@ -312,7 +312,7 @@ def create_analysis( with open(bkg_pdf_pathfilename, 'rb') as f: bkg_pdf_data = pickle.load(f) energy_bkgpdf = PDMCBackgroundI3EnergyPDF( - pdf_sindecmu_log10emu=bkg_pdf_data['pdf'], + pdf_log10emu_sindecmu=bkg_pdf_data['pdf'], sindecmu_binning=bkg_pdf_data['sindecmu_binning'], log10emu_binning=bkg_pdf_data['log10emu_binning'] ) From 251b7bc2211e8643a730ee6be8e34fb91f9e436c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 14 Nov 2022 17:11:45 +0100 Subject: [PATCH 168/274] Improve logging and handle inf values --- skyllh/analyses/i3/publicdata_ps/pdfratio.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pdfratio.py b/skyllh/analyses/i3/publicdata_ps/pdfratio.py index dd103daf5e..b8bc7e52ab 100644 --- a/skyllh/analyses/i3/publicdata_ps/pdfratio.py +++ b/skyllh/analyses/i3/publicdata_ps/pdfratio.py @@ -1,4 +1,6 @@ # -*- coding: utf-8 -*- +# Authors: +# Dr. Martin Wolf import sys @@ -18,7 +20,15 @@ def __init__(self, sig_pdf_set, bkg_pdf, cap_ratio=False, **kwargs): Parameters ---------- - sig_pdf_set : + sig_pdf_set : instance of PDSignalEnergyPDFSet + The PDSignalEnergyPDFSet instance holding the set of signal energy + PDFs. + bkg_pdf : instance of PDDataBackgroundI3EnergyPDF + The PDDataBackgroundI3EnergyPDF instance holding the background + energy PDF. + cap_ratio : bool + Switch whether the S/B PDF ratio should get capped where no + background is available. Default is False. """ self._logger = get_logger(module_classname(self)) @@ -34,7 +44,7 @@ def __init__(self, sig_pdf_set, bkg_pdf, cap_ratio=False, **kwargs): self.cap_ratio = cap_ratio if self.cap_ratio: - self._logger.info('The PDF ratio will be capped!') + self._logger.info('The energy PDF ratio will be capped!') # Calculate the ratio value for the phase space where no background # is available. We will take the p_sig percentile of the signal @@ -53,8 +63,13 @@ def __init__(self, sig_pdf_set, bkg_pdf, cap_ratio=False, **kwargs): sigvals = sigpdf.get_pd_by_log10_reco_e(log10_e_bc) sigvals = np.broadcast_to(sigvals, (n_sinDec, n_logE)).T r = sigvals[bd] / bkg_pdf._hist_logE_sinDec[bd] + # Remove possible inf values. + r = r[np.invert(np.isinf(r))] val = np.percentile(r[r > 1.], ratio_perc) self.ratio_fill_value_dict[sig_pdf_key] = val + self._logger.info( + f'The cap value for the energy PDF ratio key {sig_pdf_key} ' + f'is {val}.') # Create cache variables for the last ratio value and gradients in # order to avoid the recalculation of the ratio value when the From f27ba68a5765d217706ab1d65668992540cbd69e Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 14 Nov 2022 17:58:06 +0100 Subject: [PATCH 169/274] Some error handling. --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index f3587fcadc..118f3f6824 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -99,6 +99,10 @@ def _generate_inv_cdf_spline(self, flux_model, log_e_min, @staticmethod def _eval_spline(x, spl): + if (x < 0 or x > 1): + raise ValueError( + f'{x} is outside of the valid spline range. ' + 'The valid range is [0,1].') values = interpolate.splev(x, spl, ext=3) return values From fad94b8edcf86f80a7902501f01e1c2de35cae32 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 14 Nov 2022 18:40:23 +0100 Subject: [PATCH 170/274] Renamed signal generator objects --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 9 +++++---- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 4 ++-- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 118f3f6824..fbf52f358d 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -20,7 +20,7 @@ from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff -class PublicDataDatasetSignalGenerator(object): +class PDDatasetSignalGenerator(object): def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): """Creates a new instance of the signal generator for generating @@ -99,7 +99,8 @@ def _generate_inv_cdf_spline(self, flux_model, log_e_min, @staticmethod def _eval_spline(x, spl): - if (x < 0 or x > 1): + x = np.asarray(x) + if (x.any() < 0 or x.any() > 1): raise ValueError( f'{x} is outside of the valid spline range. ' 'The valid range is [0,1].') @@ -259,7 +260,7 @@ def generate_signal_events( return events -class PublicDataSignalGenerator(object): +class PDSignalGenerator(object): """This class provides a signal generation method for a point-like source seen in the IceCube detector using the 10 years public data release. """ @@ -356,7 +357,7 @@ def generate_signal_events(self, rss, mean, poisson=True): events_ = None for (shg_src_idx, src) in enumerate(shg.source_list): ds = self._dataset_list[ds_idx] - sig_gen = PublicDataDatasetSignalGenerator( + sig_gen = PDDatasetSignalGenerator( ds, src.dec, self.effA[ds_idx], self.sm[ds_idx]) if self.effA[ds_idx] is None: self.effA[ds_idx] = sig_gen.effA diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 61b774a172..7145dafd98 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -73,7 +73,7 @@ # Analysis specific classes for working with the public data. from skyllh.analyses.i3.publicdata_ps.signal_generator import ( - PublicDataSignalGenerator + PDSignalGenerator ) from skyllh.analyses.i3.publicdata_ps.detsigyield import ( PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod @@ -254,7 +254,7 @@ def create_analysis( fitparam_ns, test_statistic, bkg_gen_method, - custom_sig_generator=PublicDataSignalGenerator + custom_sig_generator=PDSignalGenerator ) # Define the event selection method for pure optimization purposes. From 3a14f4508b76bbc8889f802db1f9b560c8394e6d Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 9 Dec 2022 10:26:47 +0100 Subject: [PATCH 171/274] First commit analysis with MC. --- .../analyses/i3/publicdata_ps/trad_ps_wMC.py | 330 ++++++++++++++++++ skyllh/datasets/i3/PublicData_10y_ps.py | 41 ++- 2 files changed, 360 insertions(+), 11 deletions(-) create mode 100644 skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py b/skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py new file mode 100644 index 0000000000..f80e2a150a --- /dev/null +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py @@ -0,0 +1,330 @@ +# -*- coding: utf-8 -*- + +"""The IC170922A_wGFU analysis is a multi-dataset time-integrated single source +analysis with a two-component likelihood function using a spacial and an energy +event PDF. +""" + +import argparse +import logging +import numpy as np + +from skyllh.core.progressbar import ProgressBar + +# Classes to define the source hypothesis. +from skyllh.physics.source import PointLikeSource +from skyllh.physics.flux import PowerLawFlux +from skyllh.core.source_hypo_group import SourceHypoGroup +from skyllh.core.source_hypothesis import SourceHypoGroupManager + +# Classes to define the fit parameters. +from skyllh.core.parameters import ( + SingleSourceFitParameterMapper, + FitParameter +) + +# Classes for the minimizer. +from skyllh.core.minimizer import Minimizer, LBFGSMinimizerImpl + +# Classes for utility functionality. +from skyllh.core.config import CFG +from skyllh.core.random import RandomStateService +from skyllh.core.optimize import SpatialBoxEventSelectionMethod +from skyllh.core.smoothing import BlockSmoothingFilter +from skyllh.core.timing import TimeLord +from skyllh.core.trialdata import TrialDataManager + +# Classes for defining the analysis. +from skyllh.core.test_statistic import TestStatisticWilks +from skyllh.core.analysis import ( + TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis +) + +# Classes to define the background generation. +from skyllh.core.scrambling import DataScrambler, UniformRAScramblingMethod +from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod + +# Classes to define the detector signal yield tailored to the source hypothesis. +from skyllh.i3.detsigyield import PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod + +# Classes to define the signal and background PDFs. +from skyllh.core.signalpdf import GaussianPSFPointLikeSourceSignalSpatialPDF +from skyllh.i3.signalpdf import SignalI3EnergyPDFSet +from skyllh.i3.backgroundpdf import ( + DataBackgroundI3SpatialPDF, + DataBackgroundI3EnergyPDF +) +# Classes to define the spatial and energy PDF ratios. +from skyllh.core.pdfratio import ( + SpatialSigOverBkgPDFRatio, + Skylab2SkylabPDFRatioFillMethod +) +from skyllh.i3.pdfratio import I3EnergySigSetOverBkgPDFRatioSpline + +from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod + +# Analysis utilities. +from skyllh.core.analysis_utils import ( + pointlikesource_to_data_field_array +) + +# Logging setup utilities. +from skyllh.core.debugging import ( + setup_logger, + setup_console_handler, + setup_file_handler +) + +# The pre-defined data samples. +from skyllh.datasets.i3 import data_samples + +def TXS_location(): + src_ra = np.radians(77.358) + src_dec = np.radians(5.693) + return (src_ra, src_dec) + +def create_analysis( + datasets, + source, + refplflux_Phi0=1, + refplflux_E0=1e3, + refplflux_gamma=2, + ns_seed=10.0, + gamma_seed=3, + compress_data=False, + keep_data_fields=None, + optimize_delta_angle=10, + efficiency_mode=None, + tl=None, + ppbar=None +): + """Creates the Analysis instance for this particular analysis. + + Parameters: + ----------- + datasets : list of Dataset instances + The list of Dataset instances, which should be used in the + analysis. + source : PointLikeSource instance + The PointLikeSource instance defining the point source position. + refplflux_Phi0 : float + The flux normalization to use for the reference power law flux model. + refplflux_E0 : float + The reference energy to use for the reference power law flux model. + refplflux_gamma : float + The spectral index to use for the reference power law flux model. + ns_seed : float + Value to seed the minimizer with for the ns fit. + gamma_seed : float | None + Value to seed the minimizer with for the gamma fit. If set to None, + the refplflux_gamma value will be set as gamma_seed. + compress_data : bool + Flag if the data should get converted from float64 into float32. + keep_data_fields : list of str | None + List of additional data field names that should get kept when loading + the data. + optimize_delta_angle : float + The delta angle in degrees for the event selection optimization methods. + efficiency_mode : str | None + The efficiency mode the data should get loaded with. Possible values + are: + + - 'memory': + The data will be load in a memory efficient way. This will + require more time, because all data records of a file will + be loaded sequentially. + - 'time': + The data will be loaded in a time efficient way. This will + require more memory, because each data file gets loaded in + memory at once. + + The default value is ``'time'``. If set to ``None``, the default + value will be used. + tl : TimeLord instance | None + The TimeLord instance to use to time the creation of the analysis. + ppbar : ProgressBar instance | None + The instance of ProgressBar for the optional parent progress bar. + + Returns + ------- + analysis : SpatialEnergyTimeIntegratedMultiDatasetSingleSourceAnalysis + The Analysis instance for this analysis. + """ + # Define the flux model. + fluxmodel = PowerLawFlux( + Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) + + # Define the fit parameter ns. + fitparam_ns = FitParameter('ns', 0, 1e3, ns_seed) + + # Define the gamma fit parameter. + fitparam_gamma = FitParameter('gamma', valmin=1, valmax=4, initial=gamma_seed) + + # Define the detector signal efficiency implementation method for the + # IceCube detector and this source and fluxmodel. + # The sin(dec) binning will be taken by the implementation method + # automatically from the Dataset instance. + gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) + detsigyield_implmethod = PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( + gamma_grid) + + # Define the signal generation method. + sig_gen_method = PointLikeSourceI3SignalGenerationMethod() + + # Create a source hypothesis group manager. + src_hypo_group_manager = SourceHypoGroupManager( + SourceHypoGroup( + source, fluxmodel, detsigyield_implmethod, sig_gen_method)) + + # Create a source fit parameter mapper and define the fit parameters. + src_fitparam_mapper = SingleSourceFitParameterMapper() + src_fitparam_mapper.def_fit_parameter(fitparam_gamma) + + # Define the test statistic. + test_statistic = TestStatisticWilks() + + # Define the data scrambler with its data scrambling method, which is used + # for background generation. + data_scrambler = DataScrambler(UniformRAScramblingMethod()) + + # Create background generation method. + bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) + + # Create the minimizer instance. + minimizer = Minimizer(LBFGSMinimizerImpl()) + + # Create the Analysis instance. + analysis = Analysis( + src_hypo_group_manager, + src_fitparam_mapper, + fitparam_ns, + test_statistic, + bkg_gen_method + ) + + # Define the event selection method for pure optimization purposes. + # We will use the same method for all datasets. + event_selection_method = SpatialBoxEventSelectionMethod( + src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) + + # Add the data sets to the analysis. + pbar = ProgressBar(len(datasets), parent=ppbar).start() + for ds in datasets: + # Load the data of the data set. + data = ds.load_and_prepare_data( + keep_fields=keep_data_fields, + compress=compress_data, + efficiency_mode=efficiency_mode, + tl=tl) + + # Create a trial data manager and add the required data fields. + tdm = TrialDataManager() + tdm.add_source_data_field('src_array', pointlikesource_to_data_field_array) + + sin_dec_binning = ds.get_binning_definition('sin_dec') + log_energy_binning = ds.get_binning_definition('log_energy') + + # Create the spatial PDF ratio instance for this dataset. + spatial_sigpdf = GaussianPSFPointLikeSourceSignalSpatialPDF( + dec_range=np.arcsin(sin_dec_binning.range)) + spatial_bkgpdf = DataBackgroundI3SpatialPDF( + data.exp, sin_dec_binning) + spatial_pdfratio = SpatialSigOverBkgPDFRatio( + spatial_sigpdf, spatial_bkgpdf) + + # Create the energy PDF ratio instance for this dataset. + smoothing_filter = BlockSmoothingFilter(nbins=1) + energy_sigpdfset = SignalI3EnergyPDFSet( + data.mc, log_energy_binning, sin_dec_binning, fluxmodel, gamma_grid, + smoothing_filter, ppbar=pbar) + energy_bkgpdf = DataBackgroundI3EnergyPDF( + data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) + fillmethod = Skylab2SkylabPDFRatioFillMethod() + energy_pdfratio = I3EnergySigSetOverBkgPDFRatioSpline( + energy_sigpdfset, energy_bkgpdf, + fillmethod=fillmethod, + ppbar=pbar) + + pdfratios = [ spatial_pdfratio, energy_pdfratio ] + #pdfratios = [ spatial_pdfratio ] + + analysis.add_dataset( + ds, data, pdfratios, tdm, event_selection_method) + + pbar.increment() + pbar.finish() + + analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) + + analysis.construct_signal_generator() + + return analysis + +if(__name__ == '__main__'): + p = argparse.ArgumentParser( + description = "Calculates TS for a given source location using 7-year " + "point source sample and 3-year GFU sample.", + formatter_class = argparse.RawTextHelpFormatter + ) + p.add_argument("--data_base_path", default=None, type=str, + help='The base path to the data samples (default=None)' + ) + p.add_argument("--ncpu", default=1, type=int, + help='The number of CPUs to utilize where parallelization is possible.' + ) + args = p.parse_args() + + # Setup `skyllh` package logging. + # To optimize logging set the logging level to the lowest handling level. + setup_logger('skyllh', logging.DEBUG) + log_format = '%(asctime)s %(processName)s %(name)s %(levelname)s: '\ + '%(message)s' + setup_console_handler('skyllh', logging.INFO, log_format) + setup_file_handler('skyllh', logging.DEBUG, log_format, 'debug.log') + + CFG['multiproc']['ncpu'] = args.ncpu + + sample_seasons = [ + ("PointSourceTracks", "IC40"), + ("PointSourceTracks", "IC59"), + ("PointSourceTracks", "IC79"), + ("PointSourceTracks", "IC86, 2011"), + ("PointSourceTracks", "IC86, 2012-2014"), + ("GFU", "IC86, 2015-2017") + ] + + datasets = [] + for (sample, season) in sample_seasons: + # Get the dataset from the correct dataset collection. + dsc = data_samples[sample].create_dataset_collection(args.data_base_path) + datasets.append(dsc.get_dataset(season)) + + rss_seed = 1 + # Define a random state service. + rss = RandomStateService(rss_seed) + + # Define the point source. + source = PointLikeSource(*TXS_location()) + + tl = TimeLord() + + with tl.task_timer('Creating analysis.'): + ana = create_analysis( + datasets, source, compress_data=False, tl=tl) + + with tl.task_timer('Unblinding data.'): + (TS, fitparam_dict, status) = ana.unblind(rss) + + #print('log_lambda_max: %g'%(log_lambda_max)) + print('TS = %g'%(TS)) + print('ns_fit = %g'%(fitparam_dict['ns'])) + print('gamma_fit = %g'%(fitparam_dict['gamma'])) + + # Generate some signal events. + with tl.task_timer('Generating signal events.'): + (n_sig, signal_events_dict) = ana.sig_generator.generate_signal_events(rss, 100) + + print('n_sig: %d', n_sig) + print('signal datasets: '+str(signal_events_dict.keys())) + + print(tl) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index b64161c7aa..7c89e2da3f 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -270,8 +270,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC40 = I3Dataset( name = 'IC40', exp_pathfilenames = 'events/IC40_exp.csv', + mc_pathfilenames = 'sim/IC40_MC.npy', grl_pathfilenames = 'uptime/IC40_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC40.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -296,8 +296,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC59 = I3Dataset( name = 'IC59', exp_pathfilenames = 'events/IC59_exp.csv', + mc_pathfilenames = 'sim/IC59_MC.npy', grl_pathfilenames = 'uptime/IC59_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC59.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -323,8 +323,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC79 = I3Dataset( name = 'IC79', exp_pathfilenames = 'events/IC79_exp.csv', + mc_pathfilenames = 'sim/IC79_MC.npy', grl_pathfilenames = 'uptime/IC79_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC79.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -349,8 +349,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_I = I3Dataset( name = 'IC86_I', exp_pathfilenames = 'events/IC86_I_exp.csv', + mc_pathfilenames = 'sim/IC86_I_MC.npy', grl_pathfilenames = 'uptime/IC86_I_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC86_I.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -377,8 +377,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_II = I3Dataset( name = 'IC86_II', exp_pathfilenames = 'events/IC86_II_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', grl_pathfilenames = 'uptime/IC86_II_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC86_II.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -406,8 +406,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_III = I3Dataset( name = 'IC86_III', exp_pathfilenames = 'events/IC86_III_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', grl_pathfilenames = 'uptime/IC86_III_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC86_III.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -427,8 +427,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_IV = I3Dataset( name = 'IC86_IV', exp_pathfilenames = 'events/IC86_IV_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', grl_pathfilenames = 'uptime/IC86_IV_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC86_IV.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -448,8 +448,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_V = I3Dataset( name = 'IC86_V', exp_pathfilenames = 'events/IC86_V_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', grl_pathfilenames = 'uptime/IC86_V_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC86_V.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -469,8 +469,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_VI = I3Dataset( name = 'IC86_VI', exp_pathfilenames = 'events/IC86_VI_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', grl_pathfilenames = 'uptime/IC86_VI_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC86_VI.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -490,8 +490,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_VII = I3Dataset( name = 'IC86_VII', exp_pathfilenames = 'events/IC86_VII_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', grl_pathfilenames = 'uptime/IC86_VII_exp.csv', - mc_pathfilenames = None, **ds_kwargs ) IC86_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -519,8 +519,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_II_VII = I3Dataset( name = 'IC86_II-VII', exp_pathfilenames = I3Dataset.get_combined_exp_pathfilenames(ds_list), + mc_pathfilenames = IC86_II.mc_pathfilename_list, grl_pathfilenames = I3Dataset.get_combined_grl_pathfilenames(ds_list), - mc_pathfilenames = None, **ds_kwargs ) IC86_II_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict @@ -563,9 +563,27 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'Zenith[deg]': 'zen' }) +# dsc.set_mc_field_name_renaming_dict({ +# 'true_dec': 'true_dec', +# 'true_ra': 'true_ra', +# 'true_energy': 'true_energy', +# 'log_energy': 'log_energy', +# 'ra': 'ra', +# 'dec': 'dec', +# 'ang_err': 'ang_err', +# 'mcweight': 'mcweight' +# }) + def add_run_number(data): exp = data.exp + mc = data.mc exp.append_field('run', np.repeat(0, len(exp))) + mc.append_field('run', np.repeat(0, len(mc))) + + def add_time(data): + mc = data.mc + mc.append_field('time', np.repeat(0, len(mc))) + def convert_deg2rad(data): exp = data.exp exp['ang_err'] = np.deg2rad(exp['ang_err']) @@ -575,6 +593,7 @@ def convert_deg2rad(data): exp['zen'] = np.deg2rad(exp['zen']) dsc.add_data_preparation(add_run_number) + dsc.add_data_preparation(add_time) dsc.add_data_preparation(convert_deg2rad) return dsc From bb37675f665297bcd691c84802504c8f29da0cde Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Fri, 9 Dec 2022 11:21:06 +0100 Subject: [PATCH 172/274] Fix a bug where `keep_fields` argument gets overwritten when loading MC --- skyllh/core/dataset.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skyllh/core/dataset.py b/skyllh/core/dataset.py index 752e1641e5..dc6378e6e7 100644 --- a/skyllh/core/dataset.py +++ b/skyllh/core/dataset.py @@ -709,7 +709,7 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): fileloader_exp = create_FileLoader( self.exp_abs_pathfilename_list) # Create the list of field names that should get kept. - keep_fields = list(set( + keep_fields_exp = list(set( _conv_new2orig_field_names( CFG['dataset']['analysis_required_exp_field_names'] + self._loading_extra_exp_field_name_list + @@ -719,7 +719,7 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): )) data_exp = fileloader_exp.load_data( - keep_fields=keep_fields, + keep_fields=keep_fields_exp, dtype_convertions=dtc_dict, dtype_convertion_except_fields=_conv_new2orig_field_names( dtc_except_fields, @@ -734,7 +734,7 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): with TaskTimer(tl, 'Loading mc data from disk.'): fileloader_mc = create_FileLoader( self.mc_abs_pathfilename_list) - keep_fields = list(set( + keep_fields_mc = list(set( _conv_new2orig_field_names( CFG['dataset']['analysis_required_exp_field_names'] + self._loading_extra_exp_field_name_list + @@ -747,7 +747,7 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): self._mc_field_name_renaming_dict) )) data_mc = fileloader_mc.load_data( - keep_fields=keep_fields, + keep_fields=keep_fields_mc, dtype_convertions=dtc_dict, dtype_convertion_except_fields=_conv_new2orig_field_names( dtc_except_fields, From 0f2e905aec727fb9a66a63959157e722cfd6dcff Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Fri, 9 Dec 2022 16:30:36 +0100 Subject: [PATCH 173/274] Append `sin_dec` and `sin_true_dec` fields only when they do not already exist --- skyllh/i3/dataset.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/skyllh/i3/dataset.py b/skyllh/i3/dataset.py index a40a38e052..bd597aa967 100644 --- a/skyllh/i3/dataset.py +++ b/skyllh/i3/dataset.py @@ -300,17 +300,20 @@ def prepare_data(self, data, tl=None): # Append sin(dec) data field to the experimental data. task = 'Appending IceCube-specific data fields to exp data.' with TaskTimer(tl, task): - data.exp.append_field( - 'sin_dec', np.sin(data.exp['dec'])) + if 'sin_dec' not in data.exp.field_name_list: + data.exp.append_field( + 'sin_dec', np.sin(data.exp['dec'])) if(data.mc is not None): # Append sin(dec) and sin(true_dec) to the MC data. task = 'Appending IceCube-specific data fields to MC data.' with TaskTimer(tl, task): - data.mc.append_field( - 'sin_dec', np.sin(data.mc['dec'])) - data.mc.append_field( - 'sin_true_dec', np.sin(data.mc['true_dec'])) + if 'sin_dec' not in data.mc.field_name_list: + data.mc.append_field( + 'sin_dec', np.sin(data.mc['dec'])) + if 'sin_true_dec' not in data.mc.field_name_list: + data.mc.append_field( + 'sin_true_dec', np.sin(data.mc['true_dec'])) # Set the livetime of the dataset from the GRL data when no livetime # was specified previously. From 3c215e22a5e36ce976d6f6273d0403217954c398 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Fri, 9 Dec 2022 16:35:37 +0100 Subject: [PATCH 174/274] Support loading mc dataset when it has different renaming dictionary for the exp field names --- skyllh/core/dataset.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skyllh/core/dataset.py b/skyllh/core/dataset.py index dc6378e6e7..8c0502357f 100644 --- a/skyllh/core/dataset.py +++ b/skyllh/core/dataset.py @@ -741,6 +741,10 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): keep_fields, self._exp_field_name_renaming_dict) + _conv_new2orig_field_names( + # Special case when exp and mc files have different + # renaming dictionaries. + CFG['dataset']['analysis_required_exp_field_names'] + + self._loading_extra_exp_field_name_list + CFG['dataset']['analysis_required_mc_field_names'] + self._loading_extra_mc_field_name_list + keep_fields, From 768d0b2d9917fc73e417e68d717539b4aaeccbeb Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> Date: Mon, 12 Dec 2022 17:17:30 +0100 Subject: [PATCH 175/274] Update comment --- skyllh/core/dataset.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skyllh/core/dataset.py b/skyllh/core/dataset.py index 8c0502357f..480229dbab 100644 --- a/skyllh/core/dataset.py +++ b/skyllh/core/dataset.py @@ -734,6 +734,9 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): with TaskTimer(tl, 'Loading mc data from disk.'): fileloader_mc = create_FileLoader( self.mc_abs_pathfilename_list) + # Determine `keep_fields_mc` for the generic case, where MC + # field names are an union of exp and mc field names. + # But the renaming dictionary can differ for exp and MC fields. keep_fields_mc = list(set( _conv_new2orig_field_names( CFG['dataset']['analysis_required_exp_field_names'] + @@ -741,8 +744,6 @@ def _conv_new2orig_field_names(new_field_names, orig2new_renaming_dict): keep_fields, self._exp_field_name_renaming_dict) + _conv_new2orig_field_names( - # Special case when exp and mc files have different - # renaming dictionaries. CFG['dataset']['analysis_required_exp_field_names'] + self._loading_extra_exp_field_name_list + CFG['dataset']['analysis_required_mc_field_names'] + From c0976f6b2db8d0be162b7b66d1754d2c6a7352c9 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 20 Dec 2022 11:48:24 +0100 Subject: [PATCH 176/274] Add data preparation function. --- skyllh/datasets/i3/PublicData_10y_ps.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 7c89e2da3f..d0e17ddf23 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -584,6 +584,11 @@ def add_time(data): mc = data.mc mc.append_field('time', np.repeat(0, len(mc))) + def add_azimuth_and_zenith(data): + mc = data.mc + mc.append_field('azi', np.repeat(0, len(mc))) + mc.append_field('zen', np.repeat(0, len(mc))) + def convert_deg2rad(data): exp = data.exp exp['ang_err'] = np.deg2rad(exp['ang_err']) @@ -594,6 +599,7 @@ def convert_deg2rad(data): dsc.add_data_preparation(add_run_number) dsc.add_data_preparation(add_time) + dsc.add_data_preparation(add_azimuth_and_zenith) dsc.add_data_preparation(convert_deg2rad) return dsc From 99709c3fd7f79784d950a433520ca47a458e3de5 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 23 Jan 2023 10:12:38 +0100 Subject: [PATCH 177/274] Effective area pre-calculation. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 191 +++++++++++--------- 1 file changed, 101 insertions(+), 90 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 079e301fce..f435f82d62 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -115,12 +115,12 @@ def __init__( should get pre-calculated using the ``get_detection_prob_for_decnu`` method. min_log10enu : float | None - The minimum log10(E_nu/GeV) value that should get used for + The minimum log10(E_nu/GeV) value that should be used for calculating the detection probability. If None, the lowest available neutrino energy bin edge of the effective area is used. max_log10enu : float | None - The maximum log10(E_nu/GeV) value that should get used for + The maximum log10(E_nu/GeV) value that should be used for calculating the detection probability. If None, the highest available neutrino energy bin edge of the effective area is used. @@ -153,13 +153,24 @@ def __init__( self._log10_enu_binedges_upper[-1:]) ) - # Pre-calculate detection probabilities for certain neutrino - # declinations if requested. + # Pre-calculate detection probabilities for a certain neutrino + # declination if requested. if src_dec is not None: + # Ignore bins where Aeff = 0. + m = self.get_aeff_for_decnu(src_dec) > 0 if min_log10enu is None: - min_log10enu = self._log10_enu_binedges_lower[0] + min_log10enu = self._log10_enu_binedges_lower[m][0] + else: + min_log10enu = max( + self._log10_enu_binedges_lower[m][0], + min_log10enu) + if max_log10enu is None: - max_log10enu = self._log10_enu_binedges_upper[-1] + max_log10enu = self._log10_enu_binedges_upper[m][-1] + else: + max_log10enu = min( + self._log10_enu_binedges_upper[m][-1], + max_log10enu) m = ( (self.log10_enu_bincenters >= min_log10enu) & @@ -267,24 +278,24 @@ def get_aeff_for_decnu(self, decnu): return aeff - #def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): - #"""Calculates the detection probability density p(E_nu|sin_dec) in - #unit GeV^-1 for the given true energy values. + # def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): + # """Calculates the detection probability density p(E_nu|sin_dec) in + # unit GeV^-1 for the given true energy values. - #Parameters - #---------- + # Parameters + # ---------- #sin_true_dec : float - #The sin of the true declination. - #true_e : (n,)-shaped 1d numpy ndarray of float - #The values of the true energy in GeV for which the probability - #density value should get calculated. - - #Returns - #------- - #det_pd : (n,)-shaped 1d numpy ndarray of float - #The detection probability density values for the given true energy - #value. - #""" + # The sin of the true declination. + # true_e : (n,)-shaped 1d numpy ndarray of float + # The values of the true energy in GeV for which the probability + # density value should get calculated. + + # Returns + # ------- + # det_pd : (n,)-shaped 1d numpy ndarray of float + # The detection probability density values for the given true energy + # value. + # """ #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) #dE = np.diff(np.power(10, self.log_true_e_binedges)) @@ -297,39 +308,39 @@ def get_aeff_for_decnu(self, decnu): #det_pd = interpolate.splev(true_e, tck, der=0) - #return det_pd + # return det_pd - #def get_detection_pd_in_log10E_for_sin_true_dec( - #self, sin_true_dec, log10_true_e): - #"""Calculates the detection probability density p(E_nu|sin_dec) in - #unit log10(GeV)^-1 for the given true energy values. + # def get_detection_pd_in_log10E_for_sin_true_dec( + # self, sin_true_dec, log10_true_e): + # """Calculates the detection probability density p(E_nu|sin_dec) in + # unit log10(GeV)^-1 for the given true energy values. - #Parameters - #---------- + # Parameters + # ---------- #sin_true_dec : float - #The sin of the true declination. - #log10_true_e : (n,)-shaped 1d numpy ndarray of float - #The log10 values of the true energy in GeV for which the - #probability density value should get calculated. - - #Returns - #------- - #det_pd : (n,)-shaped 1d numpy ndarray of float - #The detection probability density values for the given true energy - #value. - #""" + # The sin of the true declination. + # log10_true_e : (n,)-shaped 1d numpy ndarray of float + # The log10 values of the true energy in GeV for which the + # probability density value should get calculated. + + # Returns + # ------- + # det_pd : (n,)-shaped 1d numpy ndarray of float + # The detection probability density values for the given true energy + # value. + # """ #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) #dlog10E = np.diff(self.log_true_e_binedges) #det_pdf = aeff / np.sum(aeff) / dlog10E - #spl = interpolate.splrep( - #self.log_true_e_bincenters, det_pdf, k=1, s=0) + # spl = interpolate.splrep( + # self.log_true_e_bincenters, det_pdf, k=1, s=0) #det_pd = interpolate.splev(log10_true_e, spl, der=0) - #return det_pd + # return det_pd def get_detection_prob_for_decnu( self, decnu, enu_min, enu_max, enu_range_min, enu_range_max): @@ -430,58 +441,58 @@ def _eval_spl_func(x): return det_prob - #def get_aeff_integral_for_sin_true_dec( - #self, sin_true_dec, log_true_e_min, log_true_e_max): - #"""Calculates the integral of the effective area using the trapezoid - #method. + # def get_aeff_integral_for_sin_true_dec( + # self, sin_true_dec, log_true_e_min, log_true_e_max): + # """Calculates the integral of the effective area using the trapezoid + # method. - #Returns - #------- + # Returns + # ------- #integral : float - #The integral in unit cm^2 GeV. - #""" + # The integral in unit cm^2 GeV. + # """ #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - #integral = ( - #(np.power(10, log_true_e_max) - - #np.power(10, log_true_e_min)) * - #0.5 * - #(np.interp(log_true_e_min, self.log_true_e_bincenters, aeff) + - #np.interp(log_true_e_max, self.log_true_e_bincenters, aeff)) - #) - - #return integral - - #def get_aeff(self, sin_true_dec, log_true_e): - #"""Retrieves the effective area for the given sin(dec_true) and - #log(E_true) value pairs. - - #Parameters - #---------- - #sin_true_dec : (n,)-shaped 1D ndarray - #The sin(dec_true) values. - #log_true_e : (n,)-shaped 1D ndarray - #The log(E_true) values. - - #Returns - #------- - #aeff : (n,)-shaped 1D ndarray - #The 1D ndarray holding the effective area values for each value - #pair. For value pairs outside the effective area data zero is - #returned. - #""" - #valid = ( - #(sin_true_dec >= self.sin_true_dec_binedges[0]) & - #(sin_true_dec <= self.sin_true_dec_binedges[-1]) & - #(log_true_e >= self.log_true_e_binedges[0]) & - #(log_true_e <= self.log_true_e_binedges[-1]) - #) - #sin_true_dec_idxs = np.digitize( - #sin_true_dec[valid], self.sin_true_dec_binedges) - 1 - #log_true_e_idxs = np.digitize( - #log_true_e[valid], self.log_true_e_binedges) - 1 + # integral = ( + # (np.power(10, log_true_e_max) - + # np.power(10, log_true_e_min)) * + # 0.5 * + # (np.interp(log_true_e_min, self.log_true_e_bincenters, aeff) + + # np.interp(log_true_e_max, self.log_true_e_bincenters, aeff)) + # ) + + # return integral + + # def get_aeff(self, sin_true_dec, log_true_e): + # """Retrieves the effective area for the given sin(dec_true) and + # log(E_true) value pairs. + + # Parameters + # ---------- + # sin_true_dec : (n,)-shaped 1D ndarray + # The sin(dec_true) values. + # log_true_e : (n,)-shaped 1D ndarray + # The log(E_true) values. + + # Returns + # ------- + # aeff : (n,)-shaped 1D ndarray + # The 1D ndarray holding the effective area values for each value + # pair. For value pairs outside the effective area data zero is + # returned. + # """ + # valid = ( + # (sin_true_dec >= self.sin_true_dec_binedges[0]) & + # (sin_true_dec <= self.sin_true_dec_binedges[-1]) & + # (log_true_e >= self.log_true_e_binedges[0]) & + #(log_true_e <= self.log_true_e_binedges[-1]) + # ) + # sin_true_dec_idxs = np.digitize( + # sin_true_dec[valid], self.sin_true_dec_binedges) - 1 + # log_true_e_idxs = np.digitize( + # log_true_e[valid], self.log_true_e_binedges) - 1 #aeff = np.zeros((len(valid),), dtype=np.double) #aeff[valid] = self.aeff_arr[sin_true_dec_idxs,log_true_e_idxs] - #return aeff + # return aeff From d25ba881e95960b40f615ea5db583c12e29fc1b4 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 23 Jan 2023 11:44:05 +0100 Subject: [PATCH 178/274] Update to main code restructuring. --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 3 ++- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index fbf52f358d..a162bbbcf3 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -8,6 +8,7 @@ float_cast, int_cast ) +from skyllh.core.signal_generator import SignalGeneratorBase from skyllh.core.llhratio import LLHRatio from skyllh.core.dataset import Dataset from skyllh.core.source_hypothesis import SourceHypoGroupManager @@ -260,7 +261,7 @@ def generate_signal_events( return events -class PDSignalGenerator(object): +class PDSignalGenerator(SignalGeneratorBase): """This class provides a signal generation method for a point-like source seen in the IceCube detector using the 10 years public data release. """ diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 7145dafd98..69e084a1ca 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -254,7 +254,7 @@ def create_analysis( fitparam_ns, test_statistic, bkg_gen_method, - custom_sig_generator=PDSignalGenerator + sig_generator_cls=PDSignalGenerator ) # Define the event selection method for pure optimization purposes. From b5e706baf5b70718fc31a1d5e82165035da0bef1 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 8 Feb 2023 16:42:51 +0100 Subject: [PATCH 179/274] Fixed detection probability calculation. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 22 ++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index f435f82d62..2d2b5c927b 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -156,20 +156,18 @@ def __init__( # Pre-calculate detection probabilities for a certain neutrino # declination if requested. if src_dec is not None: - # Ignore bins where Aeff = 0. - m = self.get_aeff_for_decnu(src_dec) > 0 if min_log10enu is None: - min_log10enu = self._log10_enu_binedges_lower[m][0] + min_log10enu = self._log10_enu_binedges_lower[0] else: min_log10enu = max( - self._log10_enu_binedges_lower[m][0], + self._log10_enu_binedges_lower[0], min_log10enu) if max_log10enu is None: - max_log10enu = self._log10_enu_binedges_upper[m][-1] + max_log10enu = self._log10_enu_binedges_upper[-1] else: max_log10enu = min( - self._log10_enu_binedges_upper[m][-1], + self._log10_enu_binedges_upper[-1], max_log10enu) m = ( @@ -179,6 +177,8 @@ def __init__( bin_centers = self.log10_enu_bincenters[m] low_bin_edges = self._log10_enu_binedges_lower[m] high_bin_edges = self._log10_enu_binedges_upper[m] + + print(f"\nActual bin_edges for det_prob calculation: [{low_bin_edges[0]}, {high_bin_edges[-1]}]") # Get the detection probability P(E_nu | sin(dec)) per bin. self.det_prob = self.get_detection_prob_for_decnu( @@ -375,7 +375,7 @@ def get_detection_prob_for_decnu( np.array([enu_range_min]) ) if enu_range_max >= enu_binedges[-1]: - uidx = len(enu_binedges)-2 + uidx = len(enu_binedges)-1 else: (uidx,) = get_bin_indices_from_lower_and_upper_binedges( enu_binedges[:-1], @@ -385,11 +385,11 @@ def get_detection_prob_for_decnu( # Note: The get_bin_indices_from_lower_and_upper_binedges function # is based on the lower edges. So by definition the upper bin # index is one too large. - uidx -= 1 + # uidx -= 1 aeff = self.get_aeff_for_decnu(decnu) - aeff = aeff[lidx:uidx+1] - enu_binedges = enu_binedges[lidx:uidx+2] + aeff = aeff[lidx:uidx] + enu_binedges = enu_binedges[lidx:uidx+1] dE = np.diff(enu_binedges) @@ -423,7 +423,7 @@ def _eval_spl_func(x): limit=200, full_output=1 )[0] - + enu_min = np.atleast_1d(enu_min) enu_max = np.atleast_1d(enu_max) From f9251139af20ce0b7f65461f79cb5808f4b7fa2d Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 8 Feb 2023 16:43:25 +0100 Subject: [PATCH 180/274] Added some doc strings. --- .../i3/publicdata_ps/signal_generator.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index a162bbbcf3..31ff87ae1c 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -42,6 +42,7 @@ def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): max_log_true_e) = \ self.smearing_matrix.get_true_log_e_range_with_valid_log_e_pdfs( dec_idx) + print(f"Detection probability to be computed in log10(E) [{min_log_true_e}, {max_log_true_e}]") kwargs = { 'src_dec': src_dec, 'min_log10enu': min_log_true_e, @@ -267,6 +268,24 @@ class PDSignalGenerator(SignalGeneratorBase): """ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhratio=None): + """Constructs a new signal generator instance. + + Parameters + ---------- + src_hypo_group_manager : SourceHypoGroupManager instance + The SourceHypoGroupManager instance defining the source hypothesis + groups. + dataset_list : list of Dataset instances + The list of Dataset instances for which signal events should get + generated for. + data_list : list of DatasetData instances + The list of DatasetData instances holding the actual data of each + dataset. The order must match the order of ``dataset_list``. + llhratio : LLHRatio + The likelihood ratio object contains the datasets signal weights + needed for distributing the event generation among the different + datsets. + """ self.src_hypo_group_manager = src_hypo_group_manager self.dataset_list = dataset_list self.data_list = data_list From 3f3244ed16a4546feda14e3a2ec2ff2558eacdc5 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 8 Feb 2023 16:44:12 +0100 Subject: [PATCH 181/274] Construct the signal generator when the analysis is created. --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 1 + 1 file changed, 1 insertion(+) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 69e084a1ca..9c97b26e16 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -317,6 +317,7 @@ def create_analysis( pbar.finish() analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) + analysis.construct_signal_generator(llhratio=analysis.llhratio) return analysis From b2a7b79d2ed12a5959a5087f717d1d4baefd5017 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 8 Feb 2023 16:53:24 +0100 Subject: [PATCH 182/274] Remove prints. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 2 -- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 1 - 2 files changed, 3 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 2d2b5c927b..f20aff7738 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -177,8 +177,6 @@ def __init__( bin_centers = self.log10_enu_bincenters[m] low_bin_edges = self._log10_enu_binedges_lower[m] high_bin_edges = self._log10_enu_binedges_upper[m] - - print(f"\nActual bin_edges for det_prob calculation: [{low_bin_edges[0]}, {high_bin_edges[-1]}]") # Get the detection probability P(E_nu | sin(dec)) per bin. self.det_prob = self.get_detection_prob_for_decnu( diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 31ff87ae1c..cd0f819d24 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -42,7 +42,6 @@ def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): max_log_true_e) = \ self.smearing_matrix.get_true_log_e_range_with_valid_log_e_pdfs( dec_idx) - print(f"Detection probability to be computed in log10(E) [{min_log_true_e}, {max_log_true_e}]") kwargs = { 'src_dec': src_dec, 'min_log10enu': min_log_true_e, From eedd2bf9e3d2ae34546ecec14f74a96e94e2de52 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 9 Feb 2023 11:24:03 +0100 Subject: [PATCH 183/274] Fix for signal injection in the southern sky. --- .../i3/publicdata_ps/signal_generator.py | 32 ++++++++++++-- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 43 +++++++++++++++++-- 2 files changed, 69 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index cd0f819d24..0931fb3383 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -209,9 +209,22 @@ def _generate_events( events['run'] = -1 * np.ones(n_events) return events + + @staticmethod + @np.vectorize + def energy_filter(events, spline, cut_sindec): + # The energy filter will cut all events below cut_sindec + # that have an energy smaller than the energy spline at + # their declination. + energy_filter = np.logical_and( + events['sin_dec']=edges[i], data.exp['sin_dec'] Date: Thu, 9 Feb 2023 11:52:40 +0100 Subject: [PATCH 184/274] Small fix for error handling. --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 0931fb3383..679f5cafcf 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -216,6 +216,8 @@ def energy_filter(events, spline, cut_sindec): # The energy filter will cut all events below cut_sindec # that have an energy smaller than the energy spline at # their declination. + if cut_sindec is None: + cut_sindec = 0 energy_filter = np.logical_and( events['sin_dec'] Date: Thu, 9 Feb 2023 12:47:36 +0100 Subject: [PATCH 185/274] Some restructuring. --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 45 ++++++++++++++------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 2107c22645..c12573ea4c 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -89,10 +89,6 @@ PDDataBackgroundI3EnergyPDF ) -# Dataset specific features for the energy cut splines -cut_sindec = np.sin(np.radians([-2, 0, -3, 0, 0])) -spl_smooth = [0., 0.005, 0.05, 0.2, 0.3] - def psi_func(tdm, src_hypo_group_manager, fitparams): """Function to calculate the opening angle between the source position @@ -140,6 +136,8 @@ def create_analysis( gamma_seed=3, kde_smoothing=False, minimizer_impl="LBFGS", + cut_sindec = None, + spl_smooth = None, cap_ratio=False, compress_data=False, keep_data_fields=None, @@ -175,6 +173,11 @@ def create_analysis( (L-BFG-S minimizer used from the :mod:`scipy.optimize` module), or "minuit" (Minuit minimizer used by the :mod:`iminuit` module). Default: "LBFGS". + cut_sindec : list of float | None + sin(dec) values at which the energy cut in the southern sky should + start. If None, np.sin(np.radians([-2, 0, -3, 0, 0])) is used. + spl_smooth : list of float + cap_ratio : bool If set to True, the energy PDF ratio will be capped to a finite value where no background energy PDF information is available. This will @@ -267,7 +270,17 @@ def create_analysis( event_selection_method = SpatialBoxEventSelectionMethod( src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) #event_selection_method = None - + + # Prepare the spline parameters. + if cut_sindec is None: + cut_sindec = np.sin(np.radians([-2, 0, -3, 0, 0])) + if spl_smooth is None: + spl_smooth = [0., 0.005, 0.05, 0.2, 0.3] + if len(spl_smooth) != len(datasets) or len(cut_sindec) != len(datasets): + raise AssertionError("The length of the spl_smooth and of the " + "cut_sindec must be equal to the length of datasets: " + f"{len(datasets)}.") + # Add the data sets to the analysis. pbar = ProgressBar(len(datasets), parent=ppbar).start() energy_cut_splines = [] @@ -326,24 +339,26 @@ def create_analysis( # their experimental dataset shows events that should probably have # been cut by the IceCube selection. # ToDo: Move this to the dataset definition as a ds preparation step + data_exp = data.exp.copy(keep_fields=['sin_dec', 'log_energy']) if ds.name == 'IC79': - m = np.logical_and( - data.exp['sin_dec']<-0.75, - data.exp['log_energy'] < 4.2) - data.exp = data.exp[~m] + m = np.invert(np.logical_and( + data_exp['sin_dec']<-0.75, + data_exp['log_energy'] < 4.2)) + data.exp = data.exp[m] if ds.name == 'IC86_I': - m = np.logical_and( - data.exp['sin_dec']<-0.2, - data.exp['log_energy'] < 2.5) - data.exp = data.exp[~m] + m = np.invert(np.logical_and( + data_exp['sin_dec']<-0.2, + data_exp['log_energy'] < 2.5)) + data_exp = data_exp[m] sin_dec_binning = ds.get_binning_definition('sin_dec') edges = sin_dec_binning.binedges e_filter = np.zeros(len(edges)-1, dtype=float) for i in range(len(edges)-1): mask = np.logical_and( - data.exp['sin_dec']>=edges[i], data.exp['sin_dec']=edges[i], data_exp['sin_dec'] Date: Thu, 9 Feb 2023 13:13:34 +0100 Subject: [PATCH 186/274] Bug fix and renaming. --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index c12573ea4c..ef688cb645 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -177,7 +177,8 @@ def create_analysis( sin(dec) values at which the energy cut in the southern sky should start. If None, np.sin(np.radians([-2, 0, -3, 0, 0])) is used. spl_smooth : list of float - + Smoothing parameters for the 1D spline for the energy cut. If None, + [0., 0.005, 0.05, 0.2, 0.3] is used. cap_ratio : bool If set to True, the energy PDF ratio will be capped to a finite value where no background energy PDF information is available. This will @@ -338,13 +339,12 @@ def create_analysis( # Some special conditions are needed for IC79 and IC86_I, because # their experimental dataset shows events that should probably have # been cut by the IceCube selection. - # ToDo: Move this to the dataset definition as a ds preparation step data_exp = data.exp.copy(keep_fields=['sin_dec', 'log_energy']) if ds.name == 'IC79': m = np.invert(np.logical_and( data_exp['sin_dec']<-0.75, data_exp['log_energy'] < 4.2)) - data.exp = data.exp[m] + data_exp = data_exp[m] if ds.name == 'IC86_I': m = np.invert(np.logical_and( data_exp['sin_dec']<-0.2, @@ -352,17 +352,18 @@ def create_analysis( data_exp = data_exp[m] sin_dec_binning = ds.get_binning_definition('sin_dec') - edges = sin_dec_binning.binedges - e_filter = np.zeros(len(edges)-1, dtype=float) - for i in range(len(edges)-1): + sindec_edges = sin_dec_binning.binedges + min_log_e = np.zeros(len(sindec_edges)-1, dtype=float) + for i in range(len(sindec_edges)-1): mask = np.logical_and( - data_exp['sin_dec']>=edges[i], data_exp['sin_dec']=sindec_edges[i], + data_exp['sin_dec'] Date: Thu, 9 Feb 2023 15:29:58 +0100 Subject: [PATCH 187/274] Comment out outdated code for the signal energy pdf. --- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 1144 ++++++++--------- 1 file changed, 572 insertions(+), 572 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 48120077d5..da509dee56 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -55,578 +55,578 @@ -class PublicDataSignalGenerator(object): - def __init__(self, ds, **kwargs): - """Creates a new instance of the signal generator for generating signal - events from the provided public data smearing matrix. - """ - super().__init__(**kwargs) - - self.smearing_matrix = PublicDataSmearingMatrix( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('smearing_datafile'))) - - def _generate_events( - self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): - """Generates `n_events` signal events for the given source location - and flux model. - - Note: - Some values can be NaN in cases where a PDF was not available! - - Parameters - ---------- - rss : instance of RandomStateService - The instance of RandomStateService to use for drawing random - numbers. - src_dec : float - The declination of the source in radians. - src_ra : float - The right-ascention of the source in radians. - - Returns - ------- - events : numpy record array of size `n_events` - The numpy record array holding the event data. - It contains the following data fields: - - 'isvalid' - - 'log_true_energy' - - 'log_energy' - - 'sin_dec' - Single values can be NaN in cases where a pdf was not available. - """ - # Create the output event array. - out_dtype = [ - ('isvalid', np.bool_), - ('log_true_energy', np.double), - ('log_energy', np.double), - ('sin_dec', np.double) - ] - events = np.empty((n_events,), dtype=out_dtype) - - sm = self.smearing_matrix - - # Determine the true energy range for which log_e PDFs are available. - (min_log_true_e, - max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( - dec_idx) - - # First draw a true neutrino energy from the hypothesis spectrum. - log_true_e = np.log10(flux_model.get_inv_normed_cdf( - rss.random.uniform(size=n_events), - E_min=10**min_log_true_e, - E_max=10**max_log_true_e - )) - - events['log_true_energy'] = log_true_e - - log_true_e_idxs = ( - np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 - ) - # Sample reconstructed energies given true neutrino energies. - (log_e_idxs, log_e) = sm.sample_log_e( - rss, dec_idx, log_true_e_idxs) - events['log_energy'] = log_e - - # Sample reconstructed psi values given true neutrino energy and - # reconstructed energy. - (psi_idxs, psi) = sm.sample_psi( - rss, dec_idx, log_true_e_idxs, log_e_idxs) - - # Sample reconstructed ang_err values given true neutrino energy, - # reconstructed energy, and psi. - (ang_err_idxs, ang_err) = sm.sample_ang_err( - rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs) - - isvalid = np.invert( - np.isnan(log_e) | np.isnan(psi) | np.isnan(ang_err)) - events['isvalid'] = isvalid - - # Convert the psf into a set of (r.a. and dec.). Only use non-nan - # values. - (dec, ra) = psi_to_dec_and_ra(rss, src_dec, src_ra, psi[isvalid]) - events['sin_dec'][isvalid] = np.sin(dec) - - return events - - def generate_signal_events( - self, rss, src_dec, src_ra, flux_model, n_events): - """Generates ``n_events`` signal events for the given source location - and flux model. - - Returns - ------- - events : numpy record array - The numpy record array holding the event data. - It contains the following data fields: - - 'isvalid' - - 'log_energy' - - 'sin_dec' - """ - sm = self.smearing_matrix - - # Find the declination bin index. - dec_idx = sm.get_true_dec_idx(src_dec) - - events = None - n_evt_generated = 0 - while n_evt_generated != n_events: - n_evt = n_events - n_evt_generated - - events_ = self._generate_events( - rss, src_dec, src_ra, dec_idx, flux_model, n_evt) - - # Cut events that failed to be generated due to missing PDFs. - events_ = events_[events_['isvalid']] - - n_evt_generated += len(events_) - if events is None: - events = events_ - else: - events = np.concatenate((events, events_)) - - return events - - -class PublicDataSignalI3EnergyPDF(EnergyPDF, IsSignalPDF, UsesBinning): - """Class that implements the enegry signal PDF for a given flux model given - the public data. - """ - - def __init__(self, ds, flux_model, data_dict=None): - """Constructs a new enegry PDF instance using the public IceCube data. - - Parameters - ---------- - ds : instance of I3Dataset - The I3Dataset instance holding the file name of the smearing data of - the public data. - flux_model : instance of FluxModel - The flux model that should be used to calculate the energy signal - pdf. - data_dict : dict | None - If not None, the histogram data and its bin edges can be provided. - The dictionary needs the following entries: - - - 'histogram' - - 'true_e_bin_edges' - - 'true_dec_bin_edges' - - 'reco_e_lower_edges' - - 'reco_e_upper_edges' - """ - super().__init__() - - if(not isinstance(ds, I3Dataset)): - raise TypeError( - 'The ds argument must be an instance of I3Dataset!') - if(not isinstance(flux_model, FluxModel)): - raise TypeError( - 'The flux_model argument must be an instance of FluxModel!') - - self._ds = ds - self._flux_model = flux_model - - if(data_dict is None): - (self.histogram, - true_e_bin_edges, - true_dec_bin_edges, - self.reco_e_lower_edges, - self.reco_e_upper_edges - ) = load_smearing_histogram( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('smearing_datafile'))) - else: - self.histogram = data_dict['histogram'] - true_e_bin_edges = data_dict['true_e_bin_edges'] - true_dec_bin_edges = data_dict['true_dec_bin_edges'] - self.reco_e_lower_edges = data_dict['reco_e_lower_edges'] - self.reco_e_upper_edges = data_dict['reco_e_upper_edges'] - - # Get the number of bins for each of the variables in the matrix. - # The number of bins for e_mu, psf, and ang_err for each true_e and - # true_dec bin are equal. - self.add_binning(BinningDefinition('true_e', true_e_bin_edges)) - self.add_binning(BinningDefinition('true_dec', true_dec_bin_edges)) - - # Marginalize over the PSF and angular error axes. - self.histogram = np.sum(self.histogram, axis=(3, 4)) - - # Create a (prob vs E_reco) spline for each source declination bin. - n_true_dec = len(true_dec_bin_edges) - 1 - true_e_binning = self.get_binning('true_e') - self.spline_norm_list = [] - for true_dec_idx in range(n_true_dec): - (spl, norm) = self.get_total_weighted_energy_pdf( - true_dec_idx, true_e_binning) - self.spline_norm_list.append((spl, norm)) - - @property - def ds(self): - """(read-only) The I3Dataset instance for which this enegry signal PDF - was constructed. - """ - return self._ds - - @property - def flux_model(self): - """(read-only) The FluxModel instance for which this energy signal PDF - was constructed. - """ - return self._flux_model - - def _create_spline(self, bin_centers, values, order=1, smooth=0): - """Creates a :class:`scipy.interpolate.UnivariateSpline` with the - given order and smoothing factor. - """ - spline = UnivariateSpline( - bin_centers, values, k=order, s=smooth, ext='zeros' - ) - - return spline - - def get_weighted_energy_pdf_hist_for_true_energy_dec_bin( - self, true_e_idx, true_dec_idx, flux_model, log_e_min=0): - """Gets the reconstructed muon energy pdf histogram for a specific true - neutrino energy and declination bin weighted with the assumed flux - model. - - Parameters - ---------- - true_e_idx : int - The index of the true enegry bin. - true_dec_idx : int - The index of the true declination bin. - flux_model : instance of FluxModel - The FluxModel instance that represents the flux formula. - log_e_min : float - The minimal reconstructed energy in log10 to be considered for the - PDF. - - Returns - ------- - energy_pdf_hist : 1d ndarray | None - The enegry PDF values. - None is returned if all PDF values are zero. - bin_centers : 1d ndarray | None - The bin center values for the energy PDF values. - None is returned if all PDF values are zero. - """ - # Find the index of the true neutrino energy bin and the corresponding - # distribution for the reconstructed muon energy. - energy_pdf_hist = deepcopy(self.histogram[true_e_idx, true_dec_idx]) - - # Check whether there is no pdf in the table for this neutrino energy. - if(np.sum(energy_pdf_hist) == 0): - return (None, None) - - # Get the reco energy bin centers. - lower_binedges = self.reco_e_lower_edges[true_e_idx, true_dec_idx] - upper_binedges = self.reco_e_upper_edges[true_e_idx, true_dec_idx] - bin_centers = 0.5 * (lower_binedges + upper_binedges) - - # Convolve the reco energy pdf with the flux model. - energy_pdf_hist *= flux_model.get_integral( - np.power(10, lower_binedges), np.power(10, upper_binedges) - ) - - # Find where the reconstructed energy is below the minimal energy and - # mask those values. We don't have any reco energy below the minimal - # enegry in the data. - mask = bin_centers >= log_e_min - bin_centers = bin_centers[mask] - bin_widths = upper_binedges[mask] - lower_binedges[mask] - energy_pdf_hist = energy_pdf_hist[mask] - - # Re-normalize in case some bins were cut. - energy_pdf_hist /= np.sum(energy_pdf_hist * bin_widths) - - return (energy_pdf_hist, bin_centers) - - def get_total_weighted_energy_pdf( - self, true_dec_idx, true_e_binning, log_e_min=2, order=1, smooth=0): - """Gets the reconstructed muon energy distribution weighted with the - assumed flux model and marginalized over all possible true neutrino - energies for a given true declination bin. The function generates a - spline, and calculates its integral for later normalization. - - Parameters - ---------- - true_dec_idx : int - The index of the true declination bin. - true_e_binning : instance of BinningDefinition - The BinningDefinition instance holding the true energy binning - information. - log_e_min : float - The log10 value of the minimal energy to be considered. - order : int - The order of the spline. - smooth : int - The smooth strength of the spline. - - Returns - ------- - spline : instance of scipy.interpolate.UnivariateSpline - The enegry PDF spline. - norm : float - The integral of the enegry PDF spline. - """ - # Loop over the true energy bins and for each create a spline for the - # reconstructed muon energy pdf. - splines = [] - bin_centers = [] - for true_e_idx in range(true_e_binning.nbins): - (e_pdf, e_pdf_bin_centers) =\ - self.get_weighted_energy_pdf_hist_for_true_energy_dec_bin( - true_e_idx, true_dec_idx, self.flux_model - ) - if(e_pdf is None): - continue - splines.append( - self._create_spline(e_pdf_bin_centers, e_pdf) - ) - bin_centers.append(e_pdf_bin_centers) - - # Build a (non-normalized) spline for the total reconstructed muon - # energy pdf by summing the splines corresponding to each true energy. - # Take as x values for the spline all the bin centers of the single - # reconstructed muon energy pdfs. - spline_x_vals = np.sort( - np.unique( - np.concatenate(bin_centers) - ) - ) - - spline = self._create_spline( - spline_x_vals, - np.sum([spl(spline_x_vals) for spl in splines], axis=0), - order=order, - smooth=smooth - ) - norm = spline.integral( - np.min(spline_x_vals), np.max(spline_x_vals) - ) - - return (spline, norm) - - def calc_prob_for_true_dec_idx(self, true_dec_idx, log_energy, tl=None): - """Calculates the PDF value for the given true declination bin and the - given log10(E_reco) energy values. - """ - (spline, norm) = self.spline_norm_list[true_dec_idx] - with TaskTimer(tl, 'Evaluating logE spline.'): - prob = spline(log_energy) / norm - return prob - - def get_prob(self, tdm, fitparams=None, tl=None): - """Calculates the energy probability (in log10(E)) of each event. - - Parameters - ---------- - tdm : instance of TrialDataManager - The TrialDataManager instance holding the data events for which the - probability should be calculated for. The following data fields must - exist: - - - 'log_energy' : float - The 10-base logarithm of the energy value of the event. - - 'src_array' : (n_sources,)-shaped record array with the follwing - data fields: - - - 'dec' : float - The declination of the source. - fitparams : None - Unused interface parameter. - tl : TimeLord instance | None - The optional TimeLord instance that should be used to measure - timing information. - - Returns - ------- - prob : 1D (N_events,) shaped ndarray - The array with the energy probability for each event. - """ - get_data = tdm.get_data - - src_array = get_data('src_array') - if(len(src_array) != 1): - raise NotImplementedError( - 'The PDF class "{}" is only implemneted for a single ' - 'source! {} sources were defined!'.format( - self.__class__.name, len(src_array))) - - src_dec = get_data('src_array')['dec'][0] - true_dec_binning = self.get_binning('true_dec') - true_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) - - log_energy = get_data('log_energy') - - prob = self.calc_prob_for_true_dec_idx(true_dec_idx, log_energy, tl=tl) - - return prob - - -class PublicDataSignalI3EnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): - """This is the signal energy PDF for IceCube using public data. - It creates a set of PublicDataI3EnergyPDF objects for a discrete set of - energy signal parameters. - """ - - def __init__( - self, - rss, - ds, - flux_model, - fitparam_grid_set, - n_events=int(1e6), - smoothing_filter=None, - ncpu=None, - ppbar=None): - """ - """ - if(isinstance(fitparam_grid_set, ParameterGrid)): - fitparam_grid_set = ParameterGridSet([fitparam_grid_set]) - if(not isinstance(fitparam_grid_set, ParameterGridSet)): - raise TypeError('The fitparam_grid_set argument must be an ' - 'instance of ParameterGrid or ParameterGridSet!') - - if((smoothing_filter is not None) and - (not isinstance(smoothing_filter, SmoothingFilter))): - raise TypeError('The smoothing_filter argument must be None or ' - 'an instance of SmoothingFilter!') - - # We need to extend the fit parameter grids on the lower and upper end - # by one bin to allow for the calculation of the interpolation. But we - # will do this on a copy of the object. - fitparam_grid_set = fitparam_grid_set.copy() - fitparam_grid_set.add_extra_lower_and_upper_bin() - - super().__init__( - pdf_type=I3EnergyPDF, - fitparams_grid_set=fitparam_grid_set, - ncpu=ncpu) - - def create_I3EnergyPDF( - logE_binning, sinDec_binning, smoothing_filter, - aeff, siggen, flux_model, n_events, gridfitparams, rss): - # Create a copy of the FluxModel with the given flux parameters. - # The copy is needed to not interfer with other CPU processes. - my_flux_model = flux_model.copy(newprop=gridfitparams) - - # Generate signal events for sources in every sin(dec) bin. - # The physics weight is the effective area of the event given its - # true energy and true declination. - data_physicsweight = None - events = None - n_evts = int(np.round(n_events / sinDec_binning.nbins)) - for sin_dec in sinDec_binning.bincenters: - src_dec = np.arcsin(sin_dec) - events_ = siggen.generate_signal_events( - rss=rss, - src_dec=src_dec, - src_ra=np.radians(180), - flux_model=my_flux_model, - n_events=n_evts) - data_physicsweight_ = aeff.get_aeff( - np.repeat(sin_dec, len(events_)), - events_['log_true_energy']) - if events is None: - events = events_ - data_physicsweight = data_physicsweight_ - else: - events = np.concatenate( - (events, events_)) - data_physicsweight = np.concatenate( - (data_physicsweight, data_physicsweight_)) - - data_logE = events['log_energy'] - data_sinDec = events['sin_dec'] - data_mcweight = np.ones((len(events),), dtype=np.double) - - epdf = I3EnergyPDF( - data_logE=data_logE, - data_sinDec=data_sinDec, - data_mcweight=data_mcweight, - data_physicsweight=data_physicsweight, - logE_binning=logE_binning, - sinDec_binning=sinDec_binning, - smoothing_filter=smoothing_filter - ) - - return epdf - - print('Generate signal energy PDF for ds {} with {} CPUs'.format( - ds.name, self.ncpu)) - - # Create a signal generator for this dataset. - siggen = PublicDataSignalGenerator(ds) - - aeff = PDAeff( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('eff_area_datafile'))) - - logE_binning = ds.get_binning_definition('log_energy') - sinDec_binning = ds.get_binning_definition('sin_dec') - - args_list = [ - ((logE_binning, sinDec_binning, smoothing_filter, aeff, - siggen, flux_model, n_events, gridfitparams), {}) - for gridfitparams in self.gridfitparams_list - ] - - epdf_list = parallelize( - create_I3EnergyPDF, - args_list, - self.ncpu, - rss=rss, - ppbar=ppbar) - - # Save all the energy PDF objects in the PDFSet PDF registry with - # the hash of the individual parameters as key. - for (gridfitparams, epdf) in zip(self.gridfitparams_list, epdf_list): - self.add_pdf(epdf, gridfitparams) - - def assert_is_valid_for_exp_data(self, data_exp): - pass - - def get_prob(self, tdm, gridfitparams): - """Calculates the signal energy probability (in logE) of each event for - a given set of signal fit parameters on a grid. - - Parameters - ---------- - tdm : instance of TrialDataManager - The TrialDataManager instance holding the data events for which the - probability should be calculated for. The following data fields must - exist: - - - 'log_energy' : float - The logarithm of the energy value of the event. - - 'src_array' : 1d record array - The source record array containing the declination of the - sources. - gridfitparams : dict - The dictionary holding the signal parameter values for which the - signal energy probability should be calculated. Note, that the - parameter values must match a set of parameter grid values for which - a PublicDataSignalI3EnergyPDF object has been created at - construction time of this PublicDataSignalI3EnergyPDFSet object. - There is no interpolation method defined - at this point to allow for arbitrary parameter values! - - Returns - ------- - prob : 1d ndarray - The array with the signal energy probability for each event. - - Raises - ------ - KeyError - If no energy PDF can be found for the given signal parameter values. - """ - epdf = self.get_pdf(gridfitparams) - - prob = epdf.get_prob(tdm) - return prob +# class PublicDataSignalGenerator(object): +# def __init__(self, ds, **kwargs): +# """Creates a new instance of the signal generator for generating signal +# events from the provided public data smearing matrix. +# """ +# super().__init__(**kwargs) + +# self.smearing_matrix = PublicDataSmearingMatrix( +# pathfilenames=ds.get_abs_pathfilename_list( +# ds.get_aux_data_definition('smearing_datafile'))) + +# def _generate_events( +# self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): +# """Generates `n_events` signal events for the given source location +# and flux model. + +# Note: +# Some values can be NaN in cases where a PDF was not available! + +# Parameters +# ---------- +# rss : instance of RandomStateService +# The instance of RandomStateService to use for drawing random +# numbers. +# src_dec : float +# The declination of the source in radians. +# src_ra : float +# The right-ascention of the source in radians. + +# Returns +# ------- +# events : numpy record array of size `n_events` +# The numpy record array holding the event data. +# It contains the following data fields: +# - 'isvalid' +# - 'log_true_energy' +# - 'log_energy' +# - 'sin_dec' +# Single values can be NaN in cases where a pdf was not available. +# """ +# # Create the output event array. +# out_dtype = [ +# ('isvalid', np.bool_), +# ('log_true_energy', np.double), +# ('log_energy', np.double), +# ('sin_dec', np.double) +# ] +# events = np.empty((n_events,), dtype=out_dtype) + +# sm = self.smearing_matrix + +# # Determine the true energy range for which log_e PDFs are available. +# (min_log_true_e, +# max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( +# dec_idx) + +# # First draw a true neutrino energy from the hypothesis spectrum. +# log_true_e = np.log10(flux_model.get_inv_normed_cdf( +# rss.random.uniform(size=n_events), +# E_min=10**min_log_true_e, +# E_max=10**max_log_true_e +# )) + +# events['log_true_energy'] = log_true_e + +# log_true_e_idxs = ( +# np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 +# ) +# # Sample reconstructed energies given true neutrino energies. +# (log_e_idxs, log_e) = sm.sample_log_e( +# rss, dec_idx, log_true_e_idxs) +# events['log_energy'] = log_e + +# # Sample reconstructed psi values given true neutrino energy and +# # reconstructed energy. +# (psi_idxs, psi) = sm.sample_psi( +# rss, dec_idx, log_true_e_idxs, log_e_idxs) + +# # Sample reconstructed ang_err values given true neutrino energy, +# # reconstructed energy, and psi. +# (ang_err_idxs, ang_err) = sm.sample_ang_err( +# rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs) + +# isvalid = np.invert( +# np.isnan(log_e) | np.isnan(psi) | np.isnan(ang_err)) +# events['isvalid'] = isvalid + +# # Convert the psf into a set of (r.a. and dec.). Only use non-nan +# # values. +# (dec, ra) = psi_to_dec_and_ra(rss, src_dec, src_ra, psi[isvalid]) +# events['sin_dec'][isvalid] = np.sin(dec) + +# return events + +# def generate_signal_events( +# self, rss, src_dec, src_ra, flux_model, n_events): +# """Generates ``n_events`` signal events for the given source location +# and flux model. + +# Returns +# ------- +# events : numpy record array +# The numpy record array holding the event data. +# It contains the following data fields: +# - 'isvalid' +# - 'log_energy' +# - 'sin_dec' +# """ +# sm = self.smearing_matrix + +# # Find the declination bin index. +# dec_idx = sm.get_true_dec_idx(src_dec) + +# events = None +# n_evt_generated = 0 +# while n_evt_generated != n_events: +# n_evt = n_events - n_evt_generated + +# events_ = self._generate_events( +# rss, src_dec, src_ra, dec_idx, flux_model, n_evt) + +# # Cut events that failed to be generated due to missing PDFs. +# events_ = events_[events_['isvalid']] + +# n_evt_generated += len(events_) +# if events is None: +# events = events_ +# else: +# events = np.concatenate((events, events_)) + +# return events + + +# class PublicDataSignalI3EnergyPDF(EnergyPDF, IsSignalPDF, UsesBinning): +# """Class that implements the enegry signal PDF for a given flux model given +# the public data. +# """ + +# def __init__(self, ds, flux_model, data_dict=None): +# """Constructs a new enegry PDF instance using the public IceCube data. + +# Parameters +# ---------- +# ds : instance of I3Dataset +# The I3Dataset instance holding the file name of the smearing data of +# the public data. +# flux_model : instance of FluxModel +# The flux model that should be used to calculate the energy signal +# pdf. +# data_dict : dict | None +# If not None, the histogram data and its bin edges can be provided. +# The dictionary needs the following entries: + +# - 'histogram' +# - 'true_e_bin_edges' +# - 'true_dec_bin_edges' +# - 'reco_e_lower_edges' +# - 'reco_e_upper_edges' +# """ +# super().__init__() + +# if(not isinstance(ds, I3Dataset)): +# raise TypeError( +# 'The ds argument must be an instance of I3Dataset!') +# if(not isinstance(flux_model, FluxModel)): +# raise TypeError( +# 'The flux_model argument must be an instance of FluxModel!') + +# self._ds = ds +# self._flux_model = flux_model + +# if(data_dict is None): +# (self.histogram, +# true_e_bin_edges, +# true_dec_bin_edges, +# self.reco_e_lower_edges, +# self.reco_e_upper_edges +# ) = load_smearing_histogram( +# pathfilenames=ds.get_abs_pathfilename_list( +# ds.get_aux_data_definition('smearing_datafile'))) +# else: +# self.histogram = data_dict['histogram'] +# true_e_bin_edges = data_dict['true_e_bin_edges'] +# true_dec_bin_edges = data_dict['true_dec_bin_edges'] +# self.reco_e_lower_edges = data_dict['reco_e_lower_edges'] +# self.reco_e_upper_edges = data_dict['reco_e_upper_edges'] + +# # Get the number of bins for each of the variables in the matrix. +# # The number of bins for e_mu, psf, and ang_err for each true_e and +# # true_dec bin are equal. +# self.add_binning(BinningDefinition('true_e', true_e_bin_edges)) +# self.add_binning(BinningDefinition('true_dec', true_dec_bin_edges)) + +# # Marginalize over the PSF and angular error axes. +# self.histogram = np.sum(self.histogram, axis=(3, 4)) + +# # Create a (prob vs E_reco) spline for each source declination bin. +# n_true_dec = len(true_dec_bin_edges) - 1 +# true_e_binning = self.get_binning('true_e') +# self.spline_norm_list = [] +# for true_dec_idx in range(n_true_dec): +# (spl, norm) = self.get_total_weighted_energy_pdf( +# true_dec_idx, true_e_binning) +# self.spline_norm_list.append((spl, norm)) + +# @property +# def ds(self): +# """(read-only) The I3Dataset instance for which this enegry signal PDF +# was constructed. +# """ +# return self._ds + +# @property +# def flux_model(self): +# """(read-only) The FluxModel instance for which this energy signal PDF +# was constructed. +# """ +# return self._flux_model + +# def _create_spline(self, bin_centers, values, order=1, smooth=0): +# """Creates a :class:`scipy.interpolate.UnivariateSpline` with the +# given order and smoothing factor. +# """ +# spline = UnivariateSpline( +# bin_centers, values, k=order, s=smooth, ext='zeros' +# ) + +# return spline + +# def get_weighted_energy_pdf_hist_for_true_energy_dec_bin( +# self, true_e_idx, true_dec_idx, flux_model, log_e_min=0): +# """Gets the reconstructed muon energy pdf histogram for a specific true +# neutrino energy and declination bin weighted with the assumed flux +# model. + +# Parameters +# ---------- +# true_e_idx : int +# The index of the true enegry bin. +# true_dec_idx : int +# The index of the true declination bin. +# flux_model : instance of FluxModel +# The FluxModel instance that represents the flux formula. +# log_e_min : float +# The minimal reconstructed energy in log10 to be considered for the +# PDF. + +# Returns +# ------- +# energy_pdf_hist : 1d ndarray | None +# The enegry PDF values. +# None is returned if all PDF values are zero. +# bin_centers : 1d ndarray | None +# The bin center values for the energy PDF values. +# None is returned if all PDF values are zero. +# """ +# # Find the index of the true neutrino energy bin and the corresponding +# # distribution for the reconstructed muon energy. +# energy_pdf_hist = deepcopy(self.histogram[true_e_idx, true_dec_idx]) + +# # Check whether there is no pdf in the table for this neutrino energy. +# if(np.sum(energy_pdf_hist) == 0): +# return (None, None) + +# # Get the reco energy bin centers. +# lower_binedges = self.reco_e_lower_edges[true_e_idx, true_dec_idx] +# upper_binedges = self.reco_e_upper_edges[true_e_idx, true_dec_idx] +# bin_centers = 0.5 * (lower_binedges + upper_binedges) + +# # Convolve the reco energy pdf with the flux model. +# energy_pdf_hist *= flux_model.get_integral( +# np.power(10, lower_binedges), np.power(10, upper_binedges) +# ) + +# # Find where the reconstructed energy is below the minimal energy and +# # mask those values. We don't have any reco energy below the minimal +# # enegry in the data. +# mask = bin_centers >= log_e_min +# bin_centers = bin_centers[mask] +# bin_widths = upper_binedges[mask] - lower_binedges[mask] +# energy_pdf_hist = energy_pdf_hist[mask] + +# # Re-normalize in case some bins were cut. +# energy_pdf_hist /= np.sum(energy_pdf_hist * bin_widths) + +# return (energy_pdf_hist, bin_centers) + +# def get_total_weighted_energy_pdf( +# self, true_dec_idx, true_e_binning, log_e_min=2, order=1, smooth=0): +# """Gets the reconstructed muon energy distribution weighted with the +# assumed flux model and marginalized over all possible true neutrino +# energies for a given true declination bin. The function generates a +# spline, and calculates its integral for later normalization. + +# Parameters +# ---------- +# true_dec_idx : int +# The index of the true declination bin. +# true_e_binning : instance of BinningDefinition +# The BinningDefinition instance holding the true energy binning +# information. +# log_e_min : float +# The log10 value of the minimal energy to be considered. +# order : int +# The order of the spline. +# smooth : int +# The smooth strength of the spline. + +# Returns +# ------- +# spline : instance of scipy.interpolate.UnivariateSpline +# The enegry PDF spline. +# norm : float +# The integral of the enegry PDF spline. +# """ +# # Loop over the true energy bins and for each create a spline for the +# # reconstructed muon energy pdf. +# splines = [] +# bin_centers = [] +# for true_e_idx in range(true_e_binning.nbins): +# (e_pdf, e_pdf_bin_centers) =\ +# self.get_weighted_energy_pdf_hist_for_true_energy_dec_bin( +# true_e_idx, true_dec_idx, self.flux_model +# ) +# if(e_pdf is None): +# continue +# splines.append( +# self._create_spline(e_pdf_bin_centers, e_pdf) +# ) +# bin_centers.append(e_pdf_bin_centers) + +# # Build a (non-normalized) spline for the total reconstructed muon +# # energy pdf by summing the splines corresponding to each true energy. +# # Take as x values for the spline all the bin centers of the single +# # reconstructed muon energy pdfs. +# spline_x_vals = np.sort( +# np.unique( +# np.concatenate(bin_centers) +# ) +# ) + +# spline = self._create_spline( +# spline_x_vals, +# np.sum([spl(spline_x_vals) for spl in splines], axis=0), +# order=order, +# smooth=smooth +# ) +# norm = spline.integral( +# np.min(spline_x_vals), np.max(spline_x_vals) +# ) + +# return (spline, norm) + +# def calc_prob_for_true_dec_idx(self, true_dec_idx, log_energy, tl=None): +# """Calculates the PDF value for the given true declination bin and the +# given log10(E_reco) energy values. +# """ +# (spline, norm) = self.spline_norm_list[true_dec_idx] +# with TaskTimer(tl, 'Evaluating logE spline.'): +# prob = spline(log_energy) / norm +# return prob + +# def get_prob(self, tdm, fitparams=None, tl=None): +# """Calculates the energy probability (in log10(E)) of each event. + +# Parameters +# ---------- +# tdm : instance of TrialDataManager +# The TrialDataManager instance holding the data events for which the +# probability should be calculated for. The following data fields must +# exist: + +# - 'log_energy' : float +# The 10-base logarithm of the energy value of the event. +# - 'src_array' : (n_sources,)-shaped record array with the follwing +# data fields: + +# - 'dec' : float +# The declination of the source. +# fitparams : None +# Unused interface parameter. +# tl : TimeLord instance | None +# The optional TimeLord instance that should be used to measure +# timing information. + +# Returns +# ------- +# prob : 1D (N_events,) shaped ndarray +# The array with the energy probability for each event. +# """ +# get_data = tdm.get_data + +# src_array = get_data('src_array') +# if(len(src_array) != 1): +# raise NotImplementedError( +# 'The PDF class "{}" is only implemneted for a single ' +# 'source! {} sources were defined!'.format( +# self.__class__.name, len(src_array))) + +# src_dec = get_data('src_array')['dec'][0] +# true_dec_binning = self.get_binning('true_dec') +# true_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) + +# log_energy = get_data('log_energy') + +# prob = self.calc_prob_for_true_dec_idx(true_dec_idx, log_energy, tl=tl) + +# return prob + + +# class PublicDataSignalI3EnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): +# """This is the signal energy PDF for IceCube using public data. +# It creates a set of PublicDataI3EnergyPDF objects for a discrete set of +# energy signal parameters. +# """ + +# def __init__( +# self, +# rss, +# ds, +# flux_model, +# fitparam_grid_set, +# n_events=int(1e6), +# smoothing_filter=None, +# ncpu=None, +# ppbar=None): +# """ +# """ +# if(isinstance(fitparam_grid_set, ParameterGrid)): +# fitparam_grid_set = ParameterGridSet([fitparam_grid_set]) +# if(not isinstance(fitparam_grid_set, ParameterGridSet)): +# raise TypeError('The fitparam_grid_set argument must be an ' +# 'instance of ParameterGrid or ParameterGridSet!') + +# if((smoothing_filter is not None) and +# (not isinstance(smoothing_filter, SmoothingFilter))): +# raise TypeError('The smoothing_filter argument must be None or ' +# 'an instance of SmoothingFilter!') + +# # We need to extend the fit parameter grids on the lower and upper end +# # by one bin to allow for the calculation of the interpolation. But we +# # will do this on a copy of the object. +# fitparam_grid_set = fitparam_grid_set.copy() +# fitparam_grid_set.add_extra_lower_and_upper_bin() + +# super().__init__( +# pdf_type=I3EnergyPDF, +# fitparams_grid_set=fitparam_grid_set, +# ncpu=ncpu) + +# def create_I3EnergyPDF( +# logE_binning, sinDec_binning, smoothing_filter, +# aeff, siggen, flux_model, n_events, gridfitparams, rss): +# # Create a copy of the FluxModel with the given flux parameters. +# # The copy is needed to not interfer with other CPU processes. +# my_flux_model = flux_model.copy(newprop=gridfitparams) + +# # Generate signal events for sources in every sin(dec) bin. +# # The physics weight is the effective area of the event given its +# # true energy and true declination. +# data_physicsweight = None +# events = None +# n_evts = int(np.round(n_events / sinDec_binning.nbins)) +# for sin_dec in sinDec_binning.bincenters: +# src_dec = np.arcsin(sin_dec) +# events_ = siggen.generate_signal_events( +# rss=rss, +# src_dec=src_dec, +# src_ra=np.radians(180), +# flux_model=my_flux_model, +# n_events=n_evts) +# data_physicsweight_ = aeff.get_aeff( +# np.repeat(sin_dec, len(events_)), +# events_['log_true_energy']) +# if events is None: +# events = events_ +# data_physicsweight = data_physicsweight_ +# else: +# events = np.concatenate( +# (events, events_)) +# data_physicsweight = np.concatenate( +# (data_physicsweight, data_physicsweight_)) + +# data_logE = events['log_energy'] +# data_sinDec = events['sin_dec'] +# data_mcweight = np.ones((len(events),), dtype=np.double) + +# epdf = I3EnergyPDF( +# data_logE=data_logE, +# data_sinDec=data_sinDec, +# data_mcweight=data_mcweight, +# data_physicsweight=data_physicsweight, +# logE_binning=logE_binning, +# sinDec_binning=sinDec_binning, +# smoothing_filter=smoothing_filter +# ) + +# return epdf + +# print('Generate signal energy PDF for ds {} with {} CPUs'.format( +# ds.name, self.ncpu)) + +# # Create a signal generator for this dataset. +# siggen = PublicDataSignalGenerator(ds) + +# aeff = PDAeff( +# pathfilenames=ds.get_abs_pathfilename_list( +# ds.get_aux_data_definition('eff_area_datafile'))) + +# logE_binning = ds.get_binning_definition('log_energy') +# sinDec_binning = ds.get_binning_definition('sin_dec') + +# args_list = [ +# ((logE_binning, sinDec_binning, smoothing_filter, aeff, +# siggen, flux_model, n_events, gridfitparams), {}) +# for gridfitparams in self.gridfitparams_list +# ] + +# epdf_list = parallelize( +# create_I3EnergyPDF, +# args_list, +# self.ncpu, +# rss=rss, +# ppbar=ppbar) + +# # Save all the energy PDF objects in the PDFSet PDF registry with +# # the hash of the individual parameters as key. +# for (gridfitparams, epdf) in zip(self.gridfitparams_list, epdf_list): +# self.add_pdf(epdf, gridfitparams) + +# def assert_is_valid_for_exp_data(self, data_exp): +# pass + +# def get_prob(self, tdm, gridfitparams): +# """Calculates the signal energy probability (in logE) of each event for +# a given set of signal fit parameters on a grid. + +# Parameters +# ---------- +# tdm : instance of TrialDataManager +# The TrialDataManager instance holding the data events for which the +# probability should be calculated for. The following data fields must +# exist: + +# - 'log_energy' : float +# The logarithm of the energy value of the event. +# - 'src_array' : 1d record array +# The source record array containing the declination of the +# sources. +# gridfitparams : dict +# The dictionary holding the signal parameter values for which the +# signal energy probability should be calculated. Note, that the +# parameter values must match a set of parameter grid values for which +# a PublicDataSignalI3EnergyPDF object has been created at +# construction time of this PublicDataSignalI3EnergyPDFSet object. +# There is no interpolation method defined +# at this point to allow for arbitrary parameter values! + +# Returns +# ------- +# prob : 1d ndarray +# The array with the signal energy probability for each event. + +# Raises +# ------ +# KeyError +# If no energy PDF can be found for the given signal parameter values. +# """ +# epdf = self.get_pdf(gridfitparams) + +# prob = epdf.get_prob(tdm) +# return prob class PDSignalEnergyPDF(PDF, IsSignalPDF): From febf59a4f653942c8ae7a8edbffcc2757ca351e5 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 9 Feb 2023 16:07:36 +0100 Subject: [PATCH 188/274] Clean up. --- .../i3/publicdata_ps/backgroundpdf.py | 5 +- .../analyses/i3/publicdata_ps/detsigyield.py | 3 - skyllh/analyses/i3/publicdata_ps/pdfratio.py | 2 - .../i3/publicdata_ps/signal_generator.py | 2 +- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 636 +----------------- 5 files changed, 9 insertions(+), 639 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py index f2113b8206..1ecc394d66 100644 --- a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py @@ -11,7 +11,6 @@ IsBackgroundPDF, PDFAxis ) -from skyllh.core.py import issequenceof from skyllh.core.storage import DataFieldRecordArray from skyllh.core.timing import TaskTimer from skyllh.core.smoothing import ( @@ -64,7 +63,7 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, The smoothing filter to use for smoothing the energy histogram. If None, no smoothing will be applied. kde_smoothing : bool - Apply a kde smoothing to the enrgy pdf for each sine of the + Apply a kde smoothing to the energy pdf for each sine of the muon declination. Default: False. """ @@ -143,8 +142,6 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, # If a bandwidth is passed, apply a KDE-based smoothing with the given # bw parameter as bandwidth for the fit. - # Warning: right now this implies an additional dependency on an - # external package for KDE analysis. if kde_smoothing: if not isinstance(kde_smoothing, bool): raise ValueError( diff --git a/skyllh/analyses/i3/publicdata_ps/detsigyield.py b/skyllh/analyses/i3/publicdata_ps/detsigyield.py index a56e813691..43d1af9c66 100644 --- a/skyllh/analyses/i3/publicdata_ps/detsigyield.py +++ b/skyllh/analyses/i3/publicdata_ps/detsigyield.py @@ -15,9 +15,6 @@ from skyllh.core.detsigyield import ( get_integrated_livetime_in_days ) -from skyllh.core.storage import ( - create_FileLoader -) from skyllh.physics.flux import ( PowerLawFlux, get_conversion_factor_to_internal_flux_unit diff --git a/skyllh/analyses/i3/publicdata_ps/pdfratio.py b/skyllh/analyses/i3/publicdata_ps/pdfratio.py index b8bc7e52ab..e0c259c466 100644 --- a/skyllh/analyses/i3/publicdata_ps/pdfratio.py +++ b/skyllh/analyses/i3/publicdata_ps/pdfratio.py @@ -2,8 +2,6 @@ # Authors: # Dr. Martin Wolf -import sys - import numpy as np from skyllh.core.py import module_classname diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 679f5cafcf..93c9265d5b 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -368,7 +368,7 @@ def sig_gen_list(self): @sig_gen_list.setter def sig_gen_list(self, sig_gen_list): - if(not issequenceof(sig_gen_list, PublicDataDatasetSignalGenerator)): + if(not issequenceof(sig_gen_list, PDDatasetSignalGenerator)): raise TypeError('The sig_gen_list property must be a sequence of ' 'PublicDataDatasetSignalGenerator instances!') diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index da509dee56..222ce67483 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -1,32 +1,17 @@ # -*- coding: utf-8 -*- import numpy as np - -import os -import pickle - -from copy import deepcopy -from scipy import interpolate from scipy import integrate -from scipy.interpolate import UnivariateSpline -from itertools import product from skyllh.core.py import module_classname from skyllh.core.debugging import get_logger from skyllh.core.timing import TaskTimer -from skyllh.core.binning import ( - BinningDefinition, - UsesBinning, - get_bincenters_from_binedges, - get_bin_indices_from_lower_and_upper_binedges -) -from skyllh.core.storage import DataFieldRecordArray +from skyllh.core.binning import get_bincenters_from_binedges from skyllh.core.pdf import ( PDF, PDFAxis, PDFSet, IsSignalPDF, - EnergyPDF ) from skyllh.core.multiproc import ( IsParallelizable, @@ -36,599 +21,16 @@ ParameterGrid, ParameterGridSet ) -from skyllh.core.smoothing import SmoothingFilter -from skyllh.i3.pdf import I3EnergyPDF from skyllh.i3.dataset import I3Dataset from skyllh.physics.flux import FluxModel -from skyllh.analyses.i3.publicdata_ps.pd_aeff import ( - PDAeff, -) +from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff from skyllh.analyses.i3.publicdata_ps.utils import ( FctSpline1D, - create_unionized_smearing_matrix_array, - load_smearing_histogram, - psi_to_dec_and_ra, PublicDataSmearingMatrix, - merge_reco_energy_bins ) - -# class PublicDataSignalGenerator(object): -# def __init__(self, ds, **kwargs): -# """Creates a new instance of the signal generator for generating signal -# events from the provided public data smearing matrix. -# """ -# super().__init__(**kwargs) - -# self.smearing_matrix = PublicDataSmearingMatrix( -# pathfilenames=ds.get_abs_pathfilename_list( -# ds.get_aux_data_definition('smearing_datafile'))) - -# def _generate_events( -# self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): -# """Generates `n_events` signal events for the given source location -# and flux model. - -# Note: -# Some values can be NaN in cases where a PDF was not available! - -# Parameters -# ---------- -# rss : instance of RandomStateService -# The instance of RandomStateService to use for drawing random -# numbers. -# src_dec : float -# The declination of the source in radians. -# src_ra : float -# The right-ascention of the source in radians. - -# Returns -# ------- -# events : numpy record array of size `n_events` -# The numpy record array holding the event data. -# It contains the following data fields: -# - 'isvalid' -# - 'log_true_energy' -# - 'log_energy' -# - 'sin_dec' -# Single values can be NaN in cases where a pdf was not available. -# """ -# # Create the output event array. -# out_dtype = [ -# ('isvalid', np.bool_), -# ('log_true_energy', np.double), -# ('log_energy', np.double), -# ('sin_dec', np.double) -# ] -# events = np.empty((n_events,), dtype=out_dtype) - -# sm = self.smearing_matrix - -# # Determine the true energy range for which log_e PDFs are available. -# (min_log_true_e, -# max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( -# dec_idx) - -# # First draw a true neutrino energy from the hypothesis spectrum. -# log_true_e = np.log10(flux_model.get_inv_normed_cdf( -# rss.random.uniform(size=n_events), -# E_min=10**min_log_true_e, -# E_max=10**max_log_true_e -# )) - -# events['log_true_energy'] = log_true_e - -# log_true_e_idxs = ( -# np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 -# ) -# # Sample reconstructed energies given true neutrino energies. -# (log_e_idxs, log_e) = sm.sample_log_e( -# rss, dec_idx, log_true_e_idxs) -# events['log_energy'] = log_e - -# # Sample reconstructed psi values given true neutrino energy and -# # reconstructed energy. -# (psi_idxs, psi) = sm.sample_psi( -# rss, dec_idx, log_true_e_idxs, log_e_idxs) - -# # Sample reconstructed ang_err values given true neutrino energy, -# # reconstructed energy, and psi. -# (ang_err_idxs, ang_err) = sm.sample_ang_err( -# rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs) - -# isvalid = np.invert( -# np.isnan(log_e) | np.isnan(psi) | np.isnan(ang_err)) -# events['isvalid'] = isvalid - -# # Convert the psf into a set of (r.a. and dec.). Only use non-nan -# # values. -# (dec, ra) = psi_to_dec_and_ra(rss, src_dec, src_ra, psi[isvalid]) -# events['sin_dec'][isvalid] = np.sin(dec) - -# return events - -# def generate_signal_events( -# self, rss, src_dec, src_ra, flux_model, n_events): -# """Generates ``n_events`` signal events for the given source location -# and flux model. - -# Returns -# ------- -# events : numpy record array -# The numpy record array holding the event data. -# It contains the following data fields: -# - 'isvalid' -# - 'log_energy' -# - 'sin_dec' -# """ -# sm = self.smearing_matrix - -# # Find the declination bin index. -# dec_idx = sm.get_true_dec_idx(src_dec) - -# events = None -# n_evt_generated = 0 -# while n_evt_generated != n_events: -# n_evt = n_events - n_evt_generated - -# events_ = self._generate_events( -# rss, src_dec, src_ra, dec_idx, flux_model, n_evt) - -# # Cut events that failed to be generated due to missing PDFs. -# events_ = events_[events_['isvalid']] - -# n_evt_generated += len(events_) -# if events is None: -# events = events_ -# else: -# events = np.concatenate((events, events_)) - -# return events - - -# class PublicDataSignalI3EnergyPDF(EnergyPDF, IsSignalPDF, UsesBinning): -# """Class that implements the enegry signal PDF for a given flux model given -# the public data. -# """ - -# def __init__(self, ds, flux_model, data_dict=None): -# """Constructs a new enegry PDF instance using the public IceCube data. - -# Parameters -# ---------- -# ds : instance of I3Dataset -# The I3Dataset instance holding the file name of the smearing data of -# the public data. -# flux_model : instance of FluxModel -# The flux model that should be used to calculate the energy signal -# pdf. -# data_dict : dict | None -# If not None, the histogram data and its bin edges can be provided. -# The dictionary needs the following entries: - -# - 'histogram' -# - 'true_e_bin_edges' -# - 'true_dec_bin_edges' -# - 'reco_e_lower_edges' -# - 'reco_e_upper_edges' -# """ -# super().__init__() - -# if(not isinstance(ds, I3Dataset)): -# raise TypeError( -# 'The ds argument must be an instance of I3Dataset!') -# if(not isinstance(flux_model, FluxModel)): -# raise TypeError( -# 'The flux_model argument must be an instance of FluxModel!') - -# self._ds = ds -# self._flux_model = flux_model - -# if(data_dict is None): -# (self.histogram, -# true_e_bin_edges, -# true_dec_bin_edges, -# self.reco_e_lower_edges, -# self.reco_e_upper_edges -# ) = load_smearing_histogram( -# pathfilenames=ds.get_abs_pathfilename_list( -# ds.get_aux_data_definition('smearing_datafile'))) -# else: -# self.histogram = data_dict['histogram'] -# true_e_bin_edges = data_dict['true_e_bin_edges'] -# true_dec_bin_edges = data_dict['true_dec_bin_edges'] -# self.reco_e_lower_edges = data_dict['reco_e_lower_edges'] -# self.reco_e_upper_edges = data_dict['reco_e_upper_edges'] - -# # Get the number of bins for each of the variables in the matrix. -# # The number of bins for e_mu, psf, and ang_err for each true_e and -# # true_dec bin are equal. -# self.add_binning(BinningDefinition('true_e', true_e_bin_edges)) -# self.add_binning(BinningDefinition('true_dec', true_dec_bin_edges)) - -# # Marginalize over the PSF and angular error axes. -# self.histogram = np.sum(self.histogram, axis=(3, 4)) - -# # Create a (prob vs E_reco) spline for each source declination bin. -# n_true_dec = len(true_dec_bin_edges) - 1 -# true_e_binning = self.get_binning('true_e') -# self.spline_norm_list = [] -# for true_dec_idx in range(n_true_dec): -# (spl, norm) = self.get_total_weighted_energy_pdf( -# true_dec_idx, true_e_binning) -# self.spline_norm_list.append((spl, norm)) - -# @property -# def ds(self): -# """(read-only) The I3Dataset instance for which this enegry signal PDF -# was constructed. -# """ -# return self._ds - -# @property -# def flux_model(self): -# """(read-only) The FluxModel instance for which this energy signal PDF -# was constructed. -# """ -# return self._flux_model - -# def _create_spline(self, bin_centers, values, order=1, smooth=0): -# """Creates a :class:`scipy.interpolate.UnivariateSpline` with the -# given order and smoothing factor. -# """ -# spline = UnivariateSpline( -# bin_centers, values, k=order, s=smooth, ext='zeros' -# ) - -# return spline - -# def get_weighted_energy_pdf_hist_for_true_energy_dec_bin( -# self, true_e_idx, true_dec_idx, flux_model, log_e_min=0): -# """Gets the reconstructed muon energy pdf histogram for a specific true -# neutrino energy and declination bin weighted with the assumed flux -# model. - -# Parameters -# ---------- -# true_e_idx : int -# The index of the true enegry bin. -# true_dec_idx : int -# The index of the true declination bin. -# flux_model : instance of FluxModel -# The FluxModel instance that represents the flux formula. -# log_e_min : float -# The minimal reconstructed energy in log10 to be considered for the -# PDF. - -# Returns -# ------- -# energy_pdf_hist : 1d ndarray | None -# The enegry PDF values. -# None is returned if all PDF values are zero. -# bin_centers : 1d ndarray | None -# The bin center values for the energy PDF values. -# None is returned if all PDF values are zero. -# """ -# # Find the index of the true neutrino energy bin and the corresponding -# # distribution for the reconstructed muon energy. -# energy_pdf_hist = deepcopy(self.histogram[true_e_idx, true_dec_idx]) - -# # Check whether there is no pdf in the table for this neutrino energy. -# if(np.sum(energy_pdf_hist) == 0): -# return (None, None) - -# # Get the reco energy bin centers. -# lower_binedges = self.reco_e_lower_edges[true_e_idx, true_dec_idx] -# upper_binedges = self.reco_e_upper_edges[true_e_idx, true_dec_idx] -# bin_centers = 0.5 * (lower_binedges + upper_binedges) - -# # Convolve the reco energy pdf with the flux model. -# energy_pdf_hist *= flux_model.get_integral( -# np.power(10, lower_binedges), np.power(10, upper_binedges) -# ) - -# # Find where the reconstructed energy is below the minimal energy and -# # mask those values. We don't have any reco energy below the minimal -# # enegry in the data. -# mask = bin_centers >= log_e_min -# bin_centers = bin_centers[mask] -# bin_widths = upper_binedges[mask] - lower_binedges[mask] -# energy_pdf_hist = energy_pdf_hist[mask] - -# # Re-normalize in case some bins were cut. -# energy_pdf_hist /= np.sum(energy_pdf_hist * bin_widths) - -# return (energy_pdf_hist, bin_centers) - -# def get_total_weighted_energy_pdf( -# self, true_dec_idx, true_e_binning, log_e_min=2, order=1, smooth=0): -# """Gets the reconstructed muon energy distribution weighted with the -# assumed flux model and marginalized over all possible true neutrino -# energies for a given true declination bin. The function generates a -# spline, and calculates its integral for later normalization. - -# Parameters -# ---------- -# true_dec_idx : int -# The index of the true declination bin. -# true_e_binning : instance of BinningDefinition -# The BinningDefinition instance holding the true energy binning -# information. -# log_e_min : float -# The log10 value of the minimal energy to be considered. -# order : int -# The order of the spline. -# smooth : int -# The smooth strength of the spline. - -# Returns -# ------- -# spline : instance of scipy.interpolate.UnivariateSpline -# The enegry PDF spline. -# norm : float -# The integral of the enegry PDF spline. -# """ -# # Loop over the true energy bins and for each create a spline for the -# # reconstructed muon energy pdf. -# splines = [] -# bin_centers = [] -# for true_e_idx in range(true_e_binning.nbins): -# (e_pdf, e_pdf_bin_centers) =\ -# self.get_weighted_energy_pdf_hist_for_true_energy_dec_bin( -# true_e_idx, true_dec_idx, self.flux_model -# ) -# if(e_pdf is None): -# continue -# splines.append( -# self._create_spline(e_pdf_bin_centers, e_pdf) -# ) -# bin_centers.append(e_pdf_bin_centers) - -# # Build a (non-normalized) spline for the total reconstructed muon -# # energy pdf by summing the splines corresponding to each true energy. -# # Take as x values for the spline all the bin centers of the single -# # reconstructed muon energy pdfs. -# spline_x_vals = np.sort( -# np.unique( -# np.concatenate(bin_centers) -# ) -# ) - -# spline = self._create_spline( -# spline_x_vals, -# np.sum([spl(spline_x_vals) for spl in splines], axis=0), -# order=order, -# smooth=smooth -# ) -# norm = spline.integral( -# np.min(spline_x_vals), np.max(spline_x_vals) -# ) - -# return (spline, norm) - -# def calc_prob_for_true_dec_idx(self, true_dec_idx, log_energy, tl=None): -# """Calculates the PDF value for the given true declination bin and the -# given log10(E_reco) energy values. -# """ -# (spline, norm) = self.spline_norm_list[true_dec_idx] -# with TaskTimer(tl, 'Evaluating logE spline.'): -# prob = spline(log_energy) / norm -# return prob - -# def get_prob(self, tdm, fitparams=None, tl=None): -# """Calculates the energy probability (in log10(E)) of each event. - -# Parameters -# ---------- -# tdm : instance of TrialDataManager -# The TrialDataManager instance holding the data events for which the -# probability should be calculated for. The following data fields must -# exist: - -# - 'log_energy' : float -# The 10-base logarithm of the energy value of the event. -# - 'src_array' : (n_sources,)-shaped record array with the follwing -# data fields: - -# - 'dec' : float -# The declination of the source. -# fitparams : None -# Unused interface parameter. -# tl : TimeLord instance | None -# The optional TimeLord instance that should be used to measure -# timing information. - -# Returns -# ------- -# prob : 1D (N_events,) shaped ndarray -# The array with the energy probability for each event. -# """ -# get_data = tdm.get_data - -# src_array = get_data('src_array') -# if(len(src_array) != 1): -# raise NotImplementedError( -# 'The PDF class "{}" is only implemneted for a single ' -# 'source! {} sources were defined!'.format( -# self.__class__.name, len(src_array))) - -# src_dec = get_data('src_array')['dec'][0] -# true_dec_binning = self.get_binning('true_dec') -# true_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) - -# log_energy = get_data('log_energy') - -# prob = self.calc_prob_for_true_dec_idx(true_dec_idx, log_energy, tl=tl) - -# return prob - - -# class PublicDataSignalI3EnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): -# """This is the signal energy PDF for IceCube using public data. -# It creates a set of PublicDataI3EnergyPDF objects for a discrete set of -# energy signal parameters. -# """ - -# def __init__( -# self, -# rss, -# ds, -# flux_model, -# fitparam_grid_set, -# n_events=int(1e6), -# smoothing_filter=None, -# ncpu=None, -# ppbar=None): -# """ -# """ -# if(isinstance(fitparam_grid_set, ParameterGrid)): -# fitparam_grid_set = ParameterGridSet([fitparam_grid_set]) -# if(not isinstance(fitparam_grid_set, ParameterGridSet)): -# raise TypeError('The fitparam_grid_set argument must be an ' -# 'instance of ParameterGrid or ParameterGridSet!') - -# if((smoothing_filter is not None) and -# (not isinstance(smoothing_filter, SmoothingFilter))): -# raise TypeError('The smoothing_filter argument must be None or ' -# 'an instance of SmoothingFilter!') - -# # We need to extend the fit parameter grids on the lower and upper end -# # by one bin to allow for the calculation of the interpolation. But we -# # will do this on a copy of the object. -# fitparam_grid_set = fitparam_grid_set.copy() -# fitparam_grid_set.add_extra_lower_and_upper_bin() - -# super().__init__( -# pdf_type=I3EnergyPDF, -# fitparams_grid_set=fitparam_grid_set, -# ncpu=ncpu) - -# def create_I3EnergyPDF( -# logE_binning, sinDec_binning, smoothing_filter, -# aeff, siggen, flux_model, n_events, gridfitparams, rss): -# # Create a copy of the FluxModel with the given flux parameters. -# # The copy is needed to not interfer with other CPU processes. -# my_flux_model = flux_model.copy(newprop=gridfitparams) - -# # Generate signal events for sources in every sin(dec) bin. -# # The physics weight is the effective area of the event given its -# # true energy and true declination. -# data_physicsweight = None -# events = None -# n_evts = int(np.round(n_events / sinDec_binning.nbins)) -# for sin_dec in sinDec_binning.bincenters: -# src_dec = np.arcsin(sin_dec) -# events_ = siggen.generate_signal_events( -# rss=rss, -# src_dec=src_dec, -# src_ra=np.radians(180), -# flux_model=my_flux_model, -# n_events=n_evts) -# data_physicsweight_ = aeff.get_aeff( -# np.repeat(sin_dec, len(events_)), -# events_['log_true_energy']) -# if events is None: -# events = events_ -# data_physicsweight = data_physicsweight_ -# else: -# events = np.concatenate( -# (events, events_)) -# data_physicsweight = np.concatenate( -# (data_physicsweight, data_physicsweight_)) - -# data_logE = events['log_energy'] -# data_sinDec = events['sin_dec'] -# data_mcweight = np.ones((len(events),), dtype=np.double) - -# epdf = I3EnergyPDF( -# data_logE=data_logE, -# data_sinDec=data_sinDec, -# data_mcweight=data_mcweight, -# data_physicsweight=data_physicsweight, -# logE_binning=logE_binning, -# sinDec_binning=sinDec_binning, -# smoothing_filter=smoothing_filter -# ) - -# return epdf - -# print('Generate signal energy PDF for ds {} with {} CPUs'.format( -# ds.name, self.ncpu)) - -# # Create a signal generator for this dataset. -# siggen = PublicDataSignalGenerator(ds) - -# aeff = PDAeff( -# pathfilenames=ds.get_abs_pathfilename_list( -# ds.get_aux_data_definition('eff_area_datafile'))) - -# logE_binning = ds.get_binning_definition('log_energy') -# sinDec_binning = ds.get_binning_definition('sin_dec') - -# args_list = [ -# ((logE_binning, sinDec_binning, smoothing_filter, aeff, -# siggen, flux_model, n_events, gridfitparams), {}) -# for gridfitparams in self.gridfitparams_list -# ] - -# epdf_list = parallelize( -# create_I3EnergyPDF, -# args_list, -# self.ncpu, -# rss=rss, -# ppbar=ppbar) - -# # Save all the energy PDF objects in the PDFSet PDF registry with -# # the hash of the individual parameters as key. -# for (gridfitparams, epdf) in zip(self.gridfitparams_list, epdf_list): -# self.add_pdf(epdf, gridfitparams) - -# def assert_is_valid_for_exp_data(self, data_exp): -# pass - -# def get_prob(self, tdm, gridfitparams): -# """Calculates the signal energy probability (in logE) of each event for -# a given set of signal fit parameters on a grid. - -# Parameters -# ---------- -# tdm : instance of TrialDataManager -# The TrialDataManager instance holding the data events for which the -# probability should be calculated for. The following data fields must -# exist: - -# - 'log_energy' : float -# The logarithm of the energy value of the event. -# - 'src_array' : 1d record array -# The source record array containing the declination of the -# sources. -# gridfitparams : dict -# The dictionary holding the signal parameter values for which the -# signal energy probability should be calculated. Note, that the -# parameter values must match a set of parameter grid values for which -# a PublicDataSignalI3EnergyPDF object has been created at -# construction time of this PublicDataSignalI3EnergyPDFSet object. -# There is no interpolation method defined -# at this point to allow for arbitrary parameter values! - -# Returns -# ------- -# prob : 1d ndarray -# The array with the signal energy probability for each event. - -# Raises -# ------ -# KeyError -# If no energy PDF can be found for the given signal parameter values. -# """ -# epdf = self.get_pdf(gridfitparams) - -# prob = epdf.get_prob(tdm) -# return prob - - class PDSignalEnergyPDF(PDF, IsSignalPDF): """This class provides a signal energy PDF for a spectrial index value. """ @@ -692,7 +94,7 @@ def get_pd_by_log10_reco_e(self, log10_reco_e, tl=None): The optional TimeLord instance that should be used to measure timing information. """ - # Select events that actually have a signal enegry PDF. + # Select events that actually have a signal energy PDF. # All other events will get zero signal probability density. m = ( (log10_reco_e >= self.log10_reco_e_min) & @@ -805,8 +207,6 @@ def __init__( # Select the slice of the smearing matrix corresponding to the # source declination band. - # Note that we take the pdfs of the reconstruction calculated - # from the smearing matrix here. true_dec_idx = sm.get_true_dec_idx(src_dec) sm_pdf = sm.pdf[:, true_dec_idx] @@ -828,23 +228,6 @@ def __init__( sm.log10_true_enu_binedges[log_true_e_mask][:-1]) ] - # Define the values at which to evaluate the splines. - # Some bins might have zero bin widths. - # m = (sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx] - - # sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx]) > 0 - # le = sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx][m] - # ue = sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx][m] - # min_log10_reco_e = np.min(le) - # max_log10_reco_e = np.max(ue) - # d_log10_reco_e = np.min(ue - le) / 20 - # n_xvals = int((max_log10_reco_e - min_log10_reco_e) / d_log10_reco_e) - # xvals_binedges = np.linspace( - # min_log10_reco_e, - # max_log10_reco_e, - # n_xvals+1 - # ) - # xvals = get_bincenters_from_binedges(xvals_binedges) - xvals_binedges = ds.get_binning_definition('log_energy').binedges xvals = get_bincenters_from_binedges(xvals_binedges) @@ -859,9 +242,8 @@ def __init__( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) - # Calculate the detector's neutrino energy detection probability to - # detect a neutrino of energy E_nu given a neutrino declination: - # p(E_nu|dec) + # Calculate the probability to detect a neutrino of energy + # E_nu given a neutrino declination: p(E_nu|dec). det_prob = aeff.get_detection_prob_for_decnu( decnu=src_dec, enu_min=true_enu_binedges[:-1], @@ -937,9 +319,8 @@ def create_reco_e_pdf_for_true_e(idx, true_e_idx): axis=(-1, -2) ) - # Now build the spline to use it in the sum over the true - # neutrino energy. At this point, add the weight of the pdf - # with the true neutrino energy probability. + # Build the spline for this P(E_reco|E_nu). Weigh the pdf + # with the true neutrino energy probability (flux prob). log10_reco_e_binedges = sm.log10_reco_e_binedges[ true_e_idx, true_dec_idx] @@ -1021,11 +402,8 @@ def get_prob(self, tdm, gridfitparams, tl=None): KeyError If no energy PDF can be found for the given signal parameter values. """ - # print('Getting signal PDF for gridfitparams={}'.format( - # str(gridfitparams))) pdf = self.get_pdf(gridfitparams) (prob, grads) = pdf.get_prob(tdm, tl=tl) return (prob, grads) - From aee77f543126b95860bb0d327283db15e2f044c4 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 10 Feb 2023 10:19:04 +0100 Subject: [PATCH 189/274] Bug fix. --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index ef688cb645..e5e41c627e 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -277,7 +277,7 @@ def create_analysis( cut_sindec = np.sin(np.radians([-2, 0, -3, 0, 0])) if spl_smooth is None: spl_smooth = [0., 0.005, 0.05, 0.2, 0.3] - if len(spl_smooth) != len(datasets) or len(cut_sindec) != len(datasets): + if len(spl_smooth) < len(datasets) or len(cut_sindec) < len(datasets): raise AssertionError("The length of the spl_smooth and of the " "cut_sindec must be equal to the length of datasets: " f"{len(datasets)}.") From 86d2020587832765c4cd9291e92239c8e255816e Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 16 Feb 2023 15:38:10 +0100 Subject: [PATCH 190/274] Added dataset definition for analysis with unbinned MC. --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 6 +- skyllh/datasets/i3/PublicData_10y_ps.py | 44 +- skyllh/datasets/i3/PublicData_10y_ps_wMC.py | 605 ++++++++++++++++++++ 3 files changed, 618 insertions(+), 37 deletions(-) create mode 100644 skyllh/datasets/i3/PublicData_10y_ps_wMC.py diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index e5e41c627e..25dfd9803f 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -131,9 +131,9 @@ def create_analysis( source, refplflux_Phi0=1, refplflux_E0=1e3, - refplflux_gamma=2, - ns_seed=10.0, - gamma_seed=3, + refplflux_gamma=2.0, + ns_seed=100.0, + gamma_seed=3.0, kde_smoothing=False, minimizer_impl="LBFGS", cut_sindec = None, diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index d0e17ddf23..afc637bd16 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -270,7 +270,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC40 = I3Dataset( name = 'IC40', exp_pathfilenames = 'events/IC40_exp.csv', - mc_pathfilenames = 'sim/IC40_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC40_exp.csv', **ds_kwargs ) @@ -296,7 +296,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC59 = I3Dataset( name = 'IC59', exp_pathfilenames = 'events/IC59_exp.csv', - mc_pathfilenames = 'sim/IC59_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC59_exp.csv', **ds_kwargs ) @@ -323,7 +323,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC79 = I3Dataset( name = 'IC79', exp_pathfilenames = 'events/IC79_exp.csv', - mc_pathfilenames = 'sim/IC79_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC79_exp.csv', **ds_kwargs ) @@ -349,7 +349,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_I = I3Dataset( name = 'IC86_I', exp_pathfilenames = 'events/IC86_I_exp.csv', - mc_pathfilenames = 'sim/IC86_I_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC86_I_exp.csv', **ds_kwargs ) @@ -377,7 +377,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_II = I3Dataset( name = 'IC86_II', exp_pathfilenames = 'events/IC86_II_exp.csv', - mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC86_II_exp.csv', **ds_kwargs ) @@ -406,7 +406,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_III = I3Dataset( name = 'IC86_III', exp_pathfilenames = 'events/IC86_III_exp.csv', - mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC86_III_exp.csv', **ds_kwargs ) @@ -427,7 +427,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_IV = I3Dataset( name = 'IC86_IV', exp_pathfilenames = 'events/IC86_IV_exp.csv', - mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC86_IV_exp.csv', **ds_kwargs ) @@ -448,7 +448,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_V = I3Dataset( name = 'IC86_V', exp_pathfilenames = 'events/IC86_V_exp.csv', - mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC86_V_exp.csv', **ds_kwargs ) @@ -469,7 +469,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_VI = I3Dataset( name = 'IC86_VI', exp_pathfilenames = 'events/IC86_VI_exp.csv', - mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC86_VI_exp.csv', **ds_kwargs ) @@ -490,7 +490,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_VII = I3Dataset( name = 'IC86_VII', exp_pathfilenames = 'events/IC86_VII_exp.csv', - mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + mc_pathfilenames = None, grl_pathfilenames = 'uptime/IC86_VII_exp.csv', **ds_kwargs ) @@ -563,31 +563,9 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'Zenith[deg]': 'zen' }) -# dsc.set_mc_field_name_renaming_dict({ -# 'true_dec': 'true_dec', -# 'true_ra': 'true_ra', -# 'true_energy': 'true_energy', -# 'log_energy': 'log_energy', -# 'ra': 'ra', -# 'dec': 'dec', -# 'ang_err': 'ang_err', -# 'mcweight': 'mcweight' -# }) - def add_run_number(data): exp = data.exp - mc = data.mc exp.append_field('run', np.repeat(0, len(exp))) - mc.append_field('run', np.repeat(0, len(mc))) - - def add_time(data): - mc = data.mc - mc.append_field('time', np.repeat(0, len(mc))) - - def add_azimuth_and_zenith(data): - mc = data.mc - mc.append_field('azi', np.repeat(0, len(mc))) - mc.append_field('zen', np.repeat(0, len(mc))) def convert_deg2rad(data): exp = data.exp @@ -598,8 +576,6 @@ def convert_deg2rad(data): exp['zen'] = np.deg2rad(exp['zen']) dsc.add_data_preparation(add_run_number) - dsc.add_data_preparation(add_time) - dsc.add_data_preparation(add_azimuth_and_zenith) dsc.add_data_preparation(convert_deg2rad) return dsc diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py new file mode 100644 index 0000000000..d0e17ddf23 --- /dev/null +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py @@ -0,0 +1,605 @@ +# -*- coding: utf-8 -*- +# Author: Dr. Martin Wolf + +import os.path +import numpy as np + +from skyllh.core.dataset import DatasetCollection +from skyllh.i3.dataset import I3Dataset + + +def create_dataset_collection(base_path=None, sub_path_fmt=None): + """Defines the dataset collection for IceCube's 10-year + point-source public data, which is available at + http://icecube.wisc.edu/data-releases/20210126_PS-IC40-IC86_VII.zip + + Parameters + ---------- + base_path : str | None + The base path of the data files. The actual path of a data file is + assumed to be of the structure //. + If None, use the default path CFG['repository']['base_path']. + sub_path_fmt : str | None + The sub path format of the data files of the public data sample. + If None, use the default sub path format + 'icecube_10year_ps'. + + Returns + ------- + dsc : DatasetCollection + The dataset collection containing all the seasons as individual + I3Dataset objects. + """ + # Define the version of the data sample (collection). + (version, verqualifiers) = (1, dict(p=0)) + + # Define the default sub path format. + default_sub_path_fmt = 'icecube_10year_ps' + + # We create a dataset collection that will hold the individual seasonal + # public data datasets (all of the same version!). + dsc = DatasetCollection('Public Data 10-year point-source') + + dsc.description = """ + The events contained in this release correspond to the IceCube's + time-integrated point source search with 10 years of data [2]. Please refer + to the description of the sample and known changes in the text at [1]. + + The data contained in this release of IceCube’s point source sample shows + evidence of a cumulative excess of events from four sources (NGC 1068, + TXS 0506+056, PKS 1424+240, and GB6 J1542+6129) from a catalogue of 110 + potential sources. NGC 1068 gives the largest excess and is coincidentally + the hottest spot in the full Northern sky search [1]. + + Data from IC86-2012 through IC86-2014 used in [2] use an updated selection + and reconstruction compared to the 7 year time-integrated search [3] and the + detection of the 2014-2015 neutrino flare from the direction of + TXS 0506+056 [4]. The 7 year and 10 year versions of the sample show + overlaps of between 80 and 90%. + + An a posteriori cross check of the updated sample has been performed on + TXS 0506+056 showing two previously-significant cascade-like events removed + in the newer sample. These two events occur near the blazar's position + during the TXS flare and give large reconstructed energies, but are likely + not well-modeled by the track-like reconstructions included in this + selection. While the events are unlikely to be track-like, their + contribution to previous results has been handled properly. + + While the significance of the 2014-2015 TXS 0505+56 flare has decreased from + p=7.0e-5 to 8.1e-3, the change is a result of changes to the sample and not + of increased data. No problems have been identified with the previously + published results and since we have no reason a priori to prefer the new + sample over the old sample, these results do not supercede those in [4]. + + This release contains data beginning in 2008 (IC40) until the spring of 2018 + (IC86-2017). This release duplicates and supplants previously released data + from 2012 and earlier. Events from this release cannot be combined with any + other releases + + ----------------------------------------- + # Experimental data events + ----------------------------------------- + The "events" folder contains the events observed in the 10 year sample of + IceCube's point source neutrino selection. Each file corresponds to a single + season of IceCube datataking, including roughly one year of data. For each + event, reconstructed particle information is included. + + - MJD: The MJD time (ut1) of the event interaction given to 1e-8 days, + corresponding to roughly millisecond precision. + + - log10(E/GeV): The reconstructed energy of a muon passing through the + detector. The reconstruction follows the prescription for unfolding the + given in Section 8 of [5]. + + - AngErr[deg]: The estimated angular uncertainty on the reconstructed + direction given in degrees. The angular uncertainty is assumed to be + symmetric in azimuth and zenith and is used to calculate the signal spatial + probabilities for each event following the procedure given in [6]. The + errors are calibrated using simulated events so that they provide correct + coverage for an E^{-2} power law flux. This sample assumes a lower limit on + the estimated angular uncertainty of 0.2 degrees. + + - RA[deg], Dec[deg]: The right ascension and declination (J2000) + corresponding to the particle's reconstructed origin. Given in degrees. + + - Azimuth[deg], Zenith[deg]: The local coordinates of the particle's + reconstructed origin. + + The local coordinates may be necessary when searching for transient + phenomena on timescales shorter than 1 day due to non-uniformity in the + detector's response as a function of azimuth. In these cases, we recommend + scrambling events in time, then using the local coordinates and time to + calculate new RA and Dec values. + + Note that during the preparation of this data release, one duplicated event + was discovered in the IC86-2015 season. This event has not contributed to + any significant excesses. + + ----------------------------------------- + # Detector uptime + ----------------------------------------- + In order to properly account for detector uptime, IceCube maintains + "good run lists". These contain information about "good runs", periods of + datataking useful for analysis. Data may be marked unusable for various + reasons, including major construction or upgrade work, calibration runs, or + other anomalies. The "uptime" folder contains lists of the good runs for + each season. + + - MJD_start[days], MJD_stop[days]: The start and end times for each good run + + ----------------------------------------- + # Instrument response functions + ----------------------------------------- + In order to best model the response of the IceCube detector to a given + signal, Monte Carlo simulations are produced for each detector + configuration. Events are sampled from these simulations to model the + response of point sources from an arbitrary source and spectrum. + + We provide several binned responses for the detector in the "irfs" folder + of this data release. + + ------------------ + # Effective Areas + ------------------ + The effective area is a property of the detector and selection which, when + convolved with a flux model, gives the expected rate of events in the + detector. Here we release the muon neutrino effective areas for each season + of data. + + The effective areas are averaged over bins using simulated muon neutrino + events ranging from 100 GeV to 100 PeV. Because the response varies widely + in both energy and declination, we provide the tabulated response in these + two dimensions. Due to IceCube's unique position at the south pole, the + effective area is uniform in right ascension for timescales longer than + 1 day. It varies by about 10% as a function of azimuth, an effect which may + be important for shorter timescales. While the azimuthal effective areas are + not included here, they are included in IceCube's internal analyses. + These may be made available upon request. + + Tabulated versions of the effective area are included in csv files in the + "irfs" folder. Plotted versions are included as pdf files in the same + location. Because the detector configuration and selection were unchanged + after the IC86-2012 season, the effective area for this season should be + used for IC86-2012 through IC86-2017. + + - log10(E_nu/GeV)_min, log10(E_nu/GeV)_max: The minimum and maximum of the + energy bin used to caclulate the average effective area. Note that this uses + the neutrino's true energy and not the reconstructed muon energy. + + - Dec_nu_min[deg], Dec_nu_max[deg]: The minimum and maximum of the + declination of the neutrino origin. Again, note that this is the true + direction of the neutrino and not the reconstructed muon direction. + + - A_Eff[cm^2]: The average effective area across a bin. + + ------------------ + # Smearing Matrices + ------------------ + IceCube has a nontrivial smearing matrix with correlations between the + directional uncertainty, the point spread function, and the reconstructed + muon energy. To provide the most complete set of information, we include + tables of these responses for each season from IC40 through IC86-2012. + Seasons after IC86-2012 reuse that season's response functions. + + The included smearing matrices take the form of 5D tables mapping a + (E_nu, Dec_nu) bin in effective area to a 3D matrix of (E, PSF, AngErr). + The contents of each 3D matrix bin give the fractional count of simulated + events within the bin relative to all events in the (E_nu, Dec_nu) bin. + + Fractional_Counts = [Events in (E_nu, Dec_nu, E, PSF, AngErr)] / + [Events in (E_nu, Dec_nu)] + + The simulations statistics, while large enough for direct sampling, are + limited when producing these tables, ranging from just 621,858 simulated + events for IC40 to 11,595,414 simulated events for IC86-2012. In order to + reduce statistical uncertainties in each 5D bin, bins are selected in each + (E_nu, Dec_nu) bin independently. The bin edges are given in the smearing + matrix files. All locations not given have a Fractional_Counts of 0. + + - log10(E_nu/GeV)_min, log10(E_nu/GeV)_max: The minimum and maximum of the + energy bin used to caclulate the average effective area. Note that this uses + the neutrino's true energy and not the reconstructed muon energy. + + - Dec_nu_min[deg], Dec_nu_max[deg]: The minimum and maximum of the + declination of the neutrino origin. Again, note that this is the true + direction of the neutrino and not the reconstructed muon direction. + + - log10(E/GeV): The reconstructed energy of a muon passing through the + detector. The reconstruction follows the prescription for unfolding the + given in Section 8 of [5]. + + - PSF_min[deg], PSF_max[deg]: The minimum and maximum of the true angle + between the neutrino origin and the reconstructed muon direction. + + - AngErr_min[deg], AngErr_max[deg]: The estimated angular uncertainty on the + reconstructed direction given in degrees. The angular uncertainty is assumed + to be symmetric in azimuth and zenith and is used to calculate the signal + spatial probabilities for each event following the procedure given in [6]. + The errors are calibrated so that they provide correct coverage for an + E^{-2} power law flux. This sample assumes a lower limit on the estimated + angular uncertainty of 0.2 degrees. + + - Fractional_Counts: The fraction of simulated events falling within each + 5D bin relative to all events in the (E_nu, Dec_nu) bin. + + ----------------------------------------- + # References + ----------------------------------------- + [1] IceCube Data for Neutrino Point-Source Searches: Years 2008-2018, + [[ArXiv link]] + [2] Time-integrated Neutrino Source Searches with 10 years of IceCube Data, + Phys. Rev. Lett. 124, 051103 (2020) + [3] All-sky search for time-integrated neutrino emission from astrophysical + sources with 7 years of IceCube data, + Astrophys. J., 835 (2017) no. 2, 151 + [4] Neutrino emission from the direction of the blazar TXS 0506+056 prior to + the IceCube-170922A alert, + Science 361, 147-151 (2018) + [5] Energy Reconstruction Methods in the IceCube Neutrino Telescope, + JINST 9 (2014), P03009 + [6] Methods for point source analysis in high energy neutrino telescopes, + Astropart.Phys.29:299-305,2008 + + ----------------------------------------- + # Last Update + ----------------------------------------- + 28 January 2021 + """ + + # Define the common keyword arguments for all data sets. + ds_kwargs = dict( + livetime = None, + version = version, + verqualifiers = verqualifiers, + base_path = base_path, + default_sub_path_fmt = default_sub_path_fmt, + sub_path_fmt = sub_path_fmt + ) + + grl_field_name_renaming_dict = { + 'MJD_start[days]': 'start', + 'MJD_stop[days]': 'stop' + } + + # Define the datasets for the different seasons. + # For the declination and energy binning we use the same binning as was + # used in the original point-source analysis using the PointSourceTracks + # dataset. + + # ---------- IC40 ---------------------------------------------------------- + IC40 = I3Dataset( + name = 'IC40', + exp_pathfilenames = 'events/IC40_exp.csv', + mc_pathfilenames = 'sim/IC40_MC.npy', + grl_pathfilenames = 'uptime/IC40_exp.csv', + **ds_kwargs + ) + IC40.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC40.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC40_effectiveArea.csv') + IC40.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC40_smearing.csv') + IC40.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC40.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.25, 10 + 1), + np.linspace(-0.25, 0.0, 10 + 1), + np.linspace(0.0, 1., 10 + 1), + ])) + IC40.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) + IC40.define_binning('log_energy', energy_bins) + + # ---------- IC59 ---------------------------------------------------------- + IC59 = I3Dataset( + name = 'IC59', + exp_pathfilenames = 'events/IC59_exp.csv', + mc_pathfilenames = 'sim/IC59_MC.npy', + grl_pathfilenames = 'uptime/IC59_exp.csv', + **ds_kwargs + ) + IC59.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC59.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC59_effectiveArea.csv') + IC59.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC59_smearing.csv') + IC59.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC59.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.95, 2 + 1), + np.linspace(-0.95, -0.25, 25 + 1), + np.linspace(-0.25, 0.05, 15 + 1), + np.linspace(0.05, 1., 10 + 1), + ])) + IC59.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) + IC59.define_binning('log_energy', energy_bins) + + # ---------- IC79 ---------------------------------------------------------- + IC79 = I3Dataset( + name = 'IC79', + exp_pathfilenames = 'events/IC79_exp.csv', + mc_pathfilenames = 'sim/IC79_MC.npy', + grl_pathfilenames = 'uptime/IC79_exp.csv', + **ds_kwargs + ) + IC79.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC79.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC79_effectiveArea.csv') + IC79.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC79_smearing.csv') + IC79.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC79.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.75, 10 + 1), + np.linspace(-0.75, 0., 15 + 1), + np.linspace(0., 1., 20 + 1) + ])) + IC79.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) + IC79.define_binning('log_energy', energy_bins) + + # ---------- IC86-I -------------------------------------------------------- + IC86_I = I3Dataset( + name = 'IC86_I', + exp_pathfilenames = 'events/IC86_I_exp.csv', + mc_pathfilenames = 'sim/IC86_I_MC.npy', + grl_pathfilenames = 'uptime/IC86_I_exp.csv', + **ds_kwargs + ) + IC86_I.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_I.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_I_effectiveArea.csv') + IC86_I.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_I_smearing.csv') + IC86_I.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_I.pkl') + + b = np.sin(np.radians(-5.)) # North/South transition boundary. + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.2, 10 + 1), + np.linspace(-0.2, b, 4 + 1), + np.linspace(b, 0.2, 5 + 1), + np.linspace(0.2, 1., 10), + ])) + IC86_I.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + IC86_I.define_binning('log_energy', energy_bins) + + # ---------- IC86-II ------------------------------------------------------- + IC86_II = I3Dataset( + name = 'IC86_II', + exp_pathfilenames = 'events/IC86_II_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + grl_pathfilenames = 'uptime/IC86_II_exp.csv', + **ds_kwargs + ) + IC86_II.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_II.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_II.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_II.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_II.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_II.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.93, 4 + 1), + np.linspace(-0.93, -0.3, 10 + 1), + np.linspace(-0.3, 0.05, 9 + 1), + np.linspace(0.05, 1., 18 + 1), + ])) + IC86_II.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + IC86_II.define_binning('log_energy', energy_bins) + + # ---------- IC86-III ------------------------------------------------------ + IC86_III = I3Dataset( + name = 'IC86_III', + exp_pathfilenames = 'events/IC86_III_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + grl_pathfilenames = 'uptime/IC86_III_exp.csv', + **ds_kwargs + ) + IC86_III.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_III.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_III.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_III.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + + IC86_III.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_III.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-IV ------------------------------------------------------- + IC86_IV = I3Dataset( + name = 'IC86_IV', + exp_pathfilenames = 'events/IC86_IV_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + grl_pathfilenames = 'uptime/IC86_IV_exp.csv', + **ds_kwargs + ) + IC86_IV.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_IV.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_IV.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_IV.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + + IC86_IV.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_IV.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-V -------------------------------------------------------- + IC86_V = I3Dataset( + name = 'IC86_V', + exp_pathfilenames = 'events/IC86_V_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + grl_pathfilenames = 'uptime/IC86_V_exp.csv', + **ds_kwargs + ) + IC86_V.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_V.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_V.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_V.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + + IC86_V.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_V.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-VI ------------------------------------------------------- + IC86_VI = I3Dataset( + name = 'IC86_VI', + exp_pathfilenames = 'events/IC86_VI_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + grl_pathfilenames = 'uptime/IC86_VI_exp.csv', + **ds_kwargs + ) + IC86_VI.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VI.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_VI.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_VI.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + + IC86_VI.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_VI.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-VII ------------------------------------------------------ + IC86_VII = I3Dataset( + name = 'IC86_VII', + exp_pathfilenames = 'events/IC86_VII_exp.csv', + mc_pathfilenames = 'sim/IC86_II-VII_MC.npy', + grl_pathfilenames = 'uptime/IC86_VII_exp.csv', + **ds_kwargs + ) + IC86_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VII.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_VII.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_VII.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + + IC86_VII.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_VII.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-II-VII --------------------------------------------------- + ds_list = [ + IC86_II, + IC86_III, + IC86_IV, + IC86_V, + IC86_VI, + IC86_VII, + ] + IC86_II_VII = I3Dataset( + name = 'IC86_II-VII', + exp_pathfilenames = I3Dataset.get_combined_exp_pathfilenames(ds_list), + mc_pathfilenames = IC86_II.mc_pathfilename_list, + grl_pathfilenames = I3Dataset.get_combined_grl_pathfilenames(ds_list), + **ds_kwargs + ) + IC86_II_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_II_VII.add_aux_data_definition( + 'eff_area_datafile', + IC86_II.get_aux_data_definition('eff_area_datafile')) + + IC86_II_VII.add_aux_data_definition( + 'smearing_datafile', + IC86_II.get_aux_data_definition('smearing_datafile')) + + IC86_II_VII.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_II_VII.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + #--------------------------------------------------------------------------- + + dsc.add_datasets(( + IC40, + IC59, + IC79, + IC86_I, + IC86_II, + IC86_III, + IC86_IV, + IC86_V, + IC86_VI, + IC86_VII, + IC86_II_VII + )) + + dsc.set_exp_field_name_renaming_dict({ + 'MJD[days]': 'time', + 'log10(E/GeV)': 'log_energy', + 'AngErr[deg]': 'ang_err', + 'RA[deg]': 'ra', + 'Dec[deg]': 'dec', + 'Azimuth[deg]': 'azi', + 'Zenith[deg]': 'zen' + }) + +# dsc.set_mc_field_name_renaming_dict({ +# 'true_dec': 'true_dec', +# 'true_ra': 'true_ra', +# 'true_energy': 'true_energy', +# 'log_energy': 'log_energy', +# 'ra': 'ra', +# 'dec': 'dec', +# 'ang_err': 'ang_err', +# 'mcweight': 'mcweight' +# }) + + def add_run_number(data): + exp = data.exp + mc = data.mc + exp.append_field('run', np.repeat(0, len(exp))) + mc.append_field('run', np.repeat(0, len(mc))) + + def add_time(data): + mc = data.mc + mc.append_field('time', np.repeat(0, len(mc))) + + def add_azimuth_and_zenith(data): + mc = data.mc + mc.append_field('azi', np.repeat(0, len(mc))) + mc.append_field('zen', np.repeat(0, len(mc))) + + def convert_deg2rad(data): + exp = data.exp + exp['ang_err'] = np.deg2rad(exp['ang_err']) + exp['ra'] = np.deg2rad(exp['ra']) + exp['dec'] = np.deg2rad(exp['dec']) + exp['azi'] = np.deg2rad(exp['azi']) + exp['zen'] = np.deg2rad(exp['zen']) + + dsc.add_data_preparation(add_run_number) + dsc.add_data_preparation(add_time) + dsc.add_data_preparation(add_azimuth_and_zenith) + dsc.add_data_preparation(convert_deg2rad) + + return dsc From 6a62de9a2dd6c743296921ac779ac55cff68fe2e Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 20 Feb 2023 18:20:21 +0100 Subject: [PATCH 191/274] added keywords for ns_max, gamma_min, and gamma_max in create_analysis --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 25dfd9803f..ce8242c7e3 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -143,7 +143,10 @@ def create_analysis( keep_data_fields=None, optimize_delta_angle=10, tl=None, - ppbar=None + ppbar=None, + gamma_min=1, + gamma_max=5, + ns_max=1e3 ): """Creates the Analysis instance for this particular analysis. @@ -218,11 +221,11 @@ def create_analysis( Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) # Define the fit parameter ns. - fitparam_ns = FitParameter('ns', 0, 1e3, ns_seed) + fitparam_ns = FitParameter('ns', 0, ns_max, ns_seed) # Define the gamma fit parameter. fitparam_gamma = FitParameter( - 'gamma', valmin=1, valmax=5, initial=gamma_seed) + 'gamma', valmin=gamma_min, valmax=gamma_max, initial=gamma_seed) # Define the detector signal efficiency implementation method for the # IceCube detector and this source and flux_model. From 0910e2abd55781218895bb4eccac5060e4163603 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Tue, 21 Feb 2023 10:15:51 +0100 Subject: [PATCH 192/274] added ns_min, ns_max, gamma_min, gamma_max to the docstring in create_analysis --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index ce8242c7e3..971453944c 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -133,7 +133,11 @@ def create_analysis( refplflux_E0=1e3, refplflux_gamma=2.0, ns_seed=100.0, + ns_min=0., + ns_max=1e3, gamma_seed=3.0, + gamma_min=1., + gamma_max=5., kde_smoothing=False, minimizer_impl="LBFGS", cut_sindec = None, @@ -143,10 +147,7 @@ def create_analysis( keep_data_fields=None, optimize_delta_angle=10, tl=None, - ppbar=None, - gamma_min=1, - gamma_max=5, - ns_max=1e3 + ppbar=None ): """Creates the Analysis instance for this particular analysis. @@ -165,9 +166,17 @@ def create_analysis( The spectral index to use for the reference power law flux model. ns_seed : float Value to seed the minimizer with for the ns fit. + ns_min : float + Lower bound for ns fit. + ns_max : float + Upper bound for ns fit. gamma_seed : float | None Value to seed the minimizer with for the gamma fit. If set to None, the refplflux_gamma value will be set as gamma_seed. + gamma_min : float + Lower bound for gamma fit. + gamma_max : float + Upper bound for gamma fit. kde_smoothing : bool Apply a KDE-based smoothing to the data-driven background pdf. Default: False. @@ -221,7 +230,7 @@ def create_analysis( Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) # Define the fit parameter ns. - fitparam_ns = FitParameter('ns', 0, ns_max, ns_seed) + fitparam_ns = FitParameter('ns', ns_min, ns_max, ns_seed) # Define the gamma fit parameter. fitparam_gamma = FitParameter( From dc389cf61aa3fe777568cd419c245fbddf5fc6c5 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Wed, 8 Mar 2023 16:53:35 +0100 Subject: [PATCH 193/274] minor changes for compatibility with newer numpy versions (#123) * minor changes for compatibility with newer numpy versions * bug in previous fix for numpy 1.24 fixed --- skyllh/analyses/i3/publicdata_ps/backgroundpdf.py | 6 +++--- skyllh/core/analysis.py | 2 +- skyllh/core/storage.py | 4 ++-- skyllh/i3/dataset.py | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py index 1ecc394d66..3ee004c7c3 100644 --- a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py @@ -106,7 +106,7 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, logE_binning.binedges, sinDec_binning.binedges], range=[ logE_binning.range, sinDec_binning.range], - normed=False) + density=False) h = self._hist_smoothing_method.smooth(h) self._hist_mask_mc_covered = h > 0 @@ -123,7 +123,7 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, logE_binning.binedges, sinDec_binning.binedges], range=[ logE_binning.range, sinDec_binning.range], - normed=False) + density=False) h = self._hist_smoothing_method.smooth(h) self._hist_mask_mc_covered_zero_physics = h > 0 @@ -138,7 +138,7 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, weights=data_weights, range=[ logE_binning.range, sinDec_binning.range], - normed=False) + density=False) # If a bandwidth is passed, apply a KDE-based smoothing with the given # bw parameter as bandwidth for the fit. diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 8f06aa7ae0..2a124dee42 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -1034,7 +1034,7 @@ def do_trials( result_dtype = result_list[0].dtype result = np.empty(n, dtype=result_dtype) - result[:] = result_list[:] + result[:] = np.array(result_list)[:,0] return result diff --git a/skyllh/core/storage.py b/skyllh/core/storage.py index a6422bad1c..126e63b669 100644 --- a/skyllh/core/storage.py +++ b/skyllh/core/storage.py @@ -502,7 +502,7 @@ def _load_file(self, pathfilename, keep_fields, dtype_convertions, raise ValueError('The data text file "{}" does not contain a ' 'readable table header as first line!'.format(pathfilename)) usecols = None - dtype = [(n,np.float) for n in column_names] + dtype = [(n,np.float64) for n in column_names] if(keep_fields is not None): # Select only the given columns. usecols = [] @@ -510,7 +510,7 @@ def _load_file(self, pathfilename, keep_fields, dtype_convertions, for (idx,name) in enumerate(column_names): if(name in keep_fields): usecols.append(idx) - dtype.append((name,np.float)) + dtype.append((name,np.float64)) usecols = tuple(usecols) if(len(dtype) == 0): raise ValueError('No data columns were selected to be loaded!') diff --git a/skyllh/i3/dataset.py b/skyllh/i3/dataset.py index bd597aa967..a2f625d7ba 100644 --- a/skyllh/i3/dataset.py +++ b/skyllh/i3/dataset.py @@ -347,7 +347,7 @@ def prepare_data(self, data, tl=None): 'detector\'s on-time information in the GRL for dataset '\ '"%s".'%(self.name) with TaskTimer(tl, task): - mask = np.zeros((len(data.exp),), dtype=np.bool) + mask = np.zeros((len(data.exp),), dtype=np.bool_) for (start, stop) in zip(data.grl['start'], data.grl['stop']): mask |= ( From 4065fe20bd04eca16c3fa67b8031a6f78f4a0ad2 Mon Sep 17 00:00:00 2001 From: "Martin Wolf, PhD" Date: Wed, 8 Mar 2023 17:19:26 +0100 Subject: [PATCH 194/274] Fix #108 (#126) --- doc/sphinx/tutorials/kdepdf_mcbg_ps.ipynb | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/doc/sphinx/tutorials/kdepdf_mcbg_ps.ipynb b/doc/sphinx/tutorials/kdepdf_mcbg_ps.ipynb index 887e8c9bf9..616c7f11c4 100644 --- a/doc/sphinx/tutorials/kdepdf_mcbg_ps.ipynb +++ b/doc/sphinx/tutorials/kdepdf_mcbg_ps.ipynb @@ -41,6 +41,23 @@ "## Create `datasets` object" ] }, + { + "cell_type": "markdown", + "id": "12fc366a", + "metadata": {}, + "source": [ + "The ``i3skyllh.analyses.kdepdf_mcbg_ps.analysis`` support datasets where PDFs are pre-generated using the KDE method:\n", + "\n", + " NorthernTracks_v005p00_KDE_PDF_v007: IC86_2011 -- IC86_2019\n", + " NorthernTracks_v005p01_KDE_PDF_v007: IC86_2011 -- IC86_2021 (with additionally added NuTau simulation datasets to MC)\n", + "\n", + "The following analyses should support all datasets as they generate PDFs either from experimental or MC data:\n", + "\n", + " i3skyllh.analyses.trad_diffuse_ps.analysis: uses MC data for PDF generation\n", + " i3skyllh.analyses.trad_stacking.analysis: same as above, with additional support for stacking multiple sources\n", + " i3skyllh.analyses.IC170922A_wGFU.analysis: uses scrambled experimental data for PDF generation" + ] + }, { "cell_type": "code", "execution_count": 2, @@ -716,7 +733,7 @@ ], "metadata": { "kernelspec": { - "display_name": "Python 3 (ipykernel)", + "display_name": "Python 3", "language": "python", "name": "python3" }, From dc58832f1dd2ead58675bb564575ce127aa7db65 Mon Sep 17 00:00:00 2001 From: "Martin Wolf, PhD" Date: Wed, 8 Mar 2023 17:20:40 +0100 Subject: [PATCH 195/274] Fix #108 (#127) From d0daa9e5e787022eb5be09fa1444d28ee1e185e0 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Tue, 14 Mar 2023 19:37:48 +0100 Subject: [PATCH 196/274] implementation of single dataset time dependence --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 292 +++++++++++++++++++- skyllh/core/backgroundpdf.py | 97 ++++++- skyllh/core/pdfratio.py | 24 +- skyllh/core/signalpdf.py | 219 +++++++++++++++ 4 files changed, 629 insertions(+), 3 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 971453944c..70c5b9d0c8 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -47,7 +47,12 @@ from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod # Classes to define the signal and background PDFs. -from skyllh.core.signalpdf import RayleighPSFPointSourceSignalSpatialPDF +from skyllh.core.signalpdf import ( + RayleighPSFPointSourceSignalSpatialPDF, + SignalBoxTimePDF, + SignalGaussTimePDF +) +from skyllh.core.backgroundpdf import BackgroundUniformTimePDF from skyllh.i3.backgroundpdf import ( DataBackgroundI3SpatialPDF ) @@ -55,6 +60,7 @@ # Classes to define the spatial and energy PDF ratios. from skyllh.core.pdfratio import ( SpatialSigOverBkgPDFRatio, + TimeSigOverBkgPDFRatio ) # Analysis utilities. @@ -388,6 +394,290 @@ def create_analysis( return analysis +def create_timedep_analysis( + datasets, + source, + gauss=None, + box=None, + refplflux_Phi0=1, + refplflux_E0=1e3, + refplflux_gamma=2.0, + ns_seed=100.0, + ns_min=0., + ns_max=1e3, + gamma_seed=3.0, + gamma_min=1., + gamma_max=5., + kde_smoothing=False, + minimizer_impl="LBFGS", + cut_sindec = None, + spl_smooth = None, + cap_ratio=False, + compress_data=False, + keep_data_fields=None, + optimize_delta_angle=10, + tl=None, + ppbar=None +): + """Creates the Analysis instance for this particular analysis. + + Parameters: + ----------- + datasets : list of Dataset instances + The list of Dataset instances, which should be used in the + analysis. + source : PointLikeSource instance + The PointLikeSource instance defining the point source position. + gauss : None or dictionary with mu, sigma + None if no Gaussian time pdf. Else dictionary with {"mu": float, "sigma": float} of Gauss + box : None or dictionary with start, end + None if no Box shaped time pdf. Else dictionary with {"start": float, "end": float} of box. + refplflux_Phi0 : float + The flux normalization to use for the reference power law flux model. + refplflux_E0 : float + The reference energy to use for the reference power law flux model. + refplflux_gamma : float + The spectral index to use for the reference power law flux model. + ns_seed : float + Value to seed the minimizer with for the ns fit. + ns_min : float + Lower bound for ns fit. + ns_max : float + Upper bound for ns fit. + gamma_seed : float | None + Value to seed the minimizer with for the gamma fit. If set to None, + the refplflux_gamma value will be set as gamma_seed. + gamma_min : float + Lower bound for gamma fit. + gamma_max : float + Upper bound for gamma fit. + kde_smoothing : bool + Apply a KDE-based smoothing to the data-driven background pdf. + Default: False. + minimizer_impl : str | "LBFGS" + Minimizer implementation to be used. Supported options are "LBFGS" + (L-BFG-S minimizer used from the :mod:`scipy.optimize` module), or + "minuit" (Minuit minimizer used by the :mod:`iminuit` module). + Default: "LBFGS". + cut_sindec : list of float | None + sin(dec) values at which the energy cut in the southern sky should + start. If None, np.sin(np.radians([-2, 0, -3, 0, 0])) is used. + spl_smooth : list of float + Smoothing parameters for the 1D spline for the energy cut. If None, + [0., 0.005, 0.05, 0.2, 0.3] is used. + cap_ratio : bool + If set to True, the energy PDF ratio will be capped to a finite value + where no background energy PDF information is available. This will + ensure that an energy PDF ratio is available for high energies where + no background is available from the experimental data. + If kde_smoothing is set to True, cap_ratio should be set to False! + Default is False. + compress_data : bool + Flag if the data should get converted from float64 into float32. + keep_data_fields : list of str | None + List of additional data field names that should get kept when loading + the data. + optimize_delta_angle : float + The delta angle in degrees for the event selection optimization methods. + tl : TimeLord instance | None + The TimeLord instance to use to time the creation of the analysis. + ppbar : ProgressBar instance | None + The instance of ProgressBar for the optional parent progress bar. + + Returns + ------- + analysis : TimeIntegratedMultiDatasetSingleSourceAnalysis + The Analysis instance for this analysis. + """ + + if gauss is None and box is None: + print("No time pdf specified, will create time integrated analysis") + if gauss is not None and box is not None: + raise ValueError("Time PDF cannot be both Gaussian and box shaped. Please specify only one shape.") + + # Create the minimizer instance. + if minimizer_impl == "LBFGS": + minimizer = Minimizer(LBFGSMinimizerImpl()) + elif minimizer_impl == "minuit": + minimizer = Minimizer(IMinuitMinimizerImpl(ftol=1e-8)) + else: + raise NameError(f"Minimizer implementation `{minimizer_impl}` is not " + "supported. Please use `LBFGS` or `minuit`.") + + # Define the flux model. + flux_model = PowerLawFlux( + Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) + + # Define the fit parameter ns. + fitparam_ns = FitParameter('ns', ns_min, ns_max, ns_seed) + + # Define the gamma fit parameter. + fitparam_gamma = FitParameter( + 'gamma', valmin=gamma_min, valmax=gamma_max, initial=gamma_seed) + + # Define the detector signal efficiency implementation method for the + # IceCube detector and this source and flux_model. + # The sin(dec) binning will be taken by the implementation method + # automatically from the Dataset instance. + gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) + detsigyield_implmethod = \ + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( + gamma_grid) + + # Define the signal generation method. + #sig_gen_method = PointLikeSourceI3SignalGenerationMethod() + sig_gen_method = None + + # Create a source hypothesis group manager. + src_hypo_group_manager = SourceHypoGroupManager( + SourceHypoGroup( + source, flux_model, detsigyield_implmethod, sig_gen_method)) + + # Create a source fit parameter mapper and define the fit parameters. + src_fitparam_mapper = SingleSourceFitParameterMapper() + src_fitparam_mapper.def_fit_parameter(fitparam_gamma) + + # Define the test statistic. + test_statistic = TestStatisticWilks() + + # Define the data scrambler with its data scrambling method, which is used + # for background generation. + data_scrambler = DataScrambler(UniformRAScramblingMethod()) + + # Create background generation method. + bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) + + # Create the Analysis instance. + analysis = Analysis( + src_hypo_group_manager, + src_fitparam_mapper, + fitparam_ns, + test_statistic, + bkg_gen_method, + sig_generator_cls=PDSignalGenerator + ) + + # Define the event selection method for pure optimization purposes. + # We will use the same method for all datasets. + event_selection_method = SpatialBoxEventSelectionMethod( + src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) + #event_selection_method = None + + # Prepare the spline parameters. + if cut_sindec is None: + cut_sindec = np.sin(np.radians([-2, 0, -3, 0, 0])) + if spl_smooth is None: + spl_smooth = [0., 0.005, 0.05, 0.2, 0.3] + if len(spl_smooth) < len(datasets) or len(cut_sindec) < len(datasets): + raise AssertionError("The length of the spl_smooth and of the " + "cut_sindec must be equal to the length of datasets: " + f"{len(datasets)}.") + + # Add the data sets to the analysis. + pbar = ProgressBar(len(datasets), parent=ppbar).start() + energy_cut_splines = [] + for idx,ds in enumerate(datasets): + # Load the data of the data set. + data = ds.load_and_prepare_data( + keep_fields=keep_data_fields, + compress=compress_data, + tl=tl) + + # Create a trial data manager and add the required data fields. + tdm = TrialDataManager() + tdm.add_source_data_field('src_array', + pointlikesource_to_data_field_array) + tdm.add_data_field('psi', psi_func) + + sin_dec_binning = ds.get_binning_definition('sin_dec') + log_energy_binning = ds.get_binning_definition('log_energy') + + # Create the spatial PDF ratio instance for this dataset. + spatial_sigpdf = RayleighPSFPointSourceSignalSpatialPDF( + dec_range=np.arcsin(sin_dec_binning.range)) + spatial_bkgpdf = DataBackgroundI3SpatialPDF( + data.exp, sin_dec_binning) + spatial_pdfratio = SpatialSigOverBkgPDFRatio( + spatial_sigpdf, spatial_bkgpdf) + + # Create the energy PDF ratio instance for this dataset. + energy_sigpdfset = PDSignalEnergyPDFSet( + ds=ds, + src_dec=source.dec, + flux_model=flux_model, + fitparam_grid_set=gamma_grid, + ppbar=ppbar + ) + smoothing_filter = BlockSmoothingFilter(nbins=1) + energy_bkgpdf = PDDataBackgroundI3EnergyPDF( + data.exp, log_energy_binning, sin_dec_binning, + smoothing_filter, kde_smoothing) + + energy_pdfratio = PDPDFRatio( + sig_pdf_set=energy_sigpdfset, + bkg_pdf=energy_bkgpdf, + cap_ratio=cap_ratio + ) + + pdfratios = [spatial_pdfratio, energy_pdfratio] + + # Create the time PDF ratio instance for this dataset. + if gauss is not None or box is not None: + time_bkgpdf = BackgroundUniformTimePDF(data.grl) + if gauss is not None: + time_sigpdf = SignalGaussTimePDF(data.grl, gauss['mu'], gauss['sigma']) + elif box is not None: + time_sigpdf = SignalBoxTimePDF(data.grl, box["start"], box["end"]) + time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + pdfratios.append(time_pdfratio) + + + analysis.add_dataset( + ds, data, pdfratios, tdm, event_selection_method) + + # Create the spline for the declination-dependent energy cut + # that the signal generator needs for injection in the southern sky + + # Some special conditions are needed for IC79 and IC86_I, because + # their experimental dataset shows events that should probably have + # been cut by the IceCube selection. + data_exp = data.exp.copy(keep_fields=['sin_dec', 'log_energy']) + if ds.name == 'IC79': + m = np.invert(np.logical_and( + data_exp['sin_dec']<-0.75, + data_exp['log_energy'] < 4.2)) + data_exp = data_exp[m] + if ds.name == 'IC86_I': + m = np.invert(np.logical_and( + data_exp['sin_dec']<-0.2, + data_exp['log_energy'] < 2.5)) + data_exp = data_exp[m] + + sin_dec_binning = ds.get_binning_definition('sin_dec') + sindec_edges = sin_dec_binning.binedges + min_log_e = np.zeros(len(sindec_edges)-1, dtype=float) + for i in range(len(sindec_edges)-1): + mask = np.logical_and( + data_exp['sin_dec']>=sindec_edges[i], + data_exp['sin_dec'] sample_start: + t_start = sample_start + if t_end > sample_end and t_start < sample_end: + t_end = sample_end + + # values between start and stop times + mask = (t_start <= t_arr) & (t_arr <= t_end) + cdf[mask] = (t_arr[mask] - t_start) / [t_end - t_start] + + # take care of values beyond stop time in sample + if t_end > sample_start: + mask = (t_end < t_arr) + cdf[mask] = 1. + + return cdf + + + def norm_uptime(self, t): + """compute the normalization with the dataset uptime. Distributions like + scipy.stats.norm are normalized (-inf, inf). + These must be re-normalized such that the function sums to 1 over the + finite good run list domain. + + Parameters + ---------- + t : float, ndarray + MJD times + + Returns + ------- + norm : float + Normalization such that cdf sums to 1 over good run list domain + """ + + integral = (self.cdf(self.grl["stop"]) - self.cdf(self.grl["start"])).sum() + + if integral < 1.e-50: + return 0 + + return 1. / integral + + + def get_prob(self, tdm, fitparams=None, tl=None): + """Calculates the signal time probability of each event for the given + set of signal time fit parameter values. + + Parameters + ---------- + tdm : instance of TrialDataManager + The instance of TrialDataManager holding the trial event data for + which to calculate the PDF value. The following data fields must + exist: + + - 'time' : float + The MJD time of the event. + + fitparams : None + Unused interface argument. + + tl : TimeLord instance | None + The optional TimeLord instance to use for measuring timing + information. + + Returns + ------- + prob : array of float + The (N,)-shaped ndarray holding the probability for each event. + grads : empty array of float + Does not depend on fit parameter, so no gradient + """ + + events_time = tdm.get_data('time') + + # Get a mask of the event times which fall inside a detector on-time + # interval. + time = events_time + + # gives 1 for outside the flare and 0 for inside the flare. + inverse_box = np.piecewise(time, [time < self.start, time > self.end], [1., 1.]) + + sample_start, sample_end = min(self.grl["start"]), max(self.grl["stop"]) + t_start, t_end = self.start, self.end + # check if the whole flare lies in this dataset for normalization. + # If one part lies outside, adjust to datasample start or end time. + # For the case where everything lies outside, the pdf will be 0 by definition. + if t_start < sample_start and t_end > sample_start: + t_start = sample_start + if t_end > sample_end and t_start < sample_end: + t_end = sample_end + + grads = np.array([], dtype=np.double) + + return (1. - inverse_box) / (t_start - t_end) * self.norm_uptime(time), grads + + class SignalMultiDimGridPDF(MultiDimGridPDF, IsSignalPDF): From b7b27369d5f182e0824793e5e49f49edff536861 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Wed, 15 Mar 2023 15:07:57 +0100 Subject: [PATCH 197/274] fixed small bug in box pdf --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 1 - skyllh/core/analysis.py | 22 +++++++++++++++++++++ skyllh/core/signalpdf.py | 2 +- 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 70c5b9d0c8..7f4afb5bd9 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -631,7 +631,6 @@ def create_timedep_analysis( time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) pdfratios.append(time_pdfratio) - analysis.add_dataset( ds, data, pdfratios, tdm, event_selection_method) diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 2a124dee42..605ab0d5f2 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -1591,3 +1591,25 @@ def initialize_trial(self, events_list, n_events_list=None, tl=None): store_src_ev_idxs=True, tl=tl) self._llhratio.initialize_for_new_trial(tl=tl) + + +# class TimeDependentMultiDatasetSingleSourceAnalysis(TimeIntegratedMultiDatasetSingleSourceAnalysis): + +# def change_time_pdf(gauss=None, box=None): +# """ changes the time pdf +# Parameters +# ---------- +# gauss : None or dictionary with {"mu": float, "sigma": float} +# box : None or dictionary with {"start": float, "end": float} + +# """ +# if gauss is None and box is None: +# raise TypeError("Either gauss or box have to be specified as time pdf.") + +# # credo this in case the background pdf was not calculated before +# time_bkgpdf = BackgroundUniformTimePDF(self._data_list[0].grl) +# if gauss is not None: +# time_sigpdf = SignalGaussTimePDF(self._data_list[0].grl, gauss['mu'], gauss['sigma']) +# elif box is not None: +# time_sigpdf = SignalBoxTimePDF(self._data_list[0].grl, box["start"], box["end"]) +# time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index 38f78707bc..93ecfec517 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -655,7 +655,7 @@ def get_prob(self, tdm, fitparams=None, tl=None): grads = np.array([], dtype=np.double) - return (1. - inverse_box) / (t_start - t_end) * self.norm_uptime(time), grads + return (1. - inverse_box) / (t_end - t_start) * self.norm_uptime(time), grads From 404a3457f0aac9ec0d08218a1f51061f0a9d43fb Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Wed, 15 Mar 2023 18:17:30 +0100 Subject: [PATCH 198/274] time dependent signal injection --- .../i3/publicdata_ps/signal_generator.py | 114 ++++++++++++++++++ skyllh/analyses/i3/publicdata_ps/trad_ps.py | 14 ++- skyllh/core/analysis.py | 57 ++++++--- 3 files changed, 160 insertions(+), 25 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 93c9265d5b..addfccb44b 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -2,6 +2,7 @@ import numpy as np from scipy import interpolate +from scipy.stats import norm from skyllh.core.py import ( issequenceof, @@ -428,3 +429,116 @@ def generate_signal_events(self, rss, mean, poisson=True): signal_events_dict[ds_idx].append(events_) return tot_n_events, signal_events_dict + + +class PDTimeDependentSignalGenerator(PDSignalGenerator): + """ The time dependent signal generator works so far only for one single dataset. For multi datasets one + needs to adjust the dataset weights accordingly (scaling of the effective area with livetime of the flare + in the dataset) + """ + + def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhratio=None, + energy_cut_splines=None, cut_sindec=None, gauss=None, box=None): + + if gauss is None and box is None: + raise ValueError("Either box or gauss keywords must define the neutrino flare") + if gauss is not None and box is not None: + raise ValueError("Either box or gauss keywords must define the neutrino flare, cannot use both.") + + super().__init__(src_hypo_group_manager, dataset_list, data_list, llhratio, + energy_cut_splines, cut_sindec) + self.box = box + self.gauss = gauss + + + def set_flare(self, gauss=None, box=None): + """ change the flare to something new + + Parameters + ---------- + gauss : None or dictionary with {"mu": float, "sigma": float} + box : None or dictionary with {"start": float, "end": float} + """ + if gauss is None and box is None: + raise ValueError("Either box or gauss keywords must define the neutrino flare") + if gauss is not None and box is not None: + raise ValueError("Either box or gauss keywords must define the neutrino flare, cannot use both.") + + self.box = box + self.gauss = gauss + + + def generate_signal_events(self, rss, mean, poisson=True): + """ same as in PDSignalGenerator, but we assign times here. + """ + shg_list = self._src_hypo_group_manager.src_hypo_group_list + + tot_n_events = 0 + signal_events_dict = {} + + for shg in shg_list: + # This only works with power-laws for now. + # Each source hypo group can have a different power-law + gamma = shg.fluxmodel.gamma + weights, _ = self.llhratio.dataset_signal_weights([mean, gamma]) + for (ds_idx, w) in enumerate(weights): + w_mean = mean * w + if(poisson): + n_events = rss.random.poisson( + float_cast( + w_mean, + '`mean` must be castable to type of float!' + ) + ) + else: + n_events = int_cast( + w_mean, + '`mean` must be castable to type of int!' + ) + tot_n_events += n_events + + events_ = None + for (shg_src_idx, src) in enumerate(shg.source_list): + ds = self._dataset_list[ds_idx] + sig_gen = PDDatasetSignalGenerator( + ds, src.dec, self.effA[ds_idx], self.sm[ds_idx]) + if self.effA[ds_idx] is None: + self.effA[ds_idx] = sig_gen.effA + if self.sm[ds_idx] is None: + self.sm[ds_idx] = sig_gen.smearing_matrix + # ToDo: here n_events should be split according to some + # source weight + events_ = sig_gen.generate_signal_events( + rss, + src.dec, + src.ra, + shg.fluxmodel, + n_events, + energy_cut_spline=self.splines[ds_idx], + cut_sindec=self.cut_sindec[ds_idx] + ) + if events_ is None: + continue + + # Assign times for flare. We can also use inverse transform sampling instead of the + # lazy version implemented here + tmp_grl = self.data_list[ds_idx].grl + for event_index in events_.indices: + while events_._data_fields["time"][event_index] == 1: + if self.gauss is not None: + time = norm(self.mu, self.sigma).rvs() + if self.box is not None: + livetime = self.box["end"] - self.box["start"] + time = rss.random.random() * livetime + time += self.box["start"] + # check if time is in grl + is_in_grl = (tmp_grl["start"] <= time) & (tmp_grl["stop"] >= time) + if np.any(is_in_grl): + events_._data_fields["time"][event_index] = time + + if shg_src_idx == 0: + signal_events_dict[ds_idx] = events_ + else: + signal_events_dict[ds_idx].append(events_) + + return tot_n_events, signal_events_dict \ No newline at end of file diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 7f4afb5bd9..e1d6b0567b 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -39,7 +39,8 @@ # Classes for defining the analysis. from skyllh.core.test_statistic import TestStatisticWilks from skyllh.core.analysis import ( - TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis + TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis, + TimeDependentSingleDatasetSingleSourceAnalysis as TimedepSingleDatasetAnalysis ) # Classes to define the background generation. @@ -80,7 +81,8 @@ # Analysis specific classes for working with the public data. from skyllh.analyses.i3.publicdata_ps.signal_generator import ( - PDSignalGenerator + PDSignalGenerator, + PDTimeDependentSignalGenerator ) from skyllh.analyses.i3.publicdata_ps.detsigyield import ( PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod @@ -491,7 +493,7 @@ def create_timedep_analysis( """ if gauss is None and box is None: - print("No time pdf specified, will create time integrated analysis") + raise ValueError("No time pdf specified (box or gauss)") if gauss is not None and box is not None: raise ValueError("Time PDF cannot be both Gaussian and box shaped. Please specify only one shape.") @@ -548,13 +550,13 @@ def create_timedep_analysis( bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) # Create the Analysis instance. - analysis = Analysis( + analysis = TimedepSingleDatasetAnalysis( src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, bkg_gen_method, - sig_generator_cls=PDSignalGenerator + sig_generator_cls=PDTimeDependentSignalGenerator ) # Define the event selection method for pure optimization purposes. @@ -672,7 +674,7 @@ def create_timedep_analysis( analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) analysis.construct_signal_generator( llhratio=analysis.llhratio, energy_cut_splines=energy_cut_splines, - cut_sindec=cut_sindec) + cut_sindec=cut_sindec, box=box, gauss=gauss) return analysis diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 605ab0d5f2..ff209b25b5 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -1593,23 +1593,42 @@ def initialize_trial(self, events_list, n_events_list=None, tl=None): self._llhratio.initialize_for_new_trial(tl=tl) -# class TimeDependentMultiDatasetSingleSourceAnalysis(TimeIntegratedMultiDatasetSingleSourceAnalysis): - -# def change_time_pdf(gauss=None, box=None): -# """ changes the time pdf -# Parameters -# ---------- -# gauss : None or dictionary with {"mu": float, "sigma": float} -# box : None or dictionary with {"start": float, "end": float} - -# """ -# if gauss is None and box is None: -# raise TypeError("Either gauss or box have to be specified as time pdf.") + +class TimeDependentSingleDatasetSingleSourceAnalysis(TimeIntegratedMultiDatasetSingleSourceAnalysis): + + def __init__(self, src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, + bkg_gen_method=None, sig_generator_cls=None): + + super().__init__(src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, + test_statistic, bkg_gen_method, sig_generator_cls) + + + def change_time_pdf(self, gauss=None, box=None): + """ changes the time pdf + Parameters + ---------- + gauss : None or dictionary with {"mu": float, "sigma": float} + box : None or dictionary with {"start": float, "end": float} + + """ + if gauss is None and box is None: + raise TypeError("Either gauss or box have to be specified as time pdf.") -# # credo this in case the background pdf was not calculated before -# time_bkgpdf = BackgroundUniformTimePDF(self._data_list[0].grl) -# if gauss is not None: -# time_sigpdf = SignalGaussTimePDF(self._data_list[0].grl, gauss['mu'], gauss['sigma']) -# elif box is not None: -# time_sigpdf = SignalBoxTimePDF(self._data_list[0].grl, box["start"], box["end"]) -# time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + grl = self._data_list[0].grl + # redo this in case the background pdf was not calculated before + time_bkgpdf = BackgroundUniformTimePDF(grl) + if gauss is not None: + time_sigpdf = SignalGaussTimePDF(grl, gauss['mu'], gauss['sigma']) + elif box is not None: + time_sigpdf = SignalBoxTimePDF(grl, box["start"], box["end"]) + + time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + + # the next line seems to make no difference in the llh evaluation. We keep it for consistency + self._llhratio.llhratio_list[0].pdfratio_list[2] = time_pdfratio + # this line here is relevant for the llh evaluation + self._llhratio.llhratio_list[0]._pdfratioarray._pdfratio_list[2] = time_pdfratio + + # change detector signal yield with flare livetime in sample (1 / grl_norm in pdf), + # rebuild the histograms if it is changed... + # signal injection? \ No newline at end of file From 478ed4bc0bea64f97765527de486dae3b7afe486 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Wed, 15 Mar 2023 18:17:30 +0100 Subject: [PATCH 199/274] time dependent signal injection --- .../i3/publicdata_ps/signal_generator.py | 114 ++++++++++++++++++ skyllh/analyses/i3/publicdata_ps/trad_ps.py | 14 ++- skyllh/core/analysis.py | 57 ++++++--- 3 files changed, 160 insertions(+), 25 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 93c9265d5b..addfccb44b 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -2,6 +2,7 @@ import numpy as np from scipy import interpolate +from scipy.stats import norm from skyllh.core.py import ( issequenceof, @@ -428,3 +429,116 @@ def generate_signal_events(self, rss, mean, poisson=True): signal_events_dict[ds_idx].append(events_) return tot_n_events, signal_events_dict + + +class PDTimeDependentSignalGenerator(PDSignalGenerator): + """ The time dependent signal generator works so far only for one single dataset. For multi datasets one + needs to adjust the dataset weights accordingly (scaling of the effective area with livetime of the flare + in the dataset) + """ + + def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhratio=None, + energy_cut_splines=None, cut_sindec=None, gauss=None, box=None): + + if gauss is None and box is None: + raise ValueError("Either box or gauss keywords must define the neutrino flare") + if gauss is not None and box is not None: + raise ValueError("Either box or gauss keywords must define the neutrino flare, cannot use both.") + + super().__init__(src_hypo_group_manager, dataset_list, data_list, llhratio, + energy_cut_splines, cut_sindec) + self.box = box + self.gauss = gauss + + + def set_flare(self, gauss=None, box=None): + """ change the flare to something new + + Parameters + ---------- + gauss : None or dictionary with {"mu": float, "sigma": float} + box : None or dictionary with {"start": float, "end": float} + """ + if gauss is None and box is None: + raise ValueError("Either box or gauss keywords must define the neutrino flare") + if gauss is not None and box is not None: + raise ValueError("Either box or gauss keywords must define the neutrino flare, cannot use both.") + + self.box = box + self.gauss = gauss + + + def generate_signal_events(self, rss, mean, poisson=True): + """ same as in PDSignalGenerator, but we assign times here. + """ + shg_list = self._src_hypo_group_manager.src_hypo_group_list + + tot_n_events = 0 + signal_events_dict = {} + + for shg in shg_list: + # This only works with power-laws for now. + # Each source hypo group can have a different power-law + gamma = shg.fluxmodel.gamma + weights, _ = self.llhratio.dataset_signal_weights([mean, gamma]) + for (ds_idx, w) in enumerate(weights): + w_mean = mean * w + if(poisson): + n_events = rss.random.poisson( + float_cast( + w_mean, + '`mean` must be castable to type of float!' + ) + ) + else: + n_events = int_cast( + w_mean, + '`mean` must be castable to type of int!' + ) + tot_n_events += n_events + + events_ = None + for (shg_src_idx, src) in enumerate(shg.source_list): + ds = self._dataset_list[ds_idx] + sig_gen = PDDatasetSignalGenerator( + ds, src.dec, self.effA[ds_idx], self.sm[ds_idx]) + if self.effA[ds_idx] is None: + self.effA[ds_idx] = sig_gen.effA + if self.sm[ds_idx] is None: + self.sm[ds_idx] = sig_gen.smearing_matrix + # ToDo: here n_events should be split according to some + # source weight + events_ = sig_gen.generate_signal_events( + rss, + src.dec, + src.ra, + shg.fluxmodel, + n_events, + energy_cut_spline=self.splines[ds_idx], + cut_sindec=self.cut_sindec[ds_idx] + ) + if events_ is None: + continue + + # Assign times for flare. We can also use inverse transform sampling instead of the + # lazy version implemented here + tmp_grl = self.data_list[ds_idx].grl + for event_index in events_.indices: + while events_._data_fields["time"][event_index] == 1: + if self.gauss is not None: + time = norm(self.mu, self.sigma).rvs() + if self.box is not None: + livetime = self.box["end"] - self.box["start"] + time = rss.random.random() * livetime + time += self.box["start"] + # check if time is in grl + is_in_grl = (tmp_grl["start"] <= time) & (tmp_grl["stop"] >= time) + if np.any(is_in_grl): + events_._data_fields["time"][event_index] = time + + if shg_src_idx == 0: + signal_events_dict[ds_idx] = events_ + else: + signal_events_dict[ds_idx].append(events_) + + return tot_n_events, signal_events_dict \ No newline at end of file diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 7f4afb5bd9..e1d6b0567b 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -39,7 +39,8 @@ # Classes for defining the analysis. from skyllh.core.test_statistic import TestStatisticWilks from skyllh.core.analysis import ( - TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis + TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis, + TimeDependentSingleDatasetSingleSourceAnalysis as TimedepSingleDatasetAnalysis ) # Classes to define the background generation. @@ -80,7 +81,8 @@ # Analysis specific classes for working with the public data. from skyllh.analyses.i3.publicdata_ps.signal_generator import ( - PDSignalGenerator + PDSignalGenerator, + PDTimeDependentSignalGenerator ) from skyllh.analyses.i3.publicdata_ps.detsigyield import ( PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod @@ -491,7 +493,7 @@ def create_timedep_analysis( """ if gauss is None and box is None: - print("No time pdf specified, will create time integrated analysis") + raise ValueError("No time pdf specified (box or gauss)") if gauss is not None and box is not None: raise ValueError("Time PDF cannot be both Gaussian and box shaped. Please specify only one shape.") @@ -548,13 +550,13 @@ def create_timedep_analysis( bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) # Create the Analysis instance. - analysis = Analysis( + analysis = TimedepSingleDatasetAnalysis( src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, bkg_gen_method, - sig_generator_cls=PDSignalGenerator + sig_generator_cls=PDTimeDependentSignalGenerator ) # Define the event selection method for pure optimization purposes. @@ -672,7 +674,7 @@ def create_timedep_analysis( analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) analysis.construct_signal_generator( llhratio=analysis.llhratio, energy_cut_splines=energy_cut_splines, - cut_sindec=cut_sindec) + cut_sindec=cut_sindec, box=box, gauss=gauss) return analysis diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 605ab0d5f2..ff209b25b5 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -1593,23 +1593,42 @@ def initialize_trial(self, events_list, n_events_list=None, tl=None): self._llhratio.initialize_for_new_trial(tl=tl) -# class TimeDependentMultiDatasetSingleSourceAnalysis(TimeIntegratedMultiDatasetSingleSourceAnalysis): - -# def change_time_pdf(gauss=None, box=None): -# """ changes the time pdf -# Parameters -# ---------- -# gauss : None or dictionary with {"mu": float, "sigma": float} -# box : None or dictionary with {"start": float, "end": float} - -# """ -# if gauss is None and box is None: -# raise TypeError("Either gauss or box have to be specified as time pdf.") + +class TimeDependentSingleDatasetSingleSourceAnalysis(TimeIntegratedMultiDatasetSingleSourceAnalysis): + + def __init__(self, src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, + bkg_gen_method=None, sig_generator_cls=None): + + super().__init__(src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, + test_statistic, bkg_gen_method, sig_generator_cls) + + + def change_time_pdf(self, gauss=None, box=None): + """ changes the time pdf + Parameters + ---------- + gauss : None or dictionary with {"mu": float, "sigma": float} + box : None or dictionary with {"start": float, "end": float} + + """ + if gauss is None and box is None: + raise TypeError("Either gauss or box have to be specified as time pdf.") -# # credo this in case the background pdf was not calculated before -# time_bkgpdf = BackgroundUniformTimePDF(self._data_list[0].grl) -# if gauss is not None: -# time_sigpdf = SignalGaussTimePDF(self._data_list[0].grl, gauss['mu'], gauss['sigma']) -# elif box is not None: -# time_sigpdf = SignalBoxTimePDF(self._data_list[0].grl, box["start"], box["end"]) -# time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + grl = self._data_list[0].grl + # redo this in case the background pdf was not calculated before + time_bkgpdf = BackgroundUniformTimePDF(grl) + if gauss is not None: + time_sigpdf = SignalGaussTimePDF(grl, gauss['mu'], gauss['sigma']) + elif box is not None: + time_sigpdf = SignalBoxTimePDF(grl, box["start"], box["end"]) + + time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + + # the next line seems to make no difference in the llh evaluation. We keep it for consistency + self._llhratio.llhratio_list[0].pdfratio_list[2] = time_pdfratio + # this line here is relevant for the llh evaluation + self._llhratio.llhratio_list[0]._pdfratioarray._pdfratio_list[2] = time_pdfratio + + # change detector signal yield with flare livetime in sample (1 / grl_norm in pdf), + # rebuild the histograms if it is changed... + # signal injection? \ No newline at end of file From 8566a69c48e8c9fd9d6f55bfcb1e0b30da43c45f Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Wed, 15 Mar 2023 18:42:14 +0100 Subject: [PATCH 200/274] made signal generation more fool proof --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index addfccb44b..75ac96ec7a 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -476,6 +476,7 @@ def generate_signal_events(self, rss, mean, poisson=True): tot_n_events = 0 signal_events_dict = {} + for shg in shg_list: # This only works with power-laws for now. # Each source hypo group can have a different power-law @@ -526,8 +527,15 @@ def generate_signal_events(self, rss, mean, poisson=True): for event_index in events_.indices: while events_._data_fields["time"][event_index] == 1: if self.gauss is not None: - time = norm(self.mu, self.sigma).rvs() + # make sure flare is in dataset + if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( + self.gauss["mu"] + 4 * self.gauss["sigma"] < tmp_grl["start"][0]): + break # this should never happen + time = norm(self.gauss["mu"], self.gauss["sigma"]).rvs() if self.box is not None: + # make sure flare is in dataset + if (self.box["start"] > tmp_grl["stop"][-1]) or (self.box["end"] < tmp_grl["start"][0]): + break # this should never be the case, since there should no events be generated livetime = self.box["end"] - self.box["start"] time = rss.random.random() * livetime time += self.box["start"] From 4ba9c9ec743c12ec6e9d23ac07fa748d08909490 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Thu, 30 Mar 2023 13:58:46 +0200 Subject: [PATCH 201/274] split trad_ps.py in two files --- .gitignore | 3 + .../i3/publicdata_ps/time_dependent_ps.py | 384 ++++++++++++++++++ .../{trad_ps.py => time_integrated_ps.py} | 283 ------------- 3 files changed, 387 insertions(+), 283 deletions(-) create mode 100644 skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py rename skyllh/analyses/i3/publicdata_ps/{trad_ps.py => time_integrated_ps.py} (60%) diff --git a/.gitignore b/.gitignore index b6e47617de..574915fab8 100644 --- a/.gitignore +++ b/.gitignore @@ -127,3 +127,6 @@ dmypy.json # Pyre type checker .pyre/ + +# testing skript +examples/test_time_analysis.py diff --git a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py new file mode 100644 index 0000000000..bd057dfff2 --- /dev/null +++ b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py @@ -0,0 +1,384 @@ +# -*- coding: utf-8 -*- + +"""Setup the time-dependent analysis. For now this works on a single dataset. +""" + +import argparse +import logging +import numpy as np +from scipy.interpolate import UnivariateSpline + +from skyllh.core.progressbar import ProgressBar + +# Classes to define the source hypothesis. +from skyllh.physics.source import PointLikeSource +from skyllh.physics.flux import PowerLawFlux +from skyllh.core.source_hypo_group import SourceHypoGroup +from skyllh.core.source_hypothesis import SourceHypoGroupManager + +# Classes to define the fit parameters. +from skyllh.core.parameters import ( + SingleSourceFitParameterMapper, + FitParameter +) + +# Classes for the minimizer. +from skyllh.core.minimizer import Minimizer, LBFGSMinimizerImpl +from skyllh.core.minimizers.iminuit import IMinuitMinimizerImpl + +# Classes for utility functionality. +from skyllh.core.config import CFG +from skyllh.core.random import RandomStateService +from skyllh.core.optimize import SpatialBoxEventSelectionMethod +from skyllh.core.smoothing import BlockSmoothingFilter +from skyllh.core.timing import TimeLord +from skyllh.core.trialdata import TrialDataManager + +# Classes for defining the analysis. +from skyllh.core.test_statistic import TestStatisticWilks +from skyllh.core.analysis import ( + TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis, + TimeDependentSingleDatasetSingleSourceAnalysis as TimedepSingleDatasetAnalysis +) + +# Classes to define the background generation. +from skyllh.core.scrambling import DataScrambler, UniformRAScramblingMethod +from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod + +# Classes to define the signal and background PDFs. +from skyllh.core.signalpdf import ( + RayleighPSFPointSourceSignalSpatialPDF, + SignalBoxTimePDF, + SignalGaussTimePDF +) +from skyllh.core.backgroundpdf import BackgroundUniformTimePDF +from skyllh.i3.backgroundpdf import ( + DataBackgroundI3SpatialPDF +) + +# Classes to define the spatial and energy PDF ratios. +from skyllh.core.pdfratio import ( + SpatialSigOverBkgPDFRatio, + TimeSigOverBkgPDFRatio +) + +# Analysis utilities. +from skyllh.core.analysis_utils import ( + pointlikesource_to_data_field_array +) + +# Logging setup utilities. +from skyllh.core.debugging import ( + setup_logger, + setup_console_handler, + setup_file_handler +) + +# Pre-defined public IceCube data samples. +from skyllh.datasets.i3 import data_samples + +# Analysis specific classes for working with the public data. +from skyllh.analyses.i3.publicdata_ps.signal_generator import ( + PDSignalGenerator, + PDTimeDependentSignalGenerator +) +from skyllh.analyses.i3.publicdata_ps.detsigyield import ( + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod +) +from skyllh.analyses.i3.publicdata_ps.signalpdf import ( + PDSignalEnergyPDFSet +) +from skyllh.analyses.i3.publicdata_ps.pdfratio import ( + PDPDFRatio +) +from skyllh.analyses.i3.publicdata_ps.backgroundpdf import ( + PDDataBackgroundI3EnergyPDF +) +from skyllh.analyses.i3.publicdata_ps.time_integrated_ps import ( + psi_func, + TXS_location +) + + +def create_timedep_analysis( + datasets, + source, + gauss=None, + box=None, + refplflux_Phi0=1, + refplflux_E0=1e3, + refplflux_gamma=2.0, + ns_seed=100.0, + ns_min=0., + ns_max=1e3, + gamma_seed=3.0, + gamma_min=1., + gamma_max=5., + kde_smoothing=False, + minimizer_impl="LBFGS", + cut_sindec = None, + spl_smooth = None, + cap_ratio=False, + compress_data=False, + keep_data_fields=None, + optimize_delta_angle=10, + tl=None, + ppbar=None +): + """Creates the Analysis instance for this particular analysis. + + Parameters: + ----------- + datasets : list of Dataset instances + The list of Dataset instances, which should be used in the + analysis. + source : PointLikeSource instance + The PointLikeSource instance defining the point source position. + gauss : None or dictionary with mu, sigma + None if no Gaussian time pdf. Else dictionary with {"mu": float, "sigma": float} of Gauss + box : None or dictionary with start, end + None if no Box shaped time pdf. Else dictionary with {"start": float, "end": float} of box. + refplflux_Phi0 : float + The flux normalization to use for the reference power law flux model. + refplflux_E0 : float + The reference energy to use for the reference power law flux model. + refplflux_gamma : float + The spectral index to use for the reference power law flux model. + ns_seed : float + Value to seed the minimizer with for the ns fit. + ns_min : float + Lower bound for ns fit. + ns_max : float + Upper bound for ns fit. + gamma_seed : float | None + Value to seed the minimizer with for the gamma fit. If set to None, + the refplflux_gamma value will be set as gamma_seed. + gamma_min : float + Lower bound for gamma fit. + gamma_max : float + Upper bound for gamma fit. + kde_smoothing : bool + Apply a KDE-based smoothing to the data-driven background pdf. + Default: False. + minimizer_impl : str | "LBFGS" + Minimizer implementation to be used. Supported options are "LBFGS" + (L-BFG-S minimizer used from the :mod:`scipy.optimize` module), or + "minuit" (Minuit minimizer used by the :mod:`iminuit` module). + Default: "LBFGS". + cut_sindec : list of float | None + sin(dec) values at which the energy cut in the southern sky should + start. If None, np.sin(np.radians([-2, 0, -3, 0, 0])) is used. + spl_smooth : list of float + Smoothing parameters for the 1D spline for the energy cut. If None, + [0., 0.005, 0.05, 0.2, 0.3] is used. + cap_ratio : bool + If set to True, the energy PDF ratio will be capped to a finite value + where no background energy PDF information is available. This will + ensure that an energy PDF ratio is available for high energies where + no background is available from the experimental data. + If kde_smoothing is set to True, cap_ratio should be set to False! + Default is False. + compress_data : bool + Flag if the data should get converted from float64 into float32. + keep_data_fields : list of str | None + List of additional data field names that should get kept when loading + the data. + optimize_delta_angle : float + The delta angle in degrees for the event selection optimization methods. + tl : TimeLord instance | None + The TimeLord instance to use to time the creation of the analysis. + ppbar : ProgressBar instance | None + The instance of ProgressBar for the optional parent progress bar. + + Returns + ------- + analysis : TimeIntegratedMultiDatasetSingleSourceAnalysis + The Analysis instance for this analysis. + """ + + if gauss is None and box is None: + raise ValueError("No time pdf specified (box or gauss)") + if gauss is not None and box is not None: + raise ValueError("Time PDF cannot be both Gaussian and box shaped. Please specify only one shape.") + + # Create the minimizer instance. + if minimizer_impl == "LBFGS": + minimizer = Minimizer(LBFGSMinimizerImpl()) + elif minimizer_impl == "minuit": + minimizer = Minimizer(IMinuitMinimizerImpl(ftol=1e-8)) + else: + raise NameError(f"Minimizer implementation `{minimizer_impl}` is not " + "supported. Please use `LBFGS` or `minuit`.") + + # Define the flux model. + flux_model = PowerLawFlux( + Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) + + # Define the fit parameter ns. + fitparam_ns = FitParameter('ns', ns_min, ns_max, ns_seed) + + # Define the gamma fit parameter. + fitparam_gamma = FitParameter( + 'gamma', valmin=gamma_min, valmax=gamma_max, initial=gamma_seed) + + # Define the detector signal efficiency implementation method for the + # IceCube detector and this source and flux_model. + # The sin(dec) binning will be taken by the implementation method + # automatically from the Dataset instance. + gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) + detsigyield_implmethod = \ + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( + gamma_grid) + + # Define the signal generation method. + #sig_gen_method = PointLikeSourceI3SignalGenerationMethod() + sig_gen_method = None + + # Create a source hypothesis group manager. + src_hypo_group_manager = SourceHypoGroupManager( + SourceHypoGroup( + source, flux_model, detsigyield_implmethod, sig_gen_method)) + + # Create a source fit parameter mapper and define the fit parameters. + src_fitparam_mapper = SingleSourceFitParameterMapper() + src_fitparam_mapper.def_fit_parameter(fitparam_gamma) + + # Define the test statistic. + test_statistic = TestStatisticWilks() + + # Define the data scrambler with its data scrambling method, which is used + # for background generation. + data_scrambler = DataScrambler(UniformRAScramblingMethod()) + + # Create background generation method. + bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) + + # Create the Analysis instance. + analysis = TimedepSingleDatasetAnalysis( + src_hypo_group_manager, + src_fitparam_mapper, + fitparam_ns, + test_statistic, + bkg_gen_method, + sig_generator_cls=PDTimeDependentSignalGenerator + ) + + # Define the event selection method for pure optimization purposes. + # We will use the same method for all datasets. + event_selection_method = SpatialBoxEventSelectionMethod( + src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) + #event_selection_method = None + + # Prepare the spline parameters. + if cut_sindec is None: + cut_sindec = np.sin(np.radians([-2, 0, -3, 0, 0])) + if spl_smooth is None: + spl_smooth = [0., 0.005, 0.05, 0.2, 0.3] + if len(spl_smooth) < len(datasets) or len(cut_sindec) < len(datasets): + raise AssertionError("The length of the spl_smooth and of the " + "cut_sindec must be equal to the length of datasets: " + f"{len(datasets)}.") + + # Add the data sets to the analysis. + pbar = ProgressBar(len(datasets), parent=ppbar).start() + energy_cut_splines = [] + for idx,ds in enumerate(datasets): + # Load the data of the data set. + data = ds.load_and_prepare_data( + keep_fields=keep_data_fields, + compress=compress_data, + tl=tl) + + # Create a trial data manager and add the required data fields. + tdm = TrialDataManager() + tdm.add_source_data_field('src_array', + pointlikesource_to_data_field_array) + tdm.add_data_field('psi', psi_func) + + sin_dec_binning = ds.get_binning_definition('sin_dec') + log_energy_binning = ds.get_binning_definition('log_energy') + + # Create the spatial PDF ratio instance for this dataset. + spatial_sigpdf = RayleighPSFPointSourceSignalSpatialPDF( + dec_range=np.arcsin(sin_dec_binning.range)) + spatial_bkgpdf = DataBackgroundI3SpatialPDF( + data.exp, sin_dec_binning) + spatial_pdfratio = SpatialSigOverBkgPDFRatio( + spatial_sigpdf, spatial_bkgpdf) + + # Create the energy PDF ratio instance for this dataset. + energy_sigpdfset = PDSignalEnergyPDFSet( + ds=ds, + src_dec=source.dec, + flux_model=flux_model, + fitparam_grid_set=gamma_grid, + ppbar=ppbar + ) + smoothing_filter = BlockSmoothingFilter(nbins=1) + energy_bkgpdf = PDDataBackgroundI3EnergyPDF( + data.exp, log_energy_binning, sin_dec_binning, + smoothing_filter, kde_smoothing) + + energy_pdfratio = PDPDFRatio( + sig_pdf_set=energy_sigpdfset, + bkg_pdf=energy_bkgpdf, + cap_ratio=cap_ratio + ) + + pdfratios = [spatial_pdfratio, energy_pdfratio] + + # Create the time PDF ratio instance for this dataset. + if gauss is not None or box is not None: + time_bkgpdf = BackgroundUniformTimePDF(data.grl) + if gauss is not None: + time_sigpdf = SignalGaussTimePDF(data.grl, gauss['mu'], gauss['sigma']) + elif box is not None: + time_sigpdf = SignalBoxTimePDF(data.grl, box["start"], box["end"]) + time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + pdfratios.append(time_pdfratio) + + analysis.add_dataset( + ds, data, pdfratios, tdm, event_selection_method) + + # Create the spline for the declination-dependent energy cut + # that the signal generator needs for injection in the southern sky + + # Some special conditions are needed for IC79 and IC86_I, because + # their experimental dataset shows events that should probably have + # been cut by the IceCube selection. + data_exp = data.exp.copy(keep_fields=['sin_dec', 'log_energy']) + if ds.name == 'IC79': + m = np.invert(np.logical_and( + data_exp['sin_dec']<-0.75, + data_exp['log_energy'] < 4.2)) + data_exp = data_exp[m] + if ds.name == 'IC86_I': + m = np.invert(np.logical_and( + data_exp['sin_dec']<-0.2, + data_exp['log_energy'] < 2.5)) + data_exp = data_exp[m] + + sin_dec_binning = ds.get_binning_definition('sin_dec') + sindec_edges = sin_dec_binning.binedges + min_log_e = np.zeros(len(sindec_edges)-1, dtype=float) + for i in range(len(sindec_edges)-1): + mask = np.logical_and( + data_exp['sin_dec']>=sindec_edges[i], + data_exp['sin_dec']=sindec_edges[i], - data_exp['sin_dec'] Date: Thu, 30 Mar 2023 15:09:03 +0200 Subject: [PATCH 202/274] included expectation maximization in the time dependent analysis --- .../publicdata_ps/expectation_maximization.py | 57 +++++ skyllh/core/analysis.py | 208 +++++++++++++++++- 2 files changed, 263 insertions(+), 2 deletions(-) create mode 100644 skyllh/analyses/i3/publicdata_ps/expectation_maximization.py diff --git a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py new file mode 100644 index 0000000000..7f844f45c9 --- /dev/null +++ b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py @@ -0,0 +1,57 @@ +import numpy as np +from scipy.stats import norm + +def expectation_em(ns, mu, sigma, t, sob): + """ + Expectation step of expectation maximization + + Parameters + ---------- + ns: the number of signal neutrinos, as weight for the gaussian flare + mu: the mean of the gaussian flare + sigma: sigma of gaussian flare + t: [array] times of the events + sob: [array] the signal over background values of events + + Returns + ------- + array, weighted "responsibility" function of each event to belong to the flare + """ + b_term = (1 - np.cos(10 / 180 * np.pi)) / 2 + N = len(t) + e_sig = [] + for i in range(len(ns)): + e_sig.append(norm(loc=mu[i], scale=sigma[i]).pdf(t) * sob * ns[i]) + e_bg = (N - np.sum(ns)) / (np.max(t) - np.min(t)) / b_term # 2198.918456004788 + denom = sum(e_sig) + e_bg + + return [e / denom for e in e_sig], np.sum(np.log(denom)) + + +def maximization_em(e_sig, t): + """ + maximization step of expectation maximization + + Parameters + ---------- + + e_sig: [array] the weights for each event form the expectation step + t: [array] the times of each event + + Returns + ------- + mu (float) : best fit mean + sigma (float) : best fit width + ns (float) : scaling of gaussian + + """ + mu = [] + sigma = [] + ns = [] + for i in range(len(e_sig)): + mu.append(np.average(t, weights=e_sig[i])) + sigma.append(np.sqrt(np.average(np.square(t - mu[i]), weights=e_sig[i]))) + ns.append(np.sum(e_sig[i])) + sigma = [max(1, s) for s in sigma] + + return mu, sigma, ns \ No newline at end of file diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index ff209b25b5..80342925bc 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -55,6 +55,23 @@ ) from skyllh.physics.source import SourceModel +from skyllh.analyses.i3.publicdata_ps.expectation_maximization import ( + expectation_em, + maximization_em +) + +from skyllh.core.signalpdf import ( + SignalBoxTimePDF, + SignalGaussTimePDF +) + +from skyllh.core.backgroundpdf import BackgroundUniformTimePDF + +from skyllh.core.pdfratio import ( + TimeSigOverBkgPDFRatio +) + + logger = get_logger(__name__) @@ -1630,5 +1647,192 @@ def change_time_pdf(self, gauss=None, box=None): self._llhratio.llhratio_list[0]._pdfratioarray._pdfratio_list[2] = time_pdfratio # change detector signal yield with flare livetime in sample (1 / grl_norm in pdf), - # rebuild the histograms if it is changed... - # signal injection? \ No newline at end of file + # rebuild the histograms if it is changed... + # signal injection? + + + def get_energy_spatial_signal_over_backround(self, fitparams): + """ returns the signal over background ratio for + (spatial_signal * energy_signal) / (spatial_background * energy_background) + + Parameter + --------- + analysis : analysis instance + fitparams : dictionary with {"gamma": float} for energy pdf + + Returns + ------- + product of spatial and energy signal over background pdfs + """ + ratio = self._llhratio.llhratio_list[0].pdfratio_list[0].get_ratio(self._tdm_list[0], fitparams) + ratio *= self._llhratio.llhratio_list[0].pdfratio_list[1].get_ratio(self._tdm_list[0], fitparams) + + return ratio + + + def change_fluxmodel_gamma(self, gamma): + """ set new gamma for the flux model + Parameter + --------- + analysis : analysis instance + gamma : spectral index for flux model + """ + + self.src_hypo_group_manager.src_hypo_group_list[0].fluxmodel.gamma = gamma + + + def change_signal_time(self, gauss=None, box=None): + """ change the signal injection to gauss or box + + Parameters + ---------- + analysis : analysis instance + gauss : None or dictionary {"mu": float, "sigma": float} + box : None or dictionary {"start" : float, "end" : float} + """ + self.sig_generator.set_flare(box=box, gauss=gauss) + + + def em_fit(self, fitparams, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, initial_width=5000, + remove_time=None): + """ + run expectation maximization + + Parameters + ---------- + + fitparams : dictionary with value for gamma, e.g. {'gamma': 2} + n : how many gaussians flares we are looking for + tol : the stopping criteria for expectation maximization. This is the difference in the normalized likelihood over the + last 20 iterations + iter_max : the maximum number of iterations, even if stopping criteria tolerance (tol) is not yet reached + sob_thres : set a minimum threshold for signal over background ratios. ratios below this threshold will be removed + initial_width : starting width for the gaussian flare in days + + Returns + ------- + mean flare time, flare width, normalization factor for time pdf + + """ + + ratio = self.get_energy_spatial_signal_over_backround(fitparams) + time = self._tdm_list[0].get_data("time") + + if sob_thresh > 0: # remove events below threshold + for i in range(len(ratio)): + mask = ratio > sob_thresh + ratio[i] = ratio[i][mask] + time[i] = time[i][mask] + + # in case, remove event + if remove_time is not None: + mask = time == remove_time + ratio = ratio[~mask] + time = time[~mask] + + # expectation maximization + mu = np.linspace(self._data_list[0].grl["start"][0], self._data_list[-1].grl["stop"][-1], n+2)[1:-1] + sigma = np.ones(n) * initial_width + ns = np.ones(n) * 10 + llh_diff = 100 + llh_old = 0 + llh_diff_list = [100] * 20 + + iteration = 0 + + while iteration < iter_max and llh_diff > tol: # run until convergence or maximum number of iterations + iteration += 1 + + e, logllh = expectation_em(ns, mu, sigma, time, ratio) + + llh_new = np.sum(logllh) + tmp_diff = np.abs(llh_old - llh_new) / llh_new + llh_diff_list = llh_diff_list[:-1] + llh_diff_list.insert(0, tmp_diff) + llh_diff = np.max(llh_diff_list) + llh_old = llh_new + mu, sigma, ns = maximization_em(e, time) + + return mu, sigma, ns + + + def run_gamma_scan_single_flare(self, remove_time=None, gamma_min=1, gamma_max=5, n_gamma=51): + """ run em for different gammas in the signal energy pdf + + Parameters + ---------- + + remove_time : time information of event that should be removed + gamma_min : lower bound for gamma scan + gamma_max : upper bound for gamma scan + n_gamma : number of steps for gamma scan + + Returns + ------- + array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" + """ + + dtype = [("gamma", "f8"), ("mu", "f8"), ("sigma", "f8"), ("ns_em", "f8")] + results = np.empty(51, dtype=dtype) + + for index, g in enumerate(np.linspace(gamma_min, gamma_max, n_gamma)): + mu, sigma, ns = self.em_fit({"gamma": g}, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, + initial_width=5000, remove_time=remove_time) + results[index] = (g, mu[0], sigma[0], ns[0]) + + return results + + + def calculate_TS(self, em_results, rss): + """ calculate the best TS value for the expectation maximization gamma scan + + Parameters + ---------- + + em_results : + rss : random state service for optimization + + Returns + ------- + float maximized TS value + tuple(gamma from em scan [float], best fit mean time [float], best fit width [float]) + (float ns, float gamma) fitparams from TS optimization + + + """ + max_TS = 0 + best_time = None + best_flux = None + for index, result in enumerate(em_results): + self.change_signal_time(gauss={"mu": em_results["mu"], "sigma": em_results["sigma"]}) + (fitparamset, log_lambda_max, fitparam_values, status) = self.maximize_llhratio(rss) + TS = self.calculate_test_statistic(log_lambda_max, fitparam_values) + if TS > max_TS: + max_TS = TS + best_time = result + best_flux = fitparam_values + + return max_TS, best_time, fitparam_values + + + def unblind_flare(self, remove_time=None): + """ rum EM on unscrambeled data. Similar to the original analysis, remove the alert event. \ + Parameters + ---------- + + remove_time : time of event that should be removed from dataset prior to analysis. In the case of the TXS analysis: remove_time=58018.8711856 + + Returns + ------- + array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" + """ + + # get the original unblinded data + rss = RandomStateService(seed=1) + + self.unblind(rss) + + time_results = self.run_gamma_scan_single_flare(remove_time=remove_time) + + return time_results + From 409a3f26b8b97ee394a42c3d836312a560b22bb1 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 30 Mar 2023 18:38:20 +0200 Subject: [PATCH 203/274] Fixing issues #130, #131 and #132. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 3 +- .../i3/publicdata_ps/pd_smearing_matrix.py | 885 ++++++++++++++ .../i3/publicdata_ps/signal_generator.py | 8 +- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 4 +- skyllh/analyses/i3/publicdata_ps/utils.py | 1088 +---------------- 5 files changed, 895 insertions(+), 1093 deletions(-) create mode 100644 skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index f20aff7738..a42592926e 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -240,7 +240,8 @@ def aeff_decnu_log10enu(self): return self._aeff_decnu_log10enu def create_sin_decnu_log10_enu_spline(self): - """Creates a FctSpline2D object representing a 2D spline of the + """DEPRECATED! + Creates a FctSpline2D object representing a 2D spline of the effective area in sin(dec_nu)-log10(E_nu/GeV)-space. Returns diff --git a/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py b/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py new file mode 100644 index 0000000000..c772523687 --- /dev/null +++ b/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py @@ -0,0 +1,885 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +from skyllh.core.storage import create_FileLoader + +def load_smearing_histogram(pathfilenames): + """Loads the 5D smearing histogram from the given data file. + + Parameters + ---------- + pathfilenames : str | list of str + The file name of the data file. + + Returns + ------- + histogram : 5d ndarray + The 5d histogram array holding the probability values of the smearing + matrix. + The axes are (true_e, true_dec, reco_e, psi, ang_err). + true_e_bin_edges : 1d ndarray + The ndarray holding the bin edges of the true energy axis. + true_dec_bin_edges : 1d ndarray + The ndarray holding the bin edges of the true declination axis in + radians. + reco_e_lower_edges : 3d ndarray + The 3d ndarray holding the lower bin edges of the reco energy axis. + For each pair of true_e and true_dec different reco energy bin edges + are provided. + The shape is (n_true_e, n_true_dec, n_reco_e). + reco_e_upper_edges : 3d ndarray + The 3d ndarray holding the upper bin edges of the reco energy axis. + For each pair of true_e and true_dec different reco energy bin edges + are provided. + The shape is (n_true_e, n_true_dec, n_reco_e). + psi_lower_edges : 4d ndarray + The 4d ndarray holding the lower bin edges of the psi axis in radians. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). + psi_upper_edges : 4d ndarray + The 4d ndarray holding the upper bin edges of the psi axis in radians. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). + ang_err_lower_edges : 5d ndarray + The 5d ndarray holding the lower bin edges of the angular error axis + in radians. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). + ang_err_upper_edges : 5d ndarray + The 5d ndarray holding the upper bin edges of the angular error axis + in radians. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). + """ + # Load the smearing data from the public dataset. + loader = create_FileLoader(pathfilenames=pathfilenames) + data = loader.load_data() + # Rename the data fields. + renaming_dict = { + 'log10(E_nu/GeV)_min': 'true_e_min', + 'log10(E_nu/GeV)_max': 'true_e_max', + 'Dec_nu_min[deg]': 'true_dec_min', + 'Dec_nu_max[deg]': 'true_dec_max', + 'log10(E/GeV)_min': 'e_min', + 'log10(E/GeV)_max': 'e_max', + 'PSF_min[deg]': 'psi_min', + 'PSF_max[deg]': 'psi_max', + 'AngErr_min[deg]': 'ang_err_min', + 'AngErr_max[deg]': 'ang_err_max', + 'Fractional_Counts': 'norm_counts' + } + data.rename_fields(renaming_dict) + + def _get_nbins_from_edges(lower_edges, upper_edges): + """Helper function to extract the number of bins from the data's + bin edges. + """ + n = 0 + # Select only valid rows. + mask = (upper_edges - lower_edges) > 0 + data = lower_edges[mask] + # Go through the valid rows and search for the number of increasing + # bin edge values. + v0 = None + for v in data: + if(v0 is not None and v < v0): + # Reached the end of the edges block. + break + if(v0 is None or v > v0): + v0 = v + n += 1 + return n + + true_e_bin_edges = np.union1d( + data['true_e_min'], data['true_e_max']) + true_dec_bin_edges = np.union1d( + data['true_dec_min'], data['true_dec_max']) + + n_true_e = len(true_e_bin_edges) - 1 + n_true_dec = len(true_dec_bin_edges) - 1 + + n_reco_e = _get_nbins_from_edges( + data['e_min'], data['e_max']) + n_psi = _get_nbins_from_edges( + data['psi_min'], data['psi_max']) + n_ang_err = _get_nbins_from_edges( + data['ang_err_min'], data['ang_err_max']) + + # Get reco energy bin_edges as a 3d array. + idxs = np.array( + range(len(data)) + ) % (n_psi * n_ang_err) == 0 + + reco_e_lower_edges = np.reshape( + data['e_min'][idxs], + (n_true_e, n_true_dec, n_reco_e) + ) + reco_e_upper_edges = np.reshape( + data['e_max'][idxs], + (n_true_e, n_true_dec, n_reco_e) + ) + + # Get psi bin_edges as a 4d array. + idxs = np.array( + range(len(data)) + ) % n_ang_err == 0 + + psi_lower_edges = np.reshape( + data['psi_min'][idxs], + (n_true_e, n_true_dec, n_reco_e, n_psi) + ) + psi_upper_edges = np.reshape( + data['psi_max'][idxs], + (n_true_e, n_true_dec, n_reco_e, n_psi) + ) + + # Get angular error bin_edges as a 5d array. + ang_err_lower_edges = np.reshape( + data['ang_err_min'], + (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err) + ) + ang_err_upper_edges = np.reshape( + data['ang_err_max'], + (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err) + ) + + # Create 5D histogram for the probabilities. + histogram = np.reshape( + data['norm_counts'], + ( + n_true_e, + n_true_dec, + n_reco_e, + n_psi, + n_ang_err + ) + ) + + # Convert degrees into radians. + true_dec_bin_edges = np.radians(true_dec_bin_edges) + psi_lower_edges = np.radians(psi_lower_edges) + psi_upper_edges = np.radians(psi_upper_edges) + ang_err_lower_edges = np.radians(ang_err_lower_edges) + ang_err_upper_edges = np.radians(ang_err_upper_edges) + + return ( + histogram, + true_e_bin_edges, + true_dec_bin_edges, + reco_e_lower_edges, + reco_e_upper_edges, + psi_lower_edges, + psi_upper_edges, + ang_err_lower_edges, + ang_err_upper_edges + ) + + +class PublicDataSmearingMatrix(object): + """This class is a helper class for dealing with the smearing matrix + provided by the public data. + """ + def __init__( + self, pathfilenames, **kwargs): + """Creates a smearing matrix instance by loading the smearing matrix + from the given file. + """ + super().__init__(**kwargs) + + ( + self.histogram, + self._true_e_bin_edges, + self._true_dec_bin_edges, + self.reco_e_lower_edges, + self.reco_e_upper_edges, + self.psi_lower_edges, + self.psi_upper_edges, + self.ang_err_lower_edges, + self.ang_err_upper_edges + ) = load_smearing_histogram(pathfilenames) + + self.n_psi_bins = self.histogram.shape[3] + self.n_ang_err_bins = self.histogram.shape[4] + + # Create bin edges array for log10_reco_e. + s = np.array(self.reco_e_lower_edges.shape) + s[-1] += 1 + self.log10_reco_e_binedges = np.empty(s, dtype=np.double) + self.log10_reco_e_binedges[...,:-1] = self.reco_e_lower_edges + self.log10_reco_e_binedges[...,-1] = self.reco_e_upper_edges[...,-1] + + # Create bin edges array for psi. + s = np.array(self.psi_lower_edges.shape) + s[-1] += 1 + self.psi_binedges = np.empty(s, dtype=np.double) + self.psi_binedges[...,:-1] = self.psi_lower_edges + self.psi_binedges[...,-1] = self.psi_upper_edges[...,-1] + + # Create bin edges array for ang_err. + s = np.array(self.ang_err_lower_edges.shape) + s[-1] += 1 + self.ang_err_binedges = np.empty(s, dtype=np.double) + self.ang_err_binedges[...,:-1] = self.ang_err_lower_edges + self.ang_err_binedges[...,-1] = self.ang_err_upper_edges[...,-1] + + @property + def n_log10_true_e_bins(self): + """(read-only) The number of log10 true energy bins. + """ + return len(self._true_e_bin_edges) - 1 + + @property + def true_e_bin_edges(self): + """(read-only) The (n_true_e+1,)-shaped 1D numpy ndarray holding the + bin edges of the true energy. + + Depricated! Use log10_true_enu_binedges instead! + """ + return self._true_e_bin_edges + + @property + def true_e_bin_centers(self): + """(read-only) The (n_true_e,)-shaped 1D numpy ndarray holding the bin + center values of the true energy. + """ + return 0.5*(self._true_e_bin_edges[:-1] + + self._true_e_bin_edges[1:]) + + @property + def log10_true_enu_binedges(self): + """(read-only) The (n_log10_true_enu+1,)-shaped 1D numpy ndarray holding + the bin edges of the log10 true neutrino energy. + """ + return self._true_e_bin_edges + + @property + def n_true_dec_bins(self): + """(read-only) The number of true declination bins. + """ + return len(self._true_dec_bin_edges) - 1 + + @property + def true_dec_bin_edges(self): + """(read-only) The (n_true_dec+1,)-shaped 1D numpy ndarray holding the + bin edges of the true declination. + """ + return self._true_dec_bin_edges + + @property + def true_dec_bin_centers(self): + """(read-only) The (n_true_dec,)-shaped 1D ndarray holding the bin + center values of the true declination. + """ + return 0.5*(self._true_dec_bin_edges[:-1] + + self._true_dec_bin_edges[1:]) + + @property + def log10_reco_e_binedges_lower(self): + """(read-only) The upper bin edges of the log10 reco energy axes. + """ + return self.reco_e_lower_edges + + @property + def log10_reco_e_binedges_upper(self): + """(read-only) The upper bin edges of the log10 reco energy axes. + """ + return self.reco_e_upper_edges + + @property + def min_log10_reco_e(self): + """(read-only) The minimum value of the reconstructed energy axis. + """ + # Select only valid reco energy bins with bin widths greater than zero. + m = (self.reco_e_upper_edges - self.reco_e_lower_edges) > 0 + return np.min(self.reco_e_lower_edges[m]) + + @property + def max_log10_reco_e(self): + """(read-only) The maximum value of the reconstructed energy axis. + """ + # Select only valid reco energy bins with bin widths greater than zero. + m = (self.reco_e_upper_edges - self.reco_e_lower_edges) > 0 + return np.max(self.reco_e_upper_edges[m]) + + @property + def min_log10_psi(self): + """(read-only) The minimum log10 value of the psi axis. + """ + # Select only valid psi bins with bin widths greater than zero. + m = (self.psi_upper_edges - self.psi_lower_edges) > 0 + return np.min(np.log10(self.psi_lower_edges[m])) + + @property + def max_log10_psi(self): + """(read-only) The maximum log10 value of the psi axis. + """ + # Select only valid psi bins with bin widths greater than zero. + m = (self.psi_upper_edges - self.psi_lower_edges) > 0 + return np.max(np.log10(self.psi_upper_edges[m])) + + @property + def pdf(self): + """(read-only) The probability-density-function + P(E_reco,psi,ang_err|E_nu,dec_nu), which, by definition, is the + histogram property divided by the 3D bin volumes for E_reco, psi, and + ang_err. + """ + log10_reco_e_bw = self.reco_e_upper_edges - self.reco_e_lower_edges + psi_bw = self.psi_upper_edges - self.psi_lower_edges + ang_err_bw = self.ang_err_upper_edges - self.ang_err_lower_edges + + bin_volumes = ( + log10_reco_e_bw[ + :, :, :, np.newaxis, np.newaxis + ] * + psi_bw[ + :, :, :, :, np.newaxis + ] * + ang_err_bw[ + :, :, :, :, : + ] + ) + + # Divide the histogram bin probability values by their bin volume. + # We do this only where the histogram actually has non-zero entries. + pdf = np.copy(self.histogram) + m = self.histogram != 0 + pdf[m] /= bin_volumes[m] + + return pdf + + def get_true_dec_idx(self, true_dec): + """Returns the true declination index for the given true declination + value. + + Parameters + ---------- + true_dec : float + The true declination value in radians. + + Returns + ------- + true_dec_idx : int + The index of the declination bin for the given declination value. + """ + if (true_dec < self.true_dec_bin_edges[0]) or\ + (true_dec > self.true_dec_bin_edges[-1]): + raise ValueError('The declination {} degrees is not supported by ' + 'the smearing matrix!'.format(true_dec)) + + true_dec_idx = np.digitize(true_dec, self.true_dec_bin_edges) - 1 + + return true_dec_idx + + def get_log10_true_e_idx(self, log10_true_e): + """Returns the bin index for the given true log10 energy value. + + Parameters + ---------- + log10_true_e : float + The log10 value of the true energy. + + Returns + ------- + log10_true_e_idx : int + The index of the true log10 energy bin for the given log10 true + energy value. + """ + if (log10_true_e < self.true_e_bin_edges[0]) or\ + (log10_true_e > self.true_e_bin_edges[-1]): + raise ValueError( + 'The log10 true energy value {} is not supported by the ' + 'smearing matrix!'.format(log10_true_e)) + + log10_true_e_idx = np.digitize( + log10_true_e, self._true_e_bin_edges) - 1 + + return log10_true_e_idx + + def get_reco_e_idx(self, true_e_idx, true_dec_idx, reco_e): + """Returns the bin index for the given reco energy value given the + given true energy and true declination bin indices. + + Parameters + ---------- + true_e_idx : int + The index of the true energy bin. + true_dec_idx : int + The index of the true declination bin. + reco_e : float + The reco energy value for which the bin index should get returned. + + Returns + ------- + reco_e_idx : int | None + The index of the reco energy bin the given reco energy value falls + into. It returns None if the value is out of range. + """ + lower_edges = self.reco_e_lower_edges[true_e_idx,true_dec_idx] + upper_edges = self.reco_e_upper_edges[true_e_idx,true_dec_idx] + + m = (lower_edges <= reco_e) & (upper_edges > reco_e) + idxs = np.nonzero(m)[0] + if(len(idxs) == 0): + return None + + reco_e_idx = idxs[0] + + return reco_e_idx + + def get_psi_idx(self, true_e_idx, true_dec_idx, reco_e_idx, psi): + """Returns the bin index for the given psi value given the + true energy, true declination and reco energy bin indices. + + Parameters + ---------- + true_e_idx : int + The index of the true energy bin. + true_dec_idx : int + The index of the true declination bin. + reco_e_idx : int + The index of the reco energy bin. + psi : float + The psi value in radians for which the bin index should get + returned. + + Returns + ------- + psi_idx : int | None + The index of the psi bin the given psi value falls into. + It returns None if the value is out of range. + """ + lower_edges = self.psi_lower_edges[true_e_idx,true_dec_idx,reco_e_idx] + upper_edges = self.psi_upper_edges[true_e_idx,true_dec_idx,reco_e_idx] + + m = (lower_edges <= psi) & (upper_edges > psi) + idxs = np.nonzero(m)[0] + if(len(idxs) == 0): + return None + + psi_idx = idxs[0] + + return psi_idx + + def get_ang_err_idx( + self, true_e_idx, true_dec_idx, reco_e_idx, psi_idx, ang_err): + """Returns the bin index for the given angular error value given the + true energy, true declination, reco energy, and psi bin indices. + + Parameters + ---------- + true_e_idx : int + The index of the true energy bin. + true_dec_idx : int + The index of the true declination bin. + reco_e_idx : int + The index of the reco energy bin. + psi_idx : int + The index of the psi bin. + ang_err : float + The angular error value in radians for which the bin index should + get returned. + + Returns + ------- + ang_err_idx : int | None + The index of the angular error bin the given angular error value + falls into. It returns None if the value is out of range. + """ + lower_edges = self.ang_err_lower_edges[ + true_e_idx,true_dec_idx,reco_e_idx,psi_idx] + upper_edges = self.ang_err_upper_edges[ + true_e_idx,true_dec_idx,reco_e_idx,psi_idx] + + m = (lower_edges <= ang_err) & (upper_edges > ang_err) + idxs = np.nonzero(m)[0] + if(len(idxs) == 0): + return None + + ang_err_idx = idxs[0] + + return ang_err_idx + + def get_true_log_e_range_with_valid_log_e_pdfs(self, dec_idx): + """Determines the true log energy range for which log_e PDFs are + available for the given declination bin. + + Parameters + ---------- + dec_idx : int + The declination bin index. + + Returns + ------- + min_log_true_e : float + The minimum true log energy value. + max_log_true_e : float + The maximum true log energy value. + """ + m = np.sum( + (self.reco_e_upper_edges[:,dec_idx] - + self.reco_e_lower_edges[:,dec_idx] > 0), + axis=1) != 0 + min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) + max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) + + return (min_log_true_e, max_log_true_e) + + def get_log_e_pdf( + self, log_true_e_idx, dec_idx): + """Retrieves the log_e PDF from the given true energy bin index and + source bin index. + Returns (None, None, None, None) if any of the bin indices are less then + zero, or if the sum of all pdf bins is zero. + + Parameters + ---------- + log_true_e_idx : int + The index of the true energy bin. + dec_idx : int + The index of the declination bin. + + Returns + ------- + pdf : 1d ndarray + The log_e pdf values. + lower_bin_edges : 1d ndarray + The lower bin edges of the energy pdf histogram. + upper_bin_edges : 1d ndarray + The upper bin edges of the energy pdf histogram. + bin_widths : 1d ndarray + The bin widths of the energy pdf histogram. + """ + if log_true_e_idx < 0 or dec_idx < 0: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, dec_idx] + pdf = np.sum(pdf, axis=(-2, -1)) + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the reco energy bin edges and widths. + lower_bin_edges = self.reco_e_lower_edges[ + log_true_e_idx, dec_idx + ] + upper_bin_edges = self.reco_e_upper_edges[ + log_true_e_idx, dec_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf /= np.sum(pdf) * bin_widths + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def get_psi_pdf( + self, log_true_e_idx, dec_idx, log_e_idx): + """Retrieves the psi PDF from the given true energy bin index, the + source bin index, and the log_e bin index. + Returns (None, None, None, None) if any of the bin indices are less then + zero, or if the sum of all pdf bins is zero. + + Parameters + ---------- + log_true_e_idx : int + The index of the true energy bin. + dec_idx : int + The index of the declination bin. + log_e_idx : int + The index of the log_e bin. + + Returns + ------- + pdf : 1d ndarray + The psi pdf values. + lower_bin_edges : 1d ndarray + The lower bin edges of the psi pdf histogram. + upper_bin_edges : 1d ndarray + The upper bin edges of the psi pdf histogram. + bin_widths : 1d ndarray + The bin widths of the psi pdf histogram. + """ + if log_true_e_idx < 0 or dec_idx < 0 or log_e_idx < 0: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, dec_idx, log_e_idx] + pdf = np.sum(pdf, axis=-1) + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the PSI bin edges and widths. + lower_bin_edges = self.psi_lower_edges[ + log_true_e_idx, dec_idx, log_e_idx + ] + upper_bin_edges = self.psi_upper_edges[ + log_true_e_idx, dec_idx, log_e_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf /= np.sum(pdf) * bin_widths + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def get_ang_err_pdf( + self, log_true_e_idx, dec_idx, log_e_idx, psi_idx): + """Retrieves the angular error PDF from the given true energy bin index, + the source bin index, the log_e bin index, and the psi bin index. + Returns (None, None, None, None) if any of the bin indices are less then + zero, or if the sum of all pdf bins is zero. + + Parameters + ---------- + log_true_e_idx : int + The index of the true energy bin. + dec_idx : int + The index of the declination bin. + log_e_idx : int + The index of the log_e bin. + psi_idx : int + The index of the psi bin. + + Returns + ------- + pdf : 1d ndarray + The ang_err pdf values. + lower_bin_edges : 1d ndarray + The lower bin edges of the ang_err pdf histogram. + upper_bin_edges : 1d ndarray + The upper bin edges of the ang_err pdf histogram. + bin_widths : 1d ndarray + The bin widths of the ang_err pdf histogram. + """ + if log_true_e_idx < 0 or dec_idx < 0 or log_e_idx < 0 or psi_idx < 0: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, dec_idx, log_e_idx, psi_idx] + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the ang_err bin edges and widths. + lower_bin_edges = self.ang_err_lower_edges[ + log_true_e_idx, dec_idx, log_e_idx, psi_idx + ] + upper_bin_edges = self.ang_err_upper_edges[ + log_true_e_idx, dec_idx, log_e_idx, psi_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Some bins might not be defined, i.e. have zero bin widths. + valid = bin_widths > 0 + + pdf = pdf[valid] + lower_bin_edges = lower_bin_edges[valid] + upper_bin_edges = upper_bin_edges[valid] + bin_widths = bin_widths[valid] + + # Normalize the PDF. + pdf = pdf / (np.sum(pdf) * bin_widths) + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def sample_log_e( + self, rss, dec_idx, log_true_e_idxs): + """Samples log energy values for the given source declination and true + energy bins. + + Parameters + ---------- + rss : instance of RandomStateService + The RandomStateService which should be used for drawing random + numbers from. + dec_idx : int + The index of the source declination bin. + log_true_e_idxs : 1d ndarray of int + The bin indices of the true energy bins. + + Returns + ------- + log_e_idx : 1d ndarray of int + The bin indices of the log_e pdf corresponding to the sampled + log_e values. + log_e : 1d ndarray of float + The sampled log_e values. + """ + n_evt = len(log_true_e_idxs) + log_e_idx = np.empty((n_evt,), dtype=np.int_) + log_e = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + b_size = np.count_nonzero(m) + ( + pdf, + low_bin_edges, + up_bin_edges, + bin_widths + ) = self.get_log_e_pdf( + b_log_true_e_idx, + dec_idx) + + if pdf is None: + log_e_idx[m] = -1 + log_e[m] = np.nan + continue + + b_log_e_idx = rss.random.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=b_size) + b_log_e = rss.random.uniform( + low_bin_edges[b_log_e_idx], + up_bin_edges[b_log_e_idx], + size=b_size) + + log_e_idx[m] = b_log_e_idx + log_e[m] = b_log_e + + return (log_e_idx, log_e) + + def sample_psi( + self, rss, dec_idx, log_true_e_idxs, log_e_idxs): + """Samples psi values for the given source declination, true + energy bins, and log_e bins. + + Parameters + ---------- + rss : instance of RandomStateService + The RandomStateService which should be used for drawing random + numbers from. + dec_idx : int + The index of the source declination bin. + log_true_e_idxs : 1d ndarray of int + The bin indices of the true energy bins. + log_e_idxs : 1d ndarray of int + The bin indices of the log_e bins. + + Returns + ------- + psi_idx : 1d ndarray of int + The bin indices of the psi pdf corresponding to the sampled psi + values. + psi : 1d ndarray of float + The sampled psi values in radians. + """ + if(len(log_true_e_idxs) != len(log_e_idxs)): + raise ValueError( + 'The lengths of log_true_e_idxs and log_e_idxs must be equal!') + + n_evt = len(log_true_e_idxs) + psi_idx = np.empty((n_evt,), dtype=np.int_) + psi = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) + for bb_log_e_idx in bb_unique_log_e_idxs: + mm = m & (log_e_idxs == bb_log_e_idx) + bb_size = np.count_nonzero(mm) + ( + pdf, + low_bin_edges, + up_bin_edges, + bin_widths + ) = self.get_psi_pdf( + b_log_true_e_idx, + dec_idx, + bb_log_e_idx) + + if pdf is None: + psi_idx[mm] = -1 + psi[mm] = np.nan + continue + + bb_psi_idx = rss.random.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=bb_size) + bb_psi = rss.random.uniform( + low_bin_edges[bb_psi_idx], + up_bin_edges[bb_psi_idx], + size=bb_size) + + psi_idx[mm] = bb_psi_idx + psi[mm] = bb_psi + + return (psi_idx, psi) + + def sample_ang_err( + self, rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs): + """Samples ang_err values for the given source declination, true + energy bins, log_e bins, and psi bins. + + Parameters + ---------- + rss : instance of RandomStateService + The RandomStateService which should be used for drawing random + numbers from. + dec_idx : int + The index of the source declination bin. + log_true_e_idxs : 1d ndarray of int + The bin indices of the true energy bins. + log_e_idxs : 1d ndarray of int + The bin indices of the log_e bins. + psi_idxs : 1d ndarray of int + The bin indices of the psi bins. + + Returns + ------- + ang_err_idx : 1d ndarray of int + The bin indices of the angular error pdf corresponding to the + sampled angular error values. + ang_err : 1d ndarray of float + The sampled angular error values in radians. + """ + if (len(log_true_e_idxs) != len(log_e_idxs)) and\ + (len(log_e_idxs) != len(psi_idxs)): + raise ValueError( + 'The lengths of log_true_e_idxs, log_e_idxs, and psi_idxs must ' + 'be equal!') + + n_evt = len(log_true_e_idxs) + ang_err_idx = np.empty((n_evt,), dtype=np.int_) + ang_err = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) + for bb_log_e_idx in bb_unique_log_e_idxs: + mm = m & (log_e_idxs == bb_log_e_idx) + bbb_unique_psi_idxs = np.unique(psi_idxs[mm]) + for bbb_psi_idx in bbb_unique_psi_idxs: + mmm = mm & (psi_idxs == bbb_psi_idx) + bbb_size = np.count_nonzero(mmm) + ( + pdf, + low_bin_edges, + up_bin_edges, + bin_widths + ) = self.get_ang_err_pdf( + b_log_true_e_idx, + dec_idx, + bb_log_e_idx, + bbb_psi_idx) + + if pdf is None: + ang_err_idx[mmm] = -1 + ang_err[mmm] = np.nan + continue + + bbb_ang_err_idx = rss.random.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=bbb_size) + bbb_ang_err = rss.random.uniform( + low_bin_edges[bbb_ang_err_idx], + up_bin_edges[bbb_ang_err_idx], + size=bbb_size) + + ang_err_idx[mmm] = bbb_ang_err_idx + ang_err[mmm] = bbb_ang_err + + return (ang_err_idx, ang_err) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 75ac96ec7a..28e9e717dc 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -15,9 +15,9 @@ from skyllh.core.source_hypothesis import SourceHypoGroupManager from skyllh.core.storage import DataFieldRecordArray -from skyllh.analyses.i3.publicdata_ps.utils import ( - psi_to_dec_and_ra, - PublicDataSmearingMatrix, +from skyllh.analyses.i3.publicdata_ps.utils import psi_to_dec_and_ra +from skyllh.analyses.i3.publicdata_ps.pd_smearing_matrix import ( + PublicDataSmearingMatrix ) from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff @@ -549,4 +549,4 @@ def generate_signal_events(self, rss, mean, poisson=True): else: signal_events_dict[ds_idx].append(events_) - return tot_n_events, signal_events_dict \ No newline at end of file + return tot_n_events, signal_events_dict diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 222ce67483..7452cad55a 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -27,7 +27,9 @@ from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff from skyllh.analyses.i3.publicdata_ps.utils import ( FctSpline1D, - PublicDataSmearingMatrix, +) +from skyllh.analyses.i3.publicdata_ps.pd_smearing_matrix import ( + PublicDataSmearingMatrix ) diff --git a/skyllh/analyses/i3/publicdata_ps/utils.py b/skyllh/analyses/i3/publicdata_ps/utils.py index 767f517fb5..54e8287f25 100644 --- a/skyllh/analyses/i3/publicdata_ps/utils.py +++ b/skyllh/analyses/i3/publicdata_ps/utils.py @@ -5,11 +5,7 @@ from scipy import interpolate from scipy import integrate -from skyllh.core.binning import ( - get_bincenters_from_binedges, - get_bin_indices_from_lower_and_upper_binedges -) -from skyllh.core.storage import create_FileLoader +from skyllh.core.binning import get_bincenters_from_binedges class FctSpline1D(object): @@ -158,173 +154,6 @@ def __call__(self, x, y, oor_value=0): return f -def load_smearing_histogram(pathfilenames): - """Loads the 5D smearing histogram from the given data file. - - Parameters - ---------- - pathfilenames : str | list of str - The file name of the data file. - - Returns - ------- - histogram : 5d ndarray - The 5d histogram array holding the probability values of the smearing - matrix. - The axes are (true_e, true_dec, reco_e, psi, ang_err). - true_e_bin_edges : 1d ndarray - The ndarray holding the bin edges of the true energy axis. - true_dec_bin_edges : 1d ndarray - The ndarray holding the bin edges of the true declination axis in - radians. - reco_e_lower_edges : 3d ndarray - The 3d ndarray holding the lower bin edges of the reco energy axis. - For each pair of true_e and true_dec different reco energy bin edges - are provided. - The shape is (n_true_e, n_true_dec, n_reco_e). - reco_e_upper_edges : 3d ndarray - The 3d ndarray holding the upper bin edges of the reco energy axis. - For each pair of true_e and true_dec different reco energy bin edges - are provided. - The shape is (n_true_e, n_true_dec, n_reco_e). - psi_lower_edges : 4d ndarray - The 4d ndarray holding the lower bin edges of the psi axis in radians. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). - psi_upper_edges : 4d ndarray - The 4d ndarray holding the upper bin edges of the psi axis in radians. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). - ang_err_lower_edges : 5d ndarray - The 5d ndarray holding the lower bin edges of the angular error axis - in radians. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). - ang_err_upper_edges : 5d ndarray - The 5d ndarray holding the upper bin edges of the angular error axis - in radians. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). - """ - # Load the smearing data from the public dataset. - loader = create_FileLoader(pathfilenames=pathfilenames) - data = loader.load_data() - # Rename the data fields. - renaming_dict = { - 'log10(E_nu/GeV)_min': 'true_e_min', - 'log10(E_nu/GeV)_max': 'true_e_max', - 'Dec_nu_min[deg]': 'true_dec_min', - 'Dec_nu_max[deg]': 'true_dec_max', - 'log10(E/GeV)_min': 'e_min', - 'log10(E/GeV)_max': 'e_max', - 'PSF_min[deg]': 'psi_min', - 'PSF_max[deg]': 'psi_max', - 'AngErr_min[deg]': 'ang_err_min', - 'AngErr_max[deg]': 'ang_err_max', - 'Fractional_Counts': 'norm_counts' - } - data.rename_fields(renaming_dict) - - def _get_nbins_from_edges(lower_edges, upper_edges): - """Helper function to extract the number of bins from the data's - bin edges. - """ - n = 0 - # Select only valid rows. - mask = (upper_edges - lower_edges) > 0 - data = lower_edges[mask] - # Go through the valid rows and search for the number of increasing - # bin edge values. - v0 = None - for v in data: - if(v0 is not None and v < v0): - # Reached the end of the edges block. - break - if(v0 is None or v > v0): - v0 = v - n += 1 - return n - - true_e_bin_edges = np.union1d( - data['true_e_min'], data['true_e_max']) - true_dec_bin_edges = np.union1d( - data['true_dec_min'], data['true_dec_max']) - - n_true_e = len(true_e_bin_edges) - 1 - n_true_dec = len(true_dec_bin_edges) - 1 - - n_reco_e = _get_nbins_from_edges( - data['e_min'], data['e_max']) - n_psi = _get_nbins_from_edges( - data['psi_min'], data['psi_max']) - n_ang_err = _get_nbins_from_edges( - data['ang_err_min'], data['ang_err_max']) - - # Get reco energy bin_edges as a 3d array. - idxs = np.array( - range(len(data)) - ) % (n_psi * n_ang_err) == 0 - - reco_e_lower_edges = np.reshape( - data['e_min'][idxs], - (n_true_e, n_true_dec, n_reco_e) - ) - reco_e_upper_edges = np.reshape( - data['e_max'][idxs], - (n_true_e, n_true_dec, n_reco_e) - ) - - # Get psi bin_edges as a 4d array. - idxs = np.array( - range(len(data)) - ) % n_ang_err == 0 - - psi_lower_edges = np.reshape( - data['psi_min'][idxs], - (n_true_e, n_true_dec, n_reco_e, n_psi) - ) - psi_upper_edges = np.reshape( - data['psi_max'][idxs], - (n_true_e, n_true_dec, n_reco_e, n_psi) - ) - - # Get angular error bin_edges as a 5d array. - ang_err_lower_edges = np.reshape( - data['ang_err_min'], - (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err) - ) - ang_err_upper_edges = np.reshape( - data['ang_err_max'], - (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err) - ) - - # Create 5D histogram for the probabilities. - histogram = np.reshape( - data['norm_counts'], - ( - n_true_e, - n_true_dec, - n_reco_e, - n_psi, - n_ang_err - ) - ) - - # Convert degrees into radians. - true_dec_bin_edges = np.radians(true_dec_bin_edges) - psi_lower_edges = np.radians(psi_lower_edges) - psi_upper_edges = np.radians(psi_upper_edges) - ang_err_lower_edges = np.radians(ang_err_lower_edges) - ang_err_upper_edges = np.radians(ang_err_upper_edges) - - return ( - histogram, - true_e_bin_edges, - true_dec_bin_edges, - reco_e_lower_edges, - reco_e_upper_edges, - psi_lower_edges, - psi_upper_edges, - ang_err_lower_edges, - ang_err_upper_edges - ) - def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): """Generates random declinations and right-ascension coordinates for the given source location and opening angle `psi`. @@ -385,918 +214,3 @@ def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): return (dec, ra) -def create_unionized_smearing_matrix_array(sm, src_dec): - """Creates a unionized smearing matrix array which covers the entire - observable space by keeping all original bins. - - Parameters - ---------- - sm : PublicDataSmearingMatrix instance - The PublicDataSmearingMatrix instance that holds the smearing matrix - data. - src_dec : float - The source declination in radians. - - Returns - ------- - result : dict - The result dictionary with the following fields: - union_arr : (nbins_true_e, - nbins_reco_e, - nbins_psi, - nbins_ang_err)-shaped 4D numpy ndarray - The 4D ndarray holding the smearing matrix values. - log10_true_e_bin_edges : 1D numpy ndarray - The unionized bin edges of the log10 true energy axis. - log10_reco_e_binedges : 1D numpy ndarray - The unionized bin edges of the log10 reco energy axis. - psi_binedges : 1D numpy ndarray - The unionized bin edges of psi axis. - ang_err_binedges : 1D numpy ndarray - The unionized bin edges of the angular error axis. - """ - true_dec_idx = sm.get_true_dec_idx(src_dec) - - true_e_bincenters = get_bincenters_from_binedges( - sm.true_e_bin_edges) - nbins_true_e = len(sm.true_e_bin_edges) - 1 - - # Determine the unionized bin edges along all dimensions. - reco_e_edges = np.unique(np.concatenate(( - sm.reco_e_lower_edges[:,true_dec_idx,...].flatten(), - sm.reco_e_upper_edges[:,true_dec_idx,...].flatten() - ))) - reco_e_bincenters = get_bincenters_from_binedges(reco_e_edges) - nbins_reco_e = len(reco_e_edges) - 1 - - psi_edges = np.unique(np.concatenate(( - sm.psi_lower_edges[:,true_dec_idx,...].flatten(), - sm.psi_upper_edges[:,true_dec_idx,...].flatten() - ))) - psi_bincenters = get_bincenters_from_binedges(psi_edges) - nbins_psi = len(psi_edges) - 1 - - ang_err_edges = np.unique(np.concatenate(( - sm.ang_err_lower_edges[:,true_dec_idx,...].flatten(), - sm.ang_err_upper_edges[:,true_dec_idx,...].flatten() - ))) - ang_err_bincenters = get_bincenters_from_binedges(ang_err_edges) - nbins_ang_err = len(ang_err_edges) - 1 - - # Create the unionized pdf array, which contains an axis for the - # true energy bins. - union_arr = np.zeros( - (nbins_true_e, nbins_reco_e, nbins_psi, nbins_ang_err), - dtype=np.double) - # Fill the 4D array. - for (true_e_idx, true_e) in enumerate(true_e_bincenters): - for (e_idx, e) in enumerate(reco_e_bincenters): - # Get the bin index of reco_e in the smearing matrix. - sm_e_idx = sm.get_reco_e_idx( - true_e_idx, true_dec_idx, e) - if sm_e_idx is None: - continue - for (p_idx, p) in enumerate(psi_bincenters): - # Get the bin index of psi in the smearing matrix. - sm_p_idx = sm.get_psi_idx( - true_e_idx, true_dec_idx, sm_e_idx, p) - if sm_p_idx is None: - continue - for (a_idx, a) in enumerate(ang_err_bincenters): - # Get the bin index of the angular error in the - # smearing matrix. - sm_a_idx = sm.get_ang_err_idx( - true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx, a) - if sm_a_idx is None: - continue - - # Get the bin volume of the smearing matrix's bin. - idx = ( - true_e_idx, true_dec_idx, sm_e_idx) - reco_e_bw = ( - sm.reco_e_upper_edges[idx] - - sm.reco_e_lower_edges[idx] - ) - idx = ( - true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx) - psi_bw = ( - sm.psi_upper_edges[idx] - - sm.psi_lower_edges[idx] - ) - idx = ( - true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx, sm_a_idx) - ang_err_bw = ( - sm.ang_err_upper_edges[idx] - - sm.ang_err_lower_edges[idx] - ) - bin_volume = reco_e_bw * psi_bw * ang_err_bw - - union_arr[ - true_e_idx, - e_idx, - p_idx, - a_idx - ] = sm.histogram[ - true_e_idx, - true_dec_idx, - sm_e_idx, - sm_p_idx, - sm_a_idx - ] / bin_volume - - result = dict({ - 'union_arr': union_arr, - 'log10_true_e_binedges': sm.true_e_bin_edges, - 'log10_reco_e_binedges': reco_e_edges, - 'psi_binedges': psi_edges, - 'ang_err_binedges': ang_err_edges - }) - - return result - - -def merge_bins(arr, edges, i_start, i_end): - n_to_merge = i_end - i_start + 1 - bw = np.diff(edges[i_start:i_end+2]) - - #print('i_start={}, i_end={}, n_to_merge={}, sum_bw={}'.format( - # i_start, i_end, n_to_merge, np.sum(bw))) - - new_n_e = arr.shape[1] - (i_end-i_start) - new_edges = np.empty((new_n_e+1,), dtype=np.double) - new_edges[0:i_start+1] = edges[0:i_start+1] - new_edges[i_start+1:] = edges[i_end+1:] - new_val = np.sum(arr[:,i_start:i_end+1,:,:], axis=1) / n_to_merge - new_arr = np.empty( - (arr.shape[0],new_n_e,arr.shape[2],arr.shape[3]), - dtype=np.double) - new_arr[:,i_start,:,:] = new_val - new_arr[:,0:i_start,:,:] = arr[:,0:i_start,:,:] - new_arr[:,i_start+1:,:,:] = arr[:,i_end+1:,:] - - return (new_arr, new_edges) - - -def merge_reco_energy_bins(arr, log10_reco_e_binedges, bw_th, max_bw=0.2): - """ - """ - bw = np.diff(log10_reco_e_binedges) - n = len(bw) - i = 0 - block_i_start = None - block_i_end = None - while i < n: - merge = False - if bw[i] <= bw_th: - # We need to combine this bin with the current block. - if block_i_start is None: - # Start a new block. - block_i_start = i - block_i_end = i - else: - # Extend the current block if it's not getting too large. - new_bw = ( - log10_reco_e_binedges[i+1] - - log10_reco_e_binedges[block_i_start] - ) - if new_bw <= max_bw: - block_i_end = i - else: - merge = True - elif(block_i_start is not None): - # We reached a big bin, so we combine the current block. - if block_i_end == block_i_start: - block_i_end = i - merge = True - - if merge: - (arr, log10_reco_e_binedges) = merge_bins( - arr, log10_reco_e_binedges, block_i_start, block_i_end) - bw = np.diff(log10_reco_e_binedges) - n = len(bw) - i = 0 - block_i_start = None - block_i_end = None - continue - - i += 1 - - # Merge the last block if there is any. - if block_i_start is not None: - (arr, log10_reco_e_binedges) = merge_bins( - arr, log10_reco_e_binedges, block_i_start, block_i_end) - - return (arr, log10_reco_e_binedges) - - -class PublicDataSmearingMatrix(object): - """This class is a helper class for dealing with the smearing matrix - provided by the public data. - """ - def __init__( - self, pathfilenames, **kwargs): - """Creates a smearing matrix instance by loading the smearing matrix - from the given file. - """ - super().__init__(**kwargs) - - ( - self.histogram, - self._true_e_bin_edges, - self._true_dec_bin_edges, - self.reco_e_lower_edges, - self.reco_e_upper_edges, - self.psi_lower_edges, - self.psi_upper_edges, - self.ang_err_lower_edges, - self.ang_err_upper_edges - ) = load_smearing_histogram(pathfilenames) - - self.n_psi_bins = self.histogram.shape[3] - self.n_ang_err_bins = self.histogram.shape[4] - - # Create bin edges array for log10_reco_e. - s = np.array(self.reco_e_lower_edges.shape) - s[-1] += 1 - self.log10_reco_e_binedges = np.empty(s, dtype=np.double) - self.log10_reco_e_binedges[...,:-1] = self.reco_e_lower_edges - self.log10_reco_e_binedges[...,-1] = self.reco_e_upper_edges[...,-1] - - # Create bin edges array for psi. - s = np.array(self.psi_lower_edges.shape) - s[-1] += 1 - self.psi_binedges = np.empty(s, dtype=np.double) - self.psi_binedges[...,:-1] = self.psi_lower_edges - self.psi_binedges[...,-1] = self.psi_upper_edges[...,-1] - - # Create bin edges array for ang_err. - s = np.array(self.ang_err_lower_edges.shape) - s[-1] += 1 - self.ang_err_binedges = np.empty(s, dtype=np.double) - self.ang_err_binedges[...,:-1] = self.ang_err_lower_edges - self.ang_err_binedges[...,-1] = self.ang_err_upper_edges[...,-1] - - @property - def n_log10_true_e_bins(self): - """(read-only) The number of log10 true energy bins. - """ - return len(self._true_e_bin_edges) - 1 - - @property - def true_e_bin_edges(self): - """(read-only) The (n_true_e+1,)-shaped 1D numpy ndarray holding the - bin edges of the true energy. - - Depricated! Use log10_true_enu_binedges instead! - """ - return self._true_e_bin_edges - - @property - def true_e_bin_centers(self): - """(read-only) The (n_true_e,)-shaped 1D numpy ndarray holding the bin - center values of the true energy. - """ - return 0.5*(self._true_e_bin_edges[:-1] + - self._true_e_bin_edges[1:]) - - @property - def log10_true_enu_binedges(self): - """(read-only) The (n_log10_true_enu+1,)-shaped 1D numpy ndarray holding - the bin edges of the log10 true neutrino energy. - """ - return self._true_e_bin_edges - - @property - def n_true_dec_bins(self): - """(read-only) The number of true declination bins. - """ - return len(self._true_dec_bin_edges) - 1 - - @property - def true_dec_bin_edges(self): - """(read-only) The (n_true_dec+1,)-shaped 1D numpy ndarray holding the - bin edges of the true declination. - """ - return self._true_dec_bin_edges - - @property - def true_dec_bin_centers(self): - """(read-only) The (n_true_dec,)-shaped 1D ndarray holding the bin - center values of the true declination. - """ - return 0.5*(self._true_dec_bin_edges[:-1] + - self._true_dec_bin_edges[1:]) - - @property - def log10_reco_e_binedges_lower(self): - """(read-only) The upper bin edges of the log10 reco energy axes. - """ - return self.reco_e_lower_edges - - @property - def log10_reco_e_binedges_upper(self): - """(read-only) The upper bin edges of the log10 reco energy axes. - """ - return self.reco_e_upper_edges - - @property - def min_log10_reco_e(self): - """(read-only) The minimum value of the reconstructed energy axis. - """ - # Select only valid reco energy bins with bin widths greater than zero. - m = (self.reco_e_upper_edges - self.reco_e_lower_edges) > 0 - return np.min(self.reco_e_lower_edges[m]) - - @property - def max_log10_reco_e(self): - """(read-only) The maximum value of the reconstructed energy axis. - """ - # Select only valid reco energy bins with bin widths greater than zero. - m = (self.reco_e_upper_edges - self.reco_e_lower_edges) > 0 - return np.max(self.reco_e_upper_edges[m]) - - @property - def min_log10_psi(self): - """(read-only) The minimum log10 value of the psi axis. - """ - # Select only valid psi bins with bin widths greater than zero. - m = (self.psi_upper_edges - self.psi_lower_edges) > 0 - return np.min(np.log10(self.psi_lower_edges[m])) - - @property - def max_log10_psi(self): - """(read-only) The maximum log10 value of the psi axis. - """ - # Select only valid psi bins with bin widths greater than zero. - m = (self.psi_upper_edges - self.psi_lower_edges) > 0 - return np.max(np.log10(self.psi_upper_edges[m])) - - @property - def pdf(self): - """(read-only) The probability-density-function - P(E_reco,psi,ang_err|E_nu,dec_nu), which, by definition, is the - histogram property divided by the 3D bin volumes for E_reco, psi, and - ang_err. - """ - log10_reco_e_bw = self.reco_e_upper_edges - self.reco_e_lower_edges - psi_bw = self.psi_upper_edges - self.psi_lower_edges - ang_err_bw = self.ang_err_upper_edges - self.ang_err_lower_edges - - bin_volumes = ( - log10_reco_e_bw[ - :, :, :, np.newaxis, np.newaxis - ] * - psi_bw[ - :, :, :, :, np.newaxis - ] * - ang_err_bw[ - :, :, :, :, : - ] - ) - - # Divide the histogram bin probability values by their bin volume. - # We do this only where the histogram actually has non-zero entries. - pdf = np.copy(self.histogram) - m = self.histogram != 0 - pdf[m] /= bin_volumes[m] - - return pdf - - def get_true_dec_idx(self, true_dec): - """Returns the true declination index for the given true declination - value. - - Parameters - ---------- - true_dec : float - The true declination value in radians. - - Returns - ------- - true_dec_idx : int - The index of the declination bin for the given declination value. - """ - if (true_dec < self.true_dec_bin_edges[0]) or\ - (true_dec > self.true_dec_bin_edges[-1]): - raise ValueError('The declination {} degrees is not supported by ' - 'the smearing matrix!'.format(true_dec)) - - true_dec_idx = np.digitize(true_dec, self.true_dec_bin_edges) - 1 - - return true_dec_idx - - def get_log10_true_e_idx(self, log10_true_e): - """Returns the bin index for the given true log10 energy value. - - Parameters - ---------- - log10_true_e : float - The log10 value of the true energy. - - Returns - ------- - log10_true_e_idx : int - The index of the true log10 energy bin for the given log10 true - energy value. - """ - if (log10_true_e < self.true_e_bin_edges[0]) or\ - (log10_true_e > self.true_e_bin_edges[-1]): - raise ValueError( - 'The log10 true energy value {} is not supported by the ' - 'smearing matrix!'.format(log10_true_e)) - - log10_true_e_idx = np.digitize( - log10_true_e, self._true_e_bin_edges) - 1 - - return log10_true_e_idx - - def get_reco_e_idx(self, true_e_idx, true_dec_idx, reco_e): - """Returns the bin index for the given reco energy value given the - given true energy and true declination bin indices. - - Parameters - ---------- - true_e_idx : int - The index of the true energy bin. - true_dec_idx : int - The index of the true declination bin. - reco_e : float - The reco energy value for which the bin index should get returned. - - Returns - ------- - reco_e_idx : int | None - The index of the reco energy bin the given reco energy value falls - into. It returns None if the value is out of range. - """ - lower_edges = self.reco_e_lower_edges[true_e_idx,true_dec_idx] - upper_edges = self.reco_e_upper_edges[true_e_idx,true_dec_idx] - - m = (lower_edges <= reco_e) & (upper_edges > reco_e) - idxs = np.nonzero(m)[0] - if(len(idxs) == 0): - return None - - reco_e_idx = idxs[0] - - return reco_e_idx - - def get_psi_idx(self, true_e_idx, true_dec_idx, reco_e_idx, psi): - """Returns the bin index for the given psi value given the - true energy, true declination and reco energy bin indices. - - Parameters - ---------- - true_e_idx : int - The index of the true energy bin. - true_dec_idx : int - The index of the true declination bin. - reco_e_idx : int - The index of the reco energy bin. - psi : float - The psi value in radians for which the bin index should get - returned. - - Returns - ------- - psi_idx : int | None - The index of the psi bin the given psi value falls into. - It returns None if the value is out of range. - """ - lower_edges = self.psi_lower_edges[true_e_idx,true_dec_idx,reco_e_idx] - upper_edges = self.psi_upper_edges[true_e_idx,true_dec_idx,reco_e_idx] - - m = (lower_edges <= psi) & (upper_edges > psi) - idxs = np.nonzero(m)[0] - if(len(idxs) == 0): - return None - - psi_idx = idxs[0] - - return psi_idx - - def get_ang_err_idx( - self, true_e_idx, true_dec_idx, reco_e_idx, psi_idx, ang_err): - """Returns the bin index for the given angular error value given the - true energy, true declination, reco energy, and psi bin indices. - - Parameters - ---------- - true_e_idx : int - The index of the true energy bin. - true_dec_idx : int - The index of the true declination bin. - reco_e_idx : int - The index of the reco energy bin. - psi_idx : int - The index of the psi bin. - ang_err : float - The angular error value in radians for which the bin index should - get returned. - - Returns - ------- - ang_err_idx : int | None - The index of the angular error bin the given angular error value - falls into. It returns None if the value is out of range. - """ - lower_edges = self.ang_err_lower_edges[ - true_e_idx,true_dec_idx,reco_e_idx,psi_idx] - upper_edges = self.ang_err_upper_edges[ - true_e_idx,true_dec_idx,reco_e_idx,psi_idx] - - m = (lower_edges <= ang_err) & (upper_edges > ang_err) - idxs = np.nonzero(m)[0] - if(len(idxs) == 0): - return None - - ang_err_idx = idxs[0] - - return ang_err_idx - - def get_true_log_e_range_with_valid_log_e_pdfs(self, dec_idx): - """Determines the true log energy range for which log_e PDFs are - available for the given declination bin. - - Parameters - ---------- - dec_idx : int - The declination bin index. - - Returns - ------- - min_log_true_e : float - The minimum true log energy value. - max_log_true_e : float - The maximum true log energy value. - """ - m = np.sum( - (self.reco_e_upper_edges[:,dec_idx] - - self.reco_e_lower_edges[:,dec_idx] > 0), - axis=1) != 0 - min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) - max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) - - return (min_log_true_e, max_log_true_e) - - def get_log_e_pdf( - self, log_true_e_idx, dec_idx): - """Retrieves the log_e PDF from the given true energy bin index and - source bin index. - Returns (None, None, None, None) if any of the bin indices are less then - zero, or if the sum of all pdf bins is zero. - - Parameters - ---------- - log_true_e_idx : int - The index of the true energy bin. - dec_idx : int - The index of the declination bin. - - Returns - ------- - pdf : 1d ndarray - The log_e pdf values. - lower_bin_edges : 1d ndarray - The lower bin edges of the energy pdf histogram. - upper_bin_edges : 1d ndarray - The upper bin edges of the energy pdf histogram. - bin_widths : 1d ndarray - The bin widths of the energy pdf histogram. - """ - if log_true_e_idx < 0 or dec_idx < 0: - return (None, None, None, None) - - pdf = self.histogram[log_true_e_idx, dec_idx] - pdf = np.sum(pdf, axis=(-2, -1)) - - if np.sum(pdf) == 0: - return (None, None, None, None) - - # Get the reco energy bin edges and widths. - lower_bin_edges = self.reco_e_lower_edges[ - log_true_e_idx, dec_idx - ] - upper_bin_edges = self.reco_e_upper_edges[ - log_true_e_idx, dec_idx - ] - bin_widths = upper_bin_edges - lower_bin_edges - - # Normalize the PDF. - pdf /= np.sum(pdf) * bin_widths - - return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) - - def get_psi_pdf( - self, log_true_e_idx, dec_idx, log_e_idx): - """Retrieves the psi PDF from the given true energy bin index, the - source bin index, and the log_e bin index. - Returns (None, None, None, None) if any of the bin indices are less then - zero, or if the sum of all pdf bins is zero. - - Parameters - ---------- - log_true_e_idx : int - The index of the true energy bin. - dec_idx : int - The index of the declination bin. - log_e_idx : int - The index of the log_e bin. - - Returns - ------- - pdf : 1d ndarray - The psi pdf values. - lower_bin_edges : 1d ndarray - The lower bin edges of the psi pdf histogram. - upper_bin_edges : 1d ndarray - The upper bin edges of the psi pdf histogram. - bin_widths : 1d ndarray - The bin widths of the psi pdf histogram. - """ - if log_true_e_idx < 0 or dec_idx < 0 or log_e_idx < 0: - return (None, None, None, None) - - pdf = self.histogram[log_true_e_idx, dec_idx, log_e_idx] - pdf = np.sum(pdf, axis=-1) - - if np.sum(pdf) == 0: - return (None, None, None, None) - - # Get the PSI bin edges and widths. - lower_bin_edges = self.psi_lower_edges[ - log_true_e_idx, dec_idx, log_e_idx - ] - upper_bin_edges = self.psi_upper_edges[ - log_true_e_idx, dec_idx, log_e_idx - ] - bin_widths = upper_bin_edges - lower_bin_edges - - # Normalize the PDF. - pdf /= np.sum(pdf) * bin_widths - - return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) - - def get_ang_err_pdf( - self, log_true_e_idx, dec_idx, log_e_idx, psi_idx): - """Retrieves the angular error PDF from the given true energy bin index, - the source bin index, the log_e bin index, and the psi bin index. - Returns (None, None, None, None) if any of the bin indices are less then - zero, or if the sum of all pdf bins is zero. - - Parameters - ---------- - log_true_e_idx : int - The index of the true energy bin. - dec_idx : int - The index of the declination bin. - log_e_idx : int - The index of the log_e bin. - psi_idx : int - The index of the psi bin. - - Returns - ------- - pdf : 1d ndarray - The ang_err pdf values. - lower_bin_edges : 1d ndarray - The lower bin edges of the ang_err pdf histogram. - upper_bin_edges : 1d ndarray - The upper bin edges of the ang_err pdf histogram. - bin_widths : 1d ndarray - The bin widths of the ang_err pdf histogram. - """ - if log_true_e_idx < 0 or dec_idx < 0 or log_e_idx < 0 or psi_idx < 0: - return (None, None, None, None) - - pdf = self.histogram[log_true_e_idx, dec_idx, log_e_idx, psi_idx] - - if np.sum(pdf) == 0: - return (None, None, None, None) - - # Get the ang_err bin edges and widths. - lower_bin_edges = self.ang_err_lower_edges[ - log_true_e_idx, dec_idx, log_e_idx, psi_idx - ] - upper_bin_edges = self.ang_err_upper_edges[ - log_true_e_idx, dec_idx, log_e_idx, psi_idx - ] - bin_widths = upper_bin_edges - lower_bin_edges - - # Some bins might not be defined, i.e. have zero bin widths. - valid = bin_widths > 0 - - pdf = pdf[valid] - lower_bin_edges = lower_bin_edges[valid] - upper_bin_edges = upper_bin_edges[valid] - bin_widths = bin_widths[valid] - - # Normalize the PDF. - pdf = pdf / (np.sum(pdf) * bin_widths) - - return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) - - def sample_log_e( - self, rss, dec_idx, log_true_e_idxs): - """Samples log energy values for the given source declination and true - energy bins. - - Parameters - ---------- - rss : instance of RandomStateService - The RandomStateService which should be used for drawing random - numbers from. - dec_idx : int - The index of the source declination bin. - log_true_e_idxs : 1d ndarray of int - The bin indices of the true energy bins. - - Returns - ------- - log_e_idx : 1d ndarray of int - The bin indices of the log_e pdf corresponding to the sampled - log_e values. - log_e : 1d ndarray of float - The sampled log_e values. - """ - n_evt = len(log_true_e_idxs) - log_e_idx = np.empty((n_evt,), dtype=np.int_) - log_e = np.empty((n_evt,), dtype=np.double) - - unique_log_true_e_idxs = np.unique(log_true_e_idxs) - for b_log_true_e_idx in unique_log_true_e_idxs: - m = log_true_e_idxs == b_log_true_e_idx - b_size = np.count_nonzero(m) - ( - pdf, - low_bin_edges, - up_bin_edges, - bin_widths - ) = self.get_log_e_pdf( - b_log_true_e_idx, - dec_idx) - - if pdf is None: - log_e_idx[m] = -1 - log_e[m] = np.nan - continue - - b_log_e_idx = rss.random.choice( - np.arange(len(pdf)), - p=(pdf * bin_widths), - size=b_size) - b_log_e = rss.random.uniform( - low_bin_edges[b_log_e_idx], - up_bin_edges[b_log_e_idx], - size=b_size) - - log_e_idx[m] = b_log_e_idx - log_e[m] = b_log_e - - return (log_e_idx, log_e) - - def sample_psi( - self, rss, dec_idx, log_true_e_idxs, log_e_idxs): - """Samples psi values for the given source declination, true - energy bins, and log_e bins. - - Parameters - ---------- - rss : instance of RandomStateService - The RandomStateService which should be used for drawing random - numbers from. - dec_idx : int - The index of the source declination bin. - log_true_e_idxs : 1d ndarray of int - The bin indices of the true energy bins. - log_e_idxs : 1d ndarray of int - The bin indices of the log_e bins. - - Returns - ------- - psi_idx : 1d ndarray of int - The bin indices of the psi pdf corresponding to the sampled psi - values. - psi : 1d ndarray of float - The sampled psi values in radians. - """ - if(len(log_true_e_idxs) != len(log_e_idxs)): - raise ValueError( - 'The lengths of log_true_e_idxs and log_e_idxs must be equal!') - - n_evt = len(log_true_e_idxs) - psi_idx = np.empty((n_evt,), dtype=np.int_) - psi = np.empty((n_evt,), dtype=np.double) - - unique_log_true_e_idxs = np.unique(log_true_e_idxs) - for b_log_true_e_idx in unique_log_true_e_idxs: - m = log_true_e_idxs == b_log_true_e_idx - bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) - for bb_log_e_idx in bb_unique_log_e_idxs: - mm = m & (log_e_idxs == bb_log_e_idx) - bb_size = np.count_nonzero(mm) - ( - pdf, - low_bin_edges, - up_bin_edges, - bin_widths - ) = self.get_psi_pdf( - b_log_true_e_idx, - dec_idx, - bb_log_e_idx) - - if pdf is None: - psi_idx[mm] = -1 - psi[mm] = np.nan - continue - - bb_psi_idx = rss.random.choice( - np.arange(len(pdf)), - p=(pdf * bin_widths), - size=bb_size) - bb_psi = rss.random.uniform( - low_bin_edges[bb_psi_idx], - up_bin_edges[bb_psi_idx], - size=bb_size) - - psi_idx[mm] = bb_psi_idx - psi[mm] = bb_psi - - return (psi_idx, psi) - - def sample_ang_err( - self, rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs): - """Samples ang_err values for the given source declination, true - energy bins, log_e bins, and psi bins. - - Parameters - ---------- - rss : instance of RandomStateService - The RandomStateService which should be used for drawing random - numbers from. - dec_idx : int - The index of the source declination bin. - log_true_e_idxs : 1d ndarray of int - The bin indices of the true energy bins. - log_e_idxs : 1d ndarray of int - The bin indices of the log_e bins. - psi_idxs : 1d ndarray of int - The bin indices of the psi bins. - - Returns - ------- - ang_err_idx : 1d ndarray of int - The bin indices of the angular error pdf corresponding to the - sampled angular error values. - ang_err : 1d ndarray of float - The sampled angular error values in radians. - """ - if (len(log_true_e_idxs) != len(log_e_idxs)) and\ - (len(log_e_idxs) != len(psi_idxs)): - raise ValueError( - 'The lengths of log_true_e_idxs, log_e_idxs, and psi_idxs must ' - 'be equal!') - - n_evt = len(log_true_e_idxs) - ang_err_idx = np.empty((n_evt,), dtype=np.int_) - ang_err = np.empty((n_evt,), dtype=np.double) - - unique_log_true_e_idxs = np.unique(log_true_e_idxs) - for b_log_true_e_idx in unique_log_true_e_idxs: - m = log_true_e_idxs == b_log_true_e_idx - bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) - for bb_log_e_idx in bb_unique_log_e_idxs: - mm = m & (log_e_idxs == bb_log_e_idx) - bbb_unique_psi_idxs = np.unique(psi_idxs[mm]) - for bbb_psi_idx in bbb_unique_psi_idxs: - mmm = mm & (psi_idxs == bbb_psi_idx) - bbb_size = np.count_nonzero(mmm) - ( - pdf, - low_bin_edges, - up_bin_edges, - bin_widths - ) = self.get_ang_err_pdf( - b_log_true_e_idx, - dec_idx, - bb_log_e_idx, - bbb_psi_idx) - - if pdf is None: - ang_err_idx[mmm] = -1 - ang_err[mmm] = np.nan - continue - - bbb_ang_err_idx = rss.random.choice( - np.arange(len(pdf)), - p=(pdf * bin_widths), - size=bbb_size) - bbb_ang_err = rss.random.uniform( - low_bin_edges[bbb_ang_err_idx], - up_bin_edges[bbb_ang_err_idx], - size=bbb_size) - - ang_err_idx[mmm] = bbb_ang_err_idx - ang_err[mmm] = bbb_ang_err - - return (ang_err_idx, ang_err) From 361ca69932ca739ab196483c19c61f892dbf6711 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 30 Mar 2023 18:55:34 +0200 Subject: [PATCH 204/274] Remove redundant analysis script. --- .../analyses/i3/publicdata_ps/trad_ps_wMC.py | 330 ------------------ 1 file changed, 330 deletions(-) delete mode 100644 skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py b/skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py deleted file mode 100644 index f80e2a150a..0000000000 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps_wMC.py +++ /dev/null @@ -1,330 +0,0 @@ -# -*- coding: utf-8 -*- - -"""The IC170922A_wGFU analysis is a multi-dataset time-integrated single source -analysis with a two-component likelihood function using a spacial and an energy -event PDF. -""" - -import argparse -import logging -import numpy as np - -from skyllh.core.progressbar import ProgressBar - -# Classes to define the source hypothesis. -from skyllh.physics.source import PointLikeSource -from skyllh.physics.flux import PowerLawFlux -from skyllh.core.source_hypo_group import SourceHypoGroup -from skyllh.core.source_hypothesis import SourceHypoGroupManager - -# Classes to define the fit parameters. -from skyllh.core.parameters import ( - SingleSourceFitParameterMapper, - FitParameter -) - -# Classes for the minimizer. -from skyllh.core.minimizer import Minimizer, LBFGSMinimizerImpl - -# Classes for utility functionality. -from skyllh.core.config import CFG -from skyllh.core.random import RandomStateService -from skyllh.core.optimize import SpatialBoxEventSelectionMethod -from skyllh.core.smoothing import BlockSmoothingFilter -from skyllh.core.timing import TimeLord -from skyllh.core.trialdata import TrialDataManager - -# Classes for defining the analysis. -from skyllh.core.test_statistic import TestStatisticWilks -from skyllh.core.analysis import ( - TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis -) - -# Classes to define the background generation. -from skyllh.core.scrambling import DataScrambler, UniformRAScramblingMethod -from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod - -# Classes to define the detector signal yield tailored to the source hypothesis. -from skyllh.i3.detsigyield import PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod - -# Classes to define the signal and background PDFs. -from skyllh.core.signalpdf import GaussianPSFPointLikeSourceSignalSpatialPDF -from skyllh.i3.signalpdf import SignalI3EnergyPDFSet -from skyllh.i3.backgroundpdf import ( - DataBackgroundI3SpatialPDF, - DataBackgroundI3EnergyPDF -) -# Classes to define the spatial and energy PDF ratios. -from skyllh.core.pdfratio import ( - SpatialSigOverBkgPDFRatio, - Skylab2SkylabPDFRatioFillMethod -) -from skyllh.i3.pdfratio import I3EnergySigSetOverBkgPDFRatioSpline - -from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod - -# Analysis utilities. -from skyllh.core.analysis_utils import ( - pointlikesource_to_data_field_array -) - -# Logging setup utilities. -from skyllh.core.debugging import ( - setup_logger, - setup_console_handler, - setup_file_handler -) - -# The pre-defined data samples. -from skyllh.datasets.i3 import data_samples - -def TXS_location(): - src_ra = np.radians(77.358) - src_dec = np.radians(5.693) - return (src_ra, src_dec) - -def create_analysis( - datasets, - source, - refplflux_Phi0=1, - refplflux_E0=1e3, - refplflux_gamma=2, - ns_seed=10.0, - gamma_seed=3, - compress_data=False, - keep_data_fields=None, - optimize_delta_angle=10, - efficiency_mode=None, - tl=None, - ppbar=None -): - """Creates the Analysis instance for this particular analysis. - - Parameters: - ----------- - datasets : list of Dataset instances - The list of Dataset instances, which should be used in the - analysis. - source : PointLikeSource instance - The PointLikeSource instance defining the point source position. - refplflux_Phi0 : float - The flux normalization to use for the reference power law flux model. - refplflux_E0 : float - The reference energy to use for the reference power law flux model. - refplflux_gamma : float - The spectral index to use for the reference power law flux model. - ns_seed : float - Value to seed the minimizer with for the ns fit. - gamma_seed : float | None - Value to seed the minimizer with for the gamma fit. If set to None, - the refplflux_gamma value will be set as gamma_seed. - compress_data : bool - Flag if the data should get converted from float64 into float32. - keep_data_fields : list of str | None - List of additional data field names that should get kept when loading - the data. - optimize_delta_angle : float - The delta angle in degrees for the event selection optimization methods. - efficiency_mode : str | None - The efficiency mode the data should get loaded with. Possible values - are: - - - 'memory': - The data will be load in a memory efficient way. This will - require more time, because all data records of a file will - be loaded sequentially. - - 'time': - The data will be loaded in a time efficient way. This will - require more memory, because each data file gets loaded in - memory at once. - - The default value is ``'time'``. If set to ``None``, the default - value will be used. - tl : TimeLord instance | None - The TimeLord instance to use to time the creation of the analysis. - ppbar : ProgressBar instance | None - The instance of ProgressBar for the optional parent progress bar. - - Returns - ------- - analysis : SpatialEnergyTimeIntegratedMultiDatasetSingleSourceAnalysis - The Analysis instance for this analysis. - """ - # Define the flux model. - fluxmodel = PowerLawFlux( - Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) - - # Define the fit parameter ns. - fitparam_ns = FitParameter('ns', 0, 1e3, ns_seed) - - # Define the gamma fit parameter. - fitparam_gamma = FitParameter('gamma', valmin=1, valmax=4, initial=gamma_seed) - - # Define the detector signal efficiency implementation method for the - # IceCube detector and this source and fluxmodel. - # The sin(dec) binning will be taken by the implementation method - # automatically from the Dataset instance. - gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) - detsigyield_implmethod = PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( - gamma_grid) - - # Define the signal generation method. - sig_gen_method = PointLikeSourceI3SignalGenerationMethod() - - # Create a source hypothesis group manager. - src_hypo_group_manager = SourceHypoGroupManager( - SourceHypoGroup( - source, fluxmodel, detsigyield_implmethod, sig_gen_method)) - - # Create a source fit parameter mapper and define the fit parameters. - src_fitparam_mapper = SingleSourceFitParameterMapper() - src_fitparam_mapper.def_fit_parameter(fitparam_gamma) - - # Define the test statistic. - test_statistic = TestStatisticWilks() - - # Define the data scrambler with its data scrambling method, which is used - # for background generation. - data_scrambler = DataScrambler(UniformRAScramblingMethod()) - - # Create background generation method. - bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) - - # Create the minimizer instance. - minimizer = Minimizer(LBFGSMinimizerImpl()) - - # Create the Analysis instance. - analysis = Analysis( - src_hypo_group_manager, - src_fitparam_mapper, - fitparam_ns, - test_statistic, - bkg_gen_method - ) - - # Define the event selection method for pure optimization purposes. - # We will use the same method for all datasets. - event_selection_method = SpatialBoxEventSelectionMethod( - src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) - - # Add the data sets to the analysis. - pbar = ProgressBar(len(datasets), parent=ppbar).start() - for ds in datasets: - # Load the data of the data set. - data = ds.load_and_prepare_data( - keep_fields=keep_data_fields, - compress=compress_data, - efficiency_mode=efficiency_mode, - tl=tl) - - # Create a trial data manager and add the required data fields. - tdm = TrialDataManager() - tdm.add_source_data_field('src_array', pointlikesource_to_data_field_array) - - sin_dec_binning = ds.get_binning_definition('sin_dec') - log_energy_binning = ds.get_binning_definition('log_energy') - - # Create the spatial PDF ratio instance for this dataset. - spatial_sigpdf = GaussianPSFPointLikeSourceSignalSpatialPDF( - dec_range=np.arcsin(sin_dec_binning.range)) - spatial_bkgpdf = DataBackgroundI3SpatialPDF( - data.exp, sin_dec_binning) - spatial_pdfratio = SpatialSigOverBkgPDFRatio( - spatial_sigpdf, spatial_bkgpdf) - - # Create the energy PDF ratio instance for this dataset. - smoothing_filter = BlockSmoothingFilter(nbins=1) - energy_sigpdfset = SignalI3EnergyPDFSet( - data.mc, log_energy_binning, sin_dec_binning, fluxmodel, gamma_grid, - smoothing_filter, ppbar=pbar) - energy_bkgpdf = DataBackgroundI3EnergyPDF( - data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) - fillmethod = Skylab2SkylabPDFRatioFillMethod() - energy_pdfratio = I3EnergySigSetOverBkgPDFRatioSpline( - energy_sigpdfset, energy_bkgpdf, - fillmethod=fillmethod, - ppbar=pbar) - - pdfratios = [ spatial_pdfratio, energy_pdfratio ] - #pdfratios = [ spatial_pdfratio ] - - analysis.add_dataset( - ds, data, pdfratios, tdm, event_selection_method) - - pbar.increment() - pbar.finish() - - analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) - - analysis.construct_signal_generator() - - return analysis - -if(__name__ == '__main__'): - p = argparse.ArgumentParser( - description = "Calculates TS for a given source location using 7-year " - "point source sample and 3-year GFU sample.", - formatter_class = argparse.RawTextHelpFormatter - ) - p.add_argument("--data_base_path", default=None, type=str, - help='The base path to the data samples (default=None)' - ) - p.add_argument("--ncpu", default=1, type=int, - help='The number of CPUs to utilize where parallelization is possible.' - ) - args = p.parse_args() - - # Setup `skyllh` package logging. - # To optimize logging set the logging level to the lowest handling level. - setup_logger('skyllh', logging.DEBUG) - log_format = '%(asctime)s %(processName)s %(name)s %(levelname)s: '\ - '%(message)s' - setup_console_handler('skyllh', logging.INFO, log_format) - setup_file_handler('skyllh', logging.DEBUG, log_format, 'debug.log') - - CFG['multiproc']['ncpu'] = args.ncpu - - sample_seasons = [ - ("PointSourceTracks", "IC40"), - ("PointSourceTracks", "IC59"), - ("PointSourceTracks", "IC79"), - ("PointSourceTracks", "IC86, 2011"), - ("PointSourceTracks", "IC86, 2012-2014"), - ("GFU", "IC86, 2015-2017") - ] - - datasets = [] - for (sample, season) in sample_seasons: - # Get the dataset from the correct dataset collection. - dsc = data_samples[sample].create_dataset_collection(args.data_base_path) - datasets.append(dsc.get_dataset(season)) - - rss_seed = 1 - # Define a random state service. - rss = RandomStateService(rss_seed) - - # Define the point source. - source = PointLikeSource(*TXS_location()) - - tl = TimeLord() - - with tl.task_timer('Creating analysis.'): - ana = create_analysis( - datasets, source, compress_data=False, tl=tl) - - with tl.task_timer('Unblinding data.'): - (TS, fitparam_dict, status) = ana.unblind(rss) - - #print('log_lambda_max: %g'%(log_lambda_max)) - print('TS = %g'%(TS)) - print('ns_fit = %g'%(fitparam_dict['ns'])) - print('gamma_fit = %g'%(fitparam_dict['gamma'])) - - # Generate some signal events. - with tl.task_timer('Generating signal events.'): - (n_sig, signal_events_dict) = ana.sig_generator.generate_signal_events(rss, 100) - - print('n_sig: %d', n_sig) - print('signal datasets: '+str(signal_events_dict.keys())) - - print(tl) From c4d6d45387d0c4cf998c23c79a69a6200bae83ba Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 31 Mar 2023 12:15:28 +0200 Subject: [PATCH 205/274] Moved the energy cut splines to --- .../i3/publicdata_ps/time_dependent_ps.py | 80 +++++++----------- .../i3/publicdata_ps/time_integrated_ps.py | 81 +++++-------------- skyllh/analyses/i3/publicdata_ps/utils.py | 51 ++++++++++-- 3 files changed, 92 insertions(+), 120 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py index bd057dfff2..0f2f92bf39 100644 --- a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py @@ -37,7 +37,6 @@ # Classes for defining the analysis. from skyllh.core.test_statistic import TestStatisticWilks from skyllh.core.analysis import ( - TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis, TimeDependentSingleDatasetSingleSourceAnalysis as TimedepSingleDatasetAnalysis ) @@ -48,7 +47,7 @@ # Classes to define the signal and background PDFs. from skyllh.core.signalpdf import ( RayleighPSFPointSourceSignalSpatialPDF, - SignalBoxTimePDF, + SignalBoxTimePDF, SignalGaussTimePDF ) from skyllh.core.backgroundpdf import BackgroundUniformTimePDF @@ -79,7 +78,6 @@ # Analysis specific classes for working with the public data. from skyllh.analyses.i3.publicdata_ps.signal_generator import ( - PDSignalGenerator, PDTimeDependentSignalGenerator ) from skyllh.analyses.i3.publicdata_ps.detsigyield import ( @@ -94,13 +92,14 @@ from skyllh.analyses.i3.publicdata_ps.backgroundpdf import ( PDDataBackgroundI3EnergyPDF ) +from skyllh.analyses.i3.publicdata_ps.utils import create_energy_cut_spline from skyllh.analyses.i3.publicdata_ps.time_integrated_ps import ( - psi_func, + psi_func, TXS_location ) -def create_timedep_analysis( +def create_analysis( datasets, source, gauss=None, @@ -116,8 +115,8 @@ def create_timedep_analysis( gamma_max=5., kde_smoothing=False, minimizer_impl="LBFGS", - cut_sindec = None, - spl_smooth = None, + cut_sindec=None, + spl_smooth=None, cap_ratio=False, compress_data=False, keep_data_fields=None, @@ -199,7 +198,9 @@ def create_timedep_analysis( if gauss is None and box is None: raise ValueError("No time pdf specified (box or gauss)") if gauss is not None and box is not None: - raise ValueError("Time PDF cannot be both Gaussian and box shaped. Please specify only one shape.") + raise ValueError( + "Time PDF cannot be both Gaussian and box shaped. " + "Please specify only one shape.") # Create the minimizer instance. if minimizer_impl == "LBFGS": @@ -207,8 +208,9 @@ def create_timedep_analysis( elif minimizer_impl == "minuit": minimizer = Minimizer(IMinuitMinimizerImpl(ftol=1e-8)) else: - raise NameError(f"Minimizer implementation `{minimizer_impl}` is not " - "supported. Please use `LBFGS` or `minuit`.") + raise NameError( + f"Minimizer implementation `{minimizer_impl}` is not supported " + "Please use `LBFGS` or `minuit`.") # Define the flux model. flux_model = PowerLawFlux( @@ -267,22 +269,21 @@ def create_timedep_analysis( # We will use the same method for all datasets. event_selection_method = SpatialBoxEventSelectionMethod( src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) - #event_selection_method = None - + # Prepare the spline parameters. if cut_sindec is None: cut_sindec = np.sin(np.radians([-2, 0, -3, 0, 0])) if spl_smooth is None: spl_smooth = [0., 0.005, 0.05, 0.2, 0.3] if len(spl_smooth) < len(datasets) or len(cut_sindec) < len(datasets): - raise AssertionError("The length of the spl_smooth and of the " - "cut_sindec must be equal to the length of datasets: " - f"{len(datasets)}.") - + raise AssertionError( + "The length of the spl_smooth and of the cut_sindec must be equal " + f"to the length of datasets: {len(datasets)}.") + # Add the data sets to the analysis. pbar = ProgressBar(len(datasets), parent=ppbar).start() energy_cut_splines = [] - for idx,ds in enumerate(datasets): + for idx, ds in enumerate(datasets): # Load the data of the data set. data = ds.load_and_prepare_data( keep_fields=keep_data_fields, @@ -331,46 +332,20 @@ def create_timedep_analysis( if gauss is not None or box is not None: time_bkgpdf = BackgroundUniformTimePDF(data.grl) if gauss is not None: - time_sigpdf = SignalGaussTimePDF(data.grl, gauss['mu'], gauss['sigma']) + time_sigpdf = SignalGaussTimePDF( + data.grl, gauss['mu'], gauss['sigma']) elif box is not None: - time_sigpdf = SignalBoxTimePDF(data.grl, box["start"], box["end"]) + time_sigpdf = SignalBoxTimePDF( + data.grl, box["start"], box["end"]) time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) pdfratios.append(time_pdfratio) - + analysis.add_dataset( ds, data, pdfratios, tdm, event_selection_method) - - # Create the spline for the declination-dependent energy cut - # that the signal generator needs for injection in the southern sky - - # Some special conditions are needed for IC79 and IC86_I, because - # their experimental dataset shows events that should probably have - # been cut by the IceCube selection. - data_exp = data.exp.copy(keep_fields=['sin_dec', 'log_energy']) - if ds.name == 'IC79': - m = np.invert(np.logical_and( - data_exp['sin_dec']<-0.75, - data_exp['log_energy'] < 4.2)) - data_exp = data_exp[m] - if ds.name == 'IC86_I': - m = np.invert(np.logical_and( - data_exp['sin_dec']<-0.2, - data_exp['log_energy'] < 2.5)) - data_exp = data_exp[m] - - sin_dec_binning = ds.get_binning_definition('sin_dec') - sindec_edges = sin_dec_binning.binedges - min_log_e = np.zeros(len(sindec_edges)-1, dtype=float) - for i in range(len(sindec_edges)-1): - mask = np.logical_and( - data_exp['sin_dec']>=sindec_edges[i], - data_exp['sin_dec']=sindec_edges[i], - data_exp['sin_dec']= sindec_edges[i], + data_exp['sin_dec'] < sindec_edges[i+1]) + min_log_e[i] = np.min(data_exp['log_energy'][mask]) + del data_exp + sindec_centers = 0.5 * (sindec_edges[1:]+sindec_edges[:-1]) + + spline = interpolate.UnivariateSpline( + sindec_centers, min_log_e, k=2, s=spl_smooth) + + return spline From a5ac60cfd22623c62d7d4fffe213178c3012ca4b Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 31 Mar 2023 17:36:21 +0200 Subject: [PATCH 206/274] Fix #125. --- .../i3/publicdata_ps/signal_generator.py | 93 +++++++++++-------- 1 file changed, 52 insertions(+), 41 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 28e9e717dc..b2a72e34c5 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -83,17 +83,20 @@ def _generate_inv_cdf_spline(self, flux_model, log_e_min, # 1000 times smaller than the smallest non-zero bin. m = prob_per_bin == 0 prob_per_bin[m] = np.min(prob_per_bin[np.invert(m)]) / 1000 + to_keep = np.where(prob_per_bin > 1e-15)[0] # For numerical stability + prob_per_bin = prob_per_bin[to_keep] prob_per_bin /= np.sum(prob_per_bin) # Compute the cumulative distribution CDF. - cum_per_bin = np.cumsum(prob_per_bin) - cum_per_bin = np.concatenate(([0], cum_per_bin)) + cum_per_bin = [np.sum(prob_per_bin[:i]) + for i in range(prob_per_bin.size+1)] if np.any(np.diff(cum_per_bin) == 0): raise ValueError( 'The cumulative sum of the true energy probability is not ' 'monotonically increasing! Values of the cumsum are ' f'{cum_per_bin}.') + bin_centers = bin_centers[to_keep] bin_centers = np.concatenate(([low_bin_edges[0]], bin_centers)) # Build a spline for the inverse CDF. @@ -210,7 +213,7 @@ def _generate_events( events['run'] = -1 * np.ones(n_events) return events - + @staticmethod @np.vectorize def energy_filter(events, spline, cut_sindec): @@ -220,9 +223,9 @@ def energy_filter(events, spline, cut_sindec): if cut_sindec is None: cut_sindec = 0 energy_filter = np.logical_and( - events['sin_dec'] tmp_grl["stop"][-1]) or ( + while events_._data_fields["time"][event_index] == 1: + if self.gauss is not None: + # make sure flare is in dataset + if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( self.gauss["mu"] + 4 * self.gauss["sigma"] < tmp_grl["start"][0]): - break # this should never happen - time = norm(self.gauss["mu"], self.gauss["sigma"]).rvs() - if self.box is not None: - # make sure flare is in dataset - if (self.box["start"] > tmp_grl["stop"][-1]) or (self.box["end"] < tmp_grl["start"][0]): - break # this should never be the case, since there should no events be generated - livetime = self.box["end"] - self.box["start"] - time = rss.random.random() * livetime - time += self.box["start"] - # check if time is in grl - is_in_grl = (tmp_grl["start"] <= time) & (tmp_grl["stop"] >= time) - if np.any(is_in_grl): - events_._data_fields["time"][event_index] = time + break # this should never happen + time = norm( + self.gauss["mu"], self.gauss["sigma"]).rvs() + if self.box is not None: + # make sure flare is in dataset + if (self.box["start"] > tmp_grl["stop"][-1]) or ( + self.box["end"] < tmp_grl["start"][0]): + # this should never be the case, since + # there should be no events generated + break + livetime = self.box["end"] - self.box["start"] + time = rss.random.random() * livetime + time += self.box["start"] + # check if time is in grl + is_in_grl = (tmp_grl["start"] <= time) & ( + tmp_grl["stop"] >= time) + if np.any(is_in_grl): + events_._data_fields["time"][event_index] = time if shg_src_idx == 0: signal_events_dict[ds_idx] = events_ From d5c8a980ea52eeff54a39fa7ba4f667f9c97b488 Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 10:13:52 +0200 Subject: [PATCH 207/274] Update .gitignore Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- .gitignore | 3 --- 1 file changed, 3 deletions(-) diff --git a/.gitignore b/.gitignore index 574915fab8..b6e47617de 100644 --- a/.gitignore +++ b/.gitignore @@ -127,6 +127,3 @@ dmypy.json # Pyre type checker .pyre/ - -# testing skript -examples/test_time_analysis.py From 03901b28ff4e9a41d8d5e312035b57943704c51f Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 10:14:06 +0200 Subject: [PATCH 208/274] Update doc/user_manual.tex Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- doc/user_manual.tex | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/user_manual.tex b/doc/user_manual.tex index 34bea2aa0e..f7a80bedee 100644 --- a/doc/user_manual.tex +++ b/doc/user_manual.tex @@ -956,7 +956,7 @@ \section{Inverse CDF sampling of a bounded power-law} \end{enumerate} Hence, one can randomly draw energies according to the power-law distribution by generating uniformly distributed numbers between -0 and 1 and feedinf them to the inverse CDF formula, being careful of applying the correct normalization. +0 and 1 and passing them to the inverse CDF formula, being careful of applying the correct normalization. \bibliographystyle{unsrt} \bibliography{biblio} From 9342cbe4944ea408799ab4729f849d1a03f6f66e Mon Sep 17 00:00:00 2001 From: "Martin Wolf, PhD" Date: Mon, 3 Apr 2023 10:24:36 +0200 Subject: [PATCH 209/274] Update skyllh/analyses/i3/publicdata_ps/bkg_flux.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/bkg_flux.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/bkg_flux.py b/skyllh/analyses/i3/publicdata_ps/bkg_flux.py index 142e61749c..e9b9ab7e96 100644 --- a/skyllh/analyses/i3/publicdata_ps/bkg_flux.py +++ b/skyllh/analyses/i3/publicdata_ps/bkg_flux.py @@ -387,6 +387,12 @@ def get_pd_bkg_E_nu_sin_dec_nu(pd_atmo, pd_astro, log10_e_grid_edges): log10_e_grid_edges : (n_e_grid+1,)-shaped numpy ndarray The numpy ndarray holding the log10 values of the energy grid bin edges in GeV. + + Returns + ------- + pd_bkg : (n_sin_dec, n_e_grid)-shaped 2D numpy ndarray + The numpy ndarray holding total background probability density values + p_bkg(E_nu|sin(dec_nu)) in unit 1/GeV. """ pd_bkg = pd_atmo + pd_astro From 1ce1cda1d603ea438d81e349cb0ddfc8d51316c2 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 10:26:35 +0200 Subject: [PATCH 210/274] Update backgroundpdf.py --- .../i3/publicdata_ps/backgroundpdf.py | 43 ++++++++++--------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py index 3ee004c7c3..525718ed92 100644 --- a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py @@ -27,7 +27,7 @@ class PDEnergyPDF(EnergyPDF, UsesBinning): """This is the base class for IceCube specific energy PDF models. - IceCube energy PDFs depend soley on the energy and the + IceCube energy PDFs depend solely on the energy and the zenith angle, and hence, on the declination of the event. The IceCube energy PDF is modeled as a 1d histogram in energy, @@ -63,14 +63,13 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, The smoothing filter to use for smoothing the energy histogram. If None, no smoothing will be applied. kde_smoothing : bool - Apply a kde smoothing to the energy pdf for each sine of the - muon declination. + Apply a kde smoothing to the energy pdf for each bin in sin(dec). + This is useful for signal injections, because it ensures that the + background is not zero when injecting high energy events. Default: False. """ super(PDEnergyPDF, self).__init__() - # self.logger = logging.getLogger(__name__) - # Define the PDF axes. self.add_axis(PDFAxis(name='log_energy', vmin=logE_binning.lower_edge, @@ -127,22 +126,9 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, h = self._hist_smoothing_method.smooth(h) self._hist_mask_mc_covered_zero_physics = h > 0 - # Create a 2D histogram with only the data which has physics - # contribution. We will do the normalization along the logE - # axis manually. - data_weights = data_mcweight[~mask] * data_physicsweight[~mask] - (h, bins_logE, bins_sinDec) = np.histogram2d( - data_logE[~mask], data_sinDec[~mask], - bins=[ - logE_binning.binedges, sinDec_binning.binedges], - weights=data_weights, - range=[ - logE_binning.range, sinDec_binning.range], - density=False) - - # If a bandwidth is passed, apply a KDE-based smoothing with the given - # bw parameter as bandwidth for the fit. if kde_smoothing: + # If a bandwidth is passed, apply a KDE-based smoothing with the given + # bw parameter as bandwidth for the fit. if not isinstance(kde_smoothing, bool): raise ValueError( "The bandwidth parameter must be True or False!") @@ -166,6 +152,20 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, [kde_pdf[i].evaluate(logE_binning.bincenters) for i in range(len(sinDec_binning.bincenters))]).T + else: + # Create a 2D histogram with only the data which has physics + # contribution. We will do the normalization along the logE + # axis manually. + data_weights = data_mcweight[~mask] * data_physicsweight[~mask] + (h, bins_logE, bins_sinDec) = np.histogram2d( + data_logE[~mask], data_sinDec[~mask], + bins=[ + logE_binning.binedges, sinDec_binning.binedges], + weights=data_weights, + range=[ + logE_binning.range, sinDec_binning.range], + density=False) + # Calculate the normalization for each logE bin. Hence we need to sum # over the logE bins (axis 0) for each sin(dec) bin and need to divide # by the logE bin widths along the sin(dec) bins. The result array norm @@ -313,6 +313,7 @@ class PDMCBackgroundI3EnergyPDF(EnergyPDF, IsBackgroundPDF, UsesBinning): """This class provides a background energy PDF constructed from the public data and a monte-carlo background flux model. """ + def __init__( self, pdf_log10emu_sindecmu, log10emu_binning, sindecmu_binning, **kwargs): @@ -444,6 +445,6 @@ def get_prob(self, tdm, params=None, tl=None): sindecmu, self.get_binning('sin_dec').binedges) - 1 with TaskTimer(tl, 'Evaluating sindecmu-log10emu PDF.'): - pd = self._hist_logE_sinDec[(log10emu_idxs,sindecmu_idxs)] + pd = self._hist_logE_sinDec[(log10emu_idxs, sindecmu_idxs)] return pd From e6f693d2e2789f4cf4a1cae793d151de8c53e1d9 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 10:32:50 +0200 Subject: [PATCH 211/274] Update detsigyield.py --- .../analyses/i3/publicdata_ps/detsigyield.py | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/detsigyield.py b/skyllh/analyses/i3/publicdata_ps/detsigyield.py index 43d1af9c66..1dc2794c8c 100644 --- a/skyllh/analyses/i3/publicdata_ps/detsigyield.py +++ b/skyllh/analyses/i3/publicdata_ps/detsigyield.py @@ -31,9 +31,9 @@ class PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod, multiproc.IsParallelizable): - """Thus detector signal yield constructor class constructs a - detector signal yield instance for a varibale power law flux model, which - has the spectral index gama as fit parameter, assuming a point-like source. + """This detector signal yield constructor class constructs a + detector signal yield instance for a variable power law flux model, which + has the spectral index gamma as fit parameter, assuming a point-like source. It constructs a two-dimensional spline function in sin(dec) and gamma, using a :class:`scipy.interpolate.RectBivariateSpline`. Hence, the detector signal yield can vary with the declination and the spectral index, gamma, of the @@ -43,12 +43,13 @@ class PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( PowerLawFlux flux model. It is tailored to the IceCube detector at the South Pole, where the - effective area depends soley on the zenith angle, and hence on the + effective area depends solely on the zenith angle, and hence on the declination, of the source. It takes the effective area for the detector signal yield from the auxilary detector effective area data file given by the public data. """ + def __init__( self, gamma_grid, spline_order_sinDec=2, spline_order_gamma=2, ncpu=None): @@ -110,19 +111,19 @@ def construct_detsigyield( # Check for the correct data types of the input arguments. if(not isinstance(dataset, Dataset)): raise TypeError('The dataset argument must be an instance of ' - 'Dataset!') + 'Dataset!') if(not isinstance(data, DatasetData)): raise TypeError('The data argument must be an instance of ' - 'DatasetData!') + 'DatasetData!') if(not self.supports_fluxmodel(fluxmodel)): raise TypeError('The DetSigYieldImplMethod "%s" does not support ' - 'the flux model "%s"!'%( - self.__class__.__name__, - fluxmodel.__class__.__name__)) + 'the flux model "%s"!' % ( + self.__class__.__name__, + fluxmodel.__class__.__name__)) if((not isinstance(livetime, float)) and (not isinstance(livetime, Livetime))): raise TypeError('The livetime argument must be an instance of ' - 'float or Livetime!') + 'float or Livetime!') # Get integrated live-time in days. livetime_days = get_integrated_livetime_in_days(livetime) @@ -188,7 +189,7 @@ def hist( ((energy_bin_edges_lower, energy_bin_edges_upper, aeff_arr, - fluxmodel.copy({'gamma':gamma})), {}) + fluxmodel.copy({'gamma': gamma})), {}) for gamma in gamma_grid.grid ] h = np.vstack( @@ -201,7 +202,7 @@ def hist( sin_true_dec_binedges_lower + sin_true_dec_binedges_upper) log_spl_sinDec_gamma = scipy.interpolate.RectBivariateSpline( sin_dec_bincenters, gamma_grid.grid, np.log(h), - kx = self.spline_order_sinDec, ky = self.spline_order_gamma, s = 0) + kx=self.spline_order_sinDec, ky=self.spline_order_gamma, s=0) # Construct the detector signal yield instance with the created spline. sin_dec_binedges = np.concatenate( From 11e5b120c260c75022c9c96148b8f95513a405de Mon Sep 17 00:00:00 2001 From: "Martin Wolf, PhD" Date: Mon, 3 Apr 2023 10:37:47 +0200 Subject: [PATCH 212/274] Update skyllh/analyses/i3/publicdata_ps/bkg_flux.py This function is available also through the FctSpline1D in publicdata_ps/utils.py. So it can be removed here. Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/bkg_flux.py | 27 -------------------- 1 file changed, 27 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/bkg_flux.py b/skyllh/analyses/i3/publicdata_ps/bkg_flux.py index e9b9ab7e96..e39223ddf4 100644 --- a/skyllh/analyses/i3/publicdata_ps/bkg_flux.py +++ b/skyllh/analyses/i3/publicdata_ps/bkg_flux.py @@ -27,33 +27,6 @@ def get_dOmega(dec_min, dec_max): return 2*np.pi*(np.sin(dec_max) - np.sin(dec_min)) -def eval_spline(x, spl): - values = spl(x) - values = np.nan_to_num(values, nan=0) - return values - - -def create_spline(x, y, norm=False): - """Creates the spline representation of the x and y values. - """ - - spline = interpolate.PchipInterpolator( - x, y, extrapolate=False - ) - - if norm: - spl_norm = integrate.quad( - eval_spline, - x[0], x[-1], - args=(spline,), - limit=200, full_output=1)[0] - - return spline, spl_norm - - else: - return spline - - def southpole_zen2dec(zen): """Converts zenith angles at the South Pole to declination angles. From 0a15362e20ef5316a9d358435fe463b4fc98dc39 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 10:38:14 +0200 Subject: [PATCH 213/274] Clean PublicData_10y_ps_wMC.py --- skyllh/datasets/i3/PublicData_10y_ps_wMC.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py index d0e17ddf23..3d506a09bf 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py @@ -563,17 +563,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'Zenith[deg]': 'zen' }) -# dsc.set_mc_field_name_renaming_dict({ -# 'true_dec': 'true_dec', -# 'true_ra': 'true_ra', -# 'true_energy': 'true_energy', -# 'log_energy': 'log_energy', -# 'ra': 'ra', -# 'dec': 'dec', -# 'ang_err': 'ang_err', -# 'mcweight': 'mcweight' -# }) - def add_run_number(data): exp = data.exp mc = data.mc From 2c467226724cd9fedc87b1d2cbe610ad11db3032 Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 10:40:20 +0200 Subject: [PATCH 214/274] Update skyllh/core/storage.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/storage.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/skyllh/core/storage.py b/skyllh/core/storage.py index 126e63b669..89be1323a3 100644 --- a/skyllh/core/storage.py +++ b/skyllh/core/storage.py @@ -488,6 +488,22 @@ def _load_file(self, pathfilename, keep_fields, dtype_convertions, dtype_convertion_except_fields): """Loads the given file. + Parameters + ---------- + pathfilename : str + The fully qualified file name of the data file that + need to be loaded. + keep_fields : str | sequence of str | None + Load the data into memory only for these data fields. If set to + ``None``, all in-file-present data fields are loaded into memory. + dtype_convertions : dict | None + If not None, this dictionary defines how data fields of specific + data types get converted into the specified data types. + This can be used to use less memory. + dtype_convertion_except_fields : str | sequence of str | None + The sequence of field names whose data type should not get + converted. + Returns ------- data : DataFieldRecordArray instance From f959389554b384dca7314ebfa2f2f3721be9b708 Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 10:41:06 +0200 Subject: [PATCH 215/274] Update skyllh/core/storage.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/storage.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/skyllh/core/storage.py b/skyllh/core/storage.py index 89be1323a3..b05a85c164 100644 --- a/skyllh/core/storage.py +++ b/skyllh/core/storage.py @@ -419,6 +419,17 @@ class TextFileLoader(FileLoader): def __init__(self, pathfilenames, header_comment='#', header_separator=None, **kwargs): """Creates a new file loader instance for a text data file. + + Parameters + ---------- + pathfilenames : str | sequence of str + The sequence of fully qualified file names of the data files that + need to be loaded. + header_comment : str + The character that defines a comment line in the text file. + header_separator : str | None + The separator of the header field names. If None, it assumes + whitespaces. """ super().__init__(pathfilenames, **kwargs) From 5393704c256478b274faa38138800402622a5597 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 11:06:41 +0200 Subject: [PATCH 216/274] Update utils.py --- skyllh/analyses/i3/publicdata_ps/utils.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/utils.py b/skyllh/analyses/i3/publicdata_ps/utils.py index d7382562d3..8aaa38fd65 100644 --- a/skyllh/analyses/i3/publicdata_ps/utils.py +++ b/skyllh/analyses/i3/publicdata_ps/utils.py @@ -26,7 +26,7 @@ class from scipy. x_binedges : (n_x+1,)-shaped 1D numpy ndarray The numpy ndarray holding the bin edges of the x-axis. norm : bool - Switch + Whether to precalculate and save normalization internally. """ super().__init__(**kwargs) @@ -60,6 +60,8 @@ def __call__(self, x, oor_value=0): x : (n_x,)-shaped 1D numpy ndarray The numpy ndarray holding the x values at which the spline should get evaluated. + oor_value : float + The value for out-of-range (oor) coordinates. Returns ------- @@ -218,12 +220,12 @@ def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): def create_energy_cut_spline(ds, exp_data, spl_smooth): - # Create the spline for the declination-dependent energy cut - # that the signal generator needs for injection in the southern sky - - # Some special conditions are needed for IC79 and IC86_I, because - # their experimental dataset shows events that should probably have - # been cut by the IceCube selection. + '''Create the spline for the declination-dependent energy cut + that the signal generator needs for injection in the southern sky + Some special conditions are needed for IC79 and IC86_I, because + their experimental dataset shows events that should probably have + been cut by the IceCube selection. + ''' data_exp = exp_data.copy(keep_fields=['sin_dec', 'log_energy']) if ds.name == 'IC79': m = np.invert(np.logical_and( From 8af0d59290ba23ab2c360148b1fbdacee17e51a3 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:09:38 +0200 Subject: [PATCH 217/274] Update skyllh/core/signalpdf.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/signalpdf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index 93ecfec517..864a5d5c96 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -503,7 +503,6 @@ def get_prob(self, tdm, fitparams=None, tl=None): fitparams : None Unused interface argument. - tl : TimeLord instance | None The optional TimeLord instance to use for measuring timing information. From b4d6dd11a1bc156a98d05049b3c0ff09f706800c Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:09:49 +0200 Subject: [PATCH 218/274] Update skyllh/core/signalpdf.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/signalpdf.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index 864a5d5c96..ba70b2b61d 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -535,7 +535,10 @@ def __init__(self, grl, start, end, **kwargs): ---------- grl : ndarray Array of the detector good run list - + start : float + Start time of box profile. + end : float + End time of box profile. """ super(SignalBoxTimePDF, self).__init__(**kwargs) self.start = start From 39a1580fda7347da38a6cf8b28d835ef0e8212dc Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:09:57 +0200 Subject: [PATCH 219/274] Update skyllh/core/signalpdf.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/signalpdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index ba70b2b61d..451f01bfa3 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -547,7 +547,7 @@ def __init__(self, grl, start, end, **kwargs): def cdf(self, t): - """ Compute the cumulative density function for the box pdf. This is needed for normalization. + """Compute the cumulative density function for the box pdf. This is needed for normalization. Parameters ---------- From 394f449e9889da9ce5beaa5d4696a140d5ba487d Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:10:15 +0200 Subject: [PATCH 220/274] Update skyllh/core/signalpdf.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/signalpdf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index 451f01bfa3..0830ef3603 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -623,7 +623,6 @@ def get_prob(self, tdm, fitparams=None, tl=None): fitparams : None Unused interface argument. - tl : TimeLord instance | None The optional TimeLord instance to use for measuring timing information. From 12d1e09508271a98687d490c68989bf4f6e3c2f1 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:10:32 +0200 Subject: [PATCH 221/274] Update skyllh/core/signalpdf.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/signalpdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index 0830ef3603..c493e240d5 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -583,7 +583,7 @@ def cdf(self, t): def norm_uptime(self, t): - """compute the normalization with the dataset uptime. Distributions like + """Compute the normalization with the dataset uptime. Distributions like scipy.stats.norm are normalized (-inf, inf). These must be re-normalized such that the function sums to 1 over the finite good run list domain. From 24417bba48456321484e05fa805f8663c709f032 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:16:54 +0200 Subject: [PATCH 222/274] Update skyllh/analyses/i3/publicdata_ps/expectation_maximization.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- .../publicdata_ps/expectation_maximization.py | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py index 7f844f45c9..c629f42f14 100644 --- a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py +++ b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py @@ -30,20 +30,23 @@ def expectation_em(ns, mu, sigma, t, sob): def maximization_em(e_sig, t): """ - maximization step of expectation maximization + Maximization step of expectation maximization. Parameters ---------- - - e_sig: [array] the weights for each event form the expectation step - t: [array] the times of each event + e_sig : list of 1d ndarray of float + The weights for each event from the expectation step. + t : 1d ndarray of float + The times of each event. Returns ------- - mu (float) : best fit mean - sigma (float) : best fit width - ns (float) : scaling of gaussian - + mu : list of float + Best fit mean time of the gaussian flare. + sigma : list of float + Best fit sigma of the gaussian flare. + ns : list of float + Best fit number of signal neutrinos, as weight for the gaussian flare. """ mu = [] sigma = [] From 058cc98e7ad0fe3758d597ffd057b399c0d67311 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:17:23 +0200 Subject: [PATCH 223/274] Update skyllh/analyses/i3/publicdata_ps/expectation_maximization.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- .../publicdata_ps/expectation_maximization.py | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py index c629f42f14..310bd1bab0 100644 --- a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py +++ b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py @@ -3,19 +3,27 @@ def expectation_em(ns, mu, sigma, t, sob): """ - Expectation step of expectation maximization + Expectation step of expectation maximization. Parameters ---------- - ns: the number of signal neutrinos, as weight for the gaussian flare - mu: the mean of the gaussian flare - sigma: sigma of gaussian flare - t: [array] times of the events - sob: [array] the signal over background values of events + ns : float | 1d ndarray of float + The number of signal neutrinos, as weight for the gaussian flare. + mu : float | 1d ndarray of float + The mean time of the gaussian flare. + sigma: float | 1d ndarray of float + Sigma of the gaussian flare. + t : 1d ndarray of float + Times of the events. + sob : 1d ndarray of float + The signal over background values of events. Returns ------- - array, weighted "responsibility" function of each event to belong to the flare + expectation : list of 1d ndarray of float + Weighted "responsibility" function of each event to belong to the flare. + sum_log_denom : float + Sum of log of denominators. """ b_term = (1 - np.cos(10 / 180 * np.pi)) / 2 N = len(t) From 1ffd1aefa53687e5a65394467051483c633c3561 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:17:46 +0200 Subject: [PATCH 224/274] Update skyllh/analyses/i3/publicdata_ps/expectation_maximization.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/expectation_maximization.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py index 310bd1bab0..4316717cc2 100644 --- a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py +++ b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py @@ -25,6 +25,10 @@ def expectation_em(ns, mu, sigma, t, sob): sum_log_denom : float Sum of log of denominators. """ + ns = np.atleast_1d(ns) + mu = np.atleast_1d(mu) + sigma = np.atleast_1d(sigma) + b_term = (1 - np.cos(10 / 180 * np.pi)) / 2 N = len(t) e_sig = [] From de1b5f10cb79492b21df01e5919d9feac3307d5d Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:18:02 +0200 Subject: [PATCH 225/274] Update skyllh/analyses/i3/publicdata_ps/expectation_maximization.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py index 4316717cc2..409ac2ec1c 100644 --- a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py +++ b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py @@ -34,7 +34,7 @@ def expectation_em(ns, mu, sigma, t, sob): e_sig = [] for i in range(len(ns)): e_sig.append(norm(loc=mu[i], scale=sigma[i]).pdf(t) * sob * ns[i]) - e_bg = (N - np.sum(ns)) / (np.max(t) - np.min(t)) / b_term # 2198.918456004788 + e_bg = (N - np.sum(ns)) / (np.max(t) - np.min(t)) / b_term denom = sum(e_sig) + e_bg return [e / denom for e in e_sig], np.sum(np.log(denom)) From 1edb1f930276cb0cc9e1b7d9a290651caf400a47 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:18:55 +0200 Subject: [PATCH 226/274] Update skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py index 0f2f92bf39..6b03fee2dc 100644 --- a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py @@ -191,7 +191,7 @@ def create_analysis( Returns ------- - analysis : TimeIntegratedMultiDatasetSingleSourceAnalysis + analysis : TimeDependentSingleDatasetSingleSourceAnalysis The Analysis instance for this analysis. """ From a73d5615c36e1f91857ede4c075429ff58b864ea Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:20:21 +0200 Subject: [PATCH 227/274] Update skyllh/analyses/i3/publicdata_ps/signal_generator.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- .../i3/publicdata_ps/signal_generator.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index b2a72e34c5..c1caaa53b8 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -442,7 +442,29 @@ class PDTimeDependentSignalGenerator(PDSignalGenerator): def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhratio=None, energy_cut_splines=None, cut_sindec=None, gauss=None, box=None): - + """ + Parameters + ---------- + src_hypo_group_manager : SourceHypoGroupManager instance + The instance of SourceHypoGroupManager that defines the list of + sources, i.e. the list of SourceModel instances. + dataset_list : list of Dataset instances + The list of Dataset instances for which signal events should get + generated for. + data_list : list of DatasetData instances + The list of DatasetData instances holding the actual data of each + dataset. The order must match the order of ``dataset_list``. + llhratio : LLHRatio + The likelihood ratio object contains the datasets signal weights + needed for distributing the event generation among the different + datsets. + energy_cut_splines : list of UnivariateSpline + cut_sindec : float + gauss : dict | None + None or dictionary with {"mu": float, "sigma": float}. + box : dict | None + None or dictionary with {"start": float, "end": float}. + """ if gauss is None and box is None: raise ValueError( "Either box or gauss keywords must define the neutrino flare.") From f2ca26a64a8a8046c99c77067eb2f4405cdd55ab Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:21:41 +0200 Subject: [PATCH 228/274] Update skyllh/analyses/i3/publicdata_ps/signal_generator.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index c1caaa53b8..f15b1f2b97 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -479,12 +479,14 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, self.gauss = gauss def set_flare(self, gauss=None, box=None): - """ change the flare to something new + """Set the neutrino flare given parameters. Parameters ---------- - gauss : None or dictionary with {"mu": float, "sigma": float} - box : None or dictionary with {"start": float, "end": float} + gauss : dict | None + None or dictionary with {"mu": float, "sigma": float}. + box : dict | None + None or dictionary with {"start": float, "end": float}. """ if gauss is None and box is None: raise ValueError( From 6510f86281aa4447006d8ffc50e0f36d7fe2dbc2 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:25:42 +0200 Subject: [PATCH 229/274] Update skyllh/core/signalpdf.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/signalpdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index c493e240d5..79dfa22840 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -460,7 +460,7 @@ def __init__(self, grl, mu, sigma, **kwargs): def norm_uptime(self, t): - """compute the normalization with the dataset uptime. Distributions like + """Compute the normalization with the dataset uptime. Distributions like scipy.stats.norm are normalized (-inf, inf). These must be re-normalized such that the function sums to 1 over the finite good run list domain. From fce9f6385fa9cc22c6905029424d5bcc0e278228 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 11:30:30 +0200 Subject: [PATCH 230/274] Double quotes for doc-string. --- skyllh/analyses/i3/publicdata_ps/utils.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/utils.py b/skyllh/analyses/i3/publicdata_ps/utils.py index 8aaa38fd65..00c381f37b 100644 --- a/skyllh/analyses/i3/publicdata_ps/utils.py +++ b/skyllh/analyses/i3/publicdata_ps/utils.py @@ -220,12 +220,12 @@ def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): def create_energy_cut_spline(ds, exp_data, spl_smooth): - '''Create the spline for the declination-dependent energy cut + """Create the spline for the declination-dependent energy cut that the signal generator needs for injection in the southern sky Some special conditions are needed for IC79 and IC86_I, because their experimental dataset shows events that should probably have been cut by the IceCube selection. - ''' + """ data_exp = exp_data.copy(keep_fields=['sin_dec', 'log_energy']) if ds.name == 'IC79': m = np.invert(np.logical_and( From ea92f3819c6d7ce4067ed80f295aadef658bdfa4 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:30:36 +0200 Subject: [PATCH 231/274] Update skyllh/core/analysis.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/analysis.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 80342925bc..13c5088b08 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -1682,13 +1682,14 @@ def change_fluxmodel_gamma(self, gamma): def change_signal_time(self, gauss=None, box=None): - """ change the signal injection to gauss or box + """Change the signal injection to gauss or box. Parameters ---------- - analysis : analysis instance - gauss : None or dictionary {"mu": float, "sigma": float} - box : None or dictionary {"start" : float, "end" : float} + gauss : dict | None + None or dictionary {"mu": float, "sigma": float}. + box : dict | None + None or dictionary {"start" : float, "end" : float}. """ self.sig_generator.set_flare(box=box, gauss=gauss) From 91a451f233e4d5dc4fb6cd1bb2acd5941c62e048 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Mon, 3 Apr 2023 11:51:04 +0200 Subject: [PATCH 232/274] Apply suggestions from code review Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/analysis.py | 119 +++++++++++++++++++++++------------ skyllh/core/backgroundpdf.py | 6 +- skyllh/core/signalpdf.py | 5 +- 3 files changed, 86 insertions(+), 44 deletions(-) diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 13c5088b08..f8afd7c37f 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -1615,18 +1615,45 @@ class TimeDependentSingleDatasetSingleSourceAnalysis(TimeIntegratedMultiDatasetS def __init__(self, src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, bkg_gen_method=None, sig_generator_cls=None): - + """Creates a new time-dependent single dataset point-like source analysis + assuming a single source. + + Parameters + ---------- + src_hypo_group_manager : instance of SourceHypoGroupManager + The instance of SourceHypoGroupManager, which defines the groups of + source hypotheses, their flux model, and their detector signal + efficiency implementation method. + src_fitparam_mapper : instance of SourceFitParameterMapper + The SourceFitParameterMapper instance managing the global fit + parameters and their relation to the individual sources. + fitparam_ns : FitParameter instance + The FitParameter instance defining the fit parameter ns. + test_statistic : TestStatistic instance + The TestStatistic instance that defines the test statistic function + of the analysis. + bkg_gen_method : instance of BackgroundGenerationMethod | None + The instance of BackgroundGenerationMethod that should be used to + generate background events for pseudo data. This can be set to None, + if there is no need to generate background events. + sig_generator_cls : SignalGeneratorBase class | None + The signal generator class used to create the signal generator + instance. + If set to None, the `SignalGenerator` class is used. + """ super().__init__(src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, bkg_gen_method, sig_generator_cls) def change_time_pdf(self, gauss=None, box=None): - """ changes the time pdf + """Changes the time pdf. + Parameters ---------- - gauss : None or dictionary with {"mu": float, "sigma": float} - box : None or dictionary with {"start": float, "end": float} - + gauss : dict | None + None or dictionary with {"mu": float, "sigma": float}. + box : dict | None + None or dictionary with {"start": float, "end": float}. """ if gauss is None and box is None: raise TypeError("Either gauss or box have to be specified as time pdf.") @@ -1652,17 +1679,18 @@ def change_time_pdf(self, gauss=None, box=None): def get_energy_spatial_signal_over_backround(self, fitparams): - """ returns the signal over background ratio for - (spatial_signal * energy_signal) / (spatial_background * energy_background) + """Returns the signal over background ratio for + (spatial_signal * energy_signal) / (spatial_background * energy_background). Parameter --------- - analysis : analysis instance - fitparams : dictionary with {"gamma": float} for energy pdf + fitparams : dict + Dictionary with {"gamma": float} for energy pdf. Returns ------- - product of spatial and energy signal over background pdfs + ratio : 1d ndarray + Product of spatial and energy signal over background pdfs. """ ratio = self._llhratio.llhratio_list[0].pdfratio_list[0].get_ratio(self._tdm_list[0], fitparams) ratio *= self._llhratio.llhratio_list[0].pdfratio_list[1].get_ratio(self._tdm_list[0], fitparams) @@ -1671,11 +1699,12 @@ def get_energy_spatial_signal_over_backround(self, fitparams): def change_fluxmodel_gamma(self, gamma): - """ set new gamma for the flux model + """Set new gamma for the flux model. + Parameter --------- - analysis : analysis instance - gamma : spectral index for flux model + gamma : float + Spectral index for flux model. """ self.src_hypo_group_manager.src_hypo_group_list[0].fluxmodel.gamma = gamma @@ -1696,24 +1725,29 @@ def change_signal_time(self, gauss=None, box=None): def em_fit(self, fitparams, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, initial_width=5000, remove_time=None): - """ - run expectation maximization + """Run expectation maximization. Parameters ---------- - - fitparams : dictionary with value for gamma, e.g. {'gamma': 2} - n : how many gaussians flares we are looking for - tol : the stopping criteria for expectation maximization. This is the difference in the normalized likelihood over the - last 20 iterations - iter_max : the maximum number of iterations, even if stopping criteria tolerance (tol) is not yet reached - sob_thres : set a minimum threshold for signal over background ratios. ratios below this threshold will be removed - initial_width : starting width for the gaussian flare in days + fitparams : dict + Dictionary with value for gamma, e.g. {'gamma': 2}. + n : int + How many gaussians flares we are looking for. + tol : float + the stopping criteria for expectation maximization. This is the difference in the normalized likelihood over the + last 20 iterations. + iter_max : int + The maximum number of iterations, even if stopping criteria tolerance (`tol`) is not yet reached. + sob_thres : float + Set a minimum threshold for signal over background ratios. Ratios below this threshold will be removed. + initial_width : float + Starting width for the gaussian flare in days. + remove_time : float | None + Time information of event that should be removed. Returns ------- mean flare time, flare width, normalization factor for time pdf - """ ratio = self.get_energy_spatial_signal_over_backround(fitparams) @@ -1758,15 +1792,18 @@ def em_fit(self, fitparams, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, initia def run_gamma_scan_single_flare(self, remove_time=None, gamma_min=1, gamma_max=5, n_gamma=51): - """ run em for different gammas in the signal energy pdf + """Run em for different gammas in the signal energy pdf Parameters ---------- - - remove_time : time information of event that should be removed - gamma_min : lower bound for gamma scan - gamma_max : upper bound for gamma scan - n_gamma : number of steps for gamma scan + remove_time : float + Time information of event that should be removed. + gamma_min : float + Lower bound for gamma scan. + gamma_max : float + Upper bound for gamma scan. + n_gamma : int + Number of steps for gamma scan. Returns ------- @@ -1774,7 +1811,7 @@ def run_gamma_scan_single_flare(self, remove_time=None, gamma_min=1, gamma_max=5 """ dtype = [("gamma", "f8"), ("mu", "f8"), ("sigma", "f8"), ("ns_em", "f8")] - results = np.empty(51, dtype=dtype) + results = np.empty(n_gamma, dtype=dtype) for index, g in enumerate(np.linspace(gamma_min, gamma_max, n_gamma)): mu, sigma, ns = self.em_fit({"gamma": g}, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, @@ -1785,21 +1822,21 @@ def run_gamma_scan_single_flare(self, remove_time=None, gamma_min=1, gamma_max=5 def calculate_TS(self, em_results, rss): - """ calculate the best TS value for the expectation maximization gamma scan + """Calculate the best TS value for the expectation maximization gamma scan. Parameters ---------- - - em_results : - rss : random state service for optimization + em_results : 1d ndarray of tuples + Gamma scan result. + rss : instance of RandomStateService + The instance of RandomStateService that should be used to generate + random numbers from. Returns ------- float maximized TS value tuple(gamma from em scan [float], best fit mean time [float], best fit width [float]) (float ns, float gamma) fitparams from TS optimization - - """ max_TS = 0 best_time = None @@ -1817,11 +1854,13 @@ def calculate_TS(self, em_results, rss): def unblind_flare(self, remove_time=None): - """ rum EM on unscrambeled data. Similar to the original analysis, remove the alert event. \ + """Run EM on unscrambled data. Similar to the original analysis, remove the alert event. + Parameters ---------- - - remove_time : time of event that should be removed from dataset prior to analysis. In the case of the TXS analysis: remove_time=58018.8711856 + remove_time : float + Time information of event that should be removed. + In the case of the TXS analysis: remove_time=58018.8711856 Returns ------- diff --git a/skyllh/core/backgroundpdf.py b/skyllh/core/backgroundpdf.py index e08361d63b..aee903fc2c 100644 --- a/skyllh/core/backgroundpdf.py +++ b/skyllh/core/backgroundpdf.py @@ -173,11 +173,11 @@ def get_prob(self, tdm, fitparams=None, tl=None): tdm : TrialDataManager Unused interface argument - fitparams : None Unused interface argument. - - tl : TimeLord instance for timing + tl : instance of TimeLord | None + The optional instance of TimeLord that should be used to collect + timing information about this method. Returns ------- diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index 79dfa22840..749f3f351c 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -451,7 +451,10 @@ def __init__(self, grl, mu, sigma, **kwargs): ---------- grl : ndarray Array of the detector good run list - + mu : float + Mean of the gaussian flare. + sigma : float + Sigma of the gaussian flare. """ super(SignalGaussTimePDF, self).__init__(**kwargs) self.mu = mu From e698970aa3209cdc4d700bbb69a1ca0f750aef3a Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Mon, 3 Apr 2023 13:42:17 +0200 Subject: [PATCH 233/274] Update analysis path and fix typo Didn't update outputs as we should rerun the notebook anyways after all changes are merged. --- doc/sphinx/tutorials/publicdata_ps.ipynb | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index bac0f57156..95dd49a50d 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -161,7 +161,7 @@ "metadata": {}, "outputs": [], "source": [ - "from skyllh.analyses.i3.publicdata_ps.trad_ps import create_analysis" + "from skyllh.analyses.i3.publicdata_ps.time_integrated_ps import create_analysis" ] }, { @@ -173,7 +173,7 @@ "name": "stdout", "output_type": "stream", "text": [ - "Help on function create_analysis in module skyllh.analyses.i3.publicdata_ps.trad_ps:\n", + "Help on function create_analysis in module skyllh.analyses.i3.publicdata_ps.time_integrated_ps:\n", "\n", "create_analysis(datasets, source, refplflux_Phi0=1, refplflux_E0=1000.0, refplflux_gamma=2, ns_seed=10.0, gamma_seed=3, kde_smoothing=False, minimizer_impl='LBFGS', cap_ratio=False, compress_data=False, keep_data_fields=None, optimize_delta_angle=10, tl=None, ppbar=None)\n", " Creates the Analysis instance for this particular analysis.\n", @@ -636,7 +636,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "Using the `evalaute` method of the `LLHRatio` class we can scan the log-likelihood ratio space and create a contour plot showing the best fit and the 95% quantile." + "Using the `evaluate` method of the `LLHRatio` class we can scan the log-likelihood ratio space and create a contour plot showing the best fit and the 95% quantile." ] }, { From 277b4958c5a9d8cb72e0af2e3bbff9c3999c1a09 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 15:10:06 +0200 Subject: [PATCH 234/274] Remove unused, commented out functions. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 119 -------------------- 1 file changed, 119 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index a42592926e..798b758983 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -277,70 +277,6 @@ def get_aeff_for_decnu(self, decnu): return aeff - # def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): - # """Calculates the detection probability density p(E_nu|sin_dec) in - # unit GeV^-1 for the given true energy values. - - # Parameters - # ---------- - #sin_true_dec : float - # The sin of the true declination. - # true_e : (n,)-shaped 1d numpy ndarray of float - # The values of the true energy in GeV for which the probability - # density value should get calculated. - - # Returns - # ------- - # det_pd : (n,)-shaped 1d numpy ndarray of float - # The detection probability density values for the given true energy - # value. - # """ - #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - - #dE = np.diff(np.power(10, self.log_true_e_binedges)) - - #det_pdf = aeff / np.sum(aeff) / dE - - #x = np.power(10, self.log_true_e_bincenters) - #y = det_pdf - #tck = interpolate.splrep(x, y, k=1, s=0) - - #det_pd = interpolate.splev(true_e, tck, der=0) - - # return det_pd - - # def get_detection_pd_in_log10E_for_sin_true_dec( - # self, sin_true_dec, log10_true_e): - # """Calculates the detection probability density p(E_nu|sin_dec) in - # unit log10(GeV)^-1 for the given true energy values. - - # Parameters - # ---------- - #sin_true_dec : float - # The sin of the true declination. - # log10_true_e : (n,)-shaped 1d numpy ndarray of float - # The log10 values of the true energy in GeV for which the - # probability density value should get calculated. - - # Returns - # ------- - # det_pd : (n,)-shaped 1d numpy ndarray of float - # The detection probability density values for the given true energy - # value. - # """ - #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - - #dlog10E = np.diff(self.log_true_e_binedges) - - #det_pdf = aeff / np.sum(aeff) / dlog10E - - # spl = interpolate.splrep( - # self.log_true_e_bincenters, det_pdf, k=1, s=0) - - #det_pd = interpolate.splev(log10_true_e, spl, der=0) - - # return det_pd - def get_detection_prob_for_decnu( self, decnu, enu_min, enu_max, enu_range_min, enu_range_max): """Calculates the detection probability for given true neutrino energy @@ -440,58 +376,3 @@ def _eval_spl_func(x): return det_prob - # def get_aeff_integral_for_sin_true_dec( - # self, sin_true_dec, log_true_e_min, log_true_e_max): - # """Calculates the integral of the effective area using the trapezoid - # method. - - # Returns - # ------- - #integral : float - # The integral in unit cm^2 GeV. - # """ - #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - - # integral = ( - # (np.power(10, log_true_e_max) - - # np.power(10, log_true_e_min)) * - # 0.5 * - # (np.interp(log_true_e_min, self.log_true_e_bincenters, aeff) + - # np.interp(log_true_e_max, self.log_true_e_bincenters, aeff)) - # ) - - # return integral - - # def get_aeff(self, sin_true_dec, log_true_e): - # """Retrieves the effective area for the given sin(dec_true) and - # log(E_true) value pairs. - - # Parameters - # ---------- - # sin_true_dec : (n,)-shaped 1D ndarray - # The sin(dec_true) values. - # log_true_e : (n,)-shaped 1D ndarray - # The log(E_true) values. - - # Returns - # ------- - # aeff : (n,)-shaped 1D ndarray - # The 1D ndarray holding the effective area values for each value - # pair. For value pairs outside the effective area data zero is - # returned. - # """ - # valid = ( - # (sin_true_dec >= self.sin_true_dec_binedges[0]) & - # (sin_true_dec <= self.sin_true_dec_binedges[-1]) & - # (log_true_e >= self.log_true_e_binedges[0]) & - #(log_true_e <= self.log_true_e_binedges[-1]) - # ) - # sin_true_dec_idxs = np.digitize( - # sin_true_dec[valid], self.sin_true_dec_binedges) - 1 - # log_true_e_idxs = np.digitize( - # log_true_e[valid], self.log_true_e_binedges) - 1 - - #aeff = np.zeros((len(valid),), dtype=np.double) - #aeff[valid] = self.aeff_arr[sin_true_dec_idxs,log_true_e_idxs] - - # return aeff From c7b50c7b4c0a09db29ccdc57c9039cefe32ecad9 Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:13:46 +0200 Subject: [PATCH 235/274] Update skyllh/analyses/i3/publicdata_ps/signalpdf.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 7452cad55a..80869c247a 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -131,7 +131,7 @@ def get_prob(self, tdm, params=None, tl=None): Returns ------- - prob : (N_events,)-shaped numpy ndarray + pd : (N_events,)-shaped numpy ndarray The 1D numpy ndarray with the probability density for each event. grads : (N_fitparams,N_events)-shaped ndarray | None The 2D numpy ndarray holding the gradients of the PDF w.r.t. From bde66a811dd034f1a01a4df1377e0f0092c1a640 Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:23:15 +0200 Subject: [PATCH 236/274] Apply suggestions to signalpdf.py from code review Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 80869c247a..2d9665e1ef 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -95,6 +95,11 @@ def get_pd_by_log10_reco_e(self, log10_reco_e, tl=None): tl : TimeLord instance | None The optional TimeLord instance that should be used to measure timing information. + + Returns + ------- + pd : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. """ # Select events that actually have a signal energy PDF. # All other events will get zero signal probability density. @@ -173,6 +178,11 @@ def __init__( The FluxModel instance that defines the source's flux model. fitparam_grid_set : ParameterGrid | ParameterGridSet instance The parameter grid set defining the grids of the fit parameters. + ncpu : int | None + The number of CPUs to utilize. Global setting will take place if + not specified, i.e. set to None. + ppbar : ProgressBar instance | None + The instance of ProgressBar for the optional parent progress bar. """ self._logger = get_logger(module_classname(self)) @@ -312,7 +322,7 @@ def create_reco_e_pdf_for_true_e(idx, true_e_idx): """This functions creates a spline for the reco energy distribution given a true neutrino engery. """ - # Create the enegry PDF f_e = P(log10_E_reco|dec) = + # Create the energy PDF f_e = P(log10_E_reco|dec) = # \int dPsi dang_err P(E_reco,Psi,ang_err). f_e = np.sum( sm_pdf[true_e_idx] * From 16a2d8511ea4e7113adba5ddaca573e26580082e Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:28:45 +0200 Subject: [PATCH 237/274] Apply suggestions to time_integrated_ps.py from code review Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- .../analyses/i3/publicdata_ps/time_integrated_ps.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/time_integrated_ps.py b/skyllh/analyses/i3/publicdata_ps/time_integrated_ps.py index 992d0936ef..a111c59567 100644 --- a/skyllh/analyses/i3/publicdata_ps/time_integrated_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/time_integrated_ps.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -"""The trad_ps analysis is a multi-dataset time-integrated single source +"""The time_integrated_ps analysis is a multi-dataset time-integrated single source analysis with a two-component likelihood function using a spacial and an energy event PDF. """ @@ -458,14 +458,4 @@ def create_analysis( print('ns_fit = %g' % (fitparam_dict['ns'])) print('gamma_fit = %g' % (fitparam_dict['gamma'])) - """ - # Generate some signal events. - with tl.task_timer('Generating signal events.'): - (n_sig, signal_events_dict) =\ - ana.sig_generator.generate_signal_events(rss, 100) - - print('n_sig: %d', n_sig) - print('signal datasets: '+str(signal_events_dict.keys())) - """ - print(tl) From f94605d8e305afe5b3005b2db8959be78c0286ad Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:32:29 +0200 Subject: [PATCH 238/274] Apply suggestions to signal_generator.py from code review Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- .../i3/publicdata_ps/signal_generator.py | 25 ++++++++++++++----- 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index f15b1f2b97..2161109d53 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -27,6 +27,18 @@ class PDDatasetSignalGenerator(object): def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): """Creates a new instance of the signal generator for generating signal events from a specific public data dataset. + + Parameters: + ----------- + ds : Dataset instance + Dataset instance for which signal events should get + generated for. + src_dec : float + The declination of the source in radians. + effA : PDAeff | None + Representation of the effective area provided by the public data. + sm : PublicDataSmearingMatrix | None + Representation of the smearing matrix provided by the public data. """ super().__init__(**kwargs) @@ -217,9 +229,10 @@ def _generate_events( @staticmethod @np.vectorize def energy_filter(events, spline, cut_sindec): - # The energy filter will cut all events below cut_sindec - # that have an energy smaller than the energy spline at - # their declination. + """The energy filter will cut all events below `cut_sindec` + that have an energy smaller than the energy spline at + their declination. + """ if cut_sindec is None: cut_sindec = 0 energy_filter = np.logical_and( @@ -308,9 +321,9 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhratio : LLHRatio The likelihood ratio object contains the datasets signal weights needed for distributing the event generation among the different - datsets. - energy_cut_splines : dict - cut_energy_min : dict + datasets. + energy_cut_splines : list of UnivariateSpline + cut_sindec : float """ self.src_hypo_group_manager = src_hypo_group_manager self.dataset_list = dataset_list From f86268687338d529813b715c52304c87d9ff7184 Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:33:06 +0200 Subject: [PATCH 239/274] Update skyllh/core/binning.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/binning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index 3906f9e2f4..5c4de9decf 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -14,7 +14,7 @@ def rebin( negatives=False): """Rebins the binned counts to the new desired grid. This function uses a method of moments approach. Currently it uses a three moments - appraoch. At the edges of the array it uses a two moments approach. + approach. At the edges of the array it uses a two moments approach. Parameters ---------- From 622e884b709911a99a44343fb166eb8094059db3 Mon Sep 17 00:00:00 2001 From: chiarabellenghi <62283616+chiarabellenghi@users.noreply.github.com> Date: Mon, 3 Apr 2023 15:34:40 +0200 Subject: [PATCH 240/274] Update skyllh/core/signal_generator.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/core/signal_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/core/signal_generator.py b/skyllh/core/signal_generator.py index 2f932d4d82..f65d4d39b2 100644 --- a/skyllh/core/signal_generator.py +++ b/skyllh/core/signal_generator.py @@ -397,7 +397,7 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list, A typical keyword argument is the instance of MultiDatasetTCLLHRatio. """ super(MultiSourceSignalGenerator, self).__init__( - src_hypo_group_manager, dataset_list, data_list) + src_hypo_group_manager, dataset_list, data_list, **kwargs) def _construct_signal_candidates(self): """Constructs an array holding pointer information of signal candidate From 3d6093dca3a473ab27cbab0e9c6b40723ec2bd91 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 15:40:47 +0200 Subject: [PATCH 241/274] Removed unused import. --- skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py index 6b03fee2dc..7772c7bea5 100644 --- a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py @@ -6,7 +6,6 @@ import argparse import logging import numpy as np -from scipy.interpolate import UnivariateSpline from skyllh.core.progressbar import ProgressBar From f5fc736b3d693e2abfbf0fb0e7e862d1dc6ac56f Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 3 Apr 2023 17:30:23 +0200 Subject: [PATCH 242/274] Improved documentation for the signal generator to comply with the code review. --- .../i3/publicdata_ps/signal_generator.py | 67 ++++++++++++++++--- 1 file changed, 59 insertions(+), 8 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 2161109d53..f99cb98a35 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -9,6 +9,8 @@ float_cast, int_cast ) +from skyllh.core.py import module_classname +from skyllh.core.debugging import get_logger from skyllh.core.signal_generator import SignalGeneratorBase from skyllh.core.llhratio import LLHRatio from skyllh.core.dataset import Dataset @@ -23,11 +25,16 @@ class PDDatasetSignalGenerator(object): + """This class provides a signal generation method for a point-like source + seen in the IceCube detector using one dataset of the 10 years public data + release. It is used by the PDSignalGenerato class in a loop over all the + datasets that have been added to the analysis. + """ def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): """Creates a new instance of the signal generator for generating signal events from a specific public data dataset. - + Parameters: ----------- ds : Dataset instance @@ -42,6 +49,8 @@ def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): """ super().__init__(**kwargs) + self._logger = get_logger(module_classname(self)) + if sm is None: self.smearing_matrix = PublicDataSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( @@ -228,13 +237,33 @@ def _generate_events( @staticmethod @np.vectorize - def energy_filter(events, spline, cut_sindec): - """The energy filter will cut all events below `cut_sindec` - that have an energy smaller than the energy spline at - their declination. + def energy_filter(events, spline, cut_sindec, logger): + """The energy filter will select all events below `cut_sindec` + that have an energy smaller than the energy spline at their + declination. + + Paramters + --------- + events : numpy record array + Numpy record array with the generated signal events. + energy_cut_splines : scipy.interpolate.UnivariateSpline + A spline of E(sin_dec) that defines the declination + dependent energy cut in the IceCube southern sky. + cut_sindec : float + The sine of the declination to start applying the energy cut. + The cut will be applied from this declination down. + logger : logging.Logger + The Logger instance. + + Returns + energy_filter : (len(events),)-shaped numpy ndarray + A mask of shape `len(events)` of the events to be cut. """ if cut_sindec is None: - cut_sindec = 0 + logger.warn( + 'No `cut_sindec` has been specified. The energy cut will be ' + 'applied in [-90, 0] deg.') + cut_sindec = 0. energy_filter = np.logical_and( events['sin_dec'] < cut_sindec, events['log_energy'] < spline(events['sin_dec'])) @@ -247,6 +276,24 @@ def generate_signal_events( """Generates ``n_events`` signal events for the given source location and flux model. + Paramters + --------- + rss : RandomStateService + src_dec : float + Declination coordinate of the injection point. + src_ra : float + Right ascension coordinate of the injection point. + flux_model : FluxModel + Instance of the `FluxModel` class. + n_events : int + Number of signal events to be generated. + energy_cut_splines : scipy.interpolate.UnivariateSpline + A spline of E(sin_dec) that defines the declination + dependent energy cut in the IceCube southern sky. + cut_sindec : float + The sine of the declination to start applying the energy cut. + The cut will be applied from this declination down. + Returns ------- events : numpy record array @@ -286,7 +333,7 @@ def generate_signal_events( events_ = events_[events_['isvalid']] if energy_cut_spline is not None: to_cut = self.energy_filter( - events_, energy_cut_spline, cut_sindec) + events_, energy_cut_spline, cut_sindec, self._logger) events_ = events_[~to_cut] if not len(events_) == 0: n_evt_generated += len(events_) @@ -323,7 +370,11 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, needed for distributing the event generation among the different datasets. energy_cut_splines : list of UnivariateSpline - cut_sindec : float + A list of splines of E(sin_dec) used to define the declination + dependent energy cut in the IceCube southern sky. + cut_sindec : list of float + The sine of the declination to start applying the energy cut. + The cut will be applied from this declination down. """ self.src_hypo_group_manager = src_hypo_group_manager self.dataset_list = dataset_list From 63e48a131b3600af12e6ad36a23512db7bcd8340 Mon Sep 17 00:00:00 2001 From: "Martin Wolf, PhD" Date: Tue, 4 Apr 2023 10:33:40 +0200 Subject: [PATCH 243/274] Update skyllh/datasets/i3/PublicData_10y_ps_wMC.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/datasets/i3/PublicData_10y_ps_wMC.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py index 3d506a09bf..7b1727f533 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py @@ -388,8 +388,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_II.add_aux_data_definition( 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') - IC86_II.add_aux_data_definition( - 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_II.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), From 5bafaf42b95da85c3104cc58701fada999a7ae38 Mon Sep 17 00:00:00 2001 From: "Martin Wolf, PhD" Date: Tue, 4 Apr 2023 10:36:00 +0200 Subject: [PATCH 244/274] Update skyllh/datasets/i3/PublicData_10y_ps.py Co-authored-by: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> --- skyllh/datasets/i3/PublicData_10y_ps.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index afc637bd16..2afcf0e9ff 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -388,8 +388,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_II.add_aux_data_definition( 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') - IC86_II.add_aux_data_definition( - 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_II.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), From 4667f2cac9d79d39f3db0892d42aeca23270b053 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 4 Apr 2023 11:09:32 +0200 Subject: [PATCH 245/274] Remove unused function --- skyllh/core/binning.py | 137 ----------------------------------------- 1 file changed, 137 deletions(-) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index 5c4de9decf..f9918f9f65 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -7,142 +7,6 @@ from skyllh.core.py import classname -def rebin( - bincontent: np.array, - old_binedges: np.array, - new_binedges: np.array, - negatives=False): - """Rebins the binned counts to the new desired grid. This function - uses a method of moments approach. Currently it uses a three moments - approach. At the edges of the array it uses a two moments approach. - - Parameters - ---------- - bincontent: (n,)-shaped 1D numpy ndarray - The binned content which should be rebinned. - old_binedges: (n+1,)-shaped 1D numpy ndarray - The old grid's bin edges. The shape needs to be the same as - `bincontent`. - new_binedges: (m+1)-shaped 1D numpy ndarray - The new bin edges to use. - binning_scheme: str - The binning scheme to use. Choices are "log" (logarithmic) - or "lin" (linear). This decides how to calculate the midpoints - of each bin. - negatives: bool - Switch to keep or remove negative values in the final binning. - - Returns - ------- - new_bincontent: 1D numpy ndarray - The new binned counts for the new binning. - - Raises - ------ - ValueError: - Unknown binning scheme. - - Authors - ------- - - Dr. Stephan Meighen-Berger - - Dr. Martin Wolf - """ - old_bincenters = 0.5*(old_binedges[1:] + old_binedges[:-1]) - - # Checking if shapes align. - if bincontent.shape != old_bincenters.shape: - ValueError('The arguments bincontent and old_binedges do not match!' - 'bincontent must be (n,)-shaped and old_binedges must be (n+1,)-' - 'shaped!') - - # Setting up the new binning. - new_bincenters = 0.5*(new_binedges[1:] + new_binedges[:-1]) - - - new_widths = np.diff(new_binedges) - new_nbins = len(new_widths) - - # Create output array with zeros. - new_bincontent = np.zeros(new_bincenters.shape) - - # Looping over the old bin contents and distributing - for (idx, bin_val) in enumerate(bincontent): - # Ignore empty bins. - if bin_val == 0.: - continue - - old_bincenter = old_bincenters[idx] - - new_point = (np.abs(new_binedges - old_bincenter)).argmin() - - if new_point == 0: - # It the first bin. Use 2-moments method. - start_idx = new_point - end_idx = new_point + 1 - - mat = np.vstack( - ( - new_widths[start_idx:end_idx+1], - new_widths[start_idx:end_idx+1] - * new_bincenters[start_idx:end_idx+1] - ) - ) - - b = bin_val * np.array([ - 1., - old_bincenter - ]) - elif new_point == new_nbins-1: - # It the last bin. Use 2-moments method. - start_idx = new_point - 1 - end_idx = new_point - - mat = np.vstack( - ( - new_widths[start_idx:end_idx+1], - new_widths[start_idx:end_idx+1] - * new_bincenters[start_idx:end_idx+1] - ) - ) - - b = bin_val * np.array([ - 1., - old_bincenter - ]) - else: - # Setting up the equation for 3 moments (mat*x = b) - # x is the values we want - start_idx = new_point - 1 - end_idx = new_point + 1 - - mat = np.vstack( - ( - new_widths[start_idx:end_idx+1], - new_widths[start_idx:end_idx+1] - * new_bincenters[start_idx:end_idx+1], - new_widths[start_idx:end_idx+1] - * new_bincenters[start_idx:end_idx+1]**2 - ) - ) - - b = bin_val * np.array([ - 1., - old_bincenter, - old_bincenter**2 - ]) - - # Solving and adding to the new bin content. - new_bincontent[start_idx:end_idx+1] += solve(mat, b) - - if not negatives: - new_bincontent[new_bincontent < 0.] = 0. - - new_bincontent = new_bincontent / ( - np.sum(new_bincontent) / np.sum(bincontent)) - - return new_bincontent - - def get_bincenters_from_binedges(edges): """Calculates the bin center values from the given bin edge values. @@ -176,7 +40,6 @@ def get_binedges_from_bincenters(centers): if not np.all(np.isclose(np.diff(d), 0)): raise ValueError('The bin center values are not evenly spaced!') d = d[0] - print(d) edges = np.zeros((len(centers)+1,), dtype=np.double) edges[:-1] = centers - d/2 From a75c728734003815eb628ddb9a098e89f6a4d2a0 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:12:09 +0200 Subject: [PATCH 246/274] Update skyllh/core/backgroundpdf.py Co-authored-by: Martin Wolf, PhD --- skyllh/core/backgroundpdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/core/backgroundpdf.py b/skyllh/core/backgroundpdf.py index aee903fc2c..b1530c81ee 100644 --- a/skyllh/core/backgroundpdf.py +++ b/skyllh/core/backgroundpdf.py @@ -8,7 +8,7 @@ IsBackgroundPDF, MultiDimGridPDF, NDPhotosplinePDF, - TimePDF + TimePDF, ) import numpy as np From 356a65537f551e30d42a808cf26ca917d3a79a76 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> Date: Tue, 4 Apr 2023 11:13:16 +0200 Subject: [PATCH 247/274] Update skyllh/analyses/i3/publicdata_ps/expectation_maximization.py --- skyllh/analyses/i3/publicdata_ps/expectation_maximization.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py index 409ac2ec1c..218dec99f3 100644 --- a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py +++ b/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py @@ -69,4 +69,4 @@ def maximization_em(e_sig, t): ns.append(np.sum(e_sig[i])) sigma = [max(1, s) for s in sigma] - return mu, sigma, ns \ No newline at end of file + return mu, sigma, ns From 0b4f1f743e02694d31ec2838a608cd4f5585edb7 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 4 Apr 2023 12:58:43 +0200 Subject: [PATCH 248/274] Rename `PublicDataSmearingMatrix` to `PDSmearingMatrix` --- skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py | 2 +- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 6 +++--- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py b/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py index c772523687..7b801855cd 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py @@ -172,7 +172,7 @@ def _get_nbins_from_edges(lower_edges, upper_edges): ) -class PublicDataSmearingMatrix(object): +class PDSmearingMatrix(object): """This class is a helper class for dealing with the smearing matrix provided by the public data. """ diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index f99cb98a35..cd6b762d96 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -19,7 +19,7 @@ from skyllh.analyses.i3.publicdata_ps.utils import psi_to_dec_and_ra from skyllh.analyses.i3.publicdata_ps.pd_smearing_matrix import ( - PublicDataSmearingMatrix + PDSmearingMatrix ) from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff @@ -44,7 +44,7 @@ def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): The declination of the source in radians. effA : PDAeff | None Representation of the effective area provided by the public data. - sm : PublicDataSmearingMatrix | None + sm : PDSmearingMatrix | None Representation of the smearing matrix provided by the public data. """ super().__init__(**kwargs) @@ -52,7 +52,7 @@ def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): self._logger = get_logger(module_classname(self)) if sm is None: - self.smearing_matrix = PublicDataSmearingMatrix( + self.smearing_matrix = PDSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) else: diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 2d9665e1ef..756285c9bb 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -29,7 +29,7 @@ FctSpline1D, ) from skyllh.analyses.i3.publicdata_ps.pd_smearing_matrix import ( - PublicDataSmearingMatrix + PDSmearingMatrix ) @@ -213,7 +213,7 @@ def __init__( ) # Load the smearing matrix. - sm = PublicDataSmearingMatrix( + sm = PDSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) From 70279373345703c51e6933228121e9060cb563ff Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 4 Apr 2023 13:05:47 +0200 Subject: [PATCH 249/274] Rename variables --- skyllh/analyses/i3/publicdata_ps/backgroundpdf.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py index 525718ed92..492914272e 100644 --- a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py @@ -134,14 +134,14 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, "The bandwidth parameter must be True or False!") kde_pdf = np.empty( (len(sinDec_binning.bincenters),), dtype=object) - data_logE_mask = data_logE[~mask] - data_sinDec_mask = data_sinDec[~mask] + data_logE_masked = data_logE[~mask] + data_sinDec_masked = data_sinDec[~mask] for i in range(len(sinDec_binning.bincenters)): sindec_mask = np.logical_and( - data_sinDec_mask >= sinDec_binning.binedges[i], - data_sinDec_mask < sinDec_binning.binedges[i+1] + data_sinDec_masked >= sinDec_binning.binedges[i], + data_sinDec_masked < sinDec_binning.binedges[i+1] ) - this_energy = data_logE_mask[sindec_mask] + this_energy = data_logE_masked[sindec_mask] if sinDec_binning.binedges[i] >= 0: kde_pdf[i] = gaussian_kde( this_energy, bw_method=self._KDE_BW_NORTH) From 12d79433a9d74be5bc8aa1298f5649d90ee30a9d Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> Date: Tue, 4 Apr 2023 13:10:33 +0200 Subject: [PATCH 250/274] Update skyllh/analyses/i3/publicdata_ps/signal_generator.py --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index cd6b762d96..794983a9df 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -27,7 +27,7 @@ class PDDatasetSignalGenerator(object): """This class provides a signal generation method for a point-like source seen in the IceCube detector using one dataset of the 10 years public data - release. It is used by the PDSignalGenerato class in a loop over all the + release. It is used by the PDSignalGenerator class in a loop over all the datasets that have been added to the analysis. """ From 7678416f614c40940159504ee8bc39dd60fed268 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 4 Apr 2023 16:03:55 +0200 Subject: [PATCH 251/274] Apply suggestion based on code review by Martin --- skyllh/core/signalpdf.py | 104 +++++++++++++++++---------------------- 1 file changed, 46 insertions(+), 58 deletions(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index 749f3f351c..749cfd6937 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -253,8 +253,9 @@ def get_prob(self, tdm, fitparams=None, tl=None): Returns ------- - pd : (n_events,)-shaped 1D numpy ndarray - The probability density value for each event in unit 1/rad. + pd : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event in + unit 1/rad. grads : (0,)-shaped 1D numpy ndarray Since this PDF does not depend on fit parameters, an empty array is returned. @@ -398,8 +399,8 @@ def assert_is_valid_for_exp_data(self, data_exp): time_axis.vmin, time_axis.vmax)) def get_prob(self, tdm, fitparams): - """Calculates the signal time probability of each event for the given - set of signal time fit parameter values. + """Calculates the signal time probability density of each event for the + given set of signal time fit parameter values. Parameters ---------- @@ -412,12 +413,12 @@ def get_prob(self, tdm, fitparams): The MJD time of the event. fitparams : dict The dictionary holding the signal time parameter values for which - the signal time probability should be calculated. + the signal time probability density should be calculated. Returns ------- - prob : array of float - The (N,)-shaped ndarray holding the probability for each event. + pd : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. """ # Update the time-profile if its fit-parameter values have changed and # recalculate self._I and self._S if an updated was actually performed. @@ -425,20 +426,20 @@ def get_prob(self, tdm, fitparams): if(updated): (self._I, self._S) = self._calculate_time_profile_I_and_S() - events_time = tdm.get_data('time') + time = tdm.get_data('time') # Get a mask of the event times which fall inside a detector on-time # interval. - on = self._livetime.is_on(events_time) + on = self._livetime.is_on(time) # The sum of the on-time integrals of the time profile, A, will be zero # if the time profile is entirly during detector off-time. - prob = np.zeros((tdm.n_selected_events,), dtype=np.float64) + pd = np.zeros((tdm.n_selected_events,), dtype=np.float64) if(self._S > 0): - prob[on] = self._time_profile.get_value( - events_time[on]) / (self._I * self._S) + pd[on] = self._time_profile.get_value( + time[on]) / (self._I * self._S) - return prob + return pd class SignalGaussTimePDF(TimePDF, IsSignalPDF): @@ -462,37 +463,30 @@ def __init__(self, grl, mu, sigma, **kwargs): self.grl = grl - def norm_uptime(self, t): + def norm_uptime(self): """Compute the normalization with the dataset uptime. Distributions like scipy.stats.norm are normalized (-inf, inf). These must be re-normalized such that the function sums to 1 over the finite good run list domain. - Parameters - ---------- - t : float, ndarray - MJD times - Returns ------- norm : float Normalization such that cdf sums to 1 over good run list domain """ - t = np.atleast_1d(t) - cdf = scp.stats.norm(self.mu, self.sigma).cdf integral = (cdf(self.grl["stop"]) - cdf(self.grl["start"])).sum() - if integral < 1.e-50: + if np.isclose(integral, 0): return 0 return 1. / integral def get_prob(self, tdm, fitparams=None, tl=None): - """Calculates the signal time probability of each event for the given - set of signal time fit parameter values. + """Calculates the signal time probability density of each event for the + given set of signal time fit parameter values. Parameters ---------- @@ -512,21 +506,18 @@ def get_prob(self, tdm, fitparams=None, tl=None): Returns ------- - prob : array of float - The (N,)-shaped ndarray holding the probability for each event. + pd : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. grads : empty array of float Empty, since it does not depend on any fit parameter """ + time = tdm.get_data('time') - events_time = tdm.get_data('time') - - t = events_time - + pd = scp.stats.norm.pdf(time, self.mu, self.sigma) * self.norm_uptime() grads = np.array([], dtype=np.double) - return scp.stats.norm.pdf(t, self.mu, self.sigma) * self.norm_uptime(t), grads - - + return (pd, grads) + class SignalBoxTimePDF(TimePDF, IsSignalPDF): @@ -562,11 +553,13 @@ def cdf(self, t): cdf : float, ndarray Values of cumulative density function evaluated at t """ - t_start, t_end = self.start, self.end - t_arr = np.atleast_1d(t) + t_start = self.start + t_end = self.end + t = np.atleast_1d(t) - cdf = np.zeros(t_arr.size, float) - sample_start, sample_end = self.grl["start"][0], self.grl["stop"][-1] + cdf = np.zeros(t.size, float) + sample_start = self.grl["start"][0] + sample_end = self.grl["stop"][-1] if t_start < sample_start and t_end > sample_start: t_start = sample_start @@ -574,37 +567,31 @@ def cdf(self, t): t_end = sample_end # values between start and stop times - mask = (t_start <= t_arr) & (t_arr <= t_end) - cdf[mask] = (t_arr[mask] - t_start) / [t_end - t_start] + mask = (t_start <= t) & (t <= t_end) + cdf[mask] = (t[mask] - t_start) / [t_end - t_start] # take care of values beyond stop time in sample if t_end > sample_start: - mask = (t_end < t_arr) + mask = (t_end < t) cdf[mask] = 1. return cdf - def norm_uptime(self, t): + def norm_uptime(self): """Compute the normalization with the dataset uptime. Distributions like scipy.stats.norm are normalized (-inf, inf). These must be re-normalized such that the function sums to 1 over the finite good run list domain. - Parameters - ---------- - t : float, ndarray - MJD times - Returns ------- norm : float Normalization such that cdf sums to 1 over good run list domain """ - integral = (self.cdf(self.grl["stop"]) - self.cdf(self.grl["start"])).sum() - if integral < 1.e-50: + if np.isclose(integral, 0): return 0 return 1. / integral @@ -632,23 +619,23 @@ def get_prob(self, tdm, fitparams=None, tl=None): Returns ------- - prob : array of float - The (N,)-shaped ndarray holding the probability for each event. + pd : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. grads : empty array of float Does not depend on fit parameter, so no gradient """ - - events_time = tdm.get_data('time') + time = tdm.get_data('time') # Get a mask of the event times which fall inside a detector on-time # interval. - time = events_time + # Gives 0 for outside the flare and 1 for inside the flare. + box_mask = np.piecewise(time, [self.start <= time, time <= self.end], [1., 1.]) - # gives 1 for outside the flare and 0 for inside the flare. - inverse_box = np.piecewise(time, [time < self.start, time > self.end], [1., 1.]) + sample_start = self.grl["start"][0] + sample_end = self.grl["stop"][-1] - sample_start, sample_end = min(self.grl["start"]), max(self.grl["stop"]) - t_start, t_end = self.start, self.end + t_start = self.start + t_end = self.end # check if the whole flare lies in this dataset for normalization. # If one part lies outside, adjust to datasample start or end time. # For the case where everything lies outside, the pdf will be 0 by definition. @@ -657,9 +644,10 @@ def get_prob(self, tdm, fitparams=None, tl=None): if t_end > sample_end and t_start < sample_end: t_end = sample_end + pd = box_mask / (t_end - t_start) * self.norm_uptime() grads = np.array([], dtype=np.double) - return (1. - inverse_box) / (t_end - t_start) * self.norm_uptime(time), grads + return (pd, grads) From 1b1548e44405ba9ac6acd3169ea2f569478d6af6 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 4 Apr 2023 16:17:09 +0200 Subject: [PATCH 252/274] Minor changes following the code review of signalpdf.py --- skyllh/core/backgroundpdf.py | 28 +++++++++++++--------------- 1 file changed, 13 insertions(+), 15 deletions(-) diff --git a/skyllh/core/backgroundpdf.py b/skyllh/core/backgroundpdf.py index b1530c81ee..3977116abe 100644 --- a/skyllh/core/backgroundpdf.py +++ b/skyllh/core/backgroundpdf.py @@ -132,15 +132,15 @@ def cdf(self, t): cdf : float, ndarray Values of cumulative density function evaluated at t """ - t_start, t_end = self.grl["start"][0], self.grl["stop"][-1] - t_arr = np.atleast_1d(t) - - cdf = np.zeros(t_arr.size, float) + t_start = self.grl["start"][0] + t_end = self.grl["stop"][-1] + t = np.atleast_1d(t) + cdf = np.zeros(t.size, float) # values between start and stop times - mask = (t_start <= t_arr) & (t_arr <= t_end) - cdf[mask] = (t_arr[mask] - t_start) / [t_end - t_start] + mask = (t_start <= t) & (t <= t_end) + cdf[mask] = (t[mask] - t_start) / [t_end - t_start] # take care of values beyond stop time in sample @@ -153,7 +153,6 @@ def norm_uptime(self): These must be re-normalized such that the function sums to 1 over the finite good run list domain. - Returns ------- norm : float @@ -162,14 +161,14 @@ def norm_uptime(self): integral = (self.cdf(self.grl["stop"]) - self.cdf(self.grl["start"])).sum() - if integral < 1.e-50: + if np.isclose(integral, 0): return 0 return 1. / integral def get_prob(self, tdm, fitparams=None, tl=None): - """Calculates the background time probability of each event + """Calculates the background time probability density of each event tdm : TrialDataManager Unused interface argument @@ -181,14 +180,13 @@ def get_prob(self, tdm, fitparams=None, tl=None): Returns ------- - prob : array of float - The (N,)-shaped ndarray holding the probability for each event. + pd : array of float + The (N,)-shaped ndarray holding the probability density for each event. grads : empty array of float Does not depend on fit parameter, so no gradient """ - - grads = np.array([], dtype=np.double) - livetime = self.grl["stop"][-1] - self.grl["start"][0] + pd = 1./livetime + grads = np.array([], dtype=np.double) - return 1./livetime, grads \ No newline at end of file + return (pd, grads) From baa79875d9a13665484452d4570856c5cdefa0f1 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> Date: Tue, 4 Apr 2023 17:36:07 +0200 Subject: [PATCH 253/274] Update skyllh/analyses/i3/publicdata_ps/signal_generator.py --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 794983a9df..d9794660fa 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -619,7 +619,7 @@ def generate_signal_events(self, rss, mean, poisson=True): # sampling instead of the lazy version implemented here tmp_grl = self.data_list[ds_idx].grl for event_index in events_.indices: - while events_._data_fields["time"][event_index] == 1: + while events_["time"][event_index] == 1: if self.gauss is not None: # make sure flare is in dataset if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( From 1e047b1f5557cd357fd9a40a904f305e6c72203b Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas <52071038+tomaskontrimas@users.noreply.github.com> Date: Tue, 4 Apr 2023 17:36:59 +0200 Subject: [PATCH 254/274] Update skyllh/analyses/i3/publicdata_ps/signal_generator.py Co-authored-by: Martin Wolf, PhD --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index d9794660fa..19c067009d 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -641,7 +641,7 @@ def generate_signal_events(self, rss, mean, poisson=True): is_in_grl = (tmp_grl["start"] <= time) & ( tmp_grl["stop"] >= time) if np.any(is_in_grl): - events_._data_fields["time"][event_index] = time + events_["time"][event_index] = time if shg_src_idx == 0: signal_events_dict[ds_idx] = events_ From f22ddcb0a7ef0edf78808850212a5e78270fa5a5 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 5 Apr 2023 11:16:38 +0200 Subject: [PATCH 255/274] Remove `TimeSigOverBkgPDFRatio` In SkyLLH2 there is no pdf_type argument anymore. The user needs to make sure that the correct PDFs are used. So This class is not needed anymore in SkyLLH2. We should remove it here. Within the create_analysis function one could still specify the pdf_type argument if desired. --- .../i3/publicdata_ps/time_dependent_ps.py | 9 ++++++-- skyllh/core/analysis.py | 19 ++++++++++------- skyllh/core/pdfratio.py | 21 ------------------- 3 files changed, 18 insertions(+), 31 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py index 7772c7bea5..bd051fad29 100644 --- a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py @@ -53,11 +53,12 @@ from skyllh.i3.backgroundpdf import ( DataBackgroundI3SpatialPDF ) +from skyllh.core.pdf import TimePDF # Classes to define the spatial and energy PDF ratios. from skyllh.core.pdfratio import ( SpatialSigOverBkgPDFRatio, - TimeSigOverBkgPDFRatio + SigOverBkgPDFRatio ) # Analysis utilities. @@ -336,7 +337,11 @@ def create_analysis( elif box is not None: time_sigpdf = SignalBoxTimePDF( data.grl, box["start"], box["end"]) - time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + time_pdfratio = SigOverBkgPDFRatio( + sig_pdf=time_sigpdf, + bkg_pdf=time_bkgpdf, + pdf_type=TimePDF + ) pdfratios.append(time_pdfratio) analysis.add_dataset( diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index f8afd7c37f..5f88e1eee9 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -24,9 +24,13 @@ ) from skyllh.core.pdf import ( EnergyPDF, - SpatialPDF + SpatialPDF, + TimePDF +) +from skyllh.core.pdfratio import ( + PDFRatio, + SigOverBkgPDFRatio ) -from skyllh.core.pdfratio import PDFRatio from skyllh.core.progressbar import ProgressBar from skyllh.core.random import RandomStateService from skyllh.core.llhratio import ( @@ -67,11 +71,6 @@ from skyllh.core.backgroundpdf import BackgroundUniformTimePDF -from skyllh.core.pdfratio import ( - TimeSigOverBkgPDFRatio -) - - logger = get_logger(__name__) @@ -1666,7 +1665,11 @@ def change_time_pdf(self, gauss=None, box=None): elif box is not None: time_sigpdf = SignalBoxTimePDF(grl, box["start"], box["end"]) - time_pdfratio = TimeSigOverBkgPDFRatio(time_sigpdf, time_bkgpdf) + time_pdfratio = SigOverBkgPDFRatio( + sig_pdf=time_sigpdf, + bkg_pdf=time_bkgpdf, + pdf_type=TimePDF + ) # the next line seems to make no difference in the llh evaluation. We keep it for consistency self._llhratio.llhratio_list[0].pdfratio_list[2] = time_pdfratio diff --git a/skyllh/core/pdfratio.py b/skyllh/core/pdfratio.py index 9bf7e7fd5a..cfe4de4cca 100644 --- a/skyllh/core/pdfratio.py +++ b/skyllh/core/pdfratio.py @@ -895,24 +895,3 @@ def convert_signal_fitparam_name_into_index(self, signal_fitparam_name): # At this point there is no parameter defined. raise KeyError('The PDF ratio "%s" has no signal fit parameter named ' '"%s"!'%(classname(self), signal_fitparam_name)) - - -class TimeSigOverBkgPDFRatio(SigOverBkgPDFRatio): - """This class implements a signal-over-background PDF ratio for time - PDFs. It takes a signal PDF of type TimePDF and a background PDF of type - TimePDF and calculates the PDF ratio. - """ - def __init__(self, sig_pdf, bkg_pdf, *args, **kwargs): - """Creates a new signal-over-background PDF ratio instance for time - PDFs. - - Parameters - ---------- - sig_pdf : class instance derived from TimePDF, IsSignalPDF - The instance of the time signal PDF. - bkg_pdf : class instance derived from TimePDF, IsBackgroundPDF - The instance of the time background PDF. - """ - super(TimeSigOverBkgPDFRatio, self).__init__(pdf_type=TimePDF, - sig_pdf=sig_pdf, bkg_pdf=bkg_pdf, *args, **kwargs) - From 58f5f0d6b3d0de2af3decaeefd1e1bb590743425 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 5 Apr 2023 14:16:35 +0200 Subject: [PATCH 256/274] Move expectation_maximization.py to skyllh.core --- skyllh/core/analysis.py | 2 +- .../i3/publicdata_ps => core}/expectation_maximization.py | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename skyllh/{analyses/i3/publicdata_ps => core}/expectation_maximization.py (100%) diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 5f88e1eee9..cd95ba885b 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -59,7 +59,7 @@ ) from skyllh.physics.source import SourceModel -from skyllh.analyses.i3.publicdata_ps.expectation_maximization import ( +from skyllh.core.expectation_maximization import ( expectation_em, maximization_em ) diff --git a/skyllh/analyses/i3/publicdata_ps/expectation_maximization.py b/skyllh/core/expectation_maximization.py similarity index 100% rename from skyllh/analyses/i3/publicdata_ps/expectation_maximization.py rename to skyllh/core/expectation_maximization.py From 525238fdb65002d8c8b45f9ee05211a524c50b95 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 5 Apr 2023 16:38:54 +0200 Subject: [PATCH 257/274] Refactor TimeDependentSingleDatasetSingleSourceAnalysis class as utils class with ana as an argument --- .../i3/publicdata_ps/time_dependent_ps.py | 4 +- skyllh/core/analysis.py | 289 +----------------- skyllh/core/expectation_maximization.py | 278 ++++++++++++++++- 3 files changed, 280 insertions(+), 291 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py index bd051fad29..62fe8d07d3 100644 --- a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py @@ -36,7 +36,7 @@ # Classes for defining the analysis. from skyllh.core.test_statistic import TestStatisticWilks from skyllh.core.analysis import ( - TimeDependentSingleDatasetSingleSourceAnalysis as TimedepSingleDatasetAnalysis + TimeIntegratedMultiDatasetSingleSourceAnalysis ) # Classes to define the background generation. @@ -256,7 +256,7 @@ def create_analysis( bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) # Create the Analysis instance. - analysis = TimedepSingleDatasetAnalysis( + analysis = TimeIntegratedMultiDatasetSingleSourceAnalysis( src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index cd95ba885b..c21fd2022f 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -27,10 +27,7 @@ SpatialPDF, TimePDF ) -from skyllh.core.pdfratio import ( - PDFRatio, - SigOverBkgPDFRatio -) +from skyllh.core.pdfratio import PDFRatio from skyllh.core.progressbar import ProgressBar from skyllh.core.random import RandomStateService from skyllh.core.llhratio import ( @@ -59,18 +56,6 @@ ) from skyllh.physics.source import SourceModel -from skyllh.core.expectation_maximization import ( - expectation_em, - maximization_em -) - -from skyllh.core.signalpdf import ( - SignalBoxTimePDF, - SignalGaussTimePDF -) - -from skyllh.core.backgroundpdf import BackgroundUniformTimePDF - logger = get_logger(__name__) @@ -1607,275 +1592,3 @@ def initialize_trial(self, events_list, n_events_list=None, tl=None): store_src_ev_idxs=True, tl=tl) self._llhratio.initialize_for_new_trial(tl=tl) - - - -class TimeDependentSingleDatasetSingleSourceAnalysis(TimeIntegratedMultiDatasetSingleSourceAnalysis): - - def __init__(self, src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, - bkg_gen_method=None, sig_generator_cls=None): - """Creates a new time-dependent single dataset point-like source analysis - assuming a single source. - - Parameters - ---------- - src_hypo_group_manager : instance of SourceHypoGroupManager - The instance of SourceHypoGroupManager, which defines the groups of - source hypotheses, their flux model, and their detector signal - efficiency implementation method. - src_fitparam_mapper : instance of SourceFitParameterMapper - The SourceFitParameterMapper instance managing the global fit - parameters and their relation to the individual sources. - fitparam_ns : FitParameter instance - The FitParameter instance defining the fit parameter ns. - test_statistic : TestStatistic instance - The TestStatistic instance that defines the test statistic function - of the analysis. - bkg_gen_method : instance of BackgroundGenerationMethod | None - The instance of BackgroundGenerationMethod that should be used to - generate background events for pseudo data. This can be set to None, - if there is no need to generate background events. - sig_generator_cls : SignalGeneratorBase class | None - The signal generator class used to create the signal generator - instance. - If set to None, the `SignalGenerator` class is used. - """ - super().__init__(src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, - test_statistic, bkg_gen_method, sig_generator_cls) - - - def change_time_pdf(self, gauss=None, box=None): - """Changes the time pdf. - - Parameters - ---------- - gauss : dict | None - None or dictionary with {"mu": float, "sigma": float}. - box : dict | None - None or dictionary with {"start": float, "end": float}. - """ - if gauss is None and box is None: - raise TypeError("Either gauss or box have to be specified as time pdf.") - - grl = self._data_list[0].grl - # redo this in case the background pdf was not calculated before - time_bkgpdf = BackgroundUniformTimePDF(grl) - if gauss is not None: - time_sigpdf = SignalGaussTimePDF(grl, gauss['mu'], gauss['sigma']) - elif box is not None: - time_sigpdf = SignalBoxTimePDF(grl, box["start"], box["end"]) - - time_pdfratio = SigOverBkgPDFRatio( - sig_pdf=time_sigpdf, - bkg_pdf=time_bkgpdf, - pdf_type=TimePDF - ) - - # the next line seems to make no difference in the llh evaluation. We keep it for consistency - self._llhratio.llhratio_list[0].pdfratio_list[2] = time_pdfratio - # this line here is relevant for the llh evaluation - self._llhratio.llhratio_list[0]._pdfratioarray._pdfratio_list[2] = time_pdfratio - - # change detector signal yield with flare livetime in sample (1 / grl_norm in pdf), - # rebuild the histograms if it is changed... - # signal injection? - - - def get_energy_spatial_signal_over_backround(self, fitparams): - """Returns the signal over background ratio for - (spatial_signal * energy_signal) / (spatial_background * energy_background). - - Parameter - --------- - fitparams : dict - Dictionary with {"gamma": float} for energy pdf. - - Returns - ------- - ratio : 1d ndarray - Product of spatial and energy signal over background pdfs. - """ - ratio = self._llhratio.llhratio_list[0].pdfratio_list[0].get_ratio(self._tdm_list[0], fitparams) - ratio *= self._llhratio.llhratio_list[0].pdfratio_list[1].get_ratio(self._tdm_list[0], fitparams) - - return ratio - - - def change_fluxmodel_gamma(self, gamma): - """Set new gamma for the flux model. - - Parameter - --------- - gamma : float - Spectral index for flux model. - """ - - self.src_hypo_group_manager.src_hypo_group_list[0].fluxmodel.gamma = gamma - - - def change_signal_time(self, gauss=None, box=None): - """Change the signal injection to gauss or box. - - Parameters - ---------- - gauss : dict | None - None or dictionary {"mu": float, "sigma": float}. - box : dict | None - None or dictionary {"start" : float, "end" : float}. - """ - self.sig_generator.set_flare(box=box, gauss=gauss) - - - def em_fit(self, fitparams, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, initial_width=5000, - remove_time=None): - """Run expectation maximization. - - Parameters - ---------- - fitparams : dict - Dictionary with value for gamma, e.g. {'gamma': 2}. - n : int - How many gaussians flares we are looking for. - tol : float - the stopping criteria for expectation maximization. This is the difference in the normalized likelihood over the - last 20 iterations. - iter_max : int - The maximum number of iterations, even if stopping criteria tolerance (`tol`) is not yet reached. - sob_thres : float - Set a minimum threshold for signal over background ratios. Ratios below this threshold will be removed. - initial_width : float - Starting width for the gaussian flare in days. - remove_time : float | None - Time information of event that should be removed. - - Returns - ------- - mean flare time, flare width, normalization factor for time pdf - """ - - ratio = self.get_energy_spatial_signal_over_backround(fitparams) - time = self._tdm_list[0].get_data("time") - - if sob_thresh > 0: # remove events below threshold - for i in range(len(ratio)): - mask = ratio > sob_thresh - ratio[i] = ratio[i][mask] - time[i] = time[i][mask] - - # in case, remove event - if remove_time is not None: - mask = time == remove_time - ratio = ratio[~mask] - time = time[~mask] - - # expectation maximization - mu = np.linspace(self._data_list[0].grl["start"][0], self._data_list[-1].grl["stop"][-1], n+2)[1:-1] - sigma = np.ones(n) * initial_width - ns = np.ones(n) * 10 - llh_diff = 100 - llh_old = 0 - llh_diff_list = [100] * 20 - - iteration = 0 - - while iteration < iter_max and llh_diff > tol: # run until convergence or maximum number of iterations - iteration += 1 - - e, logllh = expectation_em(ns, mu, sigma, time, ratio) - - llh_new = np.sum(logllh) - tmp_diff = np.abs(llh_old - llh_new) / llh_new - llh_diff_list = llh_diff_list[:-1] - llh_diff_list.insert(0, tmp_diff) - llh_diff = np.max(llh_diff_list) - llh_old = llh_new - mu, sigma, ns = maximization_em(e, time) - - return mu, sigma, ns - - - def run_gamma_scan_single_flare(self, remove_time=None, gamma_min=1, gamma_max=5, n_gamma=51): - """Run em for different gammas in the signal energy pdf - - Parameters - ---------- - remove_time : float - Time information of event that should be removed. - gamma_min : float - Lower bound for gamma scan. - gamma_max : float - Upper bound for gamma scan. - n_gamma : int - Number of steps for gamma scan. - - Returns - ------- - array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" - """ - - dtype = [("gamma", "f8"), ("mu", "f8"), ("sigma", "f8"), ("ns_em", "f8")] - results = np.empty(n_gamma, dtype=dtype) - - for index, g in enumerate(np.linspace(gamma_min, gamma_max, n_gamma)): - mu, sigma, ns = self.em_fit({"gamma": g}, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, - initial_width=5000, remove_time=remove_time) - results[index] = (g, mu[0], sigma[0], ns[0]) - - return results - - - def calculate_TS(self, em_results, rss): - """Calculate the best TS value for the expectation maximization gamma scan. - - Parameters - ---------- - em_results : 1d ndarray of tuples - Gamma scan result. - rss : instance of RandomStateService - The instance of RandomStateService that should be used to generate - random numbers from. - - Returns - ------- - float maximized TS value - tuple(gamma from em scan [float], best fit mean time [float], best fit width [float]) - (float ns, float gamma) fitparams from TS optimization - """ - max_TS = 0 - best_time = None - best_flux = None - for index, result in enumerate(em_results): - self.change_signal_time(gauss={"mu": em_results["mu"], "sigma": em_results["sigma"]}) - (fitparamset, log_lambda_max, fitparam_values, status) = self.maximize_llhratio(rss) - TS = self.calculate_test_statistic(log_lambda_max, fitparam_values) - if TS > max_TS: - max_TS = TS - best_time = result - best_flux = fitparam_values - - return max_TS, best_time, fitparam_values - - - def unblind_flare(self, remove_time=None): - """Run EM on unscrambled data. Similar to the original analysis, remove the alert event. - - Parameters - ---------- - remove_time : float - Time information of event that should be removed. - In the case of the TXS analysis: remove_time=58018.8711856 - - Returns - ------- - array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" - """ - - # get the original unblinded data - rss = RandomStateService(seed=1) - - self.unblind(rss) - - time_results = self.run_gamma_scan_single_flare(remove_time=remove_time) - - return time_results - diff --git a/skyllh/core/expectation_maximization.py b/skyllh/core/expectation_maximization.py index 218dec99f3..85747dbd96 100644 --- a/skyllh/core/expectation_maximization.py +++ b/skyllh/core/expectation_maximization.py @@ -1,6 +1,17 @@ import numpy as np from scipy.stats import norm +from skyllh.core.analysis import TimeIntegratedMultiDatasetSingleSourceAnalysis +from skyllh.core.backgroundpdf import BackgroundUniformTimePDF +from skyllh.core.pdf import TimePDF +from skyllh.core.pdfratio import SigOverBkgPDFRatio +from skyllh.core.random import RandomStateService +from skyllh.core.signalpdf import ( + SignalBoxTimePDF, + SignalGaussTimePDF, +) + + def expectation_em(ns, mu, sigma, t, sob): """ Expectation step of expectation maximization. @@ -28,7 +39,7 @@ def expectation_em(ns, mu, sigma, t, sob): ns = np.atleast_1d(ns) mu = np.atleast_1d(mu) sigma = np.atleast_1d(sigma) - + b_term = (1 - np.cos(10 / 180 * np.pi)) / 2 N = len(t) e_sig = [] @@ -70,3 +81,268 @@ def maximization_em(e_sig, t): sigma = [max(1, s) for s in sigma] return mu, sigma, ns + + +class ExpectationMaximizationUtils(object): + def __init__(self, ana): + """Creates a expectation maximization utility class for time-dependent + single dataset point-like source analysis assuming a single source. + + Parameters + ---------- + ana : instance of TimeIntegratedMultiDatasetSingleSourceAnalysis + Analysis instance which will be stored in this class. + """ + self.ana = ana + + @property + def ana(self): + """The TimeIntegratedMultiDatasetSingleSourceAnalysis instance. + """ + return self._ana + + @ana.setter + def ana(self, analysis): + if not isinstance(analysis, TimeIntegratedMultiDatasetSingleSourceAnalysis): + raise TypeError("The ana argument must be an instance of " + "'TimeIntegratedMultiDatasetSingleSourceAnalysis'.") + self._ana = analysis + + + def change_time_pdf(self, gauss=None, box=None): + """Changes the time pdf. + + Parameters + ---------- + gauss : dict | None + None or dictionary with {"mu": float, "sigma": float}. + box : dict | None + None or dictionary with {"start": float, "end": float}. + """ + ana = self.ana + + if gauss is None and box is None: + raise TypeError("Either gauss or box have to be specified as time pdf.") + + grl = ana._data_list[0].grl + # redo this in case the background pdf was not calculated before + time_bkgpdf = BackgroundUniformTimePDF(grl) + if gauss is not None: + time_sigpdf = SignalGaussTimePDF(grl, gauss['mu'], gauss['sigma']) + elif box is not None: + time_sigpdf = SignalBoxTimePDF(grl, box["start"], box["end"]) + + time_pdfratio = SigOverBkgPDFRatio( + sig_pdf=time_sigpdf, + bkg_pdf=time_bkgpdf, + pdf_type=TimePDF + ) + + # the next line seems to make no difference in the llh evaluation. We keep it for consistency + ana._llhratio.llhratio_list[0].pdfratio_list[2] = time_pdfratio + # this line here is relevant for the llh evaluation + ana._llhratio.llhratio_list[0]._pdfratioarray._pdfratio_list[2] = time_pdfratio + + # change detector signal yield with flare livetime in sample (1 / grl_norm in pdf), + # rebuild the histograms if it is changed... + # signal injection? + + def get_energy_spatial_signal_over_backround(self, fitparams): + """Returns the signal over background ratio for + (spatial_signal * energy_signal) / (spatial_background * energy_background). + + Parameter + --------- + fitparams : dict + Dictionary with {"gamma": float} for energy pdf. + + Returns + ------- + ratio : 1d ndarray + Product of spatial and energy signal over background pdfs. + """ + ana = self.ana + + ratio = ana._llhratio.llhratio_list[0].pdfratio_list[0].get_ratio(ana._tdm_list[0], fitparams) + ratio *= ana._llhratio.llhratio_list[0].pdfratio_list[1].get_ratio(ana._tdm_list[0], fitparams) + + return ratio + + def change_fluxmodel_gamma(self, gamma): + """Set new gamma for the flux model. + + Parameter + --------- + gamma : float + Spectral index for flux model. + """ + ana = self.ana + + ana.src_hypo_group_manager.src_hypo_group_list[0].fluxmodel.gamma = gamma + + def change_signal_time(self, gauss=None, box=None): + """Change the signal injection to gauss or box. + + Parameters + ---------- + gauss : dict | None + None or dictionary {"mu": float, "sigma": float}. + box : dict | None + None or dictionary {"start" : float, "end" : float}. + """ + ana = self.ana + + ana.sig_generator.set_flare(box=box, gauss=gauss) + + def em_fit(self, fitparams, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, initial_width=5000, + remove_time=None): + """Run expectation maximization. + + Parameters + ---------- + fitparams : dict + Dictionary with value for gamma, e.g. {'gamma': 2}. + n : int + How many gaussians flares we are looking for. + tol : float + the stopping criteria for expectation maximization. This is the difference in the normalized likelihood over the + last 20 iterations. + iter_max : int + The maximum number of iterations, even if stopping criteria tolerance (`tol`) is not yet reached. + sob_thres : float + Set a minimum threshold for signal over background ratios. Ratios below this threshold will be removed. + initial_width : float + Starting width for the gaussian flare in days. + remove_time : float | None + Time information of event that should be removed. + + Returns + ------- + mean flare time, flare width, normalization factor for time pdf + """ + ana = self.ana + + ratio = self.get_energy_spatial_signal_over_backround(fitparams) + time = ana._tdm_list[0].get_data("time") + + if sob_thresh > 0: # remove events below threshold + for i in range(len(ratio)): + mask = ratio > sob_thresh + ratio[i] = ratio[i][mask] + time[i] = time[i][mask] + + # in case, remove event + if remove_time is not None: + mask = time == remove_time + ratio = ratio[~mask] + time = time[~mask] + + # expectation maximization + mu = np.linspace(ana._data_list[0].grl["start"][0], ana._data_list[-1].grl["stop"][-1], n+2)[1:-1] + sigma = np.ones(n) * initial_width + ns = np.ones(n) * 10 + llh_diff = 100 + llh_old = 0 + llh_diff_list = [100] * 20 + + iteration = 0 + + while iteration < iter_max and llh_diff > tol: # run until convergence or maximum number of iterations + iteration += 1 + + e, logllh = expectation_em(ns, mu, sigma, time, ratio) + + llh_new = np.sum(logllh) + tmp_diff = np.abs(llh_old - llh_new) / llh_new + llh_diff_list = llh_diff_list[:-1] + llh_diff_list.insert(0, tmp_diff) + llh_diff = np.max(llh_diff_list) + llh_old = llh_new + mu, sigma, ns = maximization_em(e, time) + + return mu, sigma, ns + + def run_gamma_scan_single_flare(self, remove_time=None, gamma_min=1, gamma_max=5, n_gamma=51): + """Run em for different gammas in the signal energy pdf + + Parameters + ---------- + remove_time : float + Time information of event that should be removed. + gamma_min : float + Lower bound for gamma scan. + gamma_max : float + Upper bound for gamma scan. + n_gamma : int + Number of steps for gamma scan. + + Returns + ------- + array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" + """ + dtype = [("gamma", "f8"), ("mu", "f8"), ("sigma", "f8"), ("ns_em", "f8")] + results = np.empty(n_gamma, dtype=dtype) + + for index, g in enumerate(np.linspace(gamma_min, gamma_max, n_gamma)): + mu, sigma, ns = self.em_fit({"gamma": g}, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, + initial_width=5000, remove_time=remove_time) + results[index] = (g, mu[0], sigma[0], ns[0]) + + return results + + def calculate_TS(self, em_results, rss): + """Calculate the best TS value for the expectation maximization gamma scan. + + Parameters + ---------- + em_results : 1d ndarray of tuples + Gamma scan result. + rss : instance of RandomStateService + The instance of RandomStateService that should be used to generate + random numbers from. + + Returns + ------- + float maximized TS value + tuple(gamma from em scan [float], best fit mean time [float], best fit width [float]) + (float ns, float gamma) fitparams from TS optimization + """ + ana = self.ana + + max_TS = 0 + best_time = None + best_flux = None + for index, result in enumerate(em_results): + self.change_signal_time(gauss={"mu": em_results["mu"], "sigma": em_results["sigma"]}) + (fitparamset, log_lambda_max, fitparam_values, status) = ana.maximize_llhratio(rss) + TS = ana.calculate_test_statistic(log_lambda_max, fitparam_values) + if TS > max_TS: + max_TS = TS + best_time = result + best_flux = fitparam_values + + return max_TS, best_time, fitparam_values + + def unblind_flare(self, remove_time=None): + """Run EM on unscrambled data. Similar to the original analysis, remove the alert event. + + Parameters + ---------- + remove_time : float + Time information of event that should be removed. + In the case of the TXS analysis: remove_time=58018.8711856 + + Returns + ------- + array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" + """ + ana = self.ana + + # get the original unblinded data + rss = RandomStateService(seed=1) + + ana.unblind(rss) + + time_results = self.run_gamma_scan_single_flare(remove_time=remove_time) + + return time_results From a96eba1c33a790b862fa9d5ad414f3853c31a25d Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 5 Apr 2023 17:13:36 +0200 Subject: [PATCH 258/274] Remove unused `sig_gen_list` property --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 19c067009d..0f3ea8c3de 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -427,18 +427,6 @@ def llhratio(self, llhratio): 'LLHRatio!') self._llhratio = llhratio - @property - def sig_gen_list(self): - """The list of PublicDataDatasetSignalGenerator instances for each dataset - """ - return self._sig_gen_list - - @sig_gen_list.setter - def sig_gen_list(self, sig_gen_list): - if(not issequenceof(sig_gen_list, PDDatasetSignalGenerator)): - raise TypeError('The sig_gen_list property must be a sequence of ' - 'PublicDataDatasetSignalGenerator instances!') - def generate_signal_events(self, rss, mean, poisson=True): shg_list = self._src_hypo_group_manager.src_hypo_group_list From 9a79718bb90367d12b75114c3b77e1ef3f310824 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 5 Apr 2023 18:02:08 +0200 Subject: [PATCH 259/274] Remove redundant code --- .../i3/publicdata_ps/signal_generator.py | 112 +++++------------- 1 file changed, 31 insertions(+), 81 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 0f3ea8c3de..ecbbce7528 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -554,86 +554,36 @@ def set_flare(self, gauss=None, box=None): def generate_signal_events(self, rss, mean, poisson=True): """ same as in PDSignalGenerator, but we assign times here. """ - shg_list = self._src_hypo_group_manager.src_hypo_group_list - - tot_n_events = 0 - signal_events_dict = {} - - for shg in shg_list: - # This only works with power-laws for now. - # Each source hypo group can have a different power-law - gamma = shg.fluxmodel.gamma - weights, _ = self.llhratio.dataset_signal_weights([mean, gamma]) - for (ds_idx, w) in enumerate(weights): - w_mean = mean * w - if(poisson): - n_events = rss.random.poisson( - float_cast( - w_mean, - '`mean` must be castable to type of float!' - ) - ) - else: - n_events = int_cast( - w_mean, - '`mean` must be castable to type of int!' - ) - tot_n_events += n_events - - events_ = None - for (shg_src_idx, src) in enumerate(shg.source_list): - ds = self._dataset_list[ds_idx] - sig_gen = PDDatasetSignalGenerator( - ds, src.dec, self.effA[ds_idx], self.sm[ds_idx]) - if self.effA[ds_idx] is None: - self.effA[ds_idx] = sig_gen.effA - if self.sm[ds_idx] is None: - self.sm[ds_idx] = sig_gen.smearing_matrix - # ToDo: here n_events should be split according to some - # source weight - events_ = sig_gen.generate_signal_events( - rss, - src.dec, - src.ra, - shg.fluxmodel, - n_events, - energy_cut_spline=self.splines[ds_idx], - cut_sindec=self.cut_sindec[ds_idx] - ) - if events_ is None: - continue - - # Assign times for flare. We can also use inverse transform - # sampling instead of the lazy version implemented here - tmp_grl = self.data_list[ds_idx].grl - for event_index in events_.indices: - while events_["time"][event_index] == 1: - if self.gauss is not None: - # make sure flare is in dataset - if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( - self.gauss["mu"] + 4 * self.gauss["sigma"] < tmp_grl["start"][0]): - break # this should never happen - time = norm( - self.gauss["mu"], self.gauss["sigma"]).rvs() - if self.box is not None: - # make sure flare is in dataset - if (self.box["start"] > tmp_grl["stop"][-1]) or ( - self.box["end"] < tmp_grl["start"][0]): - # this should never be the case, since - # there should be no events generated - break - livetime = self.box["end"] - self.box["start"] - time = rss.random.random() * livetime - time += self.box["start"] - # check if time is in grl - is_in_grl = (tmp_grl["start"] <= time) & ( - tmp_grl["stop"] >= time) - if np.any(is_in_grl): - events_["time"][event_index] = time - - if shg_src_idx == 0: - signal_events_dict[ds_idx] = events_ - else: - signal_events_dict[ds_idx].append(events_) + # Call method from the parent class. + tot_n_events, signal_events_dict = super().generate_signal_events(rss, mean, poisson=poisson) + + # Assign times for flare. We can also use inverse transform + # sampling instead of the lazy version implemented here. + for (ds_idx, events_) in signal_events_dict.items(): + tmp_grl = self.data_list[ds_idx].grl + for event_index in events_.indices: + while events_["time"][event_index] == 1: + if self.gauss is not None: + # make sure flare is in dataset + if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( + self.gauss["mu"] + 4 * self.gauss["sigma"] < tmp_grl["start"][0]): + break # this should never happen + time = norm( + self.gauss["mu"], self.gauss["sigma"]).rvs() + if self.box is not None: + # make sure flare is in dataset + if (self.box["start"] > tmp_grl["stop"][-1]) or ( + self.box["end"] < tmp_grl["start"][0]): + # this should never be the case, since + # there should be no events generated + break + livetime = self.box["end"] - self.box["start"] + time = rss.random.random() * livetime + time += self.box["start"] + # check if time is in grl + is_in_grl = (tmp_grl["start"] <= time) & ( + tmp_grl["stop"] >= time) + if np.any(is_in_grl): + events_["time"][event_index] = time return tot_n_events, signal_events_dict From eecf3e5862cbe9ab71b5a70d48aad5d37724aa4c Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 6 Apr 2023 12:30:05 +0200 Subject: [PATCH 260/274] Check if the pdfratio value is already cached. --- skyllh/analyses/i3/publicdata_ps/pdfratio.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pdfratio.py b/skyllh/analyses/i3/publicdata_ps/pdfratio.py index e0c259c466..961addf28f 100644 --- a/skyllh/analyses/i3/publicdata_ps/pdfratio.py +++ b/skyllh/analyses/i3/publicdata_ps/pdfratio.py @@ -196,8 +196,8 @@ def get_ratio(self, tdm, fitparams=None, tl=None): fitparams_hash = make_params_hash(fitparams) # Check if the ratio value is already cached. - #if(self._is_cached(tdm, fitparams_hash)): - # return self._cache_ratio + if self._is_cached(tdm, fitparams_hash): + return self._cache_ratio self._calculate_ratio_and_gradients(tdm, fitparams, fitparams_hash) @@ -223,8 +223,8 @@ def get_gradient(self, tdm, fitparams, fitparam_name): pidx = self.convert_signal_fitparam_name_into_index(fitparam_name) # Check if the gradients have been calculated already. - #if(self._is_cached(tdm, fitparams_hash)): - # return self._cache_gradients[pidx] + if self._is_cached(tdm, fitparams_hash): + return self._cache_gradients[pidx] # The gradients have not been calculated yet. self._calculate_ratio_and_gradients(tdm, fitparams, fitparams_hash) From 6d50977e070d827a89ac25ac9b900701f4951431 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Thu, 6 Apr 2023 20:48:46 +0200 Subject: [PATCH 261/274] Fix import error and changed kwarg name --- skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py index cf13f8b0a2..1c0b59cc53 100644 --- a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py @@ -81,7 +81,7 @@ # Analysis specific classes for working with the public data. from skyllh.analyses.i3.publicdata_ps.signal_generator import ( - PublicDataSignalGenerator + PDSignalGenerator ) from skyllh.analyses.i3.publicdata_ps.detsigyield import ( PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod @@ -258,7 +258,7 @@ def create_analysis( fitparam_ns, test_statistic, bkg_gen_method, - custom_sig_generator=PublicDataSignalGenerator + sig_generator_cls=PDSignalGenerator ) # Define the event selection method for pure optimization purposes. From 44904b1ab342367a57362adbd45046668acfe590 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 11 Apr 2023 15:54:21 +0200 Subject: [PATCH 262/274] Minor docstring fixes --- .../analyses/i3/publicdata_ps/signal_generator.py | 2 +- skyllh/core/backgroundpdf.py | 15 +++++++-------- 2 files changed, 8 insertions(+), 9 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index ecbbce7528..dfa25cd515 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -552,7 +552,7 @@ def set_flare(self, gauss=None, box=None): self.gauss = gauss def generate_signal_events(self, rss, mean, poisson=True): - """ same as in PDSignalGenerator, but we assign times here. + """Same as in PDSignalGenerator, but we assign times here. """ # Call method from the parent class. tot_n_events, signal_events_dict = super().generate_signal_events(rss, mean, poisson=poisson) diff --git a/skyllh/core/backgroundpdf.py b/skyllh/core/backgroundpdf.py index 3977116abe..cb41ff7eb4 100644 --- a/skyllh/core/backgroundpdf.py +++ b/skyllh/core/backgroundpdf.py @@ -120,8 +120,9 @@ def __init__(self, grl): def cdf(self, t): - """ Compute the cumulative density function for the box pdf. This is needed for normalization. - + """Compute the cumulative density function for the box pdf. This is + needed for normalization. + Parameters ---------- t : float, ndarray @@ -145,10 +146,9 @@ def cdf(self, t): # take care of values beyond stop time in sample return cdf - def norm_uptime(self): - """compute the normalization with the dataset uptime. Distributions like + """Compute the normalization with the dataset uptime. Distributions like scipy.stats.norm are normalized (-inf, inf). These must be re-normalized such that the function sums to 1 over the finite good run list domain. @@ -166,12 +166,11 @@ def norm_uptime(self): return 1. / integral - def get_prob(self, tdm, fitparams=None, tl=None): - """Calculates the background time probability density of each event + """Calculates the background time probability density of each event. tdm : TrialDataManager - Unused interface argument + Unused interface argument. fitparams : None Unused interface argument. tl : instance of TimeLord | None @@ -183,7 +182,7 @@ def get_prob(self, tdm, fitparams=None, tl=None): pd : array of float The (N,)-shaped ndarray holding the probability density for each event. grads : empty array of float - Does not depend on fit parameter, so no gradient + Does not depend on fit parameter, so no gradient. """ livetime = self.grl["stop"][-1] - self.grl["start"][0] pd = 1./livetime From b22bc9dd6ae05fa84079d809e95e95076622e129 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 11 Apr 2023 15:58:55 +0200 Subject: [PATCH 263/274] Add `time_pdf` and optimize time injection --- .../i3/publicdata_ps/signal_generator.py | 104 +++++++++++++----- 1 file changed, 77 insertions(+), 27 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index dfa25cd515..6db4cac19c 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -2,7 +2,7 @@ import numpy as np from scipy import interpolate -from scipy.stats import norm +import scipy.stats from skyllh.core.py import ( issequenceof, @@ -530,6 +530,41 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, self.box = box self.gauss = gauss + self.time_pdf = self._get_time_pdf() + + def _get_time_pdf(self): + """Get the neutrino flare time pdf given parameters. + Will be used to generate random numbers by calling `rvs()` method. + + Returns + ------- + time_pdf : instance of scipy.stats.rv_continuous base class + Has to base scipy.stats.rv_continuous. + """ + # Make sure flare is in dataset. + for data_list in self.data_list: + tmp_grl = data_list.grl + + if self.gauss is not None: + if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( + self.gauss["mu"] + 4 * self.gauss["sigma"] < tmp_grl["start"][0]): + raise ValueError( + f"Gaussian {str(self.gauss)} flare is not in dataset.") + + if self.box is not None: + if (self.box["start"] > tmp_grl["stop"][-1]) or ( + self.box["end"] < tmp_grl["start"][0]): + raise ValueError( + f"Box {str(self.box)} flare is not in dataset.") + + # Create `time_pdf`. + if self.gauss is not None: + time_pdf = scipy.stats.norm(self.gauss["mu"], self.gauss["sigma"]) + if self.box is not None: + time_pdf = scipy.stats.uniform(self.box["start"], self.box["end"] - self.box["start"]) + + return time_pdf + def set_flare(self, gauss=None, box=None): """Set the neutrino flare given parameters. @@ -551,39 +586,54 @@ def set_flare(self, gauss=None, box=None): self.box = box self.gauss = gauss + self.time_pdf = self._get_time_pdf() + + def is_in_grl(self, time, grl): + """Helper function to check if given times are in the grl ontime. + + Parameters + ---------- + time : 1d ndarray + Time values. + grl : ndarray + Array of the detector good run list. + + Returns + ------- + is_in_grl : 1d ndarray + Boolean mask of `time` in grl ontime. + """ + def f(time, grl): + return np.any((grl["start"] <= time) & (time <= grl["stop"])) + + # Vectorize `f`, but exclude `grl` argument from vectorization. + # This is needed to support `time` as an array argument. + f_v = np.vectorize(f, excluded=[1]) + is_in_grl = f_v(time, grl) + + return is_in_grl + def generate_signal_events(self, rss, mean, poisson=True): """Same as in PDSignalGenerator, but we assign times here. """ - # Call method from the parent class. - tot_n_events, signal_events_dict = super().generate_signal_events(rss, mean, poisson=poisson) + # Call method from the parent class to generate signal events. + tot_n_events, signal_events_dict = super().generate_signal_events( + rss, mean, poisson=poisson) # Assign times for flare. We can also use inverse transform # sampling instead of the lazy version implemented here. for (ds_idx, events_) in signal_events_dict.items(): tmp_grl = self.data_list[ds_idx].grl - for event_index in events_.indices: - while events_["time"][event_index] == 1: - if self.gauss is not None: - # make sure flare is in dataset - if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( - self.gauss["mu"] + 4 * self.gauss["sigma"] < tmp_grl["start"][0]): - break # this should never happen - time = norm( - self.gauss["mu"], self.gauss["sigma"]).rvs() - if self.box is not None: - # make sure flare is in dataset - if (self.box["start"] > tmp_grl["stop"][-1]) or ( - self.box["end"] < tmp_grl["start"][0]): - # this should never be the case, since - # there should be no events generated - break - livetime = self.box["end"] - self.box["start"] - time = rss.random.random() * livetime - time += self.box["start"] - # check if time is in grl - is_in_grl = (tmp_grl["start"] <= time) & ( - tmp_grl["stop"] >= time) - if np.any(is_in_grl): - events_["time"][event_index] = time + # Optimized time injection version, based on csky implementation. + # https://github.com/icecube/csky/blob/7e969639c5ef6dbb42872dac9b761e1e8b0ccbe2/csky/inj.py#L1122 + time = [] + n_events = len(events_) + while len(time) < n_events: + time = np.r_[time, self.time_pdf.rvs(n_events - len(time), random_state=rss.random)] + # Check if time is in grl. + is_in_grl_mask = self.is_in_grl(time, tmp_grl) + time = time[is_in_grl_mask] + + events_["time"] = time return tot_n_events, signal_events_dict From 3946064c93ec0eb131df2d88d4eba3c82ea141d0 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Tue, 11 Apr 2023 16:32:12 +0200 Subject: [PATCH 264/274] Fix link in dataset references --- skyllh/datasets/i3/PublicData_10y_ps.py | 2 +- skyllh/datasets/i3/PublicData_10y_ps_wMC.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 2afcf0e9ff..c0d8f15925 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -226,7 +226,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): # References ----------------------------------------- [1] IceCube Data for Neutrino Point-Source Searches: Years 2008-2018, - [[ArXiv link]] + [ArXiv link](https://arxiv.org/abs/2101.09836) [2] Time-integrated Neutrino Source Searches with 10 years of IceCube Data, Phys. Rev. Lett. 124, 051103 (2020) [3] All-sky search for time-integrated neutrino emission from astrophysical diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py index 7b1727f533..028f70024e 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py @@ -226,7 +226,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): # References ----------------------------------------- [1] IceCube Data for Neutrino Point-Source Searches: Years 2008-2018, - [[ArXiv link]] + [ArXiv link](https://arxiv.org/abs/2101.09836) [2] Time-integrated Neutrino Source Searches with 10 years of IceCube Data, Phys. Rev. Lett. 124, 051103 (2020) [3] All-sky search for time-integrated neutrino emission from astrophysical From 9f865ae8e7d02847912e8c584f132a59d4b2bb52 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 12 Apr 2023 11:53:34 +0200 Subject: [PATCH 265/274] Remove unused imports --- skyllh/core/analysis.py | 20 +++++--------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index c21fd2022f..0c1dae27d4 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -5,7 +5,6 @@ import abc import numpy as np -import pickle from skyllh.core.py import ( classname, @@ -15,17 +14,12 @@ from skyllh.core.storage import DataFieldRecordArray from skyllh.core.dataset import ( Dataset, - DatasetData + DatasetData, ) from skyllh.core.parameters import ( FitParameter, SourceFitParameterMapper, - SingleSourceFitParameterMapper -) -from skyllh.core.pdf import ( - EnergyPDF, - SpatialPDF, - TimePDF + SingleSourceFitParameterMapper, ) from skyllh.core.pdfratio import PDFRatio from skyllh.core.progressbar import ProgressBar @@ -36,15 +30,11 @@ SingleSourceDatasetSignalWeights, SingleSourceZeroSigH0SingleDatasetTCLLHRatio, MultiSourceZeroSigH0SingleDatasetTCLLHRatio, - MultiSourceDatasetSignalWeights + MultiSourceDatasetSignalWeights, ) -from skyllh.core.scrambling import DataScramblingMethod from skyllh.core.timing import TaskTimer from skyllh.core.trialdata import TrialDataManager -from skyllh.core.optimize import ( - EventSelectionMethod, - AllEventSelectionMethod -) +from skyllh.core.optimize import EventSelectionMethod from skyllh.core.source_hypothesis import SourceHypoGroupManager from skyllh.core.test_statistic import TestStatistic from skyllh.core.multiproc import get_ncpu, parallelize @@ -52,7 +42,7 @@ from skyllh.core.background_generator import BackgroundGenerator from skyllh.core.signal_generator import ( SignalGeneratorBase, - SignalGenerator + SignalGenerator, ) from skyllh.physics.source import SourceModel From 7061347c06d7a4b2feba0bc426c3c7f83309677f Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 12 Apr 2023 12:20:25 +0200 Subject: [PATCH 266/274] Minor updates --- .../i3/publicdata_ps/signal_generator.py | 32 ++++++++++--------- 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 6db4cac19c..17f40759c2 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -543,17 +543,17 @@ def _get_time_pdf(self): """ # Make sure flare is in dataset. for data_list in self.data_list: - tmp_grl = data_list.grl + grl = data_list.grl if self.gauss is not None: - if (self.gauss["mu"] - 4 * self.gauss["sigma"] > tmp_grl["stop"][-1]) or ( - self.gauss["mu"] + 4 * self.gauss["sigma"] < tmp_grl["start"][0]): + if (self.gauss["mu"] - 4 * self.gauss["sigma"] > grl["stop"][-1]) or ( + self.gauss["mu"] + 4 * self.gauss["sigma"] < grl["start"][0]): raise ValueError( f"Gaussian {str(self.gauss)} flare is not in dataset.") if self.box is not None: - if (self.box["start"] > tmp_grl["stop"][-1]) or ( - self.box["end"] < tmp_grl["start"][0]): + if (self.box["start"] > grl["stop"][-1]) or ( + self.box["end"] < grl["start"][0]): raise ValueError( f"Box {str(self.box)} flare is not in dataset.") @@ -617,23 +617,25 @@ def generate_signal_events(self, rss, mean, poisson=True): """Same as in PDSignalGenerator, but we assign times here. """ # Call method from the parent class to generate signal events. - tot_n_events, signal_events_dict = super().generate_signal_events( + (tot_n_events, signal_events_dict) = super().generate_signal_events( rss, mean, poisson=poisson) # Assign times for flare. We can also use inverse transform # sampling instead of the lazy version implemented here. for (ds_idx, events_) in signal_events_dict.items(): - tmp_grl = self.data_list[ds_idx].grl + grl = self.data_list[ds_idx].grl # Optimized time injection version, based on csky implementation. # https://github.com/icecube/csky/blob/7e969639c5ef6dbb42872dac9b761e1e8b0ccbe2/csky/inj.py#L1122 - time = [] + times = np.array([]) n_events = len(events_) - while len(time) < n_events: - time = np.r_[time, self.time_pdf.rvs(n_events - len(time), random_state=rss.random)] - # Check if time is in grl. - is_in_grl_mask = self.is_in_grl(time, tmp_grl) - time = time[is_in_grl_mask] - - events_["time"] = time + while len(times) < n_events: + times = np.concatenate( + (times, self.time_pdf.rvs(n_events - len(times), random_state=rss.random)) + ) + # Check if times is in grl. + is_in_grl_mask = self.is_in_grl(times, grl) + times = times[is_in_grl_mask] + + events_["time"] = times return tot_n_events, signal_events_dict From 61a3c38453be8e6ea1e82e9af6ffaa8d829ef2f3 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 12 Apr 2023 12:34:46 +0200 Subject: [PATCH 267/274] Fix code width --- .../analyses/i3/publicdata_ps/signal_generator.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 17f40759c2..98146bf6cb 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -486,9 +486,10 @@ def generate_signal_events(self, rss, mean, poisson=True): class PDTimeDependentSignalGenerator(PDSignalGenerator): - """ The time dependent signal generator works so far only for one single dataset. For multi datasets one - needs to adjust the dataset weights accordingly (scaling of the effective area with livetime of the flare - in the dataset) + """ The time dependent signal generator works so far only for one single + dataset. For multi datasets one needs to adjust the dataset weights + accordingly (scaling of the effective area with livetime of the flare in + the dataset). """ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, @@ -561,7 +562,10 @@ def _get_time_pdf(self): if self.gauss is not None: time_pdf = scipy.stats.norm(self.gauss["mu"], self.gauss["sigma"]) if self.box is not None: - time_pdf = scipy.stats.uniform(self.box["start"], self.box["end"] - self.box["start"]) + time_pdf = scipy.stats.uniform( + self.box["start"], + self.box["end"] - self.box["start"] + ) return time_pdf @@ -631,7 +635,8 @@ def generate_signal_events(self, rss, mean, poisson=True): n_events = len(events_) while len(times) < n_events: times = np.concatenate( - (times, self.time_pdf.rvs(n_events - len(times), random_state=rss.random)) + (times, self.time_pdf.rvs(n_events - len(times), + random_state=rss.random)) ) # Check if times is in grl. is_in_grl_mask = self.is_in_grl(times, grl) From d9959d97746cca382f789ff2d4a8c265eaa1a8b3 Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 12 Apr 2023 17:49:57 +0200 Subject: [PATCH 268/274] Add explanation to docstring --- skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py index 1c0b59cc53..e909552ded 100644 --- a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- -"""The trad_ps analysis is a multi-dataset time-integrated single source +"""The mcbkg_ps analysis is a multi-dataset time-integrated single source analysis with a two-component likelihood function using a spacial and an energy -event PDF. +event PDF. It initializes the background energy pdf using auxiliary fluxes and +pdfs, which are generated by running `scripts/mceq_atm_bkg.py` script. """ import argparse From 049d838a238c1131e6978bbfe5af1dee5621b32c Mon Sep 17 00:00:00 2001 From: Tomas Kontrimas Date: Wed, 12 Apr 2023 17:54:04 +0200 Subject: [PATCH 269/274] Clean up PD data samples definitions --- skyllh/datasets/i3/PublicData_10y_ps.py | 23 +- skyllh/datasets/i3/PublicData_10y_ps_wMC.py | 21 - skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py | 598 ++++++++++++++++++ skyllh/datasets/i3/__init__.py | 10 +- 4 files changed, 607 insertions(+), 45 deletions(-) create mode 100644 skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index c0d8f15925..3542bb1059 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Author: Dr. Martin Wolf -import os.path import numpy as np from skyllh.core.dataset import DatasetCollection @@ -279,8 +278,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC40_effectiveArea.csv') IC40.add_aux_data_definition( 'smearing_datafile', 'irfs/IC40_smearing.csv') - IC40.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC40.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.25, 10 + 1), @@ -305,8 +302,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC59_effectiveArea.csv') IC59.add_aux_data_definition( 'smearing_datafile', 'irfs/IC59_smearing.csv') - IC59.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC59.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.95, 2 + 1), @@ -332,8 +327,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC79_effectiveArea.csv') IC79.add_aux_data_definition( 'smearing_datafile', 'irfs/IC79_smearing.csv') - IC79.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC79.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.75, 10 + 1), @@ -358,8 +351,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_I_effectiveArea.csv') IC86_I.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_I_smearing.csv') - IC86_I.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_I.pkl') b = np.sin(np.radians(-5.)) # North/South transition boundary. sin_dec_bins = np.unique(np.concatenate([ @@ -386,8 +377,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_II.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_II.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), @@ -413,8 +402,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_III.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_III.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_III.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -434,8 +421,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_IV.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_IV.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_IV.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -455,8 +440,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_V.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_V.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_V.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -476,8 +459,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VI.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_VI.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_VI.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -497,8 +478,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VII.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_VII.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_VII.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -517,7 +496,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_II_VII = I3Dataset( name = 'IC86_II-VII', exp_pathfilenames = I3Dataset.get_combined_exp_pathfilenames(ds_list), - mc_pathfilenames = IC86_II.mc_pathfilename_list, + mc_pathfilenames = None, grl_pathfilenames = I3Dataset.get_combined_grl_pathfilenames(ds_list), **ds_kwargs ) diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py index 028f70024e..6631ac0739 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py @@ -1,7 +1,6 @@ # -*- coding: utf-8 -*- # Author: Dr. Martin Wolf -import os.path import numpy as np from skyllh.core.dataset import DatasetCollection @@ -279,8 +278,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC40_effectiveArea.csv') IC40.add_aux_data_definition( 'smearing_datafile', 'irfs/IC40_smearing.csv') - IC40.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC40.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.25, 10 + 1), @@ -305,8 +302,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC59_effectiveArea.csv') IC59.add_aux_data_definition( 'smearing_datafile', 'irfs/IC59_smearing.csv') - IC59.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC59.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.95, 2 + 1), @@ -332,8 +327,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC79_effectiveArea.csv') IC79.add_aux_data_definition( 'smearing_datafile', 'irfs/IC79_smearing.csv') - IC79.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC79.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.75, 10 + 1), @@ -358,8 +351,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_I_effectiveArea.csv') IC86_I.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_I_smearing.csv') - IC86_I.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_I.pkl') b = np.sin(np.radians(-5.)) # North/South transition boundary. sin_dec_bins = np.unique(np.concatenate([ @@ -386,8 +377,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_II.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_II.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), @@ -413,8 +402,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_III.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_III.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_III.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -434,8 +421,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_IV.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_IV.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_IV.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -455,8 +440,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_V.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_V.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_V.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -476,8 +459,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VI.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_VI.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_VI.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -497,8 +478,6 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VII.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') - IC86_VII.add_aux_data_definition( - 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_VII.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py b/skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py new file mode 100644 index 0000000000..9fca4d3483 --- /dev/null +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py @@ -0,0 +1,598 @@ +# -*- coding: utf-8 -*- +# Author: Dr. Martin Wolf + +import numpy as np + +from skyllh.core.dataset import DatasetCollection +from skyllh.i3.dataset import I3Dataset + + +def create_dataset_collection(base_path=None, sub_path_fmt=None): + """Defines the dataset collection for IceCube's 10-year + point-source public data, which is available at + http://icecube.wisc.edu/data-releases/20210126_PS-IC40-IC86_VII.zip + + Parameters + ---------- + base_path : str | None + The base path of the data files. The actual path of a data file is + assumed to be of the structure //. + If None, use the default path CFG['repository']['base_path']. + sub_path_fmt : str | None + The sub path format of the data files of the public data sample. + If None, use the default sub path format + 'icecube_10year_ps'. + + Returns + ------- + dsc : DatasetCollection + The dataset collection containing all the seasons as individual + I3Dataset objects. + """ + # Define the version of the data sample (collection). + (version, verqualifiers) = (1, dict(p=0)) + + # Define the default sub path format. + default_sub_path_fmt = 'icecube_10year_ps' + + # We create a dataset collection that will hold the individual seasonal + # public data datasets (all of the same version!). + dsc = DatasetCollection('Public Data 10-year point-source') + + dsc.description = """ + The events contained in this release correspond to the IceCube's + time-integrated point source search with 10 years of data [2]. Please refer + to the description of the sample and known changes in the text at [1]. + + The data contained in this release of IceCube’s point source sample shows + evidence of a cumulative excess of events from four sources (NGC 1068, + TXS 0506+056, PKS 1424+240, and GB6 J1542+6129) from a catalogue of 110 + potential sources. NGC 1068 gives the largest excess and is coincidentally + the hottest spot in the full Northern sky search [1]. + + Data from IC86-2012 through IC86-2014 used in [2] use an updated selection + and reconstruction compared to the 7 year time-integrated search [3] and the + detection of the 2014-2015 neutrino flare from the direction of + TXS 0506+056 [4]. The 7 year and 10 year versions of the sample show + overlaps of between 80 and 90%. + + An a posteriori cross check of the updated sample has been performed on + TXS 0506+056 showing two previously-significant cascade-like events removed + in the newer sample. These two events occur near the blazar's position + during the TXS flare and give large reconstructed energies, but are likely + not well-modeled by the track-like reconstructions included in this + selection. While the events are unlikely to be track-like, their + contribution to previous results has been handled properly. + + While the significance of the 2014-2015 TXS 0505+56 flare has decreased from + p=7.0e-5 to 8.1e-3, the change is a result of changes to the sample and not + of increased data. No problems have been identified with the previously + published results and since we have no reason a priori to prefer the new + sample over the old sample, these results do not supercede those in [4]. + + This release contains data beginning in 2008 (IC40) until the spring of 2018 + (IC86-2017). This release duplicates and supplants previously released data + from 2012 and earlier. Events from this release cannot be combined with any + other releases + + ----------------------------------------- + # Experimental data events + ----------------------------------------- + The "events" folder contains the events observed in the 10 year sample of + IceCube's point source neutrino selection. Each file corresponds to a single + season of IceCube datataking, including roughly one year of data. For each + event, reconstructed particle information is included. + + - MJD: The MJD time (ut1) of the event interaction given to 1e-8 days, + corresponding to roughly millisecond precision. + + - log10(E/GeV): The reconstructed energy of a muon passing through the + detector. The reconstruction follows the prescription for unfolding the + given in Section 8 of [5]. + + - AngErr[deg]: The estimated angular uncertainty on the reconstructed + direction given in degrees. The angular uncertainty is assumed to be + symmetric in azimuth and zenith and is used to calculate the signal spatial + probabilities for each event following the procedure given in [6]. The + errors are calibrated using simulated events so that they provide correct + coverage for an E^{-2} power law flux. This sample assumes a lower limit on + the estimated angular uncertainty of 0.2 degrees. + + - RA[deg], Dec[deg]: The right ascension and declination (J2000) + corresponding to the particle's reconstructed origin. Given in degrees. + + - Azimuth[deg], Zenith[deg]: The local coordinates of the particle's + reconstructed origin. + + The local coordinates may be necessary when searching for transient + phenomena on timescales shorter than 1 day due to non-uniformity in the + detector's response as a function of azimuth. In these cases, we recommend + scrambling events in time, then using the local coordinates and time to + calculate new RA and Dec values. + + Note that during the preparation of this data release, one duplicated event + was discovered in the IC86-2015 season. This event has not contributed to + any significant excesses. + + ----------------------------------------- + # Detector uptime + ----------------------------------------- + In order to properly account for detector uptime, IceCube maintains + "good run lists". These contain information about "good runs", periods of + datataking useful for analysis. Data may be marked unusable for various + reasons, including major construction or upgrade work, calibration runs, or + other anomalies. The "uptime" folder contains lists of the good runs for + each season. + + - MJD_start[days], MJD_stop[days]: The start and end times for each good run + + ----------------------------------------- + # Instrument response functions + ----------------------------------------- + In order to best model the response of the IceCube detector to a given + signal, Monte Carlo simulations are produced for each detector + configuration. Events are sampled from these simulations to model the + response of point sources from an arbitrary source and spectrum. + + We provide several binned responses for the detector in the "irfs" folder + of this data release. + + ------------------ + # Effective Areas + ------------------ + The effective area is a property of the detector and selection which, when + convolved with a flux model, gives the expected rate of events in the + detector. Here we release the muon neutrino effective areas for each season + of data. + + The effective areas are averaged over bins using simulated muon neutrino + events ranging from 100 GeV to 100 PeV. Because the response varies widely + in both energy and declination, we provide the tabulated response in these + two dimensions. Due to IceCube's unique position at the south pole, the + effective area is uniform in right ascension for timescales longer than + 1 day. It varies by about 10% as a function of azimuth, an effect which may + be important for shorter timescales. While the azimuthal effective areas are + not included here, they are included in IceCube's internal analyses. + These may be made available upon request. + + Tabulated versions of the effective area are included in csv files in the + "irfs" folder. Plotted versions are included as pdf files in the same + location. Because the detector configuration and selection were unchanged + after the IC86-2012 season, the effective area for this season should be + used for IC86-2012 through IC86-2017. + + - log10(E_nu/GeV)_min, log10(E_nu/GeV)_max: The minimum and maximum of the + energy bin used to caclulate the average effective area. Note that this uses + the neutrino's true energy and not the reconstructed muon energy. + + - Dec_nu_min[deg], Dec_nu_max[deg]: The minimum and maximum of the + declination of the neutrino origin. Again, note that this is the true + direction of the neutrino and not the reconstructed muon direction. + + - A_Eff[cm^2]: The average effective area across a bin. + + ------------------ + # Smearing Matrices + ------------------ + IceCube has a nontrivial smearing matrix with correlations between the + directional uncertainty, the point spread function, and the reconstructed + muon energy. To provide the most complete set of information, we include + tables of these responses for each season from IC40 through IC86-2012. + Seasons after IC86-2012 reuse that season's response functions. + + The included smearing matrices take the form of 5D tables mapping a + (E_nu, Dec_nu) bin in effective area to a 3D matrix of (E, PSF, AngErr). + The contents of each 3D matrix bin give the fractional count of simulated + events within the bin relative to all events in the (E_nu, Dec_nu) bin. + + Fractional_Counts = [Events in (E_nu, Dec_nu, E, PSF, AngErr)] / + [Events in (E_nu, Dec_nu)] + + The simulations statistics, while large enough for direct sampling, are + limited when producing these tables, ranging from just 621,858 simulated + events for IC40 to 11,595,414 simulated events for IC86-2012. In order to + reduce statistical uncertainties in each 5D bin, bins are selected in each + (E_nu, Dec_nu) bin independently. The bin edges are given in the smearing + matrix files. All locations not given have a Fractional_Counts of 0. + + - log10(E_nu/GeV)_min, log10(E_nu/GeV)_max: The minimum and maximum of the + energy bin used to caclulate the average effective area. Note that this uses + the neutrino's true energy and not the reconstructed muon energy. + + - Dec_nu_min[deg], Dec_nu_max[deg]: The minimum and maximum of the + declination of the neutrino origin. Again, note that this is the true + direction of the neutrino and not the reconstructed muon direction. + + - log10(E/GeV): The reconstructed energy of a muon passing through the + detector. The reconstruction follows the prescription for unfolding the + given in Section 8 of [5]. + + - PSF_min[deg], PSF_max[deg]: The minimum and maximum of the true angle + between the neutrino origin and the reconstructed muon direction. + + - AngErr_min[deg], AngErr_max[deg]: The estimated angular uncertainty on the + reconstructed direction given in degrees. The angular uncertainty is assumed + to be symmetric in azimuth and zenith and is used to calculate the signal + spatial probabilities for each event following the procedure given in [6]. + The errors are calibrated so that they provide correct coverage for an + E^{-2} power law flux. This sample assumes a lower limit on the estimated + angular uncertainty of 0.2 degrees. + + - Fractional_Counts: The fraction of simulated events falling within each + 5D bin relative to all events in the (E_nu, Dec_nu) bin. + + ----------------------------------------- + # References + ----------------------------------------- + [1] IceCube Data for Neutrino Point-Source Searches: Years 2008-2018, + [ArXiv link](https://arxiv.org/abs/2101.09836) + [2] Time-integrated Neutrino Source Searches with 10 years of IceCube Data, + Phys. Rev. Lett. 124, 051103 (2020) + [3] All-sky search for time-integrated neutrino emission from astrophysical + sources with 7 years of IceCube data, + Astrophys. J., 835 (2017) no. 2, 151 + [4] Neutrino emission from the direction of the blazar TXS 0506+056 prior to + the IceCube-170922A alert, + Science 361, 147-151 (2018) + [5] Energy Reconstruction Methods in the IceCube Neutrino Telescope, + JINST 9 (2014), P03009 + [6] Methods for point source analysis in high energy neutrino telescopes, + Astropart.Phys.29:299-305,2008 + + ----------------------------------------- + # Last Update + ----------------------------------------- + 28 January 2021 + """ + + # Define the common keyword arguments for all data sets. + ds_kwargs = dict( + livetime = None, + version = version, + verqualifiers = verqualifiers, + base_path = base_path, + default_sub_path_fmt = default_sub_path_fmt, + sub_path_fmt = sub_path_fmt + ) + + grl_field_name_renaming_dict = { + 'MJD_start[days]': 'start', + 'MJD_stop[days]': 'stop' + } + + # Define the datasets for the different seasons. + # For the declination and energy binning we use the same binning as was + # used in the original point-source analysis using the PointSourceTracks + # dataset. + + # ---------- IC40 ---------------------------------------------------------- + IC40 = I3Dataset( + name = 'IC40', + exp_pathfilenames = 'events/IC40_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC40_exp.csv', + **ds_kwargs + ) + IC40.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC40.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC40_effectiveArea.csv') + IC40.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC40_smearing.csv') + IC40.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC40.pkl') + IC40.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC40.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.25, 10 + 1), + np.linspace(-0.25, 0.0, 10 + 1), + np.linspace(0.0, 1., 10 + 1), + ])) + IC40.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) + IC40.define_binning('log_energy', energy_bins) + + # ---------- IC59 ---------------------------------------------------------- + IC59 = I3Dataset( + name = 'IC59', + exp_pathfilenames = 'events/IC59_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC59_exp.csv', + **ds_kwargs + ) + IC59.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC59.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC59_effectiveArea.csv') + IC59.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC59_smearing.csv') + IC59.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC59.pkl') + IC59.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC59.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.95, 2 + 1), + np.linspace(-0.95, -0.25, 25 + 1), + np.linspace(-0.25, 0.05, 15 + 1), + np.linspace(0.05, 1., 10 + 1), + ])) + IC59.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) + IC59.define_binning('log_energy', energy_bins) + + # ---------- IC79 ---------------------------------------------------------- + IC79 = I3Dataset( + name = 'IC79', + exp_pathfilenames = 'events/IC79_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC79_exp.csv', + **ds_kwargs + ) + IC79.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC79.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC79_effectiveArea.csv') + IC79.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC79_smearing.csv') + IC79.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC79.pkl') + IC79.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC79.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.75, 10 + 1), + np.linspace(-0.75, 0., 15 + 1), + np.linspace(0., 1., 20 + 1) + ])) + IC79.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) + IC79.define_binning('log_energy', energy_bins) + + # ---------- IC86-I -------------------------------------------------------- + IC86_I = I3Dataset( + name = 'IC86_I', + exp_pathfilenames = 'events/IC86_I_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC86_I_exp.csv', + **ds_kwargs + ) + IC86_I.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_I.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_I_effectiveArea.csv') + IC86_I.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_I_smearing.csv') + IC86_I.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_I.pkl') + IC86_I.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_I.pkl') + + b = np.sin(np.radians(-5.)) # North/South transition boundary. + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.2, 10 + 1), + np.linspace(-0.2, b, 4 + 1), + np.linspace(b, 0.2, 5 + 1), + np.linspace(0.2, 1., 10), + ])) + IC86_I.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + IC86_I.define_binning('log_energy', energy_bins) + + # ---------- IC86-II ------------------------------------------------------- + IC86_II = I3Dataset( + name = 'IC86_II', + exp_pathfilenames = 'events/IC86_II_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC86_II_exp.csv', + **ds_kwargs + ) + IC86_II.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_II.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_II.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_II.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_II.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_II.pkl') + + sin_dec_bins = np.unique(np.concatenate([ + np.linspace(-1., -0.93, 4 + 1), + np.linspace(-0.93, -0.3, 10 + 1), + np.linspace(-0.3, 0.05, 9 + 1), + np.linspace(0.05, 1., 18 + 1), + ])) + IC86_II.define_binning('sin_dec', sin_dec_bins) + + energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + IC86_II.define_binning('log_energy', energy_bins) + + # ---------- IC86-III ------------------------------------------------------ + IC86_III = I3Dataset( + name = 'IC86_III', + exp_pathfilenames = 'events/IC86_III_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC86_III_exp.csv', + **ds_kwargs + ) + IC86_III.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_III.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_III.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_III.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_III.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_III.pkl') + + IC86_III.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_III.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-IV ------------------------------------------------------- + IC86_IV = I3Dataset( + name = 'IC86_IV', + exp_pathfilenames = 'events/IC86_IV_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC86_IV_exp.csv', + **ds_kwargs + ) + IC86_IV.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_IV.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_IV.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_IV.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_IV.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_IV.pkl') + + IC86_IV.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_IV.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-V -------------------------------------------------------- + IC86_V = I3Dataset( + name = 'IC86_V', + exp_pathfilenames = 'events/IC86_V_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC86_V_exp.csv', + **ds_kwargs + ) + IC86_V.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_V.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_V.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_V.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_V.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_V.pkl') + + IC86_V.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_V.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-VI ------------------------------------------------------- + IC86_VI = I3Dataset( + name = 'IC86_VI', + exp_pathfilenames = 'events/IC86_VI_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC86_VI_exp.csv', + **ds_kwargs + ) + IC86_VI.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VI.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_VI.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_VI.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_VI.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_VI.pkl') + + IC86_VI.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_VI.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-VII ------------------------------------------------------ + IC86_VII = I3Dataset( + name = 'IC86_VII', + exp_pathfilenames = 'events/IC86_VII_exp.csv', + mc_pathfilenames = None, + grl_pathfilenames = 'uptime/IC86_VII_exp.csv', + **ds_kwargs + ) + IC86_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_VII.add_aux_data_definition( + 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + IC86_VII.add_aux_data_definition( + 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_VII.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_VII.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_VII.pkl') + + IC86_VII.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_VII.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + # ---------- IC86-II-VII --------------------------------------------------- + ds_list = [ + IC86_II, + IC86_III, + IC86_IV, + IC86_V, + IC86_VI, + IC86_VII, + ] + IC86_II_VII = I3Dataset( + name = 'IC86_II-VII', + exp_pathfilenames = I3Dataset.get_combined_exp_pathfilenames(ds_list), + mc_pathfilenames = None, + grl_pathfilenames = I3Dataset.get_combined_grl_pathfilenames(ds_list), + **ds_kwargs + ) + IC86_II_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict + IC86_II_VII.add_aux_data_definition( + 'eff_area_datafile', + IC86_II.get_aux_data_definition('eff_area_datafile')) + + IC86_II_VII.add_aux_data_definition( + 'smearing_datafile', + IC86_II.get_aux_data_definition('smearing_datafile')) + + IC86_II_VII.add_binning_definition( + IC86_II.get_binning_definition('sin_dec')) + IC86_II_VII.add_binning_definition( + IC86_II.get_binning_definition('log_energy')) + + #--------------------------------------------------------------------------- + + dsc.add_datasets(( + IC40, + IC59, + IC79, + IC86_I, + IC86_II, + IC86_III, + IC86_IV, + IC86_V, + IC86_VI, + IC86_VII, + IC86_II_VII + )) + + dsc.set_exp_field_name_renaming_dict({ + 'MJD[days]': 'time', + 'log10(E/GeV)': 'log_energy', + 'AngErr[deg]': 'ang_err', + 'RA[deg]': 'ra', + 'Dec[deg]': 'dec', + 'Azimuth[deg]': 'azi', + 'Zenith[deg]': 'zen' + }) + + def add_run_number(data): + exp = data.exp + exp.append_field('run', np.repeat(0, len(exp))) + + def convert_deg2rad(data): + exp = data.exp + exp['ang_err'] = np.deg2rad(exp['ang_err']) + exp['ra'] = np.deg2rad(exp['ra']) + exp['dec'] = np.deg2rad(exp['dec']) + exp['azi'] = np.deg2rad(exp['azi']) + exp['zen'] = np.deg2rad(exp['zen']) + + dsc.add_data_preparation(add_run_number) + dsc.add_data_preparation(convert_deg2rad) + + return dsc diff --git a/skyllh/datasets/i3/__init__.py b/skyllh/datasets/i3/__init__.py index 3cd88bf4e4..3b3cb2ea35 100644 --- a/skyllh/datasets/i3/__init__.py +++ b/skyllh/datasets/i3/__init__.py @@ -1,7 +1,13 @@ # -*- coding: utf-8 -*- -from skyllh.datasets.i3 import PublicData_10y_ps +from skyllh.datasets.i3 import ( + PublicData_10y_ps, + PublicData_10y_ps_wMC, + PublicData_10y_ps_wMCEq, +) data_samples = { - 'PublicData_10y_ps': PublicData_10y_ps + 'PublicData_10y_ps': PublicData_10y_ps, + 'PublicData_10y_ps_wMC': PublicData_10y_ps_wMC, + 'PublicData_10y_ps_wMCEq': PublicData_10y_ps_wMCEq } From 7ad80bc26bfd958968f292d92a6ea1d4d05ea354 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 14 Apr 2023 12:08:42 +0200 Subject: [PATCH 270/274] Fixes #140, unifies the naming scheme for the public data scripts and removes unused imports. --- .../i3/publicdata_ps/{pd_aeff.py => aeff.py} | 0 skyllh/analyses/i3/publicdata_ps/bkg_flux.py | 2 -- skyllh/analyses/i3/publicdata_ps/detsigyield.py | 2 +- skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py | 1 - .../i3/publicdata_ps/signal_generator.py | 16 ++++++++++++++-- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 4 ++-- ...{pd_smearing_matrix.py => smearing_matrix.py} | 0 7 files changed, 17 insertions(+), 8 deletions(-) rename skyllh/analyses/i3/publicdata_ps/{pd_aeff.py => aeff.py} (100%) rename skyllh/analyses/i3/publicdata_ps/{pd_smearing_matrix.py => smearing_matrix.py} (100%) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/aeff.py similarity index 100% rename from skyllh/analyses/i3/publicdata_ps/pd_aeff.py rename to skyllh/analyses/i3/publicdata_ps/aeff.py diff --git a/skyllh/analyses/i3/publicdata_ps/bkg_flux.py b/skyllh/analyses/i3/publicdata_ps/bkg_flux.py index e39223ddf4..731489f367 100644 --- a/skyllh/analyses/i3/publicdata_ps/bkg_flux.py +++ b/skyllh/analyses/i3/publicdata_ps/bkg_flux.py @@ -2,8 +2,6 @@ import numpy as np import pickle -from scipy import interpolate -from scipy import integrate from skyllh.physics.flux import PowerLawFlux from skyllh.core.binning import get_bincenters_from_binedges diff --git a/skyllh/analyses/i3/publicdata_ps/detsigyield.py b/skyllh/analyses/i3/publicdata_ps/detsigyield.py index 1dc2794c8c..ad138b75c2 100644 --- a/skyllh/analyses/i3/publicdata_ps/detsigyield.py +++ b/skyllh/analyses/i3/publicdata_ps/detsigyield.py @@ -23,7 +23,7 @@ PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod, PowerLawFluxPointLikeSourceI3DetSigYield ) -from skyllh.analyses.i3.publicdata_ps.pd_aeff import ( +from skyllh.analyses.i3.publicdata_ps.aeff import ( load_effective_area_array ) diff --git a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py index e909552ded..5f98db4220 100644 --- a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py @@ -9,7 +9,6 @@ import argparse import logging import numpy as np -import os.path import pickle from skyllh.core.progressbar import ProgressBar diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 98146bf6cb..a36ac2a673 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -18,10 +18,10 @@ from skyllh.core.storage import DataFieldRecordArray from skyllh.analyses.i3.publicdata_ps.utils import psi_to_dec_and_ra -from skyllh.analyses.i3.publicdata_ps.pd_smearing_matrix import ( +from skyllh.analyses.i3.publicdata_ps.smearing_matrix import ( PDSmearingMatrix ) -from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff +from skyllh.analyses.i3.publicdata_ps.aeff import PDAeff class PDDatasetSignalGenerator(object): @@ -429,11 +429,23 @@ def llhratio(self, llhratio): def generate_signal_events(self, rss, mean, poisson=True): shg_list = self._src_hypo_group_manager.src_hypo_group_list + # Only supports a single source hypothesis group. Raise an error + # if more than one shg is in the source hypo group manager. + if len(shg_list) > 1: + raise RuntimeError( + 'Signal injection for multiple source hypothesis groups is ' + 'not supported yet.') tot_n_events = 0 signal_events_dict = {} for shg in shg_list: + # Only supports single point source signal injection. Raise + # an error if more than one source is in the source hypo group. + if len(shg.source_list) > 1: + raise RuntimeError( + 'Signal injection for multiple sources within a source ' + 'hypothesis group is not supported yet.') # This only works with power-laws for now. # Each source hypo group can have a different power-law gamma = shg.fluxmodel.gamma diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 756285c9bb..474e2a976c 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -24,11 +24,11 @@ from skyllh.i3.dataset import I3Dataset from skyllh.physics.flux import FluxModel -from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff +from skyllh.analyses.i3.publicdata_ps.aeff import PDAeff from skyllh.analyses.i3.publicdata_ps.utils import ( FctSpline1D, ) -from skyllh.analyses.i3.publicdata_ps.pd_smearing_matrix import ( +from skyllh.analyses.i3.publicdata_ps.smearing_matrix import ( PDSmearingMatrix ) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py b/skyllh/analyses/i3/publicdata_ps/smearing_matrix.py similarity index 100% rename from skyllh/analyses/i3/publicdata_ps/pd_smearing_matrix.py rename to skyllh/analyses/i3/publicdata_ps/smearing_matrix.py From a1662be19c697f771ac330a7e7c06e5fa6b83e57 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 14 Apr 2023 12:10:04 +0200 Subject: [PATCH 271/274] Comply with new naming scheme. --- skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py b/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py index 2cc226e1eb..9f59c46e63 100644 --- a/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py +++ b/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py @@ -7,7 +7,7 @@ import mceq_config as config from MCEq.core import MCEqRun -from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff +from skyllh.analyses.i3.publicdata_ps.aeff import PDAeff from skyllh.datasets.i3 import PublicData_10y_ps def create_flux_file(save_path, ds): From 0630f783ea6224be093e96bd5d3fa45027e84378 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 17 Apr 2023 09:51:22 +0200 Subject: [PATCH 272/274] Closes #124. --- skyllh/datasets/i3/PublicData_10y_ps.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 3542bb1059..6a14723fe8 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -361,7 +361,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC86_I.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + energy_bins = np.arange(1., 10.5 + 0.01, 0.125) IC86_I.define_binning('log_energy', energy_bins) # ---------- IC86-II ------------------------------------------------------- @@ -386,7 +386,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC86_II.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + energy_bins = np.arange(0.5, 9.5 + 0.01, 0.125) IC86_II.define_binning('log_energy', energy_bins) # ---------- IC86-III ------------------------------------------------------ From 22a0eb1613dac149e0ad171bb99fd4b06751c149 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 17 Apr 2023 14:37:23 +0200 Subject: [PATCH 273/274] Update dataset definition with MC. --- skyllh/datasets/i3/PublicData_10y_ps_wMC.py | 4 ++-- skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py index 6631ac0739..c7c40d1cde 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps_wMC.py +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMC.py @@ -361,7 +361,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC86_I.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + energy_bins = np.arange(1., 10.5 + 0.01, 0.125) IC86_I.define_binning('log_energy', energy_bins) # ---------- IC86-II ------------------------------------------------------- @@ -386,7 +386,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC86_II.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + energy_bins = np.arange(0.5, 9.5 + 0.01, 0.125) IC86_II.define_binning('log_energy', energy_bins) # ---------- IC86-III ------------------------------------------------------ diff --git a/skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py b/skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py index 9fca4d3483..38e05ce111 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py +++ b/skyllh/datasets/i3/PublicData_10y_ps_wMCEq.py @@ -377,7 +377,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC86_I.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + energy_bins = np.arange(1., 10.5 + 0.01, 0.125) IC86_I.define_binning('log_energy', energy_bins) # ---------- IC86-II ------------------------------------------------------- @@ -406,7 +406,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC86_II.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(1., 9.5 + 0.01, 0.125) + energy_bins = np.arange(0.5, 9.5 + 0.01, 0.125) IC86_II.define_binning('log_energy', energy_bins) # ---------- IC86-III ------------------------------------------------------ From 2e440591a33d55352854e99a0043e65a38252e82 Mon Sep 17 00:00:00 2001 From: Martina Karl Date: Wed, 19 Apr 2023 17:25:38 +0200 Subject: [PATCH 274/274] Publicdata time dependent (#146) Restructuring the time-dependent public data analysis. --- .../i3/publicdata_ps/time_dependent_ps.py | 221 ++++++++++-- skyllh/core/expectation_maximization.py | 328 ++++-------------- skyllh/core/scrambling.py | 4 +- skyllh/i3/scrambling.py | 79 ++++- 4 files changed, 343 insertions(+), 289 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py index 62fe8d07d3..a6e6daabcd 100644 --- a/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/time_dependent_ps.py @@ -36,11 +36,12 @@ # Classes for defining the analysis. from skyllh.core.test_statistic import TestStatisticWilks from skyllh.core.analysis import ( - TimeIntegratedMultiDatasetSingleSourceAnalysis + TimeIntegratedMultiDatasetSingleSourceAnalysis, ) # Classes to define the background generation. -from skyllh.core.scrambling import DataScrambler, UniformRAScramblingMethod +from skyllh.core.scrambling import DataScrambler +from skyllh.i3.scrambling import I3SeasonalVariationTimeScramblingMethod from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod # Classes to define the signal and background PDFs. @@ -66,15 +67,7 @@ pointlikesource_to_data_field_array ) -# Logging setup utilities. -from skyllh.core.debugging import ( - setup_logger, - setup_console_handler, - setup_file_handler -) - -# Pre-defined public IceCube data samples. -from skyllh.datasets.i3 import data_samples +from skyllh.core.expectation_maximization import em_fit # Analysis specific classes for working with the public data. from skyllh.analyses.i3.publicdata_ps.signal_generator import ( @@ -92,13 +85,184 @@ from skyllh.analyses.i3.publicdata_ps.backgroundpdf import ( PDDataBackgroundI3EnergyPDF ) -from skyllh.analyses.i3.publicdata_ps.utils import create_energy_cut_spline +from skyllh.analyses.i3.publicdata_ps.utils import ( + create_energy_cut_spline, +) from skyllh.analyses.i3.publicdata_ps.time_integrated_ps import ( psi_func, - TXS_location ) +def change_time_pdf(analysis, gauss=None, box=None): + """Changes the time pdf. + + Parameters + ---------- + gauss : dict | None + None or dictionary with {"mu": float, "sigma": float}. + box : dict | None + None or dictionary with {"start": float, "end": float}. + """ + + if gauss is None and box is None: + raise TypeError("Either gauss or box have to be specified as time pdf.") + + grl = analysis._data_list[0].grl + # redo this in case the background pdf was not calculated before + time_bkgpdf = BackgroundUniformTimePDF(grl) + if gauss is not None: + time_sigpdf = SignalGaussTimePDF(grl, gauss['mu'], gauss['sigma']) + elif box is not None: + time_sigpdf = SignalBoxTimePDF(grl, box["start"], box["end"]) + + time_pdfratio = SigOverBkgPDFRatio( + sig_pdf=time_sigpdf, + bkg_pdf=time_bkgpdf, + pdf_type=TimePDF + ) + + # the next line seems to make no difference in the llh evaluation. We keep it for consistency + analysis._llhratio.llhratio_list[0].pdfratio_list[2] = time_pdfratio + # this line here is relevant for the llh evaluation + analysis._llhratio.llhratio_list[0]._pdfratioarray._pdfratio_list[2] = time_pdfratio + + # change detector signal yield with flare livetime in sample (1 / grl_norm in pdf), + # rebuild the histograms if it is changed... + + +def get_energy_spatial_signal_over_background(analysis, fitparams): + """Returns the signal over background ratio for + (spatial_signal * energy_signal) / (spatial_background * energy_background). + + Parameter + --------- + fitparams : dict + Dictionary with {"gamma": float} for energy pdf. + + Returns + ------- + ratio : 1d ndarray + Product of spatial and energy signal over background pdfs. + """ + + ratio = analysis._llhratio.llhratio_list[0].pdfratio_list[0].get_ratio(analysis._tdm_list[0], fitparams) + ratio *= analysis._llhratio.llhratio_list[0].pdfratio_list[1].get_ratio(analysis._tdm_list[0], fitparams) + + return ratio + + +def change_fluxmodel_gamma(analysis, gamma): + """Set new gamma for the flux model. + + Parameter + --------- + gamma : float + Spectral index for flux model. + """ + + analysis.src_hypo_group_manager.src_hypo_group_list[0].fluxmodel.gamma = gamma + + +def change_signal_time(analysis, gauss=None, box=None): + """Change the signal injection to gauss or box. + + Parameters + ---------- + gauss : dict | None + None or dictionary {"mu": float, "sigma": float}. + box : dict | None + None or dictionary {"start" : float, "end" : float}. + """ + + analysis.sig_generator.set_flare(box=box, gauss=gauss) + + +def calculate_TS(analysis, em_results, rss): + """Calculate the best TS value for the expectation maximization gamma scan. + + Parameters + ---------- + em_results : 1d ndarray of tuples + Gamma scan result. + rss : instance of RandomStateService + The instance of RandomStateService that should be used to generate + random numbers from. + + Returns + ------- + float maximized TS value + tuple(gamma from em scan [float], best fit mean time [float], best fit width [float]) + (float ns, float gamma) fitparams from TS optimization + """ + + max_TS = 0 + best_time = None + best_fitparams = None + for index, result in enumerate(em_results): + change_time_pdf(analysis, gauss={"mu": result["mu"], "sigma": result["sigma"]}) + (fitparamset, log_lambda_max, fitparam_values, status) = analysis.maximize_llhratio(rss) + TS = analysis.calculate_test_statistic(log_lambda_max, fitparam_values) + if TS > max_TS: + max_TS = TS + best_time = result + best_fitparams = fitparam_values + + return max_TS, best_time, best_fitparams + + +def run_gamma_scan_single_flare(analysis, remove_time=None, gamma_min=1, gamma_max=5, n_gamma=51): + """Run em for different gammas in the signal energy pdf + + Parameters + ---------- + remove_time : float + Time information of event that should be removed. + gamma_min : float + Lower bound for gamma scan. + gamma_max : float + Upper bound for gamma scan. + n_gamma : int + Number of steps for gamma scan. + + Returns + ------- + array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" + """ + dtype = [("gamma", "f8"), ("mu", "f8"), ("sigma", "f8"), ("ns_em", "f8")] + results = np.empty(n_gamma, dtype=dtype) + + time = analysis._tdm_list[0].get_data("time") + + for index, g in enumerate(np.linspace(gamma_min, gamma_max, n_gamma)): + ratio = get_energy_spatial_signal_over_background(analysis, {"gamma": g}) + mu, sigma, ns = em_fit(time, ratio, n=1, tol=1.e-200, iter_max=500, weight_thresh=0, + initial_width=5000, remove_x=remove_time) + results[index] = (g, mu[0], sigma[0], ns[0]) + + return results + + +def unblind_flare(analysis, remove_time=None): + """Run EM on unscrambled data. Similar to the original analysis, remove the alert event. + + Parameters + ---------- + remove_time : float + Time information of event that should be removed. + In the case of the TXS analysis: remove_time=58018.8711856 + + Returns + ------- + array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" + """ + + # get the original unblinded data + rss = RandomStateService(seed=1) + analysis.unblind(rss) + time_results = run_gamma_scan_single_flare(analysis, remove_time=remove_time) + return time_results + + def create_analysis( datasets, source, @@ -136,7 +300,7 @@ def create_analysis( gauss : None or dictionary with mu, sigma None if no Gaussian time pdf. Else dictionary with {"mu": float, "sigma": float} of Gauss box : None or dictionary with start, end - None if no Box shaped time pdf. Else dictionary with {"start": float, "end": float} of box. + None if no Box shaped time pdf. Else dictionary with {"start": float, "end": float} of box. refplflux_Phi0 : float The flux normalization to use for the reference power law flux model. refplflux_E0 : float @@ -191,10 +355,15 @@ def create_analysis( Returns ------- - analysis : TimeDependentSingleDatasetSingleSourceAnalysis + analysis : TimeIntegratedMultiDatasetSingleSourceAnalysis The Analysis instance for this analysis. """ + if len(datasets) != 1: + raise RuntimeError( + 'This analysis supports only analyses with only single datasets ' + 'at the moment!') + if gauss is None and box is None: raise ValueError("No time pdf specified (box or gauss)") if gauss is not None and box is not None: @@ -248,20 +417,12 @@ def create_analysis( # Define the test statistic. test_statistic = TestStatisticWilks() - # Define the data scrambler with its data scrambling method, which is used - # for background generation. - data_scrambler = DataScrambler(UniformRAScramblingMethod()) - - # Create background generation method. - bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) - # Create the Analysis instance. analysis = TimeIntegratedMultiDatasetSingleSourceAnalysis( src_hypo_group_manager, src_fitparam_mapper, fitparam_ns, test_statistic, - bkg_gen_method, sig_generator_cls=PDTimeDependentSignalGenerator ) @@ -282,6 +443,7 @@ def create_analysis( # Add the data sets to the analysis. pbar = ProgressBar(len(datasets), parent=ppbar).start() + data_list = [] energy_cut_splines = [] for idx, ds in enumerate(datasets): # Load the data of the data set. @@ -289,6 +451,7 @@ def create_analysis( keep_fields=keep_data_fields, compress=compress_data, tl=tl) + data_list.append(data) # Create a trial data manager and add the required data fields. tdm = TrialDataManager() @@ -355,6 +518,18 @@ def create_analysis( pbar.finish() analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) + + # Define the data scrambler with its data scrambling method, which is used + # for background generation. + + # FIXME: Support multiple datasets for the DataScrambler. + data_scrambler = DataScrambler(I3SeasonalVariationTimeScramblingMethod(data_list[0])) + # Create background generation method. + bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) + + analysis.bkg_gen_method = bkg_gen_method + analysis.construct_background_generator() + analysis.construct_signal_generator( llhratio=analysis.llhratio, energy_cut_splines=energy_cut_splines, cut_sindec=cut_sindec, box=box, gauss=gauss) diff --git a/skyllh/core/expectation_maximization.py b/skyllh/core/expectation_maximization.py index 85747dbd96..c82db849a3 100644 --- a/skyllh/core/expectation_maximization.py +++ b/skyllh/core/expectation_maximization.py @@ -27,7 +27,7 @@ def expectation_em(ns, mu, sigma, t, sob): t : 1d ndarray of float Times of the events. sob : 1d ndarray of float - The signal over background values of events. + The signal over background values of events, or weights of events Returns ------- @@ -83,266 +83,70 @@ def maximization_em(e_sig, t): return mu, sigma, ns -class ExpectationMaximizationUtils(object): - def __init__(self, ana): - """Creates a expectation maximization utility class for time-dependent - single dataset point-like source analysis assuming a single source. - - Parameters - ---------- - ana : instance of TimeIntegratedMultiDatasetSingleSourceAnalysis - Analysis instance which will be stored in this class. - """ - self.ana = ana - - @property - def ana(self): - """The TimeIntegratedMultiDatasetSingleSourceAnalysis instance. - """ - return self._ana - - @ana.setter - def ana(self, analysis): - if not isinstance(analysis, TimeIntegratedMultiDatasetSingleSourceAnalysis): - raise TypeError("The ana argument must be an instance of " - "'TimeIntegratedMultiDatasetSingleSourceAnalysis'.") - self._ana = analysis - - - def change_time_pdf(self, gauss=None, box=None): - """Changes the time pdf. - - Parameters - ---------- - gauss : dict | None - None or dictionary with {"mu": float, "sigma": float}. - box : dict | None - None or dictionary with {"start": float, "end": float}. - """ - ana = self.ana - - if gauss is None and box is None: - raise TypeError("Either gauss or box have to be specified as time pdf.") - - grl = ana._data_list[0].grl - # redo this in case the background pdf was not calculated before - time_bkgpdf = BackgroundUniformTimePDF(grl) - if gauss is not None: - time_sigpdf = SignalGaussTimePDF(grl, gauss['mu'], gauss['sigma']) - elif box is not None: - time_sigpdf = SignalBoxTimePDF(grl, box["start"], box["end"]) - - time_pdfratio = SigOverBkgPDFRatio( - sig_pdf=time_sigpdf, - bkg_pdf=time_bkgpdf, - pdf_type=TimePDF - ) - - # the next line seems to make no difference in the llh evaluation. We keep it for consistency - ana._llhratio.llhratio_list[0].pdfratio_list[2] = time_pdfratio - # this line here is relevant for the llh evaluation - ana._llhratio.llhratio_list[0]._pdfratioarray._pdfratio_list[2] = time_pdfratio - - # change detector signal yield with flare livetime in sample (1 / grl_norm in pdf), - # rebuild the histograms if it is changed... - # signal injection? - - def get_energy_spatial_signal_over_backround(self, fitparams): - """Returns the signal over background ratio for - (spatial_signal * energy_signal) / (spatial_background * energy_background). - - Parameter - --------- - fitparams : dict - Dictionary with {"gamma": float} for energy pdf. - - Returns - ------- - ratio : 1d ndarray - Product of spatial and energy signal over background pdfs. - """ - ana = self.ana - - ratio = ana._llhratio.llhratio_list[0].pdfratio_list[0].get_ratio(ana._tdm_list[0], fitparams) - ratio *= ana._llhratio.llhratio_list[0].pdfratio_list[1].get_ratio(ana._tdm_list[0], fitparams) - - return ratio - - def change_fluxmodel_gamma(self, gamma): - """Set new gamma for the flux model. - - Parameter - --------- - gamma : float - Spectral index for flux model. - """ - ana = self.ana - - ana.src_hypo_group_manager.src_hypo_group_list[0].fluxmodel.gamma = gamma - - def change_signal_time(self, gauss=None, box=None): - """Change the signal injection to gauss or box. - - Parameters - ---------- - gauss : dict | None - None or dictionary {"mu": float, "sigma": float}. - box : dict | None - None or dictionary {"start" : float, "end" : float}. - """ - ana = self.ana - - ana.sig_generator.set_flare(box=box, gauss=gauss) - - def em_fit(self, fitparams, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, initial_width=5000, - remove_time=None): - """Run expectation maximization. - - Parameters - ---------- - fitparams : dict - Dictionary with value for gamma, e.g. {'gamma': 2}. - n : int - How many gaussians flares we are looking for. - tol : float - the stopping criteria for expectation maximization. This is the difference in the normalized likelihood over the - last 20 iterations. - iter_max : int - The maximum number of iterations, even if stopping criteria tolerance (`tol`) is not yet reached. - sob_thres : float - Set a minimum threshold for signal over background ratios. Ratios below this threshold will be removed. - initial_width : float - Starting width for the gaussian flare in days. - remove_time : float | None - Time information of event that should be removed. - - Returns - ------- - mean flare time, flare width, normalization factor for time pdf - """ - ana = self.ana - - ratio = self.get_energy_spatial_signal_over_backround(fitparams) - time = ana._tdm_list[0].get_data("time") - - if sob_thresh > 0: # remove events below threshold - for i in range(len(ratio)): - mask = ratio > sob_thresh - ratio[i] = ratio[i][mask] - time[i] = time[i][mask] - - # in case, remove event - if remove_time is not None: - mask = time == remove_time - ratio = ratio[~mask] - time = time[~mask] - - # expectation maximization - mu = np.linspace(ana._data_list[0].grl["start"][0], ana._data_list[-1].grl["stop"][-1], n+2)[1:-1] - sigma = np.ones(n) * initial_width - ns = np.ones(n) * 10 - llh_diff = 100 - llh_old = 0 - llh_diff_list = [100] * 20 - - iteration = 0 - - while iteration < iter_max and llh_diff > tol: # run until convergence or maximum number of iterations - iteration += 1 - - e, logllh = expectation_em(ns, mu, sigma, time, ratio) - - llh_new = np.sum(logllh) - tmp_diff = np.abs(llh_old - llh_new) / llh_new - llh_diff_list = llh_diff_list[:-1] - llh_diff_list.insert(0, tmp_diff) - llh_diff = np.max(llh_diff_list) - llh_old = llh_new - mu, sigma, ns = maximization_em(e, time) - - return mu, sigma, ns - - def run_gamma_scan_single_flare(self, remove_time=None, gamma_min=1, gamma_max=5, n_gamma=51): - """Run em for different gammas in the signal energy pdf - - Parameters - ---------- - remove_time : float - Time information of event that should be removed. - gamma_min : float - Lower bound for gamma scan. - gamma_max : float - Upper bound for gamma scan. - n_gamma : int - Number of steps for gamma scan. - - Returns - ------- - array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" - """ - dtype = [("gamma", "f8"), ("mu", "f8"), ("sigma", "f8"), ("ns_em", "f8")] - results = np.empty(n_gamma, dtype=dtype) - - for index, g in enumerate(np.linspace(gamma_min, gamma_max, n_gamma)): - mu, sigma, ns = self.em_fit({"gamma": g}, n=1, tol=1.e-200, iter_max=500, sob_thresh=0, - initial_width=5000, remove_time=remove_time) - results[index] = (g, mu[0], sigma[0], ns[0]) - - return results - - def calculate_TS(self, em_results, rss): - """Calculate the best TS value for the expectation maximization gamma scan. - - Parameters - ---------- - em_results : 1d ndarray of tuples - Gamma scan result. - rss : instance of RandomStateService - The instance of RandomStateService that should be used to generate - random numbers from. - - Returns - ------- - float maximized TS value - tuple(gamma from em scan [float], best fit mean time [float], best fit width [float]) - (float ns, float gamma) fitparams from TS optimization - """ - ana = self.ana - - max_TS = 0 - best_time = None - best_flux = None - for index, result in enumerate(em_results): - self.change_signal_time(gauss={"mu": em_results["mu"], "sigma": em_results["sigma"]}) - (fitparamset, log_lambda_max, fitparam_values, status) = ana.maximize_llhratio(rss) - TS = ana.calculate_test_statistic(log_lambda_max, fitparam_values) - if TS > max_TS: - max_TS = TS - best_time = result - best_flux = fitparam_values - - return max_TS, best_time, fitparam_values - - def unblind_flare(self, remove_time=None): - """Run EM on unscrambled data. Similar to the original analysis, remove the alert event. - - Parameters - ---------- - remove_time : float - Time information of event that should be removed. - In the case of the TXS analysis: remove_time=58018.8711856 - - Returns - ------- - array with "gamma", "mu", "sigma", and scaling factor for flare "ns_em" - """ - ana = self.ana - - # get the original unblinded data - rss = RandomStateService(seed=1) - - ana.unblind(rss) +def em_fit(x, weights, n=1, tol=1.e-200, iter_max=500, weight_thresh=0, initial_width=5000, + remove_x=None): + """Run expectation maximization. + + Parameters + ---------- + x : array[float] + Quantity to run EM on (e.g. the time if EM should find time flares) + weights : + weights for each event (e.g. the signal over background ratio) + fitparams : dict + Dictionary with value for gamma, e.g. {'gamma': 2}. + n : int + How many Gaussians flares we are looking for. + tol : float + the stopping criteria for expectation maximization. This is the difference in the normalized likelihood over the + last 20 iterations. + iter_max : int + The maximum number of iterations, even if stopping criteria tolerance (`tol`) is not yet reached. + weight_thresh : float + Set a minimum threshold for event weights. Events with smaller weights will be removed. + initial_width : float + Starting width for the gaussian flare in days. + remove_x : float | None + Specific x of event that should be removed. + + Returns + ------- + Mean, width, normalization factor + """ - time_results = self.run_gamma_scan_single_flare(remove_time=remove_time) + if weight_thresh > 0: # remove events below threshold + for i in range(len(weights)): + mask = weights > weight_thresh + weights[i] = weights[i][mask] + x[i] = x[i][mask] + + # in case, remove event + if remove_x is not None: + mask = x == remove_x + weights = weights[~mask] + x = x[~mask] + + # expectation maximization + mu = np.linspace(x[0], x[-1], n+2)[1:-1] + sigma = np.ones(n) * initial_width + ns = np.ones(n) * 10 + llh_diff = 100 + llh_old = 0 + llh_diff_list = [100] * 20 + + iteration = 0 + + while iteration < iter_max and llh_diff > tol: # run until convergence or maximum number of iterations + iteration += 1 + + e, logllh = expectation_em(ns, mu, sigma, x, weights) + + llh_new = np.sum(logllh) + tmp_diff = np.abs(llh_old - llh_new) / llh_new + llh_diff_list = llh_diff_list[:-1] + llh_diff_list.insert(0, tmp_diff) + llh_diff = np.max(llh_diff_list) + llh_old = llh_new + mu, sigma, ns = maximization_em(e, x) - return time_results + return mu, sigma, ns diff --git a/skyllh/core/scrambling.py b/skyllh/core/scrambling.py index 1ce4c58095..ab5c3454e0 100644 --- a/skyllh/core/scrambling.py +++ b/skyllh/core/scrambling.py @@ -37,13 +37,13 @@ def scramble(self, rss, data): class UniformRAScramblingMethod(DataScramblingMethod): - """The UniformRAScramblingMethod method performs right-ascention scrambling + r"""The UniformRAScramblingMethod method performs right-ascention scrambling uniformly within a given RA range. By default it's (0, 2\pi). Note: This alters only the ``ra`` values of the data! """ def __init__(self, ra_range=None): - """Initializes a new RAScramblingMethod instance. + r"""Initializes a new RAScramblingMethod instance. Parameters ---------- diff --git a/skyllh/i3/scrambling.py b/skyllh/i3/scrambling.py index 07a6d794ec..bd0616ee6d 100644 --- a/skyllh/i3/scrambling.py +++ b/skyllh/i3/scrambling.py @@ -1,7 +1,15 @@ # -*- coding: utf-8 -*- -from skyllh.core.scrambling import TimeScramblingMethod -from skyllh.i3.coords import hor_to_equ_transform, azi_to_ra_transform +import numpy as np + +from skyllh.core.scrambling import ( + DataScramblingMethod, + TimeScramblingMethod, +) +from skyllh.i3.coords import ( + azi_to_ra_transform, + hor_to_equ_transform, +) class I3TimeScramblingMethod(TimeScramblingMethod): @@ -46,3 +54,70 @@ def scramble(self, rss, data): data['ra'] = azi_to_ra_transform(data['azi'], mjds) return data + + +class I3SeasonalVariationTimeScramblingMethod(DataScramblingMethod): + """The I3SeasonalVariationTimeScramblingMethod class provides a data + scrambling method to perform data coordinate scrambling based on a generated + time, which follows seasonal variations within the experimental data. + """ + def __init__(self, data, **kwargs): + """Initializes a new seasonal time scrambling instance. + + Parameters + ---------- + data : instance of I3DatasetData + The instance of I3DatasetData holding the experimental data and + good-run-list information. + """ + super().__init__(**kwargs) + + # The run weights are the number of events in each run relative to all + # the events to account for possible seasonal variations. + self.run_weights = np.zeros((len(data.grl),), dtype=np.float64) + n_events = len(data.exp['time']) + for (i, (start, stop)) in enumerate( + zip(data.grl['start'], data.grl['stop'])): + mask = (data.exp['time'] >= start) & (data.exp['time'] < stop) + self.run_weights[i] = len(data.exp[mask]) / n_events + self.run_weights /= np.sum(self.run_weights) + + self.grl = data.grl + + def scramble(self, rss, data): + """Scrambles the given data based on random MJD times, which are + generated uniformely within the data runs, where the data runs are + weighted based on their amount of events compared to the total events. + + Parameters + ---------- + rss : instance of RandomStateService + The random state service providing the random number + generator (RNG). + data : instance of DataFieldRecordArray + The DataFieldRecordArray instance containing the to be scrambled + data. + + Returns + ------- + data : instance of DataFieldRecordArray + The given DataFieldRecordArray holding the scrambled data. + """ + # Get run indices based on their seasonal weights. + run_idxs = rss.random.choice( + self.grl['start'].size, + size=len(data['time']), + p=self.weights) + + # Draw random times uniformely within the runs. + times = rss.random.uniform( + self.grl['start'][run_idxs], + self.grl['stop'][run_idxs]) + + # Get the correct right ascension. + data['time'] = times + data['ra'] = azi_to_ra_transform( + azi=data['azi'], + mjd=times) + + return data

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%0AK$5H3WgZelr-r#|woa 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 W4Q!zB5jeQm7}?<{D8v-R;r Date: Tue, 12 Apr 2022 18:31:53 +0200 Subject: [PATCH 027/274] Added utility class for smearing matrix --- skyllh/analyses/i3/trad_ps/utils.py | 427 ++++++++++++++++++++++++++++ 1 file changed, 427 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index f2134e61c0..d0ec7897a2 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -220,3 +220,430 @@ def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): ra = np.pi - azi return (dec, ra) + + +class PublicDataSmearingMatrix(object): + """This class is a helper class for dealing with the smearing matrix + provided by the public data. + """ + def __init__( + self, pathfilenames, **kwargs): + """Creates a smearing matrix instance by loading the smearing matrix + from the given file. + """ + super().__init__(**kwargs) + + ( + self.histogram, + self.true_e_bin_edges, + self.true_dec_bin_edges, + self.reco_e_lower_edges, + self.reco_e_upper_edges, + self.psf_lower_edges, + self.psf_upper_edges, + self.ang_err_lower_edges, + self.ang_err_upper_edges + ) = load_smearing_histogram(pathfilenames) + + def get_dec_idx(self, dec): + """Returns the declination index for the given declination value. + + Parameters + ---------- + dec : float + The declination value in radians. + + Returns + ------- + dec_idx : int + The index of the declination bin for the given declination value. + """ + dec = np.degrees(dec) + + if (dec < self.true_dec_bin_edges[0]) or\ + (dec > self.true_dec_bin_edges[-1]): + raise ValueError('The declination {} degrees is not supported by ' + 'the smearing matrix!'.format(dec)) + + dec_idx = np.digitize(dec, self.true_dec_bin_edges) - 1 + + return dec_idx + + def get_true_log_e_range_with_valid_log_e_pfds(self, dec_idx): + """Determines the true log energy range for which log_e PDFs are + available for the given declination bin. + + Parameters + ---------- + dec_idx : int + The declination bin index. + + Returns + ------- + min_log_true_e : float + The minimum true log energy value. + max_log_true_e : float + The maximum true log energy value. + """ + m = np.sum( + (self.reco_e_upper_edges[:,dec_idx] - + self.reco_e_lower_edges[:,dec_idx] > 0), + axis=1) != 0 + min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) + max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) + + return (min_log_true_e, max_log_true_e) + + def get_log_e_pdf( + self, log_true_e_idx, dec_idx): + """Retrieves the log_e PDF from the given true energy bin index and + source bin index. + Returns (None, None, None, None) if any of the bin indices are less then + zero, or if the sum of all pdf bins is zero. + + Parameters + ---------- + log_true_e_idx : int + The index of the true energy bin. + dec_idx : int + The index of the declination bin. + + Returns + ------- + pdf : 1d ndarray + The log_e pdf values. + lower_bin_edges : 1d ndarray + The lower bin edges of the energy pdf histogram. + upper_bin_edges : 1d ndarray + The upper bin edges of the energy pdf histogram. + bin_widths : 1d ndarray + The bin widths of the energy pdf histogram. + """ + if log_true_e_idx < 0 or dec_idx < 0: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, dec_idx] + pdf = np.sum(pdf, axis=(-2, -1)) + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the reco energy bin edges and widths. + lower_bin_edges = self.reco_e_lower_edges[ + log_true_e_idx, dec_idx + ] + upper_bin_edges = self.reco_e_upper_edges[ + log_true_e_idx, dec_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf /= (np.sum(pdf * bin_widths)) + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def get_psi_pdf( + self, log_true_e_idx, dec_idx, log_e_idx): + """Retrieves the psi PDF from the given true energy bin index, the + source bin index, and the log_e bin index. + Returns (None, None, None, None) if any of the bin indices are less then + zero, or if the sum of all pdf bins is zero. + + Parameters + ---------- + log_true_e_idx : int + The index of the true energy bin. + dec_idx : int + The index of the declination bin. + log_e_idx : int + The index of the log_e bin. + + Returns + ------- + pdf : 1d ndarray + The psi pdf values. + lower_bin_edges : 1d ndarray + The lower bin edges of the psi pdf histogram. + upper_bin_edges : 1d ndarray + The upper bin edges of the psi pdf histogram. + bin_widths : 1d ndarray + The bin widths of the psi pdf histogram. + """ + if log_true_e_idx < 0 or dec_idx < 0 or log_e_idx < 0: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, dec_idx, log_e_idx] + pdf = np.sum(pdf, axis=-1) + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the PSI bin edges and widths. + lower_bin_edges = self.psf_lower_edges[ + log_true_e_idx, dec_idx, log_e_idx + ] + upper_bin_edges = self.psf_upper_edges[ + log_true_e_idx, dec_idx, log_e_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf /= (np.sum(pdf * bin_widths)) + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def get_ang_err_pdf( + self, log_true_e_idx, dec_idx, log_e_idx, psi_idx): + """Retrieves the angular error PDF from the given true energy bin index, + the source bin index, the log_e bin index, and the psi bin index. + Returns (None, None, None, None) if any of the bin indices are less then + zero, or if the sum of all pdf bins is zero. + + Parameters + ---------- + log_true_e_idx : int + The index of the true energy bin. + dec_idx : int + The index of the declination bin. + log_e_idx : int + The index of the log_e bin. + psi_idx : int + The index of the psi bin. + + Returns + ------- + pdf : 1d ndarray + The ang_err pdf values. + lower_bin_edges : 1d ndarray + The lower bin edges of the ang_err pdf histogram. + upper_bin_edges : 1d ndarray + The upper bin edges of the ang_err pdf histogram. + bin_widths : 1d ndarray + The bin widths of the ang_err pdf histogram. + """ + if log_true_e_idx < 0 or dec_idx < 0 or log_e_idx < 0 or psi_idx < 0: + return (None, None, None, None) + + pdf = self.histogram[log_true_e_idx, dec_idx, log_e_idx, psi_idx] + + if np.sum(pdf) == 0: + return (None, None, None, None) + + # Get the ang_err bin edges and widths. + lower_bin_edges = self.ang_err_lower_edges[ + log_true_e_idx, dec_idx, log_e_idx, psi_idx + ] + upper_bin_edges = self.ang_err_upper_edges[ + log_true_e_idx, dec_idx, log_e_idx, psi_idx + ] + bin_widths = upper_bin_edges - lower_bin_edges + + # Normalize the PDF. + pdf = pdf / np.sum(pdf * bin_widths) + + return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) + + def sample_log_e( + self, rss, dec_idx, log_true_e_idxs): + """Samples log energy values for the given source declination and true + energy bins. + + Parameters + ---------- + rss : instance of RandomStateService + The RandomStateService which should be used for drawing random + numbers from. + dec_idx : int + The index of the source declination bin. + log_true_e_idxs : 1d ndarray of int + The bin indices of the true energy bins. + + Returns + ------- + log_e_idx : 1d ndarray of int + The bin indices of the log_e pdf corresponding to the sampled + log_e values. + log_e : 1d ndarray of float + The sampled log_e values. + """ + n_evt = len(log_true_e_idxs) + log_e_idx = np.empty((n_evt,), dtype=np.int_) + log_e = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + b_size = np.count_nonzero(m) + ( + pdf, + low_bin_edges, + up_bin_edges, + bin_widths + ) = self.get_log_e_pdf( + b_log_true_e_idx, + dec_idx) + + if pdf is None: + log_e_idx[m] = -1 + log_e[m] = np.nan + continue + + b_log_e_idx = rss.random.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=b_size) + b_log_e = rss.random.uniform( + low_bin_edges[b_log_e_idx], + up_bin_edges[b_log_e_idx], + size=b_size) + + log_e_idx[m] = b_log_e_idx + log_e[m] = b_log_e + + return (log_e_idx, log_e) + + def sample_psi( + self, rss, dec_idx, log_true_e_idxs, log_e_idxs): + """Samples psi values for the given source declination, true + energy bins, and log_e bins. + + Parameters + ---------- + rss : instance of RandomStateService + The RandomStateService which should be used for drawing random + numbers from. + dec_idx : int + The index of the source declination bin. + log_true_e_idxs : 1d ndarray of int + The bin indices of the true energy bins. + log_e_idxs : 1d ndarray of int + The bin indices of the log_e bins. + + Returns + ------- + psi_idx : 1d ndarray of int + The bin indices of the psi pdf corresponding to the sampled psi + values. + psi : 1d ndarray of float + The sampled psi values in radians. + """ + if(len(log_true_e_idxs) != len(log_e_idxs)): + raise ValueError( + 'The lengths of log_true_e_idxs and log_e_idxs must be equal!') + + n_evt = len(log_true_e_idxs) + psi_idx = np.empty((n_evt,), dtype=np.int_) + psi = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) + for bb_log_e_idx in bb_unique_log_e_idxs: + mm = m & (log_e_idxs == bb_log_e_idx) + bb_size = np.count_nonzero(mm) + ( + pdf, + low_bin_edges, + up_bin_edges, + bin_widths + ) = self.get_psi_pdf( + b_log_true_e_idx, + dec_idx, + bb_log_e_idx) + + if pdf is None: + psi_idx[mm] = -1 + psi[mm] = np.nan + continue + + bb_psi_idx = rss.random.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=bb_size) + bb_psi = rss.random.uniform( + low_bin_edges[bb_psi_idx], + up_bin_edges[bb_psi_idx], + size=bb_size) + + psi_idx[mm] = bb_psi_idx + psi[mm] = bb_psi + + return (psi_idx, np.radians(psi)) + + def sample_ang_err( + self, rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs): + """Samples ang_err values for the given source declination, true + energy bins, log_e bins, and psi bins. + + Parameters + ---------- + rss : instance of RandomStateService + The RandomStateService which should be used for drawing random + numbers from. + dec_idx : int + The index of the source declination bin. + log_true_e_idxs : 1d ndarray of int + The bin indices of the true energy bins. + log_e_idxs : 1d ndarray of int + The bin indices of the log_e bins. + psi_idxs : 1d ndarray of int + The bin indices of the psi bins. + + Returns + ------- + ang_err_idx : 1d ndarray of int + The bin indices of the angular error pdf corresponding to the + sampled angular error values. + ang_err : 1d ndarray of float + The sampled angular error values in radians. + """ + if (len(log_true_e_idxs) != len(log_e_idxs)) and\ + (len(log_e_idxs) != len(psi_idxs)): + raise ValueError( + 'The lengths of log_true_e_idxs, log_e_idxs, and psi_idxs must ' + 'be equal!') + + n_evt = len(log_true_e_idxs) + ang_err_idx = np.empty((n_evt,), dtype=np.int_) + ang_err = np.empty((n_evt,), dtype=np.double) + + unique_log_true_e_idxs = np.unique(log_true_e_idxs) + for b_log_true_e_idx in unique_log_true_e_idxs: + m = log_true_e_idxs == b_log_true_e_idx + bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) + for bb_log_e_idx in bb_unique_log_e_idxs: + mm = m & (log_e_idxs == bb_log_e_idx) + bbb_unique_psi_idxs = np.unique(psi_idxs[mm]) + for bbb_psi_idx in bbb_unique_psi_idxs: + mmm = mm & (psi_idxs == bbb_psi_idx) + bbb_size = np.count_nonzero(mmm) + ( + pdf, + low_bin_edges, + up_bin_edges, + bin_widths + ) = self.get_ang_err_pdf( + b_log_true_e_idx, + dec_idx, + bb_log_e_idx, + bbb_psi_idx) + + if pdf is None: + ang_err_idx[mmm] = -1 + ang_err[mmm] = np.nan + continue + + bbb_ang_err_idx = rss.random.choice( + np.arange(len(pdf)), + p=(pdf * bin_widths), + size=bbb_size) + bbb_ang_err = rss.random.uniform( + low_bin_edges[bbb_ang_err_idx], + up_bin_edges[bbb_ang_err_idx], + size=bbb_size) + + ang_err_idx[mmm] = bbb_ang_err_idx + ang_err[mmm] = bbb_ang_err + + return (ang_err_idx, np.radians(ang_err)) From e532d481c23ac360dbe3373d709116e3b64c1505 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 12 Apr 2022 18:34:11 +0200 Subject: [PATCH 028/274] Added signal generator class for the signal pdf generation --- skyllh/analyses/i3/trad_ps/signalpdf.py | 183 +++++++++++++++++++++++- 1 file changed, 176 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 4e46c38787..e2b620c646 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -9,6 +9,7 @@ BinningDefinition, UsesBinning ) +from skyllh.core.storage import DataFieldRecordArray from skyllh.core.pdf import ( PDF, PDFSet, @@ -25,7 +26,160 @@ ) from skyllh.i3.dataset import I3Dataset from skyllh.physics.flux import FluxModel -from skyllh.analyses.i3.trad_ps.utils import load_smearing_histogram +from skyllh.analyses.i3.trad_ps.utils import ( + load_smearing_histogram, + psi_to_dec_and_ra, + PublicDataSmearingMatrix +) + +class PublicDataSignalGenerator(object): + def __init__(self, ds): + """Creates a new instance of the signal generator for generating signal + events from the provided public data smearing matrix. + """ + super().__init__() + + self.smearing_matrix = PublicDataSmearingMatrix( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) + + + def _generate_events( + self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): + """Generates `n_events` signal events for the given source location + and flux model. + + Note: + Some values can be NaN in cases where a PDF was not available! + + + Parameters + ---------- + rss : instance of RandomStateService + The instance of RandomStateService to use for drawing random + numbers. + src_dec : float + The declination of the source in radians. + src_ra : float + The right-ascention of the source in radians. + + Returns + ------- + events : numpy record array of size `n_events` + The numpy record array holding the event data. + It contains the following data fields: + - 'log_true_energy' + - 'log_energy' + - 'psi' + - 'ang_err' + - 'dec' + - 'ra' + Single values can be NaN in cases where a pdf was not available. + """ + # Create the output event array. + out_dtype = [ + ('log_true_e', float), + ('log_e', float), + ('psi', float), + ('ra', float), + ('dec', float), + ('ang_err', float) + ] + events = np.empty((n_events,), dtype=out_dtype) + + sm = self.smearing_matrix + + # Determine the true energy range for which log_e PDFs are available. + (min_log_true_e, + max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pfds( + dec_idx) + + # First draw a true neutrino energy from the hypothesis spectrum. + log_true_e = np.log10(flux_model.get_inv_normed_cdf( + rss.random.uniform(size=n_events), + E_min=10**min_log_true_e, + E_max=10**max_log_true_e + )) + + events['log_true_e'] = log_true_e + + log_true_e_idxs = ( + np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 + ) + # Sample reconstructed energies given true neutrino energies. + (log_e_idxs, log_e) = sm.sample_log_e( + rss, dec_idx, log_true_e_idxs) + events['log_e'] = log_e + + # Sample reconstructed psi values given true neutrino energy and + # reconstructed energy. + (psi_idxs, psi) = sm.sample_psi( + rss, dec_idx, log_true_e_idxs, log_e_idxs) + events['psi'] = psi + + # Sample reconstructed ang_err values given true neutrino energy, + # reconstructed energy, and psi. + (ang_err_idxs, ang_err) = sm.sample_ang_err( + rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs) + events['ang_err'] = ang_err + + # Convert the psf into a set of (r.a. and dec.) + (dec, ra) = psi_to_dec_and_ra(rss, src_dec, src_ra, psi) + events['dec'] = dec + events['ra'] = ra + + return events + + def generate_signal_events( + self, rss, src_dec, src_ra, flux_model, n_events): + """Generates ``n_events`` signal events for the given source location + and flux model. + + Returns + ------- + events : DataFieldRecordArray instance + The instance of DataFieldRecordArray holding the event data. + It contains the following data fields: + - 'log_true_energy' + - 'log_energy' + - 'psi' + - 'ang_err' + - 'dec' + - 'ra' + """ + sm = self.smearing_matrix + + # Find the declination bin index. + dec_idx = sm.get_dec_idx(src_dec) + + events = None + n_evt_generated = 0 + while n_evt_generated != n_events: + n_evt = n_events - n_evt_generated + + events_ = self._generate_events( + rss, src_dec, src_ra, dec_idx, flux_model, n_evt) + + # Cut events that failed to be generated due to missing PDFs. + m = np.invert( + np.isnan(events_['log_e']) | + np.isnan(events_['psi']) | + np.isnan(events_['ang_err']) + ) + events_ = events_[m] + + n_evt_generated += len(events_) + if events is None: + events = events_ + else: + events = np.concatenate((events, events_)) + + # Convert events array into DataFieldRecordArray instance. + events = DataFieldRecordArray(events, copy=False) + + return events + + class PublicDataSignalI3EnergyPDF(EnergyPDF, IsSignalPDF, UsesBinning): @@ -124,7 +278,7 @@ def _create_spline(self, bin_centers, values, order=1, smooth=0): return spline def get_weighted_energy_pdf_hist_for_true_energy_dec_bin( - self, true_e_idx, true_dec_idx, flux_model, log_e_min=2): + self, true_e_idx, true_dec_idx, flux_model, log_e_min=0): """Gets the reconstructed muon energy pdf histogram for a specific true neutrino energy and declination bin weighted with the assumed flux model. @@ -310,7 +464,12 @@ class PublicDataSignalI3EnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): energy signal parameters. """ def __init__( - self, ds, flux_model, fitparam_grid_set, ncpu=None, ppbar=None): + self, + ds, + flux_model, + fitparam_grid_set, + ncpu=None, + ppbar=None): """ """ if(isinstance(fitparam_grid_set, ParameterGrid)): @@ -326,10 +485,14 @@ def __init__( fitparam_grid_set.add_extra_lower_and_upper_bin() super().__init__( - pdf_type=PublicDataSignalI3EnergyPDF, + pdf_type=I3EnergyPDF, fitparams_grid_set=fitparam_grid_set, ncpu=ncpu) + # Create a signal generator for this dataset. + siggen = PublicDataSignalGenerator(ds) + + # Load the smearing data from file. (histogram, true_e_bin_edges, @@ -347,13 +510,17 @@ def __init__( self.true_dec_binning = BinningDefinition( 'true_dec', true_dec_bin_edges) - def create_PublicDataSignalI3EnergyPDF( + def create_I3EnergyPDF( ds, data_dict, flux_model, gridfitparams): # Create a copy of the FluxModel with the given flux parameters. # The copy is needed to not interfer with other CPU processes. my_flux_model = flux_model.copy(newprop=gridfitparams) - epdf = PublicDataSignalI3EnergyPDF( + # Generate signal events + # FIXME + siggen.generate_signal_events() + + epdf = I3EnergyPDF( ds, my_flux_model, data_dict=data_dict) return epdf @@ -371,7 +538,7 @@ def create_PublicDataSignalI3EnergyPDF( ] epdf_list = parallelize( - create_PublicDataSignalI3EnergyPDF, + create_energy_pdf, args_list, self.ncpu, ppbar=ppbar) @@ -424,3 +591,5 @@ def get_prob(self, tdm, gridfitparams): prob = epdf.get_prob(tdm) return prob + + From b35bebab5c8d1be7f4065cecaa0af328e2c4ee4a Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 13 Apr 2022 10:23:59 +0200 Subject: [PATCH 029/274] Added kwargs to the SignalGenerator --- skyllh/core/analysis.py | 6 ++++-- skyllh/core/signal_generator.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/skyllh/core/analysis.py b/skyllh/core/analysis.py index 30e6c013d9..93f2f647d6 100644 --- a/skyllh/core/analysis.py +++ b/skyllh/core/analysis.py @@ -371,10 +371,12 @@ def construct_signal_generator(self): """ if self._custom_sig_generator is None: self._sig_generator = SignalGenerator( - self._src_hypo_group_manager, self._dataset_list, self._data_list) + self._src_hypo_group_manager, self._dataset_list, self._data_list, + llhratio=self.llhratio) else: self._sig_generator = self._custom_sig_generator( - self._src_hypo_group_manager, self._dataset_list, self._data_list) + self._src_hypo_group_manager, self._dataset_list, self._data_list, + llhratio=self.llhratio) @abc.abstractmethod def initialize_trial(self, events_list, n_events_list=None): diff --git a/skyllh/core/signal_generator.py b/skyllh/core/signal_generator.py index 821bf1fd64..c937f72e85 100644 --- a/skyllh/core/signal_generator.py +++ b/skyllh/core/signal_generator.py @@ -25,7 +25,8 @@ class depends on the construction of the signal generation method. In case of multiple sources the handling here is very suboptimal. Therefore the MultiSourceSignalGenerator should be used instead! """ - def __init__(self, src_hypo_group_manager, dataset_list, data_list): + def __init__(self, src_hypo_group_manager, dataset_list, data_list, + **kwargs): """Constructs a new signal generator instance. Parameters From 76c1dde4da86aab8b27b071144c4a9c6ea2df628 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 13 Apr 2022 10:30:44 +0200 Subject: [PATCH 030/274] Added kwargs to the SignalGenerator + documentation. --- skyllh/core/signal_generator.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skyllh/core/signal_generator.py b/skyllh/core/signal_generator.py index c937f72e85..1d797f876e 100644 --- a/skyllh/core/signal_generator.py +++ b/skyllh/core/signal_generator.py @@ -40,6 +40,8 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list, data_list : list of DatasetData instances The list of DatasetData instances holding the actual data of each dataset. The order must match the order of ``dataset_list``. + kwargs + A typical keyword argument is the instance of MultiDatasetTCLLHRatio. """ super(SignalGenerator, self).__init__() @@ -302,7 +304,8 @@ def generate_signal_events(self, rss, mean, poisson=True): class MultiSourceSignalGenerator(SignalGenerator): """More optimal signal generator for multiple sources. """ - def __init__(self, src_hypo_group_manager, dataset_list, data_list): + def __init__(self, src_hypo_group_manager, dataset_list, data_list, + **kwargs): """Constructs a new signal generator instance. Parameters @@ -316,6 +319,8 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list): data_list : list of DatasetData instances The list of DatasetData instances holding the actual data of each dataset. The order must match the order of ``dataset_list``. + kwargs + A typical keyword argument is the instance of MultiDatasetTCLLHRatio. """ super(MultiSourceSignalGenerator, self).__init__( src_hypo_group_manager, dataset_list, data_list) From 0965ba8c5a751deb6c6ac717c1f4db41f94919eb Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 13 Apr 2022 12:38:24 +0200 Subject: [PATCH 031/274] Generate signal enegry PDF from generated events --- skyllh/analyses/i3/trad_ps/analysis.py | 33 ++++-- skyllh/analyses/i3/trad_ps/signalpdf.py | 140 ++++++++++++------------ 2 files changed, 95 insertions(+), 78 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index 915f3924d0..39bad84961 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -51,6 +51,9 @@ DataBackgroundI3SpatialPDF, DataBackgroundI3EnergyPDF ) +from skyllh.i3.pdfratio import ( + I3EnergySigSetOverBkgPDFRatioSpline +) # Classes to define the spatial and energy PDF ratios. from skyllh.core.pdfratio import ( SpatialSigOverBkgPDFRatio, @@ -81,9 +84,7 @@ from skyllh.analyses.i3.trad_ps.signalpdf import ( PublicDataSignalI3EnergyPDFSet ) -from skyllh.analyses.i3.trad_ps.pdfratio import ( - PublicDataI3EnergySigSetOverBkgPDFRatioSpline -) + def TXS_location(): src_ra = np.radians(77.358) @@ -91,6 +92,7 @@ def TXS_location(): return (src_ra, src_dec) def create_analysis( + rss, datasets, source, refplflux_Phi0=1, @@ -246,11 +248,17 @@ def create_analysis( smoothing_filter = BlockSmoothingFilter(nbins=1) energy_sigpdfset = PublicDataSignalI3EnergyPDFSet( - ds, fluxmodel, gamma_grid, ppbar=pbar) + rss=rss, + ds=ds, + flux_model=fluxmodel, + fitparam_grid_set=gamma_grid, + n_events=int(1e6), + smoothing_filter=smoothing_filter, + ppbar=pbar) energy_bkgpdf = DataBackgroundI3EnergyPDF( data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) fillmethod = Skylab2SkylabPDFRatioFillMethod() - energy_pdfratio = PublicDataI3EnergySigSetOverBkgPDFRatioSpline( + energy_pdfratio = I3EnergySigSetOverBkgPDFRatioSpline( energy_sigpdfset, energy_bkgpdf, fillmethod=fillmethod, @@ -276,6 +284,12 @@ def create_analysis( '10-year public point source sample.', formatter_class = argparse.RawTextHelpFormatter ) + p.add_argument('--dec', default=23.8, type=float, + help='The source declination in degrees.') + p.add_argument('--ra', default=216.76, type=float, + help='The source right-ascention in degrees.') + p.add_argument('--gamma-seed', default=3, type=float, + help='The seed value of the gamma fit parameter.') p.add_argument('--data_base_path', default=None, type=str, help='The base path to the data samples (default=None)' ) @@ -315,13 +329,18 @@ def create_analysis( rss = RandomStateService(rss_seed) # Define the point source. - source = PointLikeSource(*TXS_location()) + source = PointLikeSource(np.deg2rad(args.ra), np.deg2rad(args.dec)) + print('source: ', str(source)) tl = TimeLord() with tl.task_timer('Creating analysis.'): ana = create_analysis( - datasets, source, tl=tl) + rss, + datasets, + source, + gamma_seed=args.gamma_seed, + tl=tl) with tl.task_timer('Unblinding data.'): (TS, fitparam_dict, status) = ana.unblind(rss) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index e2b620c646..b4f38bad95 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -24,6 +24,8 @@ ParameterGrid, ParameterGridSet ) +from skyllh.core.smoothing import SmoothingFilter +from skyllh.i3.pdf import I3EnergyPDF from skyllh.i3.dataset import I3Dataset from skyllh.physics.flux import FluxModel from skyllh.analyses.i3.trad_ps.utils import ( @@ -52,7 +54,6 @@ def _generate_events( Note: Some values can be NaN in cases where a PDF was not available! - Parameters ---------- rss : instance of RandomStateService @@ -68,22 +69,16 @@ def _generate_events( events : numpy record array of size `n_events` The numpy record array holding the event data. It contains the following data fields: - - 'log_true_energy' + - 'isvalid' - 'log_energy' - - 'psi' - - 'ang_err' - - 'dec' - - 'ra' + - 'sin_dec' Single values can be NaN in cases where a pdf was not available. """ # Create the output event array. out_dtype = [ - ('log_true_e', float), - ('log_e', float), - ('psi', float), - ('ra', float), - ('dec', float), - ('ang_err', float) + ('isvalid', np.bool_), + ('log_energy', np.double), + ('sin_dec', np.double) ] events = np.empty((n_events,), dtype=out_dtype) @@ -101,7 +96,7 @@ def _generate_events( E_max=10**max_log_true_e )) - events['log_true_e'] = log_true_e + #events['log_true_e'] = log_true_e log_true_e_idxs = ( np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 @@ -109,24 +104,26 @@ def _generate_events( # Sample reconstructed energies given true neutrino energies. (log_e_idxs, log_e) = sm.sample_log_e( rss, dec_idx, log_true_e_idxs) - events['log_e'] = log_e + events['log_energy'] = log_e # Sample reconstructed psi values given true neutrino energy and # reconstructed energy. (psi_idxs, psi) = sm.sample_psi( rss, dec_idx, log_true_e_idxs, log_e_idxs) - events['psi'] = psi # Sample reconstructed ang_err values given true neutrino energy, # reconstructed energy, and psi. (ang_err_idxs, ang_err) = sm.sample_ang_err( rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs) - events['ang_err'] = ang_err - # Convert the psf into a set of (r.a. and dec.) - (dec, ra) = psi_to_dec_and_ra(rss, src_dec, src_ra, psi) - events['dec'] = dec - events['ra'] = ra + isvalid = np.invert( + np.isnan(log_e) | np.isnan(psi) | np.isnan(ang_err)) + events['isvalid'] = isvalid + + # Convert the psf into a set of (r.a. and dec.). Only use non-nan + # values. + (dec, ra) = psi_to_dec_and_ra(rss, src_dec, src_ra, psi[isvalid]) + events['sin_dec'][isvalid] = np.sin(dec) return events @@ -137,15 +134,12 @@ def generate_signal_events( Returns ------- - events : DataFieldRecordArray instance - The instance of DataFieldRecordArray holding the event data. + events : numpy record array + The numpy record array holding the event data. It contains the following data fields: - - 'log_true_energy' + - 'isvalid' - 'log_energy' - - 'psi' - - 'ang_err' - - 'dec' - - 'ra' + - 'sin_dec' """ sm = self.smearing_matrix @@ -161,12 +155,7 @@ def generate_signal_events( rss, src_dec, src_ra, dec_idx, flux_model, n_evt) # Cut events that failed to be generated due to missing PDFs. - m = np.invert( - np.isnan(events_['log_e']) | - np.isnan(events_['psi']) | - np.isnan(events_['ang_err']) - ) - events_ = events_[m] + events_ = events_[events_['isvalid']] n_evt_generated += len(events_) if events is None: @@ -174,9 +163,6 @@ def generate_signal_events( else: events = np.concatenate((events, events_)) - # Convert events array into DataFieldRecordArray instance. - events = DataFieldRecordArray(events, copy=False) - return events @@ -465,9 +451,12 @@ class PublicDataSignalI3EnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): """ def __init__( self, + rss, ds, flux_model, fitparam_grid_set, + n_events=int(1e6), + smoothing_filter=None, ncpu=None, ppbar=None): """ @@ -478,6 +467,11 @@ def __init__( raise TypeError('The fitparam_grid_set argument must be an ' 'instance of ParameterGrid or ParameterGridSet!') + if((smoothing_filter is not None) and + (not isinstance(smoothing_filter, SmoothingFilter))): + raise TypeError('The smoothing_filter argument must be None or ' + 'an instance of SmoothingFilter!') + # We need to extend the fit parameter grids on the lower and upper end # by one bin to allow for the calculation of the interpolation. But we # will do this on a copy of the object. @@ -489,56 +483,60 @@ def __init__( fitparams_grid_set=fitparam_grid_set, ncpu=ncpu) - # Create a signal generator for this dataset. - siggen = PublicDataSignalGenerator(ds) - - - # Load the smearing data from file. - (histogram, - true_e_bin_edges, - true_dec_bin_edges, - reco_e_lower_edges, - reco_e_upper_edges, - psf_lower_edges, - psf_upper_edges, - ang_err_lower_edges, - ang_err_upper_edges - ) = load_smearing_histogram( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('smearing_datafile'))) - - self.true_dec_binning = BinningDefinition( - 'true_dec', true_dec_bin_edges) - def create_I3EnergyPDF( - ds, data_dict, flux_model, gridfitparams): + rss, ds, logE_binning, sinDec_binning, smoothing_filter, + siggen, flux_model, n_events, gridfitparams): # Create a copy of the FluxModel with the given flux parameters. # The copy is needed to not interfer with other CPU processes. my_flux_model = flux_model.copy(newprop=gridfitparams) - # Generate signal events - # FIXME - siggen.generate_signal_events() + # Generate signal events for sources in every sin(dec) bin. + events = None + n_evts = int(np.round(n_events / sinDec_binning.nbins)) + for sin_dec in sinDec_binning.bincenters: + src_dec = np.arcsin(sin_dec) + events_ = siggen.generate_signal_events( + rss=rss, + src_dec=src_dec, + src_ra=np.radians(180), + flux_model=my_flux_model, + n_events=n_evts) + if events is None: + events = events_ + else: + events = np.concatenate((events, events_)) + + data_logE = events['log_energy'] + data_sinDec = events['sin_dec'] + data_mcweight = np.ones((len(events),), dtype=np.double) + data_physicsweight = np.ones((len(events),), dtype=np.double) epdf = I3EnergyPDF( - ds, my_flux_model, data_dict=data_dict) + data_logE=data_logE, + data_sinDec=data_sinDec, + data_mcweight=data_mcweight, + data_physicsweight=data_physicsweight, + logE_binning=logE_binning, + sinDec_binning=sinDec_binning, + smoothing_filter=smoothing_filter + ) return epdf - data_dict = { - 'histogram': histogram, - 'true_e_bin_edges': true_e_bin_edges, - 'true_dec_bin_edges': true_dec_bin_edges, - 'reco_e_lower_edges': reco_e_lower_edges, - 'reco_e_upper_edges': reco_e_upper_edges - } + # Create a signal generator for this dataset. + siggen = PublicDataSignalGenerator(ds) + + logE_binning = ds.get_binning_definition('log_energy') + sinDec_binning = ds.get_binning_definition('sin_dec') + args_list = [ - ((ds, data_dict, flux_model, gridfitparams), {}) + ((rss, ds, logE_binning, sinDec_binning, smoothing_filter, siggen, + flux_model, n_events, gridfitparams), {}) for gridfitparams in self.gridfitparams_list ] epdf_list = parallelize( - create_energy_pdf, + create_I3EnergyPDF, args_list, self.ncpu, ppbar=ppbar) From 5d4dd26d3884319260ae45107541b3550a60a342 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 20 Apr 2022 15:08:57 +0200 Subject: [PATCH 032/274] Added utility function to load the effective area array --- skyllh/analyses/i3/trad_ps/utils.py | 88 ++++++++++++++++++++++++++++- 1 file changed, 87 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index d0ec7897a2..113d585d81 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -5,12 +5,98 @@ from skyllh.core.storage import create_FileLoader +def load_effective_area_array(pathfilenames): + """Loads the (nbins_sin_true_dec, nbins_log_true_e)-shaped 2D effective + area array from the given data file. + + Parameters + ---------- + pathfilename : str | list of str + The file name of the data file. + + Returns + ------- + arr : (nbins_sin_true_dec, nbins_log_true_e)-shaped 2D ndarray + The ndarray holding the effective area for each + sin(dec_true),log(e_true) bin. + sin_true_dec_binedges_lower : (nbins_sin_true_dec,)-shaped ndarray + The ndarray holding the lower bin edges of the sin(dec_true) axis. + sin_true_dec_binedges_upper : (nbins_sin_true_dec,)-shaped ndarray + The ndarray holding the upper bin edges of the sin(dec_true) axis. + log_true_e_binedges_lower : (nbins_log_true_e,)-shaped ndarray + The ndarray holding the lower bin edges of the log(E_true) axis. + log_true_e_binedges_upper : (nbins_log_true_e,)-shaped ndarray + The ndarray holding the upper bin edges of the log(E_true) axis. + """ + loader = create_FileLoader(pathfilenames=pathfilenames) + data = loader.load_data() + renaming_dict = { + 'log10(E_nu/GeV)_min': 'log_true_e_min', + 'log10(E_nu/GeV)_max': 'log_true_e_max', + 'Dec_nu_min[deg]': 'sin_true_dec_min', + 'Dec_nu_max[deg]': 'sin_true_dec_max', + 'A_Eff[cm^2]': 'a_eff' + } + data.rename_fields(renaming_dict, must_exist=True) + + # Convert the true neutrino declination from degrees to radians and into + # sin values. + data['sin_true_dec_min'] = np.sin(np.deg2rad( + data['sin_true_dec_min'])) + data['sin_true_dec_max'] = np.sin(np.deg2rad( + data['sin_true_dec_max'])) + + # Determine the binning for energy and declination. + log_true_e_binedges_lower = np.unique( + data['log_true_e_min']) + log_true_e_binedges_upper = np.unique( + data['log_true_e_max']) + sin_true_dec_binedges_lower = np.unique( + data['sin_true_dec_min']) + sin_true_dec_binedges_upper = np.unique( + data['sin_true_dec_max']) + + if(len(log_true_e_binedges_lower) != len(log_true_e_binedges_upper)): + raise ValueError('Cannot extract the log10(E/GeV) binning of the ' + 'effective area from data file "{}". The number of lower and upper ' + 'bin edges is not equal!'.format(str(loader.pathfilename_list))) + if(len(sin_true_dec_binedges_lower) != len(sin_true_dec_binedges_upper)): + raise ValueError('Cannot extract the Dec_nu binning of the effective ' + 'area from data file "{}". The number of lower and upper bin edges ' + 'is not equal!'.format(str(loader.pathfilename_list))) + + nbins_log_true_e = len(log_true_e_binedges_lower) + nbins_sin_true_dec = len(sin_true_dec_binedges_lower) + + # Construct the 2d array for the effective area. + arr = np.zeros((nbins_sin_true_dec, nbins_log_true_e), dtype=np.double) + + sin_true_dec_idx = np.digitize( + 0.5*(data['sin_true_dec_min'] + + data['sin_true_dec_max']), + sin_true_dec_binedges_lower) - 1 + log_true_e_idx = np.digitize( + 0.5*(data['log_true_e_min'] + + data['log_true_e_max']), + log_true_e_binedges_lower) - 1 + + arr[sin_true_dec_idx, log_true_e_idx] = data['a_eff'] + + return ( + arr, + sin_true_dec_binedges_lower, + sin_true_dec_binedges_upper, + log_true_e_binedges_lower, + log_true_e_binedges_upper + ) + + def load_smearing_histogram(pathfilenames): """Loads the 5D smearing histogram from the given data file. Parameters ---------- - pathfilenames : list of str + pathfilenames : str | list of str The file name of the data file. Returns From e0968928c18e7ad53bd3b81d2d2214587291c0b5 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 20 Apr 2022 15:10:40 +0200 Subject: [PATCH 033/274] Use the new utility function for the detector signal yield implementation method --- skyllh/analyses/i3/trad_ps/detsigyield.py | 70 +++++------------------ 1 file changed, 14 insertions(+), 56 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/detsigyield.py b/skyllh/analyses/i3/trad_ps/detsigyield.py index 5b7568960b..5ef8aa8d80 100644 --- a/skyllh/analyses/i3/trad_ps/detsigyield.py +++ b/skyllh/analyses/i3/trad_ps/detsigyield.py @@ -26,6 +26,9 @@ PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod, PowerLawFluxPointLikeSourceI3DetSigYield ) +from skyllh.analyses.i3.trad_ps.utils import ( + load_effective_area_array +) class PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( @@ -134,58 +137,13 @@ def construct_detsigyield( # Load the effective area data from the public dataset. aeff_fnames = dataset.get_abs_pathfilename_list( dataset.get_aux_data_definition('eff_area_datafile')) - floader = create_FileLoader(aeff_fnames) - aeff_data = floader.load_data() - aeff_data.rename_fields( - { - 'log10(E_nu/GeV)_min': 'log_true_energy_nu_min', - 'log10(E_nu/GeV)_max': 'log_true_energy_nu_max', - 'Dec_nu_min[deg]': 'true_sin_dec_nu_min', - 'Dec_nu_max[deg]': 'true_sin_dec_nu_max', - 'A_Eff[cm^2]': 'a_eff' - }, - must_exist=True) - # Convert the true neutrino declination from degrees to radians and into - # sin values. - aeff_data['true_sin_dec_nu_min'] = np.sin(np.deg2rad( - aeff_data['true_sin_dec_nu_min'])) - aeff_data['true_sin_dec_nu_max'] = np.sin(np.deg2rad( - aeff_data['true_sin_dec_nu_max'])) - - # Determine the binning for energy and declination. - log_energy_bin_edges_lower = np.unique( - aeff_data['log_true_energy_nu_min']) - log_energy_bin_edges_upper = np.unique( - aeff_data['log_true_energy_nu_max']) - - sin_dec_bin_edges_lower = np.unique(aeff_data['true_sin_dec_nu_min']) - sin_dec_bin_edges_upper = np.unique(aeff_data['true_sin_dec_nu_max']) - - if(len(log_energy_bin_edges_lower) != len(log_energy_bin_edges_upper)): - raise ValueError('Cannot extract the log10(E/GeV) binning of the ' - 'effective area for dataset "{}". The number of lower and ' - 'upper bin edges is not equal!'.format(dataset.name)) - if(len(sin_dec_bin_edges_lower) != len(sin_dec_bin_edges_upper)): - raise ValueError('Cannot extract the Dec_nu binning of the ' - 'effective area for dataset "{}". The number of lower and ' - 'upper bin edges is not equal!'.format(dataset.name)) - - n_bins_log_energy = len(log_energy_bin_edges_lower) - n_bins_sin_dec = len(sin_dec_bin_edges_lower) - - # Construct the 2d array for the effective area. - aeff_arr = np.zeros((n_bins_sin_dec, n_bins_log_energy), dtype=np.float) - - sin_dec_idx = np.digitize( - 0.5*(aeff_data['true_sin_dec_nu_min'] + - aeff_data['true_sin_dec_nu_max']), - sin_dec_bin_edges_lower) - 1 - log_e_idx = np.digitize( - 0.5*(aeff_data['log_true_energy_nu_min'] + - aeff_data['log_true_energy_nu_max']), - log_energy_bin_edges_lower) - 1 - - aeff_arr[sin_dec_idx,log_e_idx] = aeff_data['a_eff'] + ( + aeff_arr, + sin_true_dec_binedges_lower, + sin_true_dec_binedges_upper, + log_true_e_binedges_lower, + log_true_e_binedges_upper + ) = load_effective_area_array(aeff_fnames) # Calculate the detector signal yield in sin_dec vs gamma. def hist( @@ -219,8 +177,8 @@ def hist( return h - energy_bin_edges_lower = np.power(10, log_energy_bin_edges_lower) - energy_bin_edges_upper = np.power(10, log_energy_bin_edges_upper) + energy_bin_edges_lower = np.power(10, log_true_e_binedges_lower) + energy_bin_edges_upper = np.power(10, log_true_e_binedges_upper) # Make a copy of the gamma grid and extend the grid by one bin on each # side. @@ -243,14 +201,14 @@ def hist( # Create a 2d spline in log of the detector signal yield. sin_dec_bincenters = 0.5*( - sin_dec_bin_edges_lower + sin_dec_bin_edges_upper) + sin_true_dec_binedges_lower + sin_true_dec_binedges_upper) log_spl_sinDec_gamma = scipy.interpolate.RectBivariateSpline( sin_dec_bincenters, gamma_grid.grid, np.log(h), kx = self.spline_order_sinDec, ky = self.spline_order_gamma, s = 0) # Construct the detector signal yield instance with the created spline. sin_dec_binedges = np.concatenate( - (sin_dec_bin_edges_lower, [sin_dec_bin_edges_upper[-1]])) + (sin_true_dec_binedges_lower, [sin_true_dec_binedges_upper[-1]])) sin_dec_binning = BinningDefinition('sin_dec', sin_dec_binedges) detsigyield = PowerLawFluxPointLikeSourceI3DetSigYield( self, dataset, fluxmodel, livetime, sin_dec_binning, log_spl_sinDec_gamma) From 17a4e76e6bddd1b55d0a17729bd7f48c0253f928 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 20 Apr 2022 17:43:53 +0200 Subject: [PATCH 034/274] Add helper class for the effective area of the public data --- skyllh/analyses/i3/trad_ps/utils.py | 63 +++++++++++++++++++++++++++++ 1 file changed, 63 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 113d585d81..e31983529a 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -308,6 +308,69 @@ def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): return (dec, ra) +class PublicDataAeff(object): + """This class is a helper class for dealing with the effective area + provided by the public data. + """ + def __init__( + self, pathfilenames, **kwargs): + """Creates an effective area instance by loading the effective area + data from the given file. + """ + super().__init__(**kwargs) + + ( + self.aeff_arr, + self.sin_true_dec_binedges_lower, + self.sin_true_dec_binedges_upper, + self.log_true_e_binedges_lower, + self.log_true_e_binedges_upper + ) = load_effective_area_array(pathfilenames) + + self.sin_true_dec_binedges = np.concatenate( + (self.sin_true_dec_binedges_lower, + self.sin_true_dec_binedges_upper[-1:]) + ) + self.log_true_e_binedges = np.concatenate( + (self.log_true_e_binedges_lower, + self.log_true_e_binedges_upper[-1:]) + ) + + def get_aeff(self, sin_true_dec, log_true_e): + """Retrieves the effective area for the given sin(dec_true) and + log(E_true) value pairs. + + Parameters + ---------- + sin_true_dec : (n,)-shaped 1D ndarray + The sin(dec_true) values. + log_true_e : (n,)-shaped 1D ndarray + The log(E_true) values. + + Returns + ------- + aeff : (n,)-shaped 1D ndarray + The 1D ndarray holding the effective area values for each value + pair. For value pairs outside the effective area data zero is + returned. + """ + valid = ( + (sin_true_dec >= self.sin_true_dec_binedges[0]) & + (sin_true_dec <= self.sin_true_dec_binedges[-1]) & + (log_true_e >= self.log_true_e_binedges[0]) & + (log_true_e <= self.log_true_e_binedges[-1]) + ) + sin_true_dec_idxs = np.digitize( + sin_true_dec[valid], self.sin_true_dec_binedges) - 1 + log_true_e_idxs = np.digitize( + log_true_e[valid], self.log_true_e_binedges) - 1 + + aeff = np.zeros((len(valid),), dtype=np.double) + aeff[valid] = self.aeff_arr[sin_true_dec_idxs,log_true_e_idxs] + + return aeff + + class PublicDataSmearingMatrix(object): """This class is a helper class for dealing with the smearing matrix provided by the public data. From a67a4e96b567c6b785a3ab5f41053db7064e5cc9 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 20 Apr 2022 17:45:01 +0200 Subject: [PATCH 035/274] Apply the effective area to the signal energy pdf --- skyllh/analyses/i3/trad_ps/signalpdf.py | 37 ++++++++++++++++++------- 1 file changed, 27 insertions(+), 10 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index b4f38bad95..b6c54ed8d4 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -31,15 +31,16 @@ from skyllh.analyses.i3.trad_ps.utils import ( load_smearing_histogram, psi_to_dec_and_ra, + PublicDataAeff, PublicDataSmearingMatrix ) class PublicDataSignalGenerator(object): - def __init__(self, ds): + def __init__(self, ds, **kwargs): """Creates a new instance of the signal generator for generating signal events from the provided public data smearing matrix. """ - super().__init__() + super().__init__(**kwargs) self.smearing_matrix = PublicDataSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( @@ -70,6 +71,7 @@ def _generate_events( The numpy record array holding the event data. It contains the following data fields: - 'isvalid' + - 'log_true_energy' - 'log_energy' - 'sin_dec' Single values can be NaN in cases where a pdf was not available. @@ -77,6 +79,7 @@ def _generate_events( # Create the output event array. out_dtype = [ ('isvalid', np.bool_), + ('log_true_energy', np.double), ('log_energy', np.double), ('sin_dec', np.double) ] @@ -96,7 +99,7 @@ def _generate_events( E_max=10**max_log_true_e )) - #events['log_true_e'] = log_true_e + events['log_true_energy'] = log_true_e log_true_e_idxs = ( np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 @@ -166,8 +169,6 @@ def generate_signal_events( return events - - class PublicDataSignalI3EnergyPDF(EnergyPDF, IsSignalPDF, UsesBinning): """Class that implements the enegry signal PDF for a given flux model given the public data. @@ -485,12 +486,15 @@ def __init__( def create_I3EnergyPDF( rss, ds, logE_binning, sinDec_binning, smoothing_filter, - siggen, flux_model, n_events, gridfitparams): + aeff, siggen, flux_model, n_events, gridfitparams): # Create a copy of the FluxModel with the given flux parameters. # The copy is needed to not interfer with other CPU processes. my_flux_model = flux_model.copy(newprop=gridfitparams) # Generate signal events for sources in every sin(dec) bin. + # The physics weight is the effective area of the event given its + # true energy and true declination. + data_physicsweight = None events = None n_evts = int(np.round(n_events / sinDec_binning.nbins)) for sin_dec in sinDec_binning.bincenters: @@ -501,15 +505,21 @@ def create_I3EnergyPDF( src_ra=np.radians(180), flux_model=my_flux_model, n_events=n_evts) + data_physicsweight_ = aeff.get_aeff( + np.repeat(sin_dec, len(events_)), + events_['log_true_energy']) if events is None: events = events_ + data_physicsweight = data_physicsweight_ else: - events = np.concatenate((events, events_)) + events = np.concatenate( + (events, events_)) + data_physicsweight = np.concatenate( + (data_physicsweight, data_physicsweight_)) data_logE = events['log_energy'] data_sinDec = events['sin_dec'] data_mcweight = np.ones((len(events),), dtype=np.double) - data_physicsweight = np.ones((len(events),), dtype=np.double) epdf = I3EnergyPDF( data_logE=data_logE, @@ -523,15 +533,22 @@ def create_I3EnergyPDF( return epdf + print('Generate signal energy PDF for ds {} with {} CPUs'.format( + ds.name, self.ncpu)) + # Create a signal generator for this dataset. siggen = PublicDataSignalGenerator(ds) + aeff = PublicDataAeff( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('eff_area_datafile'))) + logE_binning = ds.get_binning_definition('log_energy') sinDec_binning = ds.get_binning_definition('sin_dec') args_list = [ - ((rss, ds, logE_binning, sinDec_binning, smoothing_filter, siggen, - flux_model, n_events, gridfitparams), {}) + ((rss, ds, logE_binning, sinDec_binning, smoothing_filter, aeff, + siggen, flux_model, n_events, gridfitparams), {}) for gridfitparams in self.gridfitparams_list ] From bbf0b549a6985db445cf2b89f7dd82e2bf88d4a2 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 20 Apr 2022 17:48:57 +0200 Subject: [PATCH 036/274] Delete obsolete file --- skyllh/analyses/i3/trad_ps/pdfratio.py | 337 ------------------------- 1 file changed, 337 deletions(-) delete mode 100644 skyllh/analyses/i3/trad_ps/pdfratio.py diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py deleted file mode 100644 index 4dd9349fba..0000000000 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ /dev/null @@ -1,337 +0,0 @@ -# -*- coding: utf-8 -*- - -import numpy as np -from scipy.interpolate import RegularGridInterpolator - -from skyllh.core.parameters import make_params_hash -from skyllh.core.pdf import EnergyPDF -from skyllh.core.pdfratio import ( - SigSetOverBkgPDFRatio, - PDFRatioFillMethod, - MostSignalLikePDFRatioFillMethod -) -from skyllh.core.multiproc import ( - IsParallelizable, - parallelize -) - -from skyllh.analyses.i3.trad_ps.signalpdf import PublicDataSignalI3EnergyPDFSet - - -class PublicDataI3EnergySigSetOverBkgPDFRatioSpline( - SigSetOverBkgPDFRatio, - IsParallelizable): - """This class implements a signal over background PDF ratio spline for a - signal PDF that is derived from PublicDataSignalI3EnergyPDFSet and a - background PDF that is derived from I3EnergyPDF. It creates a spline for the - ratio of the signal and background PDFs for a grid of different discrete - energy signal fit parameters, which are defined by the signal PDF set. - """ - def __init__( - self, signalpdfset, backgroundpdf, - fillmethod=None, interpolmethod=None, ncpu=None, ppbar=None): - """Creates a new IceCube signal-over-background energy PDF ratio object - specialized for the public data. - - Paramerers - ---------- - signalpdfset : class instance derived from PDFSet (for PDF type - EnergyPDF), IsSignalPDF, and UsesBinning - The PDF set, which provides signal energy PDFs for a set of - discrete signal parameters. - backgroundpdf : class instance derived from EnergyPDF, and - IsBackgroundPDF - The background energy PDF object. - fillmethod : instance of PDFRatioFillMethod | None - An instance of class derived from PDFRatioFillMethod that implements - the desired ratio fill method. - If set to None (default), the default ratio fill method - MostSignalLikePDFRatioFillMethod will be used. - interpolmethod : class of GridManifoldInterpolationMethod - The class implementing the fit parameter interpolation method for - the PDF ratio manifold grid. - ncpu : int | None - The number of CPUs to use to create the ratio splines for the - different sets of signal parameters. - ppbar : ProgressBar instance | None - The instance of ProgressBar of the optional parent progress bar. - """ - super().__init__( - pdf_type=EnergyPDF, - signalpdfset=signalpdfset, backgroundpdf=backgroundpdf, - interpolmethod=interpolmethod, - ncpu=ncpu) - - # Define the default ratio fill method. - if(fillmethod is None): - fillmethod = MostSignalLikePDFRatioFillMethod() - self.fillmethod = fillmethod - - def create_log_ratio_spline( - sigpdfset, bkgpdf, fillmethod, gridfitparams, src_dec_idx): - """Creates the signal/background ratio 2d spline for the given - signal parameters. - - Returns - ------- - log_ratio_spline : RegularGridInterpolator - The spline of the logarithmic PDF ratio values in the - (log10(E_reco),sin(dec_reco)) space. - """ - # Get the signal PDF for the given signal parameters. - sigpdf = sigpdfset.get_pdf(gridfitparams) - - bkg_log_e_bincenters = bkgpdf.get_binning('log_energy').bincenters - sigpdf_hist = sigpdf.calc_prob_for_true_dec_idx( - src_dec_idx, bkg_log_e_bincenters) - # Transform the (log10(E_reco),)-shaped 1d array into the - # (log10(E_reco),sin(dec_reco))-shaped 2d array. - sigpdf_hist = np.repeat( - [sigpdf_hist], bkgpdf.hist.shape[1], axis=0).T - - sig_mask_mc_covered = np.ones_like(sigpdf_hist, dtype=np.bool) - bkg_mask_mc_covered = np.ones_like(bkgpdf.hist, dtype=np.bool) - sig_mask_mc_covered_zero_physics = sigpdf_hist == 0 - bkg_mask_mc_covered_zero_physics = np.zeros_like( - bkgpdf.hist, dtype=np.bool) - - # Create the ratio array with the same shape than the background pdf - # histogram. - ratio = np.ones_like(bkgpdf.hist, dtype=np.float) - - # Fill the ratio array. - ratio = fillmethod.fill_ratios(ratio, - sigpdf_hist, bkgpdf.hist, - sig_mask_mc_covered, - sig_mask_mc_covered_zero_physics, - bkg_mask_mc_covered, - bkg_mask_mc_covered_zero_physics) - - # Define the grid points for the spline. In general, we use the bin - # centers of the binning, but for the first and last point of each - # dimension we use the lower and upper bin edge, respectively, to - # ensure full coverage of the spline across the binning range. - points_list = [] - for binning in bkgpdf.binnings: - points = binning.bincenters - (points[0], points[-1]) = (binning.lower_edge, binning.upper_edge) - points_list.append(points) - - # Create the spline for the ratio values. - log_ratio_spline = RegularGridInterpolator( - tuple(points_list), - np.log(ratio), - method='linear', - bounds_error=False, - fill_value=0.) - - return log_ratio_spline - - # Get the list of fit parameter permutations on the grid for which we - # need to create PDF ratio arrays. - gridfitparams_list = signalpdfset.gridfitparams_list - - self._gridfitparams_hash_log_ratio_spline_dict_list = [] - for src_dec_idx in range(signalpdfset.true_dec_binning.nbins): - args_list = [ - ( - ( - signalpdfset, - backgroundpdf, - fillmethod, - gridfitparams, - src_dec_idx - ), - {} - ) - for gridfitparams in gridfitparams_list - ] - - log_ratio_spline_list = parallelize( - create_log_ratio_spline, args_list, self.ncpu, ppbar=ppbar) - - # Save all the log_ratio splines in a dictionary. - gridfitparams_hash_log_ratio_spline_dict = dict() - for (gridfitparams, log_ratio_spline) in zip( - gridfitparams_list, log_ratio_spline_list): - gridfitparams_hash = make_params_hash(gridfitparams) - gridfitparams_hash_log_ratio_spline_dict[ - gridfitparams_hash] = log_ratio_spline - self._gridfitparams_hash_log_ratio_spline_dict_list.append( - gridfitparams_hash_log_ratio_spline_dict) - - # Save the list of data field names. - self._data_field_names = [ - binning.name - for binning in self.backgroundpdf.binnings - ] - - # Construct the instance for the fit parameter interpolation method. - self._interpolmethod_instance = self.interpolmethod( - self._get_spline_value, - signalpdfset.fitparams_grid_set) - - # Create cache variables for the last ratio value and gradients in order - # to avoid the recalculation of the ratio value when the - # ``get_gradient`` method is called (usually after the ``get_ratio`` - # method was called). - self._cache_src_dec_idx = None - self._cache_fitparams_hash = None - self._cache_ratio = None - self._cache_gradients = None - - @property - def fillmethod(self): - """The PDFRatioFillMethod object, which should be used for filling the - PDF ratio bins. - """ - return self._fillmethod - @fillmethod.setter - def fillmethod(self, obj): - if(not isinstance(obj, PDFRatioFillMethod)): - raise TypeError('The fillmethod property must be an instance of ' - 'PDFRatioFillMethod!') - self._fillmethod = obj - - def _get_spline_value(self, tdm, gridfitparams, eventdata): - """Selects the spline object for the given fit parameter grid point and - evaluates the spline for all the given events. - """ - if(self._cache_src_dec_idx is None): - raise RuntimeError('There was no source declination bin index ' - 'pre-calculated!') - - # Get the spline object for the given fit parameter grid values. - gridfitparams_hash = make_params_hash(gridfitparams) - spline = self._gridfitparams_hash_log_ratio_spline_dict_list\ - [self._cache_src_dec_idx][gridfitparams_hash] - - # Evaluate the spline. - value = spline(eventdata) - - return value - - def _get_src_dec_idx_from_source_array(self, src_array): - """Determines the source declination index given the source array from - the trial data manager. For now only a single source is supported! - """ - if(len(src_array) != 1): - raise NotImplementedError( - 'The PDFRatio class "{}" is only implemneted for a single ' - 'source! But {} sources were defined!'.format( - self.__class__.name, len(src_array))) - src_dec = src_array['dec'][0] - true_dec_binning = self.signalpdfset.true_dec_binning - src_dec_idx = np.digitize(src_dec, true_dec_binning.binedges) - - return src_dec_idx - - def _is_cached(self, tdm, src_dec_idx, fitparams_hash): - """Checks if the ratio and gradients for the given set of fit parameters - are already cached. - """ - if((self._cache_src_dec_idx == src_dec_idx) and - (self._cache_fitparams_hash == fitparams_hash) and - (len(self._cache_ratio) == tdm.n_selected_events) - ): - return True - return False - - def _calculate_ratio_and_gradients( - self, tdm, src_dec_idx, fitparams, fitparams_hash): - """Calculates the ratio values and ratio gradients for all the events - given the fit parameters. It caches the results. - """ - get_data = tdm.get_data - - # The _get_spline_value method needs the cache source dec index for the - # current evaluation of the PDF ratio. - self._cache_src_dec_idx = src_dec_idx - - # Create a 2D event data array holding only the needed event data fields - # for the PDF ratio spline evaluation. - eventdata = np.vstack([get_data(fn) for fn in self._data_field_names]).T - - (ratio, gradients) = self._interpolmethod_instance.get_value_and_gradients( - tdm, eventdata, fitparams) - # The interpolation works on the logarithm of the ratio spline, hence - # we need to transform it using the exp function, and we need to account - # for the exp function in the gradients. - ratio = np.exp(ratio) - gradients = ratio * gradients - - # Cache the value and the gradients. - self._cache_fitparams_hash = fitparams_hash - self._cache_ratio = ratio - self._cache_gradients = gradients - - def get_ratio(self, tdm, fitparams, tl=None): - """Retrieves the PDF ratio values for each given trial event data, given - the given set of fit parameters. This method is called during the - likelihood maximization process. - For computational efficiency reasons, the gradients are calculated as - well and will be cached. - - Parameters - ---------- - tdm : instance of TrialDataManager - The TrialDataManager instance holding the trial event data for which - the PDF ratio values should get calculated. - fitparams : dict - The dictionary with the fit parameter values. - tl : TimeLord instance | None - The optional TimeLord instance that should be used to measure - timing information. - - Returns - ------- - ratio : 1d ndarray of float - The PDF ratio value for each given event. - """ - fitparams_hash = make_params_hash(fitparams) - - # Determine the source declination bin index. - src_array = tdm.get_data('src_array') - src_dec_idx = self._get_src_dec_idx_from_source_array(src_array) - - # Check if the ratio value is already cached. - if(self._is_cached(tdm, src_dec_idx, fitparams_hash)): - return self._cache_ratio - - self._calculate_ratio_and_gradients( - tdm, src_dec_idx, fitparams, fitparams_hash) - - return self._cache_ratio - - def get_gradient(self, tdm, fitparams, fitparam_name): - """Retrieves the PDF ratio gradient for the pidx'th fit parameter. - - Parameters - ---------- - tdm : instance of TrialDataManager - The TrialDataManager instance holding the trial event data for which - the PDF ratio gradient values should get calculated. - fitparams : dict - The dictionary with the fit parameter values. - fitparam_name : str - The name of the fit parameter for which the gradient should get - calculated. - """ - fitparams_hash = make_params_hash(fitparams) - - # Convert the fit parameter name into the local fit parameter index. - pidx = self.convert_signal_fitparam_name_into_index(fitparam_name) - - # Determine the source declination bin index. - src_array = tdm.get_data('src_array') - src_dec_idx = self._get_src_dec_idx_from_source_array(src_array) - - # Check if the gradients have been calculated already. - if(self._is_cached(tdm, src_dec_idx, fitparams_hash)): - return self._cache_gradients[pidx] - - # The gradients have not been calculated yet. - self._calculate_ratio_and_gradients( - tdm, src_dec_idx, fitparams, fitparams_hash) - - return self._cache_gradients[pidx] From 3b6eb9e35f26a8b5eaf2df8f9af0daa212a5d2f7 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 20 Apr 2022 17:49:56 +0200 Subject: [PATCH 037/274] Add additional arguments --- skyllh/analyses/i3/trad_ps/analysis.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index 39bad84961..bf3638af3e 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -252,7 +252,7 @@ def create_analysis( ds=ds, flux_model=fluxmodel, fitparam_grid_set=gamma_grid, - n_events=int(1e6), + n_events=int(1e7), smoothing_filter=smoothing_filter, ppbar=pbar) energy_bkgpdf = DataBackgroundI3EnergyPDF( @@ -293,6 +293,10 @@ def create_analysis( p.add_argument('--data_base_path', default=None, type=str, help='The base path to the data samples (default=None)' ) + p.add_argument('--pdf-seed', default=1, type=int, + help='The random number generator seed for generating the signal PDF.') + p.add_argument('--seed', default=1, type=int, + help='The random number generator seed for the likelihood minimization.') p.add_argument("--ncpu", default=1, type=int, help='The number of CPUs to utilize where parallelization is possible.' ) @@ -324,10 +328,10 @@ def create_analysis( dsc = data_samples[sample].create_dataset_collection(args.data_base_path) datasets.append(dsc.get_dataset(season)) - rss_seed = 1 - # Define a random state service. - rss = RandomStateService(rss_seed) + # Define a random state service. + rss_pdf = RandomStateService(args.pdf_seed) + rss = RandomStateService(args.seed) # Define the point source. source = PointLikeSource(np.deg2rad(args.ra), np.deg2rad(args.dec)) print('source: ', str(source)) @@ -336,7 +340,7 @@ def create_analysis( with tl.task_timer('Creating analysis.'): ana = create_analysis( - rss, + rss_pdf, datasets, source, gamma_seed=args.gamma_seed, From ec59574ff472101c543bff1c32816a0e368cb68b Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 25 Apr 2022 18:17:13 +0200 Subject: [PATCH 038/274] Added rebin function for 1D histogram rebinning with moments --- skyllh/core/binning.py | 139 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 139 insertions(+) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index 4e3c4d55f0..c3150fae10 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -2,8 +2,147 @@ import numpy as np +from scipy.linalg import solve + from skyllh.core.py import classname + +def rebin( + bincontent: np.array, + old_binedges: np.array, + new_binedges: np.array, + negatives=False): + """Rebins the binned counts to the new desired grid. This function + uses a method of moments approach. Currently it uses a three moments + appraoch. At the edges of the array it uses a two moments approach. + + Parameters + ---------- + bincontent: (n,)-shaped 1D numpy ndarray + The binned content which should be rebinned. + old_binedges: (n+1,)-shaped 1D numpy ndarray + The old grid's bin edges. The shape needs to be the same as + `bincontent`. + new_binedges: (m+1)-shaped 1D numpy ndarray + The new bin edges to use. + binning_scheme: str + The binning scheme to use. Choices are "log" (logarithmic) + or "lin" (linear). This decides how to calculate the midpoints + of each bin. + negatives: bool + Switch to keep or remove negative values in the final binning. + + Returns + ------- + new_bincontent: 1D numpy ndarray + The new binned counts for the new binning. + + Raises + ------ + ValueError: + Unknown binning scheme. + + Authors + ------- + - Dr. Stephan Meighen-Berger + - Dr. Martin Wolf + """ + old_bincenters = 0.5*(old_binedges[1:] + old_binedges[:-1]) + + # Checking if shapes align. + if bincontent.shape != old_bincenters.shape: + ValueError('The arguments bincontent and old_binedges do not match!' + 'bincontent must be (n,)-shaped and old_binedges must be (n+1,)-' + 'shaped!') + + # Setting up the new binning. + new_bincenters = 0.5*(new_binedges[1:] + new_binedges[:-1]) + + + new_widths = np.diff(new_binedges) + new_nbins = len(new_widths) + + # Create output array with zeros. + new_bincontent = np.zeros(new_bincenters.shape) + + # Looping over the old bin contents and distributing + for (idx, bin_val) in enumerate(bincontent): + # Ignore empty bins. + if bin_val == 0.: + continue + + old_bincenter = old_bincenters[idx] + + new_point = (np.abs(new_binedges - old_bincenter)).argmin() + + if new_point == 0: + # It the first bin. Use 2-moments method. + start_idx = new_point + end_idx = new_point + 1 + + mat = np.vstack( + ( + new_widths[start_idx:end_idx+1], + new_widths[start_idx:end_idx+1] + * new_bincenters[start_idx:end_idx+1] + ) + ) + + b = bin_val * np.array([ + 1., + old_bincenter + ]) + elif new_point == new_nbins-1: + # It the last bin. Use 2-moments method. + start_idx = new_point - 1 + end_idx = new_point + + mat = np.vstack( + ( + new_widths[start_idx:end_idx+1], + new_widths[start_idx:end_idx+1] + * new_bincenters[start_idx:end_idx+1] + ) + ) + + b = bin_val * np.array([ + 1., + old_bincenter + ]) + else: + # Setting up the equation for 3 moments (mat*x = b) + # x is the values we want + start_idx = new_point - 1 + end_idx = new_point + 1 + + mat = np.vstack( + ( + new_widths[start_idx:end_idx+1], + new_widths[start_idx:end_idx+1] + * new_bincenters[start_idx:end_idx+1], + new_widths[start_idx:end_idx+1] + * new_bincenters[start_idx:end_idx+1]**2 + ) + ) + + b = bin_val * np.array([ + 1., + old_bincenter, + old_bincenter**2 + ]) + + # Solving and adding to the new bin content. + new_bincontent[start_idx:end_idx+1] += solve(mat, b) + + if not negatives: + new_bincontent[new_bincontent < 0.] = 0. + + new_bincontent = new_bincontent / ( + np.sum(new_bincontent) / np.sum(bincontent)) + + return new_bincontent + + class BinningDefinition(object): """The BinningDefinition class provides a structure to hold histogram binning definitions for an analyis. From 52977bfaf8e5dec63cebc0d58f242d5ddf1396bc Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 27 Apr 2022 14:08:20 +0200 Subject: [PATCH 039/274] Added custom signal generator for PD analysis. --- skyllh/analyses/i3/trad_ps/analysis.py | 4 +- .../analyses/i3/trad_ps/signal_generator.py | 757 ++++++------------ 2 files changed, 240 insertions(+), 521 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index bf3638af3e..b715049228 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -61,6 +61,7 @@ ) from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod +from skyllh.analyses.i3.trad_ps.signal_generator import PublicDataSignalGenerator # Analysis utilities. from skyllh.core.analysis_utils import ( @@ -210,7 +211,8 @@ def create_analysis( src_fitparam_mapper, fitparam_ns, test_statistic, - bkg_gen_method + bkg_gen_method, + custom_sig_generator=PublicDataSignalGenerator ) # Define the event selection method for pure optimization purposes. diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index 9b26f2a12f..56566c8b8f 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -1,463 +1,174 @@ import numpy as np -from copy import deepcopy -import os.path -from skyllh.physics.flux import FluxModel -from skyllh.analyses.i3.trad_ps.utils import load_smearing_histogram +from skyllh.core.llhratio import LLHRatio +from skyllh.core.dataset import Dataset +from skyllh.core.source_hypothesis import SourceHypoGroupManager +from skyllh.core.storage import DataFieldRecordArray +from skyllh.analyses.i3.trad_ps.utils import ( + psi_to_dec_and_ra, + PublicDataSmearingMatrix +) +from skyllh.core.py import ( + issequenceof, + float_cast, + int_cast +) + + +class PublicDataDatasetSignalGenerator(object): + + def __init__(self, ds, **kwargs): + """Creates a new instance of the signal generator for generating signal + events from the provided public data dataset. + """ + super().__init__(**kwargs) + self.smearing_matrix = PublicDataSmearingMatrix( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) -class signal_injector(object): - r""" - """ + def _generate_events( + self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): + """Generates `n_events` signal events for the given source location + and flux model. + + Note: + Some values can be NaN in cases where a PDF was not available! - def __init__( - self, - name: str, - declination: float, - right_ascension: float, - flux_model: FluxModel, - data_path="/home/mwolf/projects/publicdata_ps/icecube_10year_ps/irfs" - ): - r""" Parameters ---------- - - name : str - Dataset identifier. Must be one among: - ['IC40', 'IC59', 'IC79', 'IC86_I', 'IC86_II']. - - - declination : float - Source declination in degrees. - - - right_ascension : float - Source right ascension in degrees. - - - flux_model : FluxModel - Instance of the `FluxModel` class. - - - data_path : str - Path to the smearing matrix data. + rss : instance of RandomStateService + The instance of RandomStateService to use for drawing random + numbers. + src_dec : float + The declination of the source in radians. + src_ra : float + The right-ascention of the source in radians. + + Returns + ------- + events : numpy record array of size `n_events` + The numpy record array holding the event data. + It contains the following data fields: + - 'isvalid' + - 'log_true_energy' + - 'log_energy' + - 'sin_dec' + Single values can be NaN in cases where a pdf was not available. """ - self.flux_model = flux_model - self.dec = declination - self.ra = right_ascension - - ( - self.histogram, - self.true_e_bin_edges, - self.true_dec_bin_edges, - self.reco_e_lower_edges, - self.reco_e_upper_edges, - self.psf_lower_edges, - self.psf_upper_edges, - self.ang_err_lower_edges, - self.ang_err_upper_edges - ) = load_smearing_histogram(os.path.join(data_path, f"{name}_smearing.csv")) - - # Find the declination bin - if(self.dec < self.true_dec_bin_edges[0] or self.dec > self.true_dec_bin_edges[-1]): - raise ValueError("NotImplemented") - self.dec_idx = np.digitize(self.dec, self.true_dec_bin_edges) - 1 - - @staticmethod - def _get_bin_centers(low_edges, high_edges): - r"""Given an array of lower bin edges and an array of upper bin edges, - returns the corresponding bin centers. - """ - # bin_edges = np.union1d(low_edges, high_edges) - # bin_centers = 0.5 * (bin_edges[1:] + bin_edges[:-1]) - bin_centers = 0.5 * (low_edges + high_edges) - return bin_centers - - def get_weighted_marginalized_pdf( - self, true_e_idx, reco_e_idx=None, psf_idx=None - ): - r"""Get the reconstructed muon energy pdf for a specific true neutrino - energy weighted with the assumed flux model. - The function returns both the bin center values and the pdf values, - which might be useful for plotting. - If no pdf is given for the assumed true neutrino energy, returns None. - """ - - if reco_e_idx is None: - # Get the marginalized distribution of the reconstructed energy - # for a given (true energy, true declination) bin. - pdf = deepcopy(self.histogram[true_e_idx, self.dec_idx, :]) - pdf = np.sum(pdf, axis=(-2, -1)) - label = "reco_e" - elif psf_idx is None: - # Get the marginalized distribution of the neutrino-muon opening - # angle for a given (true energy, true declination, reco energy) - # bin. - pdf = deepcopy( - self.histogram[true_e_idx, self.dec_idx, reco_e_idx, :] - ) - pdf = np.sum(pdf, axis=-1) - label = "psf" - else: - # Get the marginalized distribution of the neutrino-muon opening - # angle for a given - # (true energy, true declination, reco energy, psi) bin. - pdf = deepcopy( - self.histogram[true_e_idx, self.dec_idx, - reco_e_idx, psf_idx, :] - ) - label = "ang_err" - - # Check whether there is no pdf in the table for this neutrino energy. - if np.sum(pdf) == 0: - return None, None, None, None, None - - if label == "reco_e": - # Get the reco energy bin centers. - lower_bin_edges = ( - self.reco_e_lower_edges[true_e_idx, self.dec_idx, :] - ) - upper_bin_edges = ( - self.reco_e_upper_edges[true_e_idx, self.dec_idx, :] - ) - bin_centers = self._get_bin_centers( - lower_bin_edges, upper_bin_edges - ) - - elif label == "psf": - lower_bin_edges = ( - self.psf_lower_edges[true_e_idx, self.dec_idx, reco_e_idx, :] - ) - upper_bin_edges = ( - self.psf_upper_edges[true_e_idx, self.dec_idx, reco_e_idx, :] - ) - - elif label == "ang_err": - lower_bin_edges = ( - self.ang_err_lower_edges[ - true_e_idx, self.dec_idx, reco_e_idx, psf_idx, : - ] - ) - upper_bin_edges = ( - self.ang_err_upper_edges[ - true_e_idx, self.dec_idx, reco_e_idx, psf_idx, : - ] - ) - - bin_centers = self._get_bin_centers( - lower_bin_edges, upper_bin_edges - ) - bin_width = upper_bin_edges - lower_bin_edges - - # Re-normalize in case some bins were cut. - pdf /= (np.sum(pdf * bin_width)) - - return lower_bin_edges, upper_bin_edges, bin_centers, bin_width, pdf - - def _get_reconstruction_from_histogram( - self, rs, idxs, value=None, bin_centers=None - ): - if value is not None: - if bin_centers is None: - raise RuntimeError("NotImplemented.") - value_idx = np.argmin(abs(value - bin_centers)) - idxs[idxs.index(None)] = value_idx - - (low_edges, up_edges, new_bin_centers, bin_width, hist) = ( - self.get_weighted_marginalized_pdf(idxs[0], idxs[1], idxs[2]) - ) - if low_edges is None: - return None, None, None, None - reco_bin = rs.choice(new_bin_centers, p=(hist * bin_width)) - reco_idx = np.argmin(abs(reco_bin - new_bin_centers)) - reco_value = np.random.uniform(low_edges[reco_idx], up_edges[reco_idx]) - - return reco_value, reco_bin, new_bin_centers, idxs - - def circle_parametrization(self, rs, psf): - psf = np.atleast_1d(psf) - # Transform everything in radians and convert the source declination - # to source zenith angle - a = np.radians(psf) - b = np.radians(90 - self.dec) - c = np.radians(self.ra) - - # Random rotation angle for the 2D circle - t = rs.uniform(0, 2*np.pi, size=len(psf)) - - # Parametrize the circle - x = ( - (np.sin(a)*np.cos(b)*np.cos(c)) * np.cos(t) + - (np.sin(a)*np.sin(c)) * np.sin(t) - - (np.cos(a)*np.sin(b)*np.cos(c)) - ) - y = ( - -(np.sin(a)*np.cos(b)*np.sin(c)) * np.cos(t) + - (np.sin(a)*np.cos(c)) * np.sin(t) + - (np.cos(a)*np.sin(b)*np.sin(c)) - ) - z = ( - (np.sin(a)*np.sin(b)) * np.cos(t) + - (np.cos(a)*np.cos(b)) - ) - - # Convert back to right ascension and declination - # This is to distinguish between diametrically opposite directions. - zen = np.arccos(z) - azi = np.arctan2(y, x) - - return (np.degrees(np.pi - azi), np.degrees(np.pi/2 - zen)) - - def get_log_e_pdf(self, log_true_e_idx): - if log_true_e_idx == -1: - return (None, None, None, None) - - pdf = self.histogram[log_true_e_idx, self.dec_idx] - pdf = np.sum(pdf, axis=(-2, -1)) - - if np.sum(pdf) == 0: - return (None, None, None, None) - - # Get the reco energy bin edges and widths. - lower_bin_edges = self.reco_e_lower_edges[ - log_true_e_idx, self.dec_idx - ] - upper_bin_edges = self.reco_e_upper_edges[ - log_true_e_idx, self.dec_idx - ] - bin_widths = upper_bin_edges - lower_bin_edges - - # Normalize the PDF. - pdf /= (np.sum(pdf * bin_widths)) - - return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) - - def get_psi_pdf(self, log_true_e_idx, log_e_idx): - if log_true_e_idx == -1 or log_e_idx == -1: - return (None, None, None, None) - - pdf = self.histogram[log_true_e_idx, self.dec_idx, log_e_idx] - pdf = np.sum(pdf, axis=-1) - - if np.sum(pdf) == 0: - return (None, None, None, None) - - # Get the PSI bin edges and widths. - lower_bin_edges = self.psf_lower_edges[ - log_true_e_idx, self.dec_idx, log_e_idx - ] - upper_bin_edges = self.psf_upper_edges[ - log_true_e_idx, self.dec_idx, log_e_idx - ] - bin_widths = upper_bin_edges - lower_bin_edges - - # Normalize the PDF. - pdf /= (np.sum(pdf * bin_widths)) - - return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) - - def get_ang_err_pdf(self, log_true_e_idx, log_e_idx, psi_idx): - if log_true_e_idx == -1 or log_e_idx == -1 or psi_idx == -1: - return (None, None, None, None) - - pdf = self.histogram[log_true_e_idx, self.dec_idx, log_e_idx, psi_idx] - - if np.sum(pdf) == 0: - return (None, None, None, None) - - # Get the ang_err bin edges and widths. - lower_bin_edges = self.ang_err_lower_edges[ - log_true_e_idx, self.dec_idx, log_e_idx, psi_idx - ] - upper_bin_edges = self.ang_err_upper_edges[ - log_true_e_idx, self.dec_idx, log_e_idx, psi_idx - ] - bin_widths = upper_bin_edges - lower_bin_edges - - # Normalize the PDF. - pdf = pdf / np.sum(pdf * bin_widths) - - return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) - - def get_log_e_from_log_true_e_idxs(self, rs, log_true_e_idxs): - n_evt = len(log_true_e_idxs) - log_e_idx = np.empty((n_evt,), dtype=int) - log_e = np.empty((n_evt,), dtype=np.double) - - unique_log_true_e_idxs = np.unique(log_true_e_idxs) - for b_log_true_e_idx in unique_log_true_e_idxs: - m = log_true_e_idxs == b_log_true_e_idx - b_size = np.count_nonzero(m) - (pdf, low_bin_edges, up_bin_edges, - bin_widths) = self.get_log_e_pdf(b_log_true_e_idx) - if pdf is None: - log_e_idx[m] = -1 - log_e[m] = np.nan - continue - - b_log_e_idx = rs.choice( - np.arange(len(pdf)), - p=(pdf * bin_widths), - size=b_size) - b_log_e = rs.uniform( - low_bin_edges[b_log_e_idx], - up_bin_edges[b_log_e_idx], - size=b_size) - - log_e_idx[m] = b_log_e_idx - log_e[m] = b_log_e - - return (log_e_idx, log_e) - - def get_psi_from_log_true_e_idxs_and_log_e_idxs( - self, rs, log_true_e_idxs, log_e_idxs): - if(len(log_true_e_idxs) != len(log_e_idxs)): - raise ValueError('The lengths of log_true_e_idxs ' - 'and log_e_idxs must be equal!') - - n_evt = len(log_true_e_idxs) - psi_idx = np.empty((n_evt,), dtype=int) - psi = np.empty((n_evt,), dtype=np.double) - - unique_log_true_e_idxs = np.unique(log_true_e_idxs) - for b_log_true_e_idx in unique_log_true_e_idxs: - m = log_true_e_idxs == b_log_true_e_idx - bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) - for bb_log_e_idx in bb_unique_log_e_idxs: - mm = m & (log_e_idxs == bb_log_e_idx) - bb_size = np.count_nonzero(mm) - (pdf, low_bin_edges, up_bin_edges, bin_widths) = ( - self.get_psi_pdf(b_log_true_e_idx, bb_log_e_idx) - ) - if pdf is None: - psi_idx[mm] = -1 - psi[mm] = np.nan - continue - - bb_psi_idx = rs.choice( - np.arange(len(pdf)), - p=(pdf * bin_widths), - size=bb_size) - bb_psi = rs.uniform( - low_bin_edges[bb_psi_idx], - up_bin_edges[bb_psi_idx], - size=bb_size) - - psi_idx[mm] = bb_psi_idx - psi[mm] = bb_psi - - return (psi_idx, psi) - - def get_ang_err_from_log_true_e_idxs_and_log_e_idxs_and_psi_idxs( - self, rs, log_true_e_idxs, log_e_idxs, psi_idxs): - if (len(log_true_e_idxs) != len(log_e_idxs)) and\ - (len(log_e_idxs) != len(psi_idxs)): - raise ValueError('The lengths of log_true_e_idxs, ' - 'log_e_idxs, and psi_idxs must be equal!') - - n_evt = len(log_true_e_idxs) - ang_err = np.empty((n_evt,), dtype=np.double) - - unique_log_true_e_idxs = np.unique(log_true_e_idxs) - for b_log_true_e_idx in unique_log_true_e_idxs: - m = log_true_e_idxs == b_log_true_e_idx - bb_unique_log_e_idxs = np.unique(log_e_idxs[m]) - for bb_log_e_idx in bb_unique_log_e_idxs: - mm = m & (log_e_idxs == bb_log_e_idx) - bbb_unique_psi_idxs = np.unique(psi_idxs[mm]) - for bbb_psi_idx in bbb_unique_psi_idxs: - mmm = mm & (psi_idxs == bbb_psi_idx) - bbb_size = np.count_nonzero(mmm) - (pdf, low_bin_edges, up_bin_edges, bin_widths) = ( - self.get_ang_err_pdf( - b_log_true_e_idx, bb_log_e_idx, bbb_psi_idx) - ) - if pdf is None: - ang_err[mmm] = np.nan - continue - - bbb_ang_err_idx = rs.choice( - np.arange(len(pdf)), - p=(pdf * bin_widths), - size=bbb_size) - bbb_ang_err = rs.uniform( - low_bin_edges[bbb_ang_err_idx], - up_bin_edges[bbb_ang_err_idx], - size=bbb_size) - - ang_err[mmm] = bbb_ang_err - - return ang_err - - def _generate_fast_n_events(self, rs, n_events): - # Initialize the output: + # Create the output event DataFieldRecordArray. out_dtype = [ - ('log_true_e', np.double), - ('log_e', np.double), - ('psi', np.double), - ('ra', np.double), + ('isvalid', np.bool_), + ('log_true_energy', np.double), + ('log_energy', np.double), ('dec', np.double), + ('ra', np.double), + ('sin_dec', np.double), ('ang_err', np.double), + ('time', int), + ('azi', np.double), + ('zen', np.double), + ('run', int) ] - events = np.empty((n_events,), dtype=out_dtype) + + data = dict( + [(out_dt[0], np.empty( + (n_events,), + dtype=out_dt[1]) + ) for out_dt in out_dtype] + ) + + events = DataFieldRecordArray(data, copy=False) + + sm = self.smearing_matrix # Determine the true energy range for which log_e PDFs are available. - m = np.sum( - (self.reco_e_upper_edges[:, self.dec_idx] - - self.reco_e_lower_edges[:, self.dec_idx] > 0), - axis=1) != 0 - min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) - max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) + (min_log_true_e, + max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pfds( + dec_idx) # First draw a true neutrino energy from the hypothesis spectrum. - log_true_e = np.log10(self.flux_model.get_inv_normed_cdf( - rs.uniform(size=n_events), + log_true_e = np.log10(flux_model.get_inv_normed_cdf( + rss.random.uniform(size=n_events), E_min=10**min_log_true_e, E_max=10**max_log_true_e )) - events['log_true_e'] = log_true_e + events['log_true_energy'] = log_true_e log_true_e_idxs = ( - np.digitize(log_true_e, bins=self.true_e_bin_edges) - 1 + np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 ) - # Get reconstructed energy given true neutrino energy. - (log_e_idxs, log_e) = self.get_log_e_from_log_true_e_idxs( - rs, log_true_e_idxs) - events['log_e'] = log_e - - # Get reconstructed psi given true neutrino energy and reconstructed energy. - (psi_idxs, psi) = self.get_psi_from_log_true_e_idxs_and_log_e_idxs( - rs, log_true_e_idxs, log_e_idxs) - events['psi'] = psi - - # Get reconstructed ang_err given true neutrino energy, reconstructed energy, - # and psi. - ang_err = self.get_ang_err_from_log_true_e_idxs_and_log_e_idxs_and_psi_idxs( - rs, log_true_e_idxs, log_e_idxs, psi_idxs) - events['ang_err'] = ang_err - - # Convert the psf into a set of (r.a. and dec.) - (ra, dec) = self.circle_parametrization(rs, psi) - events['ra'] = ra - events['dec'] = dec + # Sample reconstructed energies given true neutrino energies. + (log_e_idxs, log_e) = sm.sample_log_e( + rss, dec_idx, log_true_e_idxs) + events['log_energy'] = log_e + + # Sample reconstructed psi values given true neutrino energy and + # reconstructed energy. + (psi_idxs, psi) = sm.sample_psi( + rss, dec_idx, log_true_e_idxs, log_e_idxs) + + # Sample reconstructed ang_err values given true neutrino energy, + # reconstructed energy, and psi. + (ang_err_idxs, ang_err) = sm.sample_ang_err( + rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs) + + isvalid = np.invert( + np.isnan(log_e) | np.isnan(psi) | np.isnan(ang_err)) + events['isvalid'] = isvalid + + # Convert the psf into a set of (r.a. and dec.). Only use non-nan + # values. + (dec, ra) = psi_to_dec_and_ra(rss, src_dec, src_ra, psi[isvalid]) + events['ra'][isvalid] = ra + events['dec'][isvalid] = dec + events['sin_dec'][isvalid] = np.sin(dec) + + # Add an angular error. Only use non-nan values. + events['ang_err'][isvalid] = ang_err + + # Add fields required by the framework + events['time'] = np.ones(n_events) + events['azi'] = np.ones(n_events) + events['zen'] = np.ones(n_events) + events['run'] = -1 * np.ones(n_events) return events - def generate_fast( - self, n_events, seed=1): - rs = np.random.RandomState(seed) + def generate_signal_events( + self, rss, src_dec, src_ra, flux_model, n_events): + """Generates ``n_events`` signal events for the given source location + and flux model. + + Returns + ------- + events : numpy record array + The numpy record array holding the event data. + It contains the following data fields: + - 'isvalid' + - 'log_true_energy' + - 'log_energy' + - 'dec' + - 'ra' + - 'ang_err' + """ + sm = self.smearing_matrix + + # Find the declination bin index. + dec_idx = sm.get_dec_idx(src_dec) events = None n_evt_generated = 0 while n_evt_generated != n_events: n_evt = n_events - n_evt_generated - events_ = self._generate_fast_n_events(rs, n_evt) + events_ = self._generate_events( + rss, src_dec, src_ra, dec_idx, flux_model, n_evt) # Cut events that failed to be generated due to missing PDFs. - m = np.invert( - np.isnan(events_['log_e']) | - np.isnan(events_['psi']) | - np.isnan(events_['ang_err']) - ) - events_ = events_[m] + events_ = events_[events_['isvalid']] n_evt_generated += len(events_) if events is None: @@ -467,107 +178,113 @@ def generate_fast( return events - def _generate_n_events(self, rs, n_events): - - if not isinstance(n_events, int): - raise TypeError("The number of events must be an integer.") - if n_events < 0: - raise ValueError("The number of events must be positive!") - - # Initialize the output: - out_dtype = [ - ('log_true_e', np.double), - ('log_e', np.double), - ('psi', np.double), - ('ra', np.double), - ('dec', np.double), - ('ang_err', np.double), - ] - - if n_events == 0: - print("Warning! Zero events are being generated") - return np.array([], dtype=out_dtype) - - events = np.empty((n_events, ), dtype=out_dtype) - - # Determine the true energy range for which log_e PDFs are available. - m = np.sum( - (self.reco_e_upper_edges[:, self.dec_idx] - - self.reco_e_lower_edges[:, self.dec_idx] > 0), - axis=1) != 0 - min_log_true_e = np.min(self.true_e_bin_edges[:-1][m]) - max_log_true_e = np.max(self.true_e_bin_edges[1:][m]) - - # First draw a true neutrino energy from the hypothesis spectrum. - true_energies = np.log10(self.flux_model.get_inv_normed_cdf( - rs.uniform(size=n_events), - E_min=10**min_log_true_e, - E_max=10**max_log_true_e - )) +class PublicDataSignalGenerator(object): + """This class provides a signal generation method for a point-like source + seen in the IceCube detector using the 10 years public data release. + """ - true_e_idx = ( - np.digitize(true_energies, bins=self.true_e_bin_edges) - 1 - ) + def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhratio=None): + self.src_hypo_group_manager = src_hypo_group_manager + self.dataset_list = dataset_list + self.data_list = data_list + self.llhratio = llhratio - for i in range(n_events): - # Get a reconstructed energy according to P(E_reco | E_true) - idxs = [true_e_idx[i], None, None] - - reco_energy, reco_e_bin, reco_e_bin_centers, idxs = ( - self._get_reconstruction_from_histogram(rs, idxs) - ) - if reco_energy is not None: - # Get an opening angle according to P(psf | E_true,E_reco). - psf, psf_bin, psf_bin_centers, idxs = ( - self._get_reconstruction_from_histogram( - rs, idxs, reco_e_bin, reco_e_bin_centers - ) - ) + self.sig_gen_list = [] + for ds in self._dataset_list: + self.sig_gen_list.append(PublicDataDatasetSignalGenerator(ds)) - if psf is not None: - # Get an angular error according to P(ang_err | E_true,E_reco,psf). - ang_err, ang_err_bin, ang_err_bin_centers, idxs = ( - self._get_reconstruction_from_histogram( - rs, idxs, psf_bin, psf_bin_centers + @property + def src_hypo_group_manager(self): + """The SourceHypoGroupManager instance defining the source groups with + their spectra. + """ + return self._src_hypo_group_manager + + @src_hypo_group_manager.setter + def src_hypo_group_manager(self, manager): + if(not isinstance(manager, SourceHypoGroupManager)): + raise TypeError('The src_hypo_group_manager property must be an ' + 'instance of SourceHypoGroupManager!') + self._src_hypo_group_manager = manager + + @property + def dataset_list(self): + """The list of Dataset instances for which signal events should get + generated for. + """ + return self._dataset_list + + @dataset_list.setter + def dataset_list(self, datasets): + if(not issequenceof(datasets, Dataset)): + raise TypeError('The dataset_list property must be a sequence of ' + 'Dataset instances!') + self._dataset_list = list(datasets) + + @property + def llhratio(self): + """The log-likelihood ratio function for the analysis. + """ + return self._llhratio + + @llhratio.setter + def llhratio(self, llhratio): + if llhratio is not None: + if(not isinstance(llhratio, LLHRatio)): + raise TypeError('The llratio property must be an instance of ' + 'LLHRatio!') + self._llhratio = llhratio + + def generate_signal_events(self, rss, mean, poisson=True): + shg_list = self._src_hypo_group_manager.src_hypo_group_list + + tot_n_events = 0 + signal_events_dict = {} + + for shg in shg_list: + # This only works with power-laws for now. + # Each source hypo group can have a different power-law + gamma = shg.fluxmodel.gamma + weights, _ = self.llhratio.dataset_signal_weights([mean, gamma]) + src_list = shg.source_list + for (ds_idx, (sig_gen, w)) in enumerate(zip(self.sig_gen_list, weights)): + w_mean = mean * w + if(poisson): + n_events = rss.random.poisson( + float_cast( + w_mean, + '`mean` must be castable to type of float!' ) ) - - # Convert the psf set of (r.a. and dec.) - ra, dec = self.circle_parametrization(rs, psf) - - events[i] = (true_energies[i], reco_energy, - psf, ra, dec, ang_err) else: - events[i] = (true_energies[i], reco_energy, - np.nan, np.nan, np.nan, np.nan) - else: - events[i] = (true_energies[i], np.nan, - np.nan, np.nan, np.nan, np.nan) - - return events - - def generate(self, n_events, seed=1): - rs = np.random.RandomState(seed) - - events = None - n_evt_generated = 0 - while n_evt_generated != n_events: - n_evt = n_events - n_evt_generated - - events_ = self._generate_n_events(rs, n_evt) + n_events = int_cast( + w_mean, + '`mean` must be castable to type of float!' + ) + tot_n_events += n_events + + events_ = None + for (shg_src_idx, src) in enumerate(src_list): + # ToDo: here n_events should be split according to some + # source weight + events_ = sig_gen.generate_signal_events( + rss, + src.dec, + src.ra, + shg.fluxmodel, + n_events + ) + if events_ is None: + continue + events_.append_field( + "ds_idx", np.repeat([ds_idx], len(events_))) - # Cut events that failed to be generated due to missing PDFs. - m = np.invert( - np.isnan(events_['log_e']) | - np.isnan(events_['psi']) | - np.isnan(events_['ang_err']) - ) - events_ = events_[m] - n_evt_generated += len(events_) - if events is None: - events = events_ - else: - events = np.concatenate((events, events_)) + if shg_src_idx == 0: + signal_events_dict[ds_idx] = events_[ + events_['ds_idx'] == ds_idx] + else: + signal_events_dict[ds_idx].append( + events_[events_['ds_idx'] == ds_idx]) - return events + return tot_n_events, signal_events_dict From f985aad23d043801a14db1727d44ef6fd93c05d0 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 27 Apr 2022 17:27:10 +0200 Subject: [PATCH 040/274] Add function to calculate bin centers from bin edges --- skyllh/core/binning.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index c3150fae10..097d14bfce 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -143,6 +143,22 @@ def rebin( return new_bincontent +def get_bincenters_from_binedges(edges): + """Calculates the bin center values from the given bin edge values. + + Parameters + ---------- + edges : 1D numpy ndarray + The (n+1,)-shaped 1D ndarray holding the bin edge values. + + Returns + ------- + bincenters : 1D numpy ndarray + The (n,)-shaped 1D ndarray holding the bin center values. + """ + return 0.5*(edges[:-1] + edges[1:]) + + class BinningDefinition(object): """The BinningDefinition class provides a structure to hold histogram binning definitions for an analyis. From 0dbcf7c0a1a5290571713e9d26d6b51811f0e545 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 27 Apr 2022 17:29:04 +0200 Subject: [PATCH 041/274] Add setting for number of mc events --- skyllh/analyses/i3/trad_ps/analysis.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index bf3638af3e..a492875ed4 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -100,6 +100,7 @@ def create_analysis( refplflux_gamma=2, ns_seed=10.0, gamma_seed=3, + n_mc_events=int(1e7), compress_data=False, keep_data_fields=None, optimize_delta_angle=10, @@ -252,7 +253,7 @@ def create_analysis( ds=ds, flux_model=fluxmodel, fitparam_grid_set=gamma_grid, - n_events=int(1e7), + n_events=n_mc_events, smoothing_filter=smoothing_filter, ppbar=pbar) energy_bkgpdf = DataBackgroundI3EnergyPDF( @@ -300,6 +301,9 @@ def create_analysis( p.add_argument("--ncpu", default=1, type=int, help='The number of CPUs to utilize where parallelization is possible.' ) + p.add_argument("--n-mc-events", default=int(1e7), type=int, + help='The number of MC events to sample for the energy signal PDF.' + ) args = p.parse_args() # Setup `skyllh` package logging. @@ -315,10 +319,10 @@ def create_analysis( CFG['multiproc']['ncpu'] = args.ncpu sample_seasons = [ - ('PublicData_10y_ps', 'IC40'), - ('PublicData_10y_ps', 'IC59'), - ('PublicData_10y_ps', 'IC79'), - ('PublicData_10y_ps', 'IC86_I'), + #('PublicData_10y_ps', 'IC40'), + #('PublicData_10y_ps', 'IC59'), + #('PublicData_10y_ps', 'IC79'), + #('PublicData_10y_ps', 'IC86_I'), ('PublicData_10y_ps', 'IC86_II-VII') ] @@ -343,6 +347,7 @@ def create_analysis( rss_pdf, datasets, source, + n_mc_events=args.n_mc_events, gamma_seed=args.gamma_seed, tl=tl) From 26fe532b06921b069e354d749b7e7ea30d61ef70 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 27 Apr 2022 17:32:50 +0200 Subject: [PATCH 042/274] Added function to create unionized matrix array. Also rename pdf to psi --- skyllh/analyses/i3/trad_ps/utils.py | 306 ++++++++++++++++++++++++---- 1 file changed, 264 insertions(+), 42 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index e31983529a..550b71f035 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -2,6 +2,9 @@ import numpy as np +from skyllh.core.binning import ( + get_bincenters_from_binedges +) from skyllh.core.storage import create_FileLoader @@ -104,7 +107,7 @@ def load_smearing_histogram(pathfilenames): histogram : 5d ndarray The 5d histogram array holding the probability values of the smearing matrix. - The axes are (true_e, true_dec, reco_e, psf, ang_err). + The axes are (true_e, true_dec, reco_e, psi, ang_err). true_e_bin_edges : 1d ndarray The ndarray holding the bin edges of the true energy axis. true_dec_bin_edges : 1d ndarray @@ -119,18 +122,18 @@ def load_smearing_histogram(pathfilenames): For each pair of true_e and true_dec different reco energy bin edges are provided. The shape is (n_true_e, n_true_dec, n_reco_e). - psf_lower_edges : 4d ndarray - The 4d ndarray holding the lower bin edges of the PSF axis. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psf). - psf_upper_edges : 4d ndarray - The 4d ndarray holding the upper bin edges of the PSF axis. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psf). + psi_lower_edges : 4d ndarray + The 4d ndarray holding the lower bin edges of the psi axis. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). + psi_upper_edges : 4d ndarray + The 4d ndarray holding the upper bin edges of the psi axis. + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). ang_err_lower_edges : 5d ndarray The 5d ndarray holding the lower bin edges of the angular error axis. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err). + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). ang_err_upper_edges : 5d ndarray The 5d ndarray holding the upper bin edges of the angular error axis. - The shape is (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err). + The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). """ # Load the smearing data from the public dataset. loader = create_FileLoader(pathfilenames=pathfilenames) @@ -143,8 +146,8 @@ def load_smearing_histogram(pathfilenames): 'Dec_nu_max[deg]': 'true_dec_max', 'log10(E/GeV)_min': 'e_min', 'log10(E/GeV)_max': 'e_max', - 'PSF_min[deg]': 'psf_min', - 'PSF_max[deg]': 'psf_max', + 'PSF_min[deg]': 'psi_min', + 'PSF_max[deg]': 'psi_max', 'AngErr_min[deg]': 'ang_err_min', 'AngErr_max[deg]': 'ang_err_max', 'Fractional_Counts': 'norm_counts' @@ -179,15 +182,15 @@ def _get_nbins_from_edges(lower_edges, upper_edges): n_reco_e = _get_nbins_from_edges( data['e_min'], data['e_max']) - n_psf = _get_nbins_from_edges( - data['psf_min'], data['psf_max']) + n_psi = _get_nbins_from_edges( + data['psi_min'], data['psi_max']) n_ang_err = _get_nbins_from_edges( data['ang_err_min'], data['ang_err_max']) # Get reco energy bin_edges as a 3d array. idxs = np.array( range(len(data)) - ) % (n_psf * n_ang_err) == 0 + ) % (n_psi * n_ang_err) == 0 reco_e_lower_edges = np.reshape( data['e_min'][idxs], @@ -198,31 +201,30 @@ def _get_nbins_from_edges(lower_edges, upper_edges): (n_true_e, n_true_dec, n_reco_e) ) - # Get psf bin_edges as a 4d array. + # Get psi bin_edges as a 4d array. idxs = np.array( range(len(data)) ) % n_ang_err == 0 - psf_lower_edges = np.reshape( - data['psf_min'][idxs], - (n_true_e, n_true_dec, n_reco_e, n_psf) + psi_lower_edges = np.reshape( + data['psi_min'][idxs], + (n_true_e, n_true_dec, n_reco_e, n_psi) ) - psf_upper_edges = np.reshape( - data['psf_max'][idxs], - (n_true_e, n_true_dec, n_reco_e, n_psf) + psi_upper_edges = np.reshape( + data['psi_max'][idxs], + (n_true_e, n_true_dec, n_reco_e, n_psi) ) # Get angular error bin_edges as a 5d array. ang_err_lower_edges = np.reshape( data['ang_err_min'], - (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err) + (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err) ) ang_err_upper_edges = np.reshape( data['ang_err_max'], - (n_true_e, n_true_dec, n_reco_e, n_psf, n_ang_err) + (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err) ) - # Create 5D histogram for the probabilities. histogram = np.reshape( data['norm_counts'], @@ -230,7 +232,7 @@ def _get_nbins_from_edges(lower_edges, upper_edges): n_true_e, n_true_dec, n_reco_e, - n_psf, + n_psi, n_ang_err ) ) @@ -241,8 +243,8 @@ def _get_nbins_from_edges(lower_edges, upper_edges): true_dec_bin_edges, reco_e_lower_edges, reco_e_upper_edges, - psf_lower_edges, - psf_upper_edges, + psi_lower_edges, + psi_upper_edges, ang_err_lower_edges, ang_err_upper_edges ) @@ -307,6 +309,92 @@ def psi_to_dec_and_ra(rss, src_dec, src_ra, psi): return (dec, ra) +def create_unionized_smearing_matrix_array(sm, src_dec): + """Creates a unionized smearing matrix array which covers the entire + observable space by keeping all original bins. + + Parameters + ---------- + sm : PublicDataSmearingMatrix instance + The PublicDataSmearingMatrix instance that holds the smearing matrix + data. + src_dec : float + The source declination in radians. + + Returns + ------- + arr : (nbins_true_e, nbins_reco_e, nbins_psi, nbins_ang_err)-shaped + 4D numpy ndarray + """ + true_dec_idx = sm.get_true_dec_idx(src_dec) + + true_e_bincenters = get_bincenters_from_binedges( + sm.true_e_bin_edges) + nbins_true_e = len(sm.true_e_bin_edges) - 1 + + # Determine the unionized bin edges along all dimensions. + reco_e_edges = np.unique(np.concatenate(( + sm.reco_e_lower_edges[:,true_dec_idx,...].flatten(), + sm.reco_e_upper_edges[:,true_dec_idx,...].flatten() + ))) + reco_e_bincenters = get_bincenters_from_binedges(reco_e_edges) + nbins_reco_e = len(reco_e_edges) - 1 + + psi_edges = np.unique(np.concatenate(( + sm.psi_lower_edges[:,true_dec_idx,...].flatten(), + sm.psi_upper_edges[:,true_dec_idx,...].flatten() + ))) + psi_bincenters = get_bincenters_from_binedges(psi_edges) + nbins_psi = len(psi_edges) - 1 + + ang_err_edges = np.unique(np.concatenate(( + sm.ang_err_lower_edges[:,true_dec_idx,...].flatten(), + sm.ang_err_upper_edges[:,true_dec_idx,...].flatten() + ))) + ang_err_bincenters = get_bincenters_from_binedges(ang_err_edges) + nbins_ang_err = len(ang_err_edges) - 1 + + # Create the unionized pdf array, which contains an axis for the + # true energy bins. + arr = np.zeros( + (nbins_true_e, nbins_reco_e, nbins_psi, nbins_ang_err), dtype=np.double) + # Fill the 4D array. + for (true_e_idx, true_e) in enumerate(true_e_bincenters): + for (e_idx, e) in enumerate(reco_e_bincenters): + # Get the bin index of reco_e in the smearing matrix. + sm_e_idx = sm.get_reco_e_idx( + true_e_idx, true_dec_idx, e) + if sm_e_idx is None: + continue + for (p_idx, p) in enumerate(psi_bincenters): + # Get the bin index of psi in the smearing matrix. + sm_p_idx = sm.get_psi_idx( + true_e_idx, true_dec_idx, sm_e_idx, p) + if sm_p_idx is None: + continue + for (a_idx, a) in enumerate(ang_err_bincenters): + # Get the bin index of the angular error in the + # smearing matrix. + sm_a_idx = sm.get_ang_err_idx( + true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx, a) + if sm_a_idx is None: + continue + + arr[ + true_e_idx, + e_idx, + p_idx, + a_idx + ] = sm.histogram[ + true_e_idx, + true_dec_idx, + sm_e_idx, + sm_p_idx, + sm_a_idx + ] + + return arr + class PublicDataAeff(object): """This class is a helper class for dealing with the effective area @@ -384,18 +472,49 @@ def __init__( ( self.histogram, - self.true_e_bin_edges, - self.true_dec_bin_edges, + self._true_e_bin_edges, + self._true_dec_bin_edges, self.reco_e_lower_edges, self.reco_e_upper_edges, - self.psf_lower_edges, - self.psf_upper_edges, + self.psi_lower_edges, + self.psi_upper_edges, self.ang_err_lower_edges, self.ang_err_upper_edges ) = load_smearing_histogram(pathfilenames) - def get_dec_idx(self, dec): - """Returns the declination index for the given declination value. + @property + def true_e_bin_edges(self): + """(read-only) The (n_true_e+1,)-shaped 1D numpy ndarray holding the + bin edges of the true energy. + """ + return self._true_e_bin_edges + + @property + def true_e_bin_centers(self): + """(read-only) The (n_true_e,)-shaped 1D numpy ndarray holding the bin + center values of the true energy. + """ + return 0.5*(self._true_e_bin_edges[:-1] + + self._true_e_bin_edges[1:]) + + @property + def true_dec_bin_edges(self): + """(read-only) The (n_true_dec+1,)-shaped 1D numpy ndarray holding the + bin edges of the true declination. + """ + return self._true_dec_bin_edges + + @property + def true_dec_bin_centers(self): + """(read-only) The (n_true_dec,)-shaped 1D ndarray holding the bin + center values of the true declination. + """ + return 0.5*(self._true_dec_bin_edges[:-1] + + self._true_dec_bin_edges[1:]) + + def get_true_dec_idx(self, true_dec): + """Returns the true declination index for the given true declination + value. Parameters ---------- @@ -404,19 +523,122 @@ def get_dec_idx(self, dec): Returns ------- - dec_idx : int + true_dec_idx : int The index of the declination bin for the given declination value. """ - dec = np.degrees(dec) + true_dec = np.degrees(true_dec) - if (dec < self.true_dec_bin_edges[0]) or\ - (dec > self.true_dec_bin_edges[-1]): + if (true_dec < self.true_dec_bin_edges[0]) or\ + (true_dec > self.true_dec_bin_edges[-1]): raise ValueError('The declination {} degrees is not supported by ' - 'the smearing matrix!'.format(dec)) + 'the smearing matrix!'.format(true_dec)) + + true_dec_idx = np.digitize(true_dec, self.true_dec_bin_edges) - 1 + + return true_dec_idx + + def get_reco_e_idx(self, true_e_idx, true_dec_idx, reco_e): + """Returns the bin index for the given reco energy value given the + given true energy and true declination bin indices. + + Parameters + ---------- + true_e_idx : int + The index of the true energy bin. + true_dec_idx : int + The index of the true declination bin. + reco_e : float + The reco energy value for which the bin index should get returned. + + Returns + ------- + reco_e_idx : int | None + The index of the reco energy bin the given reco energy value falls + into. It returns None if the value is out of range. + """ + lower_edges = self.reco_e_lower_edges[true_e_idx,true_dec_idx] + upper_edges = self.reco_e_upper_edges[true_e_idx,true_dec_idx] + + m = (lower_edges <= reco_e) & (upper_edges > reco_e) + idxs = np.nonzero(m)[0] + if(len(idxs) == 0): + return None + + reco_e_idx = idxs[0] + + return reco_e_idx + + def get_psi_idx(self, true_e_idx, true_dec_idx, reco_e_idx, psi): + """Returns the bin index for the given psi value given the + true energy, true declination and reco energy bin indices. + + Parameters + ---------- + true_e_idx : int + The index of the true energy bin. + true_dec_idx : int + The index of the true declination bin. + reco_e_idx : int + The index of the reco energy bin. + psi : float + The psi value for which the bin index should get returned. + + Returns + ------- + psi_idx : int | None + The index of the psi bin the given psi value falls into. + It returns None if the value is out of range. + """ + lower_edges = self.psi_lower_edges[true_e_idx,true_dec_idx,reco_e_idx] + upper_edges = self.psi_upper_edges[true_e_idx,true_dec_idx,reco_e_idx] + + m = (lower_edges <= psi) & (upper_edges > psi) + idxs = np.nonzero(m)[0] + if(len(idxs) == 0): + return None + + psi_idx = idxs[0] + + return psi_idx + + def get_ang_err_idx( + self, true_e_idx, true_dec_idx, reco_e_idx, psi_idx, ang_err): + """Returns the bin index for the given angular error value given the + true energy, true declination, reco energy, and psi bin indices. + + Parameters + ---------- + true_e_idx : int + The index of the true energy bin. + true_dec_idx : int + The index of the true declination bin. + reco_e_idx : int + The index of the reco energy bin. + psi_idx : int + The index of the psi bin. + ang_err : float + The angular error value for which the bin index should get + returned. + + Returns + ------- + ang_err_idx : int | None + The index of the angular error bin the given angular error value + falls into. It returns None if the value is out of range. + """ + lower_edges = self.ang_err_lower_edges[ + true_e_idx,true_dec_idx,reco_e_idx,psi_idx] + upper_edges = self.ang_err_upper_edges[ + true_e_idx,true_dec_idx,reco_e_idx,psi_idx] + + m = (lower_edges <= ang_err) & (upper_edges > ang_err) + idxs = np.nonzero(m)[0] + if(len(idxs) == 0): + return None - dec_idx = np.digitize(dec, self.true_dec_bin_edges) - 1 + ang_err_idx = idxs[0] - return dec_idx + return ang_err_idx def get_true_log_e_range_with_valid_log_e_pfds(self, dec_idx): """Determines the true log energy range for which log_e PDFs are @@ -528,10 +750,10 @@ def get_psi_pdf( return (None, None, None, None) # Get the PSI bin edges and widths. - lower_bin_edges = self.psf_lower_edges[ + lower_bin_edges = self.psi_lower_edges[ log_true_e_idx, dec_idx, log_e_idx ] - upper_bin_edges = self.psf_upper_edges[ + upper_bin_edges = self.psi_upper_edges[ log_true_e_idx, dec_idx, log_e_idx ] bin_widths = upper_bin_edges - lower_bin_edges From 4f88d77d415f0bc6258532c2e4e0d3be2ccbaa7b Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 27 Apr 2022 17:44:09 +0200 Subject: [PATCH 043/274] Started signal pdf set with unionized smearing matrix --- skyllh/analyses/i3/trad_ps/signalpdf.py | 90 +++++++++++++++++++++++-- 1 file changed, 86 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index b6c54ed8d4..980d7edfdc 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -3,11 +3,13 @@ import numpy as np from copy import deepcopy from scipy.interpolate import UnivariateSpline +from itertools import product from skyllh.core.timing import TaskTimer from skyllh.core.binning import ( BinningDefinition, - UsesBinning + UsesBinning, + get_bincenters_from_binedges ) from skyllh.core.storage import DataFieldRecordArray from skyllh.core.pdf import ( @@ -485,8 +487,8 @@ def __init__( ncpu=ncpu) def create_I3EnergyPDF( - rss, ds, logE_binning, sinDec_binning, smoothing_filter, - aeff, siggen, flux_model, n_events, gridfitparams): + logE_binning, sinDec_binning, smoothing_filter, + aeff, siggen, flux_model, n_events, gridfitparams, rss): # Create a copy of the FluxModel with the given flux parameters. # The copy is needed to not interfer with other CPU processes. my_flux_model = flux_model.copy(newprop=gridfitparams) @@ -547,7 +549,7 @@ def create_I3EnergyPDF( sinDec_binning = ds.get_binning_definition('sin_dec') args_list = [ - ((rss, ds, logE_binning, sinDec_binning, smoothing_filter, aeff, + ((logE_binning, sinDec_binning, smoothing_filter, aeff, siggen, flux_model, n_events, gridfitparams), {}) for gridfitparams in self.gridfitparams_list ] @@ -556,6 +558,7 @@ def create_I3EnergyPDF( create_I3EnergyPDF, args_list, self.ncpu, + rss=rss, ppbar=ppbar) # Save all the energy PDF objects in the PDFSet PDF registry with @@ -608,3 +611,82 @@ def get_prob(self, tdm, gridfitparams): return prob +class PublicDataSignalPDFSet(PDFSet, IsSignalPDF, IsParallelizable): + """This class provides a signal PDF set for the public data. + """ + def __init__( + self, + ds, + src_dec, + flux_model, + fitparam_grid_set, + union_sm_arr_pathfilename=None, + ncpu=None, + ppbar=None, + **kwargs): + """Creates a new PublicDataSignalPDFSet instance for the public data. + """ + super().__init__( + pdf_type=PDF, + fitparams_grid_set=fitparam_grid_set, + ncpu=ncpu + ) + + sm = PublicDataSmearingMatrix( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) + + if(union_sm_arr_pathfilename is not None): + pdf_arr = np.load(union_sm_arr_pathfilename) + else: + pdf_arr = create_unionized_smearing_matrix_array(sm, src_dec) + + print('pdf_arr.shape={}'.format(str(pdf_arr.shape))) + # Create the pdf in gamma for different gamma values. + def create_pdf(pdf_arr, flux_model, gridfitparams): + """Creates a pdf for a specific gamma value. + """ + # Create a copy of the FluxModel with the given flux parameters. + # The copy is needed to not interfer with other CPU processes. + my_flux_model = flux_model.copy(newprop=gridfitparams) + + E_nu = np.power(10, sm.true_e_bin_centers) + flux = my_flux_model(E_nu) + print(flux) + dE_nu = np.diff(sm.true_e_bin_edges) + print(dE_nu) + arr_ = np.copy(pdf_arr) + for true_e_idx in range(pdf_arr.shape[0]): + arr_[true_e_idx] *= flux[true_e_idx] * dE_nu[true_e_idx] + pdf = np.sum(arr_, axis=0) + del(arr_) + + # Normalize the pdf. + norm = np.sum(pdf) + if norm == 0: + raise ValueError('The signal PDF is empty for {}! This should ' + 'not happen. Check the parameter ranges!'.format( + str(gridfitparams))) + pdf /= norm + + return pdf + """ + args_list = [ + ((pdf_arr, flux_model, gridfitparams), {}) + for gridfitparams in self.gridfitparams_list + ] + + self.pdf_list = parallelize( + create_pdf, + args_list, + ncpu=self.ncpu, + ppbar=ppbar) + """ + self.pdf_list = [ + create_pdf(pdf_arr, flux_model, gridfitparams={'gamma': 2}) + ] + + del(pdf_arr) + + + From 6dec9b7c88f8929f37a68fa788996b0ca9fd971d Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 27 Apr 2022 18:46:18 +0200 Subject: [PATCH 044/274] Also return the bin edges --- skyllh/analyses/i3/trad_ps/utils.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 550b71f035..3da4e368b4 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -325,6 +325,15 @@ def create_unionized_smearing_matrix_array(sm, src_dec): ------- arr : (nbins_true_e, nbins_reco_e, nbins_psi, nbins_ang_err)-shaped 4D numpy ndarray + The 4D ndarray holding the smearing matrix values. + true_e_bin_edges : 1D numpy ndarray + The unionized bin edges of the true energy axis. + reco_e_edges : 1D numpy ndarray + The unionized bin edges of the reco energy axis. + psi_edges : 1D numpy ndarray + The unionized bin edges of psi axis. + ang_err_edges : 1D numpy ndarray + The unionized bin edges of the angular error axis. """ true_dec_idx = sm.get_true_dec_idx(src_dec) @@ -393,7 +402,13 @@ def create_unionized_smearing_matrix_array(sm, src_dec): sm_a_idx ] - return arr + return ( + arr, + sm.true_e_bin_edges, + reco_e_edges, + psi_edges, + ang_err_edges + ) class PublicDataAeff(object): From 454b87658c63ce5a1ebd5160715e2890afb3e7a9 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 27 Apr 2022 18:48:06 +0200 Subject: [PATCH 045/274] load the unionized smearing matrix from a file and normalize the pdf to probability per bin volume --- skyllh/analyses/i3/trad_ps/signalpdf.py | 39 ++++++++++++++++++++----- 1 file changed, 31 insertions(+), 8 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 980d7edfdc..f49a6d9194 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1,6 +1,9 @@ # -*- coding: utf-8 -*- import numpy as np + +import pickle + from copy import deepcopy from scipy.interpolate import UnivariateSpline from itertools import product @@ -632,16 +635,35 @@ def __init__( ncpu=ncpu ) - sm = PublicDataSmearingMatrix( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('smearing_datafile'))) - if(union_sm_arr_pathfilename is not None): - pdf_arr = np.load(union_sm_arr_pathfilename) + with open(union_sm_arr_pathfilename, 'rb') as f: + data = pickle.load(f) + pdf_arr = data['arr'] + true_e_bin_edges = data['true_e_bin_edges'] + reco_e_edges = data['reco_e_edges'] + psi_edges = data['psi_edges'] + ang_err_edges = data['ang_err_edges'] else: - pdf_arr = create_unionized_smearing_matrix_array(sm, src_dec) + sm = PublicDataSmearingMatrix( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) + (pdf_arr, + true_e_bin_edges, + reco_e_edges, + psi_edges, + ang_err_edges + ) = create_unionized_smearing_matrix_array(sm, src_dec) + + reco_e_bw = np.diff(reco_e_edges) + psi_edges_bw = np.diff(psi_edges) + ang_err_bw = np.diff(ang_err_edges) + self.bin_volumes = ( + reco_e_bw[:,np.newaxis,np.newaxis] * + psi_edges_bw[np.newaxis,:,np.newaxis] * + ang_err_bw[np.newaxis,np.newaxis,:]) print('pdf_arr.shape={}'.format(str(pdf_arr.shape))) + true_e_bin_centers = get_bincenters_from_binedges(true_e_bin_edges) # Create the pdf in gamma for different gamma values. def create_pdf(pdf_arr, flux_model, gridfitparams): """Creates a pdf for a specific gamma value. @@ -650,10 +672,10 @@ def create_pdf(pdf_arr, flux_model, gridfitparams): # The copy is needed to not interfer with other CPU processes. my_flux_model = flux_model.copy(newprop=gridfitparams) - E_nu = np.power(10, sm.true_e_bin_centers) + E_nu = np.power(10, true_e_bin_centers) flux = my_flux_model(E_nu) print(flux) - dE_nu = np.diff(sm.true_e_bin_edges) + dE_nu = np.diff(true_e_bin_edges) print(dE_nu) arr_ = np.copy(pdf_arr) for true_e_idx in range(pdf_arr.shape[0]): @@ -668,6 +690,7 @@ def create_pdf(pdf_arr, flux_model, gridfitparams): 'not happen. Check the parameter ranges!'.format( str(gridfitparams))) pdf /= norm + pdf /= self.bin_volumes return pdf """ From 6c27626508e62022576ce1e4d9518a510c0efe35 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 28 Apr 2022 15:02:51 +0200 Subject: [PATCH 046/274] Removed unused index. --- skyllh/analyses/i3/trad_ps/signal_generator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index 56566c8b8f..a805e5cf44 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -277,14 +277,10 @@ def generate_signal_events(self, rss, mean, poisson=True): ) if events_ is None: continue - events_.append_field( - "ds_idx", np.repeat([ds_idx], len(events_))) if shg_src_idx == 0: - signal_events_dict[ds_idx] = events_[ - events_['ds_idx'] == ds_idx] + signal_events_dict[ds_idx] = events_ else: - signal_events_dict[ds_idx].append( - events_[events_['ds_idx'] == ds_idx]) + signal_events_dict[ds_idx].append(events_) return tot_n_events, signal_events_dict From f2683bb20df17cc08f5561f7cba885a74eb1966c Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 28 Apr 2022 16:32:40 +0200 Subject: [PATCH 047/274] Updated method name. --- skyllh/analyses/i3/trad_ps/signal_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index a805e5cf44..88a8d686be 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -157,7 +157,7 @@ def generate_signal_events( sm = self.smearing_matrix # Find the declination bin index. - dec_idx = sm.get_dec_idx(src_dec) + dec_idx = sm.get_true_dec_idx(src_dec) events = None n_evt_generated = 0 From de1cace3fc9a5433bed899244aa2f6088adda069 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 28 Apr 2022 16:34:09 +0200 Subject: [PATCH 048/274] Added function to determine bin indices from bin edges --- skyllh/core/binning.py | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index 097d14bfce..240977d9c7 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -159,6 +159,40 @@ def get_bincenters_from_binedges(edges): return 0.5*(edges[:-1] + edges[1:]) +def get_bin_indices_from_lower_and_upper_binedges(le, ue, values): + """Returns the bin indices for the given lower and upper bin edges the given + values fall into. + + Parameters + ---------- + le : 1D numpy ndarray + The lower bin edges. + ue : 1D numpy ndarray + The upper bin edges. + values : 1D numpy ndarray + The values for which to get the bin indices. + + Returns + ------- + idxs : 1D numpy ndarray + The bin indices of the given values. + """ + if np.any(values < le[0]): + raise ValueError( + 'At least one value is smaller than the lowest bin edge!') + if np.any(values > ue[-1]): + raise ValueError( + 'At least one value is larger than the largest bin edge!') + + m = ( + (v[:,np.newaxis] >= le[np.newaxis,:]) & + (v[:,np.newaxis] < ue[np.newaxis,:]) + ) + idxs = np.nonzero(m)[1] + + return idxs + + class BinningDefinition(object): """The BinningDefinition class provides a structure to hold histogram binning definitions for an analyis. From 9d79c3deb50a2f007fcf16a0d06a73d5b3d3a74c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 28 Apr 2022 16:35:07 +0200 Subject: [PATCH 049/274] Continue work on PDSignalPDF --- skyllh/analyses/i3/trad_ps/signalpdf.py | 132 +++++++++++++++++++----- 1 file changed, 109 insertions(+), 23 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index f49a6d9194..46bfb89ac3 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -12,7 +12,8 @@ from skyllh.core.binning import ( BinningDefinition, UsesBinning, - get_bincenters_from_binedges + get_bincenters_from_binedges, + get_bin_indices_from_lower_and_upper_binedges ) from skyllh.core.storage import DataFieldRecordArray from skyllh.core.pdf import ( @@ -34,6 +35,7 @@ from skyllh.i3.dataset import I3Dataset from skyllh.physics.flux import FluxModel from skyllh.analyses.i3.trad_ps.utils import ( + create_unionized_smearing_matrix_array, load_smearing_histogram, psi_to_dec_and_ra, PublicDataAeff, @@ -152,7 +154,7 @@ def generate_signal_events( sm = self.smearing_matrix # Find the declination bin index. - dec_idx = sm.get_dec_idx(src_dec) + dec_idx = sm.get_true_dec_idx(src_dec) events = None n_evt_generated = 0 @@ -614,7 +616,85 @@ def get_prob(self, tdm, gridfitparams): return prob -class PublicDataSignalPDFSet(PDFSet, IsSignalPDF, IsParallelizable): +class PDSignalPDF(PDF, IsSignalPDF): + """This class provides a signal pdf for a given spectrial index value. + """ + def __init__( + self, pdf_arr, log_e_edges, psi_edges, ang_err_edges, **kwargs): + """Creates a new signal PDF for the public data. + """ + super().__init__(**kwargs) + + self.pdf_arr = pdf_arr + + #self.log_e_edges = log_e_edges + self.log_e_lower_edges = log_e_edges[:-1] + self.log_e_upper_edges = log_e_edges[1:] + + #self.psi_edges = psi_edges + self.psi_lower_edges = psi_edges[:-1] + self.psi_upper_edges = psi_edges[1:] + + #self.ang_err_edges = ang_err_edges + self.ang_err_lower_edges = ang_err_edges[:-1] + self.ang_err_upper_edges = ang_err_edges[1:] + + def assert_is_valid_for_trial_data(self, tdm): + pass + + def get_prob(self, tdm, params=None, tl=None): + """Looks up the probability density for the events given by the + TrialDataManager. + + Parameters + ---------- + tdm : TrialDataManager instance + The TrialDataManager instance holding the data events for which the + probability should be looked up. The following data fields are + required: + - 'log_energy' + The log10 of the reconstructed energy. + - 'psi' + The opening angle from the source to the event in radians. + - 'ang_err' + The angular error of the event in radians. + params : dict | None + The dictionary containing the parameter names and values for which + the probability should get calculated. + By definition this PDF does not depend on parameters. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + prob : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. + grads : (N_fitparams,N_events)-shaped ndarray | None + The 2D numpy ndarray holding the gradients of the PDF w.r.t. + each fit parameter for each event. The order of the gradients + is the same as the order of floating parameters specified through + the ``param_set`` property. + It is ``None``, if this PDF does not depend on any parameters. + """ + log_e = tdm.get_data('log_energy') + psi = tdm.get_data('psi') + ang_err = tdm.get_data('ang_err') + + log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( + self.log_e_lower_edges, self.log_e_upper_edges, log_e) + psi_idxs = get_bin_indices_from_lower_and_upper_binedges( + self.psi_lower_edges, self.psi_upper_edges, psi) + ang_err_idxs = get_bin_indices_from_lower_and_upper_binedges( + self.ang_err_lower_edges, self.ang_err_upper_edges, ang_err) + + idxs = tuple(zip(log_e_idxs, psi_idxs, ang_err_idxs)) + prob = self.pdf_arr[idxs] + + return (prob, None) + + +class PDSignalPDFSet(PDFSet, IsSignalPDF, IsParallelizable): """This class provides a signal PDF set for the public data. """ def __init__( @@ -638,7 +718,7 @@ def __init__( if(union_sm_arr_pathfilename is not None): with open(union_sm_arr_pathfilename, 'rb') as f: data = pickle.load(f) - pdf_arr = data['arr'] + union_arr = data['arr'] true_e_bin_edges = data['true_e_bin_edges'] reco_e_edges = data['reco_e_edges'] psi_edges = data['psi_edges'] @@ -647,7 +727,7 @@ def __init__( sm = PublicDataSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - (pdf_arr, + (union_arr, true_e_bin_edges, reco_e_edges, psi_edges, @@ -657,15 +737,14 @@ def __init__( reco_e_bw = np.diff(reco_e_edges) psi_edges_bw = np.diff(psi_edges) ang_err_bw = np.diff(ang_err_edges) - self.bin_volumes = ( + bin_volumes = ( reco_e_bw[:,np.newaxis,np.newaxis] * psi_edges_bw[np.newaxis,:,np.newaxis] * ang_err_bw[np.newaxis,np.newaxis,:]) - print('pdf_arr.shape={}'.format(str(pdf_arr.shape))) true_e_bin_centers = get_bincenters_from_binedges(true_e_bin_edges) # Create the pdf in gamma for different gamma values. - def create_pdf(pdf_arr, flux_model, gridfitparams): + def create_pdf(union_arr, flux_model, gridfitparams): """Creates a pdf for a specific gamma value. """ # Create a copy of the FluxModel with the given flux parameters. @@ -674,28 +753,32 @@ def create_pdf(pdf_arr, flux_model, gridfitparams): E_nu = np.power(10, true_e_bin_centers) flux = my_flux_model(E_nu) - print(flux) dE_nu = np.diff(true_e_bin_edges) - print(dE_nu) - arr_ = np.copy(pdf_arr) - for true_e_idx in range(pdf_arr.shape[0]): + + arr_ = np.copy(union_arr) + for true_e_idx in range(len(true_e_bin_centers)): arr_[true_e_idx] *= flux[true_e_idx] * dE_nu[true_e_idx] - pdf = np.sum(arr_, axis=0) + pdf_arr = np.sum(arr_, axis=0) del(arr_) # Normalize the pdf. - norm = np.sum(pdf) + norm = np.sum(pdf_arr) if norm == 0: - raise ValueError('The signal PDF is empty for {}! This should ' + raise ValueError( + 'The signal PDF is empty for {}! This should ' 'not happen. Check the parameter ranges!'.format( str(gridfitparams))) - pdf /= norm - pdf /= self.bin_volumes + pdf_arr /= norm + pdf_arr /= bin_volumes + + pdf = PDSignalPDF( + pdf_arr, reco_e_edges, psi_edges, ang_err_edges) return pdf + """ args_list = [ - ((pdf_arr, flux_model, gridfitparams), {}) + ((union_arr, flux_model, gridfitparams), {}) for gridfitparams in self.gridfitparams_list ] @@ -705,11 +788,14 @@ def create_pdf(pdf_arr, flux_model, gridfitparams): ncpu=self.ncpu, ppbar=ppbar) """ - self.pdf_list = [ - create_pdf(pdf_arr, flux_model, gridfitparams={'gamma': 2}) + pdf_list = [ + create_pdf(union_arr, flux_model, gridfitparams={'gamma': 2}) ] + #""" + del(union_arr) - del(pdf_arr) - - + # Save all the energy PDF objects in the PDFSet PDF registry with + # the hash of the individual parameters as key. + for (gridfitparams, pdf) in zip(self.gridfitparams_list, pdf_list): + self.add_pdf(pdf, gridfitparams) From 3fccb433fd3f623b8f9a30ce840617e8c4e4c8c3 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 28 Apr 2022 16:36:17 +0200 Subject: [PATCH 050/274] Added psi function to the trial data --- skyllh/analyses/i3/trad_ps/analysis.py | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index 1532c18e39..86659b485a 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -86,6 +86,39 @@ PublicDataSignalI3EnergyPDFSet ) +def psi_func(tdm, src_hypo_group_manager, fitparams): + """Function to calculate the opening angle between the source position + and the event's reconstructed position. + """ + ra = tdm.get_data('ra') + dec = tdm.get_data('dec') + + # Make the source position angles two-dimensional so the PDF value + # can be calculated via numpy broadcasting automatically for several + # sources. This is useful for stacking analyses. + src_ra = tdm.get_data('src_array')['ra'][:, np.newaxis] + src_dec = tdm.get_data('src_array')['dec'][:, np.newaxis] + + delta_dec = np.abs(dec - src_dec) + delta_ra = np.abs(ra - src_ra) + x = ( + (np.sin(delta_dec / 2.))**2. + np.cos(dec) * + np.cos(src_dec) * (np.sin(delta_ra / 2.))**2. + ) + + # Handle possible floating precision errors. + x[x < 0.] = 0. + x[x > 1.] = 1. + + psi = (2.0*np.arcsin(np.sqrt(x))) + # Floor psi values below the first bin location in spatial KDE PDF. + # Flooring at the boundary (1e-6) requires a regeneration of the + # spatial KDE splines. + floor = 10**(-5.95442953) + psi = np.where(psi < floor, floor, psi) + + # For now we support only a single source, hence return psi[0]. + return psi[0, :] def TXS_location(): src_ra = np.radians(77.358) @@ -235,6 +268,7 @@ def create_analysis( tdm = TrialDataManager() tdm.add_source_data_field('src_array', pointlikesource_to_data_field_array) + tdm.add_data_field('psi', psi_func) sin_dec_binning = ds.get_binning_definition('sin_dec') log_energy_binning = ds.get_binning_definition('log_energy') From f14e502c9083a13f7288896d35768dd630404c3f Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 15:28:11 +0200 Subject: [PATCH 051/274] Convert the smearing matrix degree values into radians. Also normalize the PDFs correctly to be a probability per observable volume --- skyllh/analyses/i3/trad_ps/utils.py | 47 ++++++++++++++++++----------- 1 file changed, 29 insertions(+), 18 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 3da4e368b4..926a8208c8 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -111,7 +111,8 @@ def load_smearing_histogram(pathfilenames): true_e_bin_edges : 1d ndarray The ndarray holding the bin edges of the true energy axis. true_dec_bin_edges : 1d ndarray - The ndarray holding the bin edges of the true declination axis. + The ndarray holding the bin edges of the true declination axis in + radians. reco_e_lower_edges : 3d ndarray The 3d ndarray holding the lower bin edges of the reco energy axis. For each pair of true_e and true_dec different reco energy bin edges @@ -123,16 +124,18 @@ def load_smearing_histogram(pathfilenames): are provided. The shape is (n_true_e, n_true_dec, n_reco_e). psi_lower_edges : 4d ndarray - The 4d ndarray holding the lower bin edges of the psi axis. + The 4d ndarray holding the lower bin edges of the psi axis in radians. The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). psi_upper_edges : 4d ndarray - The 4d ndarray holding the upper bin edges of the psi axis. + The 4d ndarray holding the upper bin edges of the psi axis in radians. The shape is (n_true_e, n_true_dec, n_reco_e, n_psi). ang_err_lower_edges : 5d ndarray - The 5d ndarray holding the lower bin edges of the angular error axis. + The 5d ndarray holding the lower bin edges of the angular error axis + in radians. The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). ang_err_upper_edges : 5d ndarray - The 5d ndarray holding the upper bin edges of the angular error axis. + The 5d ndarray holding the upper bin edges of the angular error axis + in radians. The shape is (n_true_e, n_true_dec, n_reco_e, n_psi, n_ang_err). """ # Load the smearing data from the public dataset. @@ -174,8 +177,10 @@ def _get_nbins_from_edges(lower_edges, upper_edges): n += 1 return n - true_e_bin_edges = np.union1d(data['true_e_min'], data['true_e_max']) - true_dec_bin_edges = np.union1d(data['true_dec_min'], data['true_dec_max']) + true_e_bin_edges = np.union1d( + data['true_e_min'], data['true_e_max']) + true_dec_bin_edges = np.union1d( + data['true_dec_min'], data['true_dec_max']) n_true_e = len(true_e_bin_edges) - 1 n_true_dec = len(true_dec_bin_edges) - 1 @@ -237,6 +242,13 @@ def _get_nbins_from_edges(lower_edges, upper_edges): ) ) + # Convert degrees into radians. + true_dec_bin_edges = np.radians(true_dec_bin_edges) + psi_lower_edges = np.radians(psi_lower_edges) + psi_upper_edges = np.radians(psi_upper_edges) + ang_err_lower_edges = np.radians(ang_err_lower_edges) + ang_err_upper_edges = np.radians(ang_err_upper_edges) + return ( histogram, true_e_bin_edges, @@ -541,8 +553,6 @@ def get_true_dec_idx(self, true_dec): true_dec_idx : int The index of the declination bin for the given declination value. """ - true_dec = np.degrees(true_dec) - if (true_dec < self.true_dec_bin_edges[0]) or\ (true_dec > self.true_dec_bin_edges[-1]): raise ValueError('The declination {} degrees is not supported by ' @@ -596,7 +606,8 @@ def get_psi_idx(self, true_e_idx, true_dec_idx, reco_e_idx, psi): reco_e_idx : int The index of the reco energy bin. psi : float - The psi value for which the bin index should get returned. + The psi value in radians for which the bin index should get + returned. Returns ------- @@ -632,8 +643,8 @@ def get_ang_err_idx( psi_idx : int The index of the psi bin. ang_err : float - The angular error value for which the bin index should get - returned. + The angular error value in radians for which the bin index should + get returned. Returns ------- @@ -655,7 +666,7 @@ def get_ang_err_idx( return ang_err_idx - def get_true_log_e_range_with_valid_log_e_pfds(self, dec_idx): + def get_true_log_e_range_with_valid_log_e_pdfs(self, dec_idx): """Determines the true log energy range for which log_e PDFs are available for the given declination bin. @@ -724,7 +735,7 @@ def get_log_e_pdf( bin_widths = upper_bin_edges - lower_bin_edges # Normalize the PDF. - pdf /= (np.sum(pdf * bin_widths)) + pdf /= np.sum(pdf) * bin_widths return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) @@ -774,7 +785,7 @@ def get_psi_pdf( bin_widths = upper_bin_edges - lower_bin_edges # Normalize the PDF. - pdf /= (np.sum(pdf * bin_widths)) + pdf /= np.sum(pdf) * bin_widths return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) @@ -825,7 +836,7 @@ def get_ang_err_pdf( bin_widths = upper_bin_edges - lower_bin_edges # Normalize the PDF. - pdf = pdf / np.sum(pdf * bin_widths) + pdf = pdf / (np.sum(pdf) * bin_widths) return (pdf, lower_bin_edges, upper_bin_edges, bin_widths) @@ -955,7 +966,7 @@ def sample_psi( psi_idx[mm] = bb_psi_idx psi[mm] = bb_psi - return (psi_idx, np.radians(psi)) + return (psi_idx, psi) def sample_ang_err( self, rss, dec_idx, log_true_e_idxs, log_e_idxs, psi_idxs): @@ -1032,4 +1043,4 @@ def sample_ang_err( ang_err_idx[mmm] = bbb_ang_err_idx ang_err[mmm] = bbb_ang_err - return (ang_err_idx, np.radians(ang_err)) + return (ang_err_idx, ang_err) From 19dcc0284e44ea1916cd34e9bffa30b842018f93 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 15:33:05 +0200 Subject: [PATCH 052/274] Added axes property to PDFSet to be conistent with the PDF class --- skyllh/core/pdf.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skyllh/core/pdf.py b/skyllh/core/pdf.py index 4444e2bdfe..7a43dcda93 100644 --- a/skyllh/core/pdf.py +++ b/skyllh/core/pdf.py @@ -1020,7 +1020,7 @@ def get_prob(self, tdm, params=None, tl=None): n_src = len(tdm.get_data('src_array')['ra']) eventdata = np.array( [np.tile(tdm.get_data(axis.name), n_src) - if 'psi' not in axis.name + if 'psi' not in axis.name else tdm.get_data(axis.name) for axis in self._axes]).T elif (self.is_background_pdf): @@ -1365,6 +1365,14 @@ def pdf_keys(self): @property def pdf_axes(self): + """DEPRECATED (read-only) The PDFAxes object of one of the PDFs of this + PDF set. + All PDFs of this set are supposed to have the same axes. + """ + return self.axes + + @property + def axes(self): """(read-only) The PDFAxes object of one of the PDFs of this PDF set. All PDFs of this set are supposed to have the same axes. """ From 01493602207888459a3f9283237255e62460b1ee Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 15:51:29 +0200 Subject: [PATCH 053/274] Only use valid bins for the ang_err --- skyllh/analyses/i3/trad_ps/utils.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 926a8208c8..6914de4ab4 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -835,6 +835,14 @@ def get_ang_err_pdf( ] bin_widths = upper_bin_edges - lower_bin_edges + # Some bins might not be defined, i.e. have zero bin widths. + valid = bin_widths > 0 + + pdf = pdf[valid] + lower_bin_edges = lower_bin_edges[valid] + upper_bin_edges = upper_bin_edges[valid] + bin_widths = bin_widths[valid] + # Normalize the PDF. pdf = pdf / (np.sum(pdf) * bin_widths) From 7173e581cdda02bb7bd087ec42a48df79b0e9a97 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 16:09:40 +0200 Subject: [PATCH 054/274] fix typo and add better error message --- skyllh/core/binning.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index 240977d9c7..8806de2e05 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -178,15 +178,19 @@ def get_bin_indices_from_lower_and_upper_binedges(le, ue, values): The bin indices of the given values. """ if np.any(values < le[0]): + invalid_values = values[values < le[0]] raise ValueError( - 'At least one value is smaller than the lowest bin edge!') + '{} values ({}) are smaller than the lowest bin edge ({})!'.format( + len(invalid_values), str(invalid_values), le[0])) if np.any(values > ue[-1]): + invalid_values = values[values > ue[-1]] raise ValueError( - 'At least one value is larger than the largest bin edge!') + '{} values ({}) are larger than the largest bin edge ({})!'.format( + len(invalid_values), str(invalid_values), ue[-1])) m = ( - (v[:,np.newaxis] >= le[np.newaxis,:]) & - (v[:,np.newaxis] < ue[np.newaxis,:]) + (values[:,np.newaxis] >= le[np.newaxis,:]) & + (values[:,np.newaxis] < ue[np.newaxis,:]) ) idxs = np.nonzero(m)[1] From cb4e9b3307af7f34c1d2ac417923005549952dab Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 16:11:27 +0200 Subject: [PATCH 055/274] Implemented new signal pdf based on the smearing matrix. It's currently broken --- skyllh/analyses/i3/trad_ps/pdfratio.py | 161 ++++++++++++++++++++++++ skyllh/analyses/i3/trad_ps/signalpdf.py | 102 +++++++++++---- 2 files changed, 240 insertions(+), 23 deletions(-) create mode 100644 skyllh/analyses/i3/trad_ps/pdfratio.py diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py new file mode 100644 index 0000000000..4bcbb2232b --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -0,0 +1,161 @@ +# -*- coding: utf-8 -*- + +from skyllh.core.parameters import make_params_hash +from skyllh.core.pdf import PDF +from skyllh.core.pdfratio import SigSetOverBkgPDFRatio + + +class PDPDFRatio(SigSetOverBkgPDFRatio): + def __init__(self, sig_pdf_set, bkg_pdf, **kwargs): + """Creates a PDFRatio instance for the public data. + It takes a signal PDF set for different discrete gamma values. + + Parameters + ---------- + sig_pdf_set : + """ + super().__init__( + pdf_type=PDF, + signalpdfset=sig_pdf_set, + backgroundpdf=bkg_pdf, + **kwargs) + + # Construct the instance for the fit parameter interpolation method. + self._interpolmethod_instance = self.interpolmethod( + self._get_ratio_values, sig_pdf_set.fitparams_grid_set) + + """ + # Get the requires field names from the background and signal pdf. + self._data_field_name_list = [] + for axis in sig_pdf_set.axes: + field_name_list.append(axis.name) + for axis in bkg_pdf.axes: + if axis.name not in field_name_list: + field_name_list.append(axis.name) + """ + + # Create cache variables for the last ratio value and gradients in order + # to avoid the recalculation of the ratio value when the + # ``get_gradient`` method is called (usually after the ``get_ratio`` + # method was called). + self._cache_fitparams_hash = None + self._cache_ratio = None + self._cache_gradients = None + + def _get_signal_fitparam_names(self): + """This method must be re-implemented by the derived class and needs to + return the list of signal fit parameter names, this PDF ratio is a + function of. If it returns an empty list, the PDF ratio is independent + of any signal fit parameters. + + Returns + ------- + list of str + The list of the signal fit parameter names, this PDF ratio is a + function of. By default this method returns an empty list indicating + that the PDF ratio depends on no signal parameter. + """ + fitparam_names = self.signalpdfset.fitparams_grid_set.parameter_names + return fitparam_names + + def _is_cached(self, tdm, fitparams_hash): + """Checks if the ratio and gradients for the given set of fit parameters + are already cached. + """ + if((self._cache_fitparams_hash == fitparams_hash) and + (len(self._cache_ratio) == tdm.n_selected_events) + ): + return True + return False + + def _get_ratio_values(self, tdm, gridfitparams, eventdata): + """Select the signal PDF for the given fit parameter grid point and + evaluates the S/B ratio for all the given events. + """ + sig_pdf = self.signalpdfset.get_pdf(gridfitparams) + bkg_pdf = self.backgroundpdf + + (sig_prob, _) = sig_pdf.get_prob(tdm) + (bkg_prob, _) = bkg_pdf.get_prob(tdm) + + if np.any(np.invert(bkg_prob > 0)): + raise ValueError( + 'For at least one event no background probability can be ' + 'calculated! Check your background PDF!') + + ratio = sig_prob / bkg_pdf + + return ratio + + def _calculate_ratio_and_gradients(self, tdm, fitparams, fitparams_hash): + """Calculates the ratio values and ratio gradients for all the events + given the fit parameters using the interpolation method for the fit + parameter. It caches the results. + """ + (ratio, gradients) =\ + self._interpolmethod_instance.get_value_and_gradients( + tdm, eventdata=None, params=fitparams) + + # Cache the value and the gradients. + self._cache_fitparams_hash = fitparams_hash + self._cache_ratio = ratio + self._cache_gradients = gradients + + def get_ratio(self, tdm, fitparams=None, tl=None): + """Calculates the PDF ratio values for all the events. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the trial data events for + which the PDF ratio values should get calculated. + fitparams : dict | None + The dictionary with the parameter name-value pairs. + It can be ``None``, if the PDF ratio does not depend on any + parameters. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + ratios : (N_events,)-shaped 1d numpy ndarray of float + The PDF ratio value for each trial event. + """ + fitparams_hash = make_params_hash(fitparams) + + # Check if the ratio value is already cached. + if(self._is_cached(tdm, fitparams_hash)): + return self._cache_ratio + + self._calculate_ratio_and_gradients(tdm, fitparams, fitparams_hash) + + return self._cache_ratio + + def get_gradient(self, tdm, fitparams, fitparam_name): + """Retrieves the PDF ratio gradient for the pidx'th fit parameter. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the trial event data for which + the PDF ratio gradient values should get calculated. + fitparams : dict + The dictionary with the fit parameter values. + fitparam_name : str + The name of the fit parameter for which the gradient should get + calculated. + """ + fitparams_hash = make_params_hash(fitparams) + + # Convert the fit parameter name into the local fit parameter index. + pidx = self.convert_signal_fitparam_name_into_index(fitparam_name) + + # Check if the gradients have been calculated already. + if(self._is_cached(tdm, fitparams_hash)): + return self._cache_gradients[pidx] + + # The gradients have not been calculated yet. + self._calculate_ratio_and_gradients(tdm, fitparams, fitparams_hash) + + return self._cache_gradients[pidx] diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 46bfb89ac3..e5a5ea2955 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -18,6 +18,7 @@ from skyllh.core.storage import DataFieldRecordArray from skyllh.core.pdf import ( PDF, + PDFAxis, PDFSet, IsSignalPDF, EnergyPDF @@ -96,7 +97,7 @@ def _generate_events( # Determine the true energy range for which log_e PDFs are available. (min_log_true_e, - max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pfds( + max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( dec_idx) # First draw a true neutrino energy from the hypothesis spectrum. @@ -590,7 +591,6 @@ def get_prob(self, tdm, gridfitparams): - 'src_array' : 1d record array The source record array containing the declination of the sources. - gridfitparams : dict The dictionary holding the signal parameter values for which the signal energy probability should be calculated. Note, that the @@ -627,18 +627,32 @@ def __init__( self.pdf_arr = pdf_arr - #self.log_e_edges = log_e_edges self.log_e_lower_edges = log_e_edges[:-1] self.log_e_upper_edges = log_e_edges[1:] - #self.psi_edges = psi_edges self.psi_lower_edges = psi_edges[:-1] self.psi_upper_edges = psi_edges[1:] - #self.ang_err_edges = ang_err_edges self.ang_err_lower_edges = ang_err_edges[:-1] self.ang_err_upper_edges = ang_err_edges[1:] + # Add the PDF axes. + self.add_axis(PDFAxis( + name='log_energy', + vmin=self.log_e_lower_edges[0], + vmax=self.log_e_upper_edges[-1]) + ) + self.add_axis(PDFAxis( + name='psi', + vmin=self.psi_lower_edges[0], + vmax=self.psi_lower_edges[-1]) + ) + self.add_axis(PDFAxis( + name='ang_err', + vmin=self.ang_err_lower_edges[0], + vmax=self.ang_err_upper_edges[-1]) + ) + def assert_is_valid_for_trial_data(self, tdm): pass @@ -689,7 +703,11 @@ def get_prob(self, tdm, params=None, tl=None): self.ang_err_lower_edges, self.ang_err_upper_edges, ang_err) idxs = tuple(zip(log_e_idxs, psi_idxs, ang_err_idxs)) - prob = self.pdf_arr[idxs] + + # FIXME with a block size + prob = np.empty((len(idxs),), dtype=np.double) + for (i,idx) in enumerate(idxs): + prob[i] = self.pdf_arr[idx] return (prob, None) @@ -707,7 +725,7 @@ def __init__( ncpu=None, ppbar=None, **kwargs): - """Creates a new PublicDataSignalPDFSet instance for the public data. + """Creates a new PDSignalPDFSet instance for the public data. """ super().__init__( pdf_type=PDF, @@ -718,11 +736,12 @@ def __init__( if(union_sm_arr_pathfilename is not None): with open(union_sm_arr_pathfilename, 'rb') as f: data = pickle.load(f) - union_arr = data['arr'] + union_arr = data['union_arr'] true_e_bin_edges = data['true_e_bin_edges'] reco_e_edges = data['reco_e_edges'] psi_edges = data['psi_edges'] ang_err_edges = data['ang_err_edges'] + del(data) else: sm = PublicDataSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( @@ -732,7 +751,8 @@ def __init__( reco_e_edges, psi_edges, ang_err_edges - ) = create_unionized_smearing_matrix_array(sm, src_dec) + ) = create_unionized_smearing_matrix_array(sm, np.degrees(src_dec)) + del(sm) reco_e_bw = np.diff(reco_e_edges) psi_edges_bw = np.diff(psi_edges) @@ -742,7 +762,6 @@ def __init__( psi_edges_bw[np.newaxis,:,np.newaxis] * ang_err_bw[np.newaxis,np.newaxis,:]) - true_e_bin_centers = get_bincenters_from_binedges(true_e_bin_edges) # Create the pdf in gamma for different gamma values. def create_pdf(union_arr, flux_model, gridfitparams): """Creates a pdf for a specific gamma value. @@ -751,17 +770,18 @@ def create_pdf(union_arr, flux_model, gridfitparams): # The copy is needed to not interfer with other CPU processes. my_flux_model = flux_model.copy(newprop=gridfitparams) - E_nu = np.power(10, true_e_bin_centers) - flux = my_flux_model(E_nu) - dE_nu = np.diff(true_e_bin_edges) + E_nu_min = np.power(10, true_e_bin_edges[:-1]) + E_nu_max = np.power(10, true_e_bin_edges[1:]) + + flux_dE = my_flux_model.get_integral(E_nu_min, E_nu_max) arr_ = np.copy(union_arr) - for true_e_idx in range(len(true_e_bin_centers)): - arr_[true_e_idx] *= flux[true_e_idx] * dE_nu[true_e_idx] + for true_e_idx in range(len(true_e_bin_edges)-1): + arr_[true_e_idx] *= flux_dE[true_e_idx] pdf_arr = np.sum(arr_, axis=0) del(arr_) - # Normalize the pdf. + # Normalize the pdf, which is the probability per bin volume. norm = np.sum(pdf_arr) if norm == 0: raise ValueError( @@ -776,22 +796,17 @@ def create_pdf(union_arr, flux_model, gridfitparams): return pdf - """ args_list = [ ((union_arr, flux_model, gridfitparams), {}) for gridfitparams in self.gridfitparams_list ] - self.pdf_list = parallelize( + pdf_list = parallelize( create_pdf, args_list, ncpu=self.ncpu, ppbar=ppbar) - """ - pdf_list = [ - create_pdf(union_arr, flux_model, gridfitparams={'gamma': 2}) - ] - #""" + del(union_arr) # Save all the energy PDF objects in the PDFSet PDF registry with @@ -799,3 +814,44 @@ def create_pdf(union_arr, flux_model, gridfitparams): for (gridfitparams, pdf) in zip(self.gridfitparams_list, pdf_list): self.add_pdf(pdf, gridfitparams) + def get_prob(self, tdm, gridfitparams, tl=None): + """Calculates the signal probability density of each event for the + given set of signal fit parameters on a grid. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the data events for which the + probability should be calculated for. The following data fields must + exist: + + - 'log_energy' + The log10 of the reconstructed energy. + - 'psi' + The opening angle from the source to the event in radians. + - 'ang_err' + The angular error of the event in radians. + gridfitparams : dict + The dictionary holding the signal parameter values for which the + signal energy probability should be calculated. Note, that the + parameter values must match a set of parameter grid values for which + a PDSignalPDF object has been created at construction time of this + PDSignalPDFSet object. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure time. + + Returns + ------- + prob : 1d ndarray + The array with the signal energy probability for each event. + + Raises + ------ + KeyError + If no energy PDF can be found for the given signal parameter values. + """ + pdf = self.get_pdf(gridfitparams) + + (prob, grads) = pdf.get_prob(tdm, tl=tl) + + return prob From 324d9431933e8144687c045ca4f7606c111fe82b Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 16:31:49 +0200 Subject: [PATCH 056/274] Handle PDFs that only return prob and no gradient --- skyllh/core/pdf.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/skyllh/core/pdf.py b/skyllh/core/pdf.py index 7a43dcda93..4ff603a939 100644 --- a/skyllh/core/pdf.py +++ b/skyllh/core/pdf.py @@ -546,7 +546,11 @@ def get_prob(self, tdm, params=None, tl=None): with TaskTimer(tl, 'Get signal prob from table.'): (prob1, grads1) = pdf1.get_prob(tdm, params, tl=tl) - (prob2, grads2) = pdf2.get_prob(tdm, params, tl=tl) + p2 = pdf2.get_prob(tdm, params, tl=tl) + if isinstance(p2, tuple): + (prob2, grads2) = p2 + else: + prob2 = p2 prob = prob1 * prob2 From 4da68ad86a5793e54746c30609593f0e4d6381ab Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 16:35:06 +0200 Subject: [PATCH 057/274] Handle PDFs that only return prob and no gradient --- skyllh/core/pdf.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/skyllh/core/pdf.py b/skyllh/core/pdf.py index 4ff603a939..53a0b1c1a8 100644 --- a/skyllh/core/pdf.py +++ b/skyllh/core/pdf.py @@ -544,8 +544,12 @@ def get_prob(self, tdm, params=None, tl=None): pdf1 = self._pdf1 pdf2 = self._pdf2 - with TaskTimer(tl, 'Get signal prob from table.'): - (prob1, grads1) = pdf1.get_prob(tdm, params, tl=tl) + with TaskTimer(tl, 'Get prob from individual PDFs.'): + p1 = pdf1.get_prob(tdm, params, tl=tl) + if isinstance(p1, tuple): + (prob1, grads1) = p1 + else + prob1 = p1 p2 = pdf2.get_prob(tdm, params, tl=tl) if isinstance(p2, tuple): (prob2, grads2) = p2 From 916a3eadd7c278f9d64498e0c7938add305bd647 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 16:36:04 +0200 Subject: [PATCH 058/274] Extend grid to handle the edges --- skyllh/analyses/i3/trad_ps/signalpdf.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index e5a5ea2955..02de397321 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -727,6 +727,10 @@ def __init__( **kwargs): """Creates a new PDSignalPDFSet instance for the public data. """ + # Extend the fitparam_grid_set + fitparam_grid_set = fitparam_grid_set.copy() + fitparam_grid_set.add_extra_lower_and_upper_bin() + super().__init__( pdf_type=PDF, fitparams_grid_set=fitparam_grid_set, From 77ceccfdfbad80d74f3eca0fc24fc995fda08e24 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 29 Apr 2022 16:36:32 +0200 Subject: [PATCH 059/274] Fix typo --- skyllh/analyses/i3/trad_ps/pdfratio.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index 4bcbb2232b..b57d5ad6bb 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import numpy as np + from skyllh.core.parameters import make_params_hash from skyllh.core.pdf import PDF from skyllh.core.pdfratio import SigSetOverBkgPDFRatio @@ -83,7 +85,7 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): 'For at least one event no background probability can be ' 'calculated! Check your background PDF!') - ratio = sig_prob / bkg_pdf + ratio = sig_prob / bkg_prob return ratio From af97753b41cebc4977195aed588ad5f48397849f Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 2 May 2022 16:26:04 +0200 Subject: [PATCH 060/274] Fix typo and format --- skyllh/core/pdf.py | 2 +- skyllh/i3/pdf.py | 7 +++++-- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/skyllh/core/pdf.py b/skyllh/core/pdf.py index 53a0b1c1a8..eb955024c8 100644 --- a/skyllh/core/pdf.py +++ b/skyllh/core/pdf.py @@ -548,7 +548,7 @@ def get_prob(self, tdm, params=None, tl=None): p1 = pdf1.get_prob(tdm, params, tl=tl) if isinstance(p1, tuple): (prob1, grads1) = p1 - else + else: prob1 = p1 p2 = pdf2.get_prob(tdm, params, tl=tl) if isinstance(p2, tuple): diff --git a/skyllh/i3/pdf.py b/skyllh/i3/pdf.py index 46e8ba492e..3fe6804a4f 100644 --- a/skyllh/i3/pdf.py +++ b/skyllh/i3/pdf.py @@ -229,9 +229,12 @@ def get_prob(self, tdm, fitparams=None, tl=None): logE_binning = self.get_binning('log_energy') sinDec_binning = self.get_binning('sin_dec') - logE_idx = np.digitize(get_data('log_energy'), logE_binning.binedges) - 1 - sinDec_idx = np.digitize(get_data('sin_dec'), sinDec_binning.binedges) - 1 + logE_idx = np.digitize( + get_data('log_energy'), logE_binning.binedges) - 1 + sinDec_idx = np.digitize( + get_data('sin_dec'), sinDec_binning.binedges) - 1 with TaskTimer(tl, 'Evaluating logE-sinDec histogram.'): prob = self._hist_logE_sinDec[(logE_idx,sinDec_idx)] + return prob From 6dfd0e0a3d13f756006c4896493a1af0b3625ab0 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 2 May 2022 16:27:14 +0200 Subject: [PATCH 061/274] Added methods to BinningDefinition --- skyllh/core/binning.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index 8806de2e05..016d6e37de 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -267,6 +267,12 @@ def bincenters(self): """ return 0.5*(self._binedges[:-1] + self._binedges[1:]) + @property + def binwidths(self): + """(read-only) The widths of the bins. + """ + return np.diff(self._binedges) + @property def lower_edge(self): """The lowest bin edge of the binning. @@ -303,6 +309,15 @@ def any_data_out_of_binning_range(self, data): (data > self.upper_edge)) return outofrange + def get_binwidth_from_value(self, value): + """Returns the width of the bin the given value falls into. + """ + idx = np.digitize(value, self._binedges) - 1 + + bin_width = self.binwidths[idx] + + return bin_width + def get_subset(self, lower_edge, upper_edge): """Creates a new BinningDefinition instance which contains only a subset of the bins of this BinningDefinition instance. The range of the subset From cfb9c11e0acf8413f6949f459eba553c984aa7e8 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 2 May 2022 19:09:40 +0200 Subject: [PATCH 062/274] Added methods --- skyllh/analyses/i3/trad_ps/utils.py | 53 +++++++++++++++++++++++++++++ 1 file changed, 53 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 6914de4ab4..bc03eb9e20 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -451,6 +451,59 @@ def __init__( self.log_true_e_binedges_upper[-1:]) ) + @property + def log_true_e_bincenters(self): + """The bin center values of the log true energy axis. + """ + bincenters = 0.5 * ( + self.log_true_e_binedges[:-1] + self.log_true_e_binedges[1:] + ) + + return bincenters + + def get_aeff_for_sin_true_dec(self, sin_true_dec): + """Retrieves the effective area as function of log_true_e. + + Parameters + ---------- + sin_true_dec : float + The sin of the true declination. + + Returns + ------- + aeff : (n,)-shaped numpy ndarray + The effective area for the given true declination as a function of + log true energy. + """ + sin_true_dec_idx = np.digitize( + sin_true_dec, self.sin_true_dec_binedges) - 1 + + aeff = self.aeff_arr[sin_true_dec_idx] + + return aeff + + def get_aeff_integral_for_sin_true_dec( + self, sin_true_dec, log_true_e_min, log_true_e_max): + """Calculates the integral of the effective area using the trapezoid + method. + + Returns + ------- + integral : float + The integral in unit cm^2 GeV. + """ + aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + + integral = ( + (np.power(10, log_true_e_max) - + np.power(10, log_true_e_min)) * + 0.5 * + (np.interp(log_true_e_min, self.log_true_e_bincenters, aeff) + + np.interp(log_true_e_max, self.log_true_e_bincenters, aeff)) + ) + + return integral + def get_aeff(self, sin_true_dec, log_true_e): """Retrieves the effective area for the given sin(dec_true) and log(E_true) value pairs. From aac2ce6ea2d369cac0a8ee2b1f949857e3ea8a4e Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 2 May 2022 19:14:54 +0200 Subject: [PATCH 063/274] Use log space for the PDF ratio --- skyllh/analyses/i3/trad_ps/pdfratio.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index b57d5ad6bb..f163bb86b6 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -1,5 +1,7 @@ # -*- coding: utf-8 -*- +import sys + import numpy as np from skyllh.core.parameters import make_params_hash @@ -85,7 +87,10 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): 'For at least one event no background probability can be ' 'calculated! Check your background PDF!') - ratio = sig_prob / bkg_prob + m = sig_prob == 0 + sig_prob[m] = sys.float_info.min + + ratio = np.log(sig_prob) - np.log(bkg_prob) return ratio @@ -98,6 +103,9 @@ def _calculate_ratio_and_gradients(self, tdm, fitparams, fitparams_hash): self._interpolmethod_instance.get_value_and_gradients( tdm, eventdata=None, params=fitparams) + ratio = np.exp(ratio) + gradients = ratio * gradients + # Cache the value and the gradients. self._cache_fitparams_hash = fitparams_hash self._cache_ratio = ratio From a18b04c9e8a7216977987873c1671bf2555de08a Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 2 May 2022 19:16:00 +0200 Subject: [PATCH 064/274] Take the effective area into account for the signal pdf --- skyllh/analyses/i3/trad_ps/signalpdf.py | 46 ++++++++++++++++++------- 1 file changed, 33 insertions(+), 13 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 02de397321..22c0f19883 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -657,7 +657,7 @@ def assert_is_valid_for_trial_data(self, tdm): pass def get_prob(self, tdm, params=None, tl=None): - """Looks up the probability density for the events given by the + """Calculates the probability density for the events given by the TrialDataManager. Parameters @@ -695,19 +695,27 @@ def get_prob(self, tdm, params=None, tl=None): psi = tdm.get_data('psi') ang_err = tdm.get_data('ang_err') + # Select events that actually have a signal PDF. + # All other events will get zero signal probability. + m = ( + (log_e >= self.log_e_lower_edges[0]) & + (log_e < self.log_e_upper_edges[-1]) & + (psi >= self.psi_lower_edges[0]) & + (psi < self.psi_upper_edges[-1]) & + (ang_err >= self.ang_err_lower_edges[0]) & + (ang_err < self.ang_err_upper_edges[-1]) + ) + + prob = np.zeros((len(log_e),), dtype=np.double) + log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.log_e_lower_edges, self.log_e_upper_edges, log_e) + self.log_e_lower_edges, self.log_e_upper_edges, log_e[m]) psi_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.psi_lower_edges, self.psi_upper_edges, psi) + self.psi_lower_edges, self.psi_upper_edges, psi[m]) ang_err_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.ang_err_lower_edges, self.ang_err_upper_edges, ang_err) + self.ang_err_lower_edges, self.ang_err_upper_edges, ang_err[m]) - idxs = tuple(zip(log_e_idxs, psi_idxs, ang_err_idxs)) - - # FIXME with a block size - prob = np.empty((len(idxs),), dtype=np.double) - for (i,idx) in enumerate(idxs): - prob[i] = self.pdf_arr[idx] + prob[m] = self.pdf_arr[(log_e_idxs, psi_idxs, ang_err_idxs)] return (prob, None) @@ -755,9 +763,14 @@ def __init__( reco_e_edges, psi_edges, ang_err_edges - ) = create_unionized_smearing_matrix_array(sm, np.degrees(src_dec)) + ) = create_unionized_smearing_matrix_array(sm, src_dec) del(sm) + # Load the effective area. + aeff = PublicDataAeff( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('eff_area_datafile'))) + reco_e_bw = np.diff(reco_e_edges) psi_edges_bw = np.diff(psi_edges) ang_err_bw = np.diff(ang_err_edges) @@ -777,11 +790,18 @@ def create_pdf(union_arr, flux_model, gridfitparams): E_nu_min = np.power(10, true_e_bin_edges[:-1]) E_nu_max = np.power(10, true_e_bin_edges[1:]) - flux_dE = my_flux_model.get_integral(E_nu_min, E_nu_max) + flux_integral = my_flux_model.get_integral(E_nu_min, E_nu_max) + + aeff_integral = aeff.get_aeff_integral_for_sin_true_dec( + np.sin(src_dec), np.log10(E_nu_min), np.log10(E_nu_max)) + + dE_nu = np.diff(np.power(10, true_e_bin_edges)) + + avg_exposure = aeff_integral / dE_nu * flux_integral arr_ = np.copy(union_arr) for true_e_idx in range(len(true_e_bin_edges)-1): - arr_[true_e_idx] *= flux_dE[true_e_idx] + arr_[true_e_idx] *= avg_exposure[true_e_idx] pdf_arr = np.sum(arr_, axis=0) del(arr_) From ab9ed2872c929e90cf1e0dc2dd3fa2079460b688 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 2 May 2022 19:17:12 +0200 Subject: [PATCH 065/274] Use the new unionized signal pdf --- skyllh/analyses/i3/trad_ps/analysis.py | 59 +++++++++++++++++++++----- 1 file changed, 48 insertions(+), 11 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index 86659b485a..6f4d6680a6 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -8,6 +8,7 @@ import argparse import logging import numpy as np +import os.path from skyllh.core.progressbar import ProgressBar @@ -83,8 +84,12 @@ PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod ) from skyllh.analyses.i3.trad_ps.signalpdf import ( - PublicDataSignalI3EnergyPDFSet + PDSignalPDFSet ) +from skyllh.analyses.i3.trad_ps.pdfratio import ( + PDPDFRatio +) + def psi_func(tdm, src_hypo_group_manager, fitparams): """Function to calculate the opening angle between the source position @@ -134,6 +139,7 @@ def create_analysis( refplflux_gamma=2, ns_seed=10.0, gamma_seed=3, + cache_dir='.', n_mc_events=int(1e7), compress_data=False, keep_data_fields=None, @@ -162,6 +168,8 @@ def create_analysis( gamma_seed : float | None Value to seed the minimizer with for the gamma fit. If set to None, the refplflux_gamma value will be set as gamma_seed. + cache_dir : str + The cache directory where to look for cached data, e.g. signal PDFs. compress_data : bool Flag if the data should get converted from float64 into float32. keep_data_fields : list of str | None @@ -195,7 +203,7 @@ def create_analysis( The Analysis instance for this analysis. """ # Define the flux model. - fluxmodel = PowerLawFlux( + flux_model = PowerLawFlux( Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) # Define the fit parameter ns. @@ -205,7 +213,7 @@ def create_analysis( fitparam_gamma = FitParameter('gamma', valmin=1, valmax=4, initial=gamma_seed) # Define the detector signal efficiency implementation method for the - # IceCube detector and this source and fluxmodel. + # IceCube detector and this source and flux_model. # The sin(dec) binning will be taken by the implementation method # automatically from the Dataset instance. gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) @@ -220,7 +228,7 @@ def create_analysis( # Create a source hypothesis group manager. src_hypo_group_manager = SourceHypoGroupManager( SourceHypoGroup( - source, fluxmodel, detsigyield_implmethod, sig_gen_method)) + source, flux_model, detsigyield_implmethod, sig_gen_method)) # Create a source fit parameter mapper and define the fit parameters. src_fitparam_mapper = SingleSourceFitParameterMapper() @@ -253,6 +261,7 @@ def create_analysis( # We will use the same method for all datasets. event_selection_method = SpatialBoxEventSelectionMethod( src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) + #event_selection_method = None # Add the data sets to the analysis. pbar = ProgressBar(len(datasets), parent=ppbar).start() @@ -274,34 +283,58 @@ def create_analysis( log_energy_binning = ds.get_binning_definition('log_energy') # Create the spatial PDF ratio instance for this dataset. + """ spatial_sigpdf = GaussianPSFPointLikeSourceSignalSpatialPDF( dec_range=np.arcsin(sin_dec_binning.range)) + """ spatial_bkgpdf = DataBackgroundI3SpatialPDF( data.exp, sin_dec_binning) + """ spatial_pdfratio = SpatialSigOverBkgPDFRatio( spatial_sigpdf, spatial_bkgpdf) - + """ # Create the energy PDF ratio instance for this dataset. smoothing_filter = BlockSmoothingFilter(nbins=1) - + """ energy_sigpdfset = PublicDataSignalI3EnergyPDFSet( rss=rss, ds=ds, - flux_model=fluxmodel, + flux_model=flux_model, fitparam_grid_set=gamma_grid, n_events=n_mc_events, smoothing_filter=smoothing_filter, ppbar=pbar) + """ energy_bkgpdf = DataBackgroundI3EnergyPDF( data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) + """ fillmethod = Skylab2SkylabPDFRatioFillMethod() energy_pdfratio = I3EnergySigSetOverBkgPDFRatioSpline( energy_sigpdfset, energy_bkgpdf, fillmethod=fillmethod, ppbar=pbar) + """ + + sig_pdf_set = PDSignalPDFSet( + ds=ds, + src_dec=source.dec, + flux_model=flux_model, + fitparam_grid_set=gamma_grid, + union_sm_arr_pathfilename=os.path.join( + cache_dir, 'union_sm_{}.pkl'.format(ds.name)), + ppbar=ppbar + ) + + bkg_pdf = spatial_bkgpdf * energy_bkgpdf + + pdfratio = PDPDFRatio( + sig_pdf_set=sig_pdf_set, + bkg_pdf=bkg_pdf + ) - pdfratios = [ spatial_pdfratio, energy_pdfratio ] + #pdfratios = [ spatial_pdfratio, energy_pdfratio ] + pdfratios = [ pdfratio ] analysis.add_dataset( ds, data, pdfratios, tdm, event_selection_method) @@ -334,12 +367,14 @@ def create_analysis( help='The random number generator seed for generating the signal PDF.') p.add_argument('--seed', default=1, type=int, help='The random number generator seed for the likelihood minimization.') - p.add_argument("--ncpu", default=1, type=int, + p.add_argument('--ncpu', default=1, type=int, help='The number of CPUs to utilize where parallelization is possible.' ) - p.add_argument("--n-mc-events", default=int(1e7), type=int, + p.add_argument('--n-mc-events', default=int(1e7), type=int, help='The number of MC events to sample for the energy signal PDF.' ) + p.add_argument('--cache-dir', default='.', type=str, + help='The cache directory to look for cached data, e.g. signal PDFs.') args = p.parse_args() # Setup `skyllh` package logging. @@ -365,7 +400,8 @@ def create_analysis( datasets = [] for (sample, season) in sample_seasons: # Get the dataset from the correct dataset collection. - dsc = data_samples[sample].create_dataset_collection(args.data_base_path) + dsc = data_samples[sample].create_dataset_collection( + args.data_base_path) datasets.append(dsc.get_dataset(season)) @@ -383,6 +419,7 @@ def create_analysis( rss_pdf, datasets, source, + cache_dir=args.cache_dir, n_mc_events=args.n_mc_events, gamma_seed=args.gamma_seed, tl=tl) From 73c9ce1fd80970c148a7e164998b158e54e13093 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 3 May 2022 15:28:06 +0200 Subject: [PATCH 066/274] Added method to calculate detection probability density values --- skyllh/analyses/i3/trad_ps/utils.py | 80 +++++++++++++++++++++-------- 1 file changed, 59 insertions(+), 21 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index bc03eb9e20..2221ec9998 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -335,17 +335,21 @@ def create_unionized_smearing_matrix_array(sm, src_dec): Returns ------- - arr : (nbins_true_e, nbins_reco_e, nbins_psi, nbins_ang_err)-shaped - 4D numpy ndarray - The 4D ndarray holding the smearing matrix values. - true_e_bin_edges : 1D numpy ndarray - The unionized bin edges of the true energy axis. - reco_e_edges : 1D numpy ndarray - The unionized bin edges of the reco energy axis. - psi_edges : 1D numpy ndarray - The unionized bin edges of psi axis. - ang_err_edges : 1D numpy ndarray - The unionized bin edges of the angular error axis. + result : dict + The result dictionary with the following fields: + union_arr : (nbins_true_e, + nbins_reco_e, + nbins_psi, + nbins_ang_err)-shaped 4D numpy ndarray + The 4D ndarray holding the smearing matrix values. + log10_true_e_bin_edges : 1D numpy ndarray + The unionized bin edges of the log10 true energy axis. + log10_reco_e_binedges : 1D numpy ndarray + The unionized bin edges of the log10 reco energy axis. + psi_binedges : 1D numpy ndarray + The unionized bin edges of psi axis. + ang_err_binedges : 1D numpy ndarray + The unionized bin edges of the angular error axis. """ true_dec_idx = sm.get_true_dec_idx(src_dec) @@ -377,8 +381,9 @@ def create_unionized_smearing_matrix_array(sm, src_dec): # Create the unionized pdf array, which contains an axis for the # true energy bins. - arr = np.zeros( - (nbins_true_e, nbins_reco_e, nbins_psi, nbins_ang_err), dtype=np.double) + union_arr = np.zeros( + (nbins_true_e, nbins_reco_e, nbins_psi, nbins_ang_err), + dtype=np.double) # Fill the 4D array. for (true_e_idx, true_e) in enumerate(true_e_bincenters): for (e_idx, e) in enumerate(reco_e_bincenters): @@ -401,7 +406,7 @@ def create_unionized_smearing_matrix_array(sm, src_dec): if sm_a_idx is None: continue - arr[ + union_arr[ true_e_idx, e_idx, p_idx, @@ -414,13 +419,15 @@ def create_unionized_smearing_matrix_array(sm, src_dec): sm_a_idx ] - return ( - arr, - sm.true_e_bin_edges, - reco_e_edges, - psi_edges, - ang_err_edges - ) + result = dict({ + 'union_arr': union_arr, + 'log10_true_e_binedges': sm.true_e_bin_edges, + 'log10_reco_e_binedges': reco_e_edges, + 'psi_binedges': psi_edges, + 'ang_err_binedges': ang_err_edges + }) + + return result class PublicDataAeff(object): @@ -482,6 +489,37 @@ def get_aeff_for_sin_true_dec(self, sin_true_dec): return aeff + def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): + """Calculates the detection probability density p(E_nu|sin_dec) in + unit GeV^-1 for the given true energy values. + + Parameters + ---------- + sin_true_dec : float + The sin of the true declination. + true_e : (n,)-shaped 1d numpy ndarray of float + The values of the true energy in GeV for which the probability + density value should get calculated. + + Returns + ------- + det_pd : (n,)-shaped 1d numpy ndarray of float + The detection probability density values for the given true energy + value. + """ + aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + + dE = np.power(10, np.diff(self.log_true_e_binedges)) + + det_pdf = aeff / np.sum(aeff) / dE + + det_pd = np.interp( + true_e, + np.power(10, self.log_true_e_bincenters), + det_pdf) + + return det_pd + def get_aeff_integral_for_sin_true_dec( self, sin_true_dec, log_true_e_min, log_true_e_max): """Calculates the integral of the effective area using the trapezoid From b296a490eefbf0e01aca78c9b0e4d4d43fa01fa5 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 3 May 2022 15:28:42 +0200 Subject: [PATCH 067/274] Don't use log and caching for now --- skyllh/analyses/i3/trad_ps/pdfratio.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index f163bb86b6..d45f8dd65d 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -87,10 +87,7 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): 'For at least one event no background probability can be ' 'calculated! Check your background PDF!') - m = sig_prob == 0 - sig_prob[m] = sys.float_info.min - - ratio = np.log(sig_prob) - np.log(bkg_prob) + ratio = sig_prob / bkg_prob return ratio @@ -103,9 +100,6 @@ def _calculate_ratio_and_gradients(self, tdm, fitparams, fitparams_hash): self._interpolmethod_instance.get_value_and_gradients( tdm, eventdata=None, params=fitparams) - ratio = np.exp(ratio) - gradients = ratio * gradients - # Cache the value and the gradients. self._cache_fitparams_hash = fitparams_hash self._cache_ratio = ratio @@ -135,8 +129,8 @@ def get_ratio(self, tdm, fitparams=None, tl=None): fitparams_hash = make_params_hash(fitparams) # Check if the ratio value is already cached. - if(self._is_cached(tdm, fitparams_hash)): - return self._cache_ratio + #if(self._is_cached(tdm, fitparams_hash)): + # return self._cache_ratio self._calculate_ratio_and_gradients(tdm, fitparams, fitparams_hash) @@ -162,8 +156,8 @@ def get_gradient(self, tdm, fitparams, fitparam_name): pidx = self.convert_signal_fitparam_name_into_index(fitparam_name) # Check if the gradients have been calculated already. - if(self._is_cached(tdm, fitparams_hash)): - return self._cache_gradients[pidx] + #if(self._is_cached(tdm, fitparams_hash)): + # return self._cache_gradients[pidx] # The gradients have not been calculated yet. self._calculate_ratio_and_gradients(tdm, fitparams, fitparams_hash) From 1c423dd99e7a73f543f4fb1e7b0e6ca2c876198b Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 3 May 2022 15:30:13 +0200 Subject: [PATCH 068/274] Calculate signal pdf conceptionally correct --- skyllh/analyses/i3/trad_ps/signalpdf.py | 82 ++++++++++++++++--------- 1 file changed, 54 insertions(+), 28 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 22c0f19883..7cbe651b62 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -735,7 +735,7 @@ def __init__( **kwargs): """Creates a new PDSignalPDFSet instance for the public data. """ - # Extend the fitparam_grid_set + # Extend the fitparam_grid_set. fitparam_grid_set = fitparam_grid_set.copy() fitparam_grid_set.add_extra_lower_and_upper_bin() @@ -748,32 +748,54 @@ def __init__( if(union_sm_arr_pathfilename is not None): with open(union_sm_arr_pathfilename, 'rb') as f: data = pickle.load(f) - union_arr = data['union_arr'] - true_e_bin_edges = data['true_e_bin_edges'] - reco_e_edges = data['reco_e_edges'] - psi_edges = data['psi_edges'] - ang_err_edges = data['ang_err_edges'] - del(data) else: sm = PublicDataSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - (union_arr, - true_e_bin_edges, - reco_e_edges, - psi_edges, - ang_err_edges - ) = create_unionized_smearing_matrix_array(sm, src_dec) + data = create_unionized_smearing_matrix_array(sm, src_dec) del(sm) + union_arr = data['union_arr'] + log_true_e_binedges = data['log10_true_e_binedges'] + reco_e_edges = data['log10_reco_e_binedges'] + psi_edges = data['psi_binedges'] + ang_err_edges = data['ang_err_binedges'] + del(data) + + true_e_bincenters = np.power( + 10, + 0.5*(log_true_e_binedges[:-1] + log_true_e_binedges[1:])) # Load the effective area. aeff = PublicDataAeff( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) + # Calculate the detector's detection probability density for a given + # true declination: p(E_nu|dec) + det_pd = aeff.get_detection_pd_for_sin_true_dec( + np.sin(src_dec), + true_e_bincenters + ) + reco_e_bw = np.diff(reco_e_edges) - psi_edges_bw = np.diff(psi_edges) - ang_err_bw = np.diff(ang_err_edges) + + # For the psi angle we need to consider the solid angle on the shpere + # the psi angle spans. For a unit sphere the solid angle of the + # spherical cap is given as 2pi * (1 - cos(psi)). Hence, for a solid + # angle slice with psi_min and psi_max it is + # 2pi * (cos(psi_min) - cos(psi_max)). + psi_edges_bw = ( + 2 * np.pi * (np.cos(psi_edges[:-1]) - + np.cos(psi_edges[1:])) + ) + + # In analog to the psi angle, the phase space of the ang_err is given + # in the same way. + ang_err_bw = ( + 2 * np.pi * (np.cos(ang_err_edges[:-1]) - + np.cos(ang_err_edges[1:])) + ) + bin_volumes = ( reco_e_bw[:,np.newaxis,np.newaxis] * psi_edges_bw[np.newaxis,:,np.newaxis] * @@ -787,23 +809,27 @@ def create_pdf(union_arr, flux_model, gridfitparams): # The copy is needed to not interfer with other CPU processes. my_flux_model = flux_model.copy(newprop=gridfitparams) - E_nu_min = np.power(10, true_e_bin_edges[:-1]) - E_nu_max = np.power(10, true_e_bin_edges[1:]) + E_nu_min = np.power(10, log_true_e_binedges[:-1]) + E_nu_max = np.power(10, log_true_e_binedges[1:]) - flux_integral = my_flux_model.get_integral(E_nu_min, E_nu_max) - - aeff_integral = aeff.get_aeff_integral_for_sin_true_dec( - np.sin(src_dec), np.log10(E_nu_min), np.log10(E_nu_max)) + # Calculate the flux probability p(E_nu|gamma). + flux_prob = ( + my_flux_model.get_integral(E_nu_min, E_nu_max) / + my_flux_model.get_integral( + np.power(10, log_true_e_binedges[0]), + np.power(10, log_true_e_binedges[-1]) + ) + ) - dE_nu = np.diff(np.power(10, true_e_bin_edges)) + dE_nu = np.diff(np.power(10, log_true_e_binedges)) - avg_exposure = aeff_integral / dE_nu * flux_integral + w = det_pd * dE_nu * flux_prob - arr_ = np.copy(union_arr) - for true_e_idx in range(len(true_e_bin_edges)-1): - arr_[true_e_idx] *= avg_exposure[true_e_idx] - pdf_arr = np.sum(arr_, axis=0) - del(arr_) + transfer = np.copy(union_arr) + for true_e_idx in range(len(log_true_e_binedges)-1): + transfer[true_e_idx] *= w[true_e_idx] + pdf_arr = np.sum(transfer, axis=0) + del(transfer) # Normalize the pdf, which is the probability per bin volume. norm = np.sum(pdf_arr) From 29e222758d58339e65d0b30113115aaf1b9c07a0 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 3 May 2022 17:04:32 +0200 Subject: [PATCH 069/274] Use the probability density instead of the probability per bin --- skyllh/analyses/i3/trad_ps/utils.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 2221ec9998..be38431d7f 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -406,6 +406,27 @@ def create_unionized_smearing_matrix_array(sm, src_dec): if sm_a_idx is None: continue + # Get the bin volume of the smearing matrix's bin. + idx = ( + true_e_idx, true_dec_idx, sm_e_idx) + reco_e_bw = ( + sm.reco_e_upper_edges[idx] - + sm.reco_e_lower_edges[idx] + ) + idx = ( + true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx) + psi_bw = 2 * np.pi * ( + np.cos(sm.psi_lower_edges[idx]) - + np.cos(sm.psi_upper_edges[idx]) + ) + idx = ( + true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx, sm_a_idx) + ang_err_bw = 2 * np.pi * ( + np.cos(sm.ang_err_lower_edges[idx]) - + np.cos(sm.ang_err_upper_edges[idx]) + ) + bin_volume = reco_e_bw * psi_bw * ang_err_bw + union_arr[ true_e_idx, e_idx, @@ -417,7 +438,7 @@ def create_unionized_smearing_matrix_array(sm, src_dec): sm_e_idx, sm_p_idx, sm_a_idx - ] + ] / bin_volume result = dict({ 'union_arr': union_arr, From d717b98905e32ce4c09ab14ee26513a0c7eb72eb Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 4 May 2022 11:24:58 +0200 Subject: [PATCH 070/274] Fixed typo. --- skyllh/analyses/i3/trad_ps/signal_generator.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index 88a8d686be..56168dfa72 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -85,7 +85,7 @@ def _generate_events( # Determine the true energy range for which log_e PDFs are available. (min_log_true_e, - max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pfds( + max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( dec_idx) # First draw a true neutrino energy from the hypothesis spectrum. From 8618911d9fabca6a5a82fb5fda8251c6a167bd63 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 4 May 2022 14:33:35 +0200 Subject: [PATCH 071/274] Add function to get the module and class name of a class inst --- skyllh/core/py.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/skyllh/core/py.py b/skyllh/core/py.py index c9c623148a..64f179b62c 100644 --- a/skyllh/core/py.py +++ b/skyllh/core/py.py @@ -84,6 +84,11 @@ def classname(obj): """ return typename(type(obj)) +def module_classname(obj): + """Returns the module and class name of the class instance ``obj``. + """ + return '{}.{}'.format(obj.__module__, classname(obj)) + def get_byte_size_prefix(size): """Determines the biggest size prefix for the given size in bytes such that the new size is still greater one. From b7dfe2346273232fa571b5da4fa824684fd8dbcf Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 4 May 2022 14:34:57 +0200 Subject: [PATCH 072/274] Call the get_prob method of the signal pdf set instance --- skyllh/analyses/i3/trad_ps/pdfratio.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index d45f8dd65d..0b71d87d81 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -76,11 +76,8 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): """Select the signal PDF for the given fit parameter grid point and evaluates the S/B ratio for all the given events. """ - sig_pdf = self.signalpdfset.get_pdf(gridfitparams) - bkg_pdf = self.backgroundpdf - - (sig_prob, _) = sig_pdf.get_prob(tdm) - (bkg_prob, _) = bkg_pdf.get_prob(tdm) + (sig_prob, _) = self.signalpdfset.get_prob(tdm, gridfitparams) + (bkg_prob, _) = self.backgroundpdf.get_prob(tdm) if np.any(np.invert(bkg_prob > 0)): raise ValueError( From 173aa054f323045d85232ac0201522ca086836ad Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 4 May 2022 17:40:05 +0200 Subject: [PATCH 073/274] Add logging information and use 1d phase spaces --- skyllh/analyses/i3/trad_ps/signalpdf.py | 99 +++++++++++++++++++++---- skyllh/analyses/i3/trad_ps/utils.py | 65 +++++++++++++--- 2 files changed, 139 insertions(+), 25 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 7cbe651b62..17498bb6f6 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -8,6 +8,8 @@ from scipy.interpolate import UnivariateSpline from itertools import product +from skyllh.core.py import module_classname +from skyllh.core.debugging import get_logger from skyllh.core.timing import TaskTimer from skyllh.core.binning import ( BinningDefinition, @@ -43,6 +45,7 @@ PublicDataSmearingMatrix ) + class PublicDataSignalGenerator(object): def __init__(self, ds, **kwargs): """Creates a new instance of the signal generator for generating signal @@ -734,8 +737,29 @@ def __init__( ppbar=None, **kwargs): """Creates a new PDSignalPDFSet instance for the public data. + + Parameters + ---------- + ds : I3Dataset instance + The I3Dataset instance that defines the public data dataset. + src_dec : float + The declination of the source in radians. + flux_model : FluxModel instance + The FluxModel instance that defines the source's flux model. """ - # Extend the fitparam_grid_set. + self._logger = get_logger(module_classname(self)) + + # Check for the correct types of the arguments. + if not isinstance(ds, I3Dataset): + raise TypeError( + 'The ds argument must be an instance of I3Dataset!') + + if not isinstance(flux_model, FluxModel): + raise TypeError( + 'The flux_model argument must be an instance of FluxModel!') + + # Extend the fitparam_grid_set to allow for parameter interpolation + # values at the grid edges. fitparam_grid_set = fitparam_grid_set.copy() fitparam_grid_set.add_extra_lower_and_upper_bin() @@ -761,21 +785,42 @@ def __init__( ang_err_edges = data['ang_err_binedges'] del(data) + log10_true_e_bincenters = 0.5*( + log_true_e_binedges[:-1] + log_true_e_binedges[1:]) + true_e_bincenters = np.power( 10, 0.5*(log_true_e_binedges[:-1] + log_true_e_binedges[1:])) + # Calculate the neutrino enegry bin widths in GeV. + dE_nu = np.diff(np.power(10, log_true_e_binedges)) + self._logger.debug( + 'dE_nu = {}'.format(dE_nu) + ) + + dlog10E_nu = np.diff(log_true_e_binedges) + # Load the effective area. aeff = PublicDataAeff( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) - # Calculate the detector's detection probability density for a given - # true declination: p(E_nu|dec) - det_pd = aeff.get_detection_pd_for_sin_true_dec( + # Calculate the detector's neutrino energy detection probability to + # detect a neutrino of energy E_nu given a neutrino declination: + # p(E_nu|dec) + det_pd_log10E = aeff.get_detection_pd_in_log10E_for_sin_true_dec( np.sin(src_dec), - true_e_bincenters + log10_true_e_bincenters ) + det_prob = det_pd_log10E * dlog10E_nu + + self._logger.debug('det_prob = {}, sum = {}'.format( + det_prob, np.sum(det_prob))) + + if not np.isclose(np.sum(det_prob), 1, rtol=0.003): + raise ValueError( + 'The sum of the detection probabilities is not unity! It is ' + '{}.'.format(np.sum(det_prob))) reco_e_bw = np.diff(reco_e_edges) @@ -784,16 +829,23 @@ def __init__( # spherical cap is given as 2pi * (1 - cos(psi)). Hence, for a solid # angle slice with psi_min and psi_max it is # 2pi * (cos(psi_min) - cos(psi_max)). + #psi_edges_bw = ( + # 2 * np.pi * (np.cos(psi_edges[:-1]) - + # np.cos(psi_edges[1:])) + #) psi_edges_bw = ( - 2 * np.pi * (np.cos(psi_edges[:-1]) - - np.cos(psi_edges[1:])) + psi_edges[1:] - + psi_edges[:-1] ) - # In analog to the psi angle, the phase space of the ang_err is given # in the same way. + #ang_err_bw = ( + # 2 * np.pi * (np.cos(ang_err_edges[:-1]) - + # np.cos(ang_err_edges[1:])) + #) ang_err_bw = ( - 2 * np.pi * (np.cos(ang_err_edges[:-1]) - - np.cos(ang_err_edges[1:])) + ang_err_edges[1:] - + ang_err_edges[:-1] ) bin_volumes = ( @@ -812,6 +864,13 @@ def create_pdf(union_arr, flux_model, gridfitparams): E_nu_min = np.power(10, log_true_e_binedges[:-1]) E_nu_max = np.power(10, log_true_e_binedges[1:]) + nbins_log_true_e = len(log_true_e_binedges) - 1 + + self._logger.debug( + 'Generate signal PDF for parameters {} in {} E_nu bins.'.format( + gridfitparams, nbins_log_true_e) + ) + # Calculate the flux probability p(E_nu|gamma). flux_prob = ( my_flux_model.get_integral(E_nu_min, E_nu_max) / @@ -820,13 +879,18 @@ def create_pdf(union_arr, flux_model, gridfitparams): np.power(10, log_true_e_binedges[-1]) ) ) + if not np.isclose(np.sum(flux_prob), 1): + raise ValueError( + 'The sum of the flux probabilities is not unity!') - dE_nu = np.diff(np.power(10, log_true_e_binedges)) + self._logger.debug( + 'flux_prob = {}'.format(flux_prob) + ) - w = det_pd * dE_nu * flux_prob + w = det_prob * flux_prob transfer = np.copy(union_arr) - for true_e_idx in range(len(log_true_e_binedges)-1): + for true_e_idx in range(nbins_log_true_e): transfer[true_e_idx] *= w[true_e_idx] pdf_arr = np.sum(transfer, axis=0) del(transfer) @@ -894,14 +958,21 @@ def get_prob(self, tdm, gridfitparams, tl=None): ------- prob : 1d ndarray The array with the signal energy probability for each event. + grads : (N_fitparams,N_events)-shaped ndarray | None + The 2D numpy ndarray holding the gradients of the PDF w.r.t. + each fit parameter for each event. The order of the gradients + is the same as the order of floating parameters specified through + the ``param_set`` property. + It is ``None``, if this PDF does not depend on any parameters. Raises ------ KeyError If no energy PDF can be found for the given signal parameter values. """ + print('Getting signal PDF for gridfitparams={}'.format(str(gridfitparams))) pdf = self.get_pdf(gridfitparams) (prob, grads) = pdf.get_prob(tdm, tl=tl) - return prob + return (prob, grads) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index be38431d7f..2db38ab575 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -2,6 +2,8 @@ import numpy as np +from scipy import interpolate + from skyllh.core.binning import ( get_bincenters_from_binedges ) @@ -415,15 +417,23 @@ def create_unionized_smearing_matrix_array(sm, src_dec): ) idx = ( true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx) - psi_bw = 2 * np.pi * ( - np.cos(sm.psi_lower_edges[idx]) - - np.cos(sm.psi_upper_edges[idx]) + #psi_bw = 2 * np.pi * ( + # np.cos(sm.psi_lower_edges[idx]) - + # np.cos(sm.psi_upper_edges[idx]) + #) + psi_bw = ( + sm.psi_upper_edges[idx] - + sm.psi_lower_edges[idx] ) idx = ( true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx, sm_a_idx) - ang_err_bw = 2 * np.pi * ( - np.cos(sm.ang_err_lower_edges[idx]) - - np.cos(sm.ang_err_upper_edges[idx]) + #ang_err_bw = 2 * np.pi * ( + # np.cos(sm.ang_err_lower_edges[idx]) - + # np.cos(sm.ang_err_upper_edges[idx]) + #) + ang_err_bw = ( + sm.ang_err_upper_edges[idx] - + sm.ang_err_lower_edges[idx] ) bin_volume = reco_e_bw * psi_bw * ang_err_bw @@ -530,14 +540,47 @@ def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): """ aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - dE = np.power(10, np.diff(self.log_true_e_binedges)) + dE = np.diff(np.power(10, self.log_true_e_binedges)) det_pdf = aeff / np.sum(aeff) / dE - det_pd = np.interp( - true_e, - np.power(10, self.log_true_e_bincenters), - det_pdf) + x = np.power(10, self.log_true_e_bincenters) + y = det_pdf + tck = interpolate.splrep(x, y, k=2, s=0) + + det_pd = interpolate.splev(true_e, tck, der=0) + + return det_pd + + def get_detection_pd_in_log10E_for_sin_true_dec( + self, sin_true_dec, log10_true_e): + """Calculates the detection probability density p(E_nu|sin_dec) in + unit log10(GeV)^-1 for the given true energy values. + + Parameters + ---------- + sin_true_dec : float + The sin of the true declination. + log10_true_e : (n,)-shaped 1d numpy ndarray of float + The log10 values of the true energy in GeV for which the + probability density value should get calculated. + + Returns + ------- + det_pd : (n,)-shaped 1d numpy ndarray of float + The detection probability density values for the given true energy + value. + """ + aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + + dlog10E = np.diff(self.log_true_e_binedges) + + det_pdf = aeff / np.sum(aeff) / dlog10E + + spl = interpolate.splrep( + self.log_true_e_bincenters, det_pdf, k=1, s=0) + + det_pd = interpolate.splev(log10_true_e, spl, der=0) return det_pd From 5571762ad96ef00d20172f4b2131ecf905b7a723 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 5 May 2022 12:33:12 +0200 Subject: [PATCH 074/274] Use linear interpolation --- skyllh/analyses/i3/trad_ps/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 2db38ab575..1200c6129b 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -546,7 +546,7 @@ def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): x = np.power(10, self.log_true_e_bincenters) y = det_pdf - tck = interpolate.splrep(x, y, k=2, s=0) + tck = interpolate.splrep(x, y, k=1, s=0) det_pd = interpolate.splev(true_e, tck, der=0) From f81233e3e1f783189d7fbbf1e64ed2013108e21e Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 6 May 2022 10:36:42 +0200 Subject: [PATCH 075/274] Use the integral of the Aeff within the E_nu bin --- skyllh/analyses/i3/trad_ps/signalpdf.py | 68 ++++++++++--------------- skyllh/analyses/i3/trad_ps/utils.py | 39 ++++++++++++++ 2 files changed, 67 insertions(+), 40 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 17498bb6f6..86a27c7fe4 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -623,7 +623,7 @@ class PDSignalPDF(PDF, IsSignalPDF): """This class provides a signal pdf for a given spectrial index value. """ def __init__( - self, pdf_arr, log_e_edges, psi_edges, ang_err_edges, **kwargs): + self, pdf_arr, log_e_edges, psi_edges, ang_err_edges, true_e_prob, **kwargs): """Creates a new signal PDF for the public data. """ super().__init__(**kwargs) @@ -639,6 +639,8 @@ def __init__( self.ang_err_lower_edges = ang_err_edges[:-1] self.ang_err_upper_edges = ang_err_edges[1:] + self.true_e_prob = true_e_prob + # Add the PDF axes. self.add_axis(PDFAxis( name='log_energy', @@ -785,21 +787,18 @@ def __init__( ang_err_edges = data['ang_err_binedges'] del(data) - log10_true_e_bincenters = 0.5*( - log_true_e_binedges[:-1] + log_true_e_binedges[1:]) - true_e_bincenters = np.power( 10, 0.5*(log_true_e_binedges[:-1] + log_true_e_binedges[1:])) + true_e_binedges = np.power(10, log_true_e_binedges) + # Calculate the neutrino enegry bin widths in GeV. - dE_nu = np.diff(np.power(10, log_true_e_binedges)) + dE_nu = np.diff(true_e_binedges) self._logger.debug( 'dE_nu = {}'.format(dE_nu) ) - dlog10E_nu = np.diff(log_true_e_binedges) - # Load the effective area. aeff = PublicDataAeff( pathfilenames=ds.get_abs_pathfilename_list( @@ -808,51 +807,32 @@ def __init__( # Calculate the detector's neutrino energy detection probability to # detect a neutrino of energy E_nu given a neutrino declination: # p(E_nu|dec) - det_pd_log10E = aeff.get_detection_pd_in_log10E_for_sin_true_dec( - np.sin(src_dec), - log10_true_e_bincenters - ) - det_prob = det_pd_log10E * dlog10E_nu + det_prob = np.empty((len(dE_nu),), dtype=np.double) + for i in range(len(dE_nu)): + det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( + sin_true_dec = np.sin(src_dec), + true_e_min = true_e_binedges[i], + true_e_max = true_e_binedges[i+1] + ) self._logger.debug('det_prob = {}, sum = {}'.format( det_prob, np.sum(det_prob))) - if not np.isclose(np.sum(det_prob), 1, rtol=0.003): + if not np.isclose(np.sum(det_prob), 1, rtol=0.06): raise ValueError( 'The sum of the detection probabilities is not unity! It is ' '{}.'.format(np.sum(det_prob))) reco_e_bw = np.diff(reco_e_edges) - - # For the psi angle we need to consider the solid angle on the shpere - # the psi angle spans. For a unit sphere the solid angle of the - # spherical cap is given as 2pi * (1 - cos(psi)). Hence, for a solid - # angle slice with psi_min and psi_max it is - # 2pi * (cos(psi_min) - cos(psi_max)). - #psi_edges_bw = ( - # 2 * np.pi * (np.cos(psi_edges[:-1]) - - # np.cos(psi_edges[1:])) - #) - psi_edges_bw = ( - psi_edges[1:] - - psi_edges[:-1] - ) - # In analog to the psi angle, the phase space of the ang_err is given - # in the same way. - #ang_err_bw = ( - # 2 * np.pi * (np.cos(ang_err_edges[:-1]) - - # np.cos(ang_err_edges[1:])) - #) - ang_err_bw = ( - ang_err_edges[1:] - - ang_err_edges[:-1] - ) + psi_edges_bw = np.diff(psi_edges) + ang_err_bw = np.diff(ang_err_edges) bin_volumes = ( reco_e_bw[:,np.newaxis,np.newaxis] * psi_edges_bw[np.newaxis,:,np.newaxis] * ang_err_bw[np.newaxis,np.newaxis,:]) + # Create the pdf in gamma for different gamma values. def create_pdf(union_arr, flux_model, gridfitparams): """Creates a pdf for a specific gamma value. @@ -887,11 +867,19 @@ def create_pdf(union_arr, flux_model, gridfitparams): 'flux_prob = {}'.format(flux_prob) ) - w = det_prob * flux_prob + p = flux_prob * det_prob + self._logger.debug( + 'p = {}, sum(p)={}'.format(p, sum(p)) + ) + + true_e_prob = p / np.sum(p) + + self._logger.debug( + f'true_e_prob = {true_e_prob}') transfer = np.copy(union_arr) for true_e_idx in range(nbins_log_true_e): - transfer[true_e_idx] *= w[true_e_idx] + transfer[true_e_idx] *= true_e_prob[true_e_idx] pdf_arr = np.sum(transfer, axis=0) del(transfer) @@ -906,7 +894,7 @@ def create_pdf(union_arr, flux_model, gridfitparams): pdf_arr /= bin_volumes pdf = PDSignalPDF( - pdf_arr, reco_e_edges, psi_edges, ang_err_edges) + pdf_arr, reco_e_edges, psi_edges, ang_err_edges, true_e_prob) return pdf diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 1200c6129b..b8536d5359 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -3,6 +3,7 @@ import numpy as np from scipy import interpolate +from scipy import integrate from skyllh.core.binning import ( get_bincenters_from_binedges @@ -584,6 +585,44 @@ def get_detection_pd_in_log10E_for_sin_true_dec( return det_pd + def get_detection_prob_for_sin_true_dec( + self, sin_true_dec, true_e_min, true_e_max): + """Calculates the detection probability for a given sin declination. + + Parameters + ---------- + sin_true_dec : float + The sin of the true declination. + true_e_min : float + The minimum energy in GeV. + true_e_max : float + The maximum energy in GeV. + + Returns + ------- + det_prob : float + The true energy detection probability. + """ + aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + + true_e_binedges = np.power(10, self.log_true_e_binedges) + + dE = np.diff(true_e_binedges) + + det_pdf = aeff / np.sum(aeff) / dE + + x = np.power(10, self.log_true_e_bincenters) + y = det_pdf + tck = interpolate.splrep(x, y, k=1, s=0) + + def _eval_func(x): + return interpolate.splev(x, tck, der=0) + + r = integrate.quad(_eval_func, true_e_min, true_e_max) + det_prob = r[0] + + return det_prob + def get_aeff_integral_for_sin_true_dec( self, sin_true_dec, log_true_e_min, log_true_e_max): """Calculates the integral of the effective area using the trapezoid From b6adb80cac61e8b3ebb60bfd6ee0a23af77d9858 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 10 May 2022 18:44:10 +0200 Subject: [PATCH 076/274] Separate spatial and energy signal pdfs --- skyllh/analyses/i3/trad_ps/signalpdf.py | 65 ++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 86a27c7fe4..a1ae943734 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -623,12 +623,20 @@ class PDSignalPDF(PDF, IsSignalPDF): """This class provides a signal pdf for a given spectrial index value. """ def __init__( - self, pdf_arr, log_e_edges, psi_edges, ang_err_edges, true_e_prob, **kwargs): + self, f_s, f_e, log_e_edges, psi_edges, ang_err_edges, + true_e_prob, **kwargs): """Creates a new signal PDF for the public data. + + Parameters + ---------- + f_s : (n_e_reco, n_psi, n_ang_err)-shaped 3D numpy ndarray + The conditional PDF array P(Psi|E_reco,ang_err). + """ super().__init__(**kwargs) - self.pdf_arr = pdf_arr + self.f_s = f_s + self.f_e = f_e self.log_e_lower_edges = log_e_edges[:-1] self.log_e_upper_edges = log_e_edges[1:] @@ -658,6 +666,24 @@ def __init__( vmax=self.ang_err_upper_edges[-1]) ) + # Check integrety. + integral = np.sum( + #1/(2*np.pi*np.sin(0.5*(psi_edges[None,1:,None]+ + # psi_edges[None,:-1,None]) + # )) * + self.f_s * np.diff(psi_edges)[None,:,None], axis=1) + if not np.all(np.isclose(integral[integral > 0], 1)): + raise ValueError( + 'The integral over Psi of the spatial term must be unity! ' + 'But it is {}!'.format(integral[integral > 0])) + integral = np.sum( + self.f_e * np.diff(log_e_edges) + ) + if not np.isclose(integral, 1): + raise ValueError( + 'The integral over log10_E of the energy term must be unity! ' + 'But it is {}!'.format(integral)) + def assert_is_valid_for_trial_data(self, tdm): pass @@ -711,8 +737,6 @@ def get_prob(self, tdm, params=None, tl=None): (ang_err < self.ang_err_upper_edges[-1]) ) - prob = np.zeros((len(log_e),), dtype=np.double) - log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( self.log_e_lower_edges, self.log_e_upper_edges, log_e[m]) psi_idxs = get_bin_indices_from_lower_and_upper_binedges( @@ -720,9 +744,16 @@ def get_prob(self, tdm, params=None, tl=None): ang_err_idxs = get_bin_indices_from_lower_and_upper_binedges( self.ang_err_lower_edges, self.ang_err_upper_edges, ang_err[m]) - prob[m] = self.pdf_arr[(log_e_idxs, psi_idxs, ang_err_idxs)] + pd_spatial = np.zeros((len(psi),), dtype=np.double) + pd_spatial[m] = ( + 1/(2*np.pi * np.sin(psi[m])) * + self.f_s[(log_e_idxs, psi_idxs, ang_err_idxs)] + ) + + pd_energy = np.zeros((len(log_e),), dtype=np.double) + pd_energy[m] = self.f_e[log_e_idxs] - return (prob, None) + return (pd_spatial * pd_energy, None) class PDSignalPDFSet(PDFSet, IsSignalPDF, IsParallelizable): @@ -893,8 +924,28 @@ def create_pdf(union_arr, flux_model, gridfitparams): pdf_arr /= norm pdf_arr /= bin_volumes + # Create the spatial PDF f_s = P(Psi|E_reco,ang_err) = + # P(E_reco,Psi,ang_err) / \int dPsi P(E_reco,Psi,ang_err). + marg_pdf = np.sum( + pdf_arr * psi_edges_bw[np.newaxis,:,np.newaxis], + axis=1, + keepdims=True + ) + f_s = pdf_arr / marg_pdf + f_s[np.isnan(f_s)] = 0 + + # Create the enegry PDF f_e = P(log10_E_reco|dec) = + # \int dPsi dang_err P(E_reco,Psi,ang_err). + f_e = np.sum( + pdf_arr * psi_edges_bw[np.newaxis,:,np.newaxis] * + ang_err_bw[np.newaxis,np.newaxis,:], + axis=(1,2)) + + del(pdf_arr) + pdf = PDSignalPDF( - pdf_arr, reco_e_edges, psi_edges, ang_err_edges, true_e_prob) + f_s, f_e, reco_e_edges, psi_edges, ang_err_edges, + true_e_prob) return pdf From d3bfc7ad122ae0dc11849e2535be6ec94b49a1a6 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 11 May 2022 18:13:00 +0200 Subject: [PATCH 077/274] Use Rayleigh distribution for the PSF and split spatial and energy PDFs --- skyllh/analyses/i3/trad_ps/analysis.py | 59 ++-- skyllh/analyses/i3/trad_ps/pdfratio.py | 9 +- skyllh/analyses/i3/trad_ps/signalpdf.py | 368 +++++++++++++++++++++++- skyllh/core/signalpdf.py | 93 +++++- 4 files changed, 480 insertions(+), 49 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index 6f4d6680a6..ee3e72c5d6 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -46,7 +46,7 @@ from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod # Classes to define the signal and background PDFs. -from skyllh.core.signalpdf import GaussianPSFPointLikeSourceSignalSpatialPDF +from skyllh.core.signalpdf import RayleighPSFPointSourceSignalSpatialPDF from skyllh.i3.signalpdf import SignalI3EnergyPDFSet from skyllh.i3.backgroundpdf import ( DataBackgroundI3SpatialPDF, @@ -84,7 +84,7 @@ PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod ) from skyllh.analyses.i3.trad_ps.signalpdf import ( - PDSignalPDFSet + PDSignalEnergyPDFSet ) from skyllh.analyses.i3.trad_ps.pdfratio import ( PDPDFRatio @@ -210,7 +210,7 @@ def create_analysis( fitparam_ns = FitParameter('ns', 0, 1e3, ns_seed) # Define the gamma fit parameter. - fitparam_gamma = FitParameter('gamma', valmin=1, valmax=4, initial=gamma_seed) + fitparam_gamma = FitParameter('gamma', valmin=1, valmax=5, initial=gamma_seed) # Define the detector signal efficiency implementation method for the # IceCube detector and this source and flux_model. @@ -283,40 +283,15 @@ def create_analysis( log_energy_binning = ds.get_binning_definition('log_energy') # Create the spatial PDF ratio instance for this dataset. - """ - spatial_sigpdf = GaussianPSFPointLikeSourceSignalSpatialPDF( + spatial_sigpdf = RayleighPSFPointSourceSignalSpatialPDF( dec_range=np.arcsin(sin_dec_binning.range)) - """ spatial_bkgpdf = DataBackgroundI3SpatialPDF( data.exp, sin_dec_binning) - """ spatial_pdfratio = SpatialSigOverBkgPDFRatio( spatial_sigpdf, spatial_bkgpdf) - """ + # Create the energy PDF ratio instance for this dataset. - smoothing_filter = BlockSmoothingFilter(nbins=1) - """ - energy_sigpdfset = PublicDataSignalI3EnergyPDFSet( - rss=rss, - ds=ds, - flux_model=flux_model, - fitparam_grid_set=gamma_grid, - n_events=n_mc_events, - smoothing_filter=smoothing_filter, - ppbar=pbar) - """ - energy_bkgpdf = DataBackgroundI3EnergyPDF( - data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) - """ - fillmethod = Skylab2SkylabPDFRatioFillMethod() - energy_pdfratio = I3EnergySigSetOverBkgPDFRatioSpline( - energy_sigpdfset, - energy_bkgpdf, - fillmethod=fillmethod, - ppbar=pbar) - """ - - sig_pdf_set = PDSignalPDFSet( + energy_sigpdfset = PDSignalEnergyPDFSet( ds=ds, src_dec=source.dec, flux_model=flux_model, @@ -325,16 +300,16 @@ def create_analysis( cache_dir, 'union_sm_{}.pkl'.format(ds.name)), ppbar=ppbar ) + smoothing_filter = BlockSmoothingFilter(nbins=1) + energy_bkgpdf = DataBackgroundI3EnergyPDF( + data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) - bkg_pdf = spatial_bkgpdf * energy_bkgpdf - - pdfratio = PDPDFRatio( - sig_pdf_set=sig_pdf_set, - bkg_pdf=bkg_pdf + energy_pdfratio = PDPDFRatio( + sig_pdf_set=energy_sigpdfset, + bkg_pdf=energy_bkgpdf ) - #pdfratios = [ spatial_pdfratio, energy_pdfratio ] - pdfratios = [ pdfratio ] + pdfratios = [ spatial_pdfratio, energy_pdfratio ] analysis.add_dataset( ds, data, pdfratios, tdm, event_selection_method) @@ -390,10 +365,10 @@ def create_analysis( CFG['multiproc']['ncpu'] = args.ncpu sample_seasons = [ - #('PublicData_10y_ps', 'IC40'), - #('PublicData_10y_ps', 'IC59'), - #('PublicData_10y_ps', 'IC79'), - #('PublicData_10y_ps', 'IC86_I'), + ('PublicData_10y_ps', 'IC40'), + ('PublicData_10y_ps', 'IC59'), + ('PublicData_10y_ps', 'IC79'), + ('PublicData_10y_ps', 'IC86_I'), ('PublicData_10y_ps', 'IC86_II-VII') ] diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index 0b71d87d81..c98c7b1645 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -76,8 +76,13 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): """Select the signal PDF for the given fit parameter grid point and evaluates the S/B ratio for all the given events. """ - (sig_prob, _) = self.signalpdfset.get_prob(tdm, gridfitparams) - (bkg_prob, _) = self.backgroundpdf.get_prob(tdm) + sig_prob = self.signalpdfset.get_prob(tdm, gridfitparams) + if isinstance(sig_prob, tuple): + (sig_prob, _) = sig_prob + + bkg_prob = self.backgroundpdf.get_prob(tdm) + if isinstance(bkg_prob, tuple): + (bkg_prob, _) = bkg_prob if np.any(np.invert(bkg_prob > 0)): raise ValueError( diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index a1ae943734..f908334dc7 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -2,6 +2,7 @@ import numpy as np +import os import pickle from copy import deepcopy @@ -619,6 +620,367 @@ def get_prob(self, tdm, gridfitparams): return prob +class PDSignalEnergyPDF(PDF, IsSignalPDF): + """This class provides a signal energy PDF for a spectrial index value. + """ + def __init__( + self, f_e, log_e_edges, **kwargs): + """Creates a new signal energy PDF instance for a particular spectral + index value. + """ + super().__init__(**kwargs) + + self.f_e = f_e + + self.log_e_lower_edges = log_e_edges[:-1] + self.log_e_upper_edges = log_e_edges[1:] + + # Add the PDF axes. + self.add_axis(PDFAxis( + name='log_energy', + vmin=self.log_e_lower_edges[0], + vmax=self.log_e_upper_edges[-1]) + ) + + # Check integrity. + integral = np.sum(self.f_e * np.diff(log_e_edges)) + if not np.isclose(integral, 1): + raise ValueError( + 'The integral over log10_E of the energy term must be unity! ' + 'But it is {}!'.format(integral)) + + def assert_is_valid_for_trial_data(self, tdm): + pass + + def get_prob(self, tdm, params=None, tl=None): + """Calculates the probability density for the events given by the + TrialDataManager. + + Parameters + ---------- + tdm : TrialDataManager instance + The TrialDataManager instance holding the data events for which the + probability should be looked up. The following data fields are + required: + - 'log_energy' + The log10 of the reconstructed energy. + - 'psi' + The opening angle from the source to the event in radians. + - 'ang_err' + The angular error of the event in radians. + params : dict | None + The dictionary containing the parameter names and values for which + the probability should get calculated. + By definition this PDF does not depend on parameters. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + prob : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. + grads : (N_fitparams,N_events)-shaped ndarray | None + The 2D numpy ndarray holding the gradients of the PDF w.r.t. + each fit parameter for each event. The order of the gradients + is the same as the order of floating parameters specified through + the ``param_set`` property. + It is ``None``, if this PDF does not depend on any parameters. + """ + log_e = tdm.get_data('log_energy') + + # Select events that actually have a signal enegry PDF. + # All other events will get zero signal probability. + m = ( + (log_e >= self.log_e_lower_edges[0]) & + (log_e < self.log_e_upper_edges[-1]) + ) + + log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( + self.log_e_lower_edges, self.log_e_upper_edges, log_e[m]) + + pd = np.zeros((len(log_e),), dtype=np.double) + pd[m] = self.f_e[log_e_idxs] + + return (pd, None) + + +class PDSignalEnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): + """This class provides a signal energy PDF set for the public data. + It creates a set of PDSignalEnergyPDF instances, one for each spectral + index value on a grid. + """ + def __init__( + self, + ds, + src_dec, + flux_model, + fitparam_grid_set, + union_sm_arr_pathfilename=None, + ncpu=None, + ppbar=None, + **kwargs): + """Creates a new PDSignalEnergyPDFSet instance for the public data. + + Parameters + ---------- + ds : I3Dataset instance + The I3Dataset instance that defines the public data dataset. + src_dec : float + The declination of the source in radians. + flux_model : FluxModel instance + The FluxModel instance that defines the source's flux model. + fitparam_grid_set : ParameterGrid | ParameterGridSet instance + The parameter grid set defining the grids of the fit parameters. + union_sm_arr_pathfilename : str | None + The pathfilename of the unionized smearing matrix array file from + which the unionized smearing matrix array should get loaded from. + If None, the unionized smearing matrix array will be created. + """ + self._logger = get_logger(module_classname(self)) + + # Check for the correct types of the arguments. + if not isinstance(ds, I3Dataset): + raise TypeError( + 'The ds argument must be an instance of I3Dataset!') + + if not isinstance(flux_model, FluxModel): + raise TypeError( + 'The flux_model argument must be an instance of FluxModel!') + + if (not isinstance(fitparam_grid_set, ParameterGrid)) and\ + (not isinstance(fitparam_grid_set, ParameterGridSet)): + raise TypeError( + 'The fitparam_grid_set argument must be an instance of type ' + 'ParameterGrid or ParameterGridSet!') + + # Extend the fitparam_grid_set to allow for parameter interpolation + # values at the grid edges. + fitparam_grid_set = fitparam_grid_set.copy() + fitparam_grid_set.add_extra_lower_and_upper_bin() + + super().__init__( + pdf_type=PDF, + fitparams_grid_set=fitparam_grid_set, + ncpu=ncpu + ) + + # Load the unionized smearing matrix array or create it if no one was + # specified. + if ((union_sm_arr_pathfilename is not None) and + os.path.exists(union_sm_arr_pathfilename)): + self._logger.info( + 'Loading unionized smearing matrix from file "{}".'.format( + union_sm_arr_pathfilename)) + with open(union_sm_arr_pathfilename, 'rb') as f: + data = pickle.load(f) + else: + pathfilenames = ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile')) + self._logger.info( + 'Creating unionized smearing matrix from smearing matrix file ' + '"{}".'.format( + pathfilenames)) + sm = PublicDataSmearingMatrix( + pathfilenames=pathfilenames) + data = create_unionized_smearing_matrix_array(sm, src_dec) + if union_sm_arr_pathfilename is not None: + self._logger.info( + 'Saving unionized smearing matrix to file "{}".'.format( + union_sm_arr_pathfilename)) + with open(union_sm_arr_pathfilename, 'wb') as f: + pickle.dump(data, f) + del(sm) + union_arr = data['union_arr'] + log10_true_e_binedges = data['log10_true_e_binedges'] + log10_reco_e_edges = data['log10_reco_e_binedges'] + psi_edges = data['psi_binedges'] + ang_err_edges = data['ang_err_binedges'] + del(data) + + true_e_binedges = np.power(10, log10_true_e_binedges) + nbins_true_e = len(true_e_binedges) - 1 + + # Calculate the neutrino enegry bin widths in GeV. + dE_nu = np.diff(true_e_binedges) + self._logger.debug( + 'dE_nu = {}'.format(dE_nu) + ) + + # Load the effective area. + aeff = PublicDataAeff( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('eff_area_datafile'))) + + # Calculate the detector's neutrino energy detection probability to + # detect a neutrino of energy E_nu given a neutrino declination: + # p(E_nu|dec) + det_prob = np.empty((len(dE_nu),), dtype=np.double) + for i in range(len(dE_nu)): + det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( + sin_true_dec = np.sin(src_dec), + true_e_min = true_e_binedges[i], + true_e_max = true_e_binedges[i+1] + ) + + self._logger.debug('det_prob = {}, sum = {}'.format( + det_prob, np.sum(det_prob))) + + if not np.isclose(np.sum(det_prob), 1, rtol=0.02): + raise ValueError( + 'The sum of the detection probabilities is not unity! It is ' + '{}.'.format(np.sum(det_prob))) + + log10_reco_e_bw = np.diff(log10_reco_e_edges) + psi_edges_bw = np.diff(psi_edges) + ang_err_bw = np.diff(ang_err_edges) + + bin_volumes = ( + log10_reco_e_bw[:,np.newaxis,np.newaxis] * + psi_edges_bw[np.newaxis,:,np.newaxis] * + ang_err_bw[np.newaxis,np.newaxis,:]) + + # Create the energy pdf for different gamma values. + def create_energy_pdf(union_arr, flux_model, gridfitparams): + """Creates an energy pdf for a specific gamma value. + """ + # Create a copy of the FluxModel with the given flux parameters. + # The copy is needed to not interfer with other CPU processes. + my_flux_model = flux_model.copy(newprop=gridfitparams) + + E_nu_min = true_e_binedges[:-1] + E_nu_max = true_e_binedges[1:] + + self._logger.debug( + 'Generate signal energy PDF for parameters {} in {} E_nu ' + 'bins.'.format( + gridfitparams, nbins_true_e) + ) + + # Calculate the flux probability p(E_nu|gamma). + flux_prob = ( + my_flux_model.get_integral(E_nu_min, E_nu_max) / + my_flux_model.get_integral( + true_e_binedges[0], + true_e_binedges[-1] + ) + ) + if not np.isclose(np.sum(flux_prob), 1): + raise ValueError( + 'The sum of the flux probabilities is not unity!') + + self._logger.debug( + 'flux_prob = {}'.format(flux_prob) + ) + + p = flux_prob * det_prob + self._logger.debug( + 'p = {}, sum(p)={}'.format(p, np.sum(p)) + ) + + true_e_prob = p / np.sum(p) + + self._logger.debug( + f'true_e_prob = {true_e_prob}') + + transfer = np.copy(union_arr) + for true_e_idx in range(nbins_true_e): + transfer[true_e_idx] *= true_e_prob[true_e_idx] + pdf_arr = np.sum(transfer, axis=0) + del(transfer) + + # Normalize the pdf, which is the probability per bin volume. + norm = np.sum(pdf_arr) + if norm == 0: + raise ValueError( + 'The signal PDF is empty for {}! This should ' + 'not happen. Check the parameter ranges!'.format( + str(gridfitparams))) + pdf_arr /= norm + pdf_arr /= bin_volumes + + # Create the enegry PDF f_e = P(log10_E_reco|dec) = + # \int dPsi dang_err P(E_reco,Psi,ang_err). + f_e = np.sum( + pdf_arr * psi_edges_bw[np.newaxis,:,np.newaxis] * + ang_err_bw[np.newaxis,np.newaxis,:], + axis=(1,2)) + + del(pdf_arr) + + pdf = PDSignalEnergyPDF(f_e, log10_reco_e_edges) + + return pdf + + args_list = [ + ((union_arr, flux_model, gridfitparams), {}) + for gridfitparams in self.gridfitparams_list + ] + + pdf_list = parallelize( + create_energy_pdf, + args_list, + ncpu=self.ncpu, + ppbar=ppbar) + + del(union_arr) + + # Save all the energy PDF objects in the PDFSet PDF registry with + # the hash of the individual parameters as key. + for (gridfitparams, pdf) in zip(self.gridfitparams_list, pdf_list): + self.add_pdf(pdf, gridfitparams) + + def get_prob(self, tdm, gridfitparams, tl=None): + """Calculates the signal probability density of each event for the + given set of signal fit parameters on a grid. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the data events for which the + probability should be calculated for. The following data fields must + exist: + + - 'log_energy' + The log10 of the reconstructed energy. + - 'psi' + The opening angle from the source to the event in radians. + - 'ang_err' + The angular error of the event in radians. + gridfitparams : dict + The dictionary holding the signal parameter values for which the + signal energy probability should be calculated. Note, that the + parameter values must match a set of parameter grid values for which + a PDSignalPDF object has been created at construction time of this + PDSignalPDFSet object. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure time. + + Returns + ------- + prob : 1d ndarray + The array with the signal energy probability for each event. + grads : (N_fitparams,N_events)-shaped ndarray | None + The 2D numpy ndarray holding the gradients of the PDF w.r.t. + each fit parameter for each event. The order of the gradients + is the same as the order of floating parameters specified through + the ``param_set`` property. + It is ``None``, if this PDF does not depend on any parameters. + + Raises + ------ + KeyError + If no energy PDF can be found for the given signal parameter values. + """ + print('Getting signal PDF for gridfitparams={}'.format( + str(gridfitparams))) + pdf = self.get_pdf(gridfitparams) + + (prob, grads) = pdf.get_prob(tdm, tl=tl) + + return (prob, grads) + + class PDSignalPDF(PDF, IsSignalPDF): """This class provides a signal pdf for a given spectrial index value. """ @@ -666,7 +1028,7 @@ def __init__( vmax=self.ang_err_upper_edges[-1]) ) - # Check integrety. + # Check integrity. integral = np.sum( #1/(2*np.pi*np.sin(0.5*(psi_edges[None,1:,None]+ # psi_edges[None,:-1,None]) @@ -816,6 +1178,7 @@ def __init__( reco_e_edges = data['log10_reco_e_binedges'] psi_edges = data['psi_binedges'] ang_err_edges = data['ang_err_binedges'] + print(np.diff(np.degrees(ang_err_edges))) del(data) true_e_bincenters = np.power( @@ -1009,7 +1372,8 @@ def get_prob(self, tdm, gridfitparams, tl=None): KeyError If no energy PDF can be found for the given signal parameter values. """ - print('Getting signal PDF for gridfitparams={}'.format(str(gridfitparams))) + print('Getting signal PDF for gridfitparams={}'.format( + str(gridfitparams))) pdf = self.get_pdf(gridfitparams) (prob, grads) = pdf.get_prob(tdm, tl=tl) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index a24ed069a3..1819f056b4 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -116,7 +116,6 @@ def get_prob(self, tdm, fitparams=None, tl=None): try: # angular difference is pre calculated prob = get_data('spatial_pdf_gauss') - src_ra = get_data('src_array')['ra'] if idxs is None: prob = prob.reshape((len(get_data('src_array')), len(ra))) @@ -139,7 +138,7 @@ def get_prob(self, tdm, fitparams=None, tl=None): np.cos(src_dec) * (np.sin(delta_ra / 2.))**2. else: # Calculate the angular difference only for events that are close - # to the respective source poisition. This is useful for stacking + # to the respective source poisition. This is useful for stacking # analyses. src_idxs, ev_idxs = idxs src_ra = get_data('src_array')['ra'][src_idxs] @@ -176,7 +175,7 @@ def get_prob(self, tdm, fitparams=None, tl=None): norm = src_w.sum() src_w /= norm src_w_grads /= norm - + if idxs is not None: prob = scp.sparse.csr_matrix((prob, (ev_idxs, src_idxs))) else: @@ -187,6 +186,94 @@ def get_prob(self, tdm, fitparams=None, tl=None): return (prob_res, np.atleast_2d(grads)) +class RayleighPSFPointSourceSignalSpatialPDF(SpatialPDF, IsSignalPDF): + """This spatial signal PDF model describes the spatial PDF for a point-like + source following a Rayleigh distribution in the opening angle between the + source and reconstructed muon direction. + Mathematically, it's the convolution of a point in the sky, i.e. the source + location, with the PSF. The result of this convolution has the following + form: + + 1/(2*\pi \sin \Psi) * \Psi/\sigma^2 \exp(-\Psi^2/(2*\sigma^2)), + + where \sigma is the spatial uncertainty of the event and \Psi the distance + on the sphere between the source and the data event. + + This PDF requires the `src_array` data field, that is numpy record ndarray + with the data fields `ra` and `dec` holding the right-ascention and + declination of the point-like sources, respectively. + """ + def __init__(self, ra_range=None, dec_range=None, **kwargs): + """Creates a new spatial signal PDF for point-like sources with a + Rayleigh point-spread-function (PSF). + + Parameters + ---------- + ra_range : 2-element tuple | None + The range in right-ascention this spatial PDF is valid for. + If set to None, the range (0, 2pi) is used. + dec_range : 2-element tuple | None + The range in declination this spatial PDF is valid for. + If set to None, the range (-pi/2, +pi/2) is used. + """ + if(ra_range is None): + ra_range = (0, 2*np.pi) + if(dec_range is None): + dec_range = (-np.pi/2, np.pi/2) + + super().__init__( + ra_range=ra_range, + dec_range=dec_range, + **kwargs + ) + + def get_prob(self, tdm, fitparams=None, tl=None): + """Calculates the spatial signal probability density of each event for + all defined sources. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the trial event data for which + to calculate the PDF values. The following data fields need to be + present: + + 'psi' : float + The opening angle in radian between the source direction and the + reconstructed muon direction. + 'ang_err': float + The reconstruction uncertainty in radian of the data event. + + fitparams : None + Unused interface argument. + tl : TimeLord instance | None + The optional TimeLord instance to use for measuring timing + information. + + Returns + ------- + pd : (n_events,)-shaped 1D numpy ndarray + The probability density values for each event. + grads : (0,)-shaped 1D numpy ndarray + Since this PDF does not depend on fit parameters, an empty array + is returned. + """ + get_data = tdm.get_data + + psi = get_data('psi') + sigma = get_data('ang_err') + + pd = ( + 0.5/(np.pi*np.sin(psi)) * + (psi / sigma**2) * + np.exp(-0.5*(psi/sigma)**2) + ) + + grads = np.array([], dtype=np.double) + + return (pd, grads) + + class SignalTimePDF(TimePDF, IsSignalPDF): """This class provides a time PDF class for a signal source. It consists of From eba9879c7137ef2942c0876e4c2875547fd6d604 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 12 May 2022 14:54:55 +0200 Subject: [PATCH 078/274] Improve the get detection probability method --- skyllh/analyses/i3/trad_ps/signalpdf.py | 15 +++++--- skyllh/analyses/i3/trad_ps/utils.py | 49 +++++++++++++++++++------ 2 files changed, 47 insertions(+), 17 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index f908334dc7..c418fae46d 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -820,16 +820,18 @@ def __init__( det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( sin_true_dec = np.sin(src_dec), true_e_min = true_e_binedges[i], - true_e_max = true_e_binedges[i+1] + true_e_max = true_e_binedges[i+1], + true_e_range_min = true_e_binedges[0], + true_e_range_max = true_e_binedges[-1] ) self._logger.debug('det_prob = {}, sum = {}'.format( det_prob, np.sum(det_prob))) - if not np.isclose(np.sum(det_prob), 1, rtol=0.02): - raise ValueError( + if not np.isclose(np.sum(det_prob), 1): + self._logger.warn( 'The sum of the detection probabilities is not unity! It is ' - '{}.'.format(np.sum(det_prob))) + '{}.'.format(np.sum(det_prob))) log10_reco_e_bw = np.diff(log10_reco_e_edges) psi_edges_bw = np.diff(psi_edges) @@ -866,8 +868,9 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): ) ) if not np.isclose(np.sum(flux_prob), 1): - raise ValueError( - 'The sum of the flux probabilities is not unity!') + self._logger.warn( + 'The sum of the flux probabilities is not unity! It is ' + '{}.'.format(np.sum(flux_prob))) self._logger.debug( 'flux_prob = {}'.format(flux_prob) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index b8536d5359..23299191d5 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -6,7 +6,8 @@ from scipy import integrate from skyllh.core.binning import ( - get_bincenters_from_binedges + get_bincenters_from_binedges, + get_bin_indices_from_lower_and_upper_binedges ) from skyllh.core.storage import create_FileLoader @@ -586,8 +587,10 @@ def get_detection_pd_in_log10E_for_sin_true_dec( return det_pd def get_detection_prob_for_sin_true_dec( - self, sin_true_dec, true_e_min, true_e_max): - """Calculates the detection probability for a given sin declination. + self, sin_true_dec, true_e_min, true_e_max, + true_e_range_min, true_e_range_max): + """Calculates the detection probability for a given energy range for a + given sin declination. Parameters ---------- @@ -597,29 +600,53 @@ def get_detection_prob_for_sin_true_dec( The minimum energy in GeV. true_e_max : float The maximum energy in GeV. + true_e_range_min : float + The minimum energy in GeV of the entire energy range. + true_e_range_max : float + The maximum energy in GeV of the entire energy range. Returns ------- det_prob : float The true energy detection probability. """ - aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - true_e_binedges = np.power(10, self.log_true_e_binedges) + # Get the bin indices for the lower and upper energy range values. + (lidx, uidx) = get_bin_indices_from_lower_and_upper_binedges( + true_e_binedges[:-1], + true_e_binedges[1:], + np.array([true_e_range_min, true_e_range_max])) + # The function determined the bin indices based on the + # lower bin edges. So the bin index of the upper energy range value + # is 1 to large. + uidx -= 1 + + aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + aeff = aeff[lidx:uidx+1] + true_e_binedges = true_e_binedges[lidx:uidx+2] + dE = np.diff(true_e_binedges) - det_pdf = aeff / np.sum(aeff) / dE + det_pdf = aeff / dE - x = np.power(10, self.log_true_e_bincenters) - y = det_pdf - tck = interpolate.splrep(x, y, k=1, s=0) + true_e_bincenters = 0.5*(true_e_binedges[:-1] + true_e_binedges[1:]) + tck = interpolate.splrep( + true_e_bincenters, det_pdf, + xb=true_e_range_min, xe=true_e_range_max, k=1, s=0) def _eval_func(x): return interpolate.splev(x, tck, der=0) - r = integrate.quad(_eval_func, true_e_min, true_e_max) - det_prob = r[0] + norm = integrate.quad( + _eval_func, true_e_range_min, true_e_range_max, + limit=200, full_output=1)[0] + + integral = integrate.quad( + _eval_func, true_e_min, true_e_max, + limit=200, full_output=1)[0] + + det_prob = integral / norm return det_prob From b77cc8494d9a33dfdf20e5a47481f05c989a03f0 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 12 May 2022 14:59:05 +0200 Subject: [PATCH 079/274] Improve logging --- skyllh/analyses/i3/trad_ps/signalpdf.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index c418fae46d..fe5ebd6fea 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -873,18 +873,17 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): '{}.'.format(np.sum(flux_prob))) self._logger.debug( - 'flux_prob = {}'.format(flux_prob) + 'flux_prob = {}, sum = {}'.format( + flux_prob, np.sum(flux_prob)) ) p = flux_prob * det_prob - self._logger.debug( - 'p = {}, sum(p)={}'.format(p, np.sum(p)) - ) true_e_prob = p / np.sum(p) self._logger.debug( - f'true_e_prob = {true_e_prob}') + 'true_e_prob = {}'.format( + true_e_prob)) transfer = np.copy(union_arr) for true_e_idx in range(nbins_true_e): @@ -975,8 +974,8 @@ def get_prob(self, tdm, gridfitparams, tl=None): KeyError If no energy PDF can be found for the given signal parameter values. """ - print('Getting signal PDF for gridfitparams={}'.format( - str(gridfitparams))) + #print('Getting signal PDF for gridfitparams={}'.format( + # str(gridfitparams))) pdf = self.get_pdf(gridfitparams) (prob, grads) = pdf.get_prob(tdm, tl=tl) From 4f2babdf6b28afd443f9b989cee50f7d5efbbd24 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 12 May 2022 18:08:50 +0200 Subject: [PATCH 080/274] Smooth the energy PDF --- skyllh/analyses/i3/trad_ps/signalpdf.py | 62 +++++++++++++++++++------ 1 file changed, 49 insertions(+), 13 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index fe5ebd6fea..81ddf5fe94 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -652,6 +652,27 @@ def __init__( def assert_is_valid_for_trial_data(self, tdm): pass + def get_pd_by_log_e(self, log_e, tl=None): + """Calculates the probability density for the given log10(E/GeV) + values. + + + """ + # Select events that actually have a signal enegry PDF. + # All other events will get zero signal probability. + m = ( + (log_e >= self.log_e_lower_edges[0]) & + (log_e < self.log_e_upper_edges[-1]) + ) + + log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( + self.log_e_lower_edges, self.log_e_upper_edges, log_e[m]) + + pd = np.zeros((len(log_e),), dtype=np.double) + pd[m] = self.f_e[log_e_idxs] + + return pd + def get_prob(self, tdm, params=None, tl=None): """Calculates the probability density for the events given by the TrialDataManager. @@ -689,18 +710,7 @@ def get_prob(self, tdm, params=None, tl=None): """ log_e = tdm.get_data('log_energy') - # Select events that actually have a signal enegry PDF. - # All other events will get zero signal probability. - m = ( - (log_e >= self.log_e_lower_edges[0]) & - (log_e < self.log_e_upper_edges[-1]) - ) - - log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.log_e_lower_edges, self.log_e_upper_edges, log_e[m]) - - pd = np.zeros((len(log_e),), dtype=np.double) - pd[m] = self.f_e[log_e_idxs] + pd = self.get_pd_by_log_e(log_e, tl=tl) return (pd, None) @@ -717,6 +727,7 @@ def __init__( flux_model, fitparam_grid_set, union_sm_arr_pathfilename=None, + smoothing=1, ncpu=None, ppbar=None, **kwargs): @@ -736,6 +747,8 @@ def __init__( The pathfilename of the unionized smearing matrix array file from which the unionized smearing matrix array should get loaded from. If None, the unionized smearing matrix array will be created. + smoothing : int + The number of bins to combine to create a smoother energy pdf. """ self._logger = get_logger(module_classname(self)) @@ -910,7 +923,30 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): del(pdf_arr) - pdf = PDSignalEnergyPDF(f_e, log10_reco_e_edges) + # Combine always step bins to smooth out the pdf. + step = smoothing + n = len(log10_reco_e_edges)-1 + n_new = int(np.ceil((len(log10_reco_e_edges)-1)/step,)) + f_e_new = np.zeros((n_new,), dtype=np.double) + log10_reco_e_edges_new = np.zeros( + (n_new+1), dtype=np.double) + start = 0 + k = 0 + while start <= n-1: + end = np.min([start+step, n]) + + v = np.sum(f_e[start:end]) #/ (end - start) + f_e_new[k] = v + log10_reco_e_edges_new[k] = log10_reco_e_edges[start] + + start += step + k += 1 + log10_reco_e_edges_new[-1] = log10_reco_e_edges[-1] + + + f_e_new = f_e_new / np.sum(f_e_new) / np.diff(log10_reco_e_edges_new) + + pdf = PDSignalEnergyPDF(f_e_new, log10_reco_e_edges_new) return pdf From 84611f1f26d1380e93c64e9c2ad0dad17c53822d Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 13 May 2022 12:38:22 +0200 Subject: [PATCH 081/274] Spline the energy PDF --- skyllh/analyses/i3/trad_ps/signalpdf.py | 73 +++++++++++++++++++++---- 1 file changed, 61 insertions(+), 12 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 81ddf5fe94..81128bbb08 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -6,6 +6,8 @@ import pickle from copy import deepcopy +from scipy import interpolate +from scipy import integrate from scipy.interpolate import UnivariateSpline from itertools import product @@ -649,26 +651,73 @@ def __init__( 'The integral over log10_E of the energy term must be unity! ' 'But it is {}!'.format(integral)) + # Create a spline of the PDF. + self._create_spline(order=1, s=0) + + def _create_spline(self, order=1, s=0): + """Creates the spline representation of the energy PDF. + """ + log10_e_bincenters = 0.5*( + self.log_e_lower_edges + self.log_e_upper_edges) + self.spl_rep = interpolate.splrep( + log10_e_bincenters, self.f_e, + xb=self.log_e_lower_edges[0], + xe=self.log_e_upper_edges[-1], + k=order, + s=s + ) + self.spl_norm = integrate.quad( + self._eval_spline, + self.log_e_lower_edges[0], self.log_e_upper_edges[-1], + limit=200, full_output=1)[0] + + def _eval_spline(self, x): + return interpolate.splev(x, self.spl_rep, der=0) + def assert_is_valid_for_trial_data(self, tdm): pass - def get_pd_by_log_e(self, log_e, tl=None): + def get_splined_pd_by_log10_e(self, log10_e, tl=None): """Calculates the probability density for the given log10(E/GeV) - values. + values using the spline representation of the PDF. """ # Select events that actually have a signal enegry PDF. # All other events will get zero signal probability. m = ( - (log_e >= self.log_e_lower_edges[0]) & - (log_e < self.log_e_upper_edges[-1]) + (log10_e >= self.log_e_lower_edges[0]) & + (log10_e < self.log_e_upper_edges[-1]) + ) + + pd = np.zeros((len(log10_e),), dtype=np.double) + + pd[m] = self._eval_spline(log10_e[m]) / self.spl_norm + + return pd + + def get_pd_by_log10_e(self, log10_e, tl=None): + """Calculates the probability density for the given log10(E/GeV) + values. + + Parameters + ---------- + log10_e : (n_events,)-shaped 1D numpy ndarray + The numpy ndarray holding the log10(E/GeV) values. + tl : TimeLord | None + The optional TimeLord instance to measure code timing information. + """ + # Select events that actually have a signal enegry PDF. + # All other events will get zero signal probability. + m = ( + (log10_e >= self.log_e_lower_edges[0]) & + (log10_e < self.log_e_upper_edges[-1]) ) log_e_idxs = get_bin_indices_from_lower_and_upper_binedges( - self.log_e_lower_edges, self.log_e_upper_edges, log_e[m]) + self.log_e_lower_edges, self.log_e_upper_edges, log10_e[m]) - pd = np.zeros((len(log_e),), dtype=np.double) + pd = np.zeros((len(log10_e),), dtype=np.double) pd[m] = self.f_e[log_e_idxs] return pd @@ -708,9 +757,9 @@ def get_prob(self, tdm, params=None, tl=None): the ``param_set`` property. It is ``None``, if this PDF does not depend on any parameters. """ - log_e = tdm.get_data('log_energy') + log10_e = tdm.get_data('log_energy') - pd = self.get_pd_by_log_e(log_e, tl=tl) + pd = self.get_splined_pd_by_log10_e(log10_e, tl=tl) return (pd, None) @@ -727,7 +776,7 @@ def __init__( flux_model, fitparam_grid_set, union_sm_arr_pathfilename=None, - smoothing=1, + smoothing=8, ncpu=None, ppbar=None, **kwargs): @@ -749,6 +798,7 @@ def __init__( If None, the unionized smearing matrix array will be created. smoothing : int The number of bins to combine to create a smoother energy pdf. + Eight seems to produce good results. """ self._logger = get_logger(module_classname(self)) @@ -935,7 +985,7 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): while start <= n-1: end = np.min([start+step, n]) - v = np.sum(f_e[start:end]) #/ (end - start) + v = np.sum(f_e[start:end]) / (end - start) f_e_new[k] = v log10_reco_e_edges_new[k] = log10_reco_e_edges[start] @@ -943,7 +993,7 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): k += 1 log10_reco_e_edges_new[-1] = log10_reco_e_edges[-1] - + # Re-normalize the PDF. f_e_new = f_e_new / np.sum(f_e_new) / np.diff(log10_reco_e_edges_new) pdf = PDSignalEnergyPDF(f_e_new, log10_reco_e_edges_new) @@ -1216,7 +1266,6 @@ def __init__( reco_e_edges = data['log10_reco_e_binedges'] psi_edges = data['psi_binedges'] ang_err_edges = data['ang_err_binedges'] - print(np.diff(np.degrees(ang_err_edges))) del(data) true_e_bincenters = np.power( From 44f9c58abbdb10a75c6786483d6ec8b21c5fd5b5 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 13 May 2022 16:13:48 +0200 Subject: [PATCH 082/274] Merge reco energy bins of the unionized smearing matrix --- skyllh/analyses/i3/trad_ps/signalpdf.py | 13 +++- skyllh/analyses/i3/trad_ps/utils.py | 82 ++++++++++++++++++++++--- 2 files changed, 84 insertions(+), 11 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 81128bbb08..be4520fcfc 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -45,7 +45,8 @@ load_smearing_histogram, psi_to_dec_and_ra, PublicDataAeff, - PublicDataSmearingMatrix + PublicDataSmearingMatrix, + merge_reco_energy_bins ) @@ -759,7 +760,7 @@ def get_prob(self, tdm, params=None, tl=None): """ log10_e = tdm.get_data('log_energy') - pd = self.get_splined_pd_by_log10_e(log10_e, tl=tl) + pd = self.get_pd_by_log10_e(log10_e, tl=tl) return (pd, None) @@ -776,7 +777,7 @@ def __init__( flux_model, fitparam_grid_set, union_sm_arr_pathfilename=None, - smoothing=8, + smoothing=1, ncpu=None, ppbar=None, **kwargs): @@ -861,6 +862,12 @@ def __init__( ang_err_edges = data['ang_err_binedges'] del(data) + # Merge small energy bins. + bw_th = 0.1 + max_bw = 0.2 + (union_arr, log10_reco_e_edges) = merge_reco_energy_bins( + union_arr, log10_reco_e_edges, bw_th, max_bw) + true_e_binedges = np.power(10, log10_true_e_binedges) nbins_true_e = len(true_e_binedges) - 1 diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 23299191d5..7a3d9d451f 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -419,20 +419,12 @@ def create_unionized_smearing_matrix_array(sm, src_dec): ) idx = ( true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx) - #psi_bw = 2 * np.pi * ( - # np.cos(sm.psi_lower_edges[idx]) - - # np.cos(sm.psi_upper_edges[idx]) - #) psi_bw = ( sm.psi_upper_edges[idx] - sm.psi_lower_edges[idx] ) idx = ( true_e_idx, true_dec_idx, sm_e_idx, sm_p_idx, sm_a_idx) - #ang_err_bw = 2 * np.pi * ( - # np.cos(sm.ang_err_lower_edges[idx]) - - # np.cos(sm.ang_err_upper_edges[idx]) - #) ang_err_bw = ( sm.ang_err_upper_edges[idx] - sm.ang_err_lower_edges[idx] @@ -463,6 +455,80 @@ def create_unionized_smearing_matrix_array(sm, src_dec): return result +def merge_bins(arr, edges, i_start, i_end): + n_to_merge = i_end - i_start + 1 + bw = np.diff(edges[i_start:i_end+2]) + + #print('i_start={}, i_end={}, n_to_merge={}, sum_bw={}'.format( + # i_start, i_end, n_to_merge, np.sum(bw))) + + new_n_e = arr.shape[1] - (i_end-i_start) + new_edges = np.empty((new_n_e+1,), dtype=np.double) + new_edges[0:i_start+1] = edges[0:i_start+1] + new_edges[i_start+1:] = edges[i_end+1:] + new_val = np.sum(arr[:,i_start:i_end+1,:,:], axis=1) / n_to_merge + new_arr = np.empty( + (arr.shape[0],new_n_e,arr.shape[2],arr.shape[3]), + dtype=np.double) + new_arr[:,i_start,:,:] = new_val + new_arr[:,0:i_start,:,:] = arr[:,0:i_start,:,:] + new_arr[:,i_start+1:,:,:] = arr[:,i_end+1:,:] + + return (new_arr, new_edges) + + +def merge_reco_energy_bins(arr, log10_reco_e_binedges, bw_th, max_bw=0.2): + """ + """ + bw = np.diff(log10_reco_e_binedges) + n = len(bw) + i = 0 + block_i_start = None + block_i_end = None + while i < n: + merge = False + if bw[i] <= bw_th: + # We need to combine this bin with the current block. + if block_i_start is None: + # Start a new block. + block_i_start = i + block_i_end = i + else: + # Extend the current block if it's not getting too large. + new_bw = ( + log10_reco_e_binedges[i+1] - + log10_reco_e_binedges[block_i_start] + ) + if new_bw <= max_bw: + block_i_end = i + else: + merge = True + elif(block_i_start is not None): + # We reached a big bin, so we combine the current block. + if block_i_end == block_i_start: + block_i_end = i + merge = True + + if merge: + (arr, log10_reco_e_binedges) = merge_bins( + arr, log10_reco_e_binedges, block_i_start, block_i_end) + bw = np.diff(log10_reco_e_binedges) + n = len(bw) + i = 0 + block_i_start = None + block_i_end = None + continue + + i += 1 + + # Merge the last block if there is any. + if block_i_start is not None: + (arr, log10_reco_e_binedges) = merge_bins( + arr, log10_reco_e_binedges, block_i_start, block_i_end) + + return (arr, log10_reco_e_binedges) + + class PublicDataAeff(object): """This class is a helper class for dealing with the effective area provided by the public data. From 9ce19b7ca0eadea6cb0524be4d576604e86f25dd Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 25 May 2022 15:02:57 +0200 Subject: [PATCH 083/274] new energy pdf without unionized matrix. --- skyllh/analyses/i3/trad_ps/analysis.py | 61 +-- skyllh/analyses/i3/trad_ps/signalpdf.py | 475 ++++++++++++++++++++++-- 2 files changed, 469 insertions(+), 67 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index ee3e72c5d6..318b228199 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -84,7 +84,7 @@ PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod ) from skyllh.analyses.i3.trad_ps.signalpdf import ( - PDSignalEnergyPDFSet + PDSignalEnergyPDFSet_new ) from skyllh.analyses.i3.trad_ps.pdfratio import ( PDPDFRatio @@ -125,11 +125,13 @@ def psi_func(tdm, src_hypo_group_manager, fitparams): # For now we support only a single source, hence return psi[0]. return psi[0, :] + def TXS_location(): - src_ra = np.radians(77.358) + src_ra = np.radians(77.358) src_dec = np.radians(5.693) return (src_ra, src_dec) + def create_analysis( rss, datasets, @@ -210,7 +212,8 @@ def create_analysis( fitparam_ns = FitParameter('ns', 0, 1e3, ns_seed) # Define the gamma fit parameter. - fitparam_gamma = FitParameter('gamma', valmin=1, valmax=5, initial=gamma_seed) + fitparam_gamma = FitParameter( + 'gamma', valmin=1, valmax=5, initial=gamma_seed) # Define the detector signal efficiency implementation method for the # IceCube detector and this source and flux_model. @@ -276,7 +279,7 @@ def create_analysis( # Create a trial data manager and add the required data fields. tdm = TrialDataManager() tdm.add_source_data_field('src_array', - pointlikesource_to_data_field_array) + pointlikesource_to_data_field_array) tdm.add_data_field('psi', psi_func) sin_dec_binning = ds.get_binning_definition('sin_dec') @@ -291,13 +294,11 @@ def create_analysis( spatial_sigpdf, spatial_bkgpdf) # Create the energy PDF ratio instance for this dataset. - energy_sigpdfset = PDSignalEnergyPDFSet( + energy_sigpdfset = PDSignalEnergyPDFSet_new( ds=ds, src_dec=source.dec, flux_model=flux_model, fitparam_grid_set=gamma_grid, - union_sm_arr_pathfilename=os.path.join( - cache_dir, 'union_sm_{}.pkl'.format(ds.name)), ppbar=ppbar ) smoothing_filter = BlockSmoothingFilter(nbins=1) @@ -309,7 +310,7 @@ def create_analysis( bkg_pdf=energy_bkgpdf ) - pdfratios = [ spatial_pdfratio, energy_pdfratio ] + pdfratios = [spatial_pdfratio, energy_pdfratio] analysis.add_dataset( ds, data, pdfratios, tdm, event_selection_method) @@ -319,37 +320,38 @@ def create_analysis( analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) - #analysis.construct_signal_generator() + # analysis.construct_signal_generator() return analysis + if(__name__ == '__main__'): p = argparse.ArgumentParser( - description = 'Calculates TS for a given source location using the ' - '10-year public point source sample.', - formatter_class = argparse.RawTextHelpFormatter + description='Calculates TS for a given source location using the ' + '10-year public point source sample.', + formatter_class=argparse.RawTextHelpFormatter ) p.add_argument('--dec', default=23.8, type=float, - help='The source declination in degrees.') + help='The source declination in degrees.') p.add_argument('--ra', default=216.76, type=float, - help='The source right-ascention in degrees.') + help='The source right-ascention in degrees.') p.add_argument('--gamma-seed', default=3, type=float, - help='The seed value of the gamma fit parameter.') + help='The seed value of the gamma fit parameter.') p.add_argument('--data_base_path', default=None, type=str, - help='The base path to the data samples (default=None)' - ) + help='The base path to the data samples (default=None)' + ) p.add_argument('--pdf-seed', default=1, type=int, - help='The random number generator seed for generating the signal PDF.') + help='The random number generator seed for generating the signal PDF.') p.add_argument('--seed', default=1, type=int, - help='The random number generator seed for the likelihood minimization.') + help='The random number generator seed for the likelihood minimization.') p.add_argument('--ncpu', default=1, type=int, - help='The number of CPUs to utilize where parallelization is possible.' - ) + help='The number of CPUs to utilize where parallelization is possible.' + ) p.add_argument('--n-mc-events', default=int(1e7), type=int, - help='The number of MC events to sample for the energy signal PDF.' - ) + help='The number of MC events to sample for the energy signal PDF.' + ) p.add_argument('--cache-dir', default='.', type=str, - help='The cache directory to look for cached data, e.g. signal PDFs.') + help='The cache directory to look for cached data, e.g. signal PDFs.') args = p.parse_args() # Setup `skyllh` package logging. @@ -359,8 +361,8 @@ def create_analysis( '%(message)s' setup_console_handler('skyllh', logging.INFO, log_format) setup_file_handler('skyllh', 'debug.log', - log_level=logging.DEBUG, - log_format=log_format) + log_level=logging.DEBUG, + log_format=log_format) CFG['multiproc']['ncpu'] = args.ncpu @@ -379,7 +381,6 @@ def create_analysis( args.data_base_path) datasets.append(dsc.get_dataset(season)) - # Define a random state service. rss_pdf = RandomStateService(args.pdf_seed) rss = RandomStateService(args.seed) @@ -402,9 +403,9 @@ def create_analysis( with tl.task_timer('Unblinding data.'): (TS, fitparam_dict, status) = ana.unblind(rss) - print('TS = %g'%(TS)) - print('ns_fit = %g'%(fitparam_dict['ns'])) - print('gamma_fit = %g'%(fitparam_dict['gamma'])) + print('TS = %g' % (TS)) + print('ns_fit = %g' % (fitparam_dict['ns'])) + print('gamma_fit = %g' % (fitparam_dict['gamma'])) """ # Generate some signal events. diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index be4520fcfc..dc50c1be43 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -61,7 +61,6 @@ def __init__(self, ds, **kwargs): pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - def _generate_events( self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): """Generates `n_events` signal events for the given source location @@ -188,6 +187,7 @@ class PublicDataSignalI3EnergyPDF(EnergyPDF, IsSignalPDF, UsesBinning): """Class that implements the enegry signal PDF for a given flux model given the public data. """ + def __init__(self, ds, flux_model, data_dict=None): """Constructs a new enegry PDF instance using the public IceCube data. @@ -227,7 +227,7 @@ def __init__(self, ds, flux_model, data_dict=None): true_dec_bin_edges, self.reco_e_lower_edges, self.reco_e_upper_edges - ) = load_smearing_histogram( + ) = load_smearing_histogram( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) else: @@ -244,7 +244,7 @@ def __init__(self, ds, flux_model, data_dict=None): self.add_binning(BinningDefinition('true_dec', true_dec_bin_edges)) # Marginalize over the PSF and angular error axes. - self.histogram = np.sum(self.histogram, axis=(3,4)) + self.histogram = np.sum(self.histogram, axis=(3, 4)) # Create a (prob vs E_reco) spline for each source declination bin. n_true_dec = len(true_dec_bin_edges) - 1 @@ -373,7 +373,7 @@ def get_total_weighted_energy_pdf( (e_pdf, e_pdf_bin_centers) =\ self.get_weighted_energy_pdf_hist_for_true_energy_dec_bin( true_e_idx, true_dec_idx, self.flux_model - ) + ) if(e_pdf is None): continue splines.append( @@ -465,6 +465,7 @@ class PublicDataSignalI3EnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): It creates a set of PublicDataI3EnergyPDF objects for a discrete set of energy signal parameters. """ + def __init__( self, rss, @@ -481,12 +482,12 @@ def __init__( fitparam_grid_set = ParameterGridSet([fitparam_grid_set]) if(not isinstance(fitparam_grid_set, ParameterGridSet)): raise TypeError('The fitparam_grid_set argument must be an ' - 'instance of ParameterGrid or ParameterGridSet!') + 'instance of ParameterGrid or ParameterGridSet!') if((smoothing_filter is not None) and (not isinstance(smoothing_filter, SmoothingFilter))): raise TypeError('The smoothing_filter argument must be None or ' - 'an instance of SmoothingFilter!') + 'an instance of SmoothingFilter!') # We need to extend the fit parameter grids on the lower and upper end # by one bin to allow for the calculation of the interpolation. But we @@ -564,7 +565,7 @@ def create_I3EnergyPDF( args_list = [ ((logE_binning, sinDec_binning, smoothing_filter, aeff, siggen, flux_model, n_events, gridfitparams), {}) - for gridfitparams in self.gridfitparams_list + for gridfitparams in self.gridfitparams_list ] epdf_list = parallelize( @@ -626,6 +627,7 @@ def get_prob(self, tdm, gridfitparams): class PDSignalEnergyPDF(PDF, IsSignalPDF): """This class provides a signal energy PDF for a spectrial index value. """ + def __init__( self, f_e, log_e_edges, **kwargs): """Creates a new signal energy PDF instance for a particular spectral @@ -770,6 +772,7 @@ class PDSignalEnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): It creates a set of PDSignalEnergyPDF instances, one for each spectral index value on a grid. """ + def __init__( self, ds, @@ -832,7 +835,7 @@ def __init__( # Load the unionized smearing matrix array or create it if no one was # specified. if ((union_sm_arr_pathfilename is not None) and - os.path.exists(union_sm_arr_pathfilename)): + os.path.exists(union_sm_arr_pathfilename)): self._logger.info( 'Loading unionized smearing matrix from file "{}".'.format( union_sm_arr_pathfilename)) @@ -888,11 +891,11 @@ def __init__( det_prob = np.empty((len(dE_nu),), dtype=np.double) for i in range(len(dE_nu)): det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( - sin_true_dec = np.sin(src_dec), - true_e_min = true_e_binedges[i], - true_e_max = true_e_binedges[i+1], - true_e_range_min = true_e_binedges[0], - true_e_range_max = true_e_binedges[-1] + sin_true_dec=np.sin(src_dec), + true_e_min=true_e_binedges[i], + true_e_max=true_e_binedges[i+1], + true_e_range_min=true_e_binedges[0], + true_e_range_max=true_e_binedges[-1] ) self._logger.debug('det_prob = {}, sum = {}'.format( @@ -908,9 +911,9 @@ def __init__( ang_err_bw = np.diff(ang_err_edges) bin_volumes = ( - log10_reco_e_bw[:,np.newaxis,np.newaxis] * - psi_edges_bw[np.newaxis,:,np.newaxis] * - ang_err_bw[np.newaxis,np.newaxis,:]) + log10_reco_e_bw[:, np.newaxis, np.newaxis] * + psi_edges_bw[np.newaxis, :, np.newaxis] * + ang_err_bw[np.newaxis, np.newaxis, :]) # Create the energy pdf for different gamma values. def create_energy_pdf(union_arr, flux_model, gridfitparams): @@ -974,9 +977,9 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): # Create the enegry PDF f_e = P(log10_E_reco|dec) = # \int dPsi dang_err P(E_reco,Psi,ang_err). f_e = np.sum( - pdf_arr * psi_edges_bw[np.newaxis,:,np.newaxis] * - ang_err_bw[np.newaxis,np.newaxis,:], - axis=(1,2)) + pdf_arr * psi_edges_bw[np.newaxis, :, np.newaxis] * + ang_err_bw[np.newaxis, np.newaxis, :], + axis=(1, 2)) del(pdf_arr) @@ -1001,7 +1004,8 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): log10_reco_e_edges_new[-1] = log10_reco_e_edges[-1] # Re-normalize the PDF. - f_e_new = f_e_new / np.sum(f_e_new) / np.diff(log10_reco_e_edges_new) + f_e_new = f_e_new / np.sum(f_e_new) / \ + np.diff(log10_reco_e_edges_new) pdf = PDSignalEnergyPDF(f_e_new, log10_reco_e_edges_new) @@ -1009,7 +1013,7 @@ def create_energy_pdf(union_arr, flux_model, gridfitparams): args_list = [ ((union_arr, flux_model, gridfitparams), {}) - for gridfitparams in self.gridfitparams_list + for gridfitparams in self.gridfitparams_list ] pdf_list = parallelize( @@ -1067,7 +1071,402 @@ def get_prob(self, tdm, gridfitparams, tl=None): KeyError If no energy PDF can be found for the given signal parameter values. """ - #print('Getting signal PDF for gridfitparams={}'.format( + # print('Getting signal PDF for gridfitparams={}'.format( + # str(gridfitparams))) + pdf = self.get_pdf(gridfitparams) + + (prob, grads) = pdf.get_prob(tdm, tl=tl) + + return (prob, grads) + + +def eval_spline(x, spl): + values = spl(x) + values = np.nan_to_num(values, nan=0) + return values + + +def create_spline(log10_e_bincenters, f_e): + """Creates the spline representation of the energy PDF. + """ + + spline = interpolate.PchipInterpolator( + log10_e_bincenters, f_e, extrapolate=False + ) + + spl_norm = integrate.quad( + eval_spline, + log10_e_bincenters[0], log10_e_bincenters[-1], + args=(spline,), + limit=200, full_output=1)[0] + + return spline, spl_norm + + +class PDSignalEnergyPDF_new(PDF, IsSignalPDF): + """This class provides a signal energy PDF for a spectrial index value. + """ + + def __init__( + self, f_e, norm, log_e_edges, **kwargs): + """Creates a new signal energy PDF instance for a particular spectral + index value. + """ + super().__init__(**kwargs) + + self.f_e = f_e + self.norm = norm + + self.log_e_lower_edges = log_e_edges[:-1] + self.log_e_upper_edges = log_e_edges[1:] + + # Add the PDF axes. + self.add_axis(PDFAxis( + name='log_energy', + vmin=self.log_e_lower_edges[0], + vmax=self.log_e_upper_edges[-1]) + ) + + # Check integrity. + integral = integrate.quad( + eval_spline, + self.log_e_lower_edges[0], + self.log_e_upper_edges[-1], + args=(self.f_e,), + limit=200, full_output=1)[0] / self.norm + if not np.isclose(integral, 1): + raise ValueError( + 'The integral over log10_E of the energy term must be unity! ' + 'But it is {}!'.format(integral)) + + def assert_is_valid_for_trial_data(self, tdm): + pass + + def get_pd_by_log10_e(self, log10_e, tl=None): + """Calculates the probability density for the given log10(E/GeV) + values using the spline representation of the PDF. + + + """ + # Select events that actually have a signal enegry PDF. + # All other events will get zero signal probability. + m = ( + (log10_e >= self.log_e_lower_edges[0]) & + (log10_e < self.log_e_upper_edges[-1]) + ) + + pd = np.zeros((len(log10_e),), dtype=np.double) + + pd[m] = eval_spline(log10_e[m], self.f_e) / self.norm + + return pd + + def get_prob(self, tdm, params=None, tl=None): + """Calculates the probability density for the events given by the + TrialDataManager. + + Parameters + ---------- + tdm : TrialDataManager instance + The TrialDataManager instance holding the data events for which the + probability should be looked up. The following data fields are + required: + - 'log_energy' + The log10 of the reconstructed energy. + - 'psi' + The opening angle from the source to the event in radians. + - 'ang_err' + The angular error of the event in radians. + params : dict | None + The dictionary containing the parameter names and values for which + the probability should get calculated. + By definition this PDF does not depend on parameters. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + prob : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. + grads : (N_fitparams,N_events)-shaped ndarray | None + The 2D numpy ndarray holding the gradients of the PDF w.r.t. + each fit parameter for each event. The order of the gradients + is the same as the order of floating parameters specified through + the ``param_set`` property. + It is ``None``, if this PDF does not depend on any parameters. + """ + log10_e = tdm.get_data('log_energy') + + pd = self.get_pd_by_log10_e(log10_e, tl=tl) + + return (pd, None) + + +class PDSignalEnergyPDFSet_new(PDFSet, IsSignalPDF, IsParallelizable): + """This class provides a signal energy PDF set for the public data. + It creates a set of PDSignalEnergyPDF instances, one for each spectral + index value on a grid. + """ + + def __init__( + self, + ds, + src_dec, + flux_model, + fitparam_grid_set, + ncpu=None, + ppbar=None, + **kwargs): + """Creates a new PDSignalEnergyPDFSet instance for the public data. + + Parameters + ---------- + ds : I3Dataset instance + The I3Dataset instance that defines the public data dataset. + src_dec : float + The declination of the source in radians. + flux_model : FluxModel instance + The FluxModel instance that defines the source's flux model. + fitparam_grid_set : ParameterGrid | ParameterGridSet instance + The parameter grid set defining the grids of the fit parameters. + """ + self._logger = get_logger(module_classname(self)) + + # Check for the correct types of the arguments. + if not isinstance(ds, I3Dataset): + raise TypeError( + 'The ds argument must be an instance of I3Dataset!') + + if not isinstance(flux_model, FluxModel): + raise TypeError( + 'The flux_model argument must be an instance of FluxModel!') + + if (not isinstance(fitparam_grid_set, ParameterGrid)) and\ + (not isinstance(fitparam_grid_set, ParameterGridSet)): + raise TypeError( + 'The fitparam_grid_set argument must be an instance of type ' + 'ParameterGrid or ParameterGridSet!') + + # Extend the fitparam_grid_set to allow for parameter interpolation + # values at the grid edges. + fitparam_grid_set = fitparam_grid_set.copy() + fitparam_grid_set.add_extra_lower_and_upper_bin() + + super().__init__( + pdf_type=PDF, + fitparams_grid_set=fitparam_grid_set, + ncpu=ncpu + ) + + # Load the smearing matrix. + sm = PublicDataSmearingMatrix( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) + + # Select the slice of the smearing matrix corresponding to the + # source declination band + true_dec_idx = sm.get_true_dec_idx(src_dec) + sm_histo = sm.histogram[:, true_dec_idx] + + true_e_binedges = np.power(10, sm.true_e_bin_edges) + nbins_true_e = len(true_e_binedges) - 1 + E_nu_min = true_e_binedges[:-1] + E_nu_max = true_e_binedges[1:] + + # Define the values at which to evaluate the splines + xvals = np.linspace( + min(sm.reco_e_lower_edges.flatten()), + max(sm.reco_e_upper_edges.flatten()), + 1000) + + # Calculate the neutrino enegry bin widths in GeV. + dE_nu = np.diff(true_e_binedges) + self._logger.debug( + 'dE_nu = {}'.format(dE_nu) + ) + + # Load the effective area. + aeff = PublicDataAeff( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('eff_area_datafile'))) + + # Calculate the detector's neutrino energy detection probability to + # detect a neutrino of energy E_nu given a neutrino declination: + # p(E_nu|dec) + det_prob = np.empty((len(dE_nu),), dtype=np.double) + for i in range(len(dE_nu)): + det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( + sin_true_dec=np.sin(src_dec), + true_e_min=true_e_binedges[i], + true_e_max=true_e_binedges[i+1], + true_e_range_min=true_e_binedges[0], + true_e_range_max=true_e_binedges[-1] + ) + + self._logger.debug('det_prob = {}, sum = {}'.format( + det_prob, np.sum(det_prob))) + + if not np.isclose(np.sum(det_prob), 1): + self._logger.warn( + 'The sum of the detection probabilities is not unity! It is ' + '{}.'.format(np.sum(det_prob))) + + log10_reco_e_bw = sm.reco_e_upper_edges-sm.reco_e_lower_edges + psi_edges_bw = sm.psi_upper_edges-sm.psi_lower_edges + ang_err_bw = sm.ang_err_upper_edges-sm.ang_err_lower_edges + + # Create the energy pdf for different gamma values. + def create_energy_pdf(sm_histo, flux_model, gridfitparams): + """Creates an energy pdf for a specific gamma value. + """ + # Create a copy of the FluxModel with the given flux parameters. + # The copy is needed to not interfer with other CPU processes. + my_flux_model = flux_model.copy(newprop=gridfitparams) + + self._logger.debug( + 'Generate signal energy PDF for parameters {} in {} E_nu ' + 'bins.'.format( + gridfitparams, nbins_true_e) + ) + + # Calculate the flux probability p(E_nu|gamma). + flux_prob = ( + my_flux_model.get_integral(E_nu_min, E_nu_max) / + my_flux_model.get_integral( + true_e_binedges[0], + true_e_binedges[-1] + ) + ) + if not np.isclose(np.sum(flux_prob), 1): + self._logger.warn( + 'The sum of the flux probabilities is not unity! It is ' + '{}.'.format(np.sum(flux_prob))) + + self._logger.debug( + 'flux_prob = {}, sum = {}'.format( + flux_prob, np.sum(flux_prob)) + ) + + p = flux_prob * det_prob + + true_e_prob = p / np.sum(p) + + self._logger.debug( + 'true_e_prob = {}'.format( + true_e_prob)) + + def create_e_pdf_for_true_e(true_e_idx): + transfer = np.copy(sm_histo[true_e_idx]) + + # Make it a pdf, i.e. the probability per bin volume. + bin_volumes = ( + log10_reco_e_bw[ + true_e_idx, true_dec_idx, :, np.newaxis, np.newaxis + ] * + psi_edges_bw[ + true_e_idx, true_dec_idx, :, :, np.newaxis + ] * + ang_err_bw[ + true_e_idx, true_dec_idx, :, :, : + ] + ) + m = transfer != 0 + transfer[m] /= bin_volumes[m] + + # Create the enegry PDF f_e = P(log10_E_reco|dec) = + # \int dPsi dang_err P(E_reco,Psi,ang_err). + f_e = np.sum( + transfer * + psi_edges_bw[true_e_idx, true_dec_idx, :, :, np.newaxis] * + ang_err_bw[true_e_idx, true_dec_idx, :, :, :], + axis=(-1, -2) + ) + + del(transfer) + + # Now build the spline and use it to sum over the true neutrino energy + log10_e_bincenters = 0.5*( + sm.reco_e_lower_edges[true_e_idx, true_dec_idx] + + sm.reco_e_upper_edges[true_e_idx, true_dec_idx] + ) + spline, norm = create_spline( + log10_e_bincenters, f_e * true_e_prob[true_e_idx]) + + return eval_spline(xvals, spline)/norm + + sum_pdf = np.sum([ + create_e_pdf_for_true_e(true_e_idx) + for true_e_idx in range(nbins_true_e) + ], axis=0) + + spline, norm = create_spline(xvals, sum_pdf) + + pdf = PDSignalEnergyPDF_new(spline, norm, xvals) + + return pdf + + args_list = [ + ((sm_histo, flux_model, gridfitparams), {}) + for gridfitparams in self.gridfitparams_list + ] + + pdf_list = parallelize( + create_energy_pdf, + args_list, + ncpu=self.ncpu, + ppbar=ppbar) + + del(sm_histo) + + # Save all the energy PDF objects in the PDFSet PDF registry with + # the hash of the individual parameters as key. + for (gridfitparams, pdf) in zip(self.gridfitparams_list, pdf_list): + self.add_pdf(pdf, gridfitparams) + + def get_prob(self, tdm, gridfitparams, tl=None): + """Calculates the signal probability density of each event for the + given set of signal fit parameters on a grid. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the data events for which the + probability should be calculated for. The following data fields must + exist: + + - 'log_energy' + The log10 of the reconstructed energy. + - 'psi' + The opening angle from the source to the event in radians. + - 'ang_err' + The angular error of the event in radians. + gridfitparams : dict + The dictionary holding the signal parameter values for which the + signal energy probability should be calculated. Note, that the + parameter values must match a set of parameter grid values for which + a PDSignalPDF object has been created at construction time of this + PDSignalPDFSet object. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure time. + + Returns + ------- + prob : 1d ndarray + The array with the signal energy probability for each event. + grads : (N_fitparams,N_events)-shaped ndarray | None + The 2D numpy ndarray holding the gradients of the PDF w.r.t. + each fit parameter for each event. The order of the gradients + is the same as the order of floating parameters specified through + the ``param_set`` property. + It is ``None``, if this PDF does not depend on any parameters. + + Raises + ------ + KeyError + If no energy PDF can be found for the given signal parameter values. + """ + # print('Getting signal PDF for gridfitparams={}'.format( # str(gridfitparams))) pdf = self.get_pdf(gridfitparams) @@ -1079,6 +1478,7 @@ def get_prob(self, tdm, gridfitparams, tl=None): class PDSignalPDF(PDF, IsSignalPDF): """This class provides a signal pdf for a given spectrial index value. """ + def __init__( self, f_s, f_e, log_e_edges, psi_edges, ang_err_edges, true_e_prob, **kwargs): @@ -1125,10 +1525,10 @@ def __init__( # Check integrity. integral = np.sum( - #1/(2*np.pi*np.sin(0.5*(psi_edges[None,1:,None]+ + # 1/(2*np.pi*np.sin(0.5*(psi_edges[None,1:,None]+ # psi_edges[None,:-1,None]) # )) * - self.f_s * np.diff(psi_edges)[None,:,None], axis=1) + self.f_s * np.diff(psi_edges)[None, :, None], axis=1) if not np.all(np.isclose(integral[integral > 0], 1)): raise ValueError( 'The integral over Psi of the spatial term must be unity! ' @@ -1216,6 +1616,7 @@ def get_prob(self, tdm, params=None, tl=None): class PDSignalPDFSet(PDFSet, IsSignalPDF, IsParallelizable): """This class provides a signal PDF set for the public data. """ + def __init__( self, ds, @@ -1298,9 +1699,9 @@ def __init__( det_prob = np.empty((len(dE_nu),), dtype=np.double) for i in range(len(dE_nu)): det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( - sin_true_dec = np.sin(src_dec), - true_e_min = true_e_binedges[i], - true_e_max = true_e_binedges[i+1] + sin_true_dec=np.sin(src_dec), + true_e_min=true_e_binedges[i], + true_e_max=true_e_binedges[i+1] ) self._logger.debug('det_prob = {}, sum = {}'.format( @@ -1309,19 +1710,19 @@ def __init__( if not np.isclose(np.sum(det_prob), 1, rtol=0.06): raise ValueError( 'The sum of the detection probabilities is not unity! It is ' - '{}.'.format(np.sum(det_prob))) + '{}.'.format(np.sum(det_prob))) reco_e_bw = np.diff(reco_e_edges) psi_edges_bw = np.diff(psi_edges) ang_err_bw = np.diff(ang_err_edges) bin_volumes = ( - reco_e_bw[:,np.newaxis,np.newaxis] * - psi_edges_bw[np.newaxis,:,np.newaxis] * - ang_err_bw[np.newaxis,np.newaxis,:]) - + reco_e_bw[:, np.newaxis, np.newaxis] * + psi_edges_bw[np.newaxis, :, np.newaxis] * + ang_err_bw[np.newaxis, np.newaxis, :]) # Create the pdf in gamma for different gamma values. + def create_pdf(union_arr, flux_model, gridfitparams): """Creates a pdf for a specific gamma value. """ @@ -1384,7 +1785,7 @@ def create_pdf(union_arr, flux_model, gridfitparams): # Create the spatial PDF f_s = P(Psi|E_reco,ang_err) = # P(E_reco,Psi,ang_err) / \int dPsi P(E_reco,Psi,ang_err). marg_pdf = np.sum( - pdf_arr * psi_edges_bw[np.newaxis,:,np.newaxis], + pdf_arr * psi_edges_bw[np.newaxis, :, np.newaxis], axis=1, keepdims=True ) @@ -1394,9 +1795,9 @@ def create_pdf(union_arr, flux_model, gridfitparams): # Create the enegry PDF f_e = P(log10_E_reco|dec) = # \int dPsi dang_err P(E_reco,Psi,ang_err). f_e = np.sum( - pdf_arr * psi_edges_bw[np.newaxis,:,np.newaxis] * - ang_err_bw[np.newaxis,np.newaxis,:], - axis=(1,2)) + pdf_arr * psi_edges_bw[np.newaxis, :, np.newaxis] * + ang_err_bw[np.newaxis, np.newaxis, :], + axis=(1, 2)) del(pdf_arr) @@ -1408,7 +1809,7 @@ def create_pdf(union_arr, flux_model, gridfitparams): args_list = [ ((union_arr, flux_model, gridfitparams), {}) - for gridfitparams in self.gridfitparams_list + for gridfitparams in self.gridfitparams_list ] pdf_list = parallelize( From bfe94d416ce35d9517b92964a92affda0b3975df Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 25 May 2022 15:22:36 +0200 Subject: [PATCH 084/274] Bug fix. --- skyllh/analyses/i3/trad_ps/signalpdf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index dc50c1be43..78ae125bdc 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1393,7 +1393,7 @@ def create_e_pdf_for_true_e(true_e_idx): spline, norm = create_spline( log10_e_bincenters, f_e * true_e_prob[true_e_idx]) - return eval_spline(xvals, spline)/norm + return eval_spline(xvals, spline) sum_pdf = np.sum([ create_e_pdf_for_true_e(true_e_idx) From feefd5d5e127d4e1b31b9f797a1e8adb2a72cf88 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 25 May 2022 17:37:33 +0200 Subject: [PATCH 085/274] Fix pdf construction for southern sky. --- skyllh/analyses/i3/trad_ps/signalpdf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 78ae125bdc..d7428d8a63 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1390,6 +1390,8 @@ def create_e_pdf_for_true_e(true_e_idx): sm.reco_e_lower_edges[true_e_idx, true_dec_idx] + sm.reco_e_upper_edges[true_e_idx, true_dec_idx] ) + if np.all(log10_e_bincenters == 0): + return np.zeros_like(xvals) spline, norm = create_spline( log10_e_bincenters, f_e * true_e_prob[true_e_idx]) From 1f251e204a0bb8d4bbe1a700b8761b0f2264e361 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 26 May 2022 16:03:49 +0200 Subject: [PATCH 086/274] Added some documentation. --- skyllh/analyses/i3/trad_ps/signalpdf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index d7428d8a63..68d83dbefa 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1386,6 +1386,7 @@ def create_e_pdf_for_true_e(true_e_idx): del(transfer) # Now build the spline and use it to sum over the true neutrino energy + # while also waiting the pdf with the true neutrino energy probability. log10_e_bincenters = 0.5*( sm.reco_e_lower_edges[true_e_idx, true_dec_idx] + sm.reco_e_upper_edges[true_e_idx, true_dec_idx] @@ -1397,6 +1398,7 @@ def create_e_pdf_for_true_e(true_e_idx): return eval_spline(xvals, spline) + # Integrate over the true neutrino energy and create a spline for this. sum_pdf = np.sum([ create_e_pdf_for_true_e(true_e_idx) for true_e_idx in range(nbins_true_e) From 2ceb355c2e8561281fa389ce3edc717e48c7ef55 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 27 May 2022 12:08:29 +0200 Subject: [PATCH 087/274] Added effective area weight to true neutrino energy sampling. --- .../analyses/i3/trad_ps/signal_generator.py | 82 +++++++++++++++---- 1 file changed, 67 insertions(+), 15 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index 56168dfa72..b670c74e30 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -1,4 +1,6 @@ +from code import interact import numpy as np +from scipy import interpolate from skyllh.core.llhratio import LLHRatio from skyllh.core.dataset import Dataset @@ -6,7 +8,8 @@ from skyllh.core.storage import DataFieldRecordArray from skyllh.analyses.i3.trad_ps.utils import ( psi_to_dec_and_ra, - PublicDataSmearingMatrix + PublicDataSmearingMatrix, + PublicDataAeff ) from skyllh.core.py import ( issequenceof, @@ -27,6 +30,54 @@ def __init__(self, ds, **kwargs): pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) + self.effA = PublicDataAeff( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('eff_area_datafile'))) + + def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, + log_e_max): + """Sample the true neutrino energy from the power-law + re-weighted with the detection probability. + """ + m = (self.effA.log_true_e_bincenters >= log_e_min) & ( + self.effA.log_true_e_bincenters < log_e_max) + bin_centers = self.effA.log_true_e_bincenters[m] + low_bin_edges = self.effA.log_true_e_binedges_lower[m] + high_bin_edges = self.effA.log_true_e_binedges_upper[m] + + # Probability P(E_nu | gamma) per bin. + flux_prob = flux_model.get_integral( + 10**low_bin_edges, 10**high_bin_edges + ) / flux_model.get_integral( + 10 ** low_bin_edges[0], + 10 ** high_bin_edges[-1]) + + # Detection probability P(E_nu | sin(dec)) per bin. + det_prob = np.empty((len(bin_centers),), dtype=np.double) + for i in range(len(bin_centers)): + det_prob[i] = self.effA.get_detection_prob_for_sin_true_dec( + src_dec, 10**low_bin_edges[i], 10**high_bin_edges[i], + 10 ** low_bin_edges[0], 10 ** high_bin_edges[-1]) + + # Do the product and normalize again to a probability per bin. + product = flux_prob * det_prob + prob_per_bin = product / np.sum(product) + + # Compute the cumulative distribution CDF. + cum_per_bin = np.cumsum(prob_per_bin) + + # Build a spline for the inverse CDF. + self.inv_cdf_spl = interpolate.splrep( + cum_per_bin, bin_centers, k=1, s=0) + + return + + @staticmethod + def _eval_spline(x, spl): + values = interpolate.splev(x, spl, ext=1) + values = np.nan_to_num(values, nan=0) + return values + def _generate_events( self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): """Generates `n_events` signal events for the given source location @@ -88,12 +139,12 @@ def _generate_events( max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( dec_idx) - # First draw a true neutrino energy from the hypothesis spectrum. - log_true_e = np.log10(flux_model.get_inv_normed_cdf( - rss.random.uniform(size=n_events), - E_min=10**min_log_true_e, - E_max=10**max_log_true_e - )) + # Build the spline for the inverse CDF and draw a true neutrino + # energy from the hypothesis spectrum. + self._generate_inv_cdf_spline(flux_model, src_dec, + min_log_true_e, max_log_true_e) + log_true_e = self._eval_spline( + rss.random.uniform(size=n_events), self.inv_cdf_spl) events['log_true_energy'] = log_true_e @@ -127,7 +178,7 @@ def _generate_events( events['sin_dec'][isvalid] = np.sin(dec) # Add an angular error. Only use non-nan values. - events['ang_err'][isvalid] = ang_err + events['ang_err'][isvalid] = ang_err[isvalid] # Add fields required by the framework events['time'] = np.ones(n_events) @@ -169,12 +220,13 @@ def generate_signal_events( # Cut events that failed to be generated due to missing PDFs. events_ = events_[events_['isvalid']] - - n_evt_generated += len(events_) - if events is None: - events = events_ - else: - events = np.concatenate((events, events_)) + print(events_) + if not len(events_) == 0: + n_evt_generated += len(events_) + if events is None: + events = events_ + else: + events.append(events_) return events @@ -260,7 +312,7 @@ def generate_signal_events(self, rss, mean, poisson=True): else: n_events = int_cast( w_mean, - '`mean` must be castable to type of float!' + '`mean` must be castable to type of int!' ) tot_n_events += n_events From d25b4030acbeb9005af9c4f0416a88e03e5ee3df Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 27 May 2022 12:50:28 +0200 Subject: [PATCH 088/274] Added pdf property to smearing matrix class --- skyllh/analyses/i3/trad_ps/utils.py | 29 +++++++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 7a3d9d451f..41862d2595 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -826,6 +826,35 @@ def true_dec_bin_centers(self): return 0.5*(self._true_dec_bin_edges[:-1] + self._true_dec_bin_edges[1:]) + @property + def pdf(self): + """(read-only) The probability-density-function + P(E_reco,psi,ang_err|E_nu,dec_nu), which, by definition, is the + histogram property divided by the 3D bin volumes for E_reco, psi, and + ang_err. + """ + log10_reco_e_bw = self.reco_e_upper_edges - self.reco_e_lower_edges + psi_bw = self.psi_upper_edges - self.psi_lower_edges + ang_err_bw = self.ang_err_upper_edges - self.ang_err_lower_edges + + bin_volumes = ( + log10_reco_e_bw[ + :, :, :, np.newaxis, np.newaxis + ] * + psi_bw[ + :, :, :, :, np.newaxis + ] * + ang_err_bw[ + :, :, :, :, : + ] + ) + # Divide the histogram bin probability values by their bin volume. + # We do this only where the histogram actually has non-zero entries. + m = self.histogram != 0 + pdf = self.histogram[m] / bin_volumes[m] + + return pdf + def get_true_dec_idx(self, true_dec): """Returns the true declination index for the given true declination value. From ff3be1bb4bb8003c2000cadfb3561a3a963fb0c6 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 27 May 2022 15:09:07 +0200 Subject: [PATCH 089/274] Fix pdf property --- skyllh/analyses/i3/trad_ps/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 41862d2595..e2af8a0f19 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -848,10 +848,12 @@ def pdf(self): :, :, :, :, : ] ) + # Divide the histogram bin probability values by their bin volume. # We do this only where the histogram actually has non-zero entries. + pdf = np.copy(self.histogram) m = self.histogram != 0 - pdf = self.histogram[m] / bin_volumes[m] + pdf[m] /= bin_volumes[m] return pdf From a1f9ffeaec1440807c75b83df69f46c58d1d92fd Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 27 May 2022 15:18:30 +0200 Subject: [PATCH 090/274] Use the pdf property of the smearing matrix to build the energy pdfs. --- skyllh/analyses/i3/trad_ps/signalpdf.py | 58 ++++++++++--------------- 1 file changed, 23 insertions(+), 35 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 68d83dbefa..3dbee585fe 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1086,7 +1086,7 @@ def eval_spline(x, spl): return values -def create_spline(log10_e_bincenters, f_e): +def create_spline(log10_e_bincenters, f_e, norm=False): """Creates the spline representation of the energy PDF. """ @@ -1094,13 +1094,17 @@ def create_spline(log10_e_bincenters, f_e): log10_e_bincenters, f_e, extrapolate=False ) - spl_norm = integrate.quad( - eval_spline, - log10_e_bincenters[0], log10_e_bincenters[-1], - args=(spline,), - limit=200, full_output=1)[0] + if norm: + spl_norm = integrate.quad( + eval_spline, + log10_e_bincenters[0], log10_e_bincenters[-1], + args=(spline,), + limit=200, full_output=1)[0] - return spline, spl_norm + return spline, spl_norm + + else: + return spline class PDSignalEnergyPDF_new(PDF, IsSignalPDF): @@ -1264,10 +1268,12 @@ def __init__( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - # Select the slice of the smearing matrix corresponding to the - # source declination band + # Select the slice of the smearing matrixcorresponding to the + # source declination band. + # Note that we take the pdfs of the reconstruction calculated + # from the smearing matrix here. true_dec_idx = sm.get_true_dec_idx(src_dec) - sm_histo = sm.histogram[:, true_dec_idx] + sm_histo = sm.pdf[:, true_dec_idx] true_e_binedges = np.power(10, sm.true_e_bin_edges) nbins_true_e = len(true_e_binedges) - 1 @@ -1357,54 +1363,36 @@ def create_energy_pdf(sm_histo, flux_model, gridfitparams): true_e_prob)) def create_e_pdf_for_true_e(true_e_idx): - transfer = np.copy(sm_histo[true_e_idx]) - - # Make it a pdf, i.e. the probability per bin volume. - bin_volumes = ( - log10_reco_e_bw[ - true_e_idx, true_dec_idx, :, np.newaxis, np.newaxis - ] * - psi_edges_bw[ - true_e_idx, true_dec_idx, :, :, np.newaxis - ] * - ang_err_bw[ - true_e_idx, true_dec_idx, :, :, : - ] - ) - m = transfer != 0 - transfer[m] /= bin_volumes[m] - # Create the enegry PDF f_e = P(log10_E_reco|dec) = # \int dPsi dang_err P(E_reco,Psi,ang_err). f_e = np.sum( - transfer * + sm_histo[true_e_idx] * psi_edges_bw[true_e_idx, true_dec_idx, :, :, np.newaxis] * ang_err_bw[true_e_idx, true_dec_idx, :, :, :], axis=(-1, -2) ) - del(transfer) - - # Now build the spline and use it to sum over the true neutrino energy - # while also waiting the pdf with the true neutrino energy probability. + # Now build the spline to then use it in the sum over the true + # neutrino energy. At this point, add the weight of the pdf + # with the true neutrino energy probability. log10_e_bincenters = 0.5*( sm.reco_e_lower_edges[true_e_idx, true_dec_idx] + sm.reco_e_upper_edges[true_e_idx, true_dec_idx] ) if np.all(log10_e_bincenters == 0): return np.zeros_like(xvals) - spline, norm = create_spline( + spline = create_spline( log10_e_bincenters, f_e * true_e_prob[true_e_idx]) return eval_spline(xvals, spline) - # Integrate over the true neutrino energy and create a spline for this. + # Integrate over the true neutrino energy and spline the output. sum_pdf = np.sum([ create_e_pdf_for_true_e(true_e_idx) for true_e_idx in range(nbins_true_e) ], axis=0) - spline, norm = create_spline(xvals, sum_pdf) + spline, norm = create_spline(xvals, sum_pdf, norm=True) pdf = PDSignalEnergyPDF_new(spline, norm, xvals) From 2c76161ca2a8ce28830bbf9dc8e0ee6ecaed2035 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 27 May 2022 15:23:47 +0200 Subject: [PATCH 091/274] Removed unused bin width calculation. --- skyllh/analyses/i3/trad_ps/signalpdf.py | 1 - 1 file changed, 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 3dbee585fe..479de7d514 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1318,7 +1318,6 @@ def __init__( 'The sum of the detection probabilities is not unity! It is ' '{}.'.format(np.sum(det_prob))) - log10_reco_e_bw = sm.reco_e_upper_edges-sm.reco_e_lower_edges psi_edges_bw = sm.psi_upper_edges-sm.psi_lower_edges ang_err_bw = sm.ang_err_upper_edges-sm.ang_err_lower_edges From 0740d0e54917beee911e258e1a0dddc202fb0082 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 27 May 2022 15:25:25 +0200 Subject: [PATCH 092/274] Bug fix in the true energy pdf calculation. --- skyllh/analyses/i3/trad_ps/signal_generator.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index b670c74e30..2a58b13ba4 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -50,7 +50,8 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, 10**low_bin_edges, 10**high_bin_edges ) / flux_model.get_integral( 10 ** low_bin_edges[0], - 10 ** high_bin_edges[-1]) + 10 ** high_bin_edges[-1] + ) / (10**high_bin_edges - 10**low_bin_edges) # Detection probability P(E_nu | sin(dec)) per bin. det_prob = np.empty((len(bin_centers),), dtype=np.double) @@ -65,6 +66,8 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, # Compute the cumulative distribution CDF. cum_per_bin = np.cumsum(prob_per_bin) + cum_per_bin = np.concatenate(([0], cum_per_bin)) + bin_centers = np.concatenate(([low_bin_edges[0]], bin_centers)) # Build a spline for the inverse CDF. self.inv_cdf_spl = interpolate.splrep( @@ -74,8 +77,7 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, @staticmethod def _eval_spline(x, spl): - values = interpolate.splev(x, spl, ext=1) - values = np.nan_to_num(values, nan=0) + values = interpolate.splev(x, spl, ext=3) return values def _generate_events( @@ -220,7 +222,6 @@ def generate_signal_events( # Cut events that failed to be generated due to missing PDFs. events_ = events_[events_['isvalid']] - print(events_) if not len(events_) == 0: n_evt_generated += len(events_) if events is None: From e6e5780598ef72de0fb36eaa840fb0747635d79e Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 27 May 2022 16:15:48 +0200 Subject: [PATCH 093/274] Add zero background prob detection --- skyllh/analyses/i3/trad_ps/pdfratio.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index c98c7b1645..3442b382ef 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -4,6 +4,8 @@ import numpy as np +from skyllh.core.py import module_classname +from skyllh.core.debugging import get_logger from skyllh.core.parameters import make_params_hash from skyllh.core.pdf import PDF from skyllh.core.pdfratio import SigSetOverBkgPDFRatio @@ -18,6 +20,8 @@ def __init__(self, sig_pdf_set, bkg_pdf, **kwargs): ---------- sig_pdf_set : """ + self._logger = get_logger(module_classname(self)) + super().__init__( pdf_type=PDF, signalpdfset=sig_pdf_set, @@ -84,12 +88,22 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): if isinstance(bkg_prob, tuple): (bkg_prob, _) = bkg_prob - if np.any(np.invert(bkg_prob > 0)): + if len(sig_prob) != len(bkg_prob): raise ValueError( - 'For at least one event no background probability can be ' - 'calculated! Check your background PDF!') - - ratio = sig_prob / bkg_prob + f'The number of signal ({len(sig_prob)}) and background ' + f'({len(bkg_prob)}) probability values is not equal!') + + m_nonzero_bkg = bkg_prob > 0 + m_zero_bkg = np.invert(m_nonzero_bkg) + if np.any(m_zero_bkg): + ev_idxs = np.where(m_zero_bkg)[0] + logger.warn( + f'For {len(ev_idxs)} events the background probability is ' + f'zero. The event indices of these events are: {ev_idxs}') + + ratio = np.empty((len(sig_prob),), dtype=np.double) + ratio[m_nonzero_bkg] = sig_prob[m_nonzero_bkg] / bkg_prob[m_nonzero_bkg] + ratio[m_zero_bkg] = sig_prob[m_zero_bkg] / np.finfo(np.double).resolution return ratio From a3fdba851bcf0c7ff1b0b8b8f8b08ec883d7ff26 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 27 May 2022 17:25:20 +0200 Subject: [PATCH 094/274] Use debug message --- skyllh/analyses/i3/trad_ps/pdfratio.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index 3442b382ef..504b91f104 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -97,7 +97,7 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): m_zero_bkg = np.invert(m_nonzero_bkg) if np.any(m_zero_bkg): ev_idxs = np.where(m_zero_bkg)[0] - logger.warn( + self._logger.debug( f'For {len(ev_idxs)} events the background probability is ' f'zero. The event indices of these events are: {ev_idxs}') From c031841b9dcef9d73815c9f26027bab3f296453e Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 27 May 2022 17:25:43 +0200 Subject: [PATCH 095/274] Changed from pdf to probability per bin for the true neutrino energy. --- skyllh/analyses/i3/trad_ps/signal_generator.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index 2a58b13ba4..d3299cee29 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -1,4 +1,3 @@ -from code import interact import numpy as np from scipy import interpolate @@ -21,8 +20,8 @@ class PublicDataDatasetSignalGenerator(object): def __init__(self, ds, **kwargs): - """Creates a new instance of the signal generator for generating signal - events from the provided public data dataset. + """Creates a new instance of the signal generator for generating + signal events from a specific public data dataset. """ super().__init__(**kwargs) @@ -45,13 +44,13 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, low_bin_edges = self.effA.log_true_e_binedges_lower[m] high_bin_edges = self.effA.log_true_e_binedges_upper[m] - # Probability P(E_nu | gamma) per bin. - flux_prob = flux_model.get_integral( + # Pdf P(E_nu | gamma). + flux_pd = flux_model.get_integral( 10**low_bin_edges, 10**high_bin_edges ) / flux_model.get_integral( 10 ** low_bin_edges[0], 10 ** high_bin_edges[-1] - ) / (10**high_bin_edges - 10**low_bin_edges) + ) # Detection probability P(E_nu | sin(dec)) per bin. det_prob = np.empty((len(bin_centers),), dtype=np.double) @@ -61,7 +60,7 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, 10 ** low_bin_edges[0], 10 ** high_bin_edges[-1]) # Do the product and normalize again to a probability per bin. - product = flux_prob * det_prob + product = flux_pd * det_prob prob_per_bin = product / np.sum(product) # Compute the cumulative distribution CDF. From 4926a9d34cfff41bdcc070491f17a6c3734b1446 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Fri, 27 May 2022 17:28:53 +0200 Subject: [PATCH 096/274] Changed variables name. --- skyllh/analyses/i3/trad_ps/signal_generator.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index d3299cee29..de85e9a097 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -44,8 +44,8 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, low_bin_edges = self.effA.log_true_e_binedges_lower[m] high_bin_edges = self.effA.log_true_e_binedges_upper[m] - # Pdf P(E_nu | gamma). - flux_pd = flux_model.get_integral( + # Flux probability P(E_nu | gamma) per bin. + flux_prob = flux_model.get_integral( 10**low_bin_edges, 10**high_bin_edges ) / flux_model.get_integral( 10 ** low_bin_edges[0], @@ -60,7 +60,7 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, 10 ** low_bin_edges[0], 10 ** high_bin_edges[-1]) # Do the product and normalize again to a probability per bin. - product = flux_pd * det_prob + product = flux_prob * det_prob prob_per_bin = product / np.sum(product) # Compute the cumulative distribution CDF. From 288090dc2dbca899c61a3746b398fe440529ca14 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 7 Jun 2022 13:02:30 +0200 Subject: [PATCH 097/274] Improve error message --- skyllh/core/dataset.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/skyllh/core/dataset.py b/skyllh/core/dataset.py index 4f1aac086d..7964f2ccf9 100644 --- a/skyllh/core/dataset.py +++ b/skyllh/core/dataset.py @@ -1368,8 +1368,11 @@ def get_dataset(self, name): collection. """ if(name not in self._datasets): + ds_names = '", "'.join(self.dataset_names) + ds_names = '"'+ds_names+'"' raise KeyError('The dataset "%s" is not part of the dataset ' - 'collection "%s"!'%(name, self.name)) + 'collection "%s"! Possible dataset names are: %s!'%( + name, self.name, ds_names)) return self._datasets[name] def get_datasets(self, names): From 540b00ede225ab26117df9d9f5cc152cdd59d8d4 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 8 Jun 2022 11:28:15 +0200 Subject: [PATCH 098/274] Add method to generate the pdf key --- skyllh/core/pdf.py | 45 ++++++++++++++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 11 deletions(-) diff --git a/skyllh/core/pdf.py b/skyllh/core/pdf.py index f401d2205a..c676e34a5e 100644 --- a/skyllh/core/pdf.py +++ b/skyllh/core/pdf.py @@ -1409,12 +1409,41 @@ def axes(self): key = next(iter(self._gridfitparams_hash_pdf_dict.keys())) return self._gridfitparams_hash_pdf_dict[key].axes + def __getitem__(self, k): + """(read-only) Returns the PDF for the given PDF key. + """ + return self._gridfitparams_hash_pdf_dict[k] + def items(self): """Returns the list of 2-element tuples for the PDF stored in this PDFSet object. """ return self._gridfitparams_hash_pdf_dict.items() + def make_pdf_key(self, gridfitparams): + """Creates the PDF key for the given grid fit parameter values. + + Parameters + ---------- + gridfitparams : dict | int + The dictionary with the grid fit parameters for which the PDF key + should get made. If an integer is given, it is assumed to be + the PDF key. + + Returns + ------- + pdf_key : int + The hash that represents the key for the PDF with the given grid + fit parameter values. + """ + if(isinstance(gridfitparams, int)): + return gridfitparams + if(isinstance(gridfitparams, dict)): + return make_params_hash(gridfitparams) + + raise TypeError( + 'The gridfitparams argument must be of type dict or int!') + def add_pdf(self, pdf, gridfitparams): """Adds the given PDF object for the given parameters to the internal registry. If this PDF set is not empty, the to-be-added PDF must have @@ -1441,10 +1470,9 @@ def add_pdf(self, pdf, gridfitparams): if(not isinstance(pdf, self.pdf_type)): raise TypeError('The pdf argument must be an instance of %s!' % ( typename(self.pdf_type))) - if(not isinstance(gridfitparams, dict)): - raise TypeError('The fitparams argument must be of type dict!') - gridfitparams_hash = make_params_hash(gridfitparams) + gridfitparams_hash = self.make_pdf_key(gridfitparams) + if(gridfitparams_hash in self._gridfitparams_hash_pdf_dict): raise KeyError('The PDF with grid fit parameters %s was already ' 'added!' % (str(gridfitparams))) @@ -1484,17 +1512,12 @@ def get_pdf(self, gridfitparams): KeyError If no PDF object was created for the given set of parameters. """ - if(isinstance(gridfitparams, int)): - gridfitparams_hash = gridfitparams - elif(isinstance(gridfitparams, dict)): - gridfitparams_hash = make_params_hash(gridfitparams) - else: - raise TypeError( - 'The gridfitparams argument must be of type dict or int!') + gridfitparams_hash = self.make_pdf_key(gridfitparams) if(gridfitparams_hash not in self._gridfitparams_hash_pdf_dict): raise KeyError( - 'No PDF was created for the parameter set "%s"!' % (str(gridfitparams))) + 'No PDF was created for the parameter set "%s"!' % + (str(gridfitparams))) pdf = self._gridfitparams_hash_pdf_dict[gridfitparams_hash] return pdf From 36e33caa0d230119d1f3c19a6b8134463500d444 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 8 Jun 2022 13:04:52 +0200 Subject: [PATCH 099/274] Adding logging information --- skyllh/i3/dataset.py | 52 +++++++++++++++++++++++++++++++++++--------- 1 file changed, 42 insertions(+), 10 deletions(-) diff --git a/skyllh/i3/dataset.py b/skyllh/i3/dataset.py index 038f64aac2..a40a38e052 100644 --- a/skyllh/i3/dataset.py +++ b/skyllh/i3/dataset.py @@ -4,11 +4,15 @@ import os.path from skyllh.core import display -from skyllh.core.py import issequenceof +from skyllh.core.py import ( + issequenceof, + module_classname +) from skyllh.core.dataset import ( Dataset, DatasetData ) +from skyllh.core.debugging import get_logger from skyllh.core.storage import ( DataFieldRecordArray, create_FileLoader @@ -19,6 +23,7 @@ # This will change the skyllh.core.config.CFG dictionary. from skyllh.i3 import config + class I3Dataset(Dataset): """The I3Dataset class is an IceCube specific Dataset class that adds IceCube specific properties to the Dataset class. These additional @@ -62,6 +67,8 @@ def __init__(self, grl_pathfilenames=None, *args, **kwargs): """ super(I3Dataset, self).__init__(*args, **kwargs) + self._logger = get_logger(module_classname(self)) + self.grl_pathfilename_list = grl_pathfilenames self.grl_field_name_renaming_dict = dict() @@ -318,15 +325,40 @@ def prepare_data(self, data, tl=None): # Select only the experimental data which fits the good-run-list for # this dataset. - if((data.grl is not None) and - ('run' in data.grl) and - ('run' in data.exp)): - task = 'Selected only the experimental data that matches the GRL '\ - 'for dataset "%s".'%(self.name) - with TaskTimer(tl, task): - runs = np.unique(data.grl['run']) - mask = np.isin(data.exp['run'], runs) - data.exp = data.exp[mask] + if data.grl is not None: + # Select based on run information. + if (('run' in data.grl) and + ('run' in data.exp)): + task = 'Selected only the experimental data that matches the '\ + 'run information in the GRL for dataset "%s".'%(self.name) + with TaskTimer(tl, task): + runs = np.unique(data.grl['run']) + mask = np.isin(data.exp['run'], runs) + data.exp = data.exp[mask] + + # Select based on detector on-time information. + if (('start' in data.grl) and + ('stop' in data.grl) and + ('time' in data.exp)): + task = 'Selected only the experimental data that matches the '\ + 'detector\'s on-time information in the GRL for dataset '\ + '"%s".'%(self.name) + with TaskTimer(tl, task): + mask = np.zeros((len(data.exp),), dtype=np.bool) + for (start, stop) in zip(data.grl['start'], + data.grl['stop']): + mask |= ( + (data.exp['time'] >= start) & + (data.exp['time'] < stop) + ) + + if np.any(~mask): + n_cut_evts = np.count_nonzero(~mask) + self._logger.info( + f'Cutting {n_cut_evts} events from dataset ' + f'{self.name} due to GRL on-time window ' + 'information.') + data.exp = data.exp[mask] class I3DatasetData(DatasetData): From b17fe890d60b6adec4696e59f4d429c2f7bca117 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 8 Jun 2022 13:16:00 +0200 Subject: [PATCH 100/274] Simplify dataset definition --- skyllh/datasets/i3/PublicData_10y_ps.py | 39 ++++++++++++------------- 1 file changed, 18 insertions(+), 21 deletions(-) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 6eee094ef2..192140c234 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -287,7 +287,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC40.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(2., 9. + 0.01, 0.125) + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) IC40.define_binning('log_energy', energy_bins) # ---------- IC59 ---------------------------------------------------------- @@ -336,7 +336,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC79.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(2., 9. + 0.01, 0.125) + energy_bins = np.arange(2., 9.5 + 0.01, 0.125) IC79.define_binning('log_energy', energy_bins) # ---------- IC86-I -------------------------------------------------------- @@ -362,7 +362,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): ])) IC86_I.define_binning('sin_dec', sin_dec_bins) - energy_bins = np.arange(1., 10. + 0.01, 0.125) + energy_bins = np.arange(1., 9.5 + 0.01, 0.125) IC86_I.define_binning('log_energy', energy_bins) # ---------- IC86-II ------------------------------------------------------- @@ -486,32 +486,29 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_II.get_binning_definition('log_energy')) # ---------- IC86-II-VII --------------------------------------------------- + ds_list = [ + IC86_II, + IC86_III, + IC86_IV, + IC86_V, + IC86_VI, + IC86_VII, + ] IC86_II_VII = I3Dataset( name = 'IC86_II-VII', - exp_pathfilenames = [ - 'events/IC86_II_exp.csv', - 'events/IC86_III_exp.csv', - 'events/IC86_IV_exp.csv', - 'events/IC86_V_exp.csv', - 'events/IC86_VI_exp.csv', - 'events/IC86_VII_exp.csv' - ], - grl_pathfilenames = [ - 'uptime/IC86_II_exp.csv', - 'uptime/IC86_III_exp.csv', - 'uptime/IC86_IV_exp.csv', - 'uptime/IC86_V_exp.csv', - 'uptime/IC86_VI_exp.csv', - 'uptime/IC86_VII_exp.csv' - ], + exp_pathfilenames = I3Dataset.get_combined_exp_pathfilenames(ds_list), + grl_pathfilenames = I3Dataset.get_combined_grl_pathfilenames(ds_list), mc_pathfilenames = None, **ds_kwargs ) IC86_II_VII.grl_field_name_renaming_dict = grl_field_name_renaming_dict IC86_II_VII.add_aux_data_definition( - 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') + 'eff_area_datafile', + IC86_II.get_aux_data_definition('eff_area_datafile')) + IC86_II_VII.add_aux_data_definition( - 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + 'smearing_datafile', + IC86_II.get_aux_data_definition('smearing_datafile')) IC86_II_VII.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) From 20dcac403ff4739a51de5e82b3674a928747240b Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 8 Jun 2022 13:45:16 +0200 Subject: [PATCH 101/274] Construct the p(E_reco|E_nu) PDF for the entire reco energy range. --- skyllh/analyses/i3/trad_ps/signalpdf.py | 44 +++++++++++++++++++++---- 1 file changed, 38 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 479de7d514..be65ea6556 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1280,11 +1280,21 @@ def __init__( E_nu_min = true_e_binedges[:-1] E_nu_max = true_e_binedges[1:] - # Define the values at which to evaluate the splines + # Define the values at which to evaluate the splines. + # Some bins might have zero bin widths. + m = (sm.reco_e_upper_edges[:,true_dec_idx] - + sm.reco_e_lower_edges[:,true_dec_idx]) > 0 + le = sm.reco_e_lower_edges[:,true_dec_idx][m].flatten() + ue = sm.reco_e_upper_edges[:,true_dec_idx][m].flatten() + min_log10_reco_e = np.min(le) + max_log10_reco_e = np.max(ue) + d_log10_reco_e = np.min(ue - le) / 20 + n_xvals = int((max_log10_reco_e - min_log10_reco_e) / d_log10_reco_e) xvals = np.linspace( - min(sm.reco_e_lower_edges.flatten()), - max(sm.reco_e_upper_edges.flatten()), - 1000) + min_log10_reco_e, + max_log10_reco_e, + n_xvals + ) # Calculate the neutrino enegry bin widths in GeV. dE_nu = np.diff(true_e_binedges) @@ -1362,6 +1372,9 @@ def create_energy_pdf(sm_histo, flux_model, gridfitparams): true_e_prob)) def create_e_pdf_for_true_e(true_e_idx): + """This functions creates a spline for the reco energy + distribution given a true neutrino engery. + """ # Create the enegry PDF f_e = P(log10_E_reco|dec) = # \int dPsi dang_err P(E_reco,Psi,ang_err). f_e = np.sum( @@ -1380,8 +1393,27 @@ def create_e_pdf_for_true_e(true_e_idx): ) if np.all(log10_e_bincenters == 0): return np.zeros_like(xvals) - spline = create_spline( - log10_e_bincenters, f_e * true_e_prob[true_e_idx]) + + p = f_e * true_e_prob[true_e_idx] + + # Create the spline from the lowest and highest bin edge in + # reconstructed enegry. + x = np.concatenate(( + np.array( + [sm.reco_e_lower_edges[true_e_idx, true_dec_idx][0]]), + log10_e_bincenters, + np.array( + [sm.reco_e_upper_edges[true_e_idx, true_dec_idx][-1]]) + )) + y = np.concatenate(( + np.array( + [p[0]]), + p, + np.array( + [p[-1]]) + )) + + spline = create_spline(x, y) return eval_spline(xvals, spline) From 1f4ff58e30682e72b1b6a33ead644a98245a9350 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 8 Jun 2022 14:21:17 +0200 Subject: [PATCH 102/274] Add feature to cap the enegry PDF ratio --- skyllh/analyses/i3/trad_ps/pdfratio.py | 55 ++++++++++++++++++++------ 1 file changed, 43 insertions(+), 12 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index 504b91f104..177c54c886 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -12,7 +12,7 @@ class PDPDFRatio(SigSetOverBkgPDFRatio): - def __init__(self, sig_pdf_set, bkg_pdf, **kwargs): + def __init__(self, sig_pdf_set, bkg_pdf, cap_ratio=False, **kwargs): """Creates a PDFRatio instance for the public data. It takes a signal PDF set for different discrete gamma values. @@ -32,15 +32,28 @@ def __init__(self, sig_pdf_set, bkg_pdf, **kwargs): self._interpolmethod_instance = self.interpolmethod( self._get_ratio_values, sig_pdf_set.fitparams_grid_set) - """ - # Get the requires field names from the background and signal pdf. - self._data_field_name_list = [] - for axis in sig_pdf_set.axes: - field_name_list.append(axis.name) - for axis in bkg_pdf.axes: - if axis.name not in field_name_list: - field_name_list.append(axis.name) - """ + # Calculate the ratio value for the phase space where no background + # is available. We will take the p_sig percentile of the signal like + # phase space. + ratio_perc = 99 + + # Get the log10 reco energy values where the background pdf has + # non-zero values. + (n_logE, n_sinDec) = bkg_pdf._hist_logE_sinDec.shape + bd = bkg_pdf._hist_logE_sinDec > 0 + log10_e_bc = bkg_pdf.get_binning('log_energy').bincenters + self.ratio_fill_value_dict = dict() + for sig_pdf_key in sig_pdf_set.pdf_keys: + sigpdf = sig_pdf_set[sig_pdf_key] + sigvals = sigpdf.get_pd_by_log10_e(log10_e_bc) + sigvals = np.broadcast_to(sigvals, (n_sinDec, n_logE)).T + r = sigvals[bd] / bkg_pdf._hist_logE_sinDec[bd] + val = np.percentile(r[r > 1.], ratio_perc) + self.ratio_fill_value_dict[sig_pdf_key] = val + + self.cap_ratio = cap_ratio + if cap_ratio: + self._logger.info('The PDF ratio will be capped!') # Create cache variables for the last ratio value and gradients in order # to avoid the recalculation of the ratio value when the @@ -50,6 +63,17 @@ def __init__(self, sig_pdf_set, bkg_pdf, **kwargs): self._cache_ratio = None self._cache_gradients = None + @property + def cap_ratio(self): + """Boolean switch whether to cap the ratio where no background + information is available (True) or use the smallest possible floating + point number greater than zero as background pdf value (False). + """ + return self._cap_ratio + @cap_ratio.setter + def cap_ratio(self, b): + self._cap_ratio = b + def _get_signal_fitparam_names(self): """This method must be re-implemented by the derived class and needs to return the list of signal fit parameter names, this PDF ratio is a @@ -80,7 +104,9 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): """Select the signal PDF for the given fit parameter grid point and evaluates the S/B ratio for all the given events. """ - sig_prob = self.signalpdfset.get_prob(tdm, gridfitparams) + sig_pdf_key = self.signalpdfset.make_pdf_key(gridfitparams) + + sig_prob = self.signalpdfset.get_pdf(sig_pdf_key).get_prob(tdm) if isinstance(sig_prob, tuple): (sig_prob, _) = sig_prob @@ -103,7 +129,12 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): ratio = np.empty((len(sig_prob),), dtype=np.double) ratio[m_nonzero_bkg] = sig_prob[m_nonzero_bkg] / bkg_prob[m_nonzero_bkg] - ratio[m_zero_bkg] = sig_prob[m_zero_bkg] / np.finfo(np.double).resolution + + if self._cap_ratio: + ratio[m_zero_bkg] = self.ratio_fill_value_dict[sig_pdf_key] + else: + ratio[m_zero_bkg] = (sig_prob[m_zero_bkg] / + np.finfo(np.double).resolution) return ratio From 023a64cce3c16ab134b5989cefe5318c60ee2cb6 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 8 Jun 2022 14:21:54 +0200 Subject: [PATCH 103/274] Add feature to cap the enegry PDF ratio --- skyllh/analyses/i3/trad_ps/analysis.py | 86 +++++++++++++++++++------- 1 file changed, 64 insertions(+), 22 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index 318b228199..de133fc7a8 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -142,6 +142,7 @@ def create_analysis( ns_seed=10.0, gamma_seed=3, cache_dir='.', + cap_ratio=False, n_mc_events=int(1e7), compress_data=False, keep_data_fields=None, @@ -307,7 +308,8 @@ def create_analysis( energy_pdfratio = PDPDFRatio( sig_pdf_set=energy_sigpdfset, - bkg_pdf=energy_bkgpdf + bkg_pdf=energy_bkgpdf, + cap_ratio=cap_ratio ) pdfratios = [spatial_pdfratio, energy_pdfratio] @@ -331,27 +333,66 @@ def create_analysis( '10-year public point source sample.', formatter_class=argparse.RawTextHelpFormatter ) - p.add_argument('--dec', default=23.8, type=float, - help='The source declination in degrees.') - p.add_argument('--ra', default=216.76, type=float, - help='The source right-ascention in degrees.') - p.add_argument('--gamma-seed', default=3, type=float, - help='The seed value of the gamma fit parameter.') - p.add_argument('--data_base_path', default=None, type=str, - help='The base path to the data samples (default=None)' - ) - p.add_argument('--pdf-seed', default=1, type=int, - help='The random number generator seed for generating the signal PDF.') - p.add_argument('--seed', default=1, type=int, - help='The random number generator seed for the likelihood minimization.') - p.add_argument('--ncpu', default=1, type=int, - help='The number of CPUs to utilize where parallelization is possible.' - ) - p.add_argument('--n-mc-events', default=int(1e7), type=int, - help='The number of MC events to sample for the energy signal PDF.' - ) - p.add_argument('--cache-dir', default='.', type=str, - help='The cache directory to look for cached data, e.g. signal PDFs.') + p.add_argument( + '--dec', + default=23.8, + type=float, + help='The source declination in degrees.' + ) + p.add_argument( + '--ra', + default=216.76, + type=float, + help='The source right-ascention in degrees.' + ) + p.add_argument( + '--gamma-seed', + default=3, + type=float, + help='The seed value of the gamma fit parameter.' + ) + p.add_argument( + '--data_base_path', + default=None, + type=str, + help='The base path to the data samples (default=None)' + ) + p.add_argument( + '--pdf-seed', + default=1, + type=int, + help='The random number generator seed for generating the ' + 'signal PDF.' + ) + p.add_argument( + '--seed', + default=1, + type=int, + help='The random number generator seed for the likelihood ' + 'minimization.' + ) + p.add_argument( + '--ncpu', + default=1, + type=int, + help='The number of CPUs to utilize where parallelization is possible.' + ) + p.add_argument( + '--n-mc-events', + default=int(1e7), + type=int, + help='The number of MC events to sample for the energy signal PDF.' + ) + p.add_argument( + '--cache-dir', + default='.', + type=str, + help='The cache directory to look for cached data, e.g. signal PDFs.') + p.add_argument( + '--cap-ratio', + action='store_true', + help='Switch to cap the energy PDF ratio.') + p.set_defaults(cap_ratio=False) args = p.parse_args() # Setup `skyllh` package logging. @@ -396,6 +437,7 @@ def create_analysis( datasets, source, cache_dir=args.cache_dir, + cap_ratio=args.cap_ratio, n_mc_events=args.n_mc_events, gamma_seed=args.gamma_seed, tl=tl) From 40a6248df1ce680ad2ee2747aa26b0d610aa9de1 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 13 Jun 2022 14:30:41 +0200 Subject: [PATCH 104/274] Added script to generate MCeq fluxes --- .../i3/trad_ps/scripts/mceq_atm_bkg.py | 174 ++++++++++++++++++ 1 file changed, 174 insertions(+) create mode 100644 skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py diff --git a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py new file mode 100644 index 0000000000..c82d953228 --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py @@ -0,0 +1,174 @@ +import argparse +import numpy as np +import os.path +import pickle + +import crflux.models as pm +import mceq_config as config +from MCEq.core import MCEqRun + +from skyllh.analyses.i3.trad_ps.utils import PublicDataAeff +from skyllh.datasets.i3 import PublicData_10y_ps + +def create_flux_file(save_path, ds): + """Creates a pickle file containing the flux for the given dataset. + """ + output_filename = f'{ds.name}.pkl' + output_pathfilename = '' + if args.save_path is None: + output_pathfilename = os.path.join( + ds.root_dir, 'fluxes', output_filename) + else: + output_pathfilename = os.path.join( + args.save_path, output_filename) + + print('Output path filename: %s'%(output_pathfilename)) + + # Load the effective area instance to get the binning information. + aeff = PublicDataAeff( + os.path.join( + ds.root_dir, + ds.get_aux_data_definition('eff_area_datafile')[0] + ) + ) + + # Setup MCeq. + config.e_min = 10**aeff.log_true_e_binedges_lower[0] + config.e_max = 10**aeff.log_true_e_binedges_upper[-1] + print('E_min = %s'%(config.e_min)) + print('E_max = %s'%(config.e_max)) + + mceq = MCEqRun( + interaction_model="SIBYLL2.3c", + primary_model=(pm.HillasGaisser2012, "H3a"), + theta_deg=0.0, + density_model=("MSIS00_IC", ("SouthPole", "January")), + ) + + mag = 0 + # Use the same binning as for the effective area. + # theta = delta + pi/2 + print('sin_true_dec_binedges: %s'%(str(aeff.sin_true_dec_binedges))) + theta_angles_binedges = np.rad2deg( + np.arcsin(aeff.sin_true_dec_binedges) + np.pi/2 + ) + theta_angles = 0.5*(theta_angles_binedges[:-1] + theta_angles_binedges[1:]) + print('Theta angles = %s'%(str(theta_angles))) + + flux_def = dict() + + all_component_names = [ + "numu_conv", + "numu_pr", + "numu_total", + "mu_conv", + "mu_pr", + "mu_total", + "nue_conv", + "nue_pr", + "nue_total", + "nutau_pr", + ] + + # Initialize empty grid + for frac in all_component_names: + flux_def[frac] = np.zeros( + (len(mceq.e_grid), len(theta_angles))) + + # fluxes calculated for different theta_angles + for ti, theta in enumerate(theta_angles): + mceq.set_theta_deg(theta) + mceq.solve() + + # same meaning of prefixes for muon neutrinos as for muons + flux_def["mu_conv"][:, ti] = ( + mceq.get_solution("conv_mu+", mag) + + mceq.get_solution("conv_mu-", mag) + ) + + flux_def["mu_pr"][:, ti] = ( + mceq.get_solution("pr_mu+", mag) + + mceq.get_solution("pr_mu-", mag) + ) + + flux_def["mu_total"][:, ti] = ( + mceq.get_solution("total_mu+", mag) + + mceq.get_solution("total_mu-", mag) + ) + + # same meaning of prefixes for muon neutrinos as for muons + flux_def["numu_conv"][:, ti] = ( + mceq.get_solution("conv_numu", mag) + + mceq.get_solution("conv_antinumu", mag) + ) + + flux_def["numu_pr"][:, ti] = ( + mceq.get_solution("pr_numu", mag) + + mceq.get_solution("pr_antinumu", mag) + ) + + flux_def["numu_total"][:, ti] = ( + mceq.get_solution("total_numu", mag) + + mceq.get_solution("total_antinumu", mag) + ) + + # same meaning of prefixes for electron neutrinos as for muons + flux_def["nue_conv"][:, ti] = ( + mceq.get_solution("conv_nue", mag) + + mceq.get_solution("conv_antinue", mag) + ) + + flux_def["nue_pr"][:, ti] = ( + mceq.get_solution("pr_nue", mag) + + mceq.get_solution("pr_antinue", mag) + ) + + flux_def["nue_total"][:, ti] = ( + mceq.get_solution("total_nue", mag) + + mceq.get_solution("total_antinue", mag) + ) + + # since there are no conventional tau neutrinos, prompt=total + flux_def["nutau_pr"][:, ti] = ( + mceq.get_solution("total_nutau", mag) + + mceq.get_solution("total_antinutau", mag) + ) + print("\U0001F973") + + # Save the result to the output file. + with open(output_pathfilename, 'wb') as f: + pickle.dump(((mceq.e_grid, theta_angles), flux_def), f) + print('Saved fluxes for dataset %s to: %s'%(ds.name, output_pathfilename)) + +#------------------------------------------------------------------------------- + +if __name__ == '__main__': + + parser = argparse.ArgumentParser( + description='Generate atmospheric background fluxes with MCEq.' + ) + parser.add_argument( + '-b', + '--data-base-path', + type=str, + default='/data/ana/analyses', + help='The base path of the data repository.' + ) + parser.add_argument( + '-s', + '--save-path', + type=str, + default=None + ) + + args = parser.parse_args() + + dsc = PublicData_10y_ps.create_dataset_collection(args.data_base_path) + + dataset_names = ['IC40', 'IC59', 'IC79', 'IC86_I', 'IC86_II'] + for ds_name in dataset_names: + ds = dsc.get_dataset(ds_name) + create_flux_file( + save_path = args.save_path, + ds=ds + ) From 37f41477901942f5e728834fc6ef33e67a0ce6a2 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 14 Jun 2022 10:48:19 +0200 Subject: [PATCH 105/274] Removed psi floor related to the kde analysis. --- skyllh/analyses/i3/trad_ps/analysis.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index de133fc7a8..f5251b6983 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -119,8 +119,8 @@ def psi_func(tdm, src_hypo_group_manager, fitparams): # Floor psi values below the first bin location in spatial KDE PDF. # Flooring at the boundary (1e-6) requires a regeneration of the # spatial KDE splines. - floor = 10**(-5.95442953) - psi = np.where(psi < floor, floor, psi) + # floor = 10**(-5.95442953) + # psi = np.where(psi < floor, floor, psi) # For now we support only a single source, hence return psi[0]. return psi[0, :] From b727e7c1b7c9df3f8b03ee296e231ea86037dc47 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 14 Jun 2022 10:58:44 +0200 Subject: [PATCH 106/274] Renamed signal energy pdf classes. --- skyllh/analyses/i3/trad_ps/analysis.py | 9 ++------- skyllh/analyses/i3/trad_ps/signalpdf.py | 20 ++++++++++---------- 2 files changed, 12 insertions(+), 17 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/trad_ps/analysis.py index f5251b6983..b07ef70f5f 100644 --- a/skyllh/analyses/i3/trad_ps/analysis.py +++ b/skyllh/analyses/i3/trad_ps/analysis.py @@ -84,7 +84,7 @@ PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod ) from skyllh.analyses.i3.trad_ps.signalpdf import ( - PDSignalEnergyPDFSet_new + PDSignalEnergyPDFSet ) from skyllh.analyses.i3.trad_ps.pdfratio import ( PDPDFRatio @@ -116,11 +116,6 @@ def psi_func(tdm, src_hypo_group_manager, fitparams): x[x > 1.] = 1. psi = (2.0*np.arcsin(np.sqrt(x))) - # Floor psi values below the first bin location in spatial KDE PDF. - # Flooring at the boundary (1e-6) requires a regeneration of the - # spatial KDE splines. - # floor = 10**(-5.95442953) - # psi = np.where(psi < floor, floor, psi) # For now we support only a single source, hence return psi[0]. return psi[0, :] @@ -295,7 +290,7 @@ def create_analysis( spatial_sigpdf, spatial_bkgpdf) # Create the energy PDF ratio instance for this dataset. - energy_sigpdfset = PDSignalEnergyPDFSet_new( + energy_sigpdfset = PDSignalEnergyPDFSet( ds=ds, src_dec=source.dec, flux_model=flux_model, diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index be65ea6556..6d13b55b5d 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -1107,7 +1107,7 @@ def create_spline(log10_e_bincenters, f_e, norm=False): return spline -class PDSignalEnergyPDF_new(PDF, IsSignalPDF): +class PDSignalEnergyPDF(PDF, IsSignalPDF): """This class provides a signal energy PDF for a spectrial index value. """ @@ -1207,7 +1207,7 @@ def get_prob(self, tdm, params=None, tl=None): return (pd, None) -class PDSignalEnergyPDFSet_new(PDFSet, IsSignalPDF, IsParallelizable): +class PDSignalEnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): """This class provides a signal energy PDF set for the public data. It creates a set of PDSignalEnergyPDF instances, one for each spectral index value on a grid. @@ -1282,10 +1282,10 @@ def __init__( # Define the values at which to evaluate the splines. # Some bins might have zero bin widths. - m = (sm.reco_e_upper_edges[:,true_dec_idx] - - sm.reco_e_lower_edges[:,true_dec_idx]) > 0 - le = sm.reco_e_lower_edges[:,true_dec_idx][m].flatten() - ue = sm.reco_e_upper_edges[:,true_dec_idx][m].flatten() + m = (sm.reco_e_upper_edges[:, true_dec_idx] - + sm.reco_e_lower_edges[:, true_dec_idx]) > 0 + le = sm.reco_e_lower_edges[:, true_dec_idx][m].flatten() + ue = sm.reco_e_upper_edges[:, true_dec_idx][m].flatten() min_log10_reco_e = np.min(le) max_log10_reco_e = np.max(ue) d_log10_reco_e = np.min(ue - le) / 20 @@ -1425,7 +1425,7 @@ def create_e_pdf_for_true_e(true_e_idx): spline, norm = create_spline(xvals, sum_pdf, norm=True) - pdf = PDSignalEnergyPDF_new(spline, norm, xvals) + pdf = PDSignalEnergyPDF(spline, norm, xvals) return pdf @@ -1498,7 +1498,7 @@ def get_prob(self, tdm, gridfitparams, tl=None): return (prob, grads) -class PDSignalPDF(PDF, IsSignalPDF): +class PDSignalPDF_unionized_matrix(PDF, IsSignalPDF): """This class provides a signal pdf for a given spectrial index value. """ @@ -1636,7 +1636,7 @@ def get_prob(self, tdm, params=None, tl=None): return (pd_spatial * pd_energy, None) -class PDSignalPDFSet(PDFSet, IsSignalPDF, IsParallelizable): +class PDSignalPDFSet_unionized_matrix(PDFSet, IsSignalPDF, IsParallelizable): """This class provides a signal PDF set for the public data. """ @@ -1824,7 +1824,7 @@ def create_pdf(union_arr, flux_model, gridfitparams): del(pdf_arr) - pdf = PDSignalPDF( + pdf = PDSignalPDF_unionized_matrix( f_s, f_e, reco_e_edges, psi_edges, ang_err_edges, true_e_prob) From 0950e81180dd7f4a5c038e315dec460cc3dfd586 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 15 Jun 2022 13:54:11 +0200 Subject: [PATCH 107/274] Add method to get the true energy bin index from the smearing matrix --- skyllh/analyses/i3/trad_ps/utils.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index e2af8a0f19..9a39f62ca8 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -863,8 +863,8 @@ def get_true_dec_idx(self, true_dec): Parameters ---------- - dec : float - The declination value in radians. + true_dec : float + The true declination value in radians. Returns ------- @@ -880,6 +880,31 @@ def get_true_dec_idx(self, true_dec): return true_dec_idx + def get_log10_true_e_idx(self, log10_true_e): + """Returns the bin index for the given true log10 energy value. + + Parameters + ---------- + log10_true_e : float + The log10 value of the true energy. + + Returns + ------- + log10_true_e_idx : int + The index of the true log10 energy bin for the given log10 true + energy value. + """ + if (log10_true_e < self.true_e_bin_edges[0]) or\ + (log10_true_e > self.true_e_bin_edges[-1]): + raise ValueError( + 'The log10 true energy value {} is not supported by the ' + 'smearing matrix!'.format(log10_true_e)) + + log10_true_e_idx = np.digitize( + log10_true_e, self._true_e_bin_edges) - 1 + + return log10_true_e_idx + def get_reco_e_idx(self, true_e_idx, true_dec_idx, reco_e): """Returns the bin index for the given reco energy value given the given true energy and true declination bin indices. From c2bb7675111740889f73f1dd1e623befedacc085 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 15 Jun 2022 14:00:13 +0200 Subject: [PATCH 108/274] Limit energy range to 2 and 9 --- skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py index c82d953228..6eab2c0cbd 100644 --- a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py +++ b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py @@ -33,8 +33,11 @@ def create_flux_file(save_path, ds): ) # Setup MCeq. - config.e_min = 10**aeff.log_true_e_binedges_lower[0] - config.e_max = 10**aeff.log_true_e_binedges_upper[-1] + config.e_min = float( + 10**(np.max([aeff.log_true_e_binedges_lower[0], 2]))) + config.e_max = float( + 10**(np.min([aeff.log_true_e_binedges_upper[-1], 9])+0.05)) + print('E_min = %s'%(config.e_min)) print('E_max = %s'%(config.e_max)) @@ -45,6 +48,8 @@ def create_flux_file(save_path, ds): density_model=("MSIS00_IC", ("SouthPole", "January")), ) + print('MCEq log10(e_grid) = %s'%(str(np.log10(mceq.e_grid)))) + mag = 0 # Use the same binning as for the effective area. # theta = delta + pi/2 From e54779d39ca2fca0cce49728ce74e6206c2011e2 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 27 Jun 2022 11:51:29 +0200 Subject: [PATCH 109/274] Define MCEq flux data files --- skyllh/datasets/i3/PublicData_10y_ps.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 192140c234..b1bf0f4864 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -279,6 +279,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC40_effectiveArea.csv') IC40.add_aux_data_definition( 'smearing_datafile', 'irfs/IC40_smearing.csv') + IC40.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC40.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.25, 10 + 1), @@ -303,6 +305,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC59_effectiveArea.csv') IC59.add_aux_data_definition( 'smearing_datafile', 'irfs/IC59_smearing.csv') + IC59.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC59.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.95, 2 + 1), @@ -328,6 +332,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC79_effectiveArea.csv') IC79.add_aux_data_definition( 'smearing_datafile', 'irfs/IC79_smearing.csv') + IC79.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC79.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.75, 10 + 1), @@ -352,6 +358,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_I_effectiveArea.csv') IC86_I.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_I_smearing.csv') + IC86_I.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_I.pkl') b = np.sin(np.radians(-5.)) # North/South transition boundary. sin_dec_bins = np.unique(np.concatenate([ @@ -378,6 +386,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_II.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_II.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), @@ -403,6 +413,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_III.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_III.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_III.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -422,6 +434,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_IV.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_IV.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_IV.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -441,6 +455,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_V.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_V.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_V.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -460,6 +476,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VI.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_VI.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_VI.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) @@ -479,6 +497,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'eff_area_datafile', 'irfs/IC86_II_effectiveArea.csv') IC86_VII.add_aux_data_definition( 'smearing_datafile', 'irfs/IC86_II_smearing.csv') + IC86_VII.add_aux_data_definition( + 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_VII.add_binning_definition( IC86_II.get_binning_definition('sin_dec')) From 265ee3f36885309c0548a1ba8245dc46eb7b712a Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 27 Jun 2022 11:52:32 +0200 Subject: [PATCH 110/274] Use MCEq flux datafile definition from the dataset --- skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py index 6eab2c0cbd..f5841d6a07 100644 --- a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py +++ b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py @@ -13,11 +13,10 @@ def create_flux_file(save_path, ds): """Creates a pickle file containing the flux for the given dataset. """ - output_filename = f'{ds.name}.pkl' + output_filename = ds.get_aux_data_definition('mceq_flux_datafile')[0] output_pathfilename = '' if args.save_path is None: - output_pathfilename = os.path.join( - ds.root_dir, 'fluxes', output_filename) + output_pathfilename = ds.get_abs_pathfilename_list([output_filename])[0] else: output_pathfilename = os.path.join( args.save_path, output_filename) @@ -142,7 +141,7 @@ def create_flux_file(save_path, ds): # Save the result to the output file. with open(output_pathfilename, 'wb') as f: - pickle.dump(((mceq.e_grid, theta_angles), flux_def), f) + pickle.dump(((mceq.e_grid, theta_angles_binedges), flux_def), f) print('Saved fluxes for dataset %s to: %s'%(ds.name, output_pathfilename)) #------------------------------------------------------------------------------- From 0eedadbd2965191ccaa0083ef16549d06d907e50 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 27 Jun 2022 11:53:41 +0200 Subject: [PATCH 111/274] Add properties for the number of true_e and dec bins to smearing matrix --- skyllh/analyses/i3/trad_ps/utils.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 9a39f62ca8..6066c2ea3a 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -796,6 +796,12 @@ def __init__( self.ang_err_upper_edges ) = load_smearing_histogram(pathfilenames) + @property + def n_log10_true_e_bins(self): + """(read-only) The number of log10 true energy bins. + """ + return len(self._true_e_bin_edges) - 1 + @property def true_e_bin_edges(self): """(read-only) The (n_true_e+1,)-shaped 1D numpy ndarray holding the @@ -811,6 +817,12 @@ def true_e_bin_centers(self): return 0.5*(self._true_e_bin_edges[:-1] + self._true_e_bin_edges[1:]) + @property + def n_true_dec_bins(self): + """(read-only) The number of true declination bins. + """ + return len(self._true_dec_bin_edges) - 1 + @property def true_dec_bin_edges(self): """(read-only) The (n_true_dec+1,)-shaped 1D numpy ndarray holding the From 42f99ff303a01e69beb2fed544dc0f4fb96d2305 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 27 Jun 2022 11:54:22 +0200 Subject: [PATCH 112/274] Add file for background flux calculations --- skyllh/analyses/i3/trad_ps/bkg_flux.py | 67 ++++++++++++++++++++++++++ 1 file changed, 67 insertions(+) create mode 100644 skyllh/analyses/i3/trad_ps/bkg_flux.py diff --git a/skyllh/analyses/i3/trad_ps/bkg_flux.py b/skyllh/analyses/i3/trad_ps/bkg_flux.py new file mode 100644 index 0000000000..26608f046d --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/bkg_flux.py @@ -0,0 +1,67 @@ +# -*- coding: utf-8 -*- + +import numpy as np +import pickle + + +def get_pd_atmo_Enu_sin_dec_nu(flux_pathfilename): + """Constructs the atmospheric PDF p_atmo(E_nu|sin(dec_nu)) in unit 1/GeV. + + Parameters + ---------- + flux_pathfilename : str + The pathfilename of the file containing the MCEq flux. + + Returns + ------- + pd_atmo : (n_sin_dec, n_e_grid)-shaped 2D numpy ndarray + The numpy ndarray holding the the atmospheric energy PDF in unit 1/GeV. + sin_dec_binedges : numpy ndarray + The (n_sin_dec+1,)-shaped 1D numpy ndarray holding the sin(dec) bin + edges. + log10_e_grid_edges : numpy ndarray + The (n_e_grid+1,)-shaped 1D numpy ndarray holding the energy bin edges + in log10. + """ + with open(flux_pathfilename, 'rb') as f: + ((e_grid, zenith_angle_binedges), flux_def) = pickle.load(f) + + # Select energy bins below 10**9 GeV. + m_e_grid = e_grid <= 10**9 + e_grid = e_grid[m_e_grid] + + zenith_angles = 0.5*(zenith_angle_binedges[:-1]+ zenith_angle_binedges[1:]) + + # Calculate the e_grid bin edges in log10. + log10_e_grid_edges = np.empty((len(e_grid)+1),) + d_log10_e_grid = np.diff(np.log10(e_grid))[0] + log10_e_grid_edges[:-1] = np.log10(e_grid) - d_log10_e_grid/2 + log10_e_grid_edges[-1] = log10_e_grid_edges[-2] + d_log10_e_grid + + # Calculate the energy bin widths of the energy grid. + dE = np.diff(10**log10_e_grid_edges) + + # Convert zenith angles into sin(declination) angles. + sin_dec_binedges = np.sin(np.deg2rad(zenith_angle_binedges) - np.pi/2) + sin_dec_angles = np.sin(np.deg2rad(zenith_angles) - np.pi/2) + + n_e_grid = len(e_grid) + n_sin_dec = len(sin_dec_angles) + + # Calculate p_atmo(E_nu|sin(dec_nu)). + pd_atmo = np.zeros((n_sin_dec, n_e_grid)) + for (sin_dec_idx, sin_dec) in enumerate(sin_dec_angles): + if sin_dec < 0: + fl = flux_def['numu_total'][:,sin_dec_idx][m_e_grid] + else: + # For up-going we use the flux calculation from the streight + # downgoing. + fl = flux_def['numu_total'][:,0][m_e_grid] + pd_atmo[sin_dec_idx] = fl/np.sum(fl*dE) + + # Cross check the normalization of the PDF. + if not np.all(np.isclose(np.sum(pd_atmo*dE[np.newaxis,:], axis=1), 1)): + raise ValueError( + 'The atmospheric true energy PDF is not normalized!') + + return (pd_atmo, sin_dec_binedges, log10_e_grid_edges) From 699e7a8b7b445fe484e928603bf6bc45b8690314 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 6 Jul 2022 17:32:28 +0200 Subject: [PATCH 113/274] Add module for effective area --- skyllh/analyses/i3/trad_ps/pd_aeff.py | 366 ++++++++++++++++++++++++++ 1 file changed, 366 insertions(+) create mode 100644 skyllh/analyses/i3/trad_ps/pd_aeff.py diff --git a/skyllh/analyses/i3/trad_ps/pd_aeff.py b/skyllh/analyses/i3/trad_ps/pd_aeff.py new file mode 100644 index 0000000000..c8f412bdea --- /dev/null +++ b/skyllh/analyses/i3/trad_ps/pd_aeff.py @@ -0,0 +1,366 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +from skyllh.core.binning import get_bincenters_from_binedges +from skyllh.core.storage import create_FileLoader + + +def load_effective_area_array(pathfilenames): + """Loads the (nbins_decnu, nbins_log10enu)-shaped 2D effective + area array from the given data file. + + Parameters + ---------- + pathfilename : str | list of str + The file name of the data file. + + Returns + ------- + aeff_decnu_log10enu : (nbins_decnu, nbins_log10enu)-shaped 2D ndarray + The ndarray holding the effective area for each + (dec_nu,log10(E_nu/GeV)) bin. + decnu_binedges_lower : (nbins_decnu,)-shaped ndarray + The ndarray holding the lower bin edges of the dec_nu axis. + decnu_binedges_upper : (nbins_decnu,)-shaped ndarray + The ndarray holding the upper bin edges of the dec_nu axis. + log10_enu_binedges_lower : (nbins_log10enu,)-shaped ndarray + The ndarray holding the lower bin edges of the log10(E_nu/GeV) axis. + log10_enu_binedges_upper : (nbins_log10enu,)-shaped ndarray + The ndarray holding the upper bin edges of the log10(E_nu/GeV) axis. + """ + loader = create_FileLoader(pathfilenames=pathfilenames) + data = loader.load_data() + renaming_dict = { + 'log10(E_nu/GeV)_min': 'log10_enu_min', + 'log10(E_nu/GeV)_max': 'log10_enu_max', + 'Dec_nu_min[deg]': 'decnu_min', + 'Dec_nu_max[deg]': 'decnu_max', + 'A_Eff[cm^2]': 'a_eff' + } + data.rename_fields(renaming_dict, must_exist=True) + + # Convert the true neutrino declination from degrees to radians. + data['decnu_min'] = np.deg2rad(data['decnu_min']) + data['decnu_max'] = np.deg2rad(data['decnu_max']) + + # Determine the binning for energy and declination. + log10_enu_binedges_lower = np.unique(data['log10_enu_min']) + log10_enu_binedges_upper = np.unique(data['log10_enu_max']) + decnu_binedges_lower = np.unique(data['decnu_min']) + decnu_binedges_upper = np.unique(data['decnu_max']) + + if(len(log10_enu_binedges_lower) != len(log10_enu_binedges_upper)): + raise ValueError('Cannot extract the log10(E/GeV) binning of the ' + 'effective area from data file "{}". The number of lower and upper ' + 'bin edges is not equal!'.format(str(loader.pathfilename_list))) + if(len(decnu_binedges_lower) != len(decnu_binedges_upper)): + raise ValueError('Cannot extract the dec_nu binning of the effective ' + 'area from data file "{}". The number of lower and upper bin edges ' + 'is not equal!'.format(str(loader.pathfilename_list))) + + nbins_log10_enu = len(log10_enu_binedges_lower) + nbins_decnu = len(decnu_binedges_lower) + + # Construct the 2d array for the effective area. + aeff_decnu_log10enu = np.zeros( + (nbins_decnu, nbins_log10_enu), dtype=np.double) + + decnu_idx = np.digitize( + 0.5*(data['decnu_min'] + + data['decnu_max']), + decnu_binedges_lower) - 1 + log10enu_idx = np.digitize( + 0.5*(data['log10_enu_min'] + + data['log10_enu_max']), + log10_enu_binedges_lower) - 1 + + aeff_decnu_log10enu[decnu_idx, log10enu_idx] = data['a_eff'] + + return ( + aeff_decnu_log10enu, + decnu_binedges_lower, + decnu_binedges_upper, + log10_enu_binedges_lower, + log10_enu_binedges_upper + ) + + +class PDAeff(object): + """This class provides a representation of the effective area provided by + the public data. + """ + def __init__( + self, pathfilenames, **kwargs): + """Creates an effective area instance by loading the effective area + data from the given file. + """ + super().__init__(**kwargs) + + ( + self._aeff_decnu_log10enu, + self._decnu_binedges_lower, + self._decnu_binedges_upper, + self._log10_enu_binedges_lower, + self._log10_enu_binedges_upper + ) = load_effective_area_array(pathfilenames) + + # Note: self._aeff_decnu_log10enu is numpy 2D ndarray of shape + # (nbins_decnu, nbins_log10enu). + + # Cut the energies where all effective areas are zero. + m = np.sum(self._aeff_decnu_log10enu, axis=0) > 0 + self._aeff_decnu_log10enu = self._aeff_decnu_log10enu[:,m] + self._log10_enu_binedges_lower = self._log10_enu_binedges_lower[m] + self._log10_enu_binedges_upper = self._log10_enu_binedges_upper[m] + + self._decnu_binedges = np.concatenate( + (self._decnu_binedges_lower, + self._decnu_binedges_upper[-1:]) + ) + self._log10_enu_binedges = np.concatenate( + (self._log10_enu_binedges_lower, + self._log10_enu_binedges_upper[-1:]) + ) + + @property + def decnu_binedges(self): + """(read-only) The bin edges of the neutrino declination axis in + radians. + """ + return self._decnu_binedges + + @property + def decnu_bincenters(self): + """(read-only) The bin center values of the neutrino declination axis in + radians. + """ + return get_bincenters_from_binedges(self._decnu_binedges) + + @property + def log10_enu_binedges(self): + """(read-only) The bin edges of the log10(E_nu/GeV) neutrino energy + axis. + """ + return self._log10_enu_binedges + + @property + def log10_enu_bincenters(self): + """(read-only) The bin center values of the log10(E_nu/GeV) neutrino + energy axis. + """ + return get_bincenters_from_binedges(self._log10_enu_binedges) + + @property + def aeff_decnu_log10enu(self): + """(read-only) The effective area in cm^2 as (n_decnu,n_log10enu)-shaped + 2D numpy ndarray. + """ + return self._aeff_decnu_log10enu + + + + #def get_aeff_for_sin_true_dec(self, sin_true_dec): + #"""Retrieves the effective area as function of log_true_e. + + #Parameters + #---------- + #sin_true_dec : float + #The sin of the true declination. + + #Returns + #------- + #aeff : (n,)-shaped numpy ndarray + #The effective area for the given true declination as a function of + #log true energy. + #""" + #sin_true_dec_idx = np.digitize( + #sin_true_dec, self.sin_true_dec_binedges) - 1 + + #aeff = self.aeff_arr[sin_true_dec_idx] + + #return aeff + + #def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): + #"""Calculates the detection probability density p(E_nu|sin_dec) in + #unit GeV^-1 for the given true energy values. + + #Parameters + #---------- + #sin_true_dec : float + #The sin of the true declination. + #true_e : (n,)-shaped 1d numpy ndarray of float + #The values of the true energy in GeV for which the probability + #density value should get calculated. + + #Returns + #------- + #det_pd : (n,)-shaped 1d numpy ndarray of float + #The detection probability density values for the given true energy + #value. + #""" + #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + + #dE = np.diff(np.power(10, self.log_true_e_binedges)) + + #det_pdf = aeff / np.sum(aeff) / dE + + #x = np.power(10, self.log_true_e_bincenters) + #y = det_pdf + #tck = interpolate.splrep(x, y, k=1, s=0) + + #det_pd = interpolate.splev(true_e, tck, der=0) + + #return det_pd + + #def get_detection_pd_in_log10E_for_sin_true_dec( + #self, sin_true_dec, log10_true_e): + #"""Calculates the detection probability density p(E_nu|sin_dec) in + #unit log10(GeV)^-1 for the given true energy values. + + #Parameters + #---------- + #sin_true_dec : float + #The sin of the true declination. + #log10_true_e : (n,)-shaped 1d numpy ndarray of float + #The log10 values of the true energy in GeV for which the + #probability density value should get calculated. + + #Returns + #------- + #det_pd : (n,)-shaped 1d numpy ndarray of float + #The detection probability density values for the given true energy + #value. + #""" + #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + + #dlog10E = np.diff(self.log_true_e_binedges) + + #det_pdf = aeff / np.sum(aeff) / dlog10E + + #spl = interpolate.splrep( + #self.log_true_e_bincenters, det_pdf, k=1, s=0) + + #det_pd = interpolate.splev(log10_true_e, spl, der=0) + + #return det_pd + + #def get_detection_prob_for_sin_true_dec( + #self, sin_true_dec, true_e_min, true_e_max, + #true_e_range_min, true_e_range_max): + #"""Calculates the detection probability for a given energy range for a + #given sin declination. + + #Parameters + #---------- + #sin_true_dec : float + #The sin of the true declination. + #true_e_min : float + #The minimum energy in GeV. + #true_e_max : float + #The maximum energy in GeV. + #true_e_range_min : float + #The minimum energy in GeV of the entire energy range. + #true_e_range_max : float + #The maximum energy in GeV of the entire energy range. + + #Returns + #------- + #det_prob : float + #The true energy detection probability. + #""" + #true_e_binedges = np.power(10, self.log_true_e_binedges) + + ## Get the bin indices for the lower and upper energy range values. + #(lidx, uidx) = get_bin_indices_from_lower_and_upper_binedges( + #true_e_binedges[:-1], + #true_e_binedges[1:], + #np.array([true_e_range_min, true_e_range_max])) + ## The function determined the bin indices based on the + ## lower bin edges. So the bin index of the upper energy range value + ## is 1 to large. + #uidx -= 1 + + #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + #aeff = aeff[lidx:uidx+1] + #true_e_binedges = true_e_binedges[lidx:uidx+2] + + #dE = np.diff(true_e_binedges) + + #det_pdf = aeff / dE + + #true_e_bincenters = 0.5*(true_e_binedges[:-1] + true_e_binedges[1:]) + #tck = interpolate.splrep( + #true_e_bincenters, det_pdf, + #xb=true_e_range_min, xe=true_e_range_max, k=1, s=0) + + #def _eval_func(x): + #return interpolate.splev(x, tck, der=0) + + #norm = integrate.quad( + #_eval_func, true_e_range_min, true_e_range_max, + #limit=200, full_output=1)[0] + + #integral = integrate.quad( + #_eval_func, true_e_min, true_e_max, + #limit=200, full_output=1)[0] + + #det_prob = integral / norm + + #return det_prob + + #def get_aeff_integral_for_sin_true_dec( + #self, sin_true_dec, log_true_e_min, log_true_e_max): + #"""Calculates the integral of the effective area using the trapezoid + #method. + + #Returns + #------- + #integral : float + #The integral in unit cm^2 GeV. + #""" + #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) + + #integral = ( + #(np.power(10, log_true_e_max) - + #np.power(10, log_true_e_min)) * + #0.5 * + #(np.interp(log_true_e_min, self.log_true_e_bincenters, aeff) + + #np.interp(log_true_e_max, self.log_true_e_bincenters, aeff)) + #) + + #return integral + + #def get_aeff(self, sin_true_dec, log_true_e): + #"""Retrieves the effective area for the given sin(dec_true) and + #log(E_true) value pairs. + + #Parameters + #---------- + #sin_true_dec : (n,)-shaped 1D ndarray + #The sin(dec_true) values. + #log_true_e : (n,)-shaped 1D ndarray + #The log(E_true) values. + + #Returns + #------- + #aeff : (n,)-shaped 1D ndarray + #The 1D ndarray holding the effective area values for each value + #pair. For value pairs outside the effective area data zero is + #returned. + #""" + #valid = ( + #(sin_true_dec >= self.sin_true_dec_binedges[0]) & + #(sin_true_dec <= self.sin_true_dec_binedges[-1]) & + #(log_true_e >= self.log_true_e_binedges[0]) & + #(log_true_e <= self.log_true_e_binedges[-1]) + #) + #sin_true_dec_idxs = np.digitize( + #sin_true_dec[valid], self.sin_true_dec_binedges) - 1 + #log_true_e_idxs = np.digitize( + #log_true_e[valid], self.log_true_e_binedges) - 1 + + #aeff = np.zeros((len(valid),), dtype=np.double) + #aeff[valid] = self.aeff_arr[sin_true_dec_idxs,log_true_e_idxs] + + #return aeff From 82fc3c0c6e82a8a3102b04cb0ebe8572afeabc8c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 6 Jul 2022 17:33:39 +0200 Subject: [PATCH 114/274] commit current state of the art --- skyllh/analyses/i3/trad_ps/bkg_flux.py | 345 ++++++++++++++++++- skyllh/analyses/i3/trad_ps/utils.py | 443 +++++++------------------ 2 files changed, 459 insertions(+), 329 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/bkg_flux.py b/skyllh/analyses/i3/trad_ps/bkg_flux.py index 26608f046d..142e61749c 100644 --- a/skyllh/analyses/i3/trad_ps/bkg_flux.py +++ b/skyllh/analyses/i3/trad_ps/bkg_flux.py @@ -2,10 +2,270 @@ import numpy as np import pickle +from scipy import interpolate +from scipy import integrate +from skyllh.physics.flux import PowerLawFlux +from skyllh.core.binning import get_bincenters_from_binedges -def get_pd_atmo_Enu_sin_dec_nu(flux_pathfilename): - """Constructs the atmospheric PDF p_atmo(E_nu|sin(dec_nu)) in unit 1/GeV. + +def get_dOmega(dec_min, dec_max): + """Calculates the solid angle given two declination angles. + + Parameters + ---------- + dec_min : float | array of float + The smaller declination angle. + dec_max : float | array of float + The larger declination angle. + + Returns + ------- + solidangle : float | array of float + The solid angle corresponding to the two given declination angles. + """ + return 2*np.pi*(np.sin(dec_max) - np.sin(dec_min)) + + +def eval_spline(x, spl): + values = spl(x) + values = np.nan_to_num(values, nan=0) + return values + + +def create_spline(x, y, norm=False): + """Creates the spline representation of the x and y values. + """ + + spline = interpolate.PchipInterpolator( + x, y, extrapolate=False + ) + + if norm: + spl_norm = integrate.quad( + eval_spline, + x[0], x[-1], + args=(spline,), + limit=200, full_output=1)[0] + + return spline, spl_norm + + else: + return spline + + +def southpole_zen2dec(zen): + """Converts zenith angles at the South Pole to declination angles. + + Parameters + ---------- + zen : (n,)-shaped 1d numpy ndarray + The numpy ndarray holding the zenith angle values in radians. + + Returns + ------- + dec : (n,)-shaped 1d numpy ndarray + The numpy ndarray holding the declination angle values in radians. + """ + dec = zen - np.pi/2 + return dec + + +def get_flux_atmo_decnu_log10enu(flux_pathfilename, log10_enu_max=9): + """Constructs the atmospheric flux map function + f_atmo(log10(E_nu/GeV),dec_nu) in unit 1/(GeV cm^2 sr s). + + Parameters + ---------- + flux_pathfilename : str + The pathfilename of the file containing the MCEq fluxes. + log10_enu_max : float + The log10(E/GeV) value of the maximum neutrino energy to be considered. + + Returns + ------- + flux_atmo : (n_dec, n_e_grid)-shaped 2D numpy ndarray + The numpy ndarray holding the the atmospheric neutrino flux function in + unit 1/(GeV cm^2 sr s). + decnu_binedges : (n_decnu+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the dec_nu bin edges. + log10_enu_binedges : (n_enu+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the neutrino energy bin edges in log10. + """ + with open(flux_pathfilename, 'rb') as f: + ((e_grid, zenith_angle_binedges), flux_def) = pickle.load(f) + zenith_angle_binedges = np.deg2rad(zenith_angle_binedges) + + # Select energy bins below 10**log10_true_e_max GeV. + m_e_grid = e_grid <= 10**log10_enu_max + e_grid = e_grid[m_e_grid] + + decnu_binedges = southpole_zen2dec(zenith_angle_binedges) + decnu_angles = get_bincenters_from_binedges(decnu_binedges) + + # Calculate the neutrino energy bin edges in log10. + log10_enu_binedges = np.empty((len(e_grid)+1),) + d_log10_enu = np.diff(np.log10(e_grid))[0] + log10_enu_binedges[:-1] = np.log10(e_grid) - d_log10_enu/2 + log10_enu_binedges[-1] = log10_enu_binedges[-2] + d_log10_enu + + n_decnu = len(decnu_angles) + n_enu = len(e_grid) + + # Calculate f_atmo(E_nu,dec_nu). + f_atmo = np.zeros((n_decnu, n_enu)) + zero_zen_idx = np.digitize(0, zenith_angle_binedges) - 1 + for (decnu_idx, decnu) in enumerate(decnu_angles): + if decnu < 0: + fl = flux_def['numu_total'][:,decnu_idx][m_e_grid] + else: + # For up-going we use the flux calculation from the streight + # downgoing. + fl = flux_def['numu_total'][:,zero_zen_idx][m_e_grid] + f_atmo[decnu_idx] = fl + + return (f_atmo, decnu_binedges, log10_enu_binedges) + + +def get_flux_astro_decnu_log10enu(decnu_binedges, log10_enu_binedges): + """Constructs the astrophysical neutrino flux function + f_astro(log10(E_nu/GeV),dec_nu) in unit 1/(GeV cm^2 sr s). + + It uses the best fit from the IceCube publication [1]. + + Parameters + ---------- + decnu_binedges : (n_decnu+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the dec_nu bin edges. + log10_enu_binedges : (n_enu+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the log10 values of the neutrino energy bin + edges in GeV. + + Returns + ------- + f_astro : (n_decnu, n_log10enu)-shaped 2D numpy ndarray + The numpy ndarray holding the astrophysical flux values in unit + 1/(GeV cm^2 sr s). + + References + ---------- + [1] https://arxiv.org/pdf/2111.10299.pdf + """ + fluxmodel = PowerLawFlux(Phi0=1.44e-18, E0=100e3, gamma=2.37) + + n_decnu = len(decnu_binedges) - 1 + + enu_binedges = np.power(10, log10_enu_binedges) + enu_bincenters = get_bincenters_from_binedges(enu_binedges) + + fl = fluxmodel(enu_bincenters) + f_astro = np.tile(fl, (n_decnu, 1)) + + return f_astro + + +def convert_flux_bkg_to_pdf_bkg(f_bkg, decnu_binedges, log10_enu_binedges): + """Converts the given background flux function f_bkg into a background flux + PDF in unit 1/(log10(E/GeV) rad). + + Parameters + ---------- + f_bkg : (n_decnu, n_enu)-shaped 2D numpy ndarray + The numpy ndarray holding the background flux values in unit + 1/(GeV cm^2 s sr). + decnu_binedges : (n_decnu+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the dec_nu bin edges in radians. + log10_enu_binedges : (n_enu+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the log10 values of the neutrino energy bin + edges in GeV. + + Returns + ------- + p_bkg : (n_decnu, n_enu)-shaped 2D numpy ndarray + The numpy ndarray holding the background flux pdf values. + """ + d_decnu = np.diff(decnu_binedges) + d_log10_enu = np.diff(log10_enu_binedges) + + bin_area = d_decnu[:,np.newaxis] * d_log10_enu[np.newaxis,:] + p_bkg = f_bkg / np.sum(f_bkg*bin_area) + + # Cross-check the normalization of the PDF. + if not np.isclose(np.sum(p_bkg*bin_area), 1): + raise ValueError( + 'The background PDF is not normalized! The integral is %f!'%(np.sum(p_bkg*bin_area))) + + return p_bkg + + +def get_pd_atmo_decnu_Enu(flux_pathfilename, log10_true_e_max=9): + """Constructs the atmospheric neutrino PDF p_atmo(E_nu,dec_nu) in unit + 1/(GeV rad). + + Parameters + ---------- + flux_pathfilename : str + The pathfilename of the file containing the MCEq flux. + log10_true_e_max : float + The log10(E/GeV) value of the maximum true energy to be considered. + + Returns + ------- + pd_atmo : (n_dec, n_e_grid)-shaped 2D numpy ndarray + The numpy ndarray holding the the atmospheric neutrino PDF in unit + 1/(GeV rad). + decnu_binedges : (n_decnu+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the dec_nu bin edges. + log10_e_grid_edges : (n_e_grid+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the energy bin edges in log10. + """ + with open(flux_pathfilename, 'rb') as f: + ((e_grid, zenith_angle_binedges), flux_def) = pickle.load(f) + + # Select energy bins below 10**log10_true_e_max GeV. + m_e_grid = e_grid <= 10**log10_true_e_max + e_grid = e_grid[m_e_grid] + + zenith_angles = 0.5*(zenith_angle_binedges[:-1] + zenith_angle_binedges[1:]) + decnu_angles = np.deg2rad(zenith_angles) - np.pi/2 + + decnu_binedges = np.deg2rad(zenith_angle_binedges) - np.pi/2 + d_decnu = np.diff(decnu_binedges) + + # Calculate the e_grid bin edges in log10. + log10_e_grid_edges = np.empty((len(e_grid)+1),) + d_log10_e_grid = np.diff(np.log10(e_grid))[0] + log10_e_grid_edges[:-1] = np.log10(e_grid) - d_log10_e_grid/2 + log10_e_grid_edges[-1] = log10_e_grid_edges[-2] + d_log10_e_grid + + n_decnu = len(decnu_angles) + n_e_grid = len(e_grid) + + # Calculate p_atmo(E_nu,dec_nu). + pd_atmo = np.zeros((n_decnu, n_e_grid)) + for (decnu_idx, decnu) in enumerate(decnu_angles): + if decnu < 0: + fl = flux_def['numu_total'][:,decnu_idx][m_e_grid] + else: + # For up-going we use the flux calculation from the streight + # downgoing. + fl = flux_def['numu_total'][:,0][m_e_grid] + pd_atmo[decnu_idx] = fl + # Normalize the PDF. + bin_area = d_decnu[:,np.newaxis] * np.diff(log10_e_grid_edges)[np.newaxis,:] + pd_atmo /= np.sum(pd_atmo*bin_area) + + # Cross-check the normalization of the PDF. + if not np.isclose(np.sum(pd_atmo*bin_area), 1): + raise ValueError( + 'The atmospheric true energy PDF is not normalized! The integral is %f!'%(np.sum(pd_atmo*bin_area))) + + return (pd_atmo, decnu_binedges, log10_e_grid_edges) + + +def get_pd_atmo_E_nu_sin_dec_nu(flux_pathfilename): + """Constructs the atmospheric energy PDF p_atmo(E_nu|sin(dec_nu)) in + unit 1/GeV. Parameters ---------- @@ -30,7 +290,7 @@ def get_pd_atmo_Enu_sin_dec_nu(flux_pathfilename): m_e_grid = e_grid <= 10**9 e_grid = e_grid[m_e_grid] - zenith_angles = 0.5*(zenith_angle_binedges[:-1]+ zenith_angle_binedges[1:]) + zenith_angles = 0.5*(zenith_angle_binedges[:-1] + zenith_angle_binedges[1:]) # Calculate the e_grid bin edges in log10. log10_e_grid_edges = np.empty((len(e_grid)+1),) @@ -59,9 +319,86 @@ def get_pd_atmo_Enu_sin_dec_nu(flux_pathfilename): fl = flux_def['numu_total'][:,0][m_e_grid] pd_atmo[sin_dec_idx] = fl/np.sum(fl*dE) - # Cross check the normalization of the PDF. + # Cross-check the normalization of the PDF. if not np.all(np.isclose(np.sum(pd_atmo*dE[np.newaxis,:], axis=1), 1)): raise ValueError( 'The atmospheric true energy PDF is not normalized!') return (pd_atmo, sin_dec_binedges, log10_e_grid_edges) + + +def get_pd_astro_E_nu_sin_dec_nu(sin_dec_binedges, log10_e_grid_edges): + """Constructs the astrophysical energy PDF p_astro(E_nu|sin(dec_nu)) in + unit 1/GeV. + It uses the best fit from the IceCube publication [1]. + + Parameters + ---------- + sin_dec_binedges : (n_sin_dec+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the sin(dec) bin edges. + log10_e_grid_edges : (n_e_grid+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the log10 values of the energy bin edges in + GeV of the energy grid. + + Returns + ------- + pd_astro : (n_sin_dec, n_e_grid)-shaped 2D numpy ndarray + The numpy ndarray holding the energy probability density values + p(E_nu|sin_dec_nu) in unit 1/GeV. + + References + ---------- + [1] https://arxiv.org/pdf/2111.10299.pdf + """ + fluxmodel = PowerLawFlux(Phi0=1.44e-18, E0=100e3, gamma=2.37) + + n_sin_dec = len(sin_dec_binedges) - 1 + n_e_grid = len(log10_e_grid_edges) - 1 + + e_grid_edges = 10**log10_e_grid_edges + e_grid_bc = 0.5*(e_grid_edges[:-1] + e_grid_edges[1:]) + + dE = np.diff(e_grid_edges) + + fl = fluxmodel(e_grid_bc) + pd = fl / np.sum(fl*dE) + pd_astro = np.tile(pd, (n_sin_dec, 1)) + + # Cross-check the normalization of the PDF. + if not np.all(np.isclose(np.sum(pd_astro*dE[np.newaxis,:], axis=1), 1)): + raise ValueError( + 'The astrophysical energy PDF is not normalized!') + + return pd_astro + + +def get_pd_bkg_E_nu_sin_dec_nu(pd_atmo, pd_astro, log10_e_grid_edges): + """Constructs the total background flux probability density + p_bkg(E_nu|sin(dec_nu)) in unit 1/GeV. + + Parameters + ---------- + pd_atmo : (n_sin_dec, n_e_grid)-shaped 2D numpy ndarray + The numpy ndarray holding the probability density values + p(E_nu|sin(dec_nu)) in 1/GeV of the atmospheric flux. + pd_astro : (n_sin_dec, n_e_grid)-shaped 2D numpy ndarray + The numpy ndarray holding the probability density values + p(E_nu|sin(dec_nu)) in 1/GeV of the astrophysical flux. + log10_e_grid_edges : (n_e_grid+1,)-shaped numpy ndarray + The numpy ndarray holding the log10 values of the energy grid bin edges + in GeV. + """ + pd_bkg = pd_atmo + pd_astro + + dE = np.diff(10**log10_e_grid_edges) + + s = np.sum(pd_bkg*dE[np.newaxis,:], axis=1, keepdims=True) + pd_bkg /= s + + if not np.all(np.isclose(np.sum(pd_bkg*dE[np.newaxis,:], axis=1), 1)): + raise ValueError( + 'The background energy PDF is not normalized!') + + return pd_bkg + + diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index 6066c2ea3a..e27f24f140 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -12,90 +12,81 @@ from skyllh.core.storage import create_FileLoader -def load_effective_area_array(pathfilenames): - """Loads the (nbins_sin_true_dec, nbins_log_true_e)-shaped 2D effective - area array from the given data file. +class FctSpline2D(object): + """Class to represent a 2D function spline using the RectBivariateSpline + class from scipy. - Parameters - ---------- - pathfilename : str | list of str - The file name of the data file. + The spline is constructed in the log10 space of the function value to + ensure a smooth spline. - Returns - ------- - arr : (nbins_sin_true_dec, nbins_log_true_e)-shaped 2D ndarray - The ndarray holding the effective area for each - sin(dec_true),log(e_true) bin. - sin_true_dec_binedges_lower : (nbins_sin_true_dec,)-shaped ndarray - The ndarray holding the lower bin edges of the sin(dec_true) axis. - sin_true_dec_binedges_upper : (nbins_sin_true_dec,)-shaped ndarray - The ndarray holding the upper bin edges of the sin(dec_true) axis. - log_true_e_binedges_lower : (nbins_log_true_e,)-shaped ndarray - The ndarray holding the lower bin edges of the log(E_true) axis. - log_true_e_binedges_upper : (nbins_log_true_e,)-shaped ndarray - The ndarray holding the upper bin edges of the log(E_true) axis. + The evaluate the spline, use the ``__call__`` method. """ - loader = create_FileLoader(pathfilenames=pathfilenames) - data = loader.load_data() - renaming_dict = { - 'log10(E_nu/GeV)_min': 'log_true_e_min', - 'log10(E_nu/GeV)_max': 'log_true_e_max', - 'Dec_nu_min[deg]': 'sin_true_dec_min', - 'Dec_nu_max[deg]': 'sin_true_dec_max', - 'A_Eff[cm^2]': 'a_eff' - } - data.rename_fields(renaming_dict, must_exist=True) - - # Convert the true neutrino declination from degrees to radians and into - # sin values. - data['sin_true_dec_min'] = np.sin(np.deg2rad( - data['sin_true_dec_min'])) - data['sin_true_dec_max'] = np.sin(np.deg2rad( - data['sin_true_dec_max'])) - - # Determine the binning for energy and declination. - log_true_e_binedges_lower = np.unique( - data['log_true_e_min']) - log_true_e_binedges_upper = np.unique( - data['log_true_e_max']) - sin_true_dec_binedges_lower = np.unique( - data['sin_true_dec_min']) - sin_true_dec_binedges_upper = np.unique( - data['sin_true_dec_max']) - - if(len(log_true_e_binedges_lower) != len(log_true_e_binedges_upper)): - raise ValueError('Cannot extract the log10(E/GeV) binning of the ' - 'effective area from data file "{}". The number of lower and upper ' - 'bin edges is not equal!'.format(str(loader.pathfilename_list))) - if(len(sin_true_dec_binedges_lower) != len(sin_true_dec_binedges_upper)): - raise ValueError('Cannot extract the Dec_nu binning of the effective ' - 'area from data file "{}". The number of lower and upper bin edges ' - 'is not equal!'.format(str(loader.pathfilename_list))) - - nbins_log_true_e = len(log_true_e_binedges_lower) - nbins_sin_true_dec = len(sin_true_dec_binedges_lower) - - # Construct the 2d array for the effective area. - arr = np.zeros((nbins_sin_true_dec, nbins_log_true_e), dtype=np.double) - - sin_true_dec_idx = np.digitize( - 0.5*(data['sin_true_dec_min'] + - data['sin_true_dec_max']), - sin_true_dec_binedges_lower) - 1 - log_true_e_idx = np.digitize( - 0.5*(data['log_true_e_min'] + - data['log_true_e_max']), - log_true_e_binedges_lower) - 1 - - arr[sin_true_dec_idx, log_true_e_idx] = data['a_eff'] + def __init__(self, f, x_binedges, y_binedges, **kwargs): + """Creates a new 2D function spline. - return ( - arr, - sin_true_dec_binedges_lower, - sin_true_dec_binedges_upper, - log_true_e_binedges_lower, - log_true_e_binedges_upper - ) + Parameters + ---------- + f : (n_x, n_y)-shaped 2D numpy ndarray + he numpy ndarray holding the function values at the bin centers. + x_binedges : (n_x+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the bin edges of the x-axis. + y_binedges : (n_y+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the bin edges of the y-axis. + """ + super().__init__(**kwargs) + + self.x_binedges = np.copy(x_binedges) + self.y_binedges = np.copy(y_binedges) + + self.x_min = self.x_binedges[0] + self.x_max = self.x_binedges[-1] + self.y_min = self.y_binedges[0] + self.y_max = self.y_binedges[-1] + + x = get_bincenters_from_binedges(x_binedges) + y = get_bincenters_from_binedges(y_binedges) + + # Note: For simplicity we approximate zero bins with 1000x smaller + # values than the minimum value. To do this correctly, one should store + # the zero bins and return zero when those bins are requested. + z = np.empty(f.shape, dtype=np.double) + m = f > 0 + z[m] = np.log10(f[m]) + z[np.invert(m)] = np.min(z[m]) - 3 + + self.spl_log10_f = interpolate.RectBivariateSpline( + x, y, z, kx=3, ky=3, s=0) + + def __call__(self, x, y, oor_value=0): + """Evaluates the spline at the given coordinates. For coordinates + outside the spline's range, the oor_value is returned. + + Parameters + ---------- + x : (n_x,)-shaped 1D numpy ndarray + The numpy ndarray holding the x values at which the spline should + get evaluated. + y : (n_y,)-shaped 1D numpy ndarray + The numpy ndarray holding the y values at which the spline should + get evaluated. + oor_value : float + The value for out-of-range (oor) coordinates. + + Returns + ------- + f : (n_x, n_y)-shaped 2D numpy ndarray + The numpy ndarray holding the evaluated values of the spline. + """ + m_x_oor = (x < self.x_min) | (x > self.x_max) + m_y_oor = (y < self.y_min) | (y > self.y_max) + + (m_xx_oor, m_yy_oor) = np.meshgrid(m_x_oor, m_y_oor, indexing='ij') + m_xy_oor = m_xx_oor | m_yy_oor + + f = np.power(10, self.spl_log10_f(x, y)) + f[m_xy_oor] = oor_value + + return f def load_smearing_histogram(pathfilenames): @@ -167,7 +158,7 @@ def _get_nbins_from_edges(lower_edges, upper_edges): """ n = 0 # Select only valid rows. - mask = upper_edges - lower_edges > 0 + mask = (upper_edges - lower_edges) > 0 data = lower_edges[mask] # Go through the valid rows and search for the number of increasing # bin edge values. @@ -529,250 +520,6 @@ def merge_reco_energy_bins(arr, log10_reco_e_binedges, bw_th, max_bw=0.2): return (arr, log10_reco_e_binedges) -class PublicDataAeff(object): - """This class is a helper class for dealing with the effective area - provided by the public data. - """ - def __init__( - self, pathfilenames, **kwargs): - """Creates an effective area instance by loading the effective area - data from the given file. - """ - super().__init__(**kwargs) - - ( - self.aeff_arr, - self.sin_true_dec_binedges_lower, - self.sin_true_dec_binedges_upper, - self.log_true_e_binedges_lower, - self.log_true_e_binedges_upper - ) = load_effective_area_array(pathfilenames) - - self.sin_true_dec_binedges = np.concatenate( - (self.sin_true_dec_binedges_lower, - self.sin_true_dec_binedges_upper[-1:]) - ) - self.log_true_e_binedges = np.concatenate( - (self.log_true_e_binedges_lower, - self.log_true_e_binedges_upper[-1:]) - ) - - @property - def log_true_e_bincenters(self): - """The bin center values of the log true energy axis. - """ - bincenters = 0.5 * ( - self.log_true_e_binedges[:-1] + self.log_true_e_binedges[1:] - ) - - return bincenters - - def get_aeff_for_sin_true_dec(self, sin_true_dec): - """Retrieves the effective area as function of log_true_e. - - Parameters - ---------- - sin_true_dec : float - The sin of the true declination. - - Returns - ------- - aeff : (n,)-shaped numpy ndarray - The effective area for the given true declination as a function of - log true energy. - """ - sin_true_dec_idx = np.digitize( - sin_true_dec, self.sin_true_dec_binedges) - 1 - - aeff = self.aeff_arr[sin_true_dec_idx] - - return aeff - - def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): - """Calculates the detection probability density p(E_nu|sin_dec) in - unit GeV^-1 for the given true energy values. - - Parameters - ---------- - sin_true_dec : float - The sin of the true declination. - true_e : (n,)-shaped 1d numpy ndarray of float - The values of the true energy in GeV for which the probability - density value should get calculated. - - Returns - ------- - det_pd : (n,)-shaped 1d numpy ndarray of float - The detection probability density values for the given true energy - value. - """ - aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - - dE = np.diff(np.power(10, self.log_true_e_binedges)) - - det_pdf = aeff / np.sum(aeff) / dE - - x = np.power(10, self.log_true_e_bincenters) - y = det_pdf - tck = interpolate.splrep(x, y, k=1, s=0) - - det_pd = interpolate.splev(true_e, tck, der=0) - - return det_pd - - def get_detection_pd_in_log10E_for_sin_true_dec( - self, sin_true_dec, log10_true_e): - """Calculates the detection probability density p(E_nu|sin_dec) in - unit log10(GeV)^-1 for the given true energy values. - - Parameters - ---------- - sin_true_dec : float - The sin of the true declination. - log10_true_e : (n,)-shaped 1d numpy ndarray of float - The log10 values of the true energy in GeV for which the - probability density value should get calculated. - - Returns - ------- - det_pd : (n,)-shaped 1d numpy ndarray of float - The detection probability density values for the given true energy - value. - """ - aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - - dlog10E = np.diff(self.log_true_e_binedges) - - det_pdf = aeff / np.sum(aeff) / dlog10E - - spl = interpolate.splrep( - self.log_true_e_bincenters, det_pdf, k=1, s=0) - - det_pd = interpolate.splev(log10_true_e, spl, der=0) - - return det_pd - - def get_detection_prob_for_sin_true_dec( - self, sin_true_dec, true_e_min, true_e_max, - true_e_range_min, true_e_range_max): - """Calculates the detection probability for a given energy range for a - given sin declination. - - Parameters - ---------- - sin_true_dec : float - The sin of the true declination. - true_e_min : float - The minimum energy in GeV. - true_e_max : float - The maximum energy in GeV. - true_e_range_min : float - The minimum energy in GeV of the entire energy range. - true_e_range_max : float - The maximum energy in GeV of the entire energy range. - - Returns - ------- - det_prob : float - The true energy detection probability. - """ - true_e_binedges = np.power(10, self.log_true_e_binedges) - - # Get the bin indices for the lower and upper energy range values. - (lidx, uidx) = get_bin_indices_from_lower_and_upper_binedges( - true_e_binedges[:-1], - true_e_binedges[1:], - np.array([true_e_range_min, true_e_range_max])) - # The function determined the bin indices based on the - # lower bin edges. So the bin index of the upper energy range value - # is 1 to large. - uidx -= 1 - - aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - aeff = aeff[lidx:uidx+1] - true_e_binedges = true_e_binedges[lidx:uidx+2] - - dE = np.diff(true_e_binedges) - - det_pdf = aeff / dE - - true_e_bincenters = 0.5*(true_e_binedges[:-1] + true_e_binedges[1:]) - tck = interpolate.splrep( - true_e_bincenters, det_pdf, - xb=true_e_range_min, xe=true_e_range_max, k=1, s=0) - - def _eval_func(x): - return interpolate.splev(x, tck, der=0) - - norm = integrate.quad( - _eval_func, true_e_range_min, true_e_range_max, - limit=200, full_output=1)[0] - - integral = integrate.quad( - _eval_func, true_e_min, true_e_max, - limit=200, full_output=1)[0] - - det_prob = integral / norm - - return det_prob - - def get_aeff_integral_for_sin_true_dec( - self, sin_true_dec, log_true_e_min, log_true_e_max): - """Calculates the integral of the effective area using the trapezoid - method. - - Returns - ------- - integral : float - The integral in unit cm^2 GeV. - """ - aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - - integral = ( - (np.power(10, log_true_e_max) - - np.power(10, log_true_e_min)) * - 0.5 * - (np.interp(log_true_e_min, self.log_true_e_bincenters, aeff) + - np.interp(log_true_e_max, self.log_true_e_bincenters, aeff)) - ) - - return integral - - def get_aeff(self, sin_true_dec, log_true_e): - """Retrieves the effective area for the given sin(dec_true) and - log(E_true) value pairs. - - Parameters - ---------- - sin_true_dec : (n,)-shaped 1D ndarray - The sin(dec_true) values. - log_true_e : (n,)-shaped 1D ndarray - The log(E_true) values. - - Returns - ------- - aeff : (n,)-shaped 1D ndarray - The 1D ndarray holding the effective area values for each value - pair. For value pairs outside the effective area data zero is - returned. - """ - valid = ( - (sin_true_dec >= self.sin_true_dec_binedges[0]) & - (sin_true_dec <= self.sin_true_dec_binedges[-1]) & - (log_true_e >= self.log_true_e_binedges[0]) & - (log_true_e <= self.log_true_e_binedges[-1]) - ) - sin_true_dec_idxs = np.digitize( - sin_true_dec[valid], self.sin_true_dec_binedges) - 1 - log_true_e_idxs = np.digitize( - log_true_e[valid], self.log_true_e_binedges) - 1 - - aeff = np.zeros((len(valid),), dtype=np.double) - aeff[valid] = self.aeff_arr[sin_true_dec_idxs,log_true_e_idxs] - - return aeff - - class PublicDataSmearingMatrix(object): """This class is a helper class for dealing with the smearing matrix provided by the public data. @@ -796,6 +543,20 @@ def __init__( self.ang_err_upper_edges ) = load_smearing_histogram(pathfilenames) + # Create bin edges array for log10_reco_e. + s = np.array(self.reco_e_lower_edges.shape) + s[-1] += 1 + self.log10_reco_e_binedges = np.empty(s, dtype=np.double) + self.log10_reco_e_binedges[:,:,:-1] = self.reco_e_lower_edges + self.log10_reco_e_binedges[:,:,-1] = self.reco_e_upper_edges[:,:,-1] + + # Create bin edges array for psi. + s = np.array(self.psi_lower_edges.shape) + s[-1] += 1 + self.psi_binedges = np.empty(s, dtype=np.double) + self.psi_binedges[:,:,:,:-1] = self.psi_lower_edges + self.psi_binedges[:,:,:,-1] = self.psi_upper_edges[:,:,:,-1] + @property def n_log10_true_e_bins(self): """(read-only) The number of log10 true energy bins. @@ -838,6 +599,38 @@ def true_dec_bin_centers(self): return 0.5*(self._true_dec_bin_edges[:-1] + self._true_dec_bin_edges[1:]) + @property + def min_log10_reco_e(self): + """(read-only) The minimum value of the reconstructed energy axis. + """ + # Select only valid reco energy bins with bin widths greater than zero. + m = (self.reco_e_upper_edges - self.reco_e_lower_edges) > 0 + return np.min(self.reco_e_lower_edges[m]) + + @property + def max_log10_reco_e(self): + """(read-only) The maximum value of the reconstructed energy axis. + """ + # Select only valid reco energy bins with bin widths greater than zero. + m = (self.reco_e_upper_edges - self.reco_e_lower_edges) > 0 + return np.max(self.reco_e_upper_edges[m]) + + @property + def min_log10_psi(self): + """(read-only) The minimum log10 value of the psi axis. + """ + # Select only valid psi bins with bin widths greater than zero. + m = (self.psi_upper_edges - self.psi_lower_edges) > 0 + return np.min(np.log10(self.psi_lower_edges[m])) + + @property + def max_log10_psi(self): + """(read-only) The maximum log10 value of the psi axis. + """ + # Select only valid psi bins with bin widths greater than zero. + m = (self.psi_upper_edges - self.psi_lower_edges) > 0 + return np.max(np.log10(self.psi_upper_edges[m])) + @property def pdf(self): """(read-only) The probability-density-function From 314a5e2eb665205ca494748ffc35e4a36828265f Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 8 Jul 2022 13:06:38 +0200 Subject: [PATCH 115/274] Be inclusive --- skyllh/core/binning.py | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index 016d6e37de..b2564ad4e7 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -158,6 +158,31 @@ def get_bincenters_from_binedges(edges): """ return 0.5*(edges[:-1] + edges[1:]) +def get_binedges_from_bincenters(centers): + """Calculates the bin edges from the given bin center values. The bin center + values must be evenly spaced. + + Parameters + ---------- + centers : 1D numpy ndarray + The (n,)-shaped 1D ndarray holding the bin center values. + + Returns + ------- + edges : 1D numpy ndarray + The (n+1,)-shaped 1D ndarray holding the bin edge values. + """ + d = np.diff(centers) + if not np.all(np.isclose(np.diff(d), 0)): + raise ValueError('The bin center values are not evenly spaced!') + d = d[0] + print(d) + + edges = np.zeros((len(centers)+1,), dtype=np.double) + edges[:-1] = centers - d/2 + edges[-1] = centers[-1] + d/2 + + return edges def get_bin_indices_from_lower_and_upper_binedges(le, ue, values): """Returns the bin indices for the given lower and upper bin edges the given @@ -190,7 +215,7 @@ def get_bin_indices_from_lower_and_upper_binedges(le, ue, values): m = ( (values[:,np.newaxis] >= le[np.newaxis,:]) & - (values[:,np.newaxis] < ue[np.newaxis,:]) + (values[:,np.newaxis] <= ue[np.newaxis,:]) ) idxs = np.nonzero(m)[1] From 3758192518a82c54b664e588450e03595d945d64 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 8 Jul 2022 13:07:15 +0200 Subject: [PATCH 116/274] Fix doc strings --- skyllh/core/signalpdf.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/skyllh/core/signalpdf.py b/skyllh/core/signalpdf.py index fa4f8e050e..6e989d404f 100644 --- a/skyllh/core/signalpdf.py +++ b/skyllh/core/signalpdf.py @@ -230,7 +230,7 @@ def __init__(self, ra_range=None, dec_range=None, **kwargs): def get_prob(self, tdm, fitparams=None, tl=None): """Calculates the spatial signal probability density of each event for - all defined sources. + the defined source. Parameters ---------- @@ -254,7 +254,7 @@ def get_prob(self, tdm, fitparams=None, tl=None): Returns ------- pd : (n_events,)-shaped 1D numpy ndarray - The probability density values for each event. + The probability density value for each event in unit 1/rad. grads : (0,)-shaped 1D numpy ndarray Since this PDF does not depend on fit parameters, an empty array is returned. From 8584a2923a1ad6a1a2d15298df3c1044057d1d26 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 8 Jul 2022 13:08:30 +0200 Subject: [PATCH 117/274] Some code restructuring --- skyllh/analyses/i3/trad_ps/pd_aeff.py | 150 +++++++-------- skyllh/analyses/i3/trad_ps/signalpdf.py | 233 ++++++++++++------------ skyllh/analyses/i3/trad_ps/utils.py | 96 +++++++++- 3 files changed, 288 insertions(+), 191 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pd_aeff.py b/skyllh/analyses/i3/trad_ps/pd_aeff.py index c8f412bdea..afd2ea4b11 100644 --- a/skyllh/analyses/i3/trad_ps/pd_aeff.py +++ b/skyllh/analyses/i3/trad_ps/pd_aeff.py @@ -2,7 +2,13 @@ import numpy as np -from skyllh.core.binning import get_bincenters_from_binedges +from scipy import interpolate +from scipy import integrate + +from skyllh.core.binning import ( + get_bincenters_from_binedges, + get_bin_indices_from_lower_and_upper_binedges, +) from skyllh.core.storage import create_FileLoader @@ -158,28 +164,25 @@ def aeff_decnu_log10enu(self): """ return self._aeff_decnu_log10enu + def get_aeff_for_decnu(self, decnu): + """Retrieves the effective area as function of log10_enu. + Parameters + ---------- + decnu : float + The true neutrino declination. - #def get_aeff_for_sin_true_dec(self, sin_true_dec): - #"""Retrieves the effective area as function of log_true_e. - - #Parameters - #---------- - #sin_true_dec : float - #The sin of the true declination. - - #Returns - #------- - #aeff : (n,)-shaped numpy ndarray - #The effective area for the given true declination as a function of - #log true energy. - #""" - #sin_true_dec_idx = np.digitize( - #sin_true_dec, self.sin_true_dec_binedges) - 1 + Returns + ------- + aeff : (n,)-shaped numpy ndarray + The effective area in cm^2 for the given true neutrino declination + as a function of log10 true neutrino energy. + """ + decnu_idx = np.digitize(decnu, self._decnu_binedges) - 1 - #aeff = self.aeff_arr[sin_true_dec_idx] + aeff = self._aeff_decnu_log10enu[decnu_idx] - #return aeff + return aeff #def get_detection_pd_for_sin_true_dec(self, sin_true_dec, true_e): #"""Calculates the detection probability density p(E_nu|sin_dec) in @@ -245,69 +248,72 @@ def aeff_decnu_log10enu(self): #return det_pd - #def get_detection_prob_for_sin_true_dec( - #self, sin_true_dec, true_e_min, true_e_max, - #true_e_range_min, true_e_range_max): - #"""Calculates the detection probability for a given energy range for a - #given sin declination. - - #Parameters - #---------- - #sin_true_dec : float - #The sin of the true declination. - #true_e_min : float - #The minimum energy in GeV. - #true_e_max : float - #The maximum energy in GeV. - #true_e_range_min : float - #The minimum energy in GeV of the entire energy range. - #true_e_range_max : float - #The maximum energy in GeV of the entire energy range. + def get_detection_prob_for_decnu( + self, decnu, enu_min, enu_max, enu_range_min, enu_range_max): + """Calculates the detection probability for a given true neutrino energy + range for a given neutrino declination. + + Parameters + ---------- + decnu : float + The neutrino declination in radians. + enu_min : float + The minimum energy in GeV. + enu_max : float + The maximum energy in GeV. + enu_range_min : float + The minimum energy in GeV of the entire energy range. + enu_range_max : float + The maximum energy in GeV of the entire energy range. + + Returns + ------- + det_prob : float + The neutrino energy detection probability. + """ + enu_binedges = np.power(10, self.log10_enu_binedges) - #Returns - #------- - #det_prob : float - #The true energy detection probability. - #""" - #true_e_binedges = np.power(10, self.log_true_e_binedges) - - ## Get the bin indices for the lower and upper energy range values. - #(lidx, uidx) = get_bin_indices_from_lower_and_upper_binedges( - #true_e_binedges[:-1], - #true_e_binedges[1:], - #np.array([true_e_range_min, true_e_range_max])) - ## The function determined the bin indices based on the - ## lower bin edges. So the bin index of the upper energy range value - ## is 1 to large. - #uidx -= 1 + # Get the bin indices for the lower and upper energy range values. + (lidx, uidx) = get_bin_indices_from_lower_and_upper_binedges( + enu_binedges[:-1], + enu_binedges[1:], + np.array([enu_range_min, enu_range_max])) - #aeff = self.get_aeff_for_sin_true_dec(sin_true_dec) - #aeff = aeff[lidx:uidx+1] - #true_e_binedges = true_e_binedges[lidx:uidx+2] + aeff = self.get_aeff_for_decnu(decnu) + aeff = aeff[lidx:uidx+1] + enu_binedges = enu_binedges[lidx:uidx+2] - #dE = np.diff(true_e_binedges) + dE = np.diff(enu_binedges) - #det_pdf = aeff / dE + daeff_dE = aeff / dE - #true_e_bincenters = 0.5*(true_e_binedges[:-1] + true_e_binedges[1:]) - #tck = interpolate.splrep( - #true_e_bincenters, det_pdf, - #xb=true_e_range_min, xe=true_e_range_max, k=1, s=0) + enu_bincenters = get_bincenters_from_binedges(enu_binedges) + tck = interpolate.splrep( + enu_bincenters, daeff_dE, + xb=enu_range_min, xe=enu_range_max, k=1, s=0) - #def _eval_func(x): - #return interpolate.splev(x, tck, der=0) + def _eval_func(x): + return interpolate.splev(x, tck, der=0) - #norm = integrate.quad( - #_eval_func, true_e_range_min, true_e_range_max, - #limit=200, full_output=1)[0] + norm = integrate.quad( + _eval_func, + enu_range_min, + enu_range_max, + limit=200, + full_output=1 + )[0] - #integral = integrate.quad( - #_eval_func, true_e_min, true_e_max, - #limit=200, full_output=1)[0] + integral = integrate.quad( + _eval_func, + enu_min, + enu_max, + limit=200, + full_output=1 + )[0] - #det_prob = integral / norm + det_prob = integral / norm - #return det_prob + return det_prob #def get_aeff_integral_for_sin_true_dec( #self, sin_true_dec, log_true_e_min, log_true_e_max): diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 6d13b55b5d..7767f396ca 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -40,16 +40,21 @@ from skyllh.i3.pdf import I3EnergyPDF from skyllh.i3.dataset import I3Dataset from skyllh.physics.flux import FluxModel + +from skyllh.analyses.i3.trad_ps.pd_aeff import ( + PDAeff, +) from skyllh.analyses.i3.trad_ps.utils import ( + FctSpline1D, create_unionized_smearing_matrix_array, load_smearing_histogram, psi_to_dec_and_ra, - PublicDataAeff, PublicDataSmearingMatrix, merge_reco_energy_bins ) + class PublicDataSignalGenerator(object): def __init__(self, ds, **kwargs): """Creates a new instance of the signal generator for generating signal @@ -624,7 +629,7 @@ def get_prob(self, tdm, gridfitparams): return prob -class PDSignalEnergyPDF(PDF, IsSignalPDF): +class PDSignalEnergyPDF_old(PDF, IsSignalPDF): """This class provides a signal energy PDF for a spectrial index value. """ @@ -767,7 +772,7 @@ def get_prob(self, tdm, params=None, tl=None): return (pd, None) -class PDSignalEnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): +class PDSignalEnergyPDFSet_old(PDFSet, IsSignalPDF, IsParallelizable): """This class provides a signal energy PDF set for the public data. It creates a set of PDSignalEnergyPDF instances, one for each spectral index value on a grid. @@ -1080,88 +1085,106 @@ def get_prob(self, tdm, gridfitparams, tl=None): return (prob, grads) -def eval_spline(x, spl): - values = spl(x) - values = np.nan_to_num(values, nan=0) - return values +#def eval_spline(x, spl): + #values = spl(x) + #values = np.nan_to_num(values, nan=0) + #return values -def create_spline(log10_e_bincenters, f_e, norm=False): - """Creates the spline representation of the energy PDF. - """ +#def create_spline(log10_e_bincenters, f_e, norm=False): + #"""Creates the spline representation of the energy PDF. + #""" - spline = interpolate.PchipInterpolator( - log10_e_bincenters, f_e, extrapolate=False - ) + #spline = interpolate.PchipInterpolator( + #log10_e_bincenters, f_e, extrapolate=False + #) - if norm: - spl_norm = integrate.quad( - eval_spline, - log10_e_bincenters[0], log10_e_bincenters[-1], - args=(spline,), - limit=200, full_output=1)[0] + #if norm: + #spl_norm = integrate.quad( + #eval_spline, + #log10_e_bincenters[0], log10_e_bincenters[-1], + #args=(spline,), + #limit=200, full_output=1)[0] - return spline, spl_norm + #return spline, spl_norm - else: - return spline + #else: + #return spline class PDSignalEnergyPDF(PDF, IsSignalPDF): """This class provides a signal energy PDF for a spectrial index value. """ - def __init__( - self, f_e, norm, log_e_edges, **kwargs): + self, f_e_spl, **kwargs): """Creates a new signal energy PDF instance for a particular spectral index value. + + Parameters + ---------- + f_e_spl : FctSpline1D instance + The FctSpline1D instance representing the spline of the energy PDF. """ super().__init__(**kwargs) - self.f_e = f_e - self.norm = norm + if not isinstance(f_e_spl, FctSpline1D): + raise TypeError( + 'The f_e_spl argument must be an instance of FctSpline1D!') - self.log_e_lower_edges = log_e_edges[:-1] - self.log_e_upper_edges = log_e_edges[1:] + self.f_e_spl = f_e_spl + + self.log10_reco_e_lower_binedges = self.f_e_spl.x_binedges[:-1] + self.log10_reco_e_upper_binedges = self.f_e_spl.x_binedges[1:] + + self.log10_reco_e_min = self.log10_reco_e_lower_binedges[0] + self.log10_reco_e_max = self.log10_reco_e_upper_binedges[-1] # Add the PDF axes. self.add_axis(PDFAxis( name='log_energy', - vmin=self.log_e_lower_edges[0], - vmax=self.log_e_upper_edges[-1]) + vmin=self.log10_reco_e_min, + vmax=self.log10_reco_e_max) ) # Check integrity. integral = integrate.quad( - eval_spline, - self.log_e_lower_edges[0], - self.log_e_upper_edges[-1], - args=(self.f_e,), - limit=200, full_output=1)[0] / self.norm + self.f_e_spl.evaluate, + self.log10_reco_e_min, + self.log10_reco_e_max, + limit=200, + full_output=1 + )[0] / self.f_e_spl.norm if not np.isclose(integral, 1): raise ValueError( - 'The integral over log10_E of the energy term must be unity! ' - 'But it is {}!'.format(integral)) + 'The integral over log10_reco_e of the energy term must be ' + 'unity! But it is {}!'.format(integral)) def assert_is_valid_for_trial_data(self, tdm): pass - def get_pd_by_log10_e(self, log10_e, tl=None): - """Calculates the probability density for the given log10(E/GeV) + def get_pd_by_log10_reco_e(self, log10_reco_e, tl=None): + """Calculates the probability density for the given log10(E_reco/GeV) values using the spline representation of the PDF. - + Parameters + ---------- + log10_reco_e : (n_log10_reco_e,)-shaped 1D numpy ndarray + The numpy ndarray holding the log10(E_reco/GeV) values for which + the energy PDF should get evaluated. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. """ # Select events that actually have a signal enegry PDF. - # All other events will get zero signal probability. + # All other events will get zero signal probability density. m = ( - (log10_e >= self.log_e_lower_edges[0]) & - (log10_e < self.log_e_upper_edges[-1]) + (log10_reco_e >= self.log10_reco_e_min) & + (log10_reco_e < self.log10_reco_e_max) ) - pd = np.zeros((len(log10_e),), dtype=np.double) - - pd[m] = eval_spline(log10_e[m], self.f_e) / self.norm + with TaskTimer(tl, 'Evaluate PDSignalEnergyPDF'): + pd = np.zeros((len(log10_reco_e),), dtype=np.double) + pd[m] = self.f_e_spl(log10_reco_e[m]) / self.f_e_spl.norm return pd @@ -1177,10 +1200,6 @@ def get_prob(self, tdm, params=None, tl=None): required: - 'log_energy' The log10 of the reconstructed energy. - - 'psi' - The opening angle from the source to the event in radians. - - 'ang_err' - The angular error of the event in radians. params : dict | None The dictionary containing the parameter names and values for which the probability should get calculated. @@ -1200,9 +1219,9 @@ def get_prob(self, tdm, params=None, tl=None): the ``param_set`` property. It is ``None``, if this PDF does not depend on any parameters. """ - log10_e = tdm.get_data('log_energy') + log10_reco_e = tdm.get_data('log_energy') - pd = self.get_pd_by_log10_e(log10_e, tl=tl) + pd = self.get_pd_by_log10_reco_e(log10_reco_e, tl=tl) return (pd, None) @@ -1212,7 +1231,6 @@ class PDSignalEnergyPDFSet(PDFSet, IsSignalPDF, IsParallelizable): It creates a set of PDSignalEnergyPDF instances, one for each spectral index value on a grid. """ - def __init__( self, ds, @@ -1227,7 +1245,7 @@ def __init__( Parameters ---------- ds : I3Dataset instance - The I3Dataset instance that defines the public data dataset. + The I3Dataset instance that defines the dataset of the public data. src_dec : float The declination of the source in radians. flux_model : FluxModel instance @@ -1273,51 +1291,52 @@ def __init__( # Note that we take the pdfs of the reconstruction calculated # from the smearing matrix here. true_dec_idx = sm.get_true_dec_idx(src_dec) - sm_histo = sm.pdf[:, true_dec_idx] + sm_pdf = sm.pdf[:, true_dec_idx] - true_e_binedges = np.power(10, sm.true_e_bin_edges) - nbins_true_e = len(true_e_binedges) - 1 - E_nu_min = true_e_binedges[:-1] - E_nu_max = true_e_binedges[1:] + true_enu_binedges = np.power(10, sm.log10_true_enu_binedges) + true_enu_binedges_lower = true_enu_binedges[:-1] + true_enu_binedges_upper = true_enu_binedges[1:] + nbins_true_e = len(true_enu_binedges) - 1 # Define the values at which to evaluate the splines. # Some bins might have zero bin widths. - m = (sm.reco_e_upper_edges[:, true_dec_idx] - - sm.reco_e_lower_edges[:, true_dec_idx]) > 0 - le = sm.reco_e_lower_edges[:, true_dec_idx][m].flatten() - ue = sm.reco_e_upper_edges[:, true_dec_idx][m].flatten() + m = (sm.log10_reco_e_binedges_upper[:, true_dec_idx] - + sm.log10_reco_e_binedges_lower[:, true_dec_idx]) > 0 + le = sm.log10_reco_e_binedges_lower[:, true_dec_idx][m] + ue = sm.log10_reco_e_binedges_upper[:, true_dec_idx][m] min_log10_reco_e = np.min(le) max_log10_reco_e = np.max(ue) d_log10_reco_e = np.min(ue - le) / 20 n_xvals = int((max_log10_reco_e - min_log10_reco_e) / d_log10_reco_e) - xvals = np.linspace( + xvals_binedges = np.linspace( min_log10_reco_e, max_log10_reco_e, - n_xvals + n_xvals+1 ) + xvals = get_bincenters_from_binedges(xvals_binedges) # Calculate the neutrino enegry bin widths in GeV. - dE_nu = np.diff(true_e_binedges) + d_enu = np.diff(true_enu_binedges) self._logger.debug( - 'dE_nu = {}'.format(dE_nu) + 'dE_nu = {}'.format(d_enu) ) # Load the effective area. - aeff = PublicDataAeff( + aeff = PDAeff( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) # Calculate the detector's neutrino energy detection probability to # detect a neutrino of energy E_nu given a neutrino declination: # p(E_nu|dec) - det_prob = np.empty((len(dE_nu),), dtype=np.double) - for i in range(len(dE_nu)): - det_prob[i] = aeff.get_detection_prob_for_sin_true_dec( - sin_true_dec=np.sin(src_dec), - true_e_min=true_e_binedges[i], - true_e_max=true_e_binedges[i+1], - true_e_range_min=true_e_binedges[0], - true_e_range_max=true_e_binedges[-1] + det_prob = np.empty((len(d_enu),), dtype=np.double) + for i in range(len(d_enu)): + det_prob[i] = aeff.get_detection_prob_for_decnu( + decnu=src_dec, + enu_min=true_enu_binedges[i], + enu_max=true_enu_binedges[i+1], + enu_range_min=true_enu_binedges[0], + enu_range_max=true_enu_binedges[-1] ) self._logger.debug('det_prob = {}, sum = {}'.format( @@ -1328,11 +1347,11 @@ def __init__( 'The sum of the detection probabilities is not unity! It is ' '{}.'.format(np.sum(det_prob))) - psi_edges_bw = sm.psi_upper_edges-sm.psi_lower_edges - ang_err_bw = sm.ang_err_upper_edges-sm.ang_err_lower_edges + psi_edges_bw = sm.psi_upper_edges - sm.psi_lower_edges + ang_err_bw = sm.ang_err_upper_edges - sm.ang_err_lower_edges # Create the energy pdf for different gamma values. - def create_energy_pdf(sm_histo, flux_model, gridfitparams): + def create_energy_pdf(sm_pdf, flux_model, gridfitparams): """Creates an energy pdf for a specific gamma value. """ # Create a copy of the FluxModel with the given flux parameters. @@ -1347,10 +1366,13 @@ def create_energy_pdf(sm_histo, flux_model, gridfitparams): # Calculate the flux probability p(E_nu|gamma). flux_prob = ( - my_flux_model.get_integral(E_nu_min, E_nu_max) / my_flux_model.get_integral( - true_e_binedges[0], - true_e_binedges[-1] + true_enu_binedges_lower, + true_enu_binedges_upper + ) / + my_flux_model.get_integral( + true_enu_binedges[0], + true_enu_binedges[-1] ) ) if not np.isclose(np.sum(flux_prob), 1): @@ -1371,66 +1393,45 @@ def create_energy_pdf(sm_histo, flux_model, gridfitparams): 'true_e_prob = {}'.format( true_e_prob)) - def create_e_pdf_for_true_e(true_e_idx): + def create_reco_e_pdf_for_true_e(true_e_idx): """This functions creates a spline for the reco energy distribution given a true neutrino engery. """ # Create the enegry PDF f_e = P(log10_E_reco|dec) = # \int dPsi dang_err P(E_reco,Psi,ang_err). f_e = np.sum( - sm_histo[true_e_idx] * + sm_pdf[true_e_idx] * psi_edges_bw[true_e_idx, true_dec_idx, :, :, np.newaxis] * ang_err_bw[true_e_idx, true_dec_idx, :, :, :], axis=(-1, -2) ) - # Now build the spline to then use it in the sum over the true + # Now build the spline to use it in the sum over the true # neutrino energy. At this point, add the weight of the pdf # with the true neutrino energy probability. - log10_e_bincenters = 0.5*( - sm.reco_e_lower_edges[true_e_idx, true_dec_idx] + - sm.reco_e_upper_edges[true_e_idx, true_dec_idx] - ) - if np.all(log10_e_bincenters == 0): - return np.zeros_like(xvals) + log10_reco_e_binedges = sm.log10_reco_e_binedges[ + true_e_idx, true_dec_idx] p = f_e * true_e_prob[true_e_idx] - # Create the spline from the lowest and highest bin edge in - # reconstructed enegry. - x = np.concatenate(( - np.array( - [sm.reco_e_lower_edges[true_e_idx, true_dec_idx][0]]), - log10_e_bincenters, - np.array( - [sm.reco_e_upper_edges[true_e_idx, true_dec_idx][-1]]) - )) - y = np.concatenate(( - np.array( - [p[0]]), - p, - np.array( - [p[-1]]) - )) - - spline = create_spline(x, y) - - return eval_spline(xvals, spline) + spline = FctSpline1D(p, log10_reco_e_binedges) + + return spline(xvals) # Integrate over the true neutrino energy and spline the output. sum_pdf = np.sum([ - create_e_pdf_for_true_e(true_e_idx) + create_reco_e_pdf_for_true_e(true_e_idx) for true_e_idx in range(nbins_true_e) ], axis=0) - spline, norm = create_spline(xvals, sum_pdf, norm=True) + spline = FctSpline1D(sum_pdf, xvals_binedges, norm=True) - pdf = PDSignalEnergyPDF(spline, norm, xvals) + pdf = PDSignalEnergyPDF(spline) return pdf args_list = [ - ((sm_histo, flux_model, gridfitparams), {}) + ((sm_pdf, flux_model, gridfitparams), {}) for gridfitparams in self.gridfitparams_list ] @@ -1440,7 +1441,7 @@ def create_e_pdf_for_true_e(true_e_idx): ncpu=self.ncpu, ppbar=ppbar) - del(sm_histo) + del(sm_pdf) # Save all the energy PDF objects in the PDFSet PDF registry with # the hash of the individual parameters as key. diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/trad_ps/utils.py index e27f24f140..568f696ea3 100644 --- a/skyllh/analyses/i3/trad_ps/utils.py +++ b/skyllh/analyses/i3/trad_ps/utils.py @@ -12,6 +12,74 @@ from skyllh.core.storage import create_FileLoader +class FctSpline1D(object): + """Class to represent a 1D function spline using the PchipInterpolator + class from scipy. + + The evaluate the spline, use the ``__call__`` method. + """ + def __init__(self, f, x_binedges, norm=False, **kwargs): + """Creates a new 1D function spline using the PchipInterpolator + class from scipy. + + Parameters + ---------- + f : (n_x,)-shaped 1D numpy ndarray + The numpy ndarray holding the function values at the bin centers. + x_binedges : (n_x+1,)-shaped 1D numpy ndarray + The numpy ndarray holding the bin edges of the x-axis. + norm : bool + Switch + """ + super().__init__(**kwargs) + + self.x_binedges = np.copy(x_binedges) + + self.x_min = self.x_binedges[0] + self.x_max = self.x_binedges[-1] + + x = get_bincenters_from_binedges(self.x_binedges) + + self.spl_f = interpolate.PchipInterpolator( + x, f, extrapolate=False + ) + + self.norm = None + if norm: + self.norm = integrate.quad( + self.__call__, + self.x_min, + self.x_max, + limit=200, + full_output=1 + )[0] + + def __call__(self, x, oor_value=0): + """Evaluates the spline at the given x values. For x-values + outside the spline's range, the oor_value is returned. + + Parameters + ---------- + x : (n_x,)-shaped 1D numpy ndarray + The numpy ndarray holding the x values at which the spline should + get evaluated. + + Returns + ------- + f : (n_x,)-shaped 1D numpy ndarray + The numpy ndarray holding the evaluated values of the spline. + """ + f = self.spl_f(x) + f = np.nan_to_num(f, nan=oor_value) + + return f + + def evaluate(self, *args, **kwargs): + """Alias for the __call__ method. + """ + return self(*args, **kwargs) + + class FctSpline2D(object): """Class to represent a 2D function spline using the RectBivariateSpline class from scipy. @@ -22,7 +90,8 @@ class from scipy. The evaluate the spline, use the ``__call__`` method. """ def __init__(self, f, x_binedges, y_binedges, **kwargs): - """Creates a new 2D function spline. + """Creates a new 2D function spline using the RectBivariateSpline + class from scipy. Parameters ---------- @@ -43,8 +112,8 @@ def __init__(self, f, x_binedges, y_binedges, **kwargs): self.y_min = self.y_binedges[0] self.y_max = self.y_binedges[-1] - x = get_bincenters_from_binedges(x_binedges) - y = get_bincenters_from_binedges(y_binedges) + x = get_bincenters_from_binedges(self.x_binedges) + y = get_bincenters_from_binedges(self.y_binedges) # Note: For simplicity we approximate zero bins with 1000x smaller # values than the minimum value. To do this correctly, one should store @@ -567,6 +636,8 @@ def n_log10_true_e_bins(self): def true_e_bin_edges(self): """(read-only) The (n_true_e+1,)-shaped 1D numpy ndarray holding the bin edges of the true energy. + + Depricated! Use log10_true_enu_binedges instead! """ return self._true_e_bin_edges @@ -578,6 +649,13 @@ def true_e_bin_centers(self): return 0.5*(self._true_e_bin_edges[:-1] + self._true_e_bin_edges[1:]) + @property + def log10_true_enu_binedges(self): + """(read-only) The (n_log10_true_enu+1,)-shaped 1D numpy ndarray holding + the bin edges of the log10 true neutrino energy. + """ + return self._true_e_bin_edges + @property def n_true_dec_bins(self): """(read-only) The number of true declination bins. @@ -599,6 +677,18 @@ def true_dec_bin_centers(self): return 0.5*(self._true_dec_bin_edges[:-1] + self._true_dec_bin_edges[1:]) + @property + def log10_reco_e_binedges_lower(self): + """(read-only) The upper bin edges of the log10 reco energy axes. + """ + return self.reco_e_lower_edges + + @property + def log10_reco_e_binedges_upper(self): + """(read-only) The upper bin edges of the log10 reco energy axes. + """ + return self.reco_e_upper_edges + @property def min_log10_reco_e(self): """(read-only) The minimum value of the reconstructed energy axis. From 4da24e2988813dbff703fbe4c01e86dcc6baee3f Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 13 Jul 2022 13:58:45 +0200 Subject: [PATCH 118/274] Make all analysis modules consistent with the new effective area class. --- skyllh/analyses/i3/trad_ps/detsigyield.py | 2 +- skyllh/analyses/i3/trad_ps/pdfratio.py | 2 +- skyllh/analyses/i3/trad_ps/signal_generator.py | 4 ++-- skyllh/analyses/i3/trad_ps/signalpdf.py | 6 +++--- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/detsigyield.py b/skyllh/analyses/i3/trad_ps/detsigyield.py index 5ef8aa8d80..383826abec 100644 --- a/skyllh/analyses/i3/trad_ps/detsigyield.py +++ b/skyllh/analyses/i3/trad_ps/detsigyield.py @@ -26,7 +26,7 @@ PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod, PowerLawFluxPointLikeSourceI3DetSigYield ) -from skyllh.analyses.i3.trad_ps.utils import ( +from skyllh.analyses.i3.trad_ps.pd_aeff import ( load_effective_area_array ) diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/trad_ps/pdfratio.py index 177c54c886..996f1677ed 100644 --- a/skyllh/analyses/i3/trad_ps/pdfratio.py +++ b/skyllh/analyses/i3/trad_ps/pdfratio.py @@ -45,7 +45,7 @@ def __init__(self, sig_pdf_set, bkg_pdf, cap_ratio=False, **kwargs): self.ratio_fill_value_dict = dict() for sig_pdf_key in sig_pdf_set.pdf_keys: sigpdf = sig_pdf_set[sig_pdf_key] - sigvals = sigpdf.get_pd_by_log10_e(log10_e_bc) + sigvals = sigpdf.get_pd_by_log10_reco_e(log10_e_bc) sigvals = np.broadcast_to(sigvals, (n_sinDec, n_logE)).T r = sigvals[bd] / bkg_pdf._hist_logE_sinDec[bd] val = np.percentile(r[r > 1.], ratio_perc) diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/trad_ps/signal_generator.py index de85e9a097..7f70300bf6 100644 --- a/skyllh/analyses/i3/trad_ps/signal_generator.py +++ b/skyllh/analyses/i3/trad_ps/signal_generator.py @@ -8,8 +8,8 @@ from skyllh.analyses.i3.trad_ps.utils import ( psi_to_dec_and_ra, PublicDataSmearingMatrix, - PublicDataAeff ) +from skyllh.analyses.i3.trad_ps.pd_aeff import PDAeff from skyllh.core.py import ( issequenceof, float_cast, @@ -29,7 +29,7 @@ def __init__(self, ds, **kwargs): pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - self.effA = PublicDataAeff( + self.effA = PDAeff( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/trad_ps/signalpdf.py index 7767f396ca..fc3348798f 100644 --- a/skyllh/analyses/i3/trad_ps/signalpdf.py +++ b/skyllh/analyses/i3/trad_ps/signalpdf.py @@ -560,7 +560,7 @@ def create_I3EnergyPDF( # Create a signal generator for this dataset. siggen = PublicDataSignalGenerator(ds) - aeff = PublicDataAeff( + aeff = PDAeff( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) @@ -886,7 +886,7 @@ def __init__( ) # Load the effective area. - aeff = PublicDataAeff( + aeff = PDAeff( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) @@ -1713,7 +1713,7 @@ def __init__( ) # Load the effective area. - aeff = PublicDataAeff( + aeff = PDAeff( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('eff_area_datafile'))) From 2bcec68ce1cb305b97ea482582f5b9a74ef98639 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 13 Jul 2022 14:24:46 +0200 Subject: [PATCH 119/274] Calculate the correct bin indices --- skyllh/analyses/i3/trad_ps/pd_aeff.py | 4 ++++ skyllh/core/binning.py | 29 ++++++++++++++++++--------- 2 files changed, 23 insertions(+), 10 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pd_aeff.py b/skyllh/analyses/i3/trad_ps/pd_aeff.py index afd2ea4b11..b0c95f82e3 100644 --- a/skyllh/analyses/i3/trad_ps/pd_aeff.py +++ b/skyllh/analyses/i3/trad_ps/pd_aeff.py @@ -278,6 +278,10 @@ def get_detection_prob_for_decnu( enu_binedges[:-1], enu_binedges[1:], np.array([enu_range_min, enu_range_max])) + # Note: The get_bin_indices_from_lower_and_upper_binedges function is + # based on the lower edges. So by definition the upper bin index + # is one too large. + uidx -= 1 aeff = self.get_aeff_for_decnu(decnu) aeff = aeff[lidx:uidx+1] diff --git a/skyllh/core/binning.py b/skyllh/core/binning.py index b2564ad4e7..3906f9e2f4 100644 --- a/skyllh/core/binning.py +++ b/skyllh/core/binning.py @@ -185,37 +185,46 @@ def get_binedges_from_bincenters(centers): return edges def get_bin_indices_from_lower_and_upper_binedges(le, ue, values): - """Returns the bin indices for the given lower and upper bin edges the given - values fall into. + """Returns the bin indices for the given values which must fall into bins + defined by the given lower and upper bin edges. + + Note: The upper edge is not included in the bin. Parameters ---------- - le : 1D numpy ndarray + le : (m,)-shaped 1D numpy ndarray The lower bin edges. - ue : 1D numpy ndarray + ue : (m,)-shaped 1D numpy ndarray The upper bin edges. - values : 1D numpy ndarray + values : (n,)-shaped 1D numpy ndarray The values for which to get the bin indices. Returns ------- - idxs : 1D numpy ndarray + idxs : (n,)-shaped 1D numpy ndarray The bin indices of the given values. """ + if len(le) != len(ue): + raise ValueError( + 'The lower {} and upper {} edge arrays must be of the same ' + 'size!'.format( + len(le), len(ue))) + if np.any(values < le[0]): invalid_values = values[values < le[0]] raise ValueError( '{} values ({}) are smaller than the lowest bin edge ({})!'.format( len(invalid_values), str(invalid_values), le[0])) - if np.any(values > ue[-1]): - invalid_values = values[values > ue[-1]] + if np.any(values >= ue[-1]): + invalid_values = values[values >= ue[-1]] raise ValueError( - '{} values ({}) are larger than the largest bin edge ({})!'.format( + '{} values ({}) are larger or equal than the largest bin edge ' + '({})!'.format( len(invalid_values), str(invalid_values), ue[-1])) m = ( (values[:,np.newaxis] >= le[np.newaxis,:]) & - (values[:,np.newaxis] <= ue[np.newaxis,:]) + (values[:,np.newaxis] < ue[np.newaxis,:]) ) idxs = np.nonzero(m)[1] From 23a1c1805e5f69c4c86eb71d6d0b8bbbcf2ad851 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 14 Jul 2022 09:26:45 +0200 Subject: [PATCH 120/274] Make mceq_atm_bkg.py compatible with the new effective area class. --- skyllh/analyses/i3/trad_ps/pd_aeff.py | 7 +++++++ .../analyses/i3/trad_ps/scripts/mceq_atm_bkg.py | 16 ++++++++-------- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pd_aeff.py b/skyllh/analyses/i3/trad_ps/pd_aeff.py index b0c95f82e3..dc6bd84f43 100644 --- a/skyllh/analyses/i3/trad_ps/pd_aeff.py +++ b/skyllh/analyses/i3/trad_ps/pd_aeff.py @@ -136,6 +136,13 @@ def decnu_binedges(self): """ return self._decnu_binedges + @property + def sin_decnu_binedges(self): + """(read-only) The sin of the bin edges of the neutrino declination + in radians. + """ + return np.sin(self._decnu_binedges) + @property def decnu_bincenters(self): """(read-only) The bin center values of the neutrino declination axis in diff --git a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py index f5841d6a07..a4b74f19dc 100644 --- a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py +++ b/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py @@ -7,7 +7,7 @@ import mceq_config as config from MCEq.core import MCEqRun -from skyllh.analyses.i3.trad_ps.utils import PublicDataAeff +from skyllh.analyses.i3.trad_ps.pd_aeff import PDAeff from skyllh.datasets.i3 import PublicData_10y_ps def create_flux_file(save_path, ds): @@ -15,16 +15,16 @@ def create_flux_file(save_path, ds): """ output_filename = ds.get_aux_data_definition('mceq_flux_datafile')[0] output_pathfilename = '' - if args.save_path is None: + if save_path is None: output_pathfilename = ds.get_abs_pathfilename_list([output_filename])[0] else: output_pathfilename = os.path.join( - args.save_path, output_filename) + save_path, output_filename) print('Output path filename: %s'%(output_pathfilename)) # Load the effective area instance to get the binning information. - aeff = PublicDataAeff( + aeff = PDAeff( os.path.join( ds.root_dir, ds.get_aux_data_definition('eff_area_datafile')[0] @@ -33,9 +33,9 @@ def create_flux_file(save_path, ds): # Setup MCeq. config.e_min = float( - 10**(np.max([aeff.log_true_e_binedges_lower[0], 2]))) + 10**(np.max([aeff._log10_enu_binedges_lower[0], 2]))) config.e_max = float( - 10**(np.min([aeff.log_true_e_binedges_upper[-1], 9])+0.05)) + 10**(np.min([aeff._log10_enu_binedges_upper[-1], 9])+0.05)) print('E_min = %s'%(config.e_min)) print('E_max = %s'%(config.e_max)) @@ -52,9 +52,9 @@ def create_flux_file(save_path, ds): mag = 0 # Use the same binning as for the effective area. # theta = delta + pi/2 - print('sin_true_dec_binedges: %s'%(str(aeff.sin_true_dec_binedges))) + print('sin_true_dec_binedges: %s'%(str(aeff.sin_decnu_binedges))) theta_angles_binedges = np.rad2deg( - np.arcsin(aeff.sin_true_dec_binedges) + np.pi/2 + np.arcsin(aeff.sin_decnu_binedges) + np.pi/2 ) theta_angles = 0.5*(theta_angles_binedges[:-1] + theta_angles_binedges[1:]) print('Theta angles = %s'%(str(theta_angles))) From e56fd9e9bdce6ac34169a71ccef713bee768ff45 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 14 Jul 2022 10:09:02 +0200 Subject: [PATCH 121/274] Handle edges cases correctly --- skyllh/analyses/i3/trad_ps/pd_aeff.py | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/trad_ps/pd_aeff.py b/skyllh/analyses/i3/trad_ps/pd_aeff.py index b0c95f82e3..7fc5859c72 100644 --- a/skyllh/analyses/i3/trad_ps/pd_aeff.py +++ b/skyllh/analyses/i3/trad_ps/pd_aeff.py @@ -274,14 +274,23 @@ def get_detection_prob_for_decnu( enu_binedges = np.power(10, self.log10_enu_binedges) # Get the bin indices for the lower and upper energy range values. - (lidx, uidx) = get_bin_indices_from_lower_and_upper_binedges( + (lidx,) = get_bin_indices_from_lower_and_upper_binedges( enu_binedges[:-1], enu_binedges[1:], - np.array([enu_range_min, enu_range_max])) - # Note: The get_bin_indices_from_lower_and_upper_binedges function is - # based on the lower edges. So by definition the upper bin index - # is one too large. - uidx -= 1 + np.array([enu_range_min]) + ) + if enu_range_max >= enu_binedges[-1]: + uidx = len(enu_binedges)-2 + else: + (uidx,) = get_bin_indices_from_lower_and_upper_binedges( + enu_binedges[:-1], + enu_binedges[1:], + np.array([enu_range_max]) + ) + # Note: The get_bin_indices_from_lower_and_upper_binedges function + # is based on the lower edges. So by definition the upper bin + # index is one too large. + uidx -= 1 aeff = self.get_aeff_for_decnu(decnu) aeff = aeff[lidx:uidx+1] From 05367039543b57daf8aeae267976d0299a9069ec Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 14 Jul 2022 11:34:09 +0200 Subject: [PATCH 122/274] Rename directory --- skyllh/analyses/i3/{trad_ps => publicdata_ps}/__init__.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/analysis.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/bkg_flux.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/detsigyield.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/pd_aeff.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/pdfratio.py | 0 .../i3/{trad_ps => publicdata_ps}/scripts/mceq_atm_bkg.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/signal_generator.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/signalpdf.py | 0 skyllh/analyses/i3/{trad_ps => publicdata_ps}/utils.py | 0 10 files changed, 0 insertions(+), 0 deletions(-) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/__init__.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/analysis.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/bkg_flux.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/detsigyield.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/pd_aeff.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/pdfratio.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/scripts/mceq_atm_bkg.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/signal_generator.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/signalpdf.py (100%) rename skyllh/analyses/i3/{trad_ps => publicdata_ps}/utils.py (100%) diff --git a/skyllh/analyses/i3/trad_ps/__init__.py b/skyllh/analyses/i3/publicdata_ps/__init__.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/__init__.py rename to skyllh/analyses/i3/publicdata_ps/__init__.py diff --git a/skyllh/analyses/i3/trad_ps/analysis.py b/skyllh/analyses/i3/publicdata_ps/analysis.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/analysis.py rename to skyllh/analyses/i3/publicdata_ps/analysis.py diff --git a/skyllh/analyses/i3/trad_ps/bkg_flux.py b/skyllh/analyses/i3/publicdata_ps/bkg_flux.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/bkg_flux.py rename to skyllh/analyses/i3/publicdata_ps/bkg_flux.py diff --git a/skyllh/analyses/i3/trad_ps/detsigyield.py b/skyllh/analyses/i3/publicdata_ps/detsigyield.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/detsigyield.py rename to skyllh/analyses/i3/publicdata_ps/detsigyield.py diff --git a/skyllh/analyses/i3/trad_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/pd_aeff.py rename to skyllh/analyses/i3/publicdata_ps/pd_aeff.py diff --git a/skyllh/analyses/i3/trad_ps/pdfratio.py b/skyllh/analyses/i3/publicdata_ps/pdfratio.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/pdfratio.py rename to skyllh/analyses/i3/publicdata_ps/pdfratio.py diff --git a/skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py b/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/scripts/mceq_atm_bkg.py rename to skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py diff --git a/skyllh/analyses/i3/trad_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/signal_generator.py rename to skyllh/analyses/i3/publicdata_ps/signal_generator.py diff --git a/skyllh/analyses/i3/trad_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/signalpdf.py rename to skyllh/analyses/i3/publicdata_ps/signalpdf.py diff --git a/skyllh/analyses/i3/trad_ps/utils.py b/skyllh/analyses/i3/publicdata_ps/utils.py similarity index 100% rename from skyllh/analyses/i3/trad_ps/utils.py rename to skyllh/analyses/i3/publicdata_ps/utils.py From 7ef38ab1cf271208207b9da6b1aa32f82c160432 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 14 Jul 2022 11:37:11 +0200 Subject: [PATCH 123/274] Rename analysis script --- skyllh/analyses/i3/publicdata_ps/{analysis.py => trad_ps.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename skyllh/analyses/i3/publicdata_ps/{analysis.py => trad_ps.py} (100%) diff --git a/skyllh/analyses/i3/publicdata_ps/analysis.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py similarity index 100% rename from skyllh/analyses/i3/publicdata_ps/analysis.py rename to skyllh/analyses/i3/publicdata_ps/trad_ps.py From 9d8efcdc855a2f5b596bf0d0be42510e340bf1b1 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 14 Jul 2022 11:45:08 +0200 Subject: [PATCH 124/274] Correct imports --- skyllh/analyses/i3/publicdata_ps/detsigyield.py | 2 +- .../i3/publicdata_ps/signal_generator.py | 17 ++++++++++------- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 4 ++-- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 6 +++--- 4 files changed, 16 insertions(+), 13 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/detsigyield.py b/skyllh/analyses/i3/publicdata_ps/detsigyield.py index 383826abec..a56e813691 100644 --- a/skyllh/analyses/i3/publicdata_ps/detsigyield.py +++ b/skyllh/analyses/i3/publicdata_ps/detsigyield.py @@ -26,7 +26,7 @@ PowerLawFluxPointLikeSourceI3DetSigYieldImplMethod, PowerLawFluxPointLikeSourceI3DetSigYield ) -from skyllh.analyses.i3.trad_ps.pd_aeff import ( +from skyllh.analyses.i3.publicdata_ps.pd_aeff import ( load_effective_area_array ) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 7f70300bf6..164e0a39f2 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -1,20 +1,23 @@ +# -*- coding: utf-8 -*- + import numpy as np from scipy import interpolate +from skyllh.core.py import ( + issequenceof, + float_cast, + int_cast +) from skyllh.core.llhratio import LLHRatio from skyllh.core.dataset import Dataset from skyllh.core.source_hypothesis import SourceHypoGroupManager from skyllh.core.storage import DataFieldRecordArray -from skyllh.analyses.i3.trad_ps.utils import ( + +from skyllh.analyses.i3.publicdata_ps.utils import ( psi_to_dec_and_ra, PublicDataSmearingMatrix, ) -from skyllh.analyses.i3.trad_ps.pd_aeff import PDAeff -from skyllh.core.py import ( - issequenceof, - float_cast, - int_cast -) +from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff class PublicDataDatasetSignalGenerator(object): diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index fc3348798f..74b704d517 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -41,10 +41,10 @@ from skyllh.i3.dataset import I3Dataset from skyllh.physics.flux import FluxModel -from skyllh.analyses.i3.trad_ps.pd_aeff import ( +from skyllh.analyses.i3.publicdata_ps.pd_aeff import ( PDAeff, ) -from skyllh.analyses.i3.trad_ps.utils import ( +from skyllh.analyses.i3.publicdata_ps.utils import ( FctSpline1D, create_unionized_smearing_matrix_array, load_smearing_histogram, diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index b07ef70f5f..784975675d 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -76,17 +76,17 @@ setup_file_handler ) -# Pre-defined IceCube data samples. +# Pre-defined public IceCube data samples. from skyllh.datasets.i3 import data_samples # Analysis specific classes for working with the public data. from skyllh.analyses.i3.trad_ps.detsigyield import ( PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod ) -from skyllh.analyses.i3.trad_ps.signalpdf import ( +from skyllh.analyses.i3.publicdata_ps.signalpdf import ( PDSignalEnergyPDFSet ) -from skyllh.analyses.i3.trad_ps.pdfratio import ( +from skyllh.analyses.i3.publicdata_ps.pdfratio import ( PDPDFRatio ) From 6a8aee2ed2ff75fe74ee8518c5dd30358a13e573 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 14 Jul 2022 11:53:16 +0200 Subject: [PATCH 125/274] Correct imports --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 784975675d..8d40894d72 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -62,7 +62,6 @@ ) from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod -from skyllh.analyses.i3.trad_ps.signal_generator import PublicDataSignalGenerator # Analysis utilities. from skyllh.core.analysis_utils import ( @@ -80,7 +79,10 @@ from skyllh.datasets.i3 import data_samples # Analysis specific classes for working with the public data. -from skyllh.analyses.i3.trad_ps.detsigyield import ( +from skyllh.analyses.i3.publicdata_ps.signal_generator import ( + PublicDataSignalGenerator +) +from skyllh.analyses.i3.publicdata_ps.detsigyield import ( PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod ) from skyllh.analyses.i3.publicdata_ps.signalpdf import ( From 39e3d56e6288be582e77e818d54ff7e69b93138a Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 14 Jul 2022 17:06:14 +0200 Subject: [PATCH 126/274] Fix imports. --- skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py b/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py index a4b74f19dc..2cc226e1eb 100644 --- a/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py +++ b/skyllh/analyses/i3/publicdata_ps/scripts/mceq_atm_bkg.py @@ -7,7 +7,7 @@ import mceq_config as config from MCEq.core import MCEqRun -from skyllh.analyses.i3.trad_ps.pd_aeff import PDAeff +from skyllh.analyses.i3.publicdata_ps.pd_aeff import PDAeff from skyllh.datasets.i3 import PublicData_10y_ps def create_flux_file(save_path, ds): From d45db463f8c155aeb02dfcdad06fa56900c21b96 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Thu, 28 Jul 2022 11:38:56 +0200 Subject: [PATCH 127/274] Updated to new effective area properties. --- skyllh/analyses/i3/publicdata_ps/signal_generator.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 164e0a39f2..7fd084bce9 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -41,11 +41,11 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, """Sample the true neutrino energy from the power-law re-weighted with the detection probability. """ - m = (self.effA.log_true_e_bincenters >= log_e_min) & ( - self.effA.log_true_e_bincenters < log_e_max) - bin_centers = self.effA.log_true_e_bincenters[m] - low_bin_edges = self.effA.log_true_e_binedges_lower[m] - high_bin_edges = self.effA.log_true_e_binedges_upper[m] + m = (self.effA.log10_enu_bincenters >= log_e_min) & ( + self.effA.log10_enu_bincenters < log_e_max) + bin_centers = self.effA.log10_enu_bincenters[m] + low_bin_edges = self.effA._log10_enu_binedges_lower[m] + high_bin_edges = self.effA._log10_enu_binedges_upper[m] # Flux probability P(E_nu | gamma) per bin. flux_prob = flux_model.get_integral( @@ -58,7 +58,7 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, # Detection probability P(E_nu | sin(dec)) per bin. det_prob = np.empty((len(bin_centers),), dtype=np.double) for i in range(len(bin_centers)): - det_prob[i] = self.effA.get_detection_prob_for_sin_true_dec( + det_prob[i] = self.effA.get_detection_prob_for_decnu( src_dec, 10**low_bin_edges[i], 10**high_bin_edges[i], 10 ** low_bin_edges[0], 10 ** high_bin_edges[-1]) From 4ff0ef0613f86bb296d45c2cf67e718c42eb0c57 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 5 Aug 2022 13:53:51 +0200 Subject: [PATCH 128/274] Add bin number properties to Aeff and SM --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 12 ++++++++++++ skyllh/analyses/i3/publicdata_ps/utils.py | 3 +++ 2 files changed, 15 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 03fd13dd21..74032407e9 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -150,6 +150,12 @@ def decnu_bincenters(self): """ return get_bincenters_from_binedges(self._decnu_binedges) + @property + def n_decnu_bins(self): + """(read-only) The number of bins of the neutrino declination axis. + """ + return len(self._decnu_binedges) - 1 + @property def log10_enu_binedges(self): """(read-only) The bin edges of the log10(E_nu/GeV) neutrino energy @@ -164,6 +170,12 @@ def log10_enu_bincenters(self): """ return get_bincenters_from_binedges(self._log10_enu_binedges) + @property + def n_log10_enu_bins(self): + """(read-only) The number of bins of the log10 neutrino energy axis. + """ + return len(self._log10_enu_binedges) - 1 + @property def aeff_decnu_log10enu(self): """(read-only) The effective area in cm^2 as (n_decnu,n_log10enu)-shaped diff --git a/skyllh/analyses/i3/publicdata_ps/utils.py b/skyllh/analyses/i3/publicdata_ps/utils.py index 568f696ea3..2e52d5e152 100644 --- a/skyllh/analyses/i3/publicdata_ps/utils.py +++ b/skyllh/analyses/i3/publicdata_ps/utils.py @@ -612,6 +612,9 @@ def __init__( self.ang_err_upper_edges ) = load_smearing_histogram(pathfilenames) + self.n_psi_bins = self.histogram.shape[3] + self.n_ang_err_bins = self.histogram.shape[4] + # Create bin edges array for log10_reco_e. s = np.array(self.reco_e_lower_edges.shape) s[-1] += 1 From 5aeb2b511015fb5429fe08ae73d8c38a7c339ac0 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 9 Aug 2022 14:03:55 +0200 Subject: [PATCH 129/274] Add method to create sin_decnu_log10_enu 2D spline --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 74032407e9..e0dc1e8e3c 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -11,6 +11,8 @@ ) from skyllh.core.storage import create_FileLoader +from skyllh.analyses.i3.publicdata_ps.utils import FctSpline2D + def load_effective_area_array(pathfilenames): """Loads the (nbins_decnu, nbins_log10enu)-shaped 2D effective @@ -183,6 +185,23 @@ def aeff_decnu_log10enu(self): """ return self._aeff_decnu_log10enu + def create_sin_decnu_log10_enu_spline(self): + """Creates a FctSpline2D object representing a 2D spline of the + effective area in sin(dec_nu)-log10(E_nu/GeV)-space. + + Returns + ------- + spl : FctSpline2D instance + The FctSpline2D instance representing a spline in the + sin(dec_nu)-log10(E_nu/GeV)-space. + """ + spl = FctSpline2D( + self._aeff_decnu_log10enu, + self.sin_decnu_binedges, + self.log10_enu_binedges + ) + return spl + def get_aeff_for_decnu(self, decnu): """Retrieves the effective area as function of log10_enu. From 77c152bb24806cc264547301fd8ed6ab2eb7dbdc Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 9 Aug 2022 14:04:49 +0200 Subject: [PATCH 130/274] Add property for ang_err binedges --- skyllh/analyses/i3/publicdata_ps/utils.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/utils.py b/skyllh/analyses/i3/publicdata_ps/utils.py index 2e52d5e152..767f517fb5 100644 --- a/skyllh/analyses/i3/publicdata_ps/utils.py +++ b/skyllh/analyses/i3/publicdata_ps/utils.py @@ -619,15 +619,22 @@ def __init__( s = np.array(self.reco_e_lower_edges.shape) s[-1] += 1 self.log10_reco_e_binedges = np.empty(s, dtype=np.double) - self.log10_reco_e_binedges[:,:,:-1] = self.reco_e_lower_edges - self.log10_reco_e_binedges[:,:,-1] = self.reco_e_upper_edges[:,:,-1] + self.log10_reco_e_binedges[...,:-1] = self.reco_e_lower_edges + self.log10_reco_e_binedges[...,-1] = self.reco_e_upper_edges[...,-1] # Create bin edges array for psi. s = np.array(self.psi_lower_edges.shape) s[-1] += 1 self.psi_binedges = np.empty(s, dtype=np.double) - self.psi_binedges[:,:,:,:-1] = self.psi_lower_edges - self.psi_binedges[:,:,:,-1] = self.psi_upper_edges[:,:,:,-1] + self.psi_binedges[...,:-1] = self.psi_lower_edges + self.psi_binedges[...,-1] = self.psi_upper_edges[...,-1] + + # Create bin edges array for ang_err. + s = np.array(self.ang_err_lower_edges.shape) + s[-1] += 1 + self.ang_err_binedges = np.empty(s, dtype=np.double) + self.ang_err_binedges[...,:-1] = self.ang_err_lower_edges + self.ang_err_binedges[...,-1] = self.ang_err_upper_edges[...,-1] @property def n_log10_true_e_bins(self): From 881ca56a7fa44e14ab330564826a9ae4d8c765f1 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 11 Aug 2022 13:08:22 +0200 Subject: [PATCH 131/274] Make the cumsum of the true energy inv PDF monotonically increasing --- .../i3/publicdata_ps/signal_generator.py | 27 +++++++++++++------ 1 file changed, 19 insertions(+), 8 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 7fd084bce9..5c8c906c2e 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -51,32 +51,43 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, flux_prob = flux_model.get_integral( 10**low_bin_edges, 10**high_bin_edges ) / flux_model.get_integral( - 10 ** low_bin_edges[0], - 10 ** high_bin_edges[-1] + 10**low_bin_edges[0], 10**high_bin_edges[-1] ) # Detection probability P(E_nu | sin(dec)) per bin. det_prob = np.empty((len(bin_centers),), dtype=np.double) for i in range(len(bin_centers)): det_prob[i] = self.effA.get_detection_prob_for_decnu( - src_dec, 10**low_bin_edges[i], 10**high_bin_edges[i], - 10 ** low_bin_edges[0], 10 ** high_bin_edges[-1]) + src_dec, + 10**low_bin_edges[i], 10**high_bin_edges[i], + 10**low_bin_edges[0], 10**high_bin_edges[-1]) # Do the product and normalize again to a probability per bin. product = flux_prob * det_prob prob_per_bin = product / np.sum(product) + # The probability per bin cannot be zero, otherwise the cumulative + # sum would not be increasing monotonically. So we set zero bins to + # 1000 times smaller than the smallest non-zero bin. + m = prob_per_bin == 0 + prob_per_bin[m] = np.min(prob_per_bin[np.invert(m)]) / 1000 + prob_per_bin /= np.sum(prob_per_bin) + # Compute the cumulative distribution CDF. cum_per_bin = np.cumsum(prob_per_bin) cum_per_bin = np.concatenate(([0], cum_per_bin)) + if np.any(np.diff(cum_per_bin) == 0): + raise ValueError( + 'The cumulative sum of the true energy probability is not ' + 'monotonically increasing! Values of the cumsum are ' + f'{cum_per_bin}.') + bin_centers = np.concatenate(([low_bin_edges[0]], bin_centers)) # Build a spline for the inverse CDF. self.inv_cdf_spl = interpolate.splrep( cum_per_bin, bin_centers, k=1, s=0) - return - @staticmethod def _eval_spline(x, spl): values = interpolate.splev(x, spl, ext=3) @@ -155,9 +166,9 @@ def _generate_events( log_true_e_idxs = ( np.digitize(log_true_e, bins=sm.true_e_bin_edges) - 1 ) + # Sample reconstructed energies given true neutrino energies. - (log_e_idxs, log_e) = sm.sample_log_e( - rss, dec_idx, log_true_e_idxs) + (log_e_idxs, log_e) = sm.sample_log_e(rss, dec_idx, log_true_e_idxs) events['log_energy'] = log_e # Sample reconstructed psi values given true neutrino energy and From 58dbec7259d20ff5e888a24909be5d1315c6c0ad Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Thu, 11 Aug 2022 16:32:26 +0200 Subject: [PATCH 132/274] Extend spline to entire enu range --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 31 +++++++++++++++------ 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index e0dc1e8e3c..9216127985 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -338,16 +338,31 @@ def get_detection_prob_for_decnu( daeff_dE = aeff / dE - enu_bincenters = get_bincenters_from_binedges(enu_binedges) - tck = interpolate.splrep( - enu_bincenters, daeff_dE, - xb=enu_range_min, xe=enu_range_max, k=1, s=0) + # Create a spline representation that spans the entire enu range. + x = np.empty((len(enu_binedges)+1,), dtype=np.double) + x[0] = enu_binedges[0] + x[1:-1] = get_bincenters_from_binedges(enu_binedges) + x[-1] = enu_binedges[-1] + + y = np.empty((len(enu_binedges)+1,), dtype=np.double) + y[0] = daeff_dE[0] + y[1:-1] = daeff_dE + y[-1] = daeff_dE[-1] + + spl = interpolate.splrep( + x, + y, + xb=enu_range_min, + xe=enu_range_max, + k=1, + s=0 + ) - def _eval_func(x): - return interpolate.splev(x, tck, der=0) + def _eval_spl_func(x): + return interpolate.splev(x, spl, der=0, ext=1) norm = integrate.quad( - _eval_func, + _eval_spl_func, enu_range_min, enu_range_max, limit=200, @@ -355,7 +370,7 @@ def _eval_func(x): )[0] integral = integrate.quad( - _eval_func, + _eval_spl_func, enu_min, enu_max, limit=200, From 06937b76b9c07815c88339426455a2dfe5d4eedc Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 17 Aug 2022 13:49:56 +0200 Subject: [PATCH 133/274] Improve code formatting --- skyllh/core/pdf.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/skyllh/core/pdf.py b/skyllh/core/pdf.py index 1392791e46..2deeb39f68 100644 --- a/skyllh/core/pdf.py +++ b/skyllh/core/pdf.py @@ -400,9 +400,10 @@ def assert_is_valid_for_trial_data(self, tdm): The method must raise a ValueError if the PDF is not valid for the given trial data. """ - raise NotImplementedError('The derived PDF class "%s" did not ' - 'implement the "assert_is_valid_for_trial_data" method!' % ( - classname(self))) + raise NotImplementedError( + 'The derived PDF class "%s" did not implement the ' + '"assert_is_valid_for_trial_data" method!' % ( + classname(self))) @abc.abstractmethod def get_prob(self, tdm, params=None, tl=None): From 04071e069762ed34a592587cc7779ebc2d197a88 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 17 Aug 2022 14:14:16 +0200 Subject: [PATCH 134/274] Added background pdf with KDE smooting. --- .../i3/publicdata_ps/backgroundpdf.py | 344 ++++++++++++++++++ skyllh/analyses/i3/publicdata_ps/trad_ps.py | 47 +-- 2 files changed, 353 insertions(+), 38 deletions(-) create mode 100644 skyllh/analyses/i3/publicdata_ps/backgroundpdf.py diff --git a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py new file mode 100644 index 0000000000..c1449b9cd3 --- /dev/null +++ b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py @@ -0,0 +1,344 @@ +# -*- coding: utf-8 -*- + +import numpy as np + +from skyllh.core.binning import UsesBinning +from skyllh.core.pdf import ( + EnergyPDF, + IsBackgroundPDF, + PDFAxis +) +from skyllh.core.py import issequenceof +from skyllh.core.storage import DataFieldRecordArray +from skyllh.core.timing import TaskTimer +from skyllh.core.smoothing import ( + UNSMOOTH_AXIS, + SmoothingFilter, + HistSmoothingMethod, + NoHistSmoothingMethod, + NeighboringBinHistSmoothingMethod +) +from skyllh.core.timing import TaskTimer + +from scipy.stats import gaussian_kde + + +class PDEnergyPDF(EnergyPDF, UsesBinning): + """This is the base class for IceCube specific energy PDF models. + IceCube energy PDFs depend soley on the energy and the + zenith angle, and hence, on the declination of the event. + + The IceCube energy PDF is modeled as a 1d histogram in energy, + but for different sin(declination) bins, hence, stored as a 2d histogram. + """ + + _KDE_BW_NORTH = 0.4 + _KDE_BW_SOUTH = 0.32 + + def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, + logE_binning, sinDec_binning, smoothing_filter, kde_smoothing=False): + """Creates a new IceCube energy PDF object. + + Parameters + ---------- + data_logE : 1d ndarray + The array holding the log10(E) values of the events. + data_sinDec : 1d ndarray + The array holding the sin(dec) values of the events. + data_mcweight : 1d ndarray + The array holding the monte-carlo weights of the events. + The final data weight will be the product of data_mcweight and + data_physicsweight. + data_physicsweight : 1d ndarray + The array holding the physics weights of the events. + The final data weight will be the product of data_mcweight and + data_physicsweight. + logE_binning : BinningDefinition + The binning definition for the log(E) axis. + sinDec_binning : BinningDefinition + The binning definition for the sin(declination) axis. + smoothing_filter : SmoothingFilter instance | None + The smoothing filter to use for smoothing the energy histogram. + If None, no smoothing will be applied. + kde_smoothing : bool + Apply a kde smoothing to the enrgy pdf for each sine of the + muon declination. + Default: False. + """ + super(PDEnergyPDF, self).__init__() + + # self.logger = logging.getLogger(__name__) + + # Define the PDF axes. + self.add_axis(PDFAxis(name='log_energy', + vmin=logE_binning.lower_edge, + vmax=logE_binning.upper_edge)) + self.add_axis(PDFAxis(name='sin_dec', + vmin=sinDec_binning.lower_edge, + vmax=sinDec_binning.upper_edge)) + + self.add_binning(logE_binning, 'log_energy') + self.add_binning(sinDec_binning, 'sin_dec') + + # Create the smoothing method instance tailored to the energy PDF. + # We will smooth only the first axis (logE). + if((smoothing_filter is not None) and + (not isinstance(smoothing_filter, SmoothingFilter))): + raise TypeError( + 'The smoothing_filter argument must be None or an instance of SmoothingFilter!') + if(smoothing_filter is None): + self.hist_smoothing_method = NoHistSmoothingMethod() + else: + self.hist_smoothing_method = NeighboringBinHistSmoothingMethod( + (smoothing_filter.axis_kernel_array, UNSMOOTH_AXIS)) + + # We have to figure out, which histogram bins are zero due to no + # monte-carlo coverage, and which due to zero physics model + # contribution. + + # Create a 2D histogram with only the MC events to determine the MC + # coverage. + (h, bins_logE, bins_sinDec) = np.histogram2d( + data_logE, data_sinDec, + bins=[ + logE_binning.binedges, sinDec_binning.binedges], + range=[ + logE_binning.range, sinDec_binning.range], + normed=False) + h = self._hist_smoothing_method.smooth(h) + self._hist_mask_mc_covered = h > 0 + + # Select the events which have MC coverage but zero physics + # contribution, i.e. the physics model predicts zero contribution. + mask = data_physicsweight == 0. + + # Create a 2D histogram with only the MC events that have zero physics + # contribution. Note: By construction the zero physics contribution bins + # are a subset of the MC covered bins. + (h, bins_logE, bins_sinDec) = np.histogram2d( + data_logE[mask], data_sinDec[mask], + bins=[ + logE_binning.binedges, sinDec_binning.binedges], + range=[ + logE_binning.range, sinDec_binning.range], + normed=False) + h = self._hist_smoothing_method.smooth(h) + self._hist_mask_mc_covered_zero_physics = h > 0 + + # Create a 2D histogram with only the data which has physics + # contribution. We will do the normalization along the logE + # axis manually. + data_weights = data_mcweight[~mask] * data_physicsweight[~mask] + (h, bins_logE, bins_sinDec) = np.histogram2d( + data_logE[~mask], data_sinDec[~mask], + bins=[ + logE_binning.binedges, sinDec_binning.binedges], + weights=data_weights, + range=[ + logE_binning.range, sinDec_binning.range], + normed=False) + + # If a bandwidth is passed, apply a KDE-based smoothing with the given + # bw parameter as bandwidth for the fit. + # Warning: right now this implies an additional dependency on an + # external package for KDE analysis. + if kde_smoothing: + if not isinstance(kde_smoothing, bool): + raise ValueError( + "The bandwidth parameter must be True or False!") + kde_pdf = np.empty( + (len(sinDec_binning.bincenters),), dtype=object) + data_logE_mask = data_logE[~mask] + data_sinDec_mask = data_sinDec[~mask] + for i in range(len(sinDec_binning.bincenters)): + sindec_mask = np.logical_and( + data_sinDec_mask >= sinDec_binning.binedges[i], + data_sinDec_mask < sinDec_binning.binedges[i+1] + ) + this_energy = data_logE_mask[sindec_mask] + if sinDec_binning.binedges[i] >= 0: + kde_pdf[i] = gaussian_kde( + this_energy, bw_method=self._KDE_BW_NORTH) + else: + kde_pdf[i] = gaussian_kde( + this_energy, bw_method=self._KDE_BW_SOUTH) + h = np.vstack( + [kde_pdf[i].evaluate(logE_binning.bincenters) + for i in range(len(sinDec_binning.bincenters))]).T + + # Calculate the normalization for each logE bin. Hence we need to sum + # over the logE bins (axis 0) for each sin(dec) bin and need to divide + # by the logE bin widths along the sin(dec) bins. The result array norm + # is a 2D array of the same shape as h. + norms = np.sum(h, axis=(0,))[np.newaxis, ...] * \ + np.diff(logE_binning.binedges)[..., np.newaxis] + h /= norms + h = self._hist_smoothing_method.smooth(h) + + self._hist_logE_sinDec = h + + @ property + def hist_smoothing_method(self): + """The HistSmoothingMethod instance defining the smoothing filter of the + energy PDF histogram. + """ + return self._hist_smoothing_method + + @ hist_smoothing_method.setter + def hist_smoothing_method(self, method): + if(not isinstance(method, HistSmoothingMethod)): + raise TypeError( + 'The hist_smoothing_method property must be an instance of HistSmoothingMethod!') + self._hist_smoothing_method = method + + @ property + def hist(self): + """(read-only) The 2D logE-sinDec histogram array. + """ + return self._hist_logE_sinDec + + @ property + def hist_mask_mc_covered(self): + """(read-only) The boolean ndarray holding the mask of the 2D histogram + bins for which there is monte-carlo coverage. + """ + return self._hist_mask_mc_covered + + @ property + def hist_mask_mc_covered_zero_physics(self): + """(read-only) The boolean ndarray holding the mask of the 2D histogram + bins for which there is monte-carlo coverage but zero physics + contribution. + """ + return self._hist_mask_mc_covered_zero_physics + + @ property + def hist_mask_mc_covered_with_physics(self): + """(read-only) The boolean ndarray holding the mask of the 2D histogram + bins for which there is monte-carlo coverage and has physics + contribution. + """ + return self._hist_mask_mc_covered & ~self._hist_mask_mc_covered_zero_physics + + def assert_is_valid_for_exp_data(self, data_exp): + """Checks if this energy PDF is valid for all the given experimental + data. + It checks if all the data is within the logE and sin(dec) binning range. + + Parameters + ---------- + data_exp : numpy record ndarray + The array holding the experimental data. The following data fields + must exist: + + - 'log_energy' : float + The logarithm of the energy value of the data event. + - 'dec' : float + The declination of the data event. + + Raises + ------ + ValueError + If some of the data is outside the logE or sin(dec) binning range. + """ + logE_binning = self.get_binning('log_energy') + sinDec_binning = self.get_binning('sin_dec') + + exp_logE = data_exp['log_energy'] + exp_sinDec = np.sin(data_exp['dec']) + + # Check if all the data is within the binning range. + # if(logE_binning.any_data_out_of_binning_range(exp_logE)): + # self.logger.warning('Some data is outside the logE range (%.3f, %.3f)', logE_binning.lower_edge, logE_binning.upper_edge) + # if(sinDec_binning.any_data_out_of_binning_range(exp_sinDec)): + # self.logger.warning('Some data is outside the sin(dec) range (%.3f, %.3f)', sinDec_binning.lower_edge, sinDec_binning.upper_edge) + + def get_prob(self, tdm, fitparams=None, tl=None): + """Calculates the energy probability (in logE) of each event. + + Parameters + ---------- + tdm : instance of TrialDataManager + The TrialDataManager instance holding the data events for which the + probability should be calculated for. The following data fields must + exist: + + - 'log_energy' : float + The logarithm of the energy value of the event. + - 'sin_dec' : float + The sin(declination) value of the event. + + fitparams : None + Unused interface parameter. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + prob : 1D (N_events,) shaped ndarray + The array with the energy probability for each event. + """ + get_data = tdm.get_data + + logE_binning = self.get_binning('log_energy') + sinDec_binning = self.get_binning('sin_dec') + + logE_idx = np.digitize( + get_data('log_energy'), logE_binning.binedges) - 1 + sinDec_idx = np.digitize( + get_data('sin_dec'), sinDec_binning.binedges) - 1 + + with TaskTimer(tl, 'Evaluating logE-sinDec histogram.'): + prob = self._hist_logE_sinDec[(logE_idx, sinDec_idx)] + + return prob + + +class PDDataBackgroundI3EnergyPDF(PDEnergyPDF, IsBackgroundPDF): + """This is the IceCube energy background PDF, which gets constructed from + experimental data. This class is derived from I3EnergyPDF. + """ + + def __init__(self, data_exp, logE_binning, sinDec_binning, + smoothing_filter=None, kde_smoothing=False): + """Constructs a new IceCube energy background PDF from experimental + data. + + Parameters + ---------- + data_exp : instance of DataFieldRecordArray + The array holding the experimental data. The following data fields + must exist: + + - 'log_energy' : float + The logarithm of the reconstructed energy value of the data + event. + - 'sin_dec' : float + The sine of the reconstructed declination of the data event. + + logE_binning : BinningDefinition + The binning definition for the binning in log10(E). + sinDec_binning : BinningDefinition + The binning definition for the sin(declination). + smoothing_filter : SmoothingFilter instance | None + The smoothing filter to use for smoothing the energy histogram. + If None, no smoothing will be applied. + """ + if(not isinstance(data_exp, DataFieldRecordArray)): + raise TypeError('The data_exp argument must be an instance of ' + 'DataFieldRecordArray!') + + data_logE = data_exp['log_energy'] + data_sinDec = data_exp['sin_dec'] + # For experimental data, the MC and physics weight are unity. + data_mcweight = np.ones((len(data_exp),)) + data_physicsweight = data_mcweight + + # Create the PDF using the base class. + super(PDDataBackgroundI3EnergyPDF, self).__init__( + data_logE, data_sinDec, data_mcweight, data_physicsweight, + logE_binning, sinDec_binning, smoothing_filter, kde_smoothing + ) + # Check if this PDF is valid for all the given experimental data. + self.assert_is_valid_for_exp_data(data_exp) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 8d40894d72..bfad507576 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -8,7 +8,6 @@ import argparse import logging import numpy as np -import os.path from skyllh.core.progressbar import ProgressBar @@ -47,22 +46,15 @@ # Classes to define the signal and background PDFs. from skyllh.core.signalpdf import RayleighPSFPointSourceSignalSpatialPDF -from skyllh.i3.signalpdf import SignalI3EnergyPDFSet from skyllh.i3.backgroundpdf import ( - DataBackgroundI3SpatialPDF, - DataBackgroundI3EnergyPDF -) -from skyllh.i3.pdfratio import ( - I3EnergySigSetOverBkgPDFRatioSpline + DataBackgroundI3SpatialPDF ) + # Classes to define the spatial and energy PDF ratios. from skyllh.core.pdfratio import ( SpatialSigOverBkgPDFRatio, - Skylab2SkylabPDFRatioFillMethod ) -from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod - # Analysis utilities. from skyllh.core.analysis_utils import ( pointlikesource_to_data_field_array @@ -91,6 +83,9 @@ from skyllh.analyses.i3.publicdata_ps.pdfratio import ( PDPDFRatio ) +from skyllh.analyses.i3.publicdata_ps.backgroundpdf import ( + PDDataBackgroundI3EnergyPDF +) def psi_func(tdm, src_hypo_group_manager, fitparams): @@ -130,7 +125,6 @@ def TXS_location(): def create_analysis( - rss, datasets, source, refplflux_Phi0=1, @@ -138,9 +132,8 @@ def create_analysis( refplflux_gamma=2, ns_seed=10.0, gamma_seed=3, - cache_dir='.', + kde_smoothing=False, cap_ratio=False, - n_mc_events=int(1e7), compress_data=False, keep_data_fields=None, optimize_delta_angle=10, @@ -300,8 +293,9 @@ def create_analysis( ppbar=ppbar ) smoothing_filter = BlockSmoothingFilter(nbins=1) - energy_bkgpdf = DataBackgroundI3EnergyPDF( - data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) + energy_bkgpdf = PDDataBackgroundI3EnergyPDF( + data.exp, log_energy_binning, sin_dec_binning, + smoothing_filter, kde_smoothing) energy_pdfratio = PDPDFRatio( sig_pdf_set=energy_sigpdfset, @@ -319,8 +313,6 @@ def create_analysis( analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) - # analysis.construct_signal_generator() - return analysis @@ -354,13 +346,6 @@ def create_analysis( type=str, help='The base path to the data samples (default=None)' ) - p.add_argument( - '--pdf-seed', - default=1, - type=int, - help='The random number generator seed for generating the ' - 'signal PDF.' - ) p.add_argument( '--seed', default=1, @@ -374,17 +359,6 @@ def create_analysis( type=int, help='The number of CPUs to utilize where parallelization is possible.' ) - p.add_argument( - '--n-mc-events', - default=int(1e7), - type=int, - help='The number of MC events to sample for the energy signal PDF.' - ) - p.add_argument( - '--cache-dir', - default='.', - type=str, - help='The cache directory to look for cached data, e.g. signal PDFs.') p.add_argument( '--cap-ratio', action='store_true', @@ -430,12 +404,9 @@ def create_analysis( with tl.task_timer('Creating analysis.'): ana = create_analysis( - rss_pdf, datasets, source, - cache_dir=args.cache_dir, cap_ratio=args.cap_ratio, - n_mc_events=args.n_mc_events, gamma_seed=args.gamma_seed, tl=tl) From 50f31662bf4a3b40b170b47cc5369e5d59c5fadb Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 17 Aug 2022 18:38:46 +0200 Subject: [PATCH 135/274] Use binning info --- skyllh/analyses/i3/publicdata_ps/pdfratio.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pdfratio.py b/skyllh/analyses/i3/publicdata_ps/pdfratio.py index 996f1677ed..611e80d6f7 100644 --- a/skyllh/analyses/i3/publicdata_ps/pdfratio.py +++ b/skyllh/analyses/i3/publicdata_ps/pdfratio.py @@ -39,7 +39,8 @@ def __init__(self, sig_pdf_set, bkg_pdf, cap_ratio=False, **kwargs): # Get the log10 reco energy values where the background pdf has # non-zero values. - (n_logE, n_sinDec) = bkg_pdf._hist_logE_sinDec.shape + n_logE = bkg_pdf.get_binning('log_energy').nbins + n_sinDec = bkg_pdf.get_binning('sin_dec').nbins bd = bkg_pdf._hist_logE_sinDec > 0 log10_e_bc = bkg_pdf.get_binning('log_energy').bincenters self.ratio_fill_value_dict = dict() From 0fe45d0ba8065672fe4beb474494406e6f06f7f7 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 17 Aug 2022 18:39:25 +0200 Subject: [PATCH 136/274] cleanup --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index bfad507576..cb94e891d4 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -383,7 +383,8 @@ def create_analysis( ('PublicData_10y_ps', 'IC59'), ('PublicData_10y_ps', 'IC79'), ('PublicData_10y_ps', 'IC86_I'), - ('PublicData_10y_ps', 'IC86_II-VII') + ('PublicData_10y_ps', 'IC86_II-VII'), + #('PublicData_10y_ps', 'IC86_II'), ] datasets = [] @@ -394,8 +395,8 @@ def create_analysis( datasets.append(dsc.get_dataset(season)) # Define a random state service. - rss_pdf = RandomStateService(args.pdf_seed) rss = RandomStateService(args.seed) + # Define the point source. source = PointLikeSource(np.deg2rad(args.ra), np.deg2rad(args.dec)) print('source: ', str(source)) From 9da0493fbfe927492b8a0d14074f96a7155c745c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 17 Aug 2022 18:40:16 +0200 Subject: [PATCH 137/274] Add bkg pdf data file definition --- skyllh/datasets/i3/PublicData_10y_ps.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index b1bf0f4864..5ae16e97cf 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -388,6 +388,8 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): 'smearing_datafile', 'irfs/IC86_II_smearing.csv') IC86_II.add_aux_data_definition( 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') + IC86_II.add_aux_data_definition( + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_sindecmu_log10emu_IC86_II.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), From 405501feb0cbda7782c32dccc3475ee627dd13c3 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 17 Aug 2022 18:41:41 +0200 Subject: [PATCH 138/274] Add MC background energy pdf class --- .../i3/publicdata_ps/backgroundpdf.py | 192 ++++++++++++++---- 1 file changed, 150 insertions(+), 42 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py index c1449b9cd3..70c4ef9eec 100644 --- a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py @@ -2,7 +2,10 @@ import numpy as np -from skyllh.core.binning import UsesBinning +from skyllh.core.binning import ( + BinningDefinition, + UsesBinning, +) from skyllh.core.pdf import ( EnergyPDF, IsBackgroundPDF, @@ -177,34 +180,34 @@ def __init__(self, data_logE, data_sinDec, data_mcweight, data_physicsweight, self._hist_logE_sinDec = h - @ property + @property def hist_smoothing_method(self): """The HistSmoothingMethod instance defining the smoothing filter of the energy PDF histogram. """ return self._hist_smoothing_method - @ hist_smoothing_method.setter + @hist_smoothing_method.setter def hist_smoothing_method(self, method): if(not isinstance(method, HistSmoothingMethod)): raise TypeError( 'The hist_smoothing_method property must be an instance of HistSmoothingMethod!') self._hist_smoothing_method = method - @ property + @property def hist(self): """(read-only) The 2D logE-sinDec histogram array. """ return self._hist_logE_sinDec - @ property + @property def hist_mask_mc_covered(self): """(read-only) The boolean ndarray holding the mask of the 2D histogram bins for which there is monte-carlo coverage. """ return self._hist_mask_mc_covered - @ property + @property def hist_mask_mc_covered_zero_physics(self): """(read-only) The boolean ndarray holding the mask of the 2D histogram bins for which there is monte-carlo coverage but zero physics @@ -212,7 +215,7 @@ def hist_mask_mc_covered_zero_physics(self): """ return self._hist_mask_mc_covered_zero_physics - @ property + @property def hist_mask_mc_covered_with_physics(self): """(read-only) The boolean ndarray holding the mask of the 2D histogram bins for which there is monte-carlo coverage and has physics @@ -220,39 +223,6 @@ def hist_mask_mc_covered_with_physics(self): """ return self._hist_mask_mc_covered & ~self._hist_mask_mc_covered_zero_physics - def assert_is_valid_for_exp_data(self, data_exp): - """Checks if this energy PDF is valid for all the given experimental - data. - It checks if all the data is within the logE and sin(dec) binning range. - - Parameters - ---------- - data_exp : numpy record ndarray - The array holding the experimental data. The following data fields - must exist: - - - 'log_energy' : float - The logarithm of the energy value of the data event. - - 'dec' : float - The declination of the data event. - - Raises - ------ - ValueError - If some of the data is outside the logE or sin(dec) binning range. - """ - logE_binning = self.get_binning('log_energy') - sinDec_binning = self.get_binning('sin_dec') - - exp_logE = data_exp['log_energy'] - exp_sinDec = np.sin(data_exp['dec']) - - # Check if all the data is within the binning range. - # if(logE_binning.any_data_out_of_binning_range(exp_logE)): - # self.logger.warning('Some data is outside the logE range (%.3f, %.3f)', logE_binning.lower_edge, logE_binning.upper_edge) - # if(sinDec_binning.any_data_out_of_binning_range(exp_sinDec)): - # self.logger.warning('Some data is outside the sin(dec) range (%.3f, %.3f)', sinDec_binning.lower_edge, sinDec_binning.upper_edge) - def get_prob(self, tdm, fitparams=None, tl=None): """Calculates the energy probability (in logE) of each event. @@ -340,5 +310,143 @@ def __init__(self, data_exp, logE_binning, sinDec_binning, data_logE, data_sinDec, data_mcweight, data_physicsweight, logE_binning, sinDec_binning, smoothing_filter, kde_smoothing ) - # Check if this PDF is valid for all the given experimental data. - self.assert_is_valid_for_exp_data(data_exp) + + +class PDMCBackgroundI3EnergyPDF(EnergyPDF, IsBackgroundPDF, UsesBinning): + """This class provides a background energy PDF constructed from the public + data and a monte-carlo background flux model. + """ + def __init__( + self, pdf_sindecmu_log10emu, sindecmu_binning, log10emu_binning, + **kwargs): + """Constructs a new background energy PDF with the given PDF data and + binning. + + Parameters + ---------- + pdf_sindecmu_log10emu : 2D numpy ndarray + The (n_sindecmu, n_log10emu)-shaped 2D numpy ndarray holding the + PDF values in unit 1/log10(E_mu/GeV). + A copy of this data will be created and held within this class + instance. + sindecmu_binning : BinningDefinition + The binning definition for the binning in sin(dec_mu). + log10emu_binning : BinningDefinition + The binning definition for the binning in log10(E_mu/GeV). + """ + if not isinstance(pdf_sindecmu_log10emu, np.ndarray): + raise TypeError( + 'The pdf_sindecmu_log10emu argument must be an instance of ' + 'numpy.ndarray!') + if not isinstance(sindecmu_binning, BinningDefinition): + raise TypeError( + 'The sindecmu_binning argument must be an instance of ' + 'BinningDefinition!') + if not isinstance(log10emu_binning, BinningDefinition): + raise TypeError( + 'The log10emu_binning argument must be an instance of ' + 'BinningDefinition!') + + super().__init__(**kwargs) + + self.add_axis(PDFAxis( + log10emu_binning.name, + log10emu_binning.lower_edge, + log10emu_binning.upper_edge, + )) + + self.add_axis(PDFAxis( + sindecmu_binning.name, + sindecmu_binning.lower_edge, + sindecmu_binning.upper_edge, + )) + + self._hist_logE_sinDec = np.copy(pdf_sindecmu_log10emu).T + self.add_binning(log10emu_binning, name='log_energy') + self.add_binning(sindecmu_binning, name='sin_dec') + + def assert_is_valid_for_trial_data(self, tdm): + """Checks if this PDF covers the entire value range of the trail + data events. + + Parameters + ---------- + tdm : TrialDataManager instance + The TrialDataManager instance holding the data events. + The following data fields need to exist: + + 'sin_dec' + + 'log_energy' + + Raises + ------ + ValueError + If parts of the trial data is outside the value range of this + PDF. + """ + sindecmu = tdm.get_data('sin_dec') + if np.min(sindecmu) < self.get_axis(0).vmin: + raise ValueError( + 'The minimum sindecmu value %e of the trial data is lower ' + 'than the minimum value of the PDF %e!' % ( + np.min(sindecmu), self.get_axis(0).vmin)) + if np.max(sindecmu) > self.get_axis(0).vmax: + raise ValueError( + 'The maximum sindecmu value %e of the trial data is larger ' + 'than the maximum value of the PDF %e!' % ( + np.max(sindecmu), self.get_axis(0).vmax)) + + log10emu = tdm.get_data('log_energy') + if np.min(log10emu) < self.get_axis(1).vmin: + raise ValueError( + 'The minimum log10emu value %e of the trial data is lower ' + 'than the minimum value of the PDF %e!' % ( + np.min(log10emu), self.get_axis(1).vmin)) + if np.max(log10emu) > self.get_axis(1).vmax: + raise ValueError( + 'The maximum log10emu value %e of the trial data is larger ' + 'than the maximum value of the PDF %e!' % ( + np.max(log10emu), self.get_axis(1).vmax)) + + def get_prob(self, tdm, params=None, tl=None): + """Gets the probability density for the given trial data events. + + Parameters + ---------- + tdm : TrialDataManager instance + The TrialDataManager instance holding the data events. + The following data fields need to exist: + + 'sin_dec' + + 'log_energy' + + params : dict | None + The dictionary containing the parameter names and values for which + the probability should get calculated. + By definition of this PDF, this is ``Ǹone``, because this PDF does + not depend on any parameters. + tl : TimeLord instance | None + The optional TimeLord instance that should be used to measure + timing information. + + Returns + ------- + prob : (N_events,)-shaped numpy ndarray + The 1D numpy ndarray with the probability density for each event. + """ + get_data = tdm.get_data + + log10emu = get_data('log_energy') + sindecmu = get_data('sin_dec') + + log10emu_idxs = np.digitize( + log10emu, self.get_binning('log_energy').binedges) - 1 + sindecmu_idxs = np.digitize( + sindecmu, self.get_binning('sin_dec').binedges) - 1 + + with TaskTimer(tl, 'Evaluating sindecmu-log10emu PDF.'): + pd = self._hist_logE_sinDec[(log10emu_idxs,sindecmu_idxs)] + + return pd From 6a9d31db718fe25dcb7d6f298d80d39fb4eb40b9 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 17 Aug 2022 18:42:24 +0200 Subject: [PATCH 139/274] Add analysis for using mc background energy pdf --- skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py | 472 +++++++++++++++++++ 1 file changed, 472 insertions(+) create mode 100644 skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py diff --git a/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py new file mode 100644 index 0000000000..9da17a7ac7 --- /dev/null +++ b/skyllh/analyses/i3/publicdata_ps/mcbkg_ps.py @@ -0,0 +1,472 @@ +# -*- coding: utf-8 -*- + +"""The trad_ps analysis is a multi-dataset time-integrated single source +analysis with a two-component likelihood function using a spacial and an energy +event PDF. +""" + +import argparse +import logging +import numpy as np +import os.path +import pickle + +from skyllh.core.progressbar import ProgressBar + +# Classes to define the source hypothesis. +from skyllh.physics.source import PointLikeSource +from skyllh.physics.flux import PowerLawFlux +from skyllh.core.source_hypo_group import SourceHypoGroup +from skyllh.core.source_hypothesis import SourceHypoGroupManager + +# Classes to define the fit parameters. +from skyllh.core.parameters import ( + SingleSourceFitParameterMapper, + FitParameter +) + +# Classes for the minimizer. +from skyllh.core.minimizer import Minimizer, LBFGSMinimizerImpl + +# Classes for utility functionality. +from skyllh.core.config import CFG +from skyllh.core.random import RandomStateService +from skyllh.core.optimize import SpatialBoxEventSelectionMethod +from skyllh.core.smoothing import BlockSmoothingFilter +from skyllh.core.timing import TimeLord +from skyllh.core.trialdata import TrialDataManager + +# Classes for defining the analysis. +from skyllh.core.test_statistic import TestStatisticWilks +from skyllh.core.analysis import ( + TimeIntegratedMultiDatasetSingleSourceAnalysis as Analysis +) + +# Classes to define the background generation. +from skyllh.core.scrambling import DataScrambler, UniformRAScramblingMethod +from skyllh.i3.background_generation import FixedScrambledExpDataI3BkgGenMethod + +# Classes to define the signal and background PDFs. +from skyllh.core.signalpdf import RayleighPSFPointSourceSignalSpatialPDF +from skyllh.i3.signalpdf import SignalI3EnergyPDFSet +from skyllh.i3.backgroundpdf import ( + DataBackgroundI3SpatialPDF, + DataBackgroundI3EnergyPDF +) +from skyllh.i3.pdfratio import ( + I3EnergySigSetOverBkgPDFRatioSpline +) +# Classes to define the spatial and energy PDF ratios. +from skyllh.core.pdfratio import ( + SpatialSigOverBkgPDFRatio, + Skylab2SkylabPDFRatioFillMethod +) + +from skyllh.i3.signal_generation import PointLikeSourceI3SignalGenerationMethod + +# Analysis utilities. +from skyllh.core.analysis_utils import ( + pointlikesource_to_data_field_array +) + +# Logging setup utilities. +from skyllh.core.debugging import ( + setup_logger, + setup_console_handler, + setup_file_handler +) + +# Pre-defined public IceCube data samples. +from skyllh.datasets.i3 import data_samples + +# Analysis specific classes for working with the public data. +from skyllh.analyses.i3.publicdata_ps.signal_generator import ( + PublicDataSignalGenerator +) +from skyllh.analyses.i3.publicdata_ps.detsigyield import ( + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod +) +from skyllh.analyses.i3.publicdata_ps.signalpdf import ( + PDSignalEnergyPDFSet +) +from skyllh.analyses.i3.publicdata_ps.backgroundpdf import ( + PDMCBackgroundI3EnergyPDF +) +from skyllh.analyses.i3.publicdata_ps.pdfratio import ( + PDPDFRatio +) + + +def psi_func(tdm, src_hypo_group_manager, fitparams): + """Function to calculate the opening angle between the source position + and the event's reconstructed position. + """ + ra = tdm.get_data('ra') + dec = tdm.get_data('dec') + + # Make the source position angles two-dimensional so the PDF value + # can be calculated via numpy broadcasting automatically for several + # sources. This is useful for stacking analyses. + src_ra = tdm.get_data('src_array')['ra'][:, np.newaxis] + src_dec = tdm.get_data('src_array')['dec'][:, np.newaxis] + + delta_dec = np.abs(dec - src_dec) + delta_ra = np.abs(ra - src_ra) + x = ( + (np.sin(delta_dec / 2.))**2. + np.cos(dec) * + np.cos(src_dec) * (np.sin(delta_ra / 2.))**2. + ) + + # Handle possible floating precision errors. + x[x < 0.] = 0. + x[x > 1.] = 1. + + psi = (2.0*np.arcsin(np.sqrt(x))) + + # For now we support only a single source, hence return psi[0]. + return psi[0, :] + + +def TXS_location(): + src_ra = np.radians(77.358) + src_dec = np.radians(5.693) + return (src_ra, src_dec) + + +def create_analysis( + rss, + datasets, + source, + refplflux_Phi0=1, + refplflux_E0=1e3, + refplflux_gamma=2, + ns_seed=10.0, + gamma_seed=3, + cache_dir='.', + cap_ratio=False, + compress_data=False, + keep_data_fields=None, + optimize_delta_angle=10, + efficiency_mode=None, + tl=None, + ppbar=None +): + """Creates the Analysis instance for this particular analysis. + + Parameters: + ----------- + datasets : list of Dataset instances + The list of Dataset instances, which should be used in the + analysis. + source : PointLikeSource instance + The PointLikeSource instance defining the point source position. + refplflux_Phi0 : float + The flux normalization to use for the reference power law flux model. + refplflux_E0 : float + The reference energy to use for the reference power law flux model. + refplflux_gamma : float + The spectral index to use for the reference power law flux model. + ns_seed : float + Value to seed the minimizer with for the ns fit. + gamma_seed : float | None + Value to seed the minimizer with for the gamma fit. If set to None, + the refplflux_gamma value will be set as gamma_seed. + cache_dir : str + The cache directory where to look for cached data, e.g. signal PDFs. + compress_data : bool + Flag if the data should get converted from float64 into float32. + keep_data_fields : list of str | None + List of additional data field names that should get kept when loading + the data. + optimize_delta_angle : float + The delta angle in degrees for the event selection optimization methods. + efficiency_mode : str | None + The efficiency mode the data should get loaded with. Possible values + are: + + - 'memory': + The data will be load in a memory efficient way. This will + require more time, because all data records of a file will + be loaded sequentially. + - 'time': + The data will be loaded in a time efficient way. This will + require more memory, because each data file gets loaded in + memory at once. + + The default value is ``'time'``. If set to ``None``, the default + value will be used. + tl : TimeLord instance | None + The TimeLord instance to use to time the creation of the analysis. + ppbar : ProgressBar instance | None + The instance of ProgressBar for the optional parent progress bar. + + Returns + ------- + analysis : SpatialEnergyTimeIntegratedMultiDatasetSingleSourceAnalysis + The Analysis instance for this analysis. + """ + # Define the flux model. + flux_model = PowerLawFlux( + Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) + + # Define the fit parameter ns. + fitparam_ns = FitParameter('ns', 0, 1e3, ns_seed) + + # Define the gamma fit parameter. + fitparam_gamma = FitParameter( + 'gamma', valmin=1, valmax=5, initial=gamma_seed) + + # Define the detector signal efficiency implementation method for the + # IceCube detector and this source and flux_model. + # The sin(dec) binning will be taken by the implementation method + # automatically from the Dataset instance. + gamma_grid = fitparam_gamma.as_linear_grid(delta=0.1) + detsigyield_implmethod = \ + PublicDataPowerLawFluxPointLikeSourceI3DetSigYieldImplMethod( + gamma_grid) + + # Define the signal generation method. + #sig_gen_method = PointLikeSourceI3SignalGenerationMethod() + sig_gen_method = None + + # Create a source hypothesis group manager. + src_hypo_group_manager = SourceHypoGroupManager( + SourceHypoGroup( + source, flux_model, detsigyield_implmethod, sig_gen_method)) + + # Create a source fit parameter mapper and define the fit parameters. + src_fitparam_mapper = SingleSourceFitParameterMapper() + src_fitparam_mapper.def_fit_parameter(fitparam_gamma) + + # Define the test statistic. + test_statistic = TestStatisticWilks() + + # Define the data scrambler with its data scrambling method, which is used + # for background generation. + data_scrambler = DataScrambler(UniformRAScramblingMethod()) + + # Create background generation method. + bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) + + # Create the minimizer instance. + minimizer = Minimizer(LBFGSMinimizerImpl()) + + # Create the Analysis instance. + analysis = Analysis( + src_hypo_group_manager, + src_fitparam_mapper, + fitparam_ns, + test_statistic, + bkg_gen_method, + custom_sig_generator=PublicDataSignalGenerator + ) + + # Define the event selection method for pure optimization purposes. + # We will use the same method for all datasets. + event_selection_method = SpatialBoxEventSelectionMethod( + src_hypo_group_manager, delta_angle=np.deg2rad(optimize_delta_angle)) + #event_selection_method = None + + # Add the data sets to the analysis. + pbar = ProgressBar(len(datasets), parent=ppbar).start() + for ds in datasets: + # Load the data of the data set. + data = ds.load_and_prepare_data( + keep_fields=keep_data_fields, + compress=compress_data, + efficiency_mode=efficiency_mode, + tl=tl) + + # Create a trial data manager and add the required data fields. + tdm = TrialDataManager() + tdm.add_source_data_field('src_array', + pointlikesource_to_data_field_array) + tdm.add_data_field('psi', psi_func) + + sin_dec_binning = ds.get_binning_definition('sin_dec') + log_energy_binning = ds.get_binning_definition('log_energy') + + # Create the spatial PDF ratio instance for this dataset. + spatial_sigpdf = RayleighPSFPointSourceSignalSpatialPDF( + dec_range=np.arcsin(sin_dec_binning.range)) + spatial_bkgpdf = DataBackgroundI3SpatialPDF( + data.exp, sin_dec_binning) + spatial_pdfratio = SpatialSigOverBkgPDFRatio( + spatial_sigpdf, spatial_bkgpdf) + + # Create the energy PDF ratio instance for this dataset. + energy_sigpdfset = PDSignalEnergyPDFSet( + ds=ds, + src_dec=source.dec, + flux_model=flux_model, + fitparam_grid_set=gamma_grid, + ppbar=ppbar + ) + + #smoothing_filter = BlockSmoothingFilter(nbins=1) + #energy_bkgpdf = DataBackgroundI3EnergyPDF( + # data.exp, log_energy_binning, sin_dec_binning, smoothing_filter) + + bkg_pdf_pathfilename = ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('pdf_bkg_datafile'))[0] + with open(bkg_pdf_pathfilename, 'rb') as f: + bkg_pdf_data = pickle.load(f) + energy_bkgpdf = PDMCBackgroundI3EnergyPDF( + pdf_sindecmu_log10emu=bkg_pdf_data['pdf'], + sindecmu_binning=bkg_pdf_data['sindecmu_binning'], + log10emu_binning=bkg_pdf_data['log10emu_binning'] + ) + + energy_pdfratio = PDPDFRatio( + sig_pdf_set=energy_sigpdfset, + bkg_pdf=energy_bkgpdf, + cap_ratio=cap_ratio + ) + + pdfratios = [spatial_pdfratio, energy_pdfratio] + + analysis.add_dataset( + ds, data, pdfratios, tdm, event_selection_method) + + pbar.increment() + pbar.finish() + + analysis.llhratio = analysis.construct_llhratio(minimizer, ppbar=ppbar) + + # analysis.construct_signal_generator() + + return analysis + + +if(__name__ == '__main__'): + p = argparse.ArgumentParser( + description='Calculates TS for a given source location using the ' + '10-year public point source sample.', + formatter_class=argparse.RawTextHelpFormatter + ) + p.add_argument( + '--dec', + default=23.8, + type=float, + help='The source declination in degrees.' + ) + p.add_argument( + '--ra', + default=216.76, + type=float, + help='The source right-ascention in degrees.' + ) + p.add_argument( + '--gamma-seed', + default=3, + type=float, + help='The seed value of the gamma fit parameter.' + ) + p.add_argument( + '--data_base_path', + default=None, + type=str, + help='The base path to the data samples (default=None)' + ) + p.add_argument( + '--pdf-seed', + default=1, + type=int, + help='The random number generator seed for generating the ' + 'signal PDF.' + ) + p.add_argument( + '--seed', + default=1, + type=int, + help='The random number generator seed for the likelihood ' + 'minimization.' + ) + p.add_argument( + '--ncpu', + default=1, + type=int, + help='The number of CPUs to utilize where parallelization is possible.' + ) + p.add_argument( + '--cache-dir', + default='.', + type=str, + help='The cache directory to look for cached data, e.g. signal PDFs.') + p.add_argument( + '--cap-ratio', + action='store_true', + help='Switch to cap the energy PDF ratio.') + p.set_defaults(cap_ratio=False) + args = p.parse_args() + + # Setup `skyllh` package logging. + # To optimize logging set the logging level to the lowest handling level. + setup_logger('skyllh', logging.DEBUG) + log_format = '%(asctime)s %(processName)s %(name)s %(levelname)s: '\ + '%(message)s' + setup_console_handler('skyllh', logging.INFO, log_format) + setup_file_handler('skyllh', 'debug.log', + log_level=logging.DEBUG, + log_format=log_format) + + CFG['multiproc']['ncpu'] = args.ncpu + + sample_seasons = [ + #('PublicData_10y_ps', 'IC40'), + #('PublicData_10y_ps', 'IC59'), + #('PublicData_10y_ps', 'IC79'), + #('PublicData_10y_ps', 'IC86_I'), + ('PublicData_10y_ps', 'IC86_II'), + #('PublicData_10y_ps', 'IC86_II-VII') + ] + + datasets = [] + for (sample, season) in sample_seasons: + # Get the dataset from the correct dataset collection. + dsc = data_samples[sample].create_dataset_collection( + args.data_base_path) + datasets.append(dsc.get_dataset(season)) + + # Define a random state service. + rss_pdf = RandomStateService(args.pdf_seed) + rss = RandomStateService(args.seed) + # Define the point source. + source = PointLikeSource(np.deg2rad(args.ra), np.deg2rad(args.dec)) + print('source: ', str(source)) + + tl = TimeLord() + + with tl.task_timer('Creating analysis.'): + ana = create_analysis( + rss_pdf, + datasets, + source, + cache_dir=args.cache_dir, + cap_ratio=args.cap_ratio, + gamma_seed=args.gamma_seed, + tl=tl) + + with tl.task_timer('Unblinding data.'): + (TS, fitparam_dict, status) = ana.unblind(rss) + + print('TS = %g' % (TS)) + print('ns_fit = %g' % (fitparam_dict['ns'])) + print('gamma_fit = %g' % (fitparam_dict['gamma'])) + + + # Generate some signal events. + #ana.construct_signal_generator() + #with tl.task_timer('Generating signal events.'): + # (n_sig, signal_events_dict) =\ + # ana.sig_generator.generate_signal_events(rss, 100) + + #trials = ana.do_trials( + # rss, 100, mean_n_sig=20 + #) + + #print('n_sig: %d'%n_sig) + #print('signal datasets: '+str(signal_events_dict.keys())) + + + print(tl) From a41ad072c278c13639acc2285c8f5e2ea9d16dee Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Wed, 31 Aug 2022 13:02:22 +0200 Subject: [PATCH 140/274] Generate the signal energy PDF only in the neutrino energy bins where the reco energy is not zero. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 4 +- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 39 ++++++++++++------- 2 files changed, 28 insertions(+), 15 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 9216127985..2a9cfbc7b5 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -331,8 +331,8 @@ def get_detection_prob_for_decnu( uidx -= 1 aeff = self.get_aeff_for_decnu(decnu) - aeff = aeff[lidx:uidx+1] - enu_binedges = enu_binedges[lidx:uidx+2] + aeff = aeff[lidx+1:uidx+1] + enu_binedges = enu_binedges[lidx+1:uidx+2] dE = np.diff(enu_binedges) diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 74b704d517..cdbce4143d 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -1286,24 +1286,37 @@ def __init__( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) - # Select the slice of the smearing matrixcorresponding to the + # Select the slice of the smearing matrix corresponding to the # source declination band. # Note that we take the pdfs of the reconstruction calculated # from the smearing matrix here. true_dec_idx = sm.get_true_dec_idx(src_dec) sm_pdf = sm.pdf[:, true_dec_idx] - - true_enu_binedges = np.power(10, sm.log10_true_enu_binedges) + + # Only look at true neutrino energies for which a recostructed + # muon energy distribution exists in the smearing matrix. + (min_log_true_e, + max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( + true_dec_idx) + log_true_e_mask = np.logical_and( + sm.log10_true_enu_binedges >= min_log_true_e, + sm.log10_true_enu_binedges <= max_log_true_e) + true_enu_binedges = np.power( + 10, sm.log10_true_enu_binedges[log_true_e_mask]) true_enu_binedges_lower = true_enu_binedges[:-1] true_enu_binedges_upper = true_enu_binedges[1:] - nbins_true_e = len(true_enu_binedges) - 1 + valid_true_e_idxs = [sm.get_log10_true_e_idx(0.5 * (he + le)) + for he,le in zip( + sm.log10_true_enu_binedges[log_true_e_mask][1:], + sm.log10_true_enu_binedges[log_true_e_mask][:-1]) + ] # Define the values at which to evaluate the splines. # Some bins might have zero bin widths. - m = (sm.log10_reco_e_binedges_upper[:, true_dec_idx] - - sm.log10_reco_e_binedges_lower[:, true_dec_idx]) > 0 - le = sm.log10_reco_e_binedges_lower[:, true_dec_idx][m] - ue = sm.log10_reco_e_binedges_upper[:, true_dec_idx][m] + m = (sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx] - + sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx]) > 0 + le = sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx][m] + ue = sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx][m] min_log10_reco_e = np.min(le) max_log10_reco_e = np.max(ue) d_log10_reco_e = np.min(ue - le) / 20 @@ -1361,7 +1374,7 @@ def create_energy_pdf(sm_pdf, flux_model, gridfitparams): self._logger.debug( 'Generate signal energy PDF for parameters {} in {} E_nu ' 'bins.'.format( - gridfitparams, nbins_true_e) + gridfitparams, len(valid_true_e_idxs)) ) # Calculate the flux probability p(E_nu|gamma). @@ -1393,7 +1406,7 @@ def create_energy_pdf(sm_pdf, flux_model, gridfitparams): 'true_e_prob = {}'.format( true_e_prob)) - def create_reco_e_pdf_for_true_e(true_e_idx): + def create_reco_e_pdf_for_true_e(idx, true_e_idx): """This functions creates a spline for the reco energy distribution given a true neutrino engery. """ @@ -1412,7 +1425,7 @@ def create_reco_e_pdf_for_true_e(true_e_idx): log10_reco_e_binedges = sm.log10_reco_e_binedges[ true_e_idx, true_dec_idx] - p = f_e * true_e_prob[true_e_idx] + p = f_e * true_e_prob[idx] spline = FctSpline1D(p, log10_reco_e_binedges) @@ -1420,8 +1433,8 @@ def create_reco_e_pdf_for_true_e(true_e_idx): # Integrate over the true neutrino energy and spline the output. sum_pdf = np.sum([ - create_reco_e_pdf_for_true_e(true_e_idx) - for true_e_idx in range(nbins_true_e) + create_reco_e_pdf_for_true_e(i, true_e_idx) + for i,true_e_idx in enumerate(valid_true_e_idxs) ], axis=0) spline = FctSpline1D(sum_pdf, xvals_binedges, norm=True) From 5aa3d803dd8d433d9c59bb1ef0bc636b06960e8c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 2 Sep 2022 15:10:50 +0200 Subject: [PATCH 141/274] exchange PDF axes --- .../analyses/i3/publicdata_ps/backgroundpdf.py | 16 ++++++++-------- skyllh/datasets/i3/PublicData_10y_ps.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py index 70c4ef9eec..f2113b8206 100644 --- a/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/backgroundpdf.py @@ -317,26 +317,26 @@ class PDMCBackgroundI3EnergyPDF(EnergyPDF, IsBackgroundPDF, UsesBinning): data and a monte-carlo background flux model. """ def __init__( - self, pdf_sindecmu_log10emu, sindecmu_binning, log10emu_binning, + self, pdf_log10emu_sindecmu, log10emu_binning, sindecmu_binning, **kwargs): """Constructs a new background energy PDF with the given PDF data and binning. Parameters ---------- - pdf_sindecmu_log10emu : 2D numpy ndarray - The (n_sindecmu, n_log10emu)-shaped 2D numpy ndarray holding the + pdf_log10emu_sindecmu : 2D numpy ndarray + The (n_log10emu, n_sindecmu)-shaped 2D numpy ndarray holding the PDF values in unit 1/log10(E_mu/GeV). A copy of this data will be created and held within this class instance. - sindecmu_binning : BinningDefinition - The binning definition for the binning in sin(dec_mu). log10emu_binning : BinningDefinition The binning definition for the binning in log10(E_mu/GeV). + sindecmu_binning : BinningDefinition + The binning definition for the binning in sin(dec_mu). """ - if not isinstance(pdf_sindecmu_log10emu, np.ndarray): + if not isinstance(pdf_log10emu_sindecmu, np.ndarray): raise TypeError( - 'The pdf_sindecmu_log10emu argument must be an instance of ' + 'The pdf_log10emu_sindecmu argument must be an instance of ' 'numpy.ndarray!') if not isinstance(sindecmu_binning, BinningDefinition): raise TypeError( @@ -361,7 +361,7 @@ def __init__( sindecmu_binning.upper_edge, )) - self._hist_logE_sinDec = np.copy(pdf_sindecmu_log10emu).T + self._hist_logE_sinDec = np.copy(pdf_log10emu_sindecmu) self.add_binning(log10emu_binning, name='log_energy') self.add_binning(sindecmu_binning, name='sin_dec') diff --git a/skyllh/datasets/i3/PublicData_10y_ps.py b/skyllh/datasets/i3/PublicData_10y_ps.py index 5ae16e97cf..b64161c7aa 100644 --- a/skyllh/datasets/i3/PublicData_10y_ps.py +++ b/skyllh/datasets/i3/PublicData_10y_ps.py @@ -389,7 +389,7 @@ def create_dataset_collection(base_path=None, sub_path_fmt=None): IC86_II.add_aux_data_definition( 'mceq_flux_datafile', 'fluxes/mceq_IC86_II.pkl') IC86_II.add_aux_data_definition( - 'pdf_bkg_datafile', 'pdfs/pdf_bkg_sindecmu_log10emu_IC86_II.pkl') + 'pdf_bkg_datafile', 'pdfs/pdf_bkg_log10emu_sindecmu_IC86_II.pkl') sin_dec_bins = np.unique(np.concatenate([ np.linspace(-1., -0.93, 4 + 1), From b1981dc05499e9f72f6c6db4c5412d0282dc1a4c Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 2 Sep 2022 15:52:39 +0200 Subject: [PATCH 142/274] Use the correct bin indices for the spline representation --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 2a9cfbc7b5..2c835f9597 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -331,8 +331,8 @@ def get_detection_prob_for_decnu( uidx -= 1 aeff = self.get_aeff_for_decnu(decnu) - aeff = aeff[lidx+1:uidx+1] - enu_binedges = enu_binedges[lidx+1:uidx+2] + aeff = aeff[lidx:uidx+1] + enu_binedges = enu_binedges[lidx:uidx+2] dE = np.diff(enu_binedges) @@ -352,8 +352,6 @@ def get_detection_prob_for_decnu( spl = interpolate.splrep( x, y, - xb=enu_range_min, - xe=enu_range_max, k=1, s=0 ) From 7b9c943e3dd7b89466804a160a8dd1ff6fe0a00d Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 5 Sep 2022 10:18:06 +0200 Subject: [PATCH 143/274] Cache smearing matrix and effective area for signal trials generation. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 17 ++++ .../i3/publicdata_ps/signal_generator.py | 96 +++++++++++-------- 2 files changed, 73 insertions(+), 40 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 2c835f9597..90baa0f7ac 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -131,6 +131,23 @@ def __init__( self._log10_enu_binedges_upper[-1:]) ) + src_dec = kwargs.pop('src_dec', None) + min_log_e = kwargs.pop('min_log_e', None) + max_log_e = kwargs.pop('max_log_e', None) + if (src_dec is not None) and (min_log_e is not None) and (max_log_e is not None): + m = (self.log10_enu_bincenters >= min_log_e) & ( + self.log10_enu_bincenters < max_log_e) + bin_centers = self.log10_enu_bincenters[m] + low_bin_edges = self._log10_enu_binedges_lower[m] + high_bin_edges = self._log10_enu_binedges_upper[m] + # Detection probability P(E_nu | sin(dec)) per bin. + self.det_prob = np.empty((len(bin_centers),), dtype=np.double) + for i in range(len(bin_centers)): + self.det_prob[i] = self.get_detection_prob_for_decnu( + src_dec, + 10**low_bin_edges[i], 10**high_bin_edges[i], + 10**low_bin_edges[0], 10**high_bin_edges[-1]) + @property def decnu_binedges(self): """(read-only) The bin edges of the neutrino declination axis in diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index 5c8c906c2e..ceafe1023f 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -22,21 +22,38 @@ class PublicDataDatasetSignalGenerator(object): - def __init__(self, ds, **kwargs): + def __init__(self, ds, src_dec, cache_effA=None, cache_sm=None, **kwargs): """Creates a new instance of the signal generator for generating signal events from a specific public data dataset. """ super().__init__(**kwargs) - self.smearing_matrix = PublicDataSmearingMatrix( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('smearing_datafile'))) - - self.effA = PDAeff( - pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('eff_area_datafile'))) - - def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, + if cache_sm is None: + self.smearing_matrix = PublicDataSmearingMatrix( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('smearing_datafile'))) + else: + self.smearing_matrix = cache_sm + + if cache_effA is None: + dec_idx = self.smearing_matrix.get_true_dec_idx(src_dec) + (min_log_true_e, + max_log_true_e) = \ + self.smearing_matrix.get_true_log_e_range_with_valid_log_e_pdfs( + dec_idx) + kwargs = { + 'src_dec': src_dec, + 'min_log_e': min_log_true_e, + 'max_log_e': max_log_true_e + } + self.effA = PDAeff( + pathfilenames=ds.get_abs_pathfilename_list( + ds.get_aux_data_definition('eff_area_datafile')), **kwargs) + + else: + self.effA = cache_effA + + def _generate_inv_cdf_spline(self, flux_model, log_e_min, log_e_max): """Sample the true neutrino energy from the power-law re-weighted with the detection probability. @@ -54,16 +71,8 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, 10**low_bin_edges[0], 10**high_bin_edges[-1] ) - # Detection probability P(E_nu | sin(dec)) per bin. - det_prob = np.empty((len(bin_centers),), dtype=np.double) - for i in range(len(bin_centers)): - det_prob[i] = self.effA.get_detection_prob_for_decnu( - src_dec, - 10**low_bin_edges[i], 10**high_bin_edges[i], - 10**low_bin_edges[0], 10**high_bin_edges[-1]) - # Do the product and normalize again to a probability per bin. - product = flux_prob * det_prob + product = flux_prob * self.effA.det_prob prob_per_bin = product / np.sum(product) # The probability per bin cannot be zero, otherwise the cumulative @@ -85,8 +94,7 @@ def _generate_inv_cdf_spline(self, flux_model, src_dec, log_e_min, bin_centers = np.concatenate(([low_bin_edges[0]], bin_centers)) # Build a spline for the inverse CDF. - self.inv_cdf_spl = interpolate.splrep( - cum_per_bin, bin_centers, k=1, s=0) + return interpolate.splrep(cum_per_bin, bin_centers, k=1, s=0) @staticmethod def _eval_spline(x, spl): @@ -94,7 +102,8 @@ def _eval_spline(x, spl): return values def _generate_events( - self, rss, src_dec, src_ra, dec_idx, flux_model, n_events): + self, rss, src_dec, src_ra, dec_idx, + log_true_e_inv_cdf_spl, n_events): """Generates `n_events` signal events for the given source location and flux model. @@ -149,17 +158,8 @@ def _generate_events( sm = self.smearing_matrix - # Determine the true energy range for which log_e PDFs are available. - (min_log_true_e, - max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( - dec_idx) - - # Build the spline for the inverse CDF and draw a true neutrino - # energy from the hypothesis spectrum. - self._generate_inv_cdf_spline(flux_model, src_dec, - min_log_true_e, max_log_true_e) log_true_e = self._eval_spline( - rss.random.uniform(size=n_events), self.inv_cdf_spl) + rss.random.uniform(size=n_events), log_true_e_inv_cdf_spl) events['log_true_energy'] = log_true_e @@ -225,13 +225,22 @@ def generate_signal_events( # Find the declination bin index. dec_idx = sm.get_true_dec_idx(src_dec) + # Determine the true energy range for which log_e PDFs are available. + (min_log_true_e, + max_log_true_e) = sm.get_true_log_e_range_with_valid_log_e_pdfs( + dec_idx) + # Build the spline for the inverse CDF and draw a true neutrino + # energy from the hypothesis spectrum. + log_true_e_inv_cdf_spl = self._generate_inv_cdf_spline( + flux_model, min_log_true_e, max_log_true_e) + events = None n_evt_generated = 0 while n_evt_generated != n_events: n_evt = n_events - n_evt_generated events_ = self._generate_events( - rss, src_dec, src_ra, dec_idx, flux_model, n_evt) + rss, src_dec, src_ra, dec_idx, log_true_e_inv_cdf_spl, n_evt) # Cut events that failed to be generated due to missing PDFs. events_ = events_[events_['isvalid']] @@ -255,10 +264,10 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhrati self.dataset_list = dataset_list self.data_list = data_list self.llhratio = llhratio - - self.sig_gen_list = [] - for ds in self._dataset_list: - self.sig_gen_list.append(PublicDataDatasetSignalGenerator(ds)) + self.cache_effA = list() + self.cache_sm = list() + [self.cache_effA.append(None) for i in self._dataset_list] + [self.cache_sm.append(None) for i in self._dataset_list] @property def src_hypo_group_manager(self): @@ -313,8 +322,7 @@ def generate_signal_events(self, rss, mean, poisson=True): # Each source hypo group can have a different power-law gamma = shg.fluxmodel.gamma weights, _ = self.llhratio.dataset_signal_weights([mean, gamma]) - src_list = shg.source_list - for (ds_idx, (sig_gen, w)) in enumerate(zip(self.sig_gen_list, weights)): + for (ds_idx, w) in enumerate(weights): w_mean = mean * w if(poisson): n_events = rss.random.poisson( @@ -331,7 +339,15 @@ def generate_signal_events(self, rss, mean, poisson=True): tot_n_events += n_events events_ = None - for (shg_src_idx, src) in enumerate(src_list): + for (shg_src_idx, src) in enumerate(shg.source_list): + ds = self._dataset_list[ds_idx] + sig_gen = PublicDataDatasetSignalGenerator( + ds, src.dec, self.cache_effA[ds_idx], + self.cache_sm[ds_idx]) + if self.cache_effA[ds_idx] is None: + self.cache_effA[ds_idx] = sig_gen.effA + if self.cache_sm[ds_idx] is None: + self.cache_sm[ds_idx] = sig_gen.smearing_matrix # ToDo: here n_events should be split according to some # source weight events_ = sig_gen.generate_signal_events( From 8379fff5cbbf0f190e184bad9fb1be71fe92ef50 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 5 Sep 2022 10:34:36 +0200 Subject: [PATCH 144/274] Small changes. --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 90baa0f7ac..598047516f 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -99,7 +99,8 @@ class PDAeff(object): the public data. """ def __init__( - self, pathfilenames, **kwargs): + self, pathfilenames, src_dec=None, min_log_e=None, max_log_e=None, + **kwargs): """Creates an effective area instance by loading the effective area data from the given file. """ @@ -131,9 +132,6 @@ def __init__( self._log10_enu_binedges_upper[-1:]) ) - src_dec = kwargs.pop('src_dec', None) - min_log_e = kwargs.pop('min_log_e', None) - max_log_e = kwargs.pop('max_log_e', None) if (src_dec is not None) and (min_log_e is not None) and (max_log_e is not None): m = (self.log10_enu_bincenters >= min_log_e) & ( self.log10_enu_bincenters < max_log_e) From 1465445db6cffaadfa2ba742d5ade3cb7919d635 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 5 Sep 2022 11:14:33 +0200 Subject: [PATCH 145/274] Style changes. --- .../i3/publicdata_ps/signal_generator.py | 39 ++++++++++++------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index ceafe1023f..caa863a9bd 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -22,20 +22,20 @@ class PublicDataDatasetSignalGenerator(object): - def __init__(self, ds, src_dec, cache_effA=None, cache_sm=None, **kwargs): + def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): """Creates a new instance of the signal generator for generating signal events from a specific public data dataset. """ super().__init__(**kwargs) - if cache_sm is None: + if sm is None: self.smearing_matrix = PublicDataSmearingMatrix( pathfilenames=ds.get_abs_pathfilename_list( ds.get_aux_data_definition('smearing_datafile'))) else: - self.smearing_matrix = cache_sm + self.smearing_matrix = sm - if cache_effA is None: + if effA is None: dec_idx = self.smearing_matrix.get_true_dec_idx(src_dec) (min_log_true_e, max_log_true_e) = \ @@ -51,7 +51,7 @@ def __init__(self, ds, src_dec, cache_effA=None, cache_sm=None, **kwargs): ds.get_aux_data_definition('eff_area_datafile')), **kwargs) else: - self.effA = cache_effA + self.effA = effA def _generate_inv_cdf_spline(self, flux_model, log_e_min, log_e_max): @@ -264,10 +264,8 @@ def __init__(self, src_hypo_group_manager, dataset_list, data_list=None, llhrati self.dataset_list = dataset_list self.data_list = data_list self.llhratio = llhratio - self.cache_effA = list() - self.cache_sm = list() - [self.cache_effA.append(None) for i in self._dataset_list] - [self.cache_sm.append(None) for i in self._dataset_list] + self.effA = [None] * len(self._dataset_list) + self.sm = [None] * len(self._dataset_list) @property def src_hypo_group_manager(self): @@ -311,6 +309,18 @@ def llhratio(self, llhratio): 'LLHRatio!') self._llhratio = llhratio + @property + def sig_gen_list(self): + """The list of PublicDataDatasetSignalGenerator instances for each dataset + """ + return self._sig_gen_list + + @sig_gen_list.setter + def sig_gen_list(self, sig_gen_list): + if(not issequenceof(sig_gen_list, PublicDataDatasetSignalGenerator)): + raise TypeError('The sig_gen_list property must be a sequence of ' + 'PublicDataDatasetSignalGenerator instances!') + def generate_signal_events(self, rss, mean, poisson=True): shg_list = self._src_hypo_group_manager.src_hypo_group_list @@ -342,12 +352,11 @@ def generate_signal_events(self, rss, mean, poisson=True): for (shg_src_idx, src) in enumerate(shg.source_list): ds = self._dataset_list[ds_idx] sig_gen = PublicDataDatasetSignalGenerator( - ds, src.dec, self.cache_effA[ds_idx], - self.cache_sm[ds_idx]) - if self.cache_effA[ds_idx] is None: - self.cache_effA[ds_idx] = sig_gen.effA - if self.cache_sm[ds_idx] is None: - self.cache_sm[ds_idx] = sig_gen.smearing_matrix + ds, src.dec, self.effA[ds_idx], self.sm[ds_idx]) + if self.effA[ds_idx] is None: + self.effA[ds_idx] = sig_gen.effA + if self.sm[ds_idx] is None: + self.sm[ds_idx] = sig_gen.smearing_matrix # ToDo: here n_events should be split according to some # source weight events_ = sig_gen.generate_signal_events( From 548c8bfff61ad203b81007452471bae1bcb4153f Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 5 Sep 2022 12:37:43 +0200 Subject: [PATCH 146/274] Add doc string and rename arguments to more precise names --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 40 ++++++++++++++++--- .../i3/publicdata_ps/signal_generator.py | 7 ++-- 2 files changed, 39 insertions(+), 8 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index 598047516f..b011b05087 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -99,10 +99,31 @@ class PDAeff(object): the public data. """ def __init__( - self, pathfilenames, src_dec=None, min_log_e=None, max_log_e=None, + self, pathfilenames, src_dec=None, + min_log10enu=None, max_log10enu=None, **kwargs): """Creates an effective area instance by loading the effective area data from the given file. + + Parameters + ---------- + pathfilenames : str | list of str + The path file names of the effective area data file(s) which should + be used for this public data effective area instance. + src_dec : float | None + The source declination in radians for which detection probabilities + should get pre-calculated using the ``get_detection_prob_for_decnu`` + method. + min_log10enu : float | None + The minimum log10(E_nu/GeV) value that should get used for + calculating the detection probability. + If None, the lowest available neutrino energy bin edge of the + effective area is used. + max_log10enu : float | None + The maximum log10(E_nu/GeV) value that should get used for + calculating the detection probability. + If None, the highest available neutrino energy bin edge of the + effective area is used. """ super().__init__(**kwargs) @@ -132,13 +153,22 @@ def __init__( self._log10_enu_binedges_upper[-1:]) ) - if (src_dec is not None) and (min_log_e is not None) and (max_log_e is not None): - m = (self.log10_enu_bincenters >= min_log_e) & ( - self.log10_enu_bincenters < max_log_e) + # Pre-calculate detection probabilities for certain neutrino + # declinations if requested. + if src_dec is not None: + if min_log10enu is None: + min_log10enu = self._log10_enu_binedges_lower[0] + if max_log10enu is None: + max_log10enu = self._log10_enu_binedges_upper[-1] + + m = ( + (self.log10_enu_bincenters >= min_log10enu) & + (self.log10_enu_bincenters < max_log10enu) + ) bin_centers = self.log10_enu_bincenters[m] low_bin_edges = self._log10_enu_binedges_lower[m] high_bin_edges = self._log10_enu_binedges_upper[m] - # Detection probability P(E_nu | sin(dec)) per bin. + # Get the detection probability P(E_nu | sin(dec)) per bin. self.det_prob = np.empty((len(bin_centers),), dtype=np.double) for i in range(len(bin_centers)): self.det_prob[i] = self.get_detection_prob_for_decnu( diff --git a/skyllh/analyses/i3/publicdata_ps/signal_generator.py b/skyllh/analyses/i3/publicdata_ps/signal_generator.py index caa863a9bd..f3587fcadc 100644 --- a/skyllh/analyses/i3/publicdata_ps/signal_generator.py +++ b/skyllh/analyses/i3/publicdata_ps/signal_generator.py @@ -43,12 +43,13 @@ def __init__(self, ds, src_dec, effA=None, sm=None, **kwargs): dec_idx) kwargs = { 'src_dec': src_dec, - 'min_log_e': min_log_true_e, - 'max_log_e': max_log_true_e + 'min_log10enu': min_log_true_e, + 'max_log10enu': max_log_true_e } self.effA = PDAeff( pathfilenames=ds.get_abs_pathfilename_list( - ds.get_aux_data_definition('eff_area_datafile')), **kwargs) + ds.get_aux_data_definition('eff_area_datafile')), + **kwargs) else: self.effA = effA From c3610f2b01a32f6b5397fed3ddc7a9104e77e47a Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 5 Sep 2022 14:30:42 +0200 Subject: [PATCH 147/274] Vectorize detection probability calculation function --- skyllh/analyses/i3/publicdata_ps/pd_aeff.py | 46 +++++++++++-------- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 18 ++++---- 2 files changed, 34 insertions(+), 30 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py index b011b05087..079e301fce 100644 --- a/skyllh/analyses/i3/publicdata_ps/pd_aeff.py +++ b/skyllh/analyses/i3/publicdata_ps/pd_aeff.py @@ -168,13 +168,13 @@ def __init__( bin_centers = self.log10_enu_bincenters[m] low_bin_edges = self._log10_enu_binedges_lower[m] high_bin_edges = self._log10_enu_binedges_upper[m] + # Get the detection probability P(E_nu | sin(dec)) per bin. - self.det_prob = np.empty((len(bin_centers),), dtype=np.double) - for i in range(len(bin_centers)): - self.det_prob[i] = self.get_detection_prob_for_decnu( - src_dec, - 10**low_bin_edges[i], 10**high_bin_edges[i], - 10**low_bin_edges[0], 10**high_bin_edges[-1]) + self.det_prob = self.get_detection_prob_for_decnu( + src_dec, + 10**low_bin_edges, 10**high_bin_edges, + 10**low_bin_edges[0], 10**high_bin_edges[-1] + ) @property def decnu_binedges(self): @@ -333,16 +333,16 @@ def get_aeff_for_decnu(self, decnu): def get_detection_prob_for_decnu( self, decnu, enu_min, enu_max, enu_range_min, enu_range_max): - """Calculates the detection probability for a given true neutrino energy - range for a given neutrino declination. + """Calculates the detection probability for given true neutrino energy + ranges for a given neutrino declination. Parameters ---------- decnu : float The neutrino declination in radians. - enu_min : float + enu_min : float | ndarray of float The minimum energy in GeV. - enu_max : float + enu_max : float | ndarray of float The maximum energy in GeV. enu_range_min : float The minimum energy in GeV of the entire energy range. @@ -351,8 +351,9 @@ def get_detection_prob_for_decnu( Returns ------- - det_prob : float - The neutrino energy detection probability. + det_prob : ndarray of float + The neutrino energy detection probabilities for the given true + enegry ranges. """ enu_binedges = np.power(10, self.log10_enu_binedges) @@ -412,15 +413,20 @@ def _eval_spl_func(x): full_output=1 )[0] - integral = integrate.quad( - _eval_spl_func, - enu_min, - enu_max, - limit=200, - full_output=1 - )[0] + enu_min = np.atleast_1d(enu_min) + enu_max = np.atleast_1d(enu_max) + + det_prob = np.empty((len(enu_min),), dtype=np.double) + for i in range(len(enu_min)): + integral = integrate.quad( + _eval_spl_func, + enu_min[i], + enu_max[i], + limit=200, + full_output=1 + )[0] - det_prob = integral / norm + det_prob[i] = integral / norm return det_prob diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index cdbce4143d..7eed80f8ca 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -1292,7 +1292,7 @@ def __init__( # from the smearing matrix here. true_dec_idx = sm.get_true_dec_idx(src_dec) sm_pdf = sm.pdf[:, true_dec_idx] - + # Only look at true neutrino energies for which a recostructed # muon energy distribution exists in the smearing matrix. (min_log_true_e, @@ -1342,15 +1342,13 @@ def __init__( # Calculate the detector's neutrino energy detection probability to # detect a neutrino of energy E_nu given a neutrino declination: # p(E_nu|dec) - det_prob = np.empty((len(d_enu),), dtype=np.double) - for i in range(len(d_enu)): - det_prob[i] = aeff.get_detection_prob_for_decnu( - decnu=src_dec, - enu_min=true_enu_binedges[i], - enu_max=true_enu_binedges[i+1], - enu_range_min=true_enu_binedges[0], - enu_range_max=true_enu_binedges[-1] - ) + det_prob = aeff.get_detection_prob_for_decnu( + decnu=src_dec, + enu_min=true_enu_binedges[:-1], + enu_max=true_enu_binedges[1:], + enu_range_min=true_enu_binedges[0], + enu_range_max=true_enu_binedges[-1] + ) self._logger.debug('det_prob = {}, sum = {}'.format( det_prob, np.sum(det_prob))) From 0df08ee781b10d48000b7501f8a20a82cd68b019 Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 6 Sep 2022 12:36:07 +0200 Subject: [PATCH 148/274] Added a minimizer implementation argument in create_analysis(). --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 28 ++++++++++++++++----- 1 file changed, 22 insertions(+), 6 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index cb94e891d4..480289fae1 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -24,7 +24,9 @@ ) # Classes for the minimizer. -from skyllh.core.minimizer import Minimizer, LBFGSMinimizerImpl +from skyllh.core.minimizer import ( + Minimizer, LBFGSMinimizerImpl, MinuitMinimizerImpl +) # Classes for utility functionality. from skyllh.core.config import CFG @@ -133,6 +135,7 @@ def create_analysis( ns_seed=10.0, gamma_seed=3, kde_smoothing=False, + minimizer_impl="LBFGS", cap_ratio=False, compress_data=False, keep_data_fields=None, @@ -161,8 +164,14 @@ def create_analysis( gamma_seed : float | None Value to seed the minimizer with for the gamma fit. If set to None, the refplflux_gamma value will be set as gamma_seed. - cache_dir : str - The cache directory where to look for cached data, e.g. signal PDFs. + kde_smoothing : bool | False + Apply a KDE-based smoothing to the data-driven backgroun pdf. + Default: False. + minimizer_impl : str | "LBFGS" + Minimizer implementation to be used. Supported options are "LBFGS" + (L-BFG-S minimizer used from the :mod:`scipy.optimize` module), or + "minuit" (Minuit minimizer used by the :mod:`iminuit` module). + Default: "LBFGS". compress_data : bool Flag if the data should get converted from float64 into float32. keep_data_fields : list of str | None @@ -195,6 +204,16 @@ def create_analysis( analysis : SpatialEnergyTimeIntegratedMultiDatasetSingleSourceAnalysis The Analysis instance for this analysis. """ + + # Create the minimizer instance. + if minimizer_impl == "LBFGS": + minimizer = Minimizer(LBFGSMinimizerImpl()) + elif minimizer_impl == "minuit": + minimizer = Minimizer(MinuitMinimizerImpl()) + else: + raise NameError(f"Minimizer implementation `{minimizer_impl}` is not " + "supported. Please use `LBFGS` or `minuit`.") + # Define the flux model. flux_model = PowerLawFlux( Phi0=refplflux_Phi0, E0=refplflux_E0, gamma=refplflux_gamma) @@ -238,9 +257,6 @@ def create_analysis( # Create background generation method. bkg_gen_method = FixedScrambledExpDataI3BkgGenMethod(data_scrambler) - # Create the minimizer instance. - minimizer = Minimizer(LBFGSMinimizerImpl()) - # Create the Analysis instance. analysis = Analysis( src_hypo_group_manager, From e1c6600f737585cff19843fb76d4c0fa3f88c43d Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Tue, 6 Sep 2022 12:45:00 +0200 Subject: [PATCH 149/274] Fixed iminuit minimizer import. --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 480289fae1..7e6e86cdae 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -24,9 +24,8 @@ ) # Classes for the minimizer. -from skyllh.core.minimizer import ( - Minimizer, LBFGSMinimizerImpl, MinuitMinimizerImpl -) +from skyllh.core.minimizer import Minimizer, LBFGSMinimizerImpl +from skyllh.core.minimizers.iminuit import IMinuitMinimizerImpl # Classes for utility functionality. from skyllh.core.config import CFG @@ -209,7 +208,7 @@ def create_analysis( if minimizer_impl == "LBFGS": minimizer = Minimizer(LBFGSMinimizerImpl()) elif minimizer_impl == "minuit": - minimizer = Minimizer(MinuitMinimizerImpl()) + minimizer = Minimizer(IMinuitMinimizerImpl()) else: raise NameError(f"Minimizer implementation `{minimizer_impl}` is not " "supported. Please use `LBFGS` or `minuit`.") From da1143fd2e1867ae236933c9d9b5a566d4b44919 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 6 Sep 2022 15:12:50 +0200 Subject: [PATCH 150/274] Add debug info --- skyllh/core/minimizer.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/skyllh/core/minimizer.py b/skyllh/core/minimizer.py index df88df9e38..f1d37b8eaf 100644 --- a/skyllh/core/minimizer.py +++ b/skyllh/core/minimizer.py @@ -1106,8 +1106,9 @@ def minimize(self, rss, fitparamset, func, args=None, kwargs=None): (fmin, grads) = func(xmin, *args) logger.debug( - '%s (%s): Minimized function: %d iterations, %d repetitions' % ( + '%s (%s): Minimized function: %d iterations, %d repetitions, ' + 'xmin=%s' % ( classname(self), classname(self._minimizer_impl), - self._minimizer_impl.get_niter(status), reps)) + self._minimizer_impl.get_niter(status), reps, str(xmin))) return (xmin, fmin, status) From 4819c27e497ce6114329615d3664bab2929c71cc Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 6 Sep 2022 15:13:41 +0200 Subject: [PATCH 151/274] Calculate fill ratio value only when needed --- skyllh/analyses/i3/publicdata_ps/pdfratio.py | 46 ++++++++++---------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/pdfratio.py b/skyllh/analyses/i3/publicdata_ps/pdfratio.py index 611e80d6f7..e26caab961 100644 --- a/skyllh/analyses/i3/publicdata_ps/pdfratio.py +++ b/skyllh/analyses/i3/publicdata_ps/pdfratio.py @@ -32,32 +32,32 @@ def __init__(self, sig_pdf_set, bkg_pdf, cap_ratio=False, **kwargs): self._interpolmethod_instance = self.interpolmethod( self._get_ratio_values, sig_pdf_set.fitparams_grid_set) - # Calculate the ratio value for the phase space where no background - # is available. We will take the p_sig percentile of the signal like - # phase space. - ratio_perc = 99 - - # Get the log10 reco energy values where the background pdf has - # non-zero values. - n_logE = bkg_pdf.get_binning('log_energy').nbins - n_sinDec = bkg_pdf.get_binning('sin_dec').nbins - bd = bkg_pdf._hist_logE_sinDec > 0 - log10_e_bc = bkg_pdf.get_binning('log_energy').bincenters - self.ratio_fill_value_dict = dict() - for sig_pdf_key in sig_pdf_set.pdf_keys: - sigpdf = sig_pdf_set[sig_pdf_key] - sigvals = sigpdf.get_pd_by_log10_reco_e(log10_e_bc) - sigvals = np.broadcast_to(sigvals, (n_sinDec, n_logE)).T - r = sigvals[bd] / bkg_pdf._hist_logE_sinDec[bd] - val = np.percentile(r[r > 1.], ratio_perc) - self.ratio_fill_value_dict[sig_pdf_key] = val - self.cap_ratio = cap_ratio - if cap_ratio: + if self.cap_ratio: self._logger.info('The PDF ratio will be capped!') - # Create cache variables for the last ratio value and gradients in order - # to avoid the recalculation of the ratio value when the + # Calculate the ratio value for the phase space where no background + # is available. We will take the p_sig percentile of the signal + # like phase space. + ratio_perc = 99 + + # Get the log10 reco energy values where the background pdf has + # non-zero values. + n_logE = bkg_pdf.get_binning('log_energy').nbins + n_sinDec = bkg_pdf.get_binning('sin_dec').nbins + bd = bkg_pdf._hist_logE_sinDec > 0 + log10_e_bc = bkg_pdf.get_binning('log_energy').bincenters + self.ratio_fill_value_dict = dict() + for sig_pdf_key in sig_pdf_set.pdf_keys: + sigpdf = sig_pdf_set[sig_pdf_key] + sigvals = sigpdf.get_pd_by_log10_reco_e(log10_e_bc) + sigvals = np.broadcast_to(sigvals, (n_sinDec, n_logE)).T + r = sigvals[bd] / bkg_pdf._hist_logE_sinDec[bd] + val = np.percentile(r[r > 1.], ratio_perc) + self.ratio_fill_value_dict[sig_pdf_key] = val + + # Create cache variables for the last ratio value and gradients in + # order to avoid the recalculation of the ratio value when the # ``get_gradient`` method is called (usually after the ``get_ratio`` # method was called). self._cache_fitparams_hash = None From a74ef9957b25b75801a6aa612a558acefed316fd Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 6 Sep 2022 17:40:45 +0200 Subject: [PATCH 152/274] Use a finer tolerance for the minimizer to find the minimum --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 7e6e86cdae..1c87be303c 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -208,7 +208,7 @@ def create_analysis( if minimizer_impl == "LBFGS": minimizer = Minimizer(LBFGSMinimizerImpl()) elif minimizer_impl == "minuit": - minimizer = Minimizer(IMinuitMinimizerImpl()) + minimizer = Minimizer(IMinuitMinimizerImpl(ftol=1e-8)) else: raise NameError(f"Minimizer implementation `{minimizer_impl}` is not " "supported. Please use `LBFGS` or `minuit`.") From eee18e5704ae7961975876cb3de6a8beac6ccdfa Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Wed, 7 Sep 2022 10:53:52 +0200 Subject: [PATCH 153/274] Add sanity check for +inf pdf ratio values --- skyllh/analyses/i3/publicdata_ps/pdfratio.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/skyllh/analyses/i3/publicdata_ps/pdfratio.py b/skyllh/analyses/i3/publicdata_ps/pdfratio.py index e26caab961..dd103daf5e 100644 --- a/skyllh/analyses/i3/publicdata_ps/pdfratio.py +++ b/skyllh/analyses/i3/publicdata_ps/pdfratio.py @@ -137,6 +137,12 @@ def _get_ratio_values(self, tdm, gridfitparams, eventdata): ratio[m_zero_bkg] = (sig_prob[m_zero_bkg] / np.finfo(np.double).resolution) + # Check for positive inf values in the ratio and set the ratio to a + # finite number. Here we choose the maximum value of float32 to keep + # room for additional computational operations. + m_inf = np.isposinf(ratio) + ratio[m_inf] = np.finfo(np.float32).max + return ratio def _calculate_ratio_and_gradients(self, tdm, fitparams, fitparams_hash): From 2c544e738bc797198547aa71774c93516ee6e038 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Tue, 13 Sep 2022 17:45:49 +0200 Subject: [PATCH 154/274] Support fixed-gamma flux models --- skyllh/i3/detsigyield.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/skyllh/i3/detsigyield.py b/skyllh/i3/detsigyield.py index 36e3331258..5283c854ff 100644 --- a/skyllh/i3/detsigyield.py +++ b/skyllh/i3/detsigyield.py @@ -432,7 +432,12 @@ def __call__(self, src, src_flux_params): parameter, i.e. gamma, the array is (N_sources,1)-shaped. """ src_dec = np.atleast_1d(src['dec']) - src_gamma = src_flux_params['gamma'] + if src_flux_params is None: + # Gamma is not a fit parameter. So we take it from the + # initial flux model. + src_gamma = np.array([self.fluxmodel.gamma], dtype=np.double) + else: + src_gamma = src_flux_params['gamma'] # Create results array. values = np.zeros_like(src_dec, dtype=np.float) From 7c4d870db24348e42ffe3b7b3f29fa7f2ef609af Mon Sep 17 00:00:00 2001 From: Chiara Bellenghi Date: Mon, 19 Sep 2022 12:15:59 -0500 Subject: [PATCH 155/274] Create the signal energy pdf spline using the reco energy bin centers from the dataset --- skyllh/analyses/i3/publicdata_ps/signalpdf.py | 29 ++++++++++--------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/signalpdf.py b/skyllh/analyses/i3/publicdata_ps/signalpdf.py index 7eed80f8ca..8b01f5a532 100644 --- a/skyllh/analyses/i3/publicdata_ps/signalpdf.py +++ b/skyllh/analyses/i3/publicdata_ps/signalpdf.py @@ -1313,19 +1313,22 @@ def __init__( # Define the values at which to evaluate the splines. # Some bins might have zero bin widths. - m = (sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx] - - sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx]) > 0 - le = sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx][m] - ue = sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx][m] - min_log10_reco_e = np.min(le) - max_log10_reco_e = np.max(ue) - d_log10_reco_e = np.min(ue - le) / 20 - n_xvals = int((max_log10_reco_e - min_log10_reco_e) / d_log10_reco_e) - xvals_binedges = np.linspace( - min_log10_reco_e, - max_log10_reco_e, - n_xvals+1 - ) + # m = (sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx] - + # sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx]) > 0 + # le = sm.log10_reco_e_binedges_lower[valid_true_e_idxs, true_dec_idx][m] + # ue = sm.log10_reco_e_binedges_upper[valid_true_e_idxs, true_dec_idx][m] + # min_log10_reco_e = np.min(le) + # max_log10_reco_e = np.max(ue) + # d_log10_reco_e = np.min(ue - le) / 20 + # n_xvals = int((max_log10_reco_e - min_log10_reco_e) / d_log10_reco_e) + # xvals_binedges = np.linspace( + # min_log10_reco_e, + # max_log10_reco_e, + # n_xvals+1 + # ) + # xvals = get_bincenters_from_binedges(xvals_binedges) + + xvals_binedges = ds.get_binning_definition('log_energy').binedges xvals = get_bincenters_from_binedges(xvals_binedges) # Calculate the neutrino enegry bin widths in GeV. From 23b1c8aab1060d1cc57281c158206378daf8588e Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Fri, 30 Sep 2022 11:45:27 +0200 Subject: [PATCH 156/274] Starting adding tutorial for public ps data --- doc/sphinx/tutorials/index.rst | 1 + doc/sphinx/tutorials/publicdata_ps.ipynb | 47 ++++++++++++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 doc/sphinx/tutorials/publicdata_ps.ipynb diff --git a/doc/sphinx/tutorials/index.rst b/doc/sphinx/tutorials/index.rst index 9934141e16..325ec04a3b 100644 --- a/doc/sphinx/tutorials/index.rst +++ b/doc/sphinx/tutorials/index.rst @@ -8,5 +8,6 @@ Tutorials :maxdepth: 3 getting_started + publicdata_ps kdepdf_mcbg_ps trad_ps_expbg diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb new file mode 100644 index 0000000000..f077a25e79 --- /dev/null +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -0,0 +1,47 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Working with the public 10-year IceCube point-source data\n", + "==" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This tutorial shows how to use the IceCube public 10-year point-source data with SkyLLH." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.8.10" + } + }, + "nbformat": 4, + "nbformat_minor": 4 +} From 2acc2fa8fbbc220494d9083dac7bc32bc8629e97 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 3 Oct 2022 12:52:08 +0200 Subject: [PATCH 157/274] Improve doc string and remove obsolete argument --- skyllh/analyses/i3/publicdata_ps/trad_ps.py | 30 +++++++-------------- 1 file changed, 10 insertions(+), 20 deletions(-) diff --git a/skyllh/analyses/i3/publicdata_ps/trad_ps.py b/skyllh/analyses/i3/publicdata_ps/trad_ps.py index 1c87be303c..61b774a172 100644 --- a/skyllh/analyses/i3/publicdata_ps/trad_ps.py +++ b/skyllh/analyses/i3/publicdata_ps/trad_ps.py @@ -139,7 +139,6 @@ def create_analysis( compress_data=False, keep_data_fields=None, optimize_delta_angle=10, - efficiency_mode=None, tl=None, ppbar=None ): @@ -163,14 +162,21 @@ def create_analysis( gamma_seed : float | None Value to seed the minimizer with for the gamma fit. If set to None, the refplflux_gamma value will be set as gamma_seed. - kde_smoothing : bool | False - Apply a KDE-based smoothing to the data-driven backgroun pdf. + kde_smoothing : bool + Apply a KDE-based smoothing to the data-driven background pdf. Default: False. minimizer_impl : str | "LBFGS" Minimizer implementation to be used. Supported options are "LBFGS" (L-BFG-S minimizer used from the :mod:`scipy.optimize` module), or "minuit" (Minuit minimizer used by the :mod:`iminuit` module). Default: "LBFGS". + cap_ratio : bool + If set to True, the energy PDF ratio will be capped to a finite value + where no background energy PDF information is available. This will + ensure that an energy PDF ratio is available for high energies where + no background is available from the experimental data. + If kde_smoothing is set to True, cap_ratio should be set to False! + Default is False. compress_data : bool Flag if the data should get converted from float64 into float32. keep_data_fields : list of str | None @@ -178,21 +184,6 @@ def create_analysis( the data. optimize_delta_angle : float The delta angle in degrees for the event selection optimization methods. - efficiency_mode : str | None - The efficiency mode the data should get loaded with. Possible values - are: - - - 'memory': - The data will be load in a memory efficient way. This will - require more time, because all data records of a file will - be loaded sequentially. - - 'time': - The data will be loaded in a time efficient way. This will - require more memory, because each data file gets loaded in - memory at once. - - The default value is ``'time'``. If set to ``None``, the default - value will be used. tl : TimeLord instance | None The TimeLord instance to use to time the creation of the analysis. ppbar : ProgressBar instance | None @@ -200,7 +191,7 @@ def create_analysis( Returns ------- - analysis : SpatialEnergyTimeIntegratedMultiDatasetSingleSourceAnalysis + analysis : TimeIntegratedMultiDatasetSingleSourceAnalysis The Analysis instance for this analysis. """ @@ -279,7 +270,6 @@ def create_analysis( data = ds.load_and_prepare_data( keep_fields=keep_data_fields, compress=compress_data, - efficiency_mode=efficiency_mode, tl=tl) # Create a trial data manager and add the required data fields. From b42363800c7d0f6c8c6e66e6276f31a2a607883b Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 3 Oct 2022 14:17:31 +0200 Subject: [PATCH 158/274] Add kwargs and pass it to underlaying function --- skyllh/core/analysis_utils.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/skyllh/core/analysis_utils.py b/skyllh/core/analysis_utils.py index 406b464234..464894e1e7 100644 --- a/skyllh/core/analysis_utils.py +++ b/skyllh/core/analysis_utils.py @@ -1219,7 +1219,7 @@ def create_trial_data_file( def extend_trial_data_file( ana, rss, n_trials, trial_data, mean_n_sig=0, mean_n_sig_null=0, mean_n_bkg_list=None, bkg_kwargs=None, sig_kwargs=None, - pathfilename=None): + pathfilename=None, **kwargs): """Appends to the trial data file `n_trials` generated trials for each mean number of injected signal events up to `ns_max` for a given analysis. @@ -1263,6 +1263,12 @@ def extend_trial_data_file( `poisson`. pathfilename : string | None Trial data file path including the filename. + + Additional keyword arguments + ---------------------------- + Additional keyword arguments are passed-on to the ``create_trial_data_file`` + function. + Returns ------- trial_data : @@ -1275,11 +1281,19 @@ def extend_trial_data_file( enumerate(sorted(np.unique(trial_data['seed'])) + [None], 1) if i != e) rss.reseed(seed) + (seed, mean_n_sig, mean_n_sig_null, trials) = create_trial_data_file( - ana, rss, n_trials, - mean_n_sig=mean_n_sig) - trial_data = np_rfn.stack_arrays([trial_data, trials], usemask=False, - asrecarray=True) + ana=ana, + rss=rss, + n_trials=n_trials, + mean_n_sig=mean_n_sig, + **kwargs + ) + trial_data = np_rfn.stack_arrays( + [trial_data, trials], + usemask=False, + asrecarray=True) + if(pathfilename is not None): # Save the trial data to file. makedirs(os.path.dirname(pathfilename), exist_ok=True) From 2f5701a8e3e7a91a2ac0bf66f84fa394f7856d58 Mon Sep 17 00:00:00 2001 From: Martin Wolf Date: Mon, 3 Oct 2022 16:45:22 +0200 Subject: [PATCH 159/274] Added tutorial for public data --- doc/sphinx/tutorials/publicdata_ps.ipynb | 791 +++++++++++++++++++++++ 1 file changed, 791 insertions(+) diff --git a/doc/sphinx/tutorials/publicdata_ps.ipynb b/doc/sphinx/tutorials/publicdata_ps.ipynb index f077a25e79..862104b348 100644 --- a/doc/sphinx/tutorials/publicdata_ps.ipynb +++ b/doc/sphinx/tutorials/publicdata_ps.ipynb @@ -15,6 +15,797 @@ "This tutorial shows how to use the IceCube public 10-year point-source data with SkyLLH." ] }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "%load_ext autoreload\n", + "%autoreload 2" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "from matplotlib import pyplot as plt" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Getting the datasets\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "First we import the dataset definition of the public 10-year point-source data set:" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.datasets.i3.PublicData_10y_ps import create_dataset_collection" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The collection of datasets can be created using the ``create_dataset_collection`` function. This function requires the base path to the data repository. It's the path where the public point-source data is stored. The public point-source data can be downloaded from the [IceCube website](http://icecube.wisc.edu/data-releases/20210126_PS-IC40-IC86_VII.zip)." + ] + }, + { + "cell_type": "code", + "execution_count": 4, + "metadata": {}, + "outputs": [], + "source": [ + "dsc = create_dataset_collection(base_path='/home/mwolf/projects/publicdata_ps/')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``dataset_names`` property provides a list of all the data sets defined in the data set collection of the public point-source data." + ] + }, + { + "cell_type": "code", + "execution_count": 5, + "metadata": {}, + "outputs": [ + { + "data": { + "text/plain": [ + "['IC40',\n", + " 'IC59',\n", + " 'IC79',\n", + " 'IC86_I',\n", + " 'IC86_II',\n", + " 'IC86_II-VII',\n", + " 'IC86_III',\n", + " 'IC86_IV',\n", + " 'IC86_V',\n", + " 'IC86_VI',\n", + " 'IC86_VII']" + ] + }, + "execution_count": 5, + "metadata": {}, + "output_type": "execute_result" + } + ], + "source": [ + "dsc.dataset_names" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The individual data sets ``IC86_II``, ``IC86_III``, ``IC86_IV``, ``IC86_V``, ``IC86_VI``, and ``IC86_VII`` are also available as a single combined data set ``IC86_II-VII``, because these data sets share the same detector simulation and event selection. Hence, we can get a list of data sets via the ``get_datasets`` method of the ``dsc`` instance:" + ] + }, + { + "cell_type": "code", + "execution_count": 6, + "metadata": {}, + "outputs": [], + "source": [ + "datasets = dsc.get_datasets(['IC40', 'IC59', 'IC79', 'IC86_I', 'IC86_II-VII'])" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Getting the analysis\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The analysis used for the published PRL results is referred in SkyLLH as \"*traditional point-source analysis*\" and is pre-defined:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.analyses.i3.publicdata_ps.trad_ps import create_analysis" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function create_analysis in module skyllh.analyses.i3.publicdata_ps.trad_ps:\n", + "\n", + "create_analysis(datasets, source, refplflux_Phi0=1, refplflux_E0=1000.0, refplflux_gamma=2, ns_seed=10.0, gamma_seed=3, kde_smoothing=False, minimizer_impl='LBFGS', cap_ratio=False, compress_data=False, keep_data_fields=None, optimize_delta_angle=10, tl=None, ppbar=None)\n", + " Creates the Analysis instance for this particular analysis.\n", + " \n", + " Parameters:\n", + " -----------\n", + " datasets : list of Dataset instances\n", + " The list of Dataset instances, which should be used in the\n", + " analysis.\n", + " source : PointLikeSource instance\n", + " The PointLikeSource instance defining the point source position.\n", + " refplflux_Phi0 : float\n", + " The flux normalization to use for the reference power law flux model.\n", + " refplflux_E0 : float\n", + " The reference energy to use for the reference power law flux model.\n", + " refplflux_gamma : float\n", + " The spectral index to use for the reference power law flux model.\n", + " ns_seed : float\n", + " Value to seed the minimizer with for the ns fit.\n", + " gamma_seed : float | None\n", + " Value to seed the minimizer with for the gamma fit. If set to None,\n", + " the refplflux_gamma value will be set as gamma_seed.\n", + " kde_smoothing : bool\n", + " Apply a KDE-based smoothing to the data-driven background pdf.\n", + " Default: False.\n", + " minimizer_impl : str | \"LBFGS\"\n", + " Minimizer implementation to be used. Supported options are \"LBFGS\"\n", + " (L-BFG-S minimizer used from the :mod:`scipy.optimize` module), or\n", + " \"minuit\" (Minuit minimizer used by the :mod:`iminuit` module).\n", + " Default: \"LBFGS\".\n", + " cap_ratio : bool\n", + " If set to True, the energy PDF ratio will be capped to a finite value\n", + " where no background energy PDF information is available. This will\n", + " ensure that an energy PDF ratio is available for high energies where\n", + " no background is available from the experimental data.\n", + " If kde_smoothing is set to True, cap_ratio should be set to False!\n", + " Default is False.\n", + " compress_data : bool\n", + " Flag if the data should get converted from float64 into float32.\n", + " keep_data_fields : list of str | None\n", + " List of additional data field names that should get kept when loading\n", + " the data.\n", + " optimize_delta_angle : float\n", + " The delta angle in degrees for the event selection optimization methods.\n", + " tl : TimeLord instance | None\n", + " The TimeLord instance to use to time the creation of the analysis.\n", + " ppbar : ProgressBar instance | None\n", + " The instance of ProgressBar for the optional parent progress bar.\n", + " \n", + " Returns\n", + " -------\n", + " analysis : TimeIntegratedMultiDatasetSingleSourceAnalysis\n", + " The Analysis instance for this analysis.\n", + "\n" + ] + } + ], + "source": [ + "help(create_analysis)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "As source we use TXS 0506+056." + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.physics.source import PointLikeSource" + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [], + "source": [ + "source = PointLikeSource(ra=np.deg2rad(77.35), dec=np.deg2rad(5.7))" + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[==========================================================] 100% ELT 0h:00m:15s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:00m:14s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:00m:14s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:00m:15s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:00m:14s[ ] 0% ELT 0h:00m:00s\n", + "[==========================================================] 100% ELT 0h:01m:47s\n", + "[==========================================================] 100% ELT 0h:00m:00s\n" + ] + } + ], + "source": [ + "ana = create_analysis(datasets=datasets, source=source)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After creating the analysis instance we can unblind the data for the choosen source. Hence, we maximize the likelihood function for all given experimental data events. The analysis instance has the method ``unblind`` that can be used for that. This method requires a ``RandomStateService`` instance in case the minimizer does not succeed and a new set of initial values for the fit parameters need to get generated." + ] + }, + { + "cell_type": "code", + "execution_count": 12, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.core.random import RandomStateService\n", + "rss = RandomStateService(seed=1)" + ] + }, + { + "cell_type": "code", + "execution_count": 13, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on method unblind in module skyllh.core.analysis:\n", + "\n", + "unblind(rss) method of skyllh.core.analysis.TimeIntegratedMultiDatasetSingleSourceAnalysis instance\n", + " Evaluates the unscrambled data, i.e. unblinds the data.\n", + " \n", + " Parameters\n", + " ----------\n", + " rss : RandomStateService instance\n", + " The RandomStateService instance that should be used draw random\n", + " numbers from.\n", + " \n", + " Returns\n", + " -------\n", + " TS : float\n", + " The test-statistic value.\n", + " fitparam_dict : dict\n", + " The dictionary holding the global fit parameter names and their best\n", + " fit values.\n", + " status : dict\n", + " The status dictionary with information about the performed\n", + " minimization process of the negative of the log-likelihood ratio\n", + " function.\n", + "\n" + ] + } + ], + "source": [ + "help(ana.unblind)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The ``unblind`` method returns the test-statistic value, the best-fit fit parameter values, and a status dictionary of the minimizer." + ] + }, + { + "cell_type": "code", + "execution_count": 14, + "metadata": {}, + "outputs": [], + "source": [ + "(ts, x, status) = ana.unblind(rss=rss)" + ] + }, + { + "cell_type": "code", + "execution_count": 15, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "TS = 13.145\n", + "ns = 14.58\n", + "gamma = 2.17\n" + ] + } + ], + "source": [ + "print(f'TS = {ts:.3f}')\n", + "print(f'ns = {x[\"ns\"]:.2f}')\n", + "print(f'gamma = {x[\"gamma\"]:.2f}')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Calculating the significance (local p-value)\n", + "---" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "The significance of the source, i.e. the local p-value, can be calculated by generating the test-statistic distribution of background-only data trials, i.e. for zero injected signal events. SkyLLH provides the helper function ``create_trial_data_file`` to do that:" + ] + }, + { + "cell_type": "code", + "execution_count": 16, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.core.analysis_utils import create_trial_data_file" + ] + }, + { + "cell_type": "code", + "execution_count": 17, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Help on function create_trial_data_file in module skyllh.core.analysis_utils:\n", + "\n", + "create_trial_data_file(ana, rss, n_trials, mean_n_sig=0, mean_n_sig_null=0, mean_n_bkg_list=None, bkg_kwargs=None, sig_kwargs=None, pathfilename=None, ncpu=None, ppbar=None, tl=None)\n", + " Creates and fills a trial data file with `n_trials` generated trials for\n", + " each mean number of injected signal events from `ns_min` up to `ns_max` for\n", + " a given analysis.\n", + " \n", + " Parameters\n", + " ----------\n", + " ana : instance of Analysis\n", + " The Analysis instance to use for the trial generation.\n", + " rss : RandomStateService\n", + " The RandomStateService instance to use for generating random\n", + " numbers.\n", + " n_trials : int\n", + " The number of trials to perform for each hypothesis test.\n", + " mean_n_sig : ndarray of float | float | 2- or 3-element sequence of float\n", + " The array of mean number of injected signal events (MNOISEs) for which\n", + " to generate trials. If this argument is not a ndarray, an array of\n", + " MNOISEs is generated based on this argument.\n", + " If a single float is given, only this given MNOISEs are injected.\n", + " If a 2-element sequence of floats is given, it specifies the range of\n", + " MNOISEs with a step size of one.\n", + " If a 3-element sequence of floats is given, it specifies the range plus\n", + " the step size of the MNOISEs.\n", + " mean_n_sig_null : ndarray of float | float | 2- or 3-element sequence of\n", + " float\n", + " The array of the fixed mean number of signal events (FMNOSEs) for the\n", + " null-hypothesis for which to generate trials. If this argument is not a\n", + " ndarray, an array of FMNOSEs is generated based on this argument.\n", + " If a single float is given, only this given FMNOSEs are used.\n", + " If a 2-element sequence of floats is given, it specifies the range of\n", + " FMNOSEs with a step size of one.\n", + " If a 3-element sequence of floats is given, it specifies the range plus\n", + " the step size of the FMNOSEs.\n", + " mean_n_bkg_list : list of float | None\n", + " The mean number of background events that should be generated for\n", + " each dataset. This parameter is passed to the ``do_trials`` method of\n", + " the ``Analysis`` class. If set to None (the default), the background\n", + " generation method needs to obtain this number itself.\n", + " bkg_kwargs : dict | None\n", + " Additional keyword arguments for the `generate_events` method of the\n", + " background generation method class. An usual keyword argument is\n", + " `poisson`.\n", + " sig_kwargs : dict | None\n", + " Additional keyword arguments for the `generate_signal_events` method\n", + " of the `SignalGenerator` class. An usual keyword argument is\n", + " `poisson`.\n", + " pathfilename : string | None\n", + " Trial data file path including the filename.\n", + " If set to None generated trials won't be saved.\n", + " ncpu : int | None\n", + " The number of CPUs to use.\n", + " ppbar : instance of ProgressBar | None\n", + " The optional instance of the parent progress bar.\n", + " tl: instance of TimeLord | None\n", + " The instance of TimeLord that should be used to measure individual\n", + " tasks.\n", + " \n", + " Returns\n", + " -------\n", + " seed : int\n", + " The seed used to generate the trials.\n", + " mean_n_sig : 1d ndarray\n", + " The array holding the mean number of signal events used to generate the\n", + " trials.\n", + " mean_n_sig_null : 1d ndarray\n", + " The array holding the fixed mean number of signal events for the\n", + " null-hypothesis used to generate the trials.\n", + " trial_data : structured numpy ndarray\n", + " The generated trial data.\n", + "\n" + ] + } + ], + "source": [ + "help(create_trial_data_file)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "At first we will generate 10k trials and look at the test-statistic distribution. We will time the trial generation using the ``TimeLord`` class." + ] + }, + { + "cell_type": "code", + "execution_count": 18, + "metadata": {}, + "outputs": [], + "source": [ + "from skyllh.core.timing import TimeLord\n", + "tl = TimeLord()" + ] + }, + { + "cell_type": "code", + "execution_count": 19, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[==========================================================] 100% ELT 0h:08m:14s\n", + "TimeLord: Executed tasks:\n", + "[Generating background events for data set 0.] 0.002 sec/iter (10000)\n", + "[Generating background events for data set 1.] 0.004 sec/iter (10000)\n", + "[Generating background events for data set 2.] 0.003 sec/iter (10000)\n", + "[Generating background events for data set 3.] 0.006 sec/iter (10000)\n", + "[Generating background events for data set 4.] 0.028 sec/iter (10000)\n", + "[Generating pseudo data. ] 0.035 sec/iter (10000)\n", + "[Initializing trial. ] 0.034 sec/iter (10000)\n", + "[Create fitparams dictionary. ] 1.2e-05 sec/iter (593990)\n", + "[Calc fit param dep data fields. ] 3.5e-06 sec/iter (593990)\n", + "[Get sig prob. ] 2.1e-04 sec/iter (593990)\n", + "[Evaluating bkg log-spline. ] 2.9e-04 sec/iter (593990)\n", + "[Get bkg prob. ] 3.6e-04 sec/iter (593990)\n", + "[Calc PDF ratios. ] 7.4e-05 sec/iter (593990)\n", + "[Calc pdfratio values. ] 9.0e-04 sec/iter (593990)\n", + "[Calc pdfratio value product Ri ] 4.2e-05 sec/iter (593990)\n", + "[Calc logLamds and grads ] 3.4e-04 sec/iter (593990)\n", + "[Evaluate llh-ratio function. ] 0.005 sec/iter (118798)\n", + "[Minimize -llhratio function. ] 0.057 sec/iter (10000)\n", + "[Maximizing LLH ratio function. ] 0.057 sec/iter (10000)\n", + "[Calculating test statistic. ] 4.2e-05 sec/iter (10000)\n" + ] + } + ], + "source": [ + "rss = RandomStateService(seed=1)\n", + "(_, _, _, trials) = create_trial_data_file(\n", + " ana=ana,\n", + " rss=rss,\n", + " n_trials=1e4,\n", + " mean_n_sig=0,\n", + " pathfilename='/home/mwolf/projects/publicdata_ps/txs_bkg_trails.npy',\n", + " ncpu=8,\n", + " tl=tl)\n", + "print(tl)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "After generating the background trials, we can histogram the test-statistic values and plot the TS distribution." + ] + }, + { + "cell_type": "code", + "execution_count": 20, + "metadata": {}, + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAAAYUAAAEGCAYAAACKB4k+AAAABHNCSVQICAgIfAhkiAAAAAlwSFlzAAALEgAACxIB0t1+/AAAADh0RVh0U29mdHdhcmUAbWF0cGxvdGxpYiB2ZXJzaW9uMy4xLjIsIGh0dHA6Ly9tYXRwbG90bGliLm9yZy8li6FKAAAgAElEQVR4nO3deXxU9fX/8dcxqOACKotEAgJClbAIGI2orUhFoYpgf4hrK7UVxa31Z6vUqoj9tVK16letC1ZxB60tIIpWvwKuNChIlUWWAmLYZBFxQ7bz+2Myt8MwM5kkM7kzyfv5eOSRzJ25956EkDOf9Zi7IyIiArBH2AGIiEjuUFIQEZGAkoKIiASUFEREJKCkICIigQZhB1ATzZo187Zt24YdhohIXpk1a9Z6d2+e6Lm8Tgpt27bl/fffDzsMEZG8YmafJHtO3UciIhLIy6RgZgPMbMwXX3wRdigiInVKXiYFd5/s7sOaNGkSdigiInVKXo8piNSWbdu2UV5ezpYtW8IORSRtDRs2pKioiD333DPtc5QURNJQXl7O/vvvT9u2bTGzsMMRqZS7s2HDBsrLy2nXrl3a5+Vl95FIbduyZQtNmzZVQpC8YWY0bdq0yq1bJQWRNCkhSL6pzu9sXiaFms4+GjV5Hmc/NINnylZkODIRkfyWl0khE7OPypZtZNKclRmMSiS7li9fTpcuXWp0jenTp3P66adnKKLMGjp0KM8//3xGrrVw4UIWLlyYkWvVN/VyoHnkgM7MX7U57DBE8oq74+7ssUdevpeUNOlfVySPbN++nQsvvJBu3boxePBgvvnmG2655RaOPvpounTpwrBhw4hWU1yyZAknn3wyRx55JD179uQ///nPLtd677336NGjB0uXLmXdunX07duXnj17cskll3DooYeyfv16li9fTqdOnbjsssvo2bMnn376KePGjaNr16506dKF6667LrjefvvtF3z9/PPPM3ToUCDSArjqqqs47rjjaN++fdAacHeuuOIKiouLOe200/jss8+y/NOTdNTLloJITYyaPC/jLc3iQxozckDnSl+3cOFCHnnkEY4//nguuugi7r//fq644gpuuukmAH7yk5/w4osvMmDAAM4//3xGjBjBmWeeyZYtW9i5cyeffvopAO+++y5XXnklkyZNok2bNlxxxRX06dOH3/72t7zyyiuMGTNml3uOHTuW+++/n1WrVnHdddcxa9YsDjzwQE455RQmTpzIoEGDUsa9evVq3n77bT7++GPOOOMMBg8ezIQJE1i4cCEfffQRa9eupbi4mIsuuqgGP0XJBLUURPJI69atOf744wG44IILePvtt5k2bRqlpaV07dqVqVOnMm/ePL788ktWrlzJmWeeCUQWMe2zzz4ALFiwgGHDhjF58mTatGkDwNtvv80555wDQL9+/TjwwAODex566KEce+yxQKR10bt3b5o3b06DBg04//zzefPNNyuNe9CgQeyxxx4UFxezdu1aAN58803OPfdcCgoKOOSQQ+jTp0+GfkpSE2opiFRROu/osyV+iqGZcdlll/H+++/TunVrbr75ZrZs2RJ0ISVSWFjIli1b+OCDDzjkkEMAUr5+3333Db5O9brY2OLnxu+9994Jr6FpvrknL1sK2hBP6qsVK1YwY8YMAMaNG8cJJ5wAQLNmzfjqq6+C/vrGjRtTVFTExIkTAfjuu+/45ptvADjggAN46aWXuP7665k+fToAJ5xwAs899xwAr776Kp9//nnC+5eWlvLGG2+wfv16duzYwbhx4zjxxBMBOPjgg1mwYAE7d+5kwoQJlX4vP/jBDxg/fjw7duxg9erVTJs2rZo/FcmkvEwK2hBP6qtOnTrx+OOP061bNzZu3Mjw4cO5+OKL6dq1K4MGDeLoo48OXvvkk09yzz330K1bN4477jjWrFkTPHfwwQczefJkLr/8csrKyhg5ciSvvvoqPXv25OWXX6awsJD9999/t/sXFhZy6623ctJJJwUD2AMHDgRg9OjRnH766fTp04fCwsJKv5czzzyTjh070rVrV4YPHx4kFwmXpWoO5rqSkhKvbpGdsx+KvNt69pJemQxJ6qgFCxbQqVOnsMPImu+++46CggIaNGjAjBkzGD58OHPmzAk7rGqLrlE4/PDDQ44kfIl+d81slruXJHq9xhREhBUrVjBkyBB27tzJXnvtxcMPPxx2SBISJQURoWPHjnzwwQdhhyE5IC/HFEREJDuUFEREJKCkICIiASUFEREJ5GVS0OI1EZHsyMukoMVrUt9s2LCB7t270717d1q2bEmrVq2Cx6NGjaJz585069aN7t27U1ZWFpw3ePBgli5dSmlpKd27d6dNmzY0b948OPejjz7isMMOY/HixQBs27aNrl27Btf4wx/+kPTaURs3bqRv37507NiRvn37Bquhly9fTqNGjYJ7XXrppcE5s2bNomvXrnTo0IGrrrpql60vnnvuOYqLi+ncuTPnnXde2j+jZcuWUVpaSseOHbn66qvZunUrEKkh0aRJkyCOW265JThn06ZNDB48mCOOOIJOnToFq8V//etfM3Xq1LTv/fHHH9OrVy/23ntv7rjjjuD4li1bOOaYYzjyyCPp3LkzI0eOrNL5UTt27KBHjx671MK4+eabd/k9mDJlStrxphTdIz0fP4466iivriEPvutDHny32udL/TJ//vywQwiMHDnSb7/9dnd3f/fdd/3YY4/1LVu2uLv7unXrfOXKle7uPnfuXB80aNAu544dO9Yvv/zyXY49++yz3rdvX3d3/+Mf/+jDhg2r9NqxfvOb3/itt97q7u633nqrX3vtte7uvmzZMu/cuXPC7+Hoo4/2d99913fu3On9+vXzKVOmuLv7okWLvHv37r5x40Z3d1+7du1u544dO9ZHjhy52/GzzjrLx40b5+7uZ599dvCaadOm+WmnnZYwjp/+9Kf+8MMPu7v7d999559//rm7uy9fvjz4maRj7dq1PnPmTL/++uuDfxt39507d/qXX37p7u5bt271Y445xmfMmJH2+VF//vOf/dxzz93l+4j9PUgl0e8u8L4n+buqdQoi1dC7d++MXi+6B1FVrV69mmbNmgUbzjVr1ix47umnnw62oEhlyJAhPProo9x22208+OCDwXqFVNeONWnSpCD+Cy+8kN69e/OnP/0pZcybN2+mV6/IbgI//elPmThxIv379+fhhx/m8ssvD3ZpbdGiRaXxQ+TN7dSpU3nmmWeAyK6s9913HzfffHPSczZv3sybb77JY489BsBee+3FXnvtBUR2ht2wYQNr1qyhZcuWld6/RYsWtGjRgpdeemmX42YW1JnYtm0b27ZtS7gJYLLzAcrLy3nppZf43e9+x5133llpLDWVl91HIhJxyimn8Omnn/K9732Pyy67jDfeeCN47p133uGoo45K6zp333031113HTfccAMHHXRQpdeOtXbt2mCvo8LCwl2K5SxbtowePXpw4okn8tZbbwGwcuVKioqKgtcUFRWxcmWkNO6iRYtYtGgRxx9/PMceeyyvvPJKWvFv2LCBAw44gAYNIu9zW7ZsuUscM2bM4Mgjj6R///7MmzcPgKVLl9K8eXN+9rOf0aNHD37xi1/w9ddfB+f07NmTd955B4Crr7466KaJ/Rg9enSlse3YsYPu3bvTokUL+vbtS2lpaVrfU9SvfvUrbrvttoQV7+677z66devGRRddlHQTw6pSS0GkGqr7zj7T9ttvP2bNmsVbb73FtGnTOPvssxk9ejRDhw5l9erVNG/ePK3rvPLKKxQWFjJ37ty0rp2OwsJCVqxYQdOmTZk1axaDBg1i3rx5Cbffjr573r59O4sXL2b69OmUl5fz/e9/n7lz57Jjxw5++MMfApExjK1btwY7wD755JMp38337NmTTz75hP32248pU6YwaNAgFi9ezPbt25k9ezb33nsvpaWl/PKXv2T06NH8/ve/ByLv3letWgXAXXfdldb3nEhBQQFz5sxh06ZNnHnmmcydOzftWtsvvvgiLVq04Kijjtrtd2748OHceOONmBk33ngj11xzDY8++mi144xSS0EkzxUUFNC7d29GjRrFfffdx9///ncAGjVqtFtdg0RWrVrFPffcw8yZM5kyZQoffvhhpdeOdfDBB7N69Wog0jUU7fLZe++9adq0KQBHHXUUhx12GIsWLaKoqIjy8vLg/PLy8qCuQ1FREQMHDmTPPfekXbt2HH744SxevJimTZsyZ84c5syZwy233MKll14aPO7atSvNmjVj06ZNbN++HYA1a9YEcTRu3DjowvnRj37Etm3bWL9+PUVFRRQVFQXv3AcPHszs2bODuLZs2UKjRo2AmrUUog444AB69+6ddusHIq29F154gbZt23LOOecwdepULrjgguDnXlBQwB577MHFF1/MzJkz075uKkoKInls4cKFwcwhgDlz5nDooYcCkW22lyxZUuk1rr76aq6//nqKioq48847ufzyy3H3lNeOdcYZZ/D4448D8PjjjwfjGOvWrWPHjh1ApKtm8eLFtG/fPtiW+1//+hfuzhNPPBGcM2jQoKCuwvr161m0aBHt27ev9HswM0466aSgnsTEiRODlsWaNWuC1snMmTPZuXMnTZs2pWXLlrRu3TrYUfX111+nuLg4uOaiRYuCd/R33XVXkIRiP0aMGJEyrnXr1rFp0yYAvv32W/73f/+XI444otLvJ+rWW2+lvLyc5cuXM378ePr06cNTTz0FECRigAkTJqTd+qiMuo9E8thXX33FlVde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\n", + "text/plain": [ + "