def test_time_interval_set_pop():

    t1 = TimeInterval(-10.0, 20.0)
    t2 = TimeInterval(10.0, 30.0)
    t3 = TimeInterval(-30.0, 50.0)

    ts = TimeIntervalSet([t1, t2, t3])

    popped = ts.pop(1)

    assert popped == t2
Exemplo n.º 2
0
def test_time_interval_set_pop():

    t1 = TimeInterval(-10.0, 20.0)
    t2 = TimeInterval(10.0, 30.0)
    t3 = TimeInterval(-30.0, 50.0)

    ts = TimeIntervalSet([t1, t2, t3])

    popped = ts.pop(1)

    assert popped == t2
Exemplo n.º 3
0
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
Exemplo n.º 4
0
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