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