def _from_xspec(cls, xspec_in, emin, emax, nbins): emin = parse_value(emin, "keV") emax = parse_value(emax, "keV") tmpdir = tempfile.mkdtemp() curdir = os.getcwd() os.chdir(tmpdir) xspec_in.append("dummyrsp %g %g %d lin\n" % (emin, emax, nbins)) xspec_in += [ "set fp [open spec_therm.xspec w+]\n", "tclout energies\n", "puts $fp $xspec_tclout\n", "tclout modval\n", "puts $fp $xspec_tclout\n", "close $fp\n", "quit\n" ] f_xin = open("xspec.in", "w") f_xin.writelines(xspec_in) f_xin.close() logfile = os.path.join(curdir, "xspec.log") with open(logfile, "ab") as xsout: subprocess.call(["xspec", "-", "xspec.in"], stdout=xsout, stderr=xsout) f_s = open("spec_therm.xspec", "r") lines = f_s.readlines() f_s.close() ebins = np.array(lines[0].split()).astype("float64") de = np.diff(ebins)[0] flux = np.array(lines[1].split()).astype("float64") / de os.chdir(curdir) shutil.rmtree(tmpdir) return cls(ebins, flux)
def _spectrum_init(self, kT, velocity, elem_abund): kT = parse_value(kT, "keV") velocity = parse_value(velocity, "km/s") v = velocity * 1.0e5 tindex = np.searchsorted(self.Tvals, kT) - 1 dT = (kT - self.Tvals[tindex]) / self.dTvals[tindex] return kT, dT, tindex, v
def from_powerlaw(cls, photon_index, redshift, norm, emin, emax, nbins): """ Create a spectrum from a power-law model. Parameters ---------- photon_index : float The photon index of the source. redshift : float The redshift of the source. norm : float The normalization of the source in units of photons/s/cm**2/keV at 1 keV in the source frame. emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The minimum energy of the spectrum in keV. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The maximum energy of the spectrum in keV. nbins : integer The number of bins in the spectrum. """ emin = parse_value(emin, 'keV') emax = parse_value(emax, 'keV') ebins = np.linspace(emin, emax, nbins + 1) emid = 0.5 * (ebins[1:] + ebins[:-1]) flux = norm * (emid * (1.0 + redshift))**(-photon_index) return cls(ebins, flux)
def generate_energies(self, t_exp, fov, prng=None, quiet=False): """ Generate photon energies from this convolved background spectrum given an exposure time and field of view. Parameters ---------- t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The width of the field of view on a side in arcminutes. prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. quiet : boolean, optional If True, log messages will not be displayed when creating energies. Useful if you have to loop over a lot of spectra. Default: False """ t_exp = parse_value(t_exp, "s") fov = parse_value(fov, "arcmin") prng = parse_prng(prng) rate = fov * fov * self.total_flux.value energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet) earea = self.arf.interpolate_area(energy).value flux = np.sum(energy) * erg_per_keV / t_exp / earea.sum() energies = Energies(energy, flux) return energies
def get_flux_in_band(self, emin, emax): """ Determine the total flux within a band specified by an energy range. Parameters ---------- emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The minimum energy in the band, in keV. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The maximum energy in the band, in keV. Returns ------- A tuple of values for the flux/intensity in the band: the first value is in terms of the photon rate, the second value is in terms of the energy rate. """ emin = parse_value(emin, "keV") emax = parse_value(emax, "keV") range = np.logical_and(self.emid.value >= emin, self.emid.value <= emax) pflux = self.flux[range].sum() * self.de eflux = (self.flux * self.emid.to("erg"))[range].sum() * self.de / (1.0 * u.photon) return pflux, eflux
def generate_fluxes(exp_time, area, fov, prng): from xcs_soxs.data import cdf_fluxes, cdf_gal, cdf_agn exp_time = parse_value(exp_time, "s") area = parse_value(area, "cm**2") fov = parse_value(fov, "arcmin") logf = np.log10(cdf_fluxes) n_gal = np.rint(cdf_gal[-1]) n_agn = np.rint(cdf_agn[-1]) F_gal = cdf_gal / cdf_gal[-1] F_agn = cdf_agn / cdf_agn[-1] f_gal = InterpolatedUnivariateSpline(F_gal, logf) f_agn = InterpolatedUnivariateSpline(F_agn, logf) eph_mean_erg = 1.0 * erg_per_keV S_min_obs = eph_mean_erg / (exp_time * area) mylog.debug("Flux of %g erg/cm^2/s gives roughly " "one photon during exposure." % S_min_obs) fov_area = fov**2 n_gal = int(n_gal * fov_area / 3600.0) n_agn = int(n_agn * fov_area / 3600.0) mylog.debug("%d AGN, %d galaxies in the FOV." % (n_agn, n_gal)) randvec1 = prng.uniform(size=n_agn) agn_fluxes = 10**f_agn(randvec1) randvec2 = prng.uniform(size=n_gal) gal_fluxes = 10**f_gal(randvec2) return agn_fluxes, gal_fluxes
def generate_energies(self, t_exp, area, prng=None, quiet=False): """ Generate photon energies from this spectrum given an exposure time and effective area. Parameters ---------- t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The effective area in cm**2. If one is creating events for a SIMPUT file, a constant should be used and it must be large enough so that a sufficiently large sample is drawn for the ARF. prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. quiet : boolean, optional If True, log messages will not be displayed when creating energies. Useful if you have to loop over a lot of spectra. Default: False """ t_exp = parse_value(t_exp, "s") area = parse_value(area, "cm**2") prng = parse_prng(prng) rate = area * self.total_flux.value energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet) flux = np.sum(energy) * erg_per_keV / t_exp / area energies = Energies(energy, flux) return energies
def to_scaled_spectrum(self, fov, focal_length=None): from xcs_soxs.instrument import FlatResponse fov = parse_value(fov, "arcmin") if focal_length is None: focal_length = self.default_focal_length else: focal_length = parse_value(focal_length, "m") flux = self.flux.value * fov * fov flux *= (focal_length / self.default_focal_length)**2 arf = FlatResponse(self.ebins.value[0], self.ebins.value[-1], 1.0, self.ebins.size - 1) return ConvolvedSpectrum(Spectrum(self.ebins.value, flux), arf)
def __init__(self, ra0, dec0, r_in, r_out, theta=0.0, ellipticity=1.0): r_in = parse_value(r_in, "arcsec") r_out = parse_value(r_out, "arcsec") def func(r): f = np.zeros(r.size) idxs = np.logical_and(r >= r_in, r < r_out) f[idxs] = 1.0 return f super(AnnulusModel, self).__init__(ra0, dec0, func, theta=theta, ellipticity=ellipticity)
def apply_foreground_absorption(self, nH, model="wabs", redshift=0.0): """ Given a hydrogen column density, apply galactic foreground absorption to the spectrum. Parameters ---------- nH : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The hydrogen column in units of 10**22 atoms/cm**2 model : string, optional The model for absorption to use. Options are "wabs" (Wisconsin, Morrison and McCammon; ApJ 270, 119) or "tbabs" (Tuebingen-Boulder, Wilms, J., Allen, A., & McCray, R. 2000, ApJ, 542, 914). Default: "wabs". redshift : float, optional The redshift of the absorbing material. Default: 0.0 """ nH = parse_value(nH, "1.0e22*cm**-2") e = self.emid.value * (1.0 + redshift) if model == "wabs": sigma = wabs_cross_section(e) elif model == "tbabs": sigma = tbabs_cross_section(e) self.flux *= np.exp(-nH * 1.0e22 * sigma) self._compute_total_flux()
def __init__(self, ra0, dec0, r_c, beta, theta=0.0, ellipticity=1.0): r_c = parse_value(r_c, "arcsec") func = lambda r: (1.0 + (r / r_c)**2)**(-3 * beta + 0.5) super(BetaModel, self).__init__(ra0, dec0, func, theta=theta, ellipticity=ellipticity)
def generate_sources(exp_time, fov, sky_center, area=40000.0, prng=None): r""" Make a catalog of point sources. Parameters ---------- exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time of the observation in seconds. fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The field of view in arcminutes. sky_center : array-like The center RA, Dec of the field of view in degrees. area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The effective area in cm**2. It must be large enough so that a sufficiently large sample is drawn for the ARF. Default: 40000. prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. """ prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") fov = parse_value(fov, "arcmin") area = parse_value(area, "cm**2") agn_fluxes, gal_fluxes = generate_fluxes(exp_time, area, fov, prng) fluxes = np.concatenate([agn_fluxes, gal_fluxes]) ind = np.concatenate([ get_agn_index(np.log10(agn_fluxes)), gal_index * np.ones(gal_fluxes.size) ]) dec_scal = np.fabs(np.cos(sky_center[1] * np.pi / 180)) ra_min = sky_center[0] - fov / (2.0 * 60.0 * dec_scal) dec_min = sky_center[1] - fov / (2.0 * 60.0) ra0 = prng.uniform(size=fluxes.size) * fov / (60.0 * dec_scal) + ra_min dec0 = prng.uniform(size=fluxes.size) * fov / 60.0 + dec_min return ra0, dec0, fluxes, ind
def new_spec_from_band(self, emin, emax): """ Create a new :class:`~xcs_soxs.spectra.Spectrum` object from a subset of an existing one defined by a particular energy band. Parameters ---------- emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The minimum energy of the band in keV. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The maximum energy of the band in keV. """ emin = parse_value(emin, "keV") emax = parse_value(emax, 'keV') band = np.logical_and(self.ebins.value >= emin, self.ebins.value <= emax) idxs = np.where(band)[0] ebins = self.ebins.value[idxs] flux = self.flux.value[idxs[:-1]] return Spectrum(ebins, flux)
def add_absorption_line(self, line_center, line_width, equiv_width, line_type='gaussian'): """ Add an absorption line to this spectrum. Parameters ---------- line_center : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The line center position in units of keV, in the observer frame. line_width : one or more float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The line width (FWHM) in units of keV, in the observer frame. Can also input the line width in units of velocity in the rest frame. For the Voigt profile, a list, tuple, or array of two values should be provided since there are two line widths, the Lorentzian and the Gaussian (in that order). equiv_width : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The equivalent width of the line, in units of milli-Angstrom line_type : string, optional The line profile type. Default: "gaussian" """ line_center = parse_value(line_center, "keV") line_width = parse_value(line_width, "keV", equivalence=line_width_equiv(line_center)) equiv_width = parse_value(equiv_width, "1.0e-3*angstrom") # in milliangstroms equiv_width *= 1.0e-3 # convert to angstroms if line_type == "gaussian": sigma = line_width / sigma_to_fwhm B = equiv_width * line_center * line_center B /= hc * sqrt2pi * sigma f = Gaussian1D(B, line_center, sigma) else: raise NotImplementedError("Line profile type '%s' " % line_type + "not implemented!") self.flux *= np.exp(-f(self.emid.value)) self._compute_total_flux()
def from_models(cls, name, spectral_model, spatial_model, t_exp, area, prng=None): """ Generate a single photon list from a spectral and a spatial model. Parameters ---------- name : string The name of the photon list. This will also be the prefix of any photon list file that is written from this photon list. spectral_model : :class:`~soxs.spectra.Spectrum` The spectral model to use to generate the event energies. spatial_model : :class:`~soxs.spatial.SpatialModel` The spatial model to use to generate the event coordinates. t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The effective area in cm**2. If one is creating events for a SIMPUT file, a constant should be used and it must be large enough so that a sufficiently large sample is drawn for the ARF. prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. """ prng = parse_prng(prng) t_exp = parse_value(t_exp, "s") area = parse_value(area, "cm**2") e = spectral_model.generate_energies(t_exp, area, prng=prng) ra, dec = spatial_model.generate_coords(e.size, prng=prng) return cls(name, ra, dec, e, e.flux)
def rescale_flux(self, new_flux, emin=None, emax=None, flux_type="photons"): """ Rescale the flux of the spectrum, optionally using a specific energy band. Parameters ---------- new_flux : float The new flux in units of photons/s/cm**2. emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The minimum energy of the band to consider, in keV. Default: Use the minimum energy of the entire spectrum. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The maximum energy of the band to consider, in keV. Default: Use the maximum energy of the entire spectrum. flux_type : string, optional The units of the flux to use in the rescaling: "photons": photons/s/cm**2 "energy": erg/s/cm**2 """ if emin is None: emin = self.ebins[0].value if emax is None: emax = self.ebins[-1].value emin = parse_value(emin, "keV") emax = parse_value(emax, 'keV') idxs = np.logical_and(self.emid.value >= emin, self.emid.value <= emax) if flux_type == "photons": f = self.flux[idxs].sum() * self.de elif flux_type == "energy": f = (self.flux * self.emid.to("erg"))[idxs].sum() * self.de self.flux *= new_flux / f.value self._compute_total_flux()
def from_constant(cls, const_flux, emin, emax, nbins): """ Create a spectrum from a constant model using XSPEC. Parameters ---------- const_flux : float The value of the constant flux in the units of the spectrum. emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The minimum energy of the spectrum in keV. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The maximum energy of the spectrum in keV. nbins : integer The number of bins in the spectrum. """ emin = parse_value(emin, "keV") emax = parse_value(emax, 'keV') ebins = np.linspace(emin, emax, nbins + 1) flux = const_flux * np.ones(nbins) return cls(ebins, flux)
def make_simple_instrument(base_inst, new_inst, fov, num_pixels, no_bkgnd=False, no_psf=False, no_dither=False): """ Using an existing imaging instrument specification, make a simple square instrument given a field of view and a resolution. Parameters ---------- base_inst : string The name for the instrument specification to base the new one on. new_inst : string The name for the new instrument specification. fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The field of view in arcminutes. num_pixels : integer The number of pixels on a side. no_bkgnd : boolean, optional Set this new instrument to have no particle background. Default: False no_psf : boolean, optional Set this new instrument to have no spatial PSF. Default: False no_dither : boolean, optional Set this new instrument to have no dithering. Default: False """ sq_inst = get_instrument_from_registry(base_inst) if sq_inst["imaging"] is False: raise RuntimeError("make_simple_instrument only works with " "imaging instruments!") sq_inst["name"] = new_inst sq_inst["chips"] = None sq_inst["fov"] = parse_value(fov, "arcmin") sq_inst["num_pixels"] = num_pixels if no_bkgnd: sq_inst["bkgnd"] = None elif base_inst.startswith("aciss"): # Special-case ACIS-S to use the BI background on S3 sq_inst["bkgnd"] = "aciss" if no_psf: sq_inst["psf"] = None if sq_inst["dither"]: sq_inst["dither"] = not no_dither add_instrument_to_registry(sq_inst)
def add_emission_line(self, line_center, line_width, line_amp, line_type="gaussian"): """ Add an emission line to this spectrum. Parameters ---------- line_center : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The line center position in units of keV, in the observer frame. line_width : one or more float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The line width (FWHM) in units of keV, in the observer frame. Can also input the line width in units of velocity in the rest frame. For the Voigt profile, a list, tuple, or array of two values should be provided since there are two line widths, the Lorentzian and the Gaussian (in that order). line_amp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The integrated line amplitude in the units of the flux line_type : string, optional The line profile type. Default: "gaussian" """ line_center = parse_value(line_center, "keV") line_width = parse_value(line_width, "keV", equivalence=line_width_equiv(line_center)) line_amp = parse_value(line_amp, self._units) if line_type == "gaussian": sigma = line_width / sigma_to_fwhm line_amp /= sqrt2pi * sigma f = Gaussian1D(line_amp, line_center, sigma) else: raise NotImplementedError("Line profile type '%s' " % line_type + "not implemented!") self.flux += u.Quantity(f(self.emid.value), self._units) self._compute_total_flux()
def from_spectrum(cls, spec, fov): """ Create a background spectrum from a regular :class:`~xcs_soxs.spectra.Spectrum` object and the width of a field of view on a side. Parameters ---------- spec : :class:`~soxs.spectra.Spectrum` The spectrum to be used. fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The width of the field of view on a side in arcminutes. """ fov = parse_value(fov, "arcmin") flux = spec.flux.value / fov / fov return cls(spec.flux.ebins.value, flux)
def add_instrumental_background(name, filename, default_focal_length): """ Add a particle/instrument background to the list of known backgrounds. Parameters ---------- name : string The short name of the background, which will be the key in the registry. filename : string The file containing the background. It must have two columns: energy in keV, and background intensity in units of photons/s/cm**2/arcmin**2/keV. default_focal_length : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The focal length of the telescope that this background is scaled to. Used for rescaling the background if an alternative focal length is provided in an instrument specification. """ default_focal_length = parse_value(default_focal_length, "m") spec = InstrumentalBackgroundSpectrum.from_file(filename, default_focal_length) instrument_backgrounds[name] = spec
def __init__(self, ra0, dec0, fov): fov = parse_value(fov, "arcmin") width = fov * 60.0 height = fov * 60.0 super(FillFOVModel, self).__init__(ra0, dec0, width, height)
def make_ptsrc_background(exp_time, fov, sky_center, absorb_model="wabs", nH=0.05, area=40000.0, input_sources=None, output_sources=None, prng=None): r""" Make a point-source background. Parameters ---------- exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time of the observation in seconds. fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The field of view in arcminutes. sky_center : array-like The center RA, Dec of the field of view in degrees. absorb_model : string, optional The absorption model to use, "wabs" or "tbabs". Default: "wabs" nH : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The hydrogen column in units of 10**22 atoms/cm**2. Default: 0.05 area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The effective area in cm**2. It must be large enough so that a sufficiently large sample is drawn for the ARF. Default: 40000. input_sources : string, optional If set to a filename, input the source positions, fluxes, and spectral indices from an ASCII table instead of generating them. Default: None output_sources : string, optional If set to a filename, output the properties of the sources within the field of view to a file. Default: None prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. """ prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") fov = parse_value(fov, "arcmin") if nH is not None: nH = parse_value(nH, "1.0e22*cm**-2") area = parse_value(area, "cm**2") if input_sources is None: ra0, dec0, fluxes, ind = generate_sources(exp_time, fov, sky_center, area=area, prng=prng) num_sources = fluxes.size else: mylog.info("Reading in point-source properties from %s." % input_sources) t = ascii.read(input_sources) ra0 = t["RA"].data dec0 = t["Dec"].data fluxes = t["flux_0.5_2.0_keV"].data ind = t["index"].data num_sources = fluxes.size mylog.debug("Generating spectra from %d sources." % num_sources) # If requested, output the source properties to a file if output_sources is not None: t = Table([ra0, dec0, fluxes, ind], names=('RA', 'Dec', 'flux_0.5_2.0_keV', 'index')) t["RA"].unit = "deg" t["Dec"].unit = "deg" t["flux_0.5_2.0_keV"].unit = "erg/(cm**2*s)" t["index"].unit = "" t.write(output_sources, format='ascii.ecsv', overwrite=True) # Pre-calculate for optimization eratio = spec_emax / spec_emin oma = 1.0 - ind invoma = 1.0 / oma invoma[oma == 0.0] = 1.0 fac1 = spec_emin**oma fac2 = spec_emax**oma - fac1 fluxscale = get_flux_scale(ind, fb_emin, fb_emax, spec_emin, spec_emax) # Using the energy flux, determine the photon flux by simple scaling ref_ph_flux = fluxes * fluxscale * keV_per_erg # Now determine the number of photons we will generate n_photons = prng.poisson(ref_ph_flux * exp_time * area) all_energies = [] all_ra = [] all_dec = [] for i, nph in enumerate(n_photons): if nph > 0: # Generate the energies in the source frame u = prng.uniform(size=nph) if ind[i] == 1.0: energies = spec_emin * (eratio**u) else: energies = fac1[i] + u * fac2[i] energies **= invoma[i] # Assign positions for this source ra = ra0[i] * np.ones(nph) dec = dec0[i] * np.ones(nph) all_energies.append(energies) all_ra.append(ra) all_dec.append(dec) mylog.debug("Finished generating spectra.") all_energies = np.concatenate(all_energies) all_ra = np.concatenate(all_ra) all_dec = np.concatenate(all_dec) all_nph = all_energies.size # Remove some of the photons due to Galactic foreground absorption. # We will throw a lot of stuff away, but this is more general and still # faster. if nH is not None: if absorb_model == "wabs": absorb = get_wabs_absorb(all_energies, nH) elif absorb_model == "tbabs": absorb = get_tbabs_absorb(all_energies, nH) randvec = prng.uniform(size=all_energies.size) all_energies = all_energies[randvec < absorb] all_ra = all_ra[randvec < absorb] all_dec = all_dec[randvec < absorb] all_nph = all_energies.size mylog.debug("%d photons remain after foreground galactic absorption." % all_nph) all_flux = np.sum(all_energies) * erg_per_keV / (exp_time * area) output_events = { "ra": all_ra, "dec": all_dec, "energy": all_energies, "flux": all_flux } return output_events
def to_spectrum(self, fov): fov = parse_value(fov, "arcmin") flux = self.flux.value * fov * fov return Spectrum(self.ebins.value, flux)
def __init__(self, ra0, dec0): self.ra0 = parse_value(ra0, "deg") self.dec0 = parse_value(dec0, "deg") self.w = construct_wcs(self.ra0, self.dec0)
def plot(self, center, width, s=None, c=None, marker=None, stride=1, emin=None, emax=None, label=None, fontsize=18, fig=None, ax=None, **kwargs): """ Plot event coordinates from this photon list in a scatter plot, optionally restricting the photon energies which are plotted and using only a subset of the photons. Parameters ---------- center : array-like The RA, Dec of the center of the plot in degrees. width : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The width of the plot in arcminutes. s : integer, optional Size of the scatter marker in points^2. c : string, optional The color of the points. marker : string, optional The marker to use for the points in the scatter plot. Default: 'o' stride : integer, optional Plot every *stride* events. Default: 1 emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The minimum energy of the photons to plot. Default is the minimum energy in the list. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The maximum energy of the photons to plot. Default is the maximum energy in the list. label : string, optional The label of the spectrum. Default: None fontsize : int Font size for labels and axes. Default: 18 fig : :class:`~matplotlib.figure.Figure`, optional A Figure instance to plot in. Default: None, one will be created if not provided. ax : :class:`~matplotlib.axes.Axes`, optional An Axes instance to plot in. Default: None, one will be created if not provided. """ import matplotlib.pyplot as plt from astropy.visualization.wcsaxes import WCSAxes if fig is None: fig = plt.figure(figsize=(10, 10)) if ax is None: wcs = construct_wcs(center[0], center[1]) ax = WCSAxes(fig, [0.15, 0.1, 0.8, 0.8], wcs=wcs) fig.add_axes(ax) else: wcs = ax.wcs if emin is None: emin = self.energy.value.min() else: emin = parse_value(emin, "keV") if emax is None: emax = self.energy.value.max() else: emax = parse_value(emax, "keV") idxs = np.logical_and(self.energy.value >= emin, self.energy.value <= emax) ra = self.ra[idxs][::stride].value dec = self.dec[idxs][::stride].value x, y = wcs.wcs_world2pix(ra, dec, 1) ax.scatter(x, y, s=s, c=c, marker=marker, label=label, **kwargs) x0, y0 = wcs.wcs_world2pix(center[0], center[1], 1) width = parse_value(width, "arcmin") * 60.0 ax.set_xlim(x0 - 0.5 * width, x0 + 0.5 * width) ax.set_ylim(y0 - 0.5 * width, y0 + 0.5 * width) ax.set_xlabel("RA") ax.set_ylabel("Dec") ax.tick_params(axis='both', labelsize=fontsize) return fig, ax
def write_image(evt_file, out_file, coord_type='sky', emin=None, emax=None, overwrite=False, expmap_file=None, reblock=1): r""" Generate a image by binning X-ray counts and write it to a FITS file. Parameters ---------- evt_file : string The name of the input event file to read. out_file : string The name of the image file to write. coord_type : string, optional The type of coordinate to bin into an image. Can be "sky" or "det". Default: "sky" emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The minimum energy of the photons to put in the image, in keV. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The maximum energy of the photons to put in the image, in keV. overwrite : boolean, optional Whether or not to overwrite an existing file with the same name. Default: False expmap_file : string, optional Supply an exposure map file to divide this image by to get a flux map. Default: None reblock : integer, optional Change this value to reblock the image to larger pixel sizes (reblock >= 1). Only supported for sky coordinates. Default: 1 """ if coord_type == "det" and reblock > 1: raise RuntimeError("Reblocking images is not supported " "for detector coordinates!") f = pyfits.open(evt_file) e = f["EVENTS"].data["ENERGY"] if emin is None: emin = e.min() else: emin = parse_value(emin, "keV") emin *= 1000. if emax is None: emax = e.max() else: emax = parse_value(emax, "keV") emax *= 1000. idxs = np.logical_and(e > emin, e < emax) xcoord, ycoord, xcol, ycol = coord_types[coord_type] x = f["EVENTS"].data[xcoord][idxs] y = f["EVENTS"].data[ycoord][idxs] exp_time = f["EVENTS"].header["EXPOSURE"] xmin = f["EVENTS"].header["TLMIN%d" % xcol] ymin = f["EVENTS"].header["TLMIN%d" % ycol] xmax = f["EVENTS"].header["TLMAX%d" % xcol] ymax = f["EVENTS"].header["TLMAX%d" % ycol] if coord_type == 'sky': xctr = f["EVENTS"].header["TCRVL%d" % xcol] yctr = f["EVENTS"].header["TCRVL%d" % ycol] xdel = f["EVENTS"].header["TCDLT%d" % xcol] * reblock ydel = f["EVENTS"].header["TCDLT%d" % ycol] * reblock f.close() nx = int(xmax - xmin) // reblock ny = int(ymax - ymin) // reblock xbins = np.linspace(xmin, xmax, nx + 1, endpoint=True) ybins = np.linspace(ymin, ymax, ny + 1, endpoint=True) H, xedges, yedges = np.histogram2d(x, y, bins=[xbins, ybins]) if expmap_file is not None: if coord_type == "det": raise RuntimeError("Cannot divide by an exposure map for images " "binned in detector coordinates!") f = pyfits.open(expmap_file) if f["EXPMAP"].shape != (nx, ny): raise RuntimeError( "Exposure map and image do not have the same shape!!") with np.errstate(invalid='ignore', divide='ignore'): H /= f["EXPMAP"].data.T H[np.isinf(H)] = 0.0 H = np.nan_to_num(H) H[H < 0.0] = 0.0 f.close() hdu = pyfits.PrimaryHDU(H.T) if coord_type == 'sky': hdu.header["MTYPE1"] = "EQPOS" hdu.header["MFORM1"] = "RA,DEC" hdu.header["CTYPE1"] = "RA---TAN" hdu.header["CTYPE2"] = "DEC--TAN" hdu.header["CRVAL1"] = xctr hdu.header["CRVAL2"] = yctr hdu.header["CUNIT1"] = "deg" hdu.header["CUNIT2"] = "deg" hdu.header["CDELT1"] = xdel hdu.header["CDELT2"] = ydel hdu.header["CRPIX1"] = 0.5 * (nx + 1) hdu.header["CRPIX2"] = 0.5 * (ny + 1) else: hdu.header["CUNIT1"] = "pixel" hdu.header["CUNIT2"] = "pixel" hdu.header["EXPOSURE"] = exp_time hdu.writeto(out_file, overwrite=overwrite)
def write_radial_profile(evt_file, out_file, ctr, rmin, rmax, nbins, ctr_type="celestial", emin=None, emax=None, expmap_file=None, overwrite=False): r""" Bin up events into a radial profile and write them to a FITS table. Parameters ---------- evt_file : string Input event file. out_file : string The output file to write the profile to. ctr : array-like The central coordinate of the profile. Can either be in celestial coordinates (the default) or "physical" pixel coordinates. If the former, the ``ctr_type`` keyword argument must be explicity set to "physical". rmin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The minimum radius of the profile, in arcseconds. rmax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The maximum radius of the profile, in arcseconds. nbins : integer The number of bins in the profile. ctr_type : string, optional The type of center coordinate. Either "celestial" for (RA, Dec) coordinates (the default), or "physical" for pixel coordinates. emin : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The minimum energy of the events to be binned in keV. Default is the lowest energy available. emax : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The maximum energy of the events to be binned in keV. Default is the highest energy available. overwrite : boolean, optional Whether or not to overwrite an existing file with the same name. Default: False expmap_file : string, optional Supply an exposure map file to determine fluxes. Default: None """ import astropy.wcs as pywcs rmin = parse_value(rmin, "arcsec") rmax = parse_value(rmax, "arcsec") f = pyfits.open(evt_file) hdu = f["EVENTS"] orig_dx = hdu.header["TCDLT3"] e = hdu.data["ENERGY"] if emin is None: emin = e.min() else: emin = parse_value(emin, "keV") emin *= 1000. if emax is None: emax = e.max() else: emax = parse_value(emax, "keV") emax *= 1000. idxs = np.logical_and(e > emin, e < emax) x = hdu.data["X"][idxs] y = hdu.data["Y"][idxs] exp_time = hdu.header["EXPOSURE"] w = wcs_from_event_file(f) dtheta = np.abs(w.wcs.cdelt[1]) * 3600.0 f.close() if ctr_type == "celestial": ctr = w.all_world2pix(ctr[0], ctr[1], 1) r = np.sqrt((x - ctr[0])**2 + (y - ctr[1])**2) rr = np.linspace(rmin / dtheta, rmax / dtheta, nbins + 1) C = np.histogram(r, bins=rr)[0] rbin = rr * dtheta rmid = 0.5 * (rbin[1:] + rbin[:-1]) A = np.pi * (rbin[1:]**2 - rbin[:-1]**2) Cerr = np.sqrt(C) R = C / exp_time Rerr = Cerr / exp_time S = R / A Serr = Rerr / A col1 = pyfits.Column(name='RLO', format='D', unit='arcsec', array=rbin[:-1]) col2 = pyfits.Column(name='RHI', format='D', unit='arcsec', array=rbin[1:]) col3 = pyfits.Column(name='RMID', format='D', unit='arcsec', array=rmid) col4 = pyfits.Column(name='AREA', format='D', unit='arcsec**2', array=A) col5 = pyfits.Column(name='NET_COUNTS', format='D', unit='count', array=C) col6 = pyfits.Column(name='NET_ERR', format='D', unit='count', array=Cerr) col7 = pyfits.Column(name='NET_RATE', format='D', unit='count/s', array=R) col8 = pyfits.Column(name='ERR_RATE', format='D', unit='count/s', array=Rerr) col9 = pyfits.Column(name='SUR_BRI', format='D', unit='count/s/arcsec**2', array=S) col10 = pyfits.Column(name='SUR_BRI_ERR', format='1D', unit='count/s/arcsec**2', array=Serr) coldefs = [col1, col2, col3, col4, col5, col6, col7, col8, col9, col10] if expmap_file is not None: f = pyfits.open(expmap_file) ehdu = f["EXPMAP"] wexp = pywcs.WCS(header=ehdu.header) cel = w.all_pix2world(ctr[0], ctr[1], 1) ectr = wexp.all_world2pix(cel[0], cel[1], 1) exp = ehdu.data[:, :] nx, ny = exp.shape reblock = ehdu.header["CDELT2"] / orig_dx x, y = np.mgrid[1:nx + 1, 1:ny + 1] r = np.sqrt((x - ectr[0])**2 + (y - ectr[1])**2) f.close() E = np.histogram(r, bins=rr / reblock, weights=exp)[0] / np.histogram( r, bins=rr / reblock)[0] with np.errstate(invalid='ignore', divide='ignore'): F = R / E Ferr = Rerr / E SF = F / A SFerr = Ferr / A col11 = pyfits.Column(name='MEAN_SRC_EXP', format='D', unit='cm**2', array=E) col12 = pyfits.Column(name='NET_FLUX', format='D', unit='count/s/cm**2', array=F) col13 = pyfits.Column(name='NET_FLUX_ERR', format='D', unit='count/s/cm**2', array=Ferr) col14 = pyfits.Column(name='SUR_FLUX', format='D', unit='count/s/cm**2/arcsec**2', array=SF) col15 = pyfits.Column(name='SUR_FLUX_ERR', format='D', unit='count/s/cm**2/arcsec**2', array=SFerr) coldefs += [col11, col12, col13, col14, col15] tbhdu = pyfits.BinTableHDU.from_columns(pyfits.ColDefs(coldefs)) tbhdu.name = "PROFILE" hdulist = pyfits.HDUList([pyfits.PrimaryHDU(), tbhdu]) hdulist.writeto(out_file, overwrite=overwrite)
def make_exposure_map(event_file, expmap_file, energy, weights=None, asol_file=None, normalize=True, overwrite=False, reblock=1, nhistx=16, nhisty=16, order=1): """ Make an exposure map for a SOXS event file, and optionally write an aspect solution file. The exposure map will be created by binning an aspect histogram over the range of the aspect solution. Parameters ---------- event_file : string The path to the event file to use for making the exposure map. expmap_file : string The path to write the exposure map file to. energy : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, or NumPy array The energy in keV to use when computing the exposure map, or a set of energies to be used with the *weights* parameter. If providing a set, it must be in keV. weights : array-like, optional The weights to use with a set of energies given in the *energy* parameter. Used to create a more accurate exposure map weighted by a range of energies. Default: None asol_file : string, optional The path to write the aspect solution file to, if desired. Default: None normalize : boolean, optional If True, the exposure map will be divided by the exposure time so that the map's units are cm**2. Default: True overwrite : boolean, optional Whether or not to overwrite an existing file. Default: False reblock : integer, optional Supply an integer power of 2 here to make an exposure map with a different binning. Default: 1 nhistx : integer, optional The number of bins in the aspect histogram in the DETX direction. Default: 16 nhisty : integer, optional The number of bins in the aspect histogram in the DETY direction. Default: 16 order : integer, optional The interpolation order to use when making the exposure map. Default: 1 """ import pyregion._region_filter as rfilter from scipy.ndimage.interpolation import rotate, shift from xcs_soxs.instrument import AuxiliaryResponseFile, perform_dither if isinstance(energy, np.ndarray) and weights is None: raise RuntimeError("Must supply a single value for the energy if " "you do not supply weights!") if not isinstance(energy, np.ndarray): energy = parse_value(energy, "keV") f_evt = pyfits.open(event_file) hdu = f_evt["EVENTS"] arf = AuxiliaryResponseFile(hdu.header["ANCRFILE"]) exp_time = hdu.header["EXPOSURE"] nx = int(hdu.header["TLMAX2"] - 0.5) // 2 ny = int(hdu.header["TLMAX3"] - 0.5) // 2 ra0 = hdu.header["TCRVL2"] dec0 = hdu.header["TCRVL3"] xdel = hdu.header["TCDLT2"] ydel = hdu.header["TCDLT3"] x0 = hdu.header["TCRPX2"] y0 = hdu.header["TCRPX3"] xdet0 = 0.5 * (2 * nx + 1) ydet0 = 0.5 * (2 * ny + 1) xaim = hdu.header.get("AIMPT_X", 0.0) yaim = hdu.header.get("AIMPT_Y", 0.0) roll = hdu.header["ROLL_PNT"] instr = instrument_registry[hdu.header["INSTRUME"].lower()] dither_params = {} if "DITHXAMP" in hdu.header: dither_params["x_amp"] = hdu.header["DITHXAMP"] dither_params["y_amp"] = hdu.header["DITHYAMP"] dither_params["x_period"] = hdu.header["DITHXPER"] dither_params["y_period"] = hdu.header["DITHYPER"] dither_params["plate_scale"] = ydel * 3600.0 dither_params["dither_on"] = True else: dither_params["dither_on"] = False f_evt.close() # Create time array for aspect solution dt = 1.0 # Seconds t = np.arange(0.0, exp_time + dt, dt) # Construct WCS w = pywcs.WCS(naxis=2) w.wcs.crval = [ra0, dec0] w.wcs.crpix = [x0, y0] w.wcs.cdelt = [xdel, ydel] w.wcs.ctype = ["RA---TAN", "DEC--TAN"] w.wcs.cunit = ["deg"] * 2 # Create aspect solution if we had dithering. # otherwise just set the offsets to zero if dither_params["dither_on"]: x_off, y_off = perform_dither(t, dither_params) # Make the aspect histogram x_amp = dither_params["x_amp"] / dither_params["plate_scale"] y_amp = dither_params["y_amp"] / dither_params["plate_scale"] x_edges = np.linspace(-x_amp, x_amp, nhistx + 1, endpoint=True) y_edges = np.linspace(-y_amp, y_amp, nhisty + 1, endpoint=True) asphist = np.histogram2d(x_off, y_off, (x_edges, y_edges))[0] asphist *= dt x_mid = 0.5 * (x_edges[1:] + x_edges[:-1]) / reblock y_mid = 0.5 * (y_edges[1:] + y_edges[:-1]) / reblock # Determine the effective area eff_area = arf.interpolate_area(energy).value if weights is not None: eff_area = np.average(eff_area, weights=weights) if instr["chips"] is None: rtypes = ["Box"] args = [[0.0, 0.0, instr["num_pixels"], instr["num_pixels"]]] else: rtypes = [] args = [] for i, chip in enumerate(instr["chips"]): rtypes.append(chip[0]) args.append(np.array(chip[1:])) tmpmap = np.zeros((2 * nx, 2 * ny)) for rtype, arg in zip(rtypes, args): rfunc = getattr(rfilter, rtype) new_args = parse_region_args(rtype, arg, xdet0 - xaim - 1.0, ydet0 - yaim - 1.0) r = rfunc(*new_args) tmpmap += r.mask(tmpmap).astype("float64") tmpmap = downsample(tmpmap, reblock) if dither_params["dither_on"]: expmap = np.zeros(tmpmap.shape) niter = nhistx * nhisty pbar = tqdm(leave=True, total=niter, desc="Creating exposure map ") for i in range(nhistx): for j in range(nhisty): expmap += shift(tmpmap, (x_mid[i], y_mid[j]), order=order) * asphist[i, j] pbar.update(nhisty) pbar.close() else: expmap = tmpmap * exp_time expmap *= eff_area if normalize: expmap /= exp_time if roll != 0.0: rotate(expmap, roll, output=expmap, reshape=False) expmap[expmap < 0.0] = 0.0 map_header = { "EXPOSURE": exp_time, "MTYPE1": "EQPOS", "MFORM1": "RA,DEC", "CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CRVAL1": ra0, "CRVAL2": dec0, "CUNIT1": "deg", "CUNIT2": "deg", "CDELT1": xdel * reblock, "CDELT2": ydel * reblock, "CRPIX1": 0.5 * (2.0 * nx // reblock + 1), "CRPIX2": 0.5 * (2.0 * ny // reblock + 1) } map_hdu = pyfits.ImageHDU(expmap, header=pyfits.Header(map_header)) map_hdu.name = "EXPMAP" map_hdu.writeto(expmap_file, overwrite=overwrite) if asol_file is not None: if dither_params["dither_on"]: det = np.array([x_off, y_off]) pix = np.dot(get_rot_mat(roll).T, det) ra, dec = w.wcs_pix2world(pix[0, :] + x0, pix[1, :] + y0, 1) col_t = pyfits.Column(name='time', format='D', unit='s', array=t) col_ra = pyfits.Column(name='ra', format='D', unit='deg', array=ra) col_dec = pyfits.Column(name='dec', format='D', unit='deg', array=dec) coldefs = pyfits.ColDefs([col_t, col_ra, col_dec]) tbhdu = pyfits.BinTableHDU.from_columns(coldefs) tbhdu.name = "ASPSOL" tbhdu.header["EXPOSURE"] = exp_time hdulist = [pyfits.PrimaryHDU(), tbhdu] pyfits.HDUList(hdulist).writeto(asol_file, overwrite=overwrite) else: mylog.warning("Refusing to write an aspect solution file because " "there was no dithering.")
def __init__(self, emin, emax, nbins, var_elem=None, apec_root=None, apec_vers=None, broadening=True, nolines=False, abund_table=None, nei=False): if apec_vers is None: filedir = os.path.join(os.path.dirname(__file__), 'files') cfile = glob.glob("%s/apec_*_coco.fits" % filedir)[0] apec_vers = cfile.split("/")[-1].split("_")[1][1:] mylog.info("Using APEC version %s." % apec_vers) if nei and apec_root is None: raise RuntimeError( "The NEI APEC tables are not supplied with " "SOXS! Download them from http://www.atomdb.org " "and set 'apec_root' to their location.") if nei and var_elem is None: raise RuntimeError( "For NEI spectra, you must specify which elements " "you want to vary using the 'var_elem' argument!") self.nei = nei emin = parse_value(emin, "keV") emax = parse_value(emax, 'keV') self.emin = emin self.emax = emax self.nbins = nbins self.ebins = np.linspace(self.emin, self.emax, nbins + 1) self.de = np.diff(self.ebins) self.emid = 0.5 * (self.ebins[1:] + self.ebins[:-1]) if apec_root is None: apec_root = soxs_files_path if nei: neistr = "_nei" ftype = "comp" else: neistr = "" ftype = "coco" self.cocofile = os.path.join( apec_root, "apec_v%s%s_%s.fits" % (apec_vers, neistr, ftype)) self.linefile = os.path.join( apec_root, "apec_v%s%s_line.fits" % (apec_vers, neistr)) if not os.path.exists(self.cocofile) or not os.path.exists( self.linefile): raise IOError("Cannot find the APEC files!\n %s\n, %s" % (self.cocofile, self.linefile)) mylog.info("Using %s for generating spectral lines." % os.path.split(self.linefile)[-1]) mylog.info("Using %s for generating the continuum." % os.path.split(self.cocofile)[-1]) self.nolines = nolines self.wvbins = hc / self.ebins[::-1] self.broadening = broadening try: self.line_handle = pyfits.open(self.linefile) except IOError: raise IOError("Line file %s does not exist" % self.linefile) try: self.coco_handle = pyfits.open(self.cocofile) except IOError: raise IOError("Continuum file %s does not exist" % self.cocofile) self.Tvals = self.line_handle[1].data.field("kT") self.nT = len(self.Tvals) self.dTvals = np.diff(self.Tvals) self.minlam = self.wvbins.min() self.maxlam = self.wvbins.max() self.var_elem_names = [] self.var_ion_names = [] if var_elem is None: self.var_elem = np.empty((0, 1), dtype='int') else: self.var_elem = [] if len(var_elem) != len(set(var_elem)): raise RuntimeError( "Duplicates were found in the \"var_elem\" list! %s" % var_elem) for elem in var_elem: if "^" in elem: if not self.nei: raise RuntimeError( "Cannot use different ionization states with a " "CIE plasma!") el = elem.split("^") e = el[0] ion = int(el[1]) else: if self.nei: raise RuntimeError( "Variable elements must include the ionization " "state for NEI plasmas!") e = elem ion = 0 self.var_elem.append([elem_names.index(e), ion]) self.var_elem.sort(key=lambda x: (x[0], x[1])) self.var_elem = np.array(self.var_elem, dtype='int') self.var_elem_names = [elem_names[e[0]] for e in self.var_elem] self.var_ion_names = [ "%s^%d" % (elem_names[e[0]], e[1]) for e in self.var_elem ] self.num_var_elem = len(self.var_elem) if self.nei: self.cosmic_elem = [ elem for elem in [1, 2] if elem not in self.var_elem[:, 0] ] self.metal_elem = [] else: self.cosmic_elem = [ elem for elem in cosmic_elem if elem not in self.var_elem[:, 0] ] self.metal_elem = [ elem for elem in metal_elem if elem not in self.var_elem[:, 0] ] if abund_table is None: abund_table = soxs_cfg.get("xcs_soxs", "abund_table") if not isinstance(abund_table, string_types): if len(abund_table) != 30: raise RuntimeError("User-supplied abundance tables " "must be 30 elements long!") self.atable = np.concatenate([[0.0], np.array(abund_table)]) else: self.atable = abund_tables[abund_table].copy() self._atable = self.atable.copy() self._atable[1:] /= abund_tables["angr"][1:]