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
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)