def test_time_interval_argsort_set(): t1 = TimeInterval(-10.0, 20.0) t2 = TimeInterval(10.0, 30.0) t3 = TimeInterval(-30.0, 50.0) ts = TimeIntervalSet([t1, t2, t3]) idx = ts.argsort() assert idx == [2, 0, 1]
class InstrumentResponseSet(object): """ A set of responses """ def __init__(self, matrix_list, exposure_getter, counts_getter, reference_time=0.0): """ :param matrix_list: :type matrix_list : list[InstrumentResponse] :param exposure_getter : a function returning the exposure between t1 and t2 :param counts_getter : a function returning the number of counts between t1 and t2 :param reference_time : a reference time to be added to the specifications of the intervals used in the weight_by_* methods. Use this if you want to express the time intervals in time units from the reference_time, instead of "absolute" time. For GRBs, this is the trigger time. NOTE: if you use a reference time, the counts_getter and the exposure_getter must accept times relative to the reference time. """ # Store list of matrices self._matrix_list = list(matrix_list) # type: list[InstrumentResponse] # Create the corresponding list of coverage intervals self._coverage_intervals = TimeIntervalSet( [x.coverage_interval for x in self._matrix_list]) # Make sure that all matrices have coverage interval set if None in self._coverage_intervals: raise NoCoverageIntervals( "You need to specify the coverage interval for all matrices in the matrix_list" ) # Remove from the list matrices that cover intervals of zero duration (yes, the GBM publishes those too, # one example is in data/ogip_test_gbm_b0.rsp2) to_be_removed = [] for i, interval in enumerate(self._coverage_intervals): if interval.duration == 0: # Remove it with custom_warnings.catch_warnings(): custom_warnings.simplefilter("always", RuntimeWarning) custom_warnings.warn( "Removing matrix %s (numbering starts at zero) because it has a coverage of " "zero seconds" % i, RuntimeWarning) to_be_removed.append(i) # Actually remove them if len(to_be_removed) > 0: [self._matrix_list.pop(index) for index in to_be_removed] [self._coverage_intervals.pop(index) for index in to_be_removed] # Order the matrices by time idx = self._coverage_intervals.argsort() # It is possible that there is only one coverage interval (these are published by GBM e.g. GRB090819607) # so we need to be sure that the array is a least 1D self._coverage_intervals = TimeIntervalSet( np.atleast_1d(itemgetter(*idx)(self._coverage_intervals))) self._matrix_list = np.atleast_1d(itemgetter(*idx)(self._matrix_list)) # Now make sure that the coverage intervals are contiguous (i.e., there are no gaps) if not self._coverage_intervals.is_contiguous(): raise NonContiguousCoverageIntervals( "The provided responses have coverage intervals which are not contiguous!" ) # Apply the reference time shift, if any self._coverage_intervals -= reference_time # Store callable self._exposure_getter = exposure_getter # type: callable self._counts_getter = counts_getter # type: callable # Store reference time self._reference_time = float(reference_time) @property def reference_time(self): return self._reference_time def __getitem__(self, item): return self._matrix_list[item] def __len__(self): return len(self._matrix_list) @classmethod def from_rsp2_file(cls, rsp2_file, exposure_getter, counts_getter, reference_time=0.0, half_shifted=True): # This assumes the Fermi/GBM rsp2 file format # make the rsp file proper rsp_file = sanitize_filename(rsp2_file) assert file_existing_and_readable( rsp_file ), "OGIPResponse file %s not existing or not readable" % rsp_file # Will fill up the list of matrices list_of_matrices = [] # Read the response with pyfits.open(rsp_file) as f: n_responses = f['PRIMARY'].header['DRM_NUM'] # we will read all the matrices and save them for rsp_number in range(1, n_responses + 1): this_response = OGIPResponse(rsp2_file + '{%i}' % rsp_number) list_of_matrices.append(this_response) if half_shifted: # Now the GBM format has a strange feature: the matrix, instead of covering from TSTART to TSTOP, covers # from (TSTART + TSTOP) / 2.0 of the previous matrix to the (TSTART + TSTOP) / 2.0 of itself. # So let's adjust the coverage intervals accordingly if len(list_of_matrices) > 1: for i, this_matrix in enumerate(list_of_matrices): if i == 0: # The first matrix covers from its TSTART to its half time this_matrix._coverage_interval = TimeInterval( this_matrix.coverage_interval.start_time, this_matrix.coverage_interval.half_time) else: # Any other matrix covers from the half time of the previous matrix to its half time # However, the previous matrix has been already processed, so we use its stop time which # has already begun the half time of what it was before processing prev_matrix = list_of_matrices[i - 1] this_matrix._coverage_interval = TimeInterval( prev_matrix.coverage_interval.stop_time, this_matrix.coverage_interval.half_time) return InstrumentResponseSet(list_of_matrices, exposure_getter, counts_getter, reference_time) # I didn't re-implement this at the moment # def _display_response_weighting(self, weights, tstarts, tstops): # # fig, ax = plt.subplots() # # # plot the time intervals # # ax.hlines(min(weights) - .1, tstarts, tstops, color='red', label='selected intervals') # # ax.hlines(np.median(weights), self._true_rsp_intervals[0], self._true_rsp_intervals[1], color='green', # label='true rsp intervals') # # ax.hlines(max(self._weight) + .1, self._matrix_start, self._matrix_stop, color='blue', # label='rsp header intervals') # # mean_true_rsp_time = np.mean(self._true_rsp_intervals.T, axis=1) # # ax.plot(mean_true_rsp_time, self._weight, '+k', label='weight') def weight_by_exposure(self, *intervals): return self._get_weighted_matrix("exposure", *intervals) def weight_by_counts(self, *intervals): return self._get_weighted_matrix("counts", *intervals) def _get_weighted_matrix(self, switch, *intervals): assert len(intervals) > 0, "You have to provide at least one interval" intervals_set = TimeIntervalSet.from_strings(*intervals) # Compute a set of weights for each interval weights = np.zeros(len(self._matrix_list)) for interval in intervals_set: weights += self._weight_response(interval, switch) # Normalize to 1 weights /= np.sum(weights) # Weight matrices matrix = np.dot( np.array(list(map(attrgetter("matrix"), self._matrix_list))).T, weights.T).T # Now generate the instance of the response # get EBOUNDS from the first matrix ebounds = self._matrix_list[0].ebounds # Get mc channels from the first matrix mc_channels = self._matrix_list[0].monte_carlo_energies matrix_instance = InstrumentResponse(matrix, ebounds, mc_channels) return matrix_instance def _weight_response(self, interval_of_interest, switch): """ :param interval_start : start time of the interval :param interval_stop : stop time of the interval :param switch: either 'counts' or 'exposure' """ ####################### # NOTE: the weights computed here are *not* normalized to one so that they can be combined if there is # more than one interval ####################### # Now mark all responses which overlap with the interval of interest # NOTE: this is a mask of the same length as _matrix_list and _coverage_intervals matrices_mask = [ c_i.overlaps_with(interval_of_interest) for c_i in self._coverage_intervals ] # Check that we have at least one matrix if not np.any(matrices_mask): raise NoMatrixForInterval( "Could not find any matrix applicable to %s\n Have intervals:%s" % (interval_of_interest, ', '.join( [str(interval) for interval in self._coverage_intervals]))) # Compute the weights weights = np.empty_like(self._matrix_list, float) # These "effective intervals" are how much of the coverage interval is really used for each matrix # NOTE: the length of effective_intervals list *will not be* the same as the weight mask or the matrix_list. # There are as many effective intervals as matrices with weight > 0 effective_intervals = [] for i, matrix in enumerate(self._matrix_list): if matrices_mask[i]: # A matrix of interest this_coverage_interval = self._coverage_intervals[i] # See how much it overlaps with the interval of interest this_effective_interval = this_coverage_interval.intersect( interval_of_interest) effective_intervals.append(this_effective_interval) # Now compute the weight if switch == 'counts': # Weight according to the number of events weights[i] = self._counts_getter( this_effective_interval.start_time, this_effective_interval.stop_time) elif switch == 'exposure': # Weight according to the exposure weights[i] = self._exposure_getter( this_effective_interval.start_time, this_effective_interval.stop_time) else: # Uninteresting matrix weights[i] = 0.0 # if all weights are zero, there is something clearly wrong with the exposure or the counts computation assert np.sum( weights ) > 0, "All weights are zero. There must be a bug in the exposure or counts computation" # Check that the first matrix with weight > 0 has an effective interval starting at the beginning of # the interval of interest (otherwise it means that part of the interval of interest is not covered!) if effective_intervals[0].start_time != interval_of_interest.start_time: raise IntervalOfInterestNotCovered( 'The interval of interest (%s) is not covered by %s' % (interval_of_interest, effective_intervals[0])) # Check that the last matrix with weight > 0 has an effective interval starting at the beginning of # the interval of interest (otherwise it means that part of the interval of interest is not covered!) if effective_intervals[-1].stop_time != interval_of_interest.stop_time: raise IntervalOfInterestNotCovered( 'The interval of interest (%s) is not covered by %s' % (interval_of_interest, effective_intervals[0])) # Lastly, check that there is no interruption in coverage (bad time intervals are *not* supported) all_tstarts = np.array([x.start_time for x in effective_intervals]) all_tstops = np.array([x.stop_time for x in effective_intervals]) if not np.all((all_tstops[:-1] == all_tstarts[1:])): raise GapInCoverageIntervals( "Gap in coverage! Bad time intervals are not supported!") return weights @property def ebounds(self): return self._matrix_list[0].ebounds @property def monte_carlo_energies(self): return self._matrix_list[0].monte_carlo_energies
class InstrumentResponseSet(object): """ A set of responses """ def __init__(self, matrix_list, exposure_getter, counts_getter, reference_time=0.0): """ :param matrix_list: :type matrix_list : list[InstrumentResponse] :param exposure_getter : a function returning the exposure between t1 and t2 :param counts_getter : a function returning the number of counts between t1 and t2 :param reference_time : a reference time to be added to the specifications of the intervals used in the weight_by_* methods. Use this if you want to express the time intervals in time units from the reference_time, instead of "absolute" time. For GRBs, this is the trigger time. NOTE: if you use a reference time, the counts_getter and the exposure_getter must accept times relative to the reference time. """ # Store list of matrices self._matrix_list = list(matrix_list) # type: list[InstrumentResponse] # Create the corresponding list of coverage intervals self._coverage_intervals = TimeIntervalSet(map(lambda x: x.coverage_interval, self._matrix_list)) # Make sure that all matrices have coverage interval set if None in self._coverage_intervals: raise NoCoverageIntervals("You need to specify the coverage interval for all matrices in the matrix_list") # Remove from the list matrices that cover intervals of zero duration (yes, the GBM publishes those too, # one example is in data/ogip_test_gbm_b0.rsp2) to_be_removed = [] for i, interval in enumerate(self._coverage_intervals): if interval.duration == 0: # Remove it with custom_warnings.catch_warnings(): custom_warnings.simplefilter("always", RuntimeWarning) custom_warnings.warn("Removing matrix %s (numbering starts at zero) because it has a coverage of " "zero seconds" % i, RuntimeWarning) to_be_removed.append(i) # Actually remove them if len(to_be_removed) > 0: [self._matrix_list.pop(index) for index in to_be_removed] [self._coverage_intervals.pop(index) for index in to_be_removed] # Order the matrices by time idx = self._coverage_intervals.argsort() # It is possible that there is only one coverage interval (these are published by GBM e.g. GRB090819607) # so we need to be sure that the array is a least 1D self._coverage_intervals = TimeIntervalSet(np.atleast_1d(itemgetter(*idx)(self._coverage_intervals))) self._matrix_list = np.atleast_1d(itemgetter(*idx)(self._matrix_list)) # Now make sure that the coverage intervals are contiguous (i.e., there are no gaps) if not self._coverage_intervals.is_contiguous(): raise NonContiguousCoverageIntervals("The provided responses have coverage intervals which are not contiguous!") # Apply the reference time shift, if any self._coverage_intervals -= reference_time # Store callable self._exposure_getter = exposure_getter # type: callable self._counts_getter = counts_getter # type: callable # Store reference time self._reference_time = float(reference_time) @property def reference_time(self): return self._reference_time def __getitem__(self, item): return self._matrix_list[item] def __len__(self): return len(self._matrix_list) @classmethod def from_rsp2_file(cls, rsp2_file, exposure_getter, counts_getter, reference_time=0.0, half_shifted=True): # This assumes the Fermi/GBM rsp2 file format # make the rsp file proper rsp_file = sanitize_filename(rsp2_file) assert file_existing_and_readable(rsp_file), "OGIPResponse file %s not existing or not readable" % rsp_file # Will fill up the list of matrices list_of_matrices = [] # Read the response with pyfits.open(rsp_file) as f: n_responses = f['PRIMARY'].header['DRM_NUM'] # we will read all the matrices and save them for rsp_number in range(1, n_responses + 1): this_response = OGIPResponse(rsp2_file + '{%i}' % rsp_number) list_of_matrices.append(this_response) if half_shifted: # Now the GBM format has a strange feature: the matrix, instead of covering from TSTART to TSTOP, covers # from (TSTART + TSTOP) / 2.0 of the previous matrix to the (TSTART + TSTOP) / 2.0 of itself. # So let's adjust the coverage intervals accordingly if len(list_of_matrices) > 1: for i, this_matrix in enumerate(list_of_matrices): if i == 0: # The first matrix covers from its TSTART to its half time this_matrix._coverage_interval = TimeInterval(this_matrix.coverage_interval.start_time, this_matrix.coverage_interval.half_time) else: # Any other matrix covers from the half time of the previous matrix to its half time # However, the previous matrix has been already processed, so we use its stop time which # has already begun the half time of what it was before processing prev_matrix = list_of_matrices[i-1] this_matrix._coverage_interval = TimeInterval(prev_matrix.coverage_interval.stop_time, this_matrix.coverage_interval.half_time) return InstrumentResponseSet(list_of_matrices, exposure_getter, counts_getter, reference_time) # I didn't re-implement this at the moment # def _display_response_weighting(self, weights, tstarts, tstops): # # fig, ax = plt.subplots() # # # plot the time intervals # # ax.hlines(min(weights) - .1, tstarts, tstops, color='red', label='selected intervals') # # ax.hlines(np.median(weights), self._true_rsp_intervals[0], self._true_rsp_intervals[1], color='green', # label='true rsp intervals') # # ax.hlines(max(self._weight) + .1, self._matrix_start, self._matrix_stop, color='blue', # label='rsp header intervals') # # mean_true_rsp_time = np.mean(self._true_rsp_intervals.T, axis=1) # # ax.plot(mean_true_rsp_time, self._weight, '+k', label='weight') def weight_by_exposure(self, *intervals): return self._get_weighted_matrix("exposure", *intervals) def weight_by_counts(self, *intervals): return self._get_weighted_matrix("counts", *intervals) def _get_weighted_matrix(self, switch, *intervals): assert len(intervals) > 0, "You have to provide at least one interval" intervals_set = TimeIntervalSet.from_strings(*intervals) # Compute a set of weights for each interval weights = np.zeros(len(self._matrix_list)) for interval in intervals_set: weights += self._weight_response(interval, switch) # Normalize to 1 weights /= np.sum(weights) # Weight matrices matrix = np.dot(np.array(map(attrgetter("matrix"), self._matrix_list)).T, weights.T).T # Now generate the instance of the response # get EBOUNDS from the first matrix ebounds = self._matrix_list[0].ebounds # Get mc channels from the first matrix mc_channels = self._matrix_list[0].monte_carlo_energies matrix_instance = InstrumentResponse(matrix, ebounds, mc_channels) return matrix_instance def _weight_response(self, interval_of_interest, switch): """ :param interval_start : start time of the interval :param interval_stop : stop time of the interval :param switch: either 'counts' or 'exposure' """ ####################### # NOTE: the weights computed here are *not* normalized to one so that they can be combined if there is # more than one interval ####################### # Now mark all responses which overlap with the interval of interest # NOTE: this is a mask of the same length as _matrix_list and _coverage_intervals matrices_mask = map(lambda c_i: c_i.overlaps_with(interval_of_interest), self._coverage_intervals) # Check that we have at least one matrix if not np.any(matrices_mask): raise NoMatrixForInterval("Could not find any matrix applicable to %s\n Have intervals:%s" % (interval_of_interest,', '.join([str(interval) for interval in self._coverage_intervals]) )) # Compute the weights weights = np.empty_like(self._matrix_list, float) # These "effective intervals" are how much of the coverage interval is really used for each matrix # NOTE: the length of effective_intervals list *will not be* the same as the weight mask or the matrix_list. # There are as many effective intervals as matrices with weight > 0 effective_intervals = [] for i, matrix in enumerate(self._matrix_list): if matrices_mask[i]: # A matrix of interest this_coverage_interval = self._coverage_intervals[i] # See how much it overlaps with the interval of interest this_effective_interval = this_coverage_interval.intersect(interval_of_interest) effective_intervals.append(this_effective_interval) # Now compute the weight if switch == 'counts': # Weight according to the number of events weights[i] = self._counts_getter(this_effective_interval.start_time, this_effective_interval.stop_time) elif switch == 'exposure': # Weight according to the exposure weights[i] = self._exposure_getter(this_effective_interval.start_time, this_effective_interval.stop_time) else: # Uninteresting matrix weights[i] = 0.0 # if all weights are zero, there is something clearly wrong with the exposure or the counts computation assert np.sum(weights) > 0, "All weights are zero. There must be a bug in the exposure or counts computation" # Check that the first matrix with weight > 0 has an effective interval starting at the beginning of # the interval of interest (otherwise it means that part of the interval of interest is not covered!) if effective_intervals[0].start_time != interval_of_interest.start_time: raise IntervalOfInterestNotCovered('The interval of interest (%s) is not covered by %s'% (interval_of_interest,effective_intervals[0])) # Check that the last matrix with weight > 0 has an effective interval starting at the beginning of # the interval of interest (otherwise it means that part of the interval of interest is not covered!) if effective_intervals[-1].stop_time != interval_of_interest.stop_time: raise IntervalOfInterestNotCovered( 'The interval of interest (%s) is not covered by %s' % (interval_of_interest, effective_intervals[0])) # Lastly, check that there is no interruption in coverage (bad time intervals are *not* supported) all_tstarts = np.array(map(lambda x:x.start_time, effective_intervals)) all_tstops = np.array(map(lambda x:x.stop_time, effective_intervals)) if not np.all((all_tstops[:-1] == all_tstarts[1:])): raise GapInCoverageIntervals("Gap in coverage! Bad time intervals are not supported!") return weights @property def ebounds(self): return self._matrix_list[0].ebounds @property def monte_carlo_energies(self): return self._matrix_list[0].monte_carlo_energies