def test_rmf1d_no_pha_matrix():
    "Can we create an RMF (matrix) with no PHA?"

    rdata = create_non_delta_rmf()
    rmf = RMF1D(rdata)

    assert rmf._rmf == rdata
    assert rmf._pha is None

    # Does the RMF1D pass through functionality to the DataRMF?
    assert rmf.name == 'non-delta-rmf'
    assert str(rmf) == str(rdata)

    assert dir(rmf) == dir(rdata)

    # We do not do a full test, just some global checks and a few
    # spot checks
    rmatrix = rmf.matrix
    assert rmatrix.sum() == pytest.approx(20.0)

    expected = [1.0, 0.4, 0.2, 1.0]
    assert_allclose(rmatrix[[0, 7, 19, 23]], expected)

    elo = np.linspace(0.05, 1.0, 20)
    assert_allclose(rmf.energ_lo, elo)

    emin = [0.1, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
    assert_allclose(rmf.e_min, emin)

    assert (rmf.energ_hi > rmf.energ_lo).all()
    assert (rmf.e_max > rmf.e_min).all()

    # Unlike the ARF, the RMF doesn't have an exposure field
    with pytest.raises(AttributeError):
        rmf.exposure
def test_rmf1d_matrix_arf_no_pha_call():
    "Can we call an RMF () with ARF (no PHA)"

    # NOTE: there is no check that the grids are compatible
    #       so this is probably not that useful a test
    #
    rdata = create_non_delta_rmf()
    elo = rdata.e_min
    ehi = rdata.e_max
    adata = create_arf(elo, ehi)
    rmf = RMF1D(rdata, arf=adata)

    mdl = Const1D('flat')
    mdl.c0 = 2.3

    wrapped = rmf(mdl)

    # It seems like the wrapper should match the following:
    #     assert isinstance(wrapped, RSPModelNoPHA)
    # but at the time the test was written (June 2017) it
    # does not.
    #
    assert isinstance(wrapped, RMFModelNoPHA)

    wmdl = wrapped.model
    assert wmdl == mdl
def test_rmf1d_delta_no_pha_zero_energy_bin_replace():
    "What happens when the first bin starts at 0, with replacement"

    ethresh = 1e-8

    egrid = np.asarray([0.0, 0.1, 0.2, 0.4, 0.5, 0.7, 0.8])
    elo = egrid[:-1]
    ehi = egrid[1:]

    with warnings.catch_warnings(record=True) as ws:
        warnings.simplefilter("always")
        rdata = create_delta_rmf(elo, ehi, ethresh=ethresh)

    validate_zero_replacement(ws, 'RMF', 'delta-rmf', ethresh)

    rmf = RMF1D(rdata)

    mdl = MyPowLaw1D()
    tmdl = PowLaw1D()

    wrapped = rmf(mdl)

    out = wrapped([0.1, 0.2])

    elo[0] = ethresh
    expected = tmdl(elo, ehi)

    assert_allclose(out, expected)
    assert not np.isnan(out[0])
def test_rmf1d_matrix_no_pha_call():
    "Can we call an RMF (delta function) with no PHA"

    rdata = create_non_delta_rmf()
    rmf = RMF1D(rdata)

    mdl = Const1D('flat')
    mdl.c0 = 2.3

    wrapped = rmf(mdl)
    assert isinstance(wrapped, RMFModelNoPHA)

    wmdl = wrapped.model
    assert wmdl == mdl
def test_rmf1d_empty():

    rmf = RMF1D(None)

    assert rmf._rmf is None
    assert rmf._pha is None

    assert str(rmf) == str(None)

    # Since there's no RMF to fall through, it should be an error
    # to access the name attribute.
    #
    with pytest.raises(AttributeError):
        rmf.name
def test_rmf1d_simple_no_pha_call():
    "Can we call an RMF (delta function) with no PHA"

    egrid = np.arange(0.1, 0.6, 0.1)
    rdata = create_delta_rmf(egrid[:-1], egrid[1:])
    rmf = RMF1D(rdata)

    mdl = Const1D('flat')
    mdl.c0 = 2.3

    wrapped = rmf(mdl)
    assert isinstance(wrapped, RMFModelNoPHA)

    wmdl = wrapped.model
    assert wmdl == mdl
def test_rmf1d_no_pha_delta():
    "Can we create an RMF (delta function) with no PHA?"

    egrid = np.arange(0.1, 0.6, 0.1)
    rdata = create_delta_rmf(egrid[:-1], egrid[1:])
    rmf = RMF1D(rdata)

    assert rmf._rmf == rdata
    assert rmf._pha is None

    # Does the RMF1D pass through functionality to the DataRMF?
    assert rmf.name == 'delta-rmf'
    assert str(rmf) == str(rdata)

    assert dir(rmf) == dir(rdata)

    matrix = np.ones(egrid.size - 1, dtype=np.float32)
    assert (matrix == rmf.matrix).all()

    # Unlike the ARF, the RMF doesn't have an exposure field
    with pytest.raises(AttributeError):
        rmf.exposure
def test_rmf1d_delta_arf_no_pha_call():
    "Can we call an RMF (delta function) with ARF (no PHA)"

    egrid = np.arange(0.1, 0.6, 0.1)
    elo = egrid[:-1]
    ehi = egrid[1:]
    rdata = create_delta_rmf(elo, ehi)
    adata = create_arf(elo, ehi)
    rmf = RMF1D(rdata, arf=adata)

    mdl = Const1D('flat')
    mdl.c0 = 2.3

    wrapped = rmf(mdl)

    # It seems like the wrapper should match the following:
    #     assert isinstance(wrapped, RSPModelNoPHA)
    # but at the time the test was written (June 2017) it
    # does not.
    #
    assert isinstance(wrapped, RMFModelNoPHA)

    wmdl = wrapped.model
    assert wmdl == mdl
Пример #9
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)