From c2e4cbcfb33f9a4801bdf5bb5532267d09344d4f Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 1 May 2018 17:26:21 -0400 Subject: [PATCH 01/72] First pass at XraySpectrum1D --- specutils/spectra/xrayspectrum1d.py | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 specutils/spectra/xrayspectrum1d.py diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py new file mode 100644 index 000000000..b7556ad77 --- /dev/null +++ b/specutils/spectra/xrayspectrum1d.py @@ -0,0 +1,47 @@ +import logging + +import numpy as np +from astropy import units as u +from .spectrum1d import Spectrum1D + +__all__ = ['XraySpectrum1D'] + +# For dealing with varied unit string choices +EV = ['eV', 'ev'] +KEV = ['kev', 'keV'] +ANGS = ['angs', 'Angs', 'Angstrom', 'angstrom', 'Angstroms', 'angstroms', 'A', 'a'] + +def _unit_parser(unit_string): + if unit_string in EV: + return u.eV + if unit_string in KEV: + return u.keV + if unit_string in ANGS: + return u.angstrom + +class XraySpectrum1D(Spectrum1D): + """ + Spectrum container for holding X-ray spectrum + WIP by eblur + + Parameters + ---------- + bin_lo + bin_hi + bin_unit + counts + arf + rmf + """ + def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): + try: + axis_unit = u.Unit(bin_unit) + else: + axis_unit = _unit_parser(bin_unit) + + bin_mid = 0.5 * (bin_lo + bin_hi) * unit + Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + + self.arf = arf + self.rmf = rmf + return From 20c8b289b212ab8a00aad758db7149dc9bb34965 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 1 May 2018 17:40:28 -0400 Subject: [PATCH 02/72] Can load an X-ray spectrum --- specutils/spectra/__init__.py | 1 + specutils/spectra/xrayspectrum1d.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/__init__.py b/specutils/spectra/__init__.py index 711a16154..95c052b1f 100644 --- a/specutils/spectra/__init__.py +++ b/specutils/spectra/__init__.py @@ -5,3 +5,4 @@ from __future__ import absolute_import from .spectrum1d import * # noqa +from .xrayspectrum1d import * diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index b7556ad77..d57146f97 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -36,10 +36,10 @@ class XraySpectrum1D(Spectrum1D): def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): try: axis_unit = u.Unit(bin_unit) - else: + except: axis_unit = _unit_parser(bin_unit) - bin_mid = 0.5 * (bin_lo + bin_hi) * unit + bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) self.arf = arf From 6ac1c0841b81ec3efe4531423e9ab25cf1415b32 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 1 May 2018 17:44:41 -0400 Subject: [PATCH 03/72] Added attribute --- specutils/spectra/xrayspectrum1d.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index d57146f97..b939b40d1 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -45,3 +45,8 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): self.arf = arf self.rmf = rmf return + + # Convenience function for Xray people + @property + def counts(self): + return self.flux From 4571350ce927f1dd0934a02b155a9295bb4f138a Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 13:53:39 -0400 Subject: [PATCH 04/72] Added ARF and RMF objects --- specutils/spectra/xrayspectrum1d.py | 326 +++++++++++++++++++++++++++- 1 file changed, 325 insertions(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index b939b40d1..e36a500a6 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -4,7 +4,7 @@ from astropy import units as u from .spectrum1d import Spectrum1D -__all__ = ['XraySpectrum1D'] +__all__ = ['XraySpectrum1D', 'ARF', 'RMF'] # For dealing with varied unit string choices EV = ['eV', 'ev'] @@ -50,3 +50,327 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): @property def counts(self): return self.flux + +## ---- Supporting response file objects + +class RMF(object): + def __init__(self, filename): + self._load_rmf(filename) + pass + + def _load_rmf(self, filename): + """ + Load an RMF from a FITS file. + + Parameters + ---------- + filename : str + The file name with the RMF file + + Attributes + ---------- + n_grp : numpy.ndarray + the Array with the number of channels in each + channel set + + f_chan : numpy.ndarray + The starting channel for each channel group; + If an element i in n_grp > 1, then the resulting + row entry in f_chan will be a list of length n_grp[i]; + otherwise it will be a single number + + n_chan : numpy.ndarray + The number of channels in each channel group. The same + logic as for f_chan applies + + matrix : numpy.ndarray + The redistribution matrix as a flattened 1D vector + + energ_lo : numpy.ndarray + The lower edges of the energy bins + + energ_hi : numpy.ndarray + The upper edges of the energy bins + + detchans : int + The number of channels in the detector + + """ + # open the FITS file and extract the MATRIX extension + # which contains the redistribution matrix and + # anxillary information + hdulist = fits.open(filename) + + # get all the extension names + extnames = np.array([h.name for h in hdulist]) + + # figure out the right extension to use + if "MATRIX" in extnames: + h = hdulist["MATRIX"] + + elif "SPECRESP MATRIX" in extnames: + h = hdulist["SPECRESP MATRIX"] + + data = h.data + hdr = h.header + hdulist.close() + + # extract + store the attributes described in the docstring + n_grp = np.array(data.field("N_GRP")) + f_chan = np.array(data.field('F_CHAN')) + n_chan = np.array(data.field("N_CHAN")) + matrix = np.array(data.field("MATRIX")) + + self.energ_lo = np.array(data.field("ENERG_LO")) + self.energ_hi = np.array(data.field("ENERG_HI")) + self.energ_unit = data.columns["ENERG_LO"].unit + self.detchans = hdr["DETCHANS"] + self.offset = self.__get_tlmin(h) + + # flatten the variable-length arrays + self.n_grp, self.f_chan, self.n_chan, self.matrix = \ + self._flatten_arrays(n_grp, f_chan, n_chan, matrix) + + return + + def __get_tlmin(self, h): + """ + Get the tlmin keyword for `F_CHAN`. + + Parameters + ---------- + h : an astropy.io.fits.hdu.table.BinTableHDU object + The extension containing the `F_CHAN` column + + Returns + ------- + tlmin : int + The tlmin keyword + """ + # get the header + hdr = h.header + # get the keys of all + keys = np.array(list(hdr.keys())) + + # find the place where the tlmin keyword is defined + t = np.array(["TLMIN" in k for k in keys]) + + # get the index of the TLMIN keyword + tlmin_idx = np.hstack(np.where(t))[0] + + # get the corresponding value + tlmin = np.int(list(hdr.items())[tlmin_idx][1]) + + return tlmin + + def _flatten_arrays(self, n_grp, f_chan, n_chan, matrix): + + if not len(n_grp) == len(f_chan) == len(n_chan) == len(matrix): + raise ValueError("Arrays must be of same length!") + + # find all non-zero groups + nz_idx = (n_grp > 0) + + # stack all non-zero rows in the matrix + matrix_flat = np.hstack(matrix[nz_idx]) + + # stack all nonzero rows in n_chan and f_chan + #n_chan_flat = np.hstack(n_chan[nz_idx]) + #f_chan_flat = np.hstack(f_chan[nz_idx]) + + # some matrices actually have more elements + # than groups in `n_grp`, so we'll only pick out + # those values that have a correspondence in + # n_grp + f_chan_new = [] + n_chan_new = [] + for i,t in enumerate(nz_idx): + if t: + n = n_grp[i] + f = f_chan[i] + nc = n_chan[i] + if np.size(f) == 1: + f_chan_new.append(f) + n_chan_new.append(nc) + else: + f_chan_new.append(f[:n]) + n_chan_new.append(nc[:n]) + + n_chan_flat = np.hstack(n_chan_new) + f_chan_flat = np.hstack(f_chan_new) + + # if n_chan is zero, we'll remove those as well. + nz_idx2 = (n_chan_flat > 0) + n_chan_flat = n_chan_flat[nz_idx2] + f_chan_flat = f_chan_flat[nz_idx2] + + return n_grp, f_chan_flat, n_chan_flat, matrix_flat + + def apply_rmf(self, spec): + """ + Fold the spectrum through the redistribution matrix. + + The redistribution matrix is saved as a flattened 1-dimensional + vector to save space. In reality, for each entry in the flux + vector, there exists one or more sets of channels that this + flux is redistributed into. The additional arrays `n_grp`, + `f_chan` and `n_chan` store this information: + * `n_group` stores the number of channel groups for each + energy bin + * `f_chan` stores the *first channel* that each channel + for each channel set + * `n_chan` stores the number of channels in each channel + set + + As a result, for a given energy bin i, we need to look up the + number of channel sets in `n_grp` for that energy bin. We + then need to loop over the number of channel sets. For each + channel set, we look up the first channel into which flux + will be distributed as well as the number of channels in the + group. We then need to also loop over the these channels and + actually use the corresponding elements in the redistribution + matrix to redistribute the photon flux into channels. + + All of this is basically a big bookkeeping exercise in making + sure to get the indices right. + + Parameters + ---------- + spec : numpy.ndarray + The (model) spectrum to be folded + + Returns + ------- + counts : numpy.ndarray + The (model) spectrum after folding, in + counts/s/channel + + """ + # get the number of channels in the data + nchannels = spec.shape[0] + + # an empty array for the output counts + counts = np.zeros(nchannels) + + # index for n_chan and f_chan incrementation + k = 0 + + # index for the response matrix incrementation + resp_idx = 0 + + # loop over all channels + for i in range(nchannels): + + # this is the current bin in the flux spectrum to + # be folded + source_bin_i = spec[i] + + # get the current number of groups + current_num_groups = self.n_grp[i] + + # loop over the current number of groups + for j in range(current_num_groups): + + current_num_chans = int(self.n_chan[k]) + + if current_num_chans == 0: + k += 1 + resp_idx += current_num_chans + continue + + + else: + # get the right index for the start of the counts array + # to put the data into + counts_idx = int(self.f_chan[k] - self.offset) + # this is the current number of channels to use + + k += 1 + # add the flux to the subarray of the counts array that starts with + # counts_idx and runs over current_num_chans channels + counts[counts_idx:counts_idx + + current_num_chans] += self.matrix[resp_idx:resp_idx + + current_num_chans] * \ + np.float(source_bin_i) + # iterate the response index for next round + resp_idx += current_num_chans + + + return counts[:self.detchans] + + +class ARF(object): + + def __init__(self, filename): + + self._load_arf(filename) + pass + + def _load_arf(self, filename): + """ + Load an ARF from a FITS file. + Parameters + ---------- + filename : str + The file name with the RMF file + Attributes + ---------- + """ + # open the FITS file and extract the MATRIX extension + # which contains the redistribution matrix and + # anxillary information + hdulist = fits.open(filename) + h = hdulist["SPECRESP"] + data = h.data + hdr = h.header + hdulist.close() + + # extract + store the attributes described in the docstring + + self.e_low = np.array(data.field("ENERG_LO")) + self.e_high = np.array(data.field("ENERG_HI")) + self.e_unit = data.columns["ENERG_LO"].unit + self.specresp = np.array(data.field("SPECRESP")) + + if "EXPOSURE" in list(hdr.keys()): + self.exposure = hdr["EXPOSURE"] + else: + self.exposure = 1.0 + + if "FRACEXPO" in data.columns.names: + self.fracexpo = data["FRACEXPO"] + else: + self.fracexpo = 1.0 + + return + + def apply_arf(self, spec, exposure=None): + """ + Fold the spectrum through the ARF. + The ARF is a single vector encoding the effective area information + about the detector. A such, applying the ARF is a simple + multiplication with the input spectrum. + Parameters + ---------- + spec : numpy.ndarray + The (model) spectrum to be folded + exposure : float, default None + Value for the exposure time. By default, `apply_arf` will use the + exposure keyword from the ARF file. If this exposure time is not + correct (for example when simulated spectra use a different exposure + time and the ARF from a real observation), one can override the + default exposure by setting the `exposure` keyword to the correct + value. + Returns + ------- + s_arf : numpy.ndarray + The (model) spectrum after folding, in + counts/s/channel + """ + assert spec.shape[0] == self.specresp.shape[0], "The input spectrum must " \ + "be of same size as the " \ + "ARF array." + if exposure is None: + return np.array(spec) * self.specresp * self.exposure + else: + return np.array(spec) * self.specresp * exposure From a92bd09751e7595466ed49bd407d0a94216b585a Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:07:50 -0400 Subject: [PATCH 05/72] Added assign_rmf and assign_arf methods --- specutils/spectra/xrayspectrum1d.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index e36a500a6..a371b94a5 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -42,8 +42,8 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) - self.arf = arf - self.rmf = rmf + self.assign_rmf(rmf) + self.assign_arf(rmf) return # Convenience function for Xray people @@ -51,6 +51,21 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): def counts(self): return self.flux + def assign_arf(self, arf_inp): + if isinstance(arf_inp, str): + self.arf = ARF(arf_inp) + else: + self.arf = arf_inp + return + + def assign_rmf(self, rmf_inp): + if isinstance(rmf_inp, str): + self.rmf = RMF(rmf_inp) + else: + self.rmf = rmf_inp + return + + ## ---- Supporting response file objects class RMF(object): @@ -69,6 +84,9 @@ def _load_rmf(self, filename): Attributes ---------- + filename : str + The file name that the RMF was drawn from + n_grp : numpy.ndarray the Array with the number of channels in each channel set @@ -100,6 +118,7 @@ def _load_rmf(self, filename): # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) + self.filename = filename # get all the extension names extnames = np.array([h.name for h in hdulist]) @@ -309,17 +328,23 @@ def __init__(self, filename): def _load_arf(self, filename): """ Load an ARF from a FITS file. + Parameters ---------- filename : str The file name with the RMF file + Attributes ---------- + filename : str + The file name that the ARF was drawn from """ # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) + self.filename = filename + h = hdulist["SPECRESP"] data = h.data hdr = h.header From 7077dc7187ed413b6e39040b821d57aaa645243d Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:16:13 -0400 Subject: [PATCH 06/72] Added exposure input --- specutils/spectra/xrayspectrum1d.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index a371b94a5..51de9e726 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -33,7 +33,7 @@ class XraySpectrum1D(Spectrum1D): arf rmf """ - def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): + def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None): try: axis_unit = u.Unit(bin_unit) except: @@ -42,6 +42,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(rmf) return @@ -344,7 +345,7 @@ def _load_arf(self, filename): # anxillary information hdulist = fits.open(filename) self.filename = filename - + h = hdulist["SPECRESP"] data = h.data hdr = h.header From 9c8159df3f7b5ce2583459056fca6b7eb77a6a66 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:18:22 -0400 Subject: [PATCH 07/72] Fixed bugs --- specutils/spectra/xrayspectrum1d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 51de9e726..fade12358 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -2,6 +2,7 @@ import numpy as np from astropy import units as u +from astropy.io import fits from .spectrum1d import Spectrum1D __all__ = ['XraySpectrum1D', 'ARF', 'RMF'] @@ -44,7 +45,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=Non self.exposure = exposure self.assign_rmf(rmf) - self.assign_arf(rmf) + self.assign_arf(arf) return # Convenience function for Xray people From fb5f1b6ed5c088bb3aa7ce0e1787dd25f156ad42 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:30:44 -0400 Subject: [PATCH 08/72] Updated docstrings --- specutils/spectra/xrayspectrum1d.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index fade12358..72c5bd11e 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -112,6 +112,9 @@ def _load_rmf(self, filename): energ_hi : numpy.ndarray The upper edges of the energy bins + energ_unit : str + Description of the energy units used + detchans : int The number of channels in the detector @@ -340,6 +343,28 @@ def _load_arf(self, filename): ---------- filename : str The file name that the ARF was drawn from + + e_low : numpy.ndarray + The lower edges of the energy bins + + e_high : numpy.ndarray + The upper edges of the energy bins + + e_unit : str + Description of the energy units used + + specresp : numpy.ndarray + Description of the energy dependent telescope response area + + fracexpo : float or numpy.ndarray + Fractional exposure time for the spectrum + (sometimes constant, sometimes dependent on spectral channel). + These values are stored for reference; generally, they are already + accounted for in the specresp array. + + exposure : + Average exposure time for the dataset + (takes telescope dithering into account) """ # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and From a08d8ae03050f359b7e2dd9b647939133adc2146 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:36:08 -0400 Subject: [PATCH 09/72] Store bin and energy units as astropy.units.Unit --- specutils/spectra/xrayspectrum1d.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 72c5bd11e..2b98164aa 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -31,6 +31,7 @@ class XraySpectrum1D(Spectrum1D): bin_hi bin_unit counts + exposure arf rmf """ @@ -43,6 +44,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=Non bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + self.bin_unit = axis_unit # keep for reference self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) @@ -112,7 +114,7 @@ def _load_rmf(self, filename): energ_hi : numpy.ndarray The upper edges of the energy bins - energ_unit : str + energ_unit : astropy.units.Unit Description of the energy units used detchans : int @@ -147,7 +149,7 @@ def _load_rmf(self, filename): self.energ_lo = np.array(data.field("ENERG_LO")) self.energ_hi = np.array(data.field("ENERG_HI")) - self.energ_unit = data.columns["ENERG_LO"].unit + self.energ_unit = _unit_parser(data.columns["ENERG_LO"].unit) self.detchans = hdr["DETCHANS"] self.offset = self.__get_tlmin(h) @@ -350,7 +352,7 @@ def _load_arf(self, filename): e_high : numpy.ndarray The upper edges of the energy bins - e_unit : str + e_unit : astropy.units.Unit Description of the energy units used specresp : numpy.ndarray @@ -381,7 +383,7 @@ def _load_arf(self, filename): self.e_low = np.array(data.field("ENERG_LO")) self.e_high = np.array(data.field("ENERG_HI")) - self.e_unit = data.columns["ENERG_LO"].unit + self.e_unit = _unit_parser(data.columns["ENERG_LO"].unit) self.specresp = np.array(data.field("SPECRESP")) if "EXPOSURE" in list(hdr.keys()): From 20ee61df2a27bd8ed6b0a1448393906d3d27130a Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 16:31:14 -0400 Subject: [PATCH 10/72] Added bin_lo and hi because wcs.bin_edges did not exist --- specutils/spectra/xrayspectrum1d.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 2b98164aa..d209c988b 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -35,15 +35,18 @@ class XraySpectrum1D(Spectrum1D): arf rmf """ - def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None): + def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, + arf=None, rmf=None, **kwargs): try: axis_unit = u.Unit(bin_unit) except: axis_unit = _unit_parser(bin_unit) bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit - Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts, **kwargs) + self.bin_lo = bin_lo + self.bin_hi = bin_hi self.bin_unit = axis_unit # keep for reference self.exposure = exposure self.assign_rmf(rmf) From 6cd49f0ccb7290f97e6812f5a67cd1b1217050e2 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 16:34:35 -0400 Subject: [PATCH 11/72] Added apply_resp method --- specutils/spectra/xrayspectrum1d.py | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index d209c988b..765e77241 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -72,6 +72,50 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return + def apply_resp(self, mflux, exposure=None): + """ + Given a model flux spectrum, apply the response. In cases where the + spectrum has both an ARF and an RMF, apply both. Otherwise, apply + whatever response is in RMF. + + The model flux spectrum *must* be created using the same units and + bins as in the ARF (where the ARF exists)! + + Parameters + ---------- + mflux : iterable + A list or array with the model flux values in ergs/keV/s/cm^-2 + + exposure : float, default None + By default, the exposure stored in the ARF will be used to compute + the total counts per bin over the effective observation time. + In cases where this might be incorrect (e.g. for simulated spectra + where the pha file might have a different exposure value than the + ARF), this keyword provides the functionality to override the + default behaviour and manually set the exposure time to use. + + Returns + ------- + count_model : numpy.ndarray + The model spectrum in units of counts/bin + + If no ARF file exists, it will return the model flux after applying the RMF + If no RMF file exists, it will return the model flux after applying the ARF (with a warning) + If no ARF and no RMF, it will return the model flux spectrum (with a warning) + """ + + if self.arf is not None: + mrate = self.arf.apply_arf(mflux, exposure=exposure) + else: + mrate = mflux + + if self.rmf is not None: + count_model = self.rmf.apply_rmf(mrate) + return count_model + else: + print("Caution: no response file specified") + return mrate + ## ---- Supporting response file objects From 9c08caaf324c4258e61b046b36d2b0f7ddea0eb4 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 16:52:47 -0400 Subject: [PATCH 12/72] Adjust apply_resp output if spectrum is stored by wavelength --- specutils/spectra/xrayspectrum1d.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 765e77241..b9f2df62d 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -110,11 +110,18 @@ def apply_resp(self, mflux, exposure=None): mrate = mflux if self.rmf is not None: - count_model = self.rmf.apply_rmf(mrate) - return count_model + result = self.rmf.apply_rmf(mrate) else: print("Caution: no response file specified") - return mrate + result = mrate + + # The response is always stored according to energy + # If the spectrum is stored in wavelength, reverse the result so it + # matches in wavelength space + if self.bin_unit.physical_type == 'length': + return result[::-1] + else: + return result ## ---- Supporting response file objects From 35d09321cf066a833adbec17e940dcf94c3b43f3 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 18:03:29 -0400 Subject: [PATCH 13/72] Don't need bin_unit --- specutils/spectra/xrayspectrum1d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index b9f2df62d..bac3f6e3e 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -47,7 +47,6 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.bin_lo = bin_lo self.bin_hi = bin_hi - self.bin_unit = axis_unit # keep for reference self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) @@ -118,7 +117,8 @@ def apply_resp(self, mflux, exposure=None): # The response is always stored according to energy # If the spectrum is stored in wavelength, reverse the result so it # matches in wavelength space - if self.bin_unit.physical_type == 'length': + if self.spectral_axis.unit.physical_type == 'length': + print("reversing output") return result[::-1] else: return result From da39e47faa109c2d18cf215955aacfa500a62c55 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 18:12:10 -0400 Subject: [PATCH 14/72] Because pha-file wavelength is in reverse order, the output from apply_resp is in correct order. No longer need to check --- specutils/spectra/xrayspectrum1d.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index bac3f6e3e..c054255df 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -34,6 +34,11 @@ class XraySpectrum1D(Spectrum1D): exposure arf rmf + + Attributes + ---------- + model : numpy.ndarray + Stores model values """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None, **kwargs): @@ -50,6 +55,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) + self.model_counts = np.zeros_like(bin_mid) return # Convenience function for Xray people @@ -114,15 +120,8 @@ def apply_resp(self, mflux, exposure=None): print("Caution: no response file specified") result = mrate - # The response is always stored according to energy - # If the spectrum is stored in wavelength, reverse the result so it - # matches in wavelength space - if self.spectral_axis.unit.physical_type == 'length': - print("reversing output") - return result[::-1] - else: - return result - + self.model_counts = result + return result ## ---- Supporting response file objects From c5db843592a0d3d525f1bd2cdd587bf157317246 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 18:15:20 -0400 Subject: [PATCH 15/72] added save_model_counts option --- specutils/spectra/xrayspectrum1d.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index c054255df..cd78d2e49 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -77,7 +77,7 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return - def apply_resp(self, mflux, exposure=None): + def apply_resp(self, mflux, exposure=None, store_model_counts=True): """ Given a model flux spectrum, apply the response. In cases where the spectrum has both an ARF and an RMF, apply both. Otherwise, apply @@ -99,9 +99,12 @@ def apply_resp(self, mflux, exposure=None): ARF), this keyword provides the functionality to override the default behaviour and manually set the exposure time to use. + store_model_counts : bool + If True, the output will also be stored in self.model_counts + Returns ------- - count_model : numpy.ndarray + model_counts : numpy.ndarray The model spectrum in units of counts/bin If no ARF file exists, it will return the model flux after applying the RMF @@ -120,7 +123,8 @@ def apply_resp(self, mflux, exposure=None): print("Caution: no response file specified") result = mrate - self.model_counts = result + if store_model_counts: + self.model_counts = result return result ## ---- Supporting response file objects From 291be38f142345a331d996655c8816ee4feea696 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 3 May 2018 10:06:41 -0400 Subject: [PATCH 16/72] Changed my mind on model_counts thing for now --- specutils/spectra/xrayspectrum1d.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index cd78d2e49..7b342cb96 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -34,11 +34,6 @@ class XraySpectrum1D(Spectrum1D): exposure arf rmf - - Attributes - ---------- - model : numpy.ndarray - Stores model values """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None, **kwargs): @@ -55,7 +50,6 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) - self.model_counts = np.zeros_like(bin_mid) return # Convenience function for Xray people @@ -77,7 +71,7 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return - def apply_resp(self, mflux, exposure=None, store_model_counts=True): + def apply_resp(self, mflux, exposure=None): """ Given a model flux spectrum, apply the response. In cases where the spectrum has both an ARF and an RMF, apply both. Otherwise, apply @@ -123,8 +117,6 @@ def apply_resp(self, mflux, exposure=None, store_model_counts=True): print("Caution: no response file specified") result = mrate - if store_model_counts: - self.model_counts = result return result ## ---- Supporting response file objects From fcd9e25667931d27b1597ca1826429975481d354 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 18:27:59 -0500 Subject: [PATCH 17/72] Hard coded in a rest_value --- specutils/spectra/xrayspectrum1d.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 7b342cb96..2c5f4d86e 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -36,14 +36,15 @@ class XraySpectrum1D(Spectrum1D): rmf """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, - arf=None, rmf=None, **kwargs): + arf=None, rmf=None, rest_value=0.0, **kwargs): try: axis_unit = u.Unit(bin_unit) except: axis_unit = _unit_parser(bin_unit) bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit - Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts, **kwargs) + Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts, + rest_value=rest_value, **kwargs) self.bin_lo = bin_lo self.bin_hi = bin_hi From fe8c1b5beec19de16bb86995a7a72af8842a6ef0 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 18:29:31 -0500 Subject: [PATCH 18/72] Added unit to rest_value --- specutils/spectra/xrayspectrum1d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 2c5f4d86e..d700837b7 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -36,7 +36,7 @@ class XraySpectrum1D(Spectrum1D): rmf """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, - arf=None, rmf=None, rest_value=0.0, **kwargs): + arf=None, rmf=None, rest_value=0.0 * u.angstrom, **kwargs): try: axis_unit = u.Unit(bin_unit) except: From 1c7f27d1eb41ccdafe874565cfc724c17d8885a8 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 18:47:34 -0500 Subject: [PATCH 19/72] Commented out distracting print statements --- specutils/spectra/xrayspectrum1d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index d700837b7..ceb3f9fdf 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -110,12 +110,13 @@ def apply_resp(self, mflux, exposure=None): if self.arf is not None: mrate = self.arf.apply_arf(mflux, exposure=exposure) else: + #print("Caution: no ARF file specified") mrate = mflux if self.rmf is not None: result = self.rmf.apply_rmf(mrate) else: - print("Caution: no response file specified") + #print("Caution: no RMF file specified") result = mrate return result From c6f5aaa55f4cb784def23effb8f50a4f54ec6f69 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 19:00:00 -0500 Subject: [PATCH 20/72] Wrote tests --- specutils/tests/test_xrayspectrum1d.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 specutils/tests/test_xrayspectrum1d.py diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py new file mode 100644 index 000000000..f8792a7ff --- /dev/null +++ b/specutils/tests/test_xrayspectrum1d.py @@ -0,0 +1,35 @@ +import numpy as np +from scipy import optimize +import astropy.units as u +from astropy.modeling.powerlaws import PowerLaw1D +from specutils import XraySpectrum1D + +def test_create_from_arrays(): + # Test that XraySpectrum1D can be initialized + amp0, alpha0 = 3.e-3, 2.0 + powlaw0 = PowerLaw1D(amplitude=amp0, alpha=alpha0, x_0=1.e3) + energy = np.linspace(0.2, 10.0, 8000) + elo = energy[:-1] + ehi = energy[1:] + emid = 0.5 * (elo + ehi) + counts = np.random.poisson(lam=powlaw0(emid), size=len(emid)) + test_spec = XraySpectrum1D(elo, ehi, u.keV, counts, exposure=1.0) + return test_spec + +def test_call_spectrum(): + # Test that the XraySpectrum1D has attributes that make it unique + # to X-ray spectrum + test_spec = test_create_from_arrays() + assert len(test_spec.counts) == len(test_spec.spectral_axis) + assert len(test_spec.bin_lo) == len(test_spec.spectral_axis) + assert len(test_spec.bin_hi) == len(test_spec.spectral_axis) + return + +def test_apply_model(): + # Test that one can evaluate XraySpectrum1D with a model + test_spec = test_create_from_arrays() + new_model = PowerLaw1D(amplitude=5.e-3, alpha=1.8, x_0=1.e3) + model_flux = new_model(test_spec.spectral_axis.value) + ymodel = test_spec.apply_resp(model_flux) + assert len(ymodel) == len(test_spec.spectral_axis) + return From c1929350e17a1a62917e69834acc5cd042fcbacc Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 19:17:48 -0500 Subject: [PATCH 21/72] Added docstrings --- specutils/spectra/xrayspectrum1d.py | 67 +++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index ceb3f9fdf..0b72ccedf 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -13,6 +13,9 @@ ANGS = ['angs', 'Angs', 'Angstrom', 'angstrom', 'Angstroms', 'angstroms', 'A', 'a'] def _unit_parser(unit_string): + """ + A function for matching unit strings to the correct Astropy unit + """ if unit_string in EV: return u.eV if unit_string in KEV: @@ -22,17 +25,51 @@ def _unit_parser(unit_string): class XraySpectrum1D(Spectrum1D): """ - Spectrum container for holding X-ray spectrum - WIP by eblur + Spectrum properties specific to X-ray data Parameters ---------- + bin_lo : numpy.ndarray + The left edges for bin values + + bin_hi : numpy.ndarray + The right edges for bin values + + bin_unit : string OR astropy.units.Unit + Unit for the bin values + + counts : numpy.ndarray + Counts histogram for the X-ray spectrum + + exposure : float + Exposure time for the dataset + + arf : specutils.ARF or string, default None + Strings will be passed to ARF.__init__ + All other input types are stored as arf attribute + + rmf : specutils.RMF or string, default None + Strings will be passed to RMF.__init__ + All other input types are stored as rmf attribute + + rest_value : astropy.units.Quantity, default 0 Angstrom + See Spectrum1D rest_value input + + Attributes + ---------- + Inherits from Spectrum1D object. + The following inputs are stored as additional attributes. + bin_lo + bin_hi - bin_unit + counts + exposure + arf + rmf """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, @@ -59,6 +96,18 @@ def counts(self): return self.flux def assign_arf(self, arf_inp): + """ + Assign an ARF object to the XraySpectrum1D object + + Input + ----- + arf_inp : string + File name for the ARF (FITS file) + + Returns + ------- + Modifies the XraySpectrum1D.arf attribute + """ if isinstance(arf_inp, str): self.arf = ARF(arf_inp) else: @@ -66,6 +115,18 @@ def assign_arf(self, arf_inp): return def assign_rmf(self, rmf_inp): + """ + Assign an RMF object to the XraySpectrum1D object + + Input + ----- + rmf_inp : string + File name for the RMF (FITS file) + + Returns + ------- + Modifies the XraySpectrum1D.rmf attribute + """ if isinstance(rmf_inp, str): self.rmf = RMF(rmf_inp) else: From 3314f7dfd7c30ea7389495e129f763091c319990 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 19:19:32 -0500 Subject: [PATCH 22/72] Removed scipy dependence --- specutils/tests/test_xrayspectrum1d.py | 1 - 1 file changed, 1 deletion(-) diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py index f8792a7ff..53fbae5c0 100644 --- a/specutils/tests/test_xrayspectrum1d.py +++ b/specutils/tests/test_xrayspectrum1d.py @@ -1,5 +1,4 @@ import numpy as np -from scipy import optimize import astropy.units as u from astropy.modeling.powerlaws import PowerLaw1D from specutils import XraySpectrum1D From 801e3b661213eb7ba273bf69d415204b2dbf5a8a Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 10:56:46 -0500 Subject: [PATCH 23/72] Spelled out acronyms, link to edu materials --- specutils/spectra/xrayspectrum1d.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 0b72ccedf..05e9cc892 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -12,6 +12,9 @@ KEV = ['kev', 'keV'] ANGS = ['angs', 'Angs', 'Angstrom', 'angstrom', 'Angstroms', 'angstroms', 'A', 'a'] +## An introduction to X-ray analysis can be found here: +## http://cxc.cfa.harvard.edu/xrayschool/talks/intro_xray_analysis.pdf + def _unit_parser(unit_string): """ A function for matching unit strings to the correct Astropy unit @@ -97,12 +100,12 @@ def counts(self): def assign_arf(self, arf_inp): """ - Assign an ARF object to the XraySpectrum1D object + Assign an area response file (ARF) object to the XraySpectrum1D object Input ----- arf_inp : string - File name for the ARF (FITS file) + File name for the area response file (FITS file) Returns ------- @@ -116,12 +119,12 @@ def assign_arf(self, arf_inp): def assign_rmf(self, rmf_inp): """ - Assign an RMF object to the XraySpectrum1D object + Assign a response matrix file (RMF) object to the XraySpectrum1D object Input ----- rmf_inp : string - File name for the RMF (FITS file) + File name for the response matrix file (FITS file) Returns ------- @@ -191,7 +194,7 @@ def __init__(self, filename): def _load_rmf(self, filename): """ - Load an RMF from a FITS file. + Load a response matrix file (RMF) from a FITS file. Parameters ---------- @@ -446,7 +449,7 @@ def __init__(self, filename): def _load_arf(self, filename): """ - Load an ARF from a FITS file. + Load an area response file (ARF) from a FITS file. Parameters ---------- @@ -512,7 +515,7 @@ def _load_arf(self, filename): def apply_arf(self, spec, exposure=None): """ - Fold the spectrum through the ARF. + Fold the spectrum through the area response file (ARF). The ARF is a single vector encoding the effective area information about the detector. A such, applying the ARF is a simple multiplication with the input spectrum. From 4dff31686315a0342673ae173534a6d8f59a0ff7 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:05:22 -0500 Subject: [PATCH 24/72] Added option to specify RMF FITS extension for response matrix --- specutils/spectra/xrayspectrum1d.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 05e9cc892..68460d8b6 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -136,7 +136,7 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return - def apply_resp(self, mflux, exposure=None): + def apply_response(self, mflux, exposure=None): """ Given a model flux spectrum, apply the response. In cases where the spectrum has both an ARF and an RMF, apply both. Otherwise, apply @@ -192,7 +192,7 @@ def __init__(self, filename): self._load_rmf(filename) pass - def _load_rmf(self, filename): + def _load_rmf(self, filename, extension=None): """ Load a response matrix file (RMF) from a FITS file. @@ -201,6 +201,10 @@ def _load_rmf(self, filename): filename : str The file name with the RMF file + extension : str (default None) + FITS file extinction keyword, if the response matrix is stored + under an extension other tham "MATRIX" or "SPECRESP MATRIX" + Attributes ---------- filename : str @@ -246,12 +250,20 @@ def _load_rmf(self, filename): extnames = np.array([h.name for h in hdulist]) # figure out the right extension to use - if "MATRIX" in extnames: + if extension is not None: + h = hdulist[extension] + + elif "MATRIX" in extnames: h = hdulist["MATRIX"] elif "SPECRESP MATRIX" in extnames: h = hdulist["SPECRESP MATRIX"] + else: + print("Cannot find common FITS file extension for response matrix values") + print("Please set the `extension` keyword") + return + data = h.data hdr = h.header hdulist.close() From cdf337340b76068a7b90ba81add91c6efbbbdb60 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:15:09 -0500 Subject: [PATCH 25/72] corrected typos --- specutils/spectra/xrayspectrum1d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 68460d8b6..e2eb6ce9c 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -202,8 +202,8 @@ def _load_rmf(self, filename, extension=None): The file name with the RMF file extension : str (default None) - FITS file extinction keyword, if the response matrix is stored - under an extension other tham "MATRIX" or "SPECRESP MATRIX" + FITS file extension keyword, if the response matrix is stored + under an extension other than "MATRIX" or "SPECRESP MATRIX" Attributes ---------- @@ -466,7 +466,7 @@ def _load_arf(self, filename): Parameters ---------- filename : str - The file name with the RMF file + The file name with the ARF file Attributes ---------- From 2e4724cc88652ee19ef19b450b7aa74152d4dbb3 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:19:14 -0500 Subject: [PATCH 26/72] Removing superfluous returns and pass --- specutils/spectra/xrayspectrum1d.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index e2eb6ce9c..610dea245 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -91,7 +91,6 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) - return # Convenience function for Xray people @property @@ -115,7 +114,6 @@ def assign_arf(self, arf_inp): self.arf = ARF(arf_inp) else: self.arf = arf_inp - return def assign_rmf(self, rmf_inp): """ @@ -190,7 +188,6 @@ def apply_response(self, mflux, exposure=None): class RMF(object): def __init__(self, filename): self._load_rmf(filename) - pass def _load_rmf(self, filename, extension=None): """ From a8a82450189dc3a4fb2863740e40797e58e0ddac Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:22:03 -0500 Subject: [PATCH 27/72] Catch ValueError --- specutils/spectra/xrayspectrum1d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 610dea245..34b34e7d8 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -79,7 +79,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None, rest_value=0.0 * u.angstrom, **kwargs): try: axis_unit = u.Unit(bin_unit) - except: + except ValueError: axis_unit = _unit_parser(bin_unit) bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit From 5d4d52c7453759d762544389f1239f582d0627d5 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:27:25 -0500 Subject: [PATCH 28/72] Spelled out ARF and RMF class names --- specutils/spectra/xrayspectrum1d.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 34b34e7d8..3c452c7d3 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -5,7 +5,7 @@ from astropy.io import fits from .spectrum1d import Spectrum1D -__all__ = ['XraySpectrum1D', 'ARF', 'RMF'] +__all__ = ['XraySpectrum1D', 'AreaResponse', 'ResponseMatrix'] # For dealing with varied unit string choices EV = ['eV', 'ev'] @@ -47,12 +47,12 @@ class XraySpectrum1D(Spectrum1D): exposure : float Exposure time for the dataset - arf : specutils.ARF or string, default None - Strings will be passed to ARF.__init__ + arf : specutils.AreaResponse or string, default None + Strings will be passed to AreaResponse.__init__ All other input types are stored as arf attribute - rmf : specutils.RMF or string, default None - Strings will be passed to RMF.__init__ + rmf : specutils.ResponseMatrix or string, default None + Strings will be passed to ResponseMatrix.__init__ All other input types are stored as rmf attribute rest_value : astropy.units.Quantity, default 0 Angstrom @@ -111,7 +111,7 @@ def assign_arf(self, arf_inp): Modifies the XraySpectrum1D.arf attribute """ if isinstance(arf_inp, str): - self.arf = ARF(arf_inp) + self.arf = AreaResponse(arf_inp) else: self.arf = arf_inp @@ -129,7 +129,7 @@ def assign_rmf(self, rmf_inp): Modifies the XraySpectrum1D.rmf attribute """ if isinstance(rmf_inp, str): - self.rmf = RMF(rmf_inp) + self.rmf = ResponseMatrix(rmf_inp) else: self.rmf = rmf_inp return @@ -185,7 +185,7 @@ def apply_response(self, mflux, exposure=None): ## ---- Supporting response file objects -class RMF(object): +class ResponseMatrix(object): def __init__(self, filename): self._load_rmf(filename) @@ -449,7 +449,7 @@ def apply_rmf(self, spec): return counts[:self.detchans] -class ARF(object): +class AreaResponse(object): def __init__(self, filename): From a297c452cdee7f22b15ebc52585a7712f95b29f9 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:30:06 -0500 Subject: [PATCH 29/72] updated docstring --- specutils/spectra/xrayspectrum1d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 3c452c7d3..cb99d21f8 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -137,8 +137,8 @@ def assign_rmf(self, rmf_inp): def apply_response(self, mflux, exposure=None): """ Given a model flux spectrum, apply the response. In cases where the - spectrum has both an ARF and an RMF, apply both. Otherwise, apply - whatever response is in RMF. + spectrum has both an area response file (ARF) and a response matrix + file (RMF), apply both. Otherwise, apply whatever response is in RMF. The model flux spectrum *must* be created using the same units and bins as in the ARF (where the ARF exists)! From 86f4a3b73772a05674f5fd7a2ed156aa018bab9b Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:34:47 -0500 Subject: [PATCH 30/72] Updated tests --- specutils/tests/test_xrayspectrum1d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py index 53fbae5c0..1e1f45bbe 100644 --- a/specutils/tests/test_xrayspectrum1d.py +++ b/specutils/tests/test_xrayspectrum1d.py @@ -29,6 +29,6 @@ def test_apply_model(): test_spec = test_create_from_arrays() new_model = PowerLaw1D(amplitude=5.e-3, alpha=1.8, x_0=1.e3) model_flux = new_model(test_spec.spectral_axis.value) - ymodel = test_spec.apply_resp(model_flux) + ymodel = test_spec.apply_response(model_flux) assert len(ymodel) == len(test_spec.spectral_axis) return From 769c923b6ba83719db4cf79f3443801e5b0384d9 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 12 Jun 2018 20:38:39 +0200 Subject: [PATCH 31/72] initialize attributes to None before loading from file --- specutils/spectra/xrayspectrum1d.py | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index cb99d21f8..5d98c79bc 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -187,9 +187,6 @@ def apply_response(self, mflux, exposure=None): class ResponseMatrix(object): def __init__(self, filename): - self._load_rmf(filename) - - def _load_rmf(self, filename, extension=None): """ Load a response matrix file (RMF) from a FITS file. @@ -237,6 +234,19 @@ def _load_rmf(self, filename, extension=None): The number of channels in the detector """ + self.filename = "" + self.offset = None + self.n_grp = None + self.f_chan = None + self.n_chan = None + self.matrix = None + self.energ_lo = None + self.energ_hi = None + self.energ_unit = None + self.detchans = None + self._load_rmf(filename) + + def _load_rmf(self, filename, extension=None): # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and # anxillary information @@ -452,11 +462,6 @@ def apply_rmf(self, spec): class AreaResponse(object): def __init__(self, filename): - - self._load_arf(filename) - pass - - def _load_arf(self, filename): """ Load an area response file (ARF) from a FITS file. @@ -492,6 +497,16 @@ def _load_arf(self, filename): Average exposure time for the dataset (takes telescope dithering into account) """ + self.filename = "" + self.e_low = None + self.e_high = None + self.e_unit = None + self.specresp = None + self.fracexpo = None + self.exposure = None + self._load_arf(filename) + + def _load_arf(self, filename): # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and # anxillary information From a89af6bc769d05fabd86350a0a12ac3518adedc3 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 12 Jun 2018 20:39:55 +0200 Subject: [PATCH 32/72] save filename attribute for attempting load function --- specutils/spectra/xrayspectrum1d.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 5d98c79bc..58dcc3b73 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -234,7 +234,7 @@ def __init__(self, filename): The number of channels in the detector """ - self.filename = "" + self.filename = filename self.offset = None self.n_grp = None self.f_chan = None @@ -251,7 +251,6 @@ def _load_rmf(self, filename, extension=None): # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) - self.filename = filename # get all the extension names extnames = np.array([h.name for h in hdulist]) @@ -497,7 +496,7 @@ def __init__(self, filename): Average exposure time for the dataset (takes telescope dithering into account) """ - self.filename = "" + self.filename = filename self.e_low = None self.e_high = None self.e_unit = None @@ -511,7 +510,6 @@ def _load_arf(self, filename): # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) - self.filename = filename h = hdulist["SPECRESP"] data = h.data From dfb9475112ea70dde9444eac8516fc5efaee1dea Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 1 May 2018 17:26:21 -0400 Subject: [PATCH 33/72] First pass at XraySpectrum1D --- specutils/spectra/xrayspectrum1d.py | 47 +++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) create mode 100644 specutils/spectra/xrayspectrum1d.py diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py new file mode 100644 index 000000000..b7556ad77 --- /dev/null +++ b/specutils/spectra/xrayspectrum1d.py @@ -0,0 +1,47 @@ +import logging + +import numpy as np +from astropy import units as u +from .spectrum1d import Spectrum1D + +__all__ = ['XraySpectrum1D'] + +# For dealing with varied unit string choices +EV = ['eV', 'ev'] +KEV = ['kev', 'keV'] +ANGS = ['angs', 'Angs', 'Angstrom', 'angstrom', 'Angstroms', 'angstroms', 'A', 'a'] + +def _unit_parser(unit_string): + if unit_string in EV: + return u.eV + if unit_string in KEV: + return u.keV + if unit_string in ANGS: + return u.angstrom + +class XraySpectrum1D(Spectrum1D): + """ + Spectrum container for holding X-ray spectrum + WIP by eblur + + Parameters + ---------- + bin_lo + bin_hi + bin_unit + counts + arf + rmf + """ + def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): + try: + axis_unit = u.Unit(bin_unit) + else: + axis_unit = _unit_parser(bin_unit) + + bin_mid = 0.5 * (bin_lo + bin_hi) * unit + Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + + self.arf = arf + self.rmf = rmf + return From 2fa21425147157573e5c416de8d3a9678036be08 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 1 May 2018 17:40:28 -0400 Subject: [PATCH 34/72] Can load an X-ray spectrum --- specutils/spectra/__init__.py | 4 ++++ specutils/spectra/xrayspectrum1d.py | 4 ++-- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/__init__.py b/specutils/spectra/__init__.py index 8ce324ecb..05d5d14c4 100644 --- a/specutils/spectra/__init__.py +++ b/specutils/spectra/__init__.py @@ -3,6 +3,10 @@ `~astropy.nddata.NDData`-inherited classes used for storing the spectrum data. """ from .spectrum1d import * # noqa +<<<<<<< HEAD from .spectral_region import * # noqa from .spectrum_collection import * #noqa from .spectrum_list import * #noqa +======= +from .xrayspectrum1d import * +>>>>>>> Can load an X-ray spectrum diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index b7556ad77..d57146f97 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -36,10 +36,10 @@ class XraySpectrum1D(Spectrum1D): def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): try: axis_unit = u.Unit(bin_unit) - else: + except: axis_unit = _unit_parser(bin_unit) - bin_mid = 0.5 * (bin_lo + bin_hi) * unit + bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) self.arf = arf From 0f37ccd0668398c6f31ae73f32fbd13b110a673a Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 1 May 2018 17:44:41 -0400 Subject: [PATCH 35/72] Added attribute --- specutils/spectra/xrayspectrum1d.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index d57146f97..b939b40d1 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -45,3 +45,8 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): self.arf = arf self.rmf = rmf return + + # Convenience function for Xray people + @property + def counts(self): + return self.flux From 56612ca4badb7127f5a1186aaa771c849d132797 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 13:53:39 -0400 Subject: [PATCH 36/72] Added ARF and RMF objects --- specutils/spectra/xrayspectrum1d.py | 326 +++++++++++++++++++++++++++- 1 file changed, 325 insertions(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index b939b40d1..e36a500a6 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -4,7 +4,7 @@ from astropy import units as u from .spectrum1d import Spectrum1D -__all__ = ['XraySpectrum1D'] +__all__ = ['XraySpectrum1D', 'ARF', 'RMF'] # For dealing with varied unit string choices EV = ['eV', 'ev'] @@ -50,3 +50,327 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): @property def counts(self): return self.flux + +## ---- Supporting response file objects + +class RMF(object): + def __init__(self, filename): + self._load_rmf(filename) + pass + + def _load_rmf(self, filename): + """ + Load an RMF from a FITS file. + + Parameters + ---------- + filename : str + The file name with the RMF file + + Attributes + ---------- + n_grp : numpy.ndarray + the Array with the number of channels in each + channel set + + f_chan : numpy.ndarray + The starting channel for each channel group; + If an element i in n_grp > 1, then the resulting + row entry in f_chan will be a list of length n_grp[i]; + otherwise it will be a single number + + n_chan : numpy.ndarray + The number of channels in each channel group. The same + logic as for f_chan applies + + matrix : numpy.ndarray + The redistribution matrix as a flattened 1D vector + + energ_lo : numpy.ndarray + The lower edges of the energy bins + + energ_hi : numpy.ndarray + The upper edges of the energy bins + + detchans : int + The number of channels in the detector + + """ + # open the FITS file and extract the MATRIX extension + # which contains the redistribution matrix and + # anxillary information + hdulist = fits.open(filename) + + # get all the extension names + extnames = np.array([h.name for h in hdulist]) + + # figure out the right extension to use + if "MATRIX" in extnames: + h = hdulist["MATRIX"] + + elif "SPECRESP MATRIX" in extnames: + h = hdulist["SPECRESP MATRIX"] + + data = h.data + hdr = h.header + hdulist.close() + + # extract + store the attributes described in the docstring + n_grp = np.array(data.field("N_GRP")) + f_chan = np.array(data.field('F_CHAN')) + n_chan = np.array(data.field("N_CHAN")) + matrix = np.array(data.field("MATRIX")) + + self.energ_lo = np.array(data.field("ENERG_LO")) + self.energ_hi = np.array(data.field("ENERG_HI")) + self.energ_unit = data.columns["ENERG_LO"].unit + self.detchans = hdr["DETCHANS"] + self.offset = self.__get_tlmin(h) + + # flatten the variable-length arrays + self.n_grp, self.f_chan, self.n_chan, self.matrix = \ + self._flatten_arrays(n_grp, f_chan, n_chan, matrix) + + return + + def __get_tlmin(self, h): + """ + Get the tlmin keyword for `F_CHAN`. + + Parameters + ---------- + h : an astropy.io.fits.hdu.table.BinTableHDU object + The extension containing the `F_CHAN` column + + Returns + ------- + tlmin : int + The tlmin keyword + """ + # get the header + hdr = h.header + # get the keys of all + keys = np.array(list(hdr.keys())) + + # find the place where the tlmin keyword is defined + t = np.array(["TLMIN" in k for k in keys]) + + # get the index of the TLMIN keyword + tlmin_idx = np.hstack(np.where(t))[0] + + # get the corresponding value + tlmin = np.int(list(hdr.items())[tlmin_idx][1]) + + return tlmin + + def _flatten_arrays(self, n_grp, f_chan, n_chan, matrix): + + if not len(n_grp) == len(f_chan) == len(n_chan) == len(matrix): + raise ValueError("Arrays must be of same length!") + + # find all non-zero groups + nz_idx = (n_grp > 0) + + # stack all non-zero rows in the matrix + matrix_flat = np.hstack(matrix[nz_idx]) + + # stack all nonzero rows in n_chan and f_chan + #n_chan_flat = np.hstack(n_chan[nz_idx]) + #f_chan_flat = np.hstack(f_chan[nz_idx]) + + # some matrices actually have more elements + # than groups in `n_grp`, so we'll only pick out + # those values that have a correspondence in + # n_grp + f_chan_new = [] + n_chan_new = [] + for i,t in enumerate(nz_idx): + if t: + n = n_grp[i] + f = f_chan[i] + nc = n_chan[i] + if np.size(f) == 1: + f_chan_new.append(f) + n_chan_new.append(nc) + else: + f_chan_new.append(f[:n]) + n_chan_new.append(nc[:n]) + + n_chan_flat = np.hstack(n_chan_new) + f_chan_flat = np.hstack(f_chan_new) + + # if n_chan is zero, we'll remove those as well. + nz_idx2 = (n_chan_flat > 0) + n_chan_flat = n_chan_flat[nz_idx2] + f_chan_flat = f_chan_flat[nz_idx2] + + return n_grp, f_chan_flat, n_chan_flat, matrix_flat + + def apply_rmf(self, spec): + """ + Fold the spectrum through the redistribution matrix. + + The redistribution matrix is saved as a flattened 1-dimensional + vector to save space. In reality, for each entry in the flux + vector, there exists one or more sets of channels that this + flux is redistributed into. The additional arrays `n_grp`, + `f_chan` and `n_chan` store this information: + * `n_group` stores the number of channel groups for each + energy bin + * `f_chan` stores the *first channel* that each channel + for each channel set + * `n_chan` stores the number of channels in each channel + set + + As a result, for a given energy bin i, we need to look up the + number of channel sets in `n_grp` for that energy bin. We + then need to loop over the number of channel sets. For each + channel set, we look up the first channel into which flux + will be distributed as well as the number of channels in the + group. We then need to also loop over the these channels and + actually use the corresponding elements in the redistribution + matrix to redistribute the photon flux into channels. + + All of this is basically a big bookkeeping exercise in making + sure to get the indices right. + + Parameters + ---------- + spec : numpy.ndarray + The (model) spectrum to be folded + + Returns + ------- + counts : numpy.ndarray + The (model) spectrum after folding, in + counts/s/channel + + """ + # get the number of channels in the data + nchannels = spec.shape[0] + + # an empty array for the output counts + counts = np.zeros(nchannels) + + # index for n_chan and f_chan incrementation + k = 0 + + # index for the response matrix incrementation + resp_idx = 0 + + # loop over all channels + for i in range(nchannels): + + # this is the current bin in the flux spectrum to + # be folded + source_bin_i = spec[i] + + # get the current number of groups + current_num_groups = self.n_grp[i] + + # loop over the current number of groups + for j in range(current_num_groups): + + current_num_chans = int(self.n_chan[k]) + + if current_num_chans == 0: + k += 1 + resp_idx += current_num_chans + continue + + + else: + # get the right index for the start of the counts array + # to put the data into + counts_idx = int(self.f_chan[k] - self.offset) + # this is the current number of channels to use + + k += 1 + # add the flux to the subarray of the counts array that starts with + # counts_idx and runs over current_num_chans channels + counts[counts_idx:counts_idx + + current_num_chans] += self.matrix[resp_idx:resp_idx + + current_num_chans] * \ + np.float(source_bin_i) + # iterate the response index for next round + resp_idx += current_num_chans + + + return counts[:self.detchans] + + +class ARF(object): + + def __init__(self, filename): + + self._load_arf(filename) + pass + + def _load_arf(self, filename): + """ + Load an ARF from a FITS file. + Parameters + ---------- + filename : str + The file name with the RMF file + Attributes + ---------- + """ + # open the FITS file and extract the MATRIX extension + # which contains the redistribution matrix and + # anxillary information + hdulist = fits.open(filename) + h = hdulist["SPECRESP"] + data = h.data + hdr = h.header + hdulist.close() + + # extract + store the attributes described in the docstring + + self.e_low = np.array(data.field("ENERG_LO")) + self.e_high = np.array(data.field("ENERG_HI")) + self.e_unit = data.columns["ENERG_LO"].unit + self.specresp = np.array(data.field("SPECRESP")) + + if "EXPOSURE" in list(hdr.keys()): + self.exposure = hdr["EXPOSURE"] + else: + self.exposure = 1.0 + + if "FRACEXPO" in data.columns.names: + self.fracexpo = data["FRACEXPO"] + else: + self.fracexpo = 1.0 + + return + + def apply_arf(self, spec, exposure=None): + """ + Fold the spectrum through the ARF. + The ARF is a single vector encoding the effective area information + about the detector. A such, applying the ARF is a simple + multiplication with the input spectrum. + Parameters + ---------- + spec : numpy.ndarray + The (model) spectrum to be folded + exposure : float, default None + Value for the exposure time. By default, `apply_arf` will use the + exposure keyword from the ARF file. If this exposure time is not + correct (for example when simulated spectra use a different exposure + time and the ARF from a real observation), one can override the + default exposure by setting the `exposure` keyword to the correct + value. + Returns + ------- + s_arf : numpy.ndarray + The (model) spectrum after folding, in + counts/s/channel + """ + assert spec.shape[0] == self.specresp.shape[0], "The input spectrum must " \ + "be of same size as the " \ + "ARF array." + if exposure is None: + return np.array(spec) * self.specresp * self.exposure + else: + return np.array(spec) * self.specresp * exposure From b7303e75966516657bb8ae88a5e1b5f4dd3cda2b Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:07:50 -0400 Subject: [PATCH 37/72] Added assign_rmf and assign_arf methods --- specutils/spectra/xrayspectrum1d.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index e36a500a6..a371b94a5 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -42,8 +42,8 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) - self.arf = arf - self.rmf = rmf + self.assign_rmf(rmf) + self.assign_arf(rmf) return # Convenience function for Xray people @@ -51,6 +51,21 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): def counts(self): return self.flux + def assign_arf(self, arf_inp): + if isinstance(arf_inp, str): + self.arf = ARF(arf_inp) + else: + self.arf = arf_inp + return + + def assign_rmf(self, rmf_inp): + if isinstance(rmf_inp, str): + self.rmf = RMF(rmf_inp) + else: + self.rmf = rmf_inp + return + + ## ---- Supporting response file objects class RMF(object): @@ -69,6 +84,9 @@ def _load_rmf(self, filename): Attributes ---------- + filename : str + The file name that the RMF was drawn from + n_grp : numpy.ndarray the Array with the number of channels in each channel set @@ -100,6 +118,7 @@ def _load_rmf(self, filename): # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) + self.filename = filename # get all the extension names extnames = np.array([h.name for h in hdulist]) @@ -309,17 +328,23 @@ def __init__(self, filename): def _load_arf(self, filename): """ Load an ARF from a FITS file. + Parameters ---------- filename : str The file name with the RMF file + Attributes ---------- + filename : str + The file name that the ARF was drawn from """ # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) + self.filename = filename + h = hdulist["SPECRESP"] data = h.data hdr = h.header From 003b63696503f64d17f44671bc6995fc3b9b9224 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:16:13 -0400 Subject: [PATCH 38/72] Added exposure input --- specutils/spectra/xrayspectrum1d.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index a371b94a5..51de9e726 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -33,7 +33,7 @@ class XraySpectrum1D(Spectrum1D): arf rmf """ - def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): + def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None): try: axis_unit = u.Unit(bin_unit) except: @@ -42,6 +42,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, arf=None, rmf=None): bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(rmf) return @@ -344,7 +345,7 @@ def _load_arf(self, filename): # anxillary information hdulist = fits.open(filename) self.filename = filename - + h = hdulist["SPECRESP"] data = h.data hdr = h.header From ce4349e4e76c20c69bbedc0fab8a154fbc165796 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:18:22 -0400 Subject: [PATCH 39/72] Fixed bugs --- specutils/spectra/xrayspectrum1d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 51de9e726..fade12358 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -2,6 +2,7 @@ import numpy as np from astropy import units as u +from astropy.io import fits from .spectrum1d import Spectrum1D __all__ = ['XraySpectrum1D', 'ARF', 'RMF'] @@ -44,7 +45,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=Non self.exposure = exposure self.assign_rmf(rmf) - self.assign_arf(rmf) + self.assign_arf(arf) return # Convenience function for Xray people From 753ab5eb08f712fa53ab2f78a41d941fcbd3da32 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:30:44 -0400 Subject: [PATCH 40/72] Updated docstrings --- specutils/spectra/xrayspectrum1d.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index fade12358..72c5bd11e 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -112,6 +112,9 @@ def _load_rmf(self, filename): energ_hi : numpy.ndarray The upper edges of the energy bins + energ_unit : str + Description of the energy units used + detchans : int The number of channels in the detector @@ -340,6 +343,28 @@ def _load_arf(self, filename): ---------- filename : str The file name that the ARF was drawn from + + e_low : numpy.ndarray + The lower edges of the energy bins + + e_high : numpy.ndarray + The upper edges of the energy bins + + e_unit : str + Description of the energy units used + + specresp : numpy.ndarray + Description of the energy dependent telescope response area + + fracexpo : float or numpy.ndarray + Fractional exposure time for the spectrum + (sometimes constant, sometimes dependent on spectral channel). + These values are stored for reference; generally, they are already + accounted for in the specresp array. + + exposure : + Average exposure time for the dataset + (takes telescope dithering into account) """ # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and From 4f195e1be723c2b9e833156ac47de979fdd693f8 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 14:36:08 -0400 Subject: [PATCH 41/72] Store bin and energy units as astropy.units.Unit --- specutils/spectra/xrayspectrum1d.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 72c5bd11e..2b98164aa 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -31,6 +31,7 @@ class XraySpectrum1D(Spectrum1D): bin_hi bin_unit counts + exposure arf rmf """ @@ -43,6 +44,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=Non bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + self.bin_unit = axis_unit # keep for reference self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) @@ -112,7 +114,7 @@ def _load_rmf(self, filename): energ_hi : numpy.ndarray The upper edges of the energy bins - energ_unit : str + energ_unit : astropy.units.Unit Description of the energy units used detchans : int @@ -147,7 +149,7 @@ def _load_rmf(self, filename): self.energ_lo = np.array(data.field("ENERG_LO")) self.energ_hi = np.array(data.field("ENERG_HI")) - self.energ_unit = data.columns["ENERG_LO"].unit + self.energ_unit = _unit_parser(data.columns["ENERG_LO"].unit) self.detchans = hdr["DETCHANS"] self.offset = self.__get_tlmin(h) @@ -350,7 +352,7 @@ def _load_arf(self, filename): e_high : numpy.ndarray The upper edges of the energy bins - e_unit : str + e_unit : astropy.units.Unit Description of the energy units used specresp : numpy.ndarray @@ -381,7 +383,7 @@ def _load_arf(self, filename): self.e_low = np.array(data.field("ENERG_LO")) self.e_high = np.array(data.field("ENERG_HI")) - self.e_unit = data.columns["ENERG_LO"].unit + self.e_unit = _unit_parser(data.columns["ENERG_LO"].unit) self.specresp = np.array(data.field("SPECRESP")) if "EXPOSURE" in list(hdr.keys()): From 67493c523e1f30624db50cca1e204319fc1fa942 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 16:31:14 -0400 Subject: [PATCH 42/72] Added bin_lo and hi because wcs.bin_edges did not exist --- specutils/spectra/xrayspectrum1d.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 2b98164aa..d209c988b 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -35,15 +35,18 @@ class XraySpectrum1D(Spectrum1D): arf rmf """ - def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None): + def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, + arf=None, rmf=None, **kwargs): try: axis_unit = u.Unit(bin_unit) except: axis_unit = _unit_parser(bin_unit) bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit - Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts) + Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts, **kwargs) + self.bin_lo = bin_lo + self.bin_hi = bin_hi self.bin_unit = axis_unit # keep for reference self.exposure = exposure self.assign_rmf(rmf) From 7974939f7a92a44b437aad94a8c31b202e59e14f Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 16:34:35 -0400 Subject: [PATCH 43/72] Added apply_resp method --- specutils/spectra/xrayspectrum1d.py | 44 +++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index d209c988b..765e77241 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -72,6 +72,50 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return + def apply_resp(self, mflux, exposure=None): + """ + Given a model flux spectrum, apply the response. In cases where the + spectrum has both an ARF and an RMF, apply both. Otherwise, apply + whatever response is in RMF. + + The model flux spectrum *must* be created using the same units and + bins as in the ARF (where the ARF exists)! + + Parameters + ---------- + mflux : iterable + A list or array with the model flux values in ergs/keV/s/cm^-2 + + exposure : float, default None + By default, the exposure stored in the ARF will be used to compute + the total counts per bin over the effective observation time. + In cases where this might be incorrect (e.g. for simulated spectra + where the pha file might have a different exposure value than the + ARF), this keyword provides the functionality to override the + default behaviour and manually set the exposure time to use. + + Returns + ------- + count_model : numpy.ndarray + The model spectrum in units of counts/bin + + If no ARF file exists, it will return the model flux after applying the RMF + If no RMF file exists, it will return the model flux after applying the ARF (with a warning) + If no ARF and no RMF, it will return the model flux spectrum (with a warning) + """ + + if self.arf is not None: + mrate = self.arf.apply_arf(mflux, exposure=exposure) + else: + mrate = mflux + + if self.rmf is not None: + count_model = self.rmf.apply_rmf(mrate) + return count_model + else: + print("Caution: no response file specified") + return mrate + ## ---- Supporting response file objects From 7f54eaeaca2e3d132d76da83789986fce76abf7e Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 16:52:47 -0400 Subject: [PATCH 44/72] Adjust apply_resp output if spectrum is stored by wavelength --- specutils/spectra/xrayspectrum1d.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 765e77241..b9f2df62d 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -110,11 +110,18 @@ def apply_resp(self, mflux, exposure=None): mrate = mflux if self.rmf is not None: - count_model = self.rmf.apply_rmf(mrate) - return count_model + result = self.rmf.apply_rmf(mrate) else: print("Caution: no response file specified") - return mrate + result = mrate + + # The response is always stored according to energy + # If the spectrum is stored in wavelength, reverse the result so it + # matches in wavelength space + if self.bin_unit.physical_type == 'length': + return result[::-1] + else: + return result ## ---- Supporting response file objects From 5eded3d6b2716a25795144687a9cf35b1ce04939 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 18:03:29 -0400 Subject: [PATCH 45/72] Don't need bin_unit --- specutils/spectra/xrayspectrum1d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index b9f2df62d..bac3f6e3e 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -47,7 +47,6 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.bin_lo = bin_lo self.bin_hi = bin_hi - self.bin_unit = axis_unit # keep for reference self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) @@ -118,7 +117,8 @@ def apply_resp(self, mflux, exposure=None): # The response is always stored according to energy # If the spectrum is stored in wavelength, reverse the result so it # matches in wavelength space - if self.bin_unit.physical_type == 'length': + if self.spectral_axis.unit.physical_type == 'length': + print("reversing output") return result[::-1] else: return result From 6705068c79c934eec26af075c8a3aab9c45490df Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 18:12:10 -0400 Subject: [PATCH 46/72] Because pha-file wavelength is in reverse order, the output from apply_resp is in correct order. No longer need to check --- specutils/spectra/xrayspectrum1d.py | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index bac3f6e3e..c054255df 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -34,6 +34,11 @@ class XraySpectrum1D(Spectrum1D): exposure arf rmf + + Attributes + ---------- + model : numpy.ndarray + Stores model values """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None, **kwargs): @@ -50,6 +55,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) + self.model_counts = np.zeros_like(bin_mid) return # Convenience function for Xray people @@ -114,15 +120,8 @@ def apply_resp(self, mflux, exposure=None): print("Caution: no response file specified") result = mrate - # The response is always stored according to energy - # If the spectrum is stored in wavelength, reverse the result so it - # matches in wavelength space - if self.spectral_axis.unit.physical_type == 'length': - print("reversing output") - return result[::-1] - else: - return result - + self.model_counts = result + return result ## ---- Supporting response file objects From 80d559d6c62b192fada371b9be12fa6905aed65e Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 2 May 2018 18:15:20 -0400 Subject: [PATCH 47/72] added save_model_counts option --- specutils/spectra/xrayspectrum1d.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index c054255df..cd78d2e49 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -77,7 +77,7 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return - def apply_resp(self, mflux, exposure=None): + def apply_resp(self, mflux, exposure=None, store_model_counts=True): """ Given a model flux spectrum, apply the response. In cases where the spectrum has both an ARF and an RMF, apply both. Otherwise, apply @@ -99,9 +99,12 @@ def apply_resp(self, mflux, exposure=None): ARF), this keyword provides the functionality to override the default behaviour and manually set the exposure time to use. + store_model_counts : bool + If True, the output will also be stored in self.model_counts + Returns ------- - count_model : numpy.ndarray + model_counts : numpy.ndarray The model spectrum in units of counts/bin If no ARF file exists, it will return the model flux after applying the RMF @@ -120,7 +123,8 @@ def apply_resp(self, mflux, exposure=None): print("Caution: no response file specified") result = mrate - self.model_counts = result + if store_model_counts: + self.model_counts = result return result ## ---- Supporting response file objects From 73535d9344d037824362a7e9fb7cbe679661c089 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 3 May 2018 10:06:41 -0400 Subject: [PATCH 48/72] Changed my mind on model_counts thing for now --- specutils/spectra/xrayspectrum1d.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index cd78d2e49..7b342cb96 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -34,11 +34,6 @@ class XraySpectrum1D(Spectrum1D): exposure arf rmf - - Attributes - ---------- - model : numpy.ndarray - Stores model values """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None, **kwargs): @@ -55,7 +50,6 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) - self.model_counts = np.zeros_like(bin_mid) return # Convenience function for Xray people @@ -77,7 +71,7 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return - def apply_resp(self, mflux, exposure=None, store_model_counts=True): + def apply_resp(self, mflux, exposure=None): """ Given a model flux spectrum, apply the response. In cases where the spectrum has both an ARF and an RMF, apply both. Otherwise, apply @@ -123,8 +117,6 @@ def apply_resp(self, mflux, exposure=None, store_model_counts=True): print("Caution: no response file specified") result = mrate - if store_model_counts: - self.model_counts = result return result ## ---- Supporting response file objects From e9be1099fea9034f52ba8ba8e6e30e1b2610b682 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 18:27:59 -0500 Subject: [PATCH 49/72] Hard coded in a rest_value --- specutils/spectra/xrayspectrum1d.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 7b342cb96..2c5f4d86e 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -36,14 +36,15 @@ class XraySpectrum1D(Spectrum1D): rmf """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, - arf=None, rmf=None, **kwargs): + arf=None, rmf=None, rest_value=0.0, **kwargs): try: axis_unit = u.Unit(bin_unit) except: axis_unit = _unit_parser(bin_unit) bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit - Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts, **kwargs) + Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts, + rest_value=rest_value, **kwargs) self.bin_lo = bin_lo self.bin_hi = bin_hi From baa613c99e45bd8b78412e4c0b999f5143962a07 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 18:29:31 -0500 Subject: [PATCH 50/72] Added unit to rest_value --- specutils/spectra/xrayspectrum1d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 2c5f4d86e..d700837b7 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -36,7 +36,7 @@ class XraySpectrum1D(Spectrum1D): rmf """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, - arf=None, rmf=None, rest_value=0.0, **kwargs): + arf=None, rmf=None, rest_value=0.0 * u.angstrom, **kwargs): try: axis_unit = u.Unit(bin_unit) except: From 56b4c1afdd8f667365ac005d804a30ca27ae10d1 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 18:47:34 -0500 Subject: [PATCH 51/72] Commented out distracting print statements --- specutils/spectra/xrayspectrum1d.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index d700837b7..ceb3f9fdf 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -110,12 +110,13 @@ def apply_resp(self, mflux, exposure=None): if self.arf is not None: mrate = self.arf.apply_arf(mflux, exposure=exposure) else: + #print("Caution: no ARF file specified") mrate = mflux if self.rmf is not None: result = self.rmf.apply_rmf(mrate) else: - print("Caution: no response file specified") + #print("Caution: no RMF file specified") result = mrate return result From afb6333c6337a5104eaf4684cd2be1e7aa53105f Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 19:00:00 -0500 Subject: [PATCH 52/72] Wrote tests --- specutils/tests/test_xrayspectrum1d.py | 35 ++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) create mode 100644 specutils/tests/test_xrayspectrum1d.py diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py new file mode 100644 index 000000000..f8792a7ff --- /dev/null +++ b/specutils/tests/test_xrayspectrum1d.py @@ -0,0 +1,35 @@ +import numpy as np +from scipy import optimize +import astropy.units as u +from astropy.modeling.powerlaws import PowerLaw1D +from specutils import XraySpectrum1D + +def test_create_from_arrays(): + # Test that XraySpectrum1D can be initialized + amp0, alpha0 = 3.e-3, 2.0 + powlaw0 = PowerLaw1D(amplitude=amp0, alpha=alpha0, x_0=1.e3) + energy = np.linspace(0.2, 10.0, 8000) + elo = energy[:-1] + ehi = energy[1:] + emid = 0.5 * (elo + ehi) + counts = np.random.poisson(lam=powlaw0(emid), size=len(emid)) + test_spec = XraySpectrum1D(elo, ehi, u.keV, counts, exposure=1.0) + return test_spec + +def test_call_spectrum(): + # Test that the XraySpectrum1D has attributes that make it unique + # to X-ray spectrum + test_spec = test_create_from_arrays() + assert len(test_spec.counts) == len(test_spec.spectral_axis) + assert len(test_spec.bin_lo) == len(test_spec.spectral_axis) + assert len(test_spec.bin_hi) == len(test_spec.spectral_axis) + return + +def test_apply_model(): + # Test that one can evaluate XraySpectrum1D with a model + test_spec = test_create_from_arrays() + new_model = PowerLaw1D(amplitude=5.e-3, alpha=1.8, x_0=1.e3) + model_flux = new_model(test_spec.spectral_axis.value) + ymodel = test_spec.apply_resp(model_flux) + assert len(ymodel) == len(test_spec.spectral_axis) + return From a15b101ec3b083b34924569bc6cd704542572ddf Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 19:17:48 -0500 Subject: [PATCH 53/72] Added docstrings --- specutils/spectra/xrayspectrum1d.py | 67 +++++++++++++++++++++++++++-- 1 file changed, 64 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index ceb3f9fdf..0b72ccedf 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -13,6 +13,9 @@ ANGS = ['angs', 'Angs', 'Angstrom', 'angstrom', 'Angstroms', 'angstroms', 'A', 'a'] def _unit_parser(unit_string): + """ + A function for matching unit strings to the correct Astropy unit + """ if unit_string in EV: return u.eV if unit_string in KEV: @@ -22,17 +25,51 @@ def _unit_parser(unit_string): class XraySpectrum1D(Spectrum1D): """ - Spectrum container for holding X-ray spectrum - WIP by eblur + Spectrum properties specific to X-ray data Parameters ---------- + bin_lo : numpy.ndarray + The left edges for bin values + + bin_hi : numpy.ndarray + The right edges for bin values + + bin_unit : string OR astropy.units.Unit + Unit for the bin values + + counts : numpy.ndarray + Counts histogram for the X-ray spectrum + + exposure : float + Exposure time for the dataset + + arf : specutils.ARF or string, default None + Strings will be passed to ARF.__init__ + All other input types are stored as arf attribute + + rmf : specutils.RMF or string, default None + Strings will be passed to RMF.__init__ + All other input types are stored as rmf attribute + + rest_value : astropy.units.Quantity, default 0 Angstrom + See Spectrum1D rest_value input + + Attributes + ---------- + Inherits from Spectrum1D object. + The following inputs are stored as additional attributes. + bin_lo + bin_hi - bin_unit + counts + exposure + arf + rmf """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, @@ -59,6 +96,18 @@ def counts(self): return self.flux def assign_arf(self, arf_inp): + """ + Assign an ARF object to the XraySpectrum1D object + + Input + ----- + arf_inp : string + File name for the ARF (FITS file) + + Returns + ------- + Modifies the XraySpectrum1D.arf attribute + """ if isinstance(arf_inp, str): self.arf = ARF(arf_inp) else: @@ -66,6 +115,18 @@ def assign_arf(self, arf_inp): return def assign_rmf(self, rmf_inp): + """ + Assign an RMF object to the XraySpectrum1D object + + Input + ----- + rmf_inp : string + File name for the RMF (FITS file) + + Returns + ------- + Modifies the XraySpectrum1D.rmf attribute + """ if isinstance(rmf_inp, str): self.rmf = RMF(rmf_inp) else: From 4ba7b5829c400d13d2bc976d241b6fb80981881d Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Wed, 23 May 2018 19:19:32 -0500 Subject: [PATCH 54/72] Removed scipy dependence --- specutils/tests/test_xrayspectrum1d.py | 1 - 1 file changed, 1 deletion(-) diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py index f8792a7ff..53fbae5c0 100644 --- a/specutils/tests/test_xrayspectrum1d.py +++ b/specutils/tests/test_xrayspectrum1d.py @@ -1,5 +1,4 @@ import numpy as np -from scipy import optimize import astropy.units as u from astropy.modeling.powerlaws import PowerLaw1D from specutils import XraySpectrum1D From f5a996a0175e8c36d1a12c91c6109ebb27fe1495 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 10:56:46 -0500 Subject: [PATCH 55/72] Spelled out acronyms, link to edu materials --- specutils/spectra/xrayspectrum1d.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 0b72ccedf..05e9cc892 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -12,6 +12,9 @@ KEV = ['kev', 'keV'] ANGS = ['angs', 'Angs', 'Angstrom', 'angstrom', 'Angstroms', 'angstroms', 'A', 'a'] +## An introduction to X-ray analysis can be found here: +## http://cxc.cfa.harvard.edu/xrayschool/talks/intro_xray_analysis.pdf + def _unit_parser(unit_string): """ A function for matching unit strings to the correct Astropy unit @@ -97,12 +100,12 @@ def counts(self): def assign_arf(self, arf_inp): """ - Assign an ARF object to the XraySpectrum1D object + Assign an area response file (ARF) object to the XraySpectrum1D object Input ----- arf_inp : string - File name for the ARF (FITS file) + File name for the area response file (FITS file) Returns ------- @@ -116,12 +119,12 @@ def assign_arf(self, arf_inp): def assign_rmf(self, rmf_inp): """ - Assign an RMF object to the XraySpectrum1D object + Assign a response matrix file (RMF) object to the XraySpectrum1D object Input ----- rmf_inp : string - File name for the RMF (FITS file) + File name for the response matrix file (FITS file) Returns ------- @@ -191,7 +194,7 @@ def __init__(self, filename): def _load_rmf(self, filename): """ - Load an RMF from a FITS file. + Load a response matrix file (RMF) from a FITS file. Parameters ---------- @@ -446,7 +449,7 @@ def __init__(self, filename): def _load_arf(self, filename): """ - Load an ARF from a FITS file. + Load an area response file (ARF) from a FITS file. Parameters ---------- @@ -512,7 +515,7 @@ def _load_arf(self, filename): def apply_arf(self, spec, exposure=None): """ - Fold the spectrum through the ARF. + Fold the spectrum through the area response file (ARF). The ARF is a single vector encoding the effective area information about the detector. A such, applying the ARF is a simple multiplication with the input spectrum. From eed588cdb5c6c785d059ba77d6d120ed8c6208f6 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:05:22 -0500 Subject: [PATCH 56/72] Added option to specify RMF FITS extension for response matrix --- specutils/spectra/xrayspectrum1d.py | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 05e9cc892..68460d8b6 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -136,7 +136,7 @@ def assign_rmf(self, rmf_inp): self.rmf = rmf_inp return - def apply_resp(self, mflux, exposure=None): + def apply_response(self, mflux, exposure=None): """ Given a model flux spectrum, apply the response. In cases where the spectrum has both an ARF and an RMF, apply both. Otherwise, apply @@ -192,7 +192,7 @@ def __init__(self, filename): self._load_rmf(filename) pass - def _load_rmf(self, filename): + def _load_rmf(self, filename, extension=None): """ Load a response matrix file (RMF) from a FITS file. @@ -201,6 +201,10 @@ def _load_rmf(self, filename): filename : str The file name with the RMF file + extension : str (default None) + FITS file extinction keyword, if the response matrix is stored + under an extension other tham "MATRIX" or "SPECRESP MATRIX" + Attributes ---------- filename : str @@ -246,12 +250,20 @@ def _load_rmf(self, filename): extnames = np.array([h.name for h in hdulist]) # figure out the right extension to use - if "MATRIX" in extnames: + if extension is not None: + h = hdulist[extension] + + elif "MATRIX" in extnames: h = hdulist["MATRIX"] elif "SPECRESP MATRIX" in extnames: h = hdulist["SPECRESP MATRIX"] + else: + print("Cannot find common FITS file extension for response matrix values") + print("Please set the `extension` keyword") + return + data = h.data hdr = h.header hdulist.close() From 0fa3718a048314f120c9d2bb63a88af574db2496 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:15:09 -0500 Subject: [PATCH 57/72] corrected typos --- specutils/spectra/xrayspectrum1d.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 68460d8b6..e2eb6ce9c 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -202,8 +202,8 @@ def _load_rmf(self, filename, extension=None): The file name with the RMF file extension : str (default None) - FITS file extinction keyword, if the response matrix is stored - under an extension other tham "MATRIX" or "SPECRESP MATRIX" + FITS file extension keyword, if the response matrix is stored + under an extension other than "MATRIX" or "SPECRESP MATRIX" Attributes ---------- @@ -466,7 +466,7 @@ def _load_arf(self, filename): Parameters ---------- filename : str - The file name with the RMF file + The file name with the ARF file Attributes ---------- From aa458114d39d123c8fb0e93a00f84a7e492bbf94 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:19:14 -0500 Subject: [PATCH 58/72] Removing superfluous returns and pass --- specutils/spectra/xrayspectrum1d.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index e2eb6ce9c..610dea245 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -91,7 +91,6 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, self.exposure = exposure self.assign_rmf(rmf) self.assign_arf(arf) - return # Convenience function for Xray people @property @@ -115,7 +114,6 @@ def assign_arf(self, arf_inp): self.arf = ARF(arf_inp) else: self.arf = arf_inp - return def assign_rmf(self, rmf_inp): """ @@ -190,7 +188,6 @@ def apply_response(self, mflux, exposure=None): class RMF(object): def __init__(self, filename): self._load_rmf(filename) - pass def _load_rmf(self, filename, extension=None): """ From 674ae640e7627528b241b780513ac15d9f55a882 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:22:03 -0500 Subject: [PATCH 59/72] Catch ValueError --- specutils/spectra/xrayspectrum1d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 610dea245..34b34e7d8 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -79,7 +79,7 @@ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None, rest_value=0.0 * u.angstrom, **kwargs): try: axis_unit = u.Unit(bin_unit) - except: + except ValueError: axis_unit = _unit_parser(bin_unit) bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit From 029f8abee7243cdec11506162860e90fec4e5e99 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:27:25 -0500 Subject: [PATCH 60/72] Spelled out ARF and RMF class names --- specutils/spectra/xrayspectrum1d.py | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 34b34e7d8..3c452c7d3 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -5,7 +5,7 @@ from astropy.io import fits from .spectrum1d import Spectrum1D -__all__ = ['XraySpectrum1D', 'ARF', 'RMF'] +__all__ = ['XraySpectrum1D', 'AreaResponse', 'ResponseMatrix'] # For dealing with varied unit string choices EV = ['eV', 'ev'] @@ -47,12 +47,12 @@ class XraySpectrum1D(Spectrum1D): exposure : float Exposure time for the dataset - arf : specutils.ARF or string, default None - Strings will be passed to ARF.__init__ + arf : specutils.AreaResponse or string, default None + Strings will be passed to AreaResponse.__init__ All other input types are stored as arf attribute - rmf : specutils.RMF or string, default None - Strings will be passed to RMF.__init__ + rmf : specutils.ResponseMatrix or string, default None + Strings will be passed to ResponseMatrix.__init__ All other input types are stored as rmf attribute rest_value : astropy.units.Quantity, default 0 Angstrom @@ -111,7 +111,7 @@ def assign_arf(self, arf_inp): Modifies the XraySpectrum1D.arf attribute """ if isinstance(arf_inp, str): - self.arf = ARF(arf_inp) + self.arf = AreaResponse(arf_inp) else: self.arf = arf_inp @@ -129,7 +129,7 @@ def assign_rmf(self, rmf_inp): Modifies the XraySpectrum1D.rmf attribute """ if isinstance(rmf_inp, str): - self.rmf = RMF(rmf_inp) + self.rmf = ResponseMatrix(rmf_inp) else: self.rmf = rmf_inp return @@ -185,7 +185,7 @@ def apply_response(self, mflux, exposure=None): ## ---- Supporting response file objects -class RMF(object): +class ResponseMatrix(object): def __init__(self, filename): self._load_rmf(filename) @@ -449,7 +449,7 @@ def apply_rmf(self, spec): return counts[:self.detchans] -class ARF(object): +class AreaResponse(object): def __init__(self, filename): From 2590b7a05e957ca5cf77213916a35a7e9982fdfc Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:30:06 -0500 Subject: [PATCH 61/72] updated docstring --- specutils/spectra/xrayspectrum1d.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 3c452c7d3..cb99d21f8 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -137,8 +137,8 @@ def assign_rmf(self, rmf_inp): def apply_response(self, mflux, exposure=None): """ Given a model flux spectrum, apply the response. In cases where the - spectrum has both an ARF and an RMF, apply both. Otherwise, apply - whatever response is in RMF. + spectrum has both an area response file (ARF) and a response matrix + file (RMF), apply both. Otherwise, apply whatever response is in RMF. The model flux spectrum *must* be created using the same units and bins as in the ARF (where the ARF exists)! From f6b66f52196879cdce5f63854b19f563c6172944 Mon Sep 17 00:00:00 2001 From: eblur Date: Fri, 1 Jun 2018 11:34:47 -0500 Subject: [PATCH 62/72] Updated tests --- specutils/tests/test_xrayspectrum1d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py index 53fbae5c0..1e1f45bbe 100644 --- a/specutils/tests/test_xrayspectrum1d.py +++ b/specutils/tests/test_xrayspectrum1d.py @@ -29,6 +29,6 @@ def test_apply_model(): test_spec = test_create_from_arrays() new_model = PowerLaw1D(amplitude=5.e-3, alpha=1.8, x_0=1.e3) model_flux = new_model(test_spec.spectral_axis.value) - ymodel = test_spec.apply_resp(model_flux) + ymodel = test_spec.apply_response(model_flux) assert len(ymodel) == len(test_spec.spectral_axis) return From 4111295c4a990873bb7f75ac0c7d221e212e3fc2 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 12 Jun 2018 20:38:39 +0200 Subject: [PATCH 63/72] initialize attributes to None before loading from file --- specutils/spectra/xrayspectrum1d.py | 31 +++++++++++++++++++++-------- 1 file changed, 23 insertions(+), 8 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index cb99d21f8..5d98c79bc 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -187,9 +187,6 @@ def apply_response(self, mflux, exposure=None): class ResponseMatrix(object): def __init__(self, filename): - self._load_rmf(filename) - - def _load_rmf(self, filename, extension=None): """ Load a response matrix file (RMF) from a FITS file. @@ -237,6 +234,19 @@ def _load_rmf(self, filename, extension=None): The number of channels in the detector """ + self.filename = "" + self.offset = None + self.n_grp = None + self.f_chan = None + self.n_chan = None + self.matrix = None + self.energ_lo = None + self.energ_hi = None + self.energ_unit = None + self.detchans = None + self._load_rmf(filename) + + def _load_rmf(self, filename, extension=None): # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and # anxillary information @@ -452,11 +462,6 @@ def apply_rmf(self, spec): class AreaResponse(object): def __init__(self, filename): - - self._load_arf(filename) - pass - - def _load_arf(self, filename): """ Load an area response file (ARF) from a FITS file. @@ -492,6 +497,16 @@ def _load_arf(self, filename): Average exposure time for the dataset (takes telescope dithering into account) """ + self.filename = "" + self.e_low = None + self.e_high = None + self.e_unit = None + self.specresp = None + self.fracexpo = None + self.exposure = None + self._load_arf(filename) + + def _load_arf(self, filename): # open the FITS file and extract the MATRIX extension # which contains the redistribution matrix and # anxillary information From 8a1aa62787b3058068ca3e1c63fbaf9eaaf3f097 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Tue, 12 Jun 2018 20:39:55 +0200 Subject: [PATCH 64/72] save filename attribute for attempting load function --- specutils/spectra/xrayspectrum1d.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 5d98c79bc..58dcc3b73 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -234,7 +234,7 @@ def __init__(self, filename): The number of channels in the detector """ - self.filename = "" + self.filename = filename self.offset = None self.n_grp = None self.f_chan = None @@ -251,7 +251,6 @@ def _load_rmf(self, filename, extension=None): # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) - self.filename = filename # get all the extension names extnames = np.array([h.name for h in hdulist]) @@ -497,7 +496,7 @@ def __init__(self, filename): Average exposure time for the dataset (takes telescope dithering into account) """ - self.filename = "" + self.filename = filename self.e_low = None self.e_high = None self.e_unit = None @@ -511,7 +510,6 @@ def _load_arf(self, filename): # which contains the redistribution matrix and # anxillary information hdulist = fits.open(filename) - self.filename = filename h = hdulist["SPECRESP"] data = h.data From 4afde8f7e1d28f2146f8e50a57ac850a655a79c2 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 6 Dec 2018 13:30:05 -0700 Subject: [PATCH 65/72] [skip ci] provide xrayspectrum1d --- specutils/spectra/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/specutils/spectra/__init__.py b/specutils/spectra/__init__.py index 05d5d14c4..d03dac32a 100644 --- a/specutils/spectra/__init__.py +++ b/specutils/spectra/__init__.py @@ -3,10 +3,8 @@ `~astropy.nddata.NDData`-inherited classes used for storing the spectrum data. """ from .spectrum1d import * # noqa -<<<<<<< HEAD from .spectral_region import * # noqa from .spectrum_collection import * #noqa from .spectrum_list import * #noqa -======= + from .xrayspectrum1d import * ->>>>>>> Can load an X-ray spectrum From 90f8ad6871bde4a1b0353bc9a85f381682369458 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 6 Dec 2018 13:57:25 -0700 Subject: [PATCH 66/72] specutils flux input needs units --- specutils/tests/test_xrayspectrum1d.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py index 1e1f45bbe..4317bc836 100644 --- a/specutils/tests/test_xrayspectrum1d.py +++ b/specutils/tests/test_xrayspectrum1d.py @@ -11,7 +11,7 @@ def test_create_from_arrays(): elo = energy[:-1] ehi = energy[1:] emid = 0.5 * (elo + ehi) - counts = np.random.poisson(lam=powlaw0(emid), size=len(emid)) + counts = np.random.poisson(lam=powlaw0(emid), size=len(emid)) * u.ct test_spec = XraySpectrum1D(elo, ehi, u.keV, counts, exposure=1.0) return test_spec From fa80d2440a18430d62541d895da0681977d1650a Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 6 Dec 2018 14:32:03 -0700 Subject: [PATCH 67/72] attempting to fix docstrings --- specutils/spectra/xrayspectrum1d.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 58dcc3b73..c82d0cced 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -541,10 +541,12 @@ def apply_arf(self, spec, exposure=None): The ARF is a single vector encoding the effective area information about the detector. A such, applying the ARF is a simple multiplication with the input spectrum. + Parameters ---------- spec : numpy.ndarray The (model) spectrum to be folded + exposure : float, default None Value for the exposure time. By default, `apply_arf` will use the exposure keyword from the ARF file. If this exposure time is not @@ -552,6 +554,7 @@ def apply_arf(self, spec, exposure=None): time and the ARF from a real observation), one can override the default exposure by setting the `exposure` keyword to the correct value. + Returns ------- s_arf : numpy.ndarray From f14e9afd4095372b2b0359d043517f62eb859021 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 6 Dec 2018 14:54:07 -0700 Subject: [PATCH 68/72] further docstring fixes for sphinx --- specutils/spectra/xrayspectrum1d.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index c82d0cced..ce0771053 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -41,7 +41,7 @@ class XraySpectrum1D(Spectrum1D): bin_unit : string OR astropy.units.Unit Unit for the bin values - counts : numpy.ndarray + counts : astropy.Quantity array Counts histogram for the X-ray spectrum exposure : float @@ -374,10 +374,13 @@ def apply_rmf(self, spec): vector, there exists one or more sets of channels that this flux is redistributed into. The additional arrays `n_grp`, `f_chan` and `n_chan` store this information: + * `n_group` stores the number of channel groups for each energy bin + * `f_chan` stores the *first channel* that each channel for each channel set + * `n_chan` stores the number of channels in each channel set From eecce756203afcafdaac91e80a05ce7769b56e25 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 6 Dec 2018 17:18:41 -0700 Subject: [PATCH 69/72] added extension keyword to _load_arf --- specutils/spectra/xrayspectrum1d.py | 25 +++++++++++++++++++------ 1 file changed, 19 insertions(+), 6 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index ce0771053..102bc16d4 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -374,7 +374,7 @@ def apply_rmf(self, spec): vector, there exists one or more sets of channels that this flux is redistributed into. The additional arrays `n_grp`, `f_chan` and `n_chan` store this information: - + * `n_group` stores the number of channel groups for each energy bin @@ -508,13 +508,26 @@ def __init__(self, filename): self.exposure = None self._load_arf(filename) - def _load_arf(self, filename): - # open the FITS file and extract the MATRIX extension - # which contains the redistribution matrix and - # anxillary information + def _load_arf(self, filename, extension=None): + # open the FITS file and extract the SPECRESP extension + # which contains the spectral response for the telescope hdulist = fits.open(filename) - h = hdulist["SPECRESP"] + # get all the extension names + extnames = np.array([h.name for h in hdulist]) + + # figure out the right extension to use + if extension is not None: + h = hdulist[extension] + + elif 'SPECRESP' in extnames: + h = hdulist['SPECRESP'] + + else: + print("Cannot find common FITS file extension for spectral response") + print("Please set the `extension` keyword") + return + data = h.data hdr = h.header hdulist.close() From bdc5a418efbe49e659f015baffe7e85dbeef232d Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Thu, 6 Dec 2018 17:22:01 -0700 Subject: [PATCH 70/72] updated init and load calls with extension keyword --- specutils/spectra/xrayspectrum1d.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 102bc16d4..5ed8365e7 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -186,7 +186,7 @@ def apply_response(self, mflux, exposure=None): ## ---- Supporting response file objects class ResponseMatrix(object): - def __init__(self, filename): + def __init__(self, filename, extension=None): """ Load a response matrix file (RMF) from a FITS file. @@ -244,7 +244,7 @@ def __init__(self, filename): self.energ_hi = None self.energ_unit = None self.detchans = None - self._load_rmf(filename) + self._load_rmf(filename, extension=extension) def _load_rmf(self, filename, extension=None): # open the FITS file and extract the MATRIX extension @@ -463,7 +463,7 @@ def apply_rmf(self, spec): class AreaResponse(object): - def __init__(self, filename): + def __init__(self, filename, extension=None): """ Load an area response file (ARF) from a FITS file. @@ -472,6 +472,10 @@ def __init__(self, filename): filename : str The file name with the ARF file + extension : str (default None) + FITS file extension keyword, if the spectral response is stored + under an extension other than "SPECRESP" + Attributes ---------- filename : str @@ -506,7 +510,7 @@ def __init__(self, filename): self.specresp = None self.fracexpo = None self.exposure = None - self._load_arf(filename) + self._load_arf(filename, extension=extension) def _load_arf(self, filename, extension=None): # open the FITS file and extract the SPECRESP extension From 3fef5b91c9fece08e5005887ab6b3b660510c3b2 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Fri, 7 Dec 2018 13:53:48 -0700 Subject: [PATCH 71/72] XraySpectrum1d uses astropy.Quantity for all inputs --- specutils/spectra/xrayspectrum1d.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 5ed8365e7..28bfb7742 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -32,19 +32,16 @@ class XraySpectrum1D(Spectrum1D): Parameters ---------- - bin_lo : numpy.ndarray + bin_lo : astropy.Quantity The left edges for bin values - bin_hi : numpy.ndarray + bin_hi : astropy.Quantity The right edges for bin values - bin_unit : string OR astropy.units.Unit - Unit for the bin values - - counts : astropy.Quantity array + counts : astropy.Quantity Counts histogram for the X-ray spectrum - exposure : float + exposure : astropy.Quantity Exposure time for the dataset arf : specutils.AreaResponse or string, default None @@ -77,12 +74,8 @@ class XraySpectrum1D(Spectrum1D): """ def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, arf=None, rmf=None, rest_value=0.0 * u.angstrom, **kwargs): - try: - axis_unit = u.Unit(bin_unit) - except ValueError: - axis_unit = _unit_parser(bin_unit) - bin_mid = 0.5 * (bin_lo + bin_hi) * axis_unit + bin_mid = 0.5 * (bin_lo + bin_hi) Spectrum1D.__init__(self, spectral_axis=bin_mid, flux=counts, rest_value=rest_value, **kwargs) From 8b4b945e83c66c7edae91e3e6b30ca4d1e5c6207 Mon Sep 17 00:00:00 2001 From: Lia Corrales Date: Fri, 7 Dec 2018 13:58:56 -0700 Subject: [PATCH 72/72] tested xrayspectrum1d with quantity inputs --- specutils/spectra/xrayspectrum1d.py | 2 +- specutils/tests/test_xrayspectrum1d.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/specutils/spectra/xrayspectrum1d.py b/specutils/spectra/xrayspectrum1d.py index 28bfb7742..68f12da3f 100644 --- a/specutils/spectra/xrayspectrum1d.py +++ b/specutils/spectra/xrayspectrum1d.py @@ -72,7 +72,7 @@ class XraySpectrum1D(Spectrum1D): rmf """ - def __init__(self, bin_lo, bin_hi, bin_unit, counts, exposure, + def __init__(self, bin_lo, bin_hi, counts, exposure, arf=None, rmf=None, rest_value=0.0 * u.angstrom, **kwargs): bin_mid = 0.5 * (bin_lo + bin_hi) diff --git a/specutils/tests/test_xrayspectrum1d.py b/specutils/tests/test_xrayspectrum1d.py index 4317bc836..6a561307f 100644 --- a/specutils/tests/test_xrayspectrum1d.py +++ b/specutils/tests/test_xrayspectrum1d.py @@ -8,11 +8,11 @@ def test_create_from_arrays(): amp0, alpha0 = 3.e-3, 2.0 powlaw0 = PowerLaw1D(amplitude=amp0, alpha=alpha0, x_0=1.e3) energy = np.linspace(0.2, 10.0, 8000) - elo = energy[:-1] - ehi = energy[1:] + elo = energy[:-1] * u.keV + ehi = energy[1:] * u.keV emid = 0.5 * (elo + ehi) - counts = np.random.poisson(lam=powlaw0(emid), size=len(emid)) * u.ct - test_spec = XraySpectrum1D(elo, ehi, u.keV, counts, exposure=1.0) + counts = np.random.poisson(lam=powlaw0(emid.value), size=len(emid)) * u.ct + test_spec = XraySpectrum1D(elo, ehi, counts, exposure=1.0*u.second) return test_spec def test_call_spectrum():