Example #1
0
    def __setattr__(self, name, val):
        rmf = None
        try:
            rmf = RMF1D.__getattribute__(self, '_rmf')
        except:
            pass

        if rmf is not None and hasattr(rmf, name):
            DataRMF.__setattr__(rmf, name, val)
        else:
            NoNewAttributesAfterInit.__setattr__(self, name, val)
Example #2
0
    def __setattr__(self, name, val):
        rmf = None
        try:
            rmf = RMF1D.__getattribute__(self, '_rmf')
        except:
            pass

        if rmf is not None and hasattr(rmf, name):
            DataRMF.__setattr__(rmf, name, val)
        else:
            NoNewAttributesAfterInit.__setattr__(self, name, val)
Example #3
0
    def to_sherpa(self, name):
        """Return `~sherpa.astro.data.DataARF`
        
        Parameters
        ----------
        name : str
            Instance name
        """
        from sherpa.astro.data import DataRMF
        from sherpa.utils import SherpaInt, SherpaUInt, SherpaFloat

        # Need to modify RMF data 
        # see https://github.com/sherpa/sherpa/blob/master/sherpa/astro/io/pyfits_backend.py#L727

        table = self.to_table()
        n_grp = table['N_GRP'].data.astype(SherpaUInt)
        f_chan = table['F_CHAN'].data
        f_chan = np.concatenate([row for row in f_chan]).astype(SherpaUInt)
        n_chan = table['N_CHAN'].data
        n_chan = np.concatenate([row for row in n_chan]).astype(SherpaUInt)
        matrix = table['MATRIX'].data

        good = n_grp > 0
        matrix = matrix[good]
        matrix = np.concatenate([row for row in matrix])
        matrix = matrix.astype(SherpaFloat)

        # TODO: Not sure if we need this if statement
        if f_chan.ndim > 1 and n_chan.ndim > 1:
            f_chan = []
            n_chan = []
            for grp, fch, nch, in izip(n_grp, f_chan, n_chan):
                for i in xrange(grp):
                    f_chan.append(fch[i])
                    n_chan.append(nch[i])

            f_chan = numpy.asarray(f_chan, SherpaUInt)
            n_chan = numpy.asarray(n_chan, SherpaUInt)
        else:
            if len(n_grp) == len(f_chan):
                good = n_grp > 0
                f_chan = f_chan[good]
                n_chan = n_chan[good]

        kwargs = dict(
            name = name,
            energ_lo = table['ENERG_LO'].quantity.to('keV').value.astype(SherpaFloat),
            energ_hi = table['ENERG_HI'].quantity.to('keV').value.astype(SherpaFloat),
            matrix = matrix,
            n_grp = n_grp,
            n_chan = n_chan,
            f_chan = f_chan,
            detchans= self.e_reco.nbins,
            e_min = self.e_reco.data[:-1].to('keV').value,
            e_max = self.e_reco.data[1:].to('keV').value,
            offset=0,
        )

        return DataRMF(**kwargs)
Example #4
0
def read_rmf(arg):
    """
    read_rmf( filename )

    read_rmf( RMFCrate )
    """
    data, filename = backend.get_rmf_data(arg)
    return DataRMF(filename, **data)
def make_rmf(elo, ehi, offset):
    """offset is the number of channels anove/below"""

    assert elo.size == ehi.size
    nchans = elo.size

    if offset > 0:
        nokay = nchans - offset

        n_grp = np.ones(nchans, dtype=np.int16)
        n_chan = np.ones(nchans, dtype=np.int16)
        n_chan[offset] = 2
        n_chan[offset + 1:] = 3

        f_chan = np.ones(nchans, dtype=np.int16)
        f_chan[offset] = 1
        f_chan[offset + 1:] = np.arange(1, nokay, dtype=np.int16)

        matrix = np.asarray([1] * offset + [0.85, 0.15] + [0.3, 0.6, 0.1] *
                            (nokay - 1))

    elif offset < 0:
        offset = -offset
        nokay = nchans - offset

        n_grp = np.ones(nchans, dtype=np.int16)
        n_chan = np.ones(nchans, dtype=np.int16)
        n_chan[:nokay - 1] = 3
        n_chan[nokay - 1] = 2

        f_chan = nchans * np.ones(nchans, dtype=np.int16)
        f_chan[:nokay - 1] = np.arange(offset, nchans - 1, dtype=np.int16)
        f_chan[nokay - 1] = nchans - 1

        matrix = np.asarray([0.3, 0.6, 0.1] * (nokay - 1) + [0.35, 0.65] +
                            [1] * offset)

    else:
        n_grp = np.ones(nchans, dtype=np.int16)
        n_chan = 3 * np.ones(nchans, dtype=np.int16)
        n_chan[0] = 2
        n_chan[-1] = 2
        f_chan = np.arange(0, nchans, dtype=np.int16)
        f_chan[0] = 1
        matrix = np.asarray([0.85, 0.15] * 1 + [0.3, 0.6, 0.1] * (nchans - 2) +
                            [0.35, 0.65] * 1)

    return DataRMF("dummy",
                   detchans=nchans,
                   energ_lo=elo,
                   energ_hi=ehi,
                   n_grp=n_grp,
                   n_chan=n_chan,
                   f_chan=f_chan,
                   matrix=matrix,
                   e_min=elo,
                   e_max=ehi)
Example #6
0
def make_ideal_rmf(e_min, e_max, offset=1, name='rmf'):
    """A simple in-memory representation of an ideal RMF.

    This RMF represents a 1-to-1 mapping from channel to energy
    bin (i.e. there's no blurring or secondary channels).

    Parameters
    ----------
    e_min, e_max : array
        The energy ranges corresponding to the channels. The units are
        in keV and each bin has energ_hi > energ_lo. The arrays are
        assumed to be ordered, but it is not clear yet whether they
        have to be in ascending order. The sizes must match each
        other. This corresponds to the E_MIN and E_MAX columns of
        the EBOUNDS extension of the RMF file format.
    offset : int, optional
        The value of the first channel (corresponding to the TLMIN
        value of the F_CHAN column of the 'MATRIX' or 'SPECRESP
        MATRIX' block. It is expected to be 0 or 1, but the only
        restriction is that it is 0 or greater.
    name : str, optional
        The name to give to the RMF instance.

    Returns
    -------
    rmf : sherpa.astro.data.DatAMRF
        The RMF.

    """

    elo = np.asarray(e_min)
    ehi = np.asarray(e_max)
    if elo.size != ehi.size:
        raise DataErr('mismatch', 'e_min', 'e_max')
    detchans = elo.size

    if offset < 0:
        raise ArgumentErr('bad', 'offset', 'value can not be negative')

    # The "ideal" matrix is the identity matrix, which, in compressed
    # form, is an array of 1.0's (matrix) and an array of locations
    # giving the column where the element is 1 (fchan). It appears
    # that this uses 1 indexing.
    #
    dummy = np.ones(detchans, dtype=np.int16)
    matrix = np.ones(detchans, dtype=np.float32)
    fchan = np.arange(1, detchans + 1, dtype=np.int16)

    return DataRMF(name=name,
                   detchans=detchans,
                   energ_lo=elo,
                   energ_hi=ehi,
                   n_grp=dummy,
                   n_chan=dummy,
                   f_chan=fchan,
                   matrix=matrix,
                   offset=offset)
Example #7
0
    def startup(self, cache):
        rmf = self._rmf  # original

        # Create a view of original RMF
        self.rmf = DataRMF(rmf.name, rmf.detchans, rmf.energ_lo, rmf.energ_hi,
                           rmf.n_grp, rmf.f_chan, rmf.n_chan, rmf.matrix,
                           rmf.offset, rmf.e_min, rmf.e_max, rmf.header)

        # Filter the view for current fitting session
        _notice_resp(self.pha.get_noticed_channels(), None, self.rmf)

        self.filter()

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

        RMFModel.startup(self, cache)
Example #8
0
def test_rmf_checks_energy_length():
    """Just check we error out"""

    elo = np.arange(1, 5)
    ehi = np.arange(2, 9)
    dummy = []

    with pytest.raises(ValueError) as ve:
        DataRMF("dummy", 1024, elo, ehi, dummy, dummy, dummy, dummy)

    assert str(ve.value) == "The energy arrays must have the same size, not 4 and 7"
Example #9
0
def create_delta_rmf(rmflo, rmfhi, startchan=1,
                     e_min=None, e_max=None, ethresh=None):
    """Create a RMF for a delta-function response.

    This is a "perfect" (delta-function) response.

    Parameters
    ----------
    rmflo, rmfhi : array
        The energy bins (low and high, in keV) for the RMF.
        It is assumed that emfhi_i > rmflo_i, rmflo_j > 0, that the energy
        bins are either ascending, so rmflo_i+1 > rmflo_i or descending
        (rmflo_i+1 < rmflo_i), and that there are no overlaps.
        These correspond to the Elow and Ehigh columns (represented
        by the ENERG_LO and ENERG_HI columns of the MATRIX block) of
        the OGIP standard.
    startchan : int, optional
        The starting channel number: expected to be 0 or 1 but this is
        not enforced.
    e_min, e_max : None or array, optional
        The E_MIN and E_MAX columns of the EBOUNDS block of the
        RMF.
    ethresh : number or None, optional
        Passed through to the DataARF call. It controls whether
        zero-energy bins are replaced.

    Returns
    -------
    rmf : DataRMF instance

    Notes
    -----
    I do not think I have the startchan=0 case correct (does the
    f_chan array have to change?).
    """

    assert rmflo.size == rmfhi.size
    assert startchan >= 0

    # Set up the delta-function response.
    # TODO: should f_chan start at startchan?
    #
    nchans = rmflo.size
    matrix = np.ones(nchans, dtype=np.float32)
    dummy = np.ones(nchans, dtype=np.int16)
    f_chan = np.arange(1, nchans + 1, dtype=np.int16)

    return DataRMF('delta-rmf', detchans=nchans,
                   energ_lo=rmflo, energ_hi=rmfhi,
                   n_grp=dummy, n_chan=dummy,
                   f_chan=f_chan, matrix=matrix,
                   offset=startchan,
                   e_min=e_min, e_max=e_max,
                   ethresh=ethresh)
Example #10
0
def test_rmf_invalid_offset():
    """Just check we error out"""

    elo = np.arange(1, 5)
    ehi = elo + 1
    dummy = []

    with pytest.raises(ValueError) as ve:
        DataRMF("dummy", 1024, elo, ehi, dummy, dummy, dummy, dummy, offset=-1)

    assert str(ve.value) == "offset must be >=0, not -1"
Example #11
0
    def __getattr__(self, name):
        rmf = None
        try:
            rmf = RMF1D.__getattribute__(self, '_rmf')
        except:
            pass

        if name in ('_rmf', '_pha'):
            return self.__dict__[name]

        if rmf is not None:
            return DataRMF.__getattribute__(rmf, name)

        return RMF1D.__getattribute__(self, name)
Example #12
0
    def __getattr__(self, name):
        rmf = None
        try:
            rmf = RMF1D.__getattribute__(self, '_rmf')
        except:
            pass

        if name in ('_rmf', '_pha'):
            return self.__dict__[name]

        if rmf is not None:
            return DataRMF.__getattribute__(rmf, name)

        return RMF1D.__getattribute__(self, name)
Example #13
0
    def startup(self):
        arf = self._arf
        rmf = self._rmf

        # Create a view of original RMF
        self.rmf = DataRMF(rmf.name, rmf.detchans, rmf.energ_lo, rmf.energ_hi,
                           rmf.n_grp, rmf.f_chan, rmf.n_chan, rmf.matrix,
                           rmf.offset, rmf.e_min, rmf.e_max, rmf.header)

        # Create a view of original ARF
        self.arf = DataARF(arf.name, arf.energ_lo, arf.energ_hi, arf.specresp,
                           arf.bin_lo, arf.bin_hi, arf.exposure, arf.header)

        # Filter the view for current fitting session
        _notice_resp(self.pha.get_noticed_channels(), self.arf, self.rmf)

        self.filter()

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

        RSPModel.startup(self)
Example #14
0
    def to_sherpa(self, name):
        """Convert to `sherpa.astro.data.DataRMF`.

        Parameters
        ----------
        name : str
            Instance name
        """
        from sherpa.astro.data import DataRMF
        from sherpa.utils import SherpaUInt, SherpaFloat

        # Need to modify RMF data
        # see https://github.com/sherpa/sherpa/blob/master/sherpa/astro/io/pyfits_backend.py#L727

        table = self.to_table()
        n_grp = table["N_GRP"].data.astype(SherpaUInt)
        f_chan = table["F_CHAN"].data
        n_chan = table["N_CHAN"].data
        matrix = table["MATRIX"].data

        good = n_grp > 0
        matrix = matrix[good]
        matrix = np.concatenate([row for row in matrix])
        matrix = matrix.astype(SherpaFloat)

        good = n_grp > 0
        f_chan = f_chan[good]
        f_chan = np.concatenate([row for row in f_chan]).astype(SherpaUInt)
        n_chan = n_chan[good]
        n_chan = np.concatenate([row for row in n_chan]).astype(SherpaUInt)

        energy = self.e_reco.edges.to_value("keV")
        return DataRMF(
            name=name,
            energ_lo=table["ENERG_LO"].quantity.to_value("keV").astype(
                SherpaFloat),
            energ_hi=table["ENERG_HI"].quantity.to_value("keV").astype(
                SherpaFloat),
            matrix=matrix,
            n_grp=n_grp,
            n_chan=n_chan,
            f_chan=f_chan,
            detchans=self.e_reco.nbin,
            e_min=energy[:-1],
            e_max=energy[1:],
            offset=0,
        )
Example #15
0
def read_rmf(arg):
    """Create a DataRMF object.

    Parameters
    ----------
    arg
        The name of the file or a representation of the file
        (the type depends on the I/O backend) containing the
        RMF data.

    Returns
    -------
    data : sherpa.astro.data.DataRMF

    """
    data, filename = backend.get_rmf_data(arg)
    return DataRMF(filename, **data)
Example #16
0
    def to_sherpa(self, name):
        """Convert to `sherpa.astro.data.DataARF`.

        Parameters
        ----------
        name : str
            Instance name
        """
        from sherpa.astro.data import DataRMF
        from sherpa.utils import SherpaInt, SherpaUInt, SherpaFloat

        # Need to modify RMF data
        # see https://github.com/sherpa/sherpa/blob/master/sherpa/astro/io/pyfits_backend.py#L727

        table = self.to_table()
        n_grp = table['N_GRP'].data.astype(SherpaUInt)
        f_chan = table['F_CHAN'].data
        f_chan = np.concatenate([row for row in f_chan]).astype(SherpaUInt)
        n_chan = table['N_CHAN'].data
        n_chan = np.concatenate([row for row in n_chan]).astype(SherpaUInt)
        matrix = table['MATRIX'].data

        good = n_grp > 0
        matrix = matrix[good]
        matrix = np.concatenate([row for row in matrix])
        matrix = matrix.astype(SherpaFloat)

        good = n_grp > 0
        f_chan = f_chan[good]
        n_chan = n_chan[good]

        return DataRMF(
            name=name,
            energ_lo=table['ENERG_LO'].quantity.to('keV').value.astype(
                SherpaFloat),
            energ_hi=table['ENERG_HI'].quantity.to('keV').value.astype(
                SherpaFloat),
            matrix=matrix,
            n_grp=n_grp,
            n_chan=n_chan,
            f_chan=f_chan,
            detchans=self.e_reco.nbins,
            e_min=self.e_reco.lo.to('keV').value,
            e_max=self.e_reco.hi.to('keV').value,
            offset=0,
        )
Example #17
0
    def startup(self):
        rmf = self._rmf  # original

        # Create a view of original RMF
        self.rmf = DataRMF(rmf.name, rmf.detchans, rmf.energ_lo, rmf.energ_hi,
                           rmf.n_grp, rmf.f_chan, rmf.n_chan, rmf.matrix,
                           rmf.offset, rmf.e_min, rmf.e_max, rmf.header)

        # Filter the view for current fitting session
        _notice_resp(self.pha.get_noticed_channels(), None, self.rmf)

        self.filter()

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

        RMFModel.startup(self)
Example #18
0
class RSPModelPHA(RSPModel):

    """
    RMF + ARF convolution model with associated PHA
    """

    def __init__(self, arf, rmf, pha, model):
        self.pha = pha
        self._arf = arf
        self._rmf = rmf
        RSPModel.__init__(self, arf, rmf, model)

    def filter(self):

        RSPModel.filter(self)

        pha = self.pha
        # If PHA is a finer grid than RMF, evaluate model on PHA and
        # rebin down to the granularity that the RMF expects.
        if pha.bin_lo is not None and pha.bin_hi is not None:
            bin_lo, bin_hi = pha.bin_lo, pha.bin_hi

            # If PHA grid is in angstroms then convert to keV for
            # consistency
            if (bin_lo[0] > bin_lo[-1]) and (bin_hi[0] > bin_hi[-1]):
                bin_lo = DataPHA._hc / pha.bin_hi
                bin_hi = DataPHA._hc / pha.bin_lo

            # FIXME: What about filtered option?? bin_lo, bin_hi are
            # unfiltered??

            # Compare disparate grids in energy space
            self.arfargs = ((self.elo, self.ehi), (bin_lo, bin_hi))

            # FIXME: Assumes ARF grid is finest

        elo, ehi = self.rmf.get_indep()
        # self.elo, self.ehi are from ARF
        if len(elo) != len(self.elo) and len(ehi) != len(self.ehi):

            self.rmfargs = ((elo, ehi), (self.elo, self.ehi))

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

    def startup(self):
        arf = self._arf
        rmf = self._rmf

        # Create a view of original RMF
        self.rmf = DataRMF(rmf.name, rmf.detchans, rmf.energ_lo, rmf.energ_hi,
                           rmf.n_grp, rmf.f_chan, rmf.n_chan, rmf.matrix,
                           rmf.offset, rmf.e_min, rmf.e_max, rmf.header)

        # Create a view of original ARF
        self.arf = DataARF(arf.name, arf.energ_lo, arf.energ_hi, arf.specresp,
                           arf.bin_lo, arf.bin_hi, arf.exposure, arf.header)

        # Filter the view for current fitting session
        _notice_resp(self.pha.get_noticed_channels(), self.arf, self.rmf)

        self.filter()

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

        RSPModel.startup(self)

    def teardown(self):
        self.arf = self._arf  # restore originals
        self.rmf = self._rmf

        self.filter()
        RSPModel.teardown(self)

    def calc(self, p, x, xhi=None, *args, **kwargs):
        # x could be channels or x, xhi could be energy|wave

        src = self.model.calc(p, self.xlo, self.xhi)
        src = self.arf.apply_arf(src, *self.arfargs)
        return self.rmf.apply_rmf(src, *self.rmfargs)
Example #19
0
class RSPModelPHA(RSPModel):
    """RMF + ARF convolution model with associated PHA.

    Notes
    -----
    Scaling by the AREASCAL setting (scalar or array) is included in
    this model.
    """
    def __init__(self, arf, rmf, pha, model):
        self.pha = pha
        self._arf = arf
        self._rmf = rmf
        RSPModel.__init__(self, arf, rmf, model)

    def filter(self):

        RSPModel.filter(self)

        pha = self.pha
        # If PHA is a finer grid than RMF, evaluate model on PHA and
        # rebin down to the granularity that the RMF expects.
        if pha.bin_lo is not None and pha.bin_hi is not None:
            bin_lo, bin_hi = pha.bin_lo, pha.bin_hi

            # If PHA grid is in angstroms then convert to keV for
            # consistency
            if (bin_lo[0] > bin_lo[-1]) and (bin_hi[0] > bin_hi[-1]):
                bin_lo = DataPHA._hc / pha.bin_hi
                bin_hi = DataPHA._hc / pha.bin_lo

            # FIXME: What about filtered option?? bin_lo, bin_hi are
            # unfiltered??

            # Compare disparate grids in energy space
            self.arfargs = ((self.elo, self.ehi), (bin_lo, bin_hi))

            # FIXME: Assumes ARF grid is finest

        elo, ehi = self.rmf.get_indep()
        # self.elo, self.ehi are from ARF
        if len(elo) != len(self.elo) and len(ehi) != len(self.ehi):

            self.rmfargs = ((elo, ehi), (self.elo, self.ehi))

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

    def startup(self, cache):
        arf = self._arf
        rmf = self._rmf

        # Create a view of original RMF
        self.rmf = DataRMF(rmf.name, rmf.detchans, rmf.energ_lo, rmf.energ_hi,
                           rmf.n_grp, rmf.f_chan, rmf.n_chan, rmf.matrix,
                           rmf.offset, rmf.e_min, rmf.e_max, rmf.header)

        # Create a view of original ARF
        self.arf = DataARF(arf.name, arf.energ_lo, arf.energ_hi, arf.specresp,
                           arf.bin_lo, arf.bin_hi, arf.exposure, arf.header)

        # Filter the view for current fitting session
        _notice_resp(self.pha.get_noticed_channels(), self.arf, self.rmf)

        self.filter()

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

        RSPModel.startup(self, cache)

    def teardown(self):
        self.arf = self._arf  # restore originals
        self.rmf = self._rmf

        self.filter()
        RSPModel.teardown(self)

    def calc(self, p, x, xhi=None, *args, **kwargs):
        # x could be channels or x, xhi could be energy|wave

        src = self.model.calc(p, self.xlo, self.xhi)
        src = self.arf.apply_arf(src, *self.arfargs)
        src = self.rmf.apply_rmf(src, *self.rmfargs)

        # Assume any issues with the binning (between AREASCAL
        # and src) is related to the RMF rather than the ARF.
        return apply_areascal(src, self.pha, "RMF: {}".format(self.rmf.name))
Example #20
0
class RMFModelPHA(RMFModel):

    """
    RMF convolution model with associated PHA
    """

    def __init__(self, rmf, pha, model):
        self.pha = pha
        self._rmf = rmf  # store a reference to original
        RMFModel.__init__(self, rmf, model)

    def filter(self):

        RMFModel.filter(self)

        pha = self.pha
        # If PHA is a finer grid than RMF, evaluate model on PHA and
        # rebin down to the granularity that the RMF expects.
        if pha.bin_lo is not None and pha.bin_hi is not None:
            bin_lo, bin_hi = pha.bin_lo, pha.bin_hi

            # If PHA grid is in angstroms then convert to keV for
            # consistency
            if (bin_lo[0] > bin_lo[-1]) and (bin_hi[0] > bin_hi[-1]):
                bin_lo = DataPHA._hc / pha.bin_hi
                bin_hi = DataPHA._hc / pha.bin_lo

            # FIXME: What about filtered option?? bin_lo, bin_hi are
            # unfiltered??

            # Compare disparate grids in energy space
            self.rmfargs = ((self.elo, self.ehi), (bin_lo, bin_hi))

            # FIXME: Compute on finer energy grid?  Assumes that PHA has
            # finer grid than RMF
            self.elo, self.ehi = bin_lo, bin_hi

            # Wavelength grid (angstroms)
            self.lo, self.hi = DataPHA._hc / self.ehi, DataPHA._hc / self.elo

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

    def startup(self):
        rmf = self._rmf  # original

        # Create a view of original RMF
        self.rmf = DataRMF(rmf.name, rmf.detchans, rmf.energ_lo, rmf.energ_hi,
                           rmf.n_grp, rmf.f_chan, rmf.n_chan, rmf.matrix,
                           rmf.offset, rmf.e_min, rmf.e_max, rmf.header)

        # Filter the view for current fitting session
        _notice_resp(self.pha.get_noticed_channels(), None, self.rmf)

        self.filter()

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

        RMFModel.startup(self)

    def teardown(self):
        self.rmf = self._rmf

        self.filter()
        RMFModel.teardown(self)

    def calc(self, p, x, xhi=None, *args, **kwargs):
        # x is noticed/full channels here

        src = self.model.calc(p, self.xlo, self.xhi)
        return self.rmf.apply_rmf(src, *self.rmfargs)
def create_non_delta_rmf():
    """Create a RMF which does not have a delta-function response.

    This is hard-coded to have a range of behavior: some energies
    it is a delta function, some a small (width) response, and some multiple
    peaks.

    Returns
    -------
    rmf : DataRMF instance

    """

    startchan = 1

    # e_min/max define the "X" axis of the matrix (which is the
    # number of channels)
    # energ_lo/hi define the "Y" axis of the matrix
    #
    # The bins do not have to be constant-width
    echan = np.asarray([0.1, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0])
    e_min = echan[:-1]
    e_max = echan[1:]

    nchans = e_min.size

    energ = np.arange(0.05, 1.1, 0.05)
    energ_lo = energ[:-1]
    energ_hi = energ[1:]

    # Deconstruct the matrix to get the "condensed" representation
    # used by the RMF.
    #
    matrix = get_non_delta_matrix()
    n_grp = []
    n_chan = []
    f_chan = []
    for row in (matrix > 0):
        # simple run-length encoding, bsaed on code in
        # https://stackoverflow.com/questions/1066758/find-length-of-sequences-of-identical-values-in-a-numpy-array
        #
        flag = np.hstack([[0], row, [0]])
        diffs = np.diff(flag, n=1)
        starts, = np.where(diffs > 0)
        ends, = np.where(diffs < 0)

        n_chan.extend(ends - starts)
        f_chan.extend(starts + 1)
        n_grp.append(len(starts))

    matrix = matrix.flatten()
    matrix = matrix[matrix > 0]

    n_grp = np.asarray(n_grp, dtype=np.int16)
    f_chan = np.asarray(f_chan, dtype=np.int16)
    n_chan = np.asarray(n_chan, dtype=np.int16)

    return DataRMF('non-delta-rmf',
                   detchans=nchans,
                   energ_lo=energ_lo,
                   energ_hi=energ_hi,
                   n_grp=n_grp,
                   n_chan=n_chan,
                   f_chan=f_chan,
                   matrix=matrix,
                   offset=startchan,
                   e_min=e_min,
                   e_max=e_max)
Example #22
0
def derive_identity_rmf(name, rmf):
    """Create an "identity" RMF that does not mix energies.

    *name*
      The name of the RMF object to be created; passed to Sherpa.
    *rmf*
      An existing RMF object on which to base this one.
    Returns:
      A new RMF1D object that has a response matrix that is as close to
      diagonal as we can get in energy space, and that has a constant
      sensitivity as a function of detector channel.

    In many X-ray observations, the relevant background signal does not behave
    like an astrophysical source that is filtered through the telescope's
    response functions. However, I have been unable to get current Sherpa
    (version 4.9) to behave how I want when working with backround models that
    are *not* filtered through these response functions. This function
    constructs an "identity" RMF response matrix that provides the best
    possible approximation of a passthrough "instrumental response": it mixes
    energies as little as possible and has a uniform sensitivity as a function
    of detector channel.

    """
    from sherpa.astro.data import DataRMF
    from sherpa.astro.instrument import RMF1D

    # The "x" axis of the desired matrix -- the columnar direction; axis 1 --
    # is "channels". There are n_chan of them and each maps to a notional
    # energy range specified by "e_min" and "e_max".
    #
    # The "y" axis of the desired matrix -- the row direction; axis 1 -- is
    # honest-to-goodness energy. There are tot_n_energy energy bins, each
    # occupying a range specified by "energ_lo" and "energ_hi".
    #
    # We want every channel that maps to a valid output energy to have a
    # nonzero entry in the matrix. The relative sizes of n_energy and n_cell
    # can vary, as can the bounds of which regions of each axis can be validly
    # mapped to each other. So this problem is basically equivalent to that of
    # drawing an arbitrary pixelated line on bitmap, without anti-aliasing.
    #
    # The output matrix is represented in a row-based sparse format.
    #
    # - There is a integer vector "n_grp" of size "n_energy". It gives the
    #   number of "groups" needed to fill in each row of the matrix. Let
    #   "tot_groups = sum(n_grp)". For a given row, "n_grp[row_index]" may
    #   be zero, indicating that the row is all zeros.
    # - There are integer vectors "f_chan" and "n_chan", each of size
    #   "tot_groups", that define each group. "f_chan" gives the index of
    #   the first channel column populated by the group; "n_chan" gives the
    #   number of columns populated by the group. Note that there can
    #   be multiple groups for a single row, so successive group records
    #   may fill in different pieces of the same row.
    # - Let "tot_cells = sum(n_chan)".
    # - There is a vector "matrix" of size "tot_cells" that stores the actual
    #   matrix data. This is just a concatenation of all the data corresponding
    #   to each group.
    # - Unpopulated matrix entries are zero.
    #
    # See expand_rmf_matrix() for a sloppy implementation of how to unpack
    # this sparse format.

    n_chan = rmf.e_min.size
    n_energy = rmf.energ_lo.size

    c_lo_offset = rmf.e_min[0]
    c_lo_slope = (rmf.e_min[-1] - c_lo_offset) / (n_chan - 1)

    c_hi_offset = rmf.e_max[0]
    c_hi_slope = (rmf.e_max[-1] - c_hi_offset) / (n_chan - 1)

    e_lo_offset = rmf.energ_lo[0]
    e_lo_slope = (rmf.energ_lo[-1] - e_lo_offset) / (n_energy - 1)

    e_hi_offset = rmf.energ_hi[0]
    e_hi_slope = (rmf.energ_hi[-1] - e_hi_offset) / (n_energy - 1)

    all_e_indices = np.arange(n_energy)
    all_e_los = e_lo_slope * all_e_indices + e_lo_offset
    start_chans = np.floor(
        (all_e_los - c_lo_offset) / c_lo_slope).astype(np.int)

    all_e_his = e_hi_slope * all_e_indices + e_hi_offset
    stop_chans = np.ceil((all_e_his - c_hi_offset) / c_hi_slope).astype(np.int)

    first_e_index_on_channel_grid = 0
    while stop_chans[first_e_index_on_channel_grid] < 0:
        first_e_index_on_channel_grid += 1

    last_e_index_on_channel_grid = n_energy - 1
    while start_chans[last_e_index_on_channel_grid] >= n_chan:
        last_e_index_on_channel_grid -= 1

    n_nonzero_rows = last_e_index_on_channel_grid + 1 - first_e_index_on_channel_grid
    e_slice = slice(first_e_index_on_channel_grid,
                    last_e_index_on_channel_grid + 1)
    n_grp = np.zeros(n_energy, dtype=np.int)
    n_grp[e_slice] = 1

    start_chans = np.maximum(start_chans[e_slice], 0)
    stop_chans = np.minimum(stop_chans[e_slice], n_chan - 1)

    # We now have a first cut at a row-oriented expression of our "identity"
    # RMF. However, it's conservative. Trim down to eliminate overlaps between
    # sequences.

    for i in range(n_nonzero_rows - 1):
        my_end = stop_chans[i]
        next_start = start_chans[i + 1]
        if next_start <= my_end:
            stop_chans[i] = max(start_chans[i], next_start - 1)

    # Results are funky unless the sums along the vertical axis are constant.
    # Ideally the sum along the *horizontal* axis would add up to 1 (since,
    # ideally, each row is a probability distribution), but it is not
    # generally possible to fulfill both of these constraints simultaneously.
    # The latter constraint does not seem to matter in practice so we ignore it.
    # Due to the funky encoding of the matrix, we need to build a helper table
    # to meet the vertical-sum constraint.

    counts = np.zeros(n_chan, dtype=np.int)

    for i in range(n_nonzero_rows):
        counts[start_chans[i]:stop_chans[i] + 1] += 1

    counts[:start_chans.min()] = 1
    counts[stop_chans.max() + 1:] = 1
    assert (counts > 0).all()

    # We can now build the matrix.

    f_chan = start_chans
    rmfnchan = stop_chans + 1 - f_chan
    assert (rmfnchan > 0).all()

    matrix = np.zeros(rmfnchan.sum())
    amounts = 1. / counts
    ofs = 0

    for i in range(n_nonzero_rows):
        f = f_chan[i]
        n = rmfnchan[i]
        matrix[ofs:ofs + n] = amounts[f:f + n]
        ofs += n

    # All that's left to do is create the Python objects.

    drmf = DataRMF(name,
                   rmf.detchans,
                   rmf.energ_lo,
                   rmf.energ_hi,
                   n_grp,
                   f_chan,
                   rmfnchan,
                   matrix,
                   offset=0,
                   e_min=rmf.e_min,
                   e_max=rmf.e_max,
                   header=None)

    return RMF1D(drmf, pha=rmf._pha)
Example #23
0
class RMFModelPHA(RMFModel):
    """RMF convolution model with associated PHA data set.

    Notes
    -----
    Scaling by the AREASCAL setting (scalar or array) is included in
    this model.
    """
    def __init__(self, rmf, pha, model):
        self.pha = pha
        self._rmf = rmf  # store a reference to original
        RMFModel.__init__(self, rmf, model)

    def filter(self):

        RMFModel.filter(self)

        pha = self.pha
        # If PHA is a finer grid than RMF, evaluate model on PHA and
        # rebin down to the granularity that the RMF expects.
        if pha.bin_lo is not None and pha.bin_hi is not None:
            bin_lo, bin_hi = pha.bin_lo, pha.bin_hi

            # If PHA grid is in angstroms then convert to keV for
            # consistency
            if (bin_lo[0] > bin_lo[-1]) and (bin_hi[0] > bin_hi[-1]):
                bin_lo = DataPHA._hc / pha.bin_hi
                bin_hi = DataPHA._hc / pha.bin_lo

            # FIXME: What about filtered option?? bin_lo, bin_hi are
            # unfiltered??

            # Compare disparate grids in energy space
            self.rmfargs = ((self.elo, self.ehi), (bin_lo, bin_hi))

            # FIXME: Compute on finer energy grid?  Assumes that PHA has
            # finer grid than RMF
            self.elo, self.ehi = bin_lo, bin_hi

            # Wavelength grid (angstroms)
            self.lo, self.hi = DataPHA._hc / self.ehi, DataPHA._hc / self.elo

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

    def startup(self, cache):
        rmf = self._rmf  # original

        # Create a view of original RMF
        self.rmf = DataRMF(rmf.name, rmf.detchans, rmf.energ_lo, rmf.energ_hi,
                           rmf.n_grp, rmf.f_chan, rmf.n_chan, rmf.matrix,
                           rmf.offset, rmf.e_min, rmf.e_max, rmf.header)

        # Filter the view for current fitting session
        _notice_resp(self.pha.get_noticed_channels(), None, self.rmf)

        self.filter()

        # Assume energy as default spectral coordinates
        self.xlo, self.xhi = self.elo, self.ehi
        if self.pha.units == 'wavelength':
            self.xlo, self.xhi = self.lo, self.hi

        RMFModel.startup(self, cache)

    def teardown(self):
        self.rmf = self._rmf

        self.filter()
        RMFModel.teardown(self)

    def calc(self, p, x, xhi=None, *args, **kwargs):
        # x is noticed/full channels here

        src = self.model.calc(p, self.xlo, self.xhi)
        out = self.rmf.apply_rmf(src, *self.rmfargs)

        return apply_areascal(out, self.pha, "RMF: {}".format(self.rmf.name))