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 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 generate_fluxes(fov, prng): from soxs.data import cdf_fluxes, cdf_gal, cdf_agn prng = parse_prng(prng) 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) fov_area = fov**2 n_gal = int(n_gal * fov_area / 3600.0) n_agn = int(n_agn * fov_area / 3600.0) mylog.debug(f"{n_agn} AGN, {n_gal} galaxies in the FOV.") 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 make_instrument_background(bkgnd_name, event_params, focal_length, rmf, prng=None): prng = parse_prng(prng) bkgnd_spec = instrument_backgrounds[bkgnd_name] # Generate background events energy = bkgnd_spec.generate_energies(event_params["exposure_time"], event_params["fov"], focal_length=focal_length, prng=prng, quiet=True).value if energy.size == 0: raise RuntimeError( "No instrumental background events were detected!!!") else: mylog.info("Making %d events from the instrumental background." % energy.size) return make_uniform_background(energy, event_params, rmf, prng=prng)
def absorb_photons(self, eobs, prng=None): r""" Determine which photons will be absorbed by foreground galactic absorption. Parameters ---------- eobs : array_like The energies of the photons in keV. prng : integer, :class:`~numpy.random.RandomState` object or :mod:`~numpy.random`, optional 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 the :mod:`numpy.random` module. """ prng = parse_prng(prng) n_events = eobs.size if n_events == 0: return np.array([], dtype='bool') detected = np.zeros(n_events, dtype='bool') nchunk = n_events // 100 if nchunk == 0: nchunk = n_events k = 0 pbar = get_pbar("Absorbing photons", n_events) while k < n_events: absorb = self.get_absorb(eobs[k:k+nchunk]) nabs = absorb.size randvec = prng.uniform(size=nabs) detected[k:k+nabs] = randvec < absorb k += nabs pbar.update(k) pbar.finish() return detected
def absorb_photons(self, eobs, prng=None): r""" Determine which photons will be absorbed by foreground galactic absorption. Parameters ---------- eobs : array_like The energies of the photons in keV. prng : integer, :class:`~numpy.random.RandomState` object or :mod:`~numpy.random`, optional 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 the :mod:`numpy.random` module. """ prng = parse_prng(prng) n_events = eobs.size if n_events == 0: return np.array([], dtype='bool') detected = np.zeros(n_events, dtype='bool') nchunk = n_events // 100 if nchunk == 0: nchunk = n_events k = 0 pbar = get_pbar("Absorbing photons", n_events) while k < n_events: absorb = self.get_absorb(eobs[k:k + nchunk]) nabs = absorb.size randvec = prng.uniform(size=nabs) detected[k:k + nabs] = randvec < absorb k += nabs pbar.update(k) pbar.finish() return detected
def convolve_spectrum(self, cspec, exp_time, noisy=True, prng=None): prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") counts = cspec.flux.value * exp_time * cspec.de.value spec = np.histogram(cspec.emid.value, self.ebins, weights=counts)[0] conv_spec = np.zeros(self.n_ch) pbar = tqdm(leave=True, total=self.n_e, desc="Convolving spectrum ") if np.all(self.data["N_GRP"] == 1): # We can do things a bit faster if there is only one group each f_chan = ensure_numpy_array(np.nan_to_num(self.data["F_CHAN"])) n_chan = ensure_numpy_array(np.nan_to_num(self.data["N_CHAN"])) mat = np.nan_to_num(np.float64(self.data["MATRIX"])) mat_size = np.minimum(n_chan, self.n_ch-f_chan) for k in range(self.n_e): conv_spec[f_chan[k]:f_chan[k]+n_chan[k]] += spec[k]*mat[k,:mat_size[k]] pbar.update() else: # Otherwise, we have to go step-by-step for k in range(self.n_e): f_chan = ensure_numpy_array(np.nan_to_num(self.data["F_CHAN"][k])) n_chan = ensure_numpy_array(np.nan_to_num(self.data["N_CHAN"][k])) mat = np.nan_to_num(np.float64(self.data["MATRIX"][k])) mat_size = np.minimum(n_chan, self.n_ch-f_chan) for i, f in enumerate(f_chan): conv_spec[f:f+n_chan[i]] += spec[k]*mat[:mat_size[i]] pbar.update() pbar.close() if noisy: return prng.poisson(lam=conv_spec) else: return conv_spec
def generate_energies(self, t_exp, prng=None, quiet=False): """ Generate photon energies from this convolved spectrum given an exposure time. Parameters ---------- t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. 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") prng = parse_prng(prng) rate = 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 generate_sources(fov, sky_center, prng=None): r""" Make a catalog of point sources. Parameters ---------- 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. 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) fov = parse_value(fov, "arcmin") agn_fluxes, gal_fluxes = generate_fluxes(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) ]) ra0, dec0 = generate_positions(fluxes.size, fov, sky_center, prng) return ra0, dec0, fluxes, ind
def ch_to_eb(self, channels, prng=None): prng = parse_prng(prng) emin = self.ebounds_data["E_MIN"] emax = self.ebounds_data["E_MAX"] de = emax-emin ch = channels - self.cmin e = emin[ch] + prng.uniform(size=channels.size)*de[ch] return e
def make_foreground(event_params, arf, rmf, prng=None): import pyregion._region_filter as rfilter prng = parse_prng(prng) conv_frgnd_spec = ConvolvedBackgroundSpectrum(hm_astro_bkgnd, arf) energy = conv_frgnd_spec.generate_energies(event_params["exposure_time"], event_params["fov"], prng=prng, quiet=True).value prng = parse_prng(prng) bkg_events = {} n_events = energy.size nx = event_params["num_pixels"] bkg_events["detx"] = prng.uniform(low=-0.5*nx, high=0.5*nx, size=n_events) bkg_events["dety"] = prng.uniform(low=-0.5*nx, high=0.5*nx, size=n_events) bkg_events["energy"] = energy if event_params["chips"] is None: bkg_events["chip_id"] = np.zeros(n_events, dtype='int') else: bkg_events["chip_id"] = -np.ones(n_events, dtype='int') for i, chip in enumerate(event_params["chips"]): thisc = np.ones(n_events, dtype='bool') rtype = chip[0] args = chip[1:] r = getattr(rfilter, rtype)(*args) inside = r.inside(bkg_events["detx"], bkg_events["dety"]) thisc = np.logical_and(thisc, inside) bkg_events["chip_id"][thisc] = i keep = bkg_events["chip_id"] > -1 if keep.sum() == 0: raise RuntimeError("No astrophysical foreground events were detected!!!") else: mylog.info("Making %d events from the astrophysical foreground." % keep.sum()) for key in bkg_events: bkg_events[key] = bkg_events[key][keep] return make_diffuse_background(bkg_events, event_params, rmf, prng=prng)
def make_point_sources(area, exp_time, positions, sky_center, spectra, prng=None): r""" Create a new :class:`~pyxsim.event_list.EventList` which contains point sources. Parameters ---------- area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The collecting area to determine the number of events. If units are not specified, it is assumed to be in cm^2. exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The exposure time to determine the number of events. If units are not specified, it is assumed to be in seconds. positions : array of source positions, shape 2xN The positions of the point sources in RA, Dec, where N is the number of point sources. Coordinates should be in degrees. sky_center : array-like Center RA, Dec of the events in degrees. spectra : list (size N) of :class:`~soxs.spectra.Spectrum` objects The spectra for the point sources, where N is the number of point sources. Assumed to be in the observer frame. prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ prng = parse_prng(prng) spectra = ensure_list(spectra) positions = ensure_list(positions) area = parse_value(area, "cm**2") exp_time = parse_value(exp_time, "s") t_exp = exp_time.value/comm.size x = [] y = [] e = [] for pos, spectrum in zip(positions, spectra): eobs = spectrum.generate_energies(t_exp, area.value, prng=prng) ne = eobs.size x.append(YTArray([pos[0]] * ne, "deg")) y.append(YTArray([pos[1]] * ne, "deg")) e.append(YTArray.from_astropy(eobs)) parameters = {"sky_center": YTArray(sky_center, "degree"), "exp_time": exp_time, "area": area} events = {} events["xsky"] = uconcatenate(x) events["ysky"] = uconcatenate(y) events["eobs"] = uconcatenate(e) return EventList(events, parameters)
def make_point_sources(area, exp_time, positions, sky_center, spectra, prng=None): r""" Create a new :class:`~pyxsim.event_list.EventList` which contains point sources. Parameters ---------- area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The collecting area to determine the number of events. If units are not specified, it is assumed to be in cm^2. exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The exposure time to determine the number of events. If units are not specified, it is assumed to be in seconds. positions : array of source positions, shape 2xN The positions of the point sources in RA, Dec, where N is the number of point sources. Coordinates should be in degrees. sky_center : array-like Center RA, Dec of the events in degrees. spectra : list (size N) of :class:`~soxs.spectra.Spectrum` objects The spectra for the point sources, where N is the number of point sources. Assumed to be in the observer frame. prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ prng = parse_prng(prng) spectra = ensure_list(spectra) positions = ensure_list(positions) area = parse_value(area, "cm**2") exp_time = parse_value(exp_time, "s") t_exp = exp_time.value/comm.size x = [] y = [] e = [] for pos, spectrum in zip(positions, spectra): eobs = spectrum.generate_energies(t_exp, area.value, prng=prng) ne = eobs.size x.append(YTArray([pos[0]] * ne, "degree")) y.append(YTArray([pos[1]] * ne, "degree")) e.append(YTArray.from_astropy(eobs)) parameters = {"sky_center": YTArray(sky_center, "degree"), "exp_time": exp_time, "area": area} events = {} events["eobs"] = uconcatenate(e) events["xsky"] = uconcatenate(x) events["ysky"] = uconcatenate(y) return EventList(events, parameters)
def __init__(self, e0, emin, emax, emission_field, alpha, prng=None): self.e0 = parse_value(e0, "keV") self.emin = parse_value(emin, "keV") self.emax = parse_value(emax, "keV") self.emission_field = emission_field self.alpha = alpha self.prng = parse_prng(prng) self.spectral_norm = None self.redshift = None
def scatter_energies(self, events, prng=None): """ Scatter photon energies with the RMF and produce the corresponding channel values. Parameters ---------- events : dict of np.ndarrays The energies and positions of the photons. 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) eidxs = np.argsort(events["energy"]) sorted_e = events["energy"][eidxs] detectedChannels = [] # run through all photon energies and find which bin they go in fcurr = 0 last = sorted_e.shape[0] emin = sorted_e[0] emax = sorted_e[-1] pbar = tqdm(leave=True, total=last, desc="Scattering energies ") for (k, low), high in zip(enumerate(self.elo), self.ehi): if high < emin or low > emax: continue e = sorted_e[fcurr:last] nn = np.logical_and(low <= e, e < high).sum() if nn == 0: continue # weight function for probabilities from RMF weights = np.nan_to_num(np.float64(self.data["MATRIX"][k])) weights /= weights.sum() trueChannel = self._make_channels(k) if len(trueChannel) > 0: channelInd = prng.choice(len(weights), size=nn, p=weights) detectedChannels.append(trueChannel[channelInd]) fcurr += nn pbar.update(nn) pbar.close() for key in events: events[key] = events[key][eidxs] events[self.header["CHANTYPE"]] = np.concatenate(detectedChannels) return events
def make_instrument_background(inst_spec, event_params, rmf, prng=None): from collections import defaultdict prng = parse_prng(prng) bkgnd_spec = inst_spec["bkgnd"] if isinstance(bkgnd_spec[0], str): nchips = len(event_params["chips"]) bkgnd_spec = [bkgnd_spec] * nchips bkg_events = defaultdict(list) pixel_area = (event_params["plate_scale"] * 60.0)**2 for i, chip in enumerate(event_params["chips"]): rtype = chip[0] args = chip[1:] r, bounds = create_region(rtype, args, 0.0, 0.0) sa = (bounds[1] - bounds[0]) * (bounds[3] - bounds[2]) * pixel_area bspec = InstrumentalBackground.from_filename(bkgnd_spec[i][0], bkgnd_spec[i][1], inst_spec['focal_length']) chan = bspec.generate_channels(event_params["exposure_time"], sa, prng=prng) n_events = chan.size detx = prng.uniform(low=bounds[0], high=bounds[1], size=n_events) dety = prng.uniform(low=bounds[2], high=bounds[3], size=n_events) if rtype in ["Box", "Rectangle"]: thisc = slice(None, None, None) n_det = n_events else: thisc = r.contains(PixCoord(detx, dety)) n_det = thisc.sum() ch = chan[thisc].astype('int') e = rmf.ch_to_eb(ch, prng=prng) bkg_events["energy"].append(e) bkg_events[rmf.chan_type].append(ch) bkg_events["detx"].append(detx[thisc]) bkg_events["dety"].append(dety[thisc]) bkg_events["chip_id"].append(i * np.ones(n_det)) for key in bkg_events: bkg_events[key] = np.concatenate(bkg_events[key]) if bkg_events["energy"].size == 0: raise RuntimeError( "No instrumental background events were detected!!!") else: mylog.info(f"Making {bkg_events['energy'].size} events " f"from the instrumental background.") return make_diffuse_background(bkg_events, event_params, rmf, prng=prng)
def detect_events(self, events, exp_time, flux, refband, prng=None): """ Use the ARF to determine a subset of photons which will be detected. Returns a boolean NumPy array which is the same is the same size as the number of photons, wherever it is "true" means those photons have been detected. Parameters ---------- events : dict of np.ndarrays The energies and positions of the photons. exp_time : float The exposure time in seconds. flux : float The total flux of the photons in erg/s/cm^2. refband : array_like A two-element array or list containing the limits of the energy band which the flux was computed in. 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) energy = events["energy"] if energy.size == 0: return events earea = self.interpolate_area(energy).value idxs = np.logical_and(energy >= refband[0], energy <= refband[1]) rate = flux / (energy[idxs].sum() * erg_per_keV) * earea[idxs].sum() n_ph = prng.poisson(lam=rate * exp_time) fak = float(n_ph) / energy.size if fak > 1.0: mylog.error( "Number of events in sample: %d, Number of events wanted: %d" % (energy.size, n_ph)) raise ValueError( "This combination of exposure time and effective area " "will result in more photons being drawn than are available " "in the sample!!!") w = earea / self.max_area randvec = prng.uniform(size=energy.size) eidxs = prng.permutation( np.where(randvec < w)[0])[:n_ph].astype("int64") mylog.info("%s events detected." % n_ph) for key in events: events[key] = events[key][eidxs] return events
def make_background(area, exp_time, fov, sky_center, spectrum, prng=None): r""" Create a new :class:`~pyxsim.event_list.EventList` which is filled uniformly with background events. Parameters ---------- area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The collecting area to determine the number of events. If units are not specified, it is assumed to be in cm^2. exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The exposure time to determine the number of events. If units are not specified, it is assumed to be in seconds. fov : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The field of view of the event file. If units are not provided, they are assumed to be in arcminutes. sky_center : array-like Center RA, Dec of the events in degrees. spectrum : :class:`~soxs.spectra.Spectrum` The spectrum for the background. prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ prng = parse_prng(prng) fov = parse_value(fov, "arcmin") exp_time = parse_value(exp_time, "s") area = parse_value(area, "cm**2") t_exp = exp_time.value / comm.size e = spectrum.generate_energies(t_exp, area.value, prng=prng) fov_model = FillFOVModel(sky_center[0], sky_center[1], fov.value) ra, dec = fov_model.generate_coords(e.size, prng=prng) parameters = { "sky_center": YTArray(sky_center, "degree"), "exp_time": exp_time, "area": area } events = {} events["xsky"] = YTArray(ra.value, "degree") events["ysky"] = YTArray(dec.value, "degree") events["eobs"] = YTArray(e.value, "keV") return EventList(events, parameters)
def make_xrb_photons(ds, redshift, area, exp_time, emin, emax, center="c", cosmology=None, prng=None): r""" Take a dataset produced by :func:`~pyxsim.source_generators.xray_binaries.make_xrb_particles` and produce a :class:`~pyxsim.photon_list.PhotonList`. Parameters ---------- ds : :class:`~yt.data_objects.static_output.Dataset` The dataset of XRB particles to use to make the photons. redshift : float The cosmological redshift for the photons. area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The collecting area to determine the number of photons. If units are not specified, it is assumed to be in cm^2. exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The exposure time to determine the number of photons. If units are not specified, it is assumed to be in seconds. emin : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The minimum energy of the photons to be generated, in the rest frame of the source. If units are not given, they are assumed to be in keV. emax : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The maximum energy of the photons to be generated, in the rest frame of the source. If units are not given, they are assumed to be in keV. center : string or array_like, optional The origin of the photon spatial coordinates. Accepts "c", "max", or a coordinate. If not specified, pyxsim attempts to use the "center" field parameter of the data_source. cosmology : :class:`~yt.utilities.cosmology.Cosmology`, optional Cosmological information. If not supplied, we try to get the cosmology from the dataset. Otherwise, LCDM with the default yt parameters is assumed. prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ dd = ds.all_data() e0 = (1.0, "keV") prng = parse_prng(prng) xrb_model = PowerLawSourceModel(e0, emin, emax, "particle_count_rate", "particle_spectral_index", prng=prng) photons = PhotonList.from_data_source(dd, redshift, area, exp_time, xrb_model, center=center, point_sources=True, cosmology=cosmology) return photons
def __init__(self, spectral_model, emin, emax, nchan, temperature_field=None, emission_measure_field=None, kT_min=0.008, kT_max=64.0, n_kT=10000, kT_scale="linear", Zmet=0.3, var_elem=None, method="invert_cdf", thermal_broad=True, model_root=None, model_vers=None, nei=False, nolines=False, abund_table="angr", prng=None): if isinstance(spectral_model, string_types): if spectral_model not in thermal_models: raise KeyError("%s is not a known thermal spectral model!" % spectral_model) spectral_model = thermal_models[spectral_model] self.temperature_field = temperature_field self.Zmet = Zmet self.nei = nei if var_elem is None: var_elem = {} var_elem_keys = None self.num_var_elem = 0 else: var_elem_keys = list(var_elem.keys()) self.num_var_elem = len(var_elem_keys) self.var_elem = var_elem self.spectral_model = spectral_model(emin, emax, nchan, var_elem=var_elem_keys, thermal_broad=thermal_broad, model_root=model_root, model_vers=model_vers, nolines=nolines, nei=nei, abund_table=abund_table) self.var_elem_keys = self.spectral_model.var_elem_names self.var_ion_keys = self.spectral_model.var_ion_names self.method = method self.prng = parse_prng(prng) self.kT_min = kT_min self.kT_max = kT_max self.kT_scale = kT_scale self.n_kT = n_kT self.spectral_norm = None self.redshift = None self.pbar = None self.kT_bins = None self.dkT = None self.emission_measure_field = emission_measure_field self.Zconvert = 1.0 self.abund_table = abund_table self.atable = self.spectral_model.atable self.mconvert = {}
def detect_events(self, events, exp_time, flux, refband, prng=None): """ Use the ARF to determine a subset of photons which will be detected. Returns a boolean NumPy array which is the same is the same size as the number of photons, wherever it is "true" means those photons have been detected. Parameters ---------- events : dict of np.ndarrays The energies and positions of the photons. exp_time : float The exposure time in seconds. flux : float The total flux of the photons in erg/s/cm^2. refband : array_like A two-element array or list containing the limits of the energy band which the flux was computed in. 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) energy = events["energy"] if energy.size == 0: return events earea = self.interpolate_area(energy).value idxs = np.logical_and(energy >= refband[0], energy <= refband[1]) rate = flux/(energy[idxs].sum()*erg_per_keV)*earea[idxs].sum() n_ph = prng.poisson(lam=rate*exp_time) fak = float(n_ph)/energy.size if fak > 1.0: mylog.error("Number of events in sample: %d, Number of events wanted: %d" % (energy.size, n_ph)) raise ValueError("This combination of exposure time and effective area " "will result in more photons being drawn than are available " "in the sample!!!") w = earea / self.max_area randvec = prng.uniform(size=energy.size) eidxs = prng.permutation(np.where(randvec < w)[0])[:n_ph].astype("int64") mylog.info("%s events detected." % n_ph) for key in events: events[key] = events[key][eidxs] return events
def convolve_spectrum(self, cspec, exp_time, prng=None): prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") counts = cspec.flux.value * exp_time * cspec.de.value spec = np.histogram(cspec.emid.value, self.ebins, weights=counts)[0] conv_spec = np.zeros(self.n_ch) pbar = tqdm(leave=True, total=self.n_e, desc="Convolving spectrum ") for k in range(self.n_e): f_chan = ensure_numpy_array(np.nan_to_num(self.data["F_CHAN"][k])) n_chan = ensure_numpy_array(np.nan_to_num(self.data["N_CHAN"][k])) mat = np.nan_to_num(np.float64(self.data["MATRIX"][k])) for f, n in zip(f_chan, n_chan): mat_size = min(n, self.n_ch-f) conv_spec[f:f+n] += spec[k]*mat[:mat_size] pbar.update() pbar.close() return prng.poisson(lam=conv_spec)
def detect_events_spec(self, src, exp_time, refband, prng=None): from soxs.spectra import ConvolvedSpectrum prng = parse_prng(prng) cspec = ConvolvedSpectrum.convolve( src.spec, self).new_spec_from_band(refband[0], refband[1]) energy = cspec.generate_energies(exp_time, quiet=True, prng=prng).value if getattr(src, "imhdu", None): x, y = image_pos(src.imhdu.data, energy.size, prng) w = pywcs.WCS(header=src.imhdu.header) w.wcs.crval = [src.ra, src.dec] ra, dec = w.wcs_pix2world(x, y, 1) else: pones = np.ones_like(energy) ra = src.ra*pones dec = src.dec*pones mylog.info(f"{energy.size} events detected.") return {"energy": energy, "ra": ra, "dec": dec}
def scatter_energies(self, events, prng=None): """ Scatter photon energies with the RMF and produce the corresponding channel values. Parameters ---------- events : dict of np.ndarrays The energies and positions of the photons. 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) eidxs = np.argsort(events["energy"]) sorted_e = events["energy"][eidxs] detectedChannels = [] # run through all photon energies and find which bin they go in fcurr = 0 last = sorted_e.shape[0] pbar = tqdm(leave=True, total=last, desc="Scattering energies ") for (k, low), high in zip(enumerate(self.elo), self.ehi): # weight function for probabilities from RMF weights = np.nan_to_num(np.float64(self.data["MATRIX"][k])) weights /= weights.sum() trueChannel = self._make_channels(k) if len(trueChannel) > 0: e = sorted_e[fcurr:last] nn = np.logical_and(low <= e, e < high).sum() channelInd = prng.choice(len(weights), size=nn, p=weights) detectedChannels.append(trueChannel[channelInd]) fcurr += nn pbar.update(nn) pbar.close() for key in events: events[key] = events[key][eidxs] events[self.header["CHANTYPE"]] = np.concatenate(detectedChannels) return events
def generate_energies(self, t_exp, fov, focal_length=None, prng=None, quiet=False): """ Generate photon energies from this instrumental background spectrum given an exposure time, effective area, 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. focal_length : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The focal length in meters. Default is to use the default focal length of the instrument configuration. 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) if focal_length is None: focal_length = self.default_focal_length else: focal_length = parse_value(focal_length, "m") rate = fov * fov * self.total_flux.value rate *= (focal_length / self.default_focal_length)**2 energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet) flux = np.sum(energy) * erg_per_keV / t_exp energies = Energies(energy, flux) return energies
def make_foreground(event_params, arf, rmf, prng=None): prng = parse_prng(prng) conv_frgnd_spec = ConvolvedBackgroundSpectrum.convolve(hm_astro_bkgnd, arf) bkg_events = {"energy": [], "detx": [], "dety": [], "chip_id": []} pixel_area = (event_params["plate_scale"]*60.0)**2 for i, chip in enumerate(event_params["chips"]): rtype = chip[0] args = chip[1:] r, bounds = create_region(rtype, args, 0.0, 0.0) fov = np.sqrt((bounds[1]-bounds[0])*(bounds[3]-bounds[2])*pixel_area) e = conv_frgnd_spec.generate_energies(event_params["exposure_time"], fov, prng=prng, quiet=True).value n_events = e.size detx = prng.uniform(low=bounds[0], high=bounds[1], size=n_events) dety = prng.uniform(low=bounds[2], high=bounds[3], size=n_events) if rtype in ["Box", "Rectangle"]: thisc = slice(None, None, None) n_det = n_events else: thisc = r.contains(PixCoord(detx, dety)) n_det = thisc.sum() bkg_events["energy"].append(e[thisc]) bkg_events["detx"].append(detx[thisc]) bkg_events["dety"].append(dety[thisc]) bkg_events["chip_id"].append(i*np.ones(n_det)) for key in bkg_events: bkg_events[key] = np.concatenate(bkg_events[key]) if bkg_events["energy"].size == 0: raise RuntimeError("No astrophysical foreground events " "were detected!!!") else: mylog.info(f"Making {bkg_events['energy'].size} events from the " f"astrophysical foreground.") bkg_events = make_diffuse_background(bkg_events, event_params, rmf, prng=prng) mylog.info(f"Scattering energies with " f"RMF {os.path.split(rmf.filename)[-1]}.") return rmf.scatter_energies(bkg_events, prng=prng)
def __call__(self, events, prng=None): """ Calling method for :class:`~pyxsim.instruments.InstrumentSimulator`. Parameters ---------- events : :class:`~pyxsim.events.EventList` An EventList instance of unconvolved events. prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ issue_deprecation_warning("The pyXSIM built-in instrument simulators " "have been deprecated and will be removed " "in a future release!") if "pi" in events or "pha" in events: raise RuntimeError( "These events have already been convolved with a response!!") prng = parse_prng(prng) flux = np.sum(events["eobs"]).to("erg") / \ events.parameters["exp_time"]/events.parameters["area"] exp_time = events.parameters["exp_time"] emin = events["eobs"].min().value emax = events["eobs"].max().value new_events = {} new_events.update(events.events) new_events["energy"] = new_events.pop("eobs") new_events = self.arf.detect_events(new_events, exp_time, flux, [emin, emax], prng=prng) new_events = self.rmf.scatter_energies(new_events, prng=prng) new_events["eobs"] = new_events.pop("energy") chantype = self.rmf.header["CHANTYPE"].lower() new_events[chantype] = new_events.pop(self.rmf.header["CHANTYPE"]) parameters = {} parameters.update(events.parameters) parameters["channel_type"] = chantype parameters["mission"] = self.rmf.header.get("MISSION", "") parameters["instrument"] = self.rmf.header["INSTRUME"] parameters["telescope"] = self.rmf.header["TELESCOP"] parameters["arf"] = self.arf.filename parameters["rmf"] = self.rmf.filename return ConvolvedEventList(new_events, parameters)
def make_foreground(event_params, arf, rmf, prng=None): prng = parse_prng(prng) conv_bkgnd_spec = ConvolvedBackgroundSpectrum(hm_astro_bkgnd, arf) energy = conv_bkgnd_spec.generate_energies(event_params["exposure_time"], event_params["fov"], prng=prng, quiet=True).value if energy.size == 0: raise RuntimeError( "No astrophysical foreground events were detected!!!") else: mylog.info("Making %d events from the astrophysical foreground." % energy.size) return make_uniform_background(energy, event_params, rmf, prng=prng)
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 generate_coords(self, num_events, prng=None): """ Generate a sample of photon positions from this spatial model. Parameters ---------- num_events : integer The number of events to generate. 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) x, y = self._generate_coords(num_events, prng) ra, dec = self.w.wcs_pix2world(x, y, 1) return u.Quantity(ra, "deg"), u.Quantity(dec, "deg")
def __call__(self, events, prng=None): """ Calling method for :class:`~pyxsim.instruments.InstrumentSimulator`. Parameters ---------- events : :class:`~pyxsim.events.EventList` An EventList instance of unconvolved events. prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ issue_deprecation_warning("The pyXSIM built-in instrument simulators " "have been deprecated and will be removed " "in a future release!") if "pi" in events or "pha" in events: raise RuntimeError("These events have already been convolved with a response!!") prng = parse_prng(prng) flux = np.sum(events["eobs"]).to("erg") / \ events.parameters["exp_time"]/events.parameters["area"] exp_time = events.parameters["exp_time"] emin = events["eobs"].min().value emax = events["eobs"].max().value new_events = {} new_events.update(events.events) new_events["energy"] = new_events.pop("eobs") new_events = self.arf.detect_events(new_events, exp_time, flux, [emin, emax], prng=prng) new_events = self.rmf.scatter_energies(new_events, prng=prng) new_events["eobs"] = new_events.pop("energy") chantype = self.rmf.header["CHANTYPE"].lower() new_events[chantype] = new_events.pop(self.rmf.header["CHANTYPE"]) parameters = {} parameters.update(events.parameters) parameters["channel_type"] = chantype parameters["mission"] = self.rmf.header.get("MISSION", "") parameters["instrument"] = self.rmf.header["INSTRUME"] parameters["telescope"] = self.rmf.header["TELESCOP"] parameters["arf"] = self.arf.filename parameters["rmf"] = self.rmf.filename return ConvolvedEventList(new_events, parameters)
def __init__(self, ra0, dec0, width, height, num_events, theta=0.0, prng=None): prng = parse_prng(prng) ra0 = parse_value(ra0, "deg") dec0 = parse_value(dec0, "deg") width = parse_value(width, "arcsec") height = parse_value(height, "arcsec") w = construct_wcs(ra0, dec0) x = prng.uniform(low=-0.5 * width, high=0.5 * width, size=num_events) y = prng.uniform(low=-0.5 * height, high=0.5 * height, size=num_events) coords = rotate_xy(theta, x, y) ra, dec = w.wcs_pix2world(coords[0, :], coords[1, :], 1) super(RectangleModel, self).__init__(ra, dec, coords[0, :], coords[1, :], w)
def generate_energies(self, t_exp, fov, focal_length=None, prng=None, quiet=False): """ Generate photon energies from this instrumental background spectrum given an exposure time, effective area, 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. focal_length : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The focal length in meters. Default is to use the default focal length of the instrument configuration. 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) if focal_length is None: focal_length = self.default_focal_length else: focal_length = parse_value(focal_length, "m") rate = fov*fov*self.total_flux.value rate *= (focal_length/self.default_focal_length)**2 energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet) flux = np.sum(energy)*erg_per_keV/t_exp energies = Energies(energy, flux) return energies
def __init__(self, e0, emission_field, sigma=None, prng=None): self.e0 = parse_value(e0, "keV") if isinstance(sigma, (float, YTQuantity)) or (isinstance(sigma, tuple) and isinstance(sigma[0], float)): # The broadening is constant try: self.sigma = parse_value(sigma, "keV") except YTUnitConversionError: try: self.sigma = parse_value(sigma, "km/s") self.sigma *= self.e0/clight self.sigma.convert_to_units("keV") except YTUnitConversionError: raise RuntimeError("Units for sigma must either be in dimensions of " "energy or velocity! sigma = %s" % sigma) else: # Either no broadening or a field name self.sigma = sigma self.emission_field = emission_field self.prng = parse_prng(prng) self.spectral_norm = None self.redshift = None
def __init__(self, ra0, dec0, func, num_events, theta=0.0, ellipticity=1.0, prng=None): prng = parse_prng(prng) ra0 = parse_value(ra0, "deg") dec0 = parse_value(dec0, "deg") theta = parse_value(theta, "deg") x, y = generate_radial_events(num_events, func, prng, ellipticity=ellipticity) w = construct_wcs(ra0, dec0) coords = rotate_xy(theta, x, y) ra, dec = w.wcs_pix2world(coords[0, :], coords[1, :], 1) super(RadialFunctionModel, self).__init__(ra, dec, coords[0, :], coords[1, :], w)
def __init__(self, e0, emission_field, sigma=None, prng=None): self.e0 = parse_value(e0, "keV") if isinstance(sigma, (float, YTQuantity)) or (isinstance( sigma, tuple) and isinstance(sigma[0], float)): # The broadening is constant try: self.sigma = parse_value(sigma, "keV") except YTUnitConversionError: try: self.sigma = parse_value(sigma, "km/s") self.sigma *= self.e0 / clight self.sigma.convert_to_units("keV") except YTUnitConversionError: raise RuntimeError( "Units for sigma must either be in dimensions of " "energy or velocity! sigma = %s" % sigma) else: # Either no broadening or a field name self.sigma = sigma self.emission_field = emission_field self.prng = parse_prng(prng) self.spectral_norm = None self.redshift = None
def generate_channel_spectrum(self, t_exp, solid_angle, focal_length=None, prng=None): """ Generate photon energy channels from this instrumental background spectrum given an exposure time, effective area, and solid angle. Parameters ---------- t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. solid_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The solid angle in arcmin**2. focal_length : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The focal length in meters. Default is to use the default focal length of the instrument configuration. 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. """ t_exp = parse_value(t_exp, "s") solid_angle = parse_value(solid_angle, "arcmin**2") prng = parse_prng(prng) if focal_length is None: focal_length = self.default_focal_length else: focal_length = parse_value(focal_length, "m") fac = t_exp * solid_angle # Backgrounds are normalized to 1 arcmin**2 fac *= (focal_length / self.default_focal_length)**2 return prng.poisson(lam=self.count_rate * fac)
def make_cosmological_sources(exp_time, fov, sky_center, cat_center=None, absorb_model="wabs", nH=0.05, area=40000.0, output_sources=None, prng=None): r""" Make an X-ray source made up of contributions from galaxy clusters, galaxy groups, and galaxies. 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. cat_center : array-like The center of the field in the coordinates of the halo catalog, which range from -5.0 to 5.0 in degrees in both directions. If None is given, a center will be randomly chosen. 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. 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. """ 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") prng = parse_prng(prng) cosmo = FlatLambdaCDM(H0=100.0*h0, Om0=omega_m) agen = ApecGenerator(0.1, 10.0, 10000, broadening=False) mylog.info("Creating photons from cosmological sources.") mylog.info("Loading halo data from catalog: %s" % halos_cat_file) halo_data = h5py.File(halos_cat_file, "r") scale = cosmo.kpc_proper_per_arcmin(halo_data["redshift"]).to("Mpc/arcmin") # 600. arcmin = 10 degrees (total FOV of catalog = 100 deg^2) fov_cat = 10.0*60.0 w = construct_wcs(*sky_center) cat_min = -0.5*fov_cat cat_max = 0.5*fov_cat if cat_center is None: xc, yc = prng.uniform(low=cat_min+0.5*fov, high=cat_max-0.5*fov, size=2) else: xc, yc = cat_center xc *= 60.0 yc *= 60.0 xc, yc = np.clip([xc, yc], cat_min+0.5*fov, cat_max-0.5*fov) mylog.info("Coordinates of the FOV within the catalog are (%g, %g) deg." % (xc/60.0, yc/60.0)) xlo = (xc-1.1*0.5*fov)*scale.value*h0 xhi = (xc+1.1*0.5*fov)*scale.value*h0 ylo = (yc-1.1*0.5*fov)*scale.value*h0 yhi = (yc+1.1*0.5*fov)*scale.value*h0 mylog.info("Selecting halos in the FOV.") fov_idxs = (halo_data["x"] >= xlo) & (halo_data["x"] <= xhi) fov_idxs = (halo_data["y"] >= ylo) & (halo_data["y"] <= yhi) & fov_idxs n_halos = fov_idxs.sum() mylog.info("Number of halos in the field of view: %d" % n_halos) # Now select the specific halos which are in the FOV z = halo_data["redshift"][fov_idxs].astype("float64") m = halo_data["M500c"][fov_idxs].astype("float64")/h0 s = scale[fov_idxs].to("Mpc/arcsec").value ra0, dec0 = w.wcs_pix2world(halo_data["x"][fov_idxs]/(h0*s)-xc*60.0, halo_data["y"][fov_idxs]/(h0*s)-yc*60.0, 1) # Close the halo catalog file halo_data.close() # Some cosmological stuff rho_crit = cosmo.critical_density(z).to("Msun/Mpc**3").value # halo temperature and k-corrected flux kT = Tx(m, z) flux_kcorr = 1.0e-14*lum(m, z)/flux2lum(kT, z) # halo scale radius r500 = (3.0*m/(4.0*np.pi*500*rho_crit))**(1.0/3.0) r500_kpc = r500 * 1000.0 rc_kpc = r500/conc * 1000.0 rc = r500/conc/s # Halo slope parameter beta = prng.normal(loc=0.666, scale=0.05, size=n_halos) beta[beta < 0.5] = 0.5 # Halo ellipticity ellip = prng.normal(loc=0.85, scale=0.15, size=n_halos) ellip[ellip < 0.0] = 1.0e-3 # Halo orientation theta = 360.0*prng.uniform(size=n_halos) # If requested, output the source properties to a file if output_sources is not None: t = Table([ra0, dec0, rc_kpc, beta, ellip, theta, m, r500_kpc, kT, z, flux_kcorr], names=('RA', 'Dec', 'r_c', 'beta', 'ellipticity', 'theta', 'M500c', 'r500', 'kT', 'redshift', 'flux_0.5_2.0_keV')) t["RA"].unit = "deg" t["Dec"].unit = "deg" t["flux_0.5_2.0_keV"].unit = "erg/(cm**2*s)" t["r_c"].unit = "kpc" t["theta"].unit = "deg" t["M500c"].unit = "solMass" t["r500"].unit = "kpc" t["kT"].unit = "kT" t.write(output_sources, format='ascii.ecsv', overwrite=True) tot_flux = 0.0 ee = [] ra = [] dec = [] pbar = tqdm(leave=True, total=n_halos, desc="Generating photons from halos ") for halo in range(n_halos): spec = agen.get_spectrum(kT[halo], abund, z[halo], 1.0) spec.rescale_flux(flux_kcorr[halo], emin=emin, emax=emax, flux_type="energy") if nH is not None: spec.apply_foreground_absorption(nH, model=absorb_model) e = spec.generate_energies(exp_time, area, prng=prng, quiet=True) beta_model = BetaModel(ra0[halo], dec0[halo], rc[halo], beta[halo], ellipticity=ellip[halo], theta=theta[halo]) xsky, ysky = beta_model.generate_coords(e.size, prng=prng) tot_flux += e.flux ee.append(e.value) ra.append(xsky.value) dec.append(ysky.value) pbar.update() pbar.close() ra = np.concatenate(ra) dec = np.concatenate(dec) ee = np.concatenate(ee) mylog.info("Created %d photons from cosmological sources." % ee.size) output_events = {"ra": ra, "dec": dec, "energy": ee, "flux": tot_flux.value} return output_events
def generate_events(input_events, exp_time, instrument, sky_center, no_dither=False, dither_params=None, roll_angle=0.0, subpixel_res=False, prng=None): """ Take unconvolved events and convolve them with instrumental responses. This function does the following: 1. Determines which events are observed using the ARF 2. Pixelizes the events, applying PSF effects and dithering 3. Determines energy channels using the RMF This function is not meant to be called by the end-user but is used by the :func:`~soxs.instrument.instrument_simulator` function. Parameters ---------- input_events : string, dict, or None The unconvolved events to be used as input. Can be one of the following: 1. The name of a SIMPUT catalog file. 2. A Python dictionary containing the following items: "ra": A NumPy array of right ascension values in degrees. "dec": A NumPy array of declination values in degrees. "energy": A NumPy array of energy values in keV. "flux": The flux of the entire source, in units of erg/cm**2/s. out_file : string The name of the event file to be written. exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time to use, in seconds. instrument : string The name of the instrument to use, which picks an instrument specification from the instrument registry. sky_center : array, tuple, or list The center RA, Dec coordinates of the observation, in degrees. no_dither : boolean, optional If True, turn off dithering entirely. Default: False dither_params : array-like of floats, optional The parameters to use to control the size and period of the dither pattern. The first two numbers are the dither amplitude in x and y detector coordinates in arcseconds, and the second two numbers are the dither period in x and y detector coordinates in seconds. Default: [8.0, 8.0, 1000.0, 707.0]. roll_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The roll angle of the observation in degrees. Default: 0.0 subpixel_res: boolean, optional If True, event positions are not randomized within the pixels within which they are detected. Default: False 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. """ import pyregion._region_filter as rfilter exp_time = parse_value(exp_time, "s") roll_angle = parse_value(roll_angle, "deg") prng = parse_prng(prng) if isinstance(input_events, dict): parameters = {} for key in ["flux", "emin", "emax", "sources"]: parameters[key] = input_events[key] event_list = [] for i in range(len(parameters["flux"])): edict = {} for key in ["ra", "dec", "energy"]: edict[key] = input_events[key][i] event_list.append(edict) elif isinstance(input_events, string_types): # Assume this is a SIMPUT catalog event_list, parameters = read_simput_catalog(input_events) try: instrument_spec = instrument_registry[instrument] except KeyError: raise KeyError("Instrument %s is not in the instrument registry!" % instrument) if not instrument_spec["imaging"]: raise RuntimeError("Instrument '%s' is not " % instrument_spec["name"] + "designed for imaging observations!") arf_file = get_response_path(instrument_spec["arf"]) rmf_file = get_response_path(instrument_spec["rmf"]) arf = AuxiliaryResponseFile(arf_file) rmf = RedistributionMatrixFile(rmf_file) nx = instrument_spec["num_pixels"] plate_scale = instrument_spec["fov"]/nx/60. # arcmin to deg plate_scale_arcsec = plate_scale * 3600.0 if not instrument_spec["dither"]: dither_on = False else: dither_on = not no_dither if dither_params is None: dither_params = [8.0, 8.0, 1000.0, 707.0] dither_dict = {"x_amp": dither_params[0], "y_amp": dither_params[1], "x_period": dither_params[2], "y_period": dither_params[3], "dither_on": dither_on, "plate_scale": plate_scale_arcsec} event_params = {} event_params["exposure_time"] = exp_time event_params["arf"] = arf.filename event_params["sky_center"] = sky_center event_params["pix_center"] = np.array([0.5*(2*nx+1)]*2) event_params["num_pixels"] = nx event_params["plate_scale"] = plate_scale event_params["rmf"] = rmf.filename event_params["channel_type"] = rmf.header["CHANTYPE"] event_params["telescope"] = rmf.header["TELESCOP"] event_params["instrument"] = instrument_spec['name'] event_params["mission"] = rmf.header.get("MISSION", "") event_params["nchan"] = rmf.n_ch event_params["roll_angle"] = roll_angle event_params["fov"] = instrument_spec["fov"] event_params["chan_lim"] = [rmf.cmin, rmf.cmax] event_params["chips"] = instrument_spec["chips"] event_params["dither_params"] = dither_dict event_params["aimpt_coords"] = instrument_spec["aimpt_coords"] w = pywcs.WCS(naxis=2) w.wcs.crval = event_params["sky_center"] w.wcs.crpix = event_params["pix_center"] w.wcs.cdelt = [-plate_scale, plate_scale] w.wcs.ctype = ["RA---TAN","DEC--TAN"] w.wcs.cunit = ["deg"]*2 rot_mat = get_rot_mat(roll_angle) all_events = defaultdict(list) for i, evts in enumerate(event_list): mylog.info("Detecting events from source %s." % parameters["sources"][i]) # Step 1: Use ARF to determine which photons are observed mylog.info("Applying energy-dependent effective area from %s." % os.path.split(arf.filename)[-1]) refband = [parameters["emin"][i], parameters["emax"][i]] events = arf.detect_events(evts, exp_time, parameters["flux"][i], refband, prng=prng) n_evt = events["energy"].size if n_evt == 0: mylog.warning("No events were observed for this source!!!") else: # Step 2: Assign pixel coordinates to events. Apply dithering and # PSF. Clip events that don't fall within the detection region. mylog.info("Pixeling events.") # Convert RA, Dec to pixel coordinates xpix, ypix = w.wcs_world2pix(events["ra"], events["dec"], 1) xpix -= event_params["pix_center"][0] ypix -= event_params["pix_center"][1] events.pop("ra") events.pop("dec") n_evt = xpix.size # Rotate physical coordinates to detector coordinates det = np.dot(rot_mat, np.array([xpix, ypix])) detx = det[0,:] + event_params["aimpt_coords"][0] dety = det[1,:] + event_params["aimpt_coords"][1] # Add times to events events['time'] = prng.uniform(size=n_evt, low=0.0, high=event_params["exposure_time"]) # Apply dithering x_offset, y_offset = perform_dither(events["time"], dither_dict) detx -= x_offset dety -= y_offset # PSF scattering of detector coordinates if instrument_spec["psf"] is not None: psf_type, psf_spec = instrument_spec["psf"] if psf_type == "gaussian": sigma = psf_spec/sigma_to_fwhm/plate_scale_arcsec detx += prng.normal(loc=0.0, scale=sigma, size=n_evt) dety += prng.normal(loc=0.0, scale=sigma, size=n_evt) else: raise NotImplementedError("PSF type %s not implemented!" % psf_type) # Convert detector coordinates to chip coordinates. # Throw out events that don't fall on any chip. cx = np.trunc(detx)+0.5*np.sign(detx) cy = np.trunc(dety)+0.5*np.sign(dety) if event_params["chips"] is None: events["chip_id"] = np.zeros(n_evt, dtype='int') keepx = np.logical_and(cx >= -0.5*nx, cx <= 0.5*nx) keepy = np.logical_and(cy >= -0.5*nx, cy <= 0.5*nx) keep = np.logical_and(keepx, keepy) else: events["chip_id"] = -np.ones(n_evt, dtype='int') for i, chip in enumerate(event_params["chips"]): thisc = np.ones(n_evt, dtype='bool') rtype = chip[0] args = chip[1:] r = getattr(rfilter, rtype)(*args) inside = r.inside(cx, cy) thisc = np.logical_and(thisc, inside) events["chip_id"][thisc] = i keep = events["chip_id"] > -1 mylog.info("%d events were rejected because " % (n_evt-keep.sum()) + "they do not fall on any CCD.") n_evt = keep.sum() if n_evt == 0: mylog.warning("No events are within the field of view for this source!!!") else: # Keep only those events which fall on a chip for key in events: events[key] = events[key][keep] # Convert chip coordinates back to detector coordinates, unless the # user has specified that they want subpixel resolution if subpixel_res: events["detx"] = detx[keep] events["dety"] = dety[keep] else: events["detx"] = cx[keep] + prng.uniform(low=-0.5, high=0.5, size=n_evt) events["dety"] = cy[keep] + prng.uniform(low=-0.5, high=0.5, size=n_evt) # Convert detector coordinates back to pixel coordinates by # adding the dither offsets back in and applying the rotation # matrix again det = np.array([events["detx"] + x_offset[keep] - event_params["aimpt_coords"][0], events["dety"] + y_offset[keep] - event_params["aimpt_coords"][1]]) pix = np.dot(rot_mat.T, det) events["xpix"] = pix[0,:] + event_params['pix_center'][0] events["ypix"] = pix[1,:] + event_params['pix_center'][1] if n_evt > 0: for key in events: all_events[key] = np.concatenate([all_events[key], events[key]]) if len(all_events["energy"]) == 0: mylog.warning("No events from any of the sources in the catalog were detected!") for key in ["xpix", "ypix", "detx", "dety", "time", "chip_id", event_params["channel_type"]]: all_events[key] = np.array([]) else: # Step 4: Scatter energies with RMF mylog.info("Scattering energies with RMF %s." % os.path.split(rmf.filename)[-1]) all_events = rmf.scatter_energies(all_events, prng=prng) return all_events, event_params
def make_background_file(out_file, exp_time, instrument, sky_center, overwrite=False, foreground=True, instr_bkgnd=True, ptsrc_bkgnd=True, no_dither=False, dither_params=None, subpixel_res=False, input_sources=None, absorb_model="wabs", nH=0.05, prng=None): """ Make an event file consisting entirely of background events. This will be useful for creating backgrounds that can be added to simulations of sources. Parameters ---------- exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time to use, in seconds. instrument : string The name of the instrument to use, which picks an instrument specification from the instrument registry. sky_center : array, tuple, or list The center RA, Dec coordinates of the observation, in degrees. overwrite : boolean, optional Whether or not to overwrite an existing file with the same name. Default: False foreground : boolean, optional Whether or not to include the Galactic foreground. Default: True instr_bkgnd : boolean, optional Whether or not to include the instrumental background. Default: True ptsrc_bkgnd : boolean, optional Whether or not to include the point-source background. Default: True no_dither : boolean, optional If True, turn off dithering entirely. Default: False dither_params : array-like of floats, optional The parameters to use to control the size and period of the dither pattern. The first two numbers are the dither amplitude in x and y detector coordinates in arcseconds, and the second two numbers are the dither period in x and y detector coordinates in seconds. Default: [8.0, 8.0, 1000.0, 707.0]. subpixel_res: boolean, optional If True, event positions are not randomized within the pixels within which they are detected. Default: False input_sources : string, optional If set to a filename, input the point source positions, fluxes, and spectral indices from an ASCII table instead of generating them. Default: None absorb_model : string, optional The absorption model to use, "wabs" or "tbabs". Default: "wabs" nH : float, optional The hydrogen column in units of 10**22 atoms/cm**2. Default: 0.05 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) events, event_params = make_background(exp_time, instrument, sky_center, ptsrc_bkgnd=ptsrc_bkgnd, foreground=foreground, instr_bkgnd=instr_bkgnd, no_dither=no_dither, dither_params=dither_params, subpixel_res=subpixel_res, input_sources=input_sources, absorb_model=absorb_model, nH=nH, prng=prng) write_event_file(events, event_params, out_file, overwrite=overwrite)
def project_photons(self, normal, sky_center, absorb_model=None, nH=None, no_shifting=False, north_vector=None, sigma_pos=None, kernel="top_hat", prng=None, **kwargs): r""" Projects photons onto an image plane given a line of sight. Returns a new :class:`~pyxsim.event_list.EventList`. Parameters ---------- normal : character or array-like Normal vector to the plane of projection. If "x", "y", or "z", will assume to be along that axis (and will probably be faster). Otherwise, should be an off-axis normal vector, e.g [1.0, 2.0, -3.0] sky_center : array-like Center RA, Dec of the events in degrees. absorb_model : string or :class:`~pyxsim.spectral_models.AbsorptionModel` A model for foreground galactic absorption, to simulate the absorption of events before being detected. This cannot be applied here if you already did this step previously in the creation of the :class:`~pyxsim.photon_list.PhotonList` instance. Known options for strings are "wabs" and "tbabs". nH : float, optional The foreground column density in units of 10^22 cm^{-2}. Only used if absorption is applied. no_shifting : boolean, optional If set, the photon energies will not be Doppler shifted. north_vector : a sequence of floats A vector defining the "up" direction. This option sets the orientation of the plane of projection. If not set, an arbitrary grid-aligned north_vector is chosen. Ignored in the case where a particular axis (e.g., "x", "y", or "z") is explicitly specified. sigma_pos : float, optional Apply a gaussian smoothing operation to the sky positions of the events. This may be useful when the binned events appear blocky due to their uniform distribution within simulation cells. However, this will move the events away from their originating position on the sky, and so may distort surface brightness profiles and/or spectra. Should probably only be used for visualization purposes. Supply a float here to smooth with a standard deviation with this fraction of the cell size. Default: None kernel : string, optional The kernel used when smoothing positions of X-rays originating from SPH particles, "gaussian" or "top_hat". Default: "top_hat". prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. Examples -------- >>> L = np.array([0.1,-0.2,0.3]) >>> events = my_photons.project_photons(L, [30., 45.]) """ prng = parse_prng(prng) scale_shift = -1.0/clight.to("km/s") if "smooth_positions" in kwargs: issue_deprecation_warning("'smooth_positions' has been renamed to " "'sigma_pos' and the former is deprecated!") sigma_pos = kwargs["smooth_positions"] if "redshift_new" in kwargs or "area_new" in kwargs or \ "exp_time_new" in kwargs or "dist_new" in kwargs: issue_deprecation_warning("Changing the redshift, distance, area, or " "exposure time has been deprecated in " "project_photons!") if sigma_pos is not None and self.parameters["data_type"] == "particles": raise RuntimeError("The 'smooth_positions' argument should not be used with " "particle-based datasets!") if isinstance(absorb_model, string_types): if absorb_model not in absorb_models: raise KeyError("%s is not a known absorption model!" % absorb_model) absorb_model = absorb_models[absorb_model] if absorb_model is not None: if nH is None: raise RuntimeError("You specified an absorption model, but didn't " "specify a value for nH!") absorb_model = absorb_model(nH) sky_center = YTArray(sky_center, "degree") n_ph = self.photons["num_photons"] if not isinstance(normal, string_types): L = np.array(normal) orient = Orientation(L, north_vector=north_vector) x_hat = orient.unit_vectors[0] y_hat = orient.unit_vectors[1] z_hat = orient.unit_vectors[2] else: x_hat = np.zeros(3) y_hat = np.zeros(3) z_hat = np.zeros(3) parameters = {} D_A = self.parameters["fid_d_a"] events = {} eobs = self.photons["energy"].v if not no_shifting: if comm.rank == 0: mylog.info("Doppler-shifting photon energies.") if isinstance(normal, string_types): shift = self.photons["vel"][:,"xyz".index(normal)]*scale_shift else: shift = np.dot(self.photons["vel"], z_hat)*scale_shift doppler_shift(shift, n_ph, eobs) if absorb_model is None: det = np.ones(eobs.size, dtype='bool') num_det = eobs.size else: if comm.rank == 0: mylog.info("Foreground galactic absorption: using " "the %s model and nH = %g." % (absorb_model._name, nH)) det = absorb_model.absorb_photons(eobs, prng=prng) num_det = det.sum() events["eobs"] = YTArray(eobs[det], "keV") num_events = comm.mpi_allreduce(num_det) if comm.rank == 0: mylog.info("%d events have been detected." % num_events) if num_det > 0: if comm.rank == 0: mylog.info("Assigning positions to events.") if isinstance(normal, string_types): norm = "xyz".index(normal) else: norm = normal xsky, ysky = scatter_events(norm, prng, kernel, self.parameters["data_type"], num_det, det, self.photons["num_photons"], self.photons["pos"].d, self.photons["dx"].d, x_hat, y_hat) if self.parameters["data_type"] == "cells" and sigma_pos is not None: if comm.rank == 0: mylog.info("Optionally smoothing sky positions.") sigma = sigma_pos*np.repeat(self.photons["dx"].d, n_ph)[det] xsky += sigma * prng.normal(loc=0.0, scale=1.0, size=num_det) ysky += sigma * prng.normal(loc=0.0, scale=1.0, size=num_det) d_a = D_A.to("kpc").v xsky /= d_a ysky /= d_a if comm.rank == 0: mylog.info("Converting pixel to sky coordinates.") pixel_to_cel(xsky, ysky, sky_center) else: xsky = [] ysky = [] events["xsky"] = YTArray(xsky, "degree") events["ysky"] = YTArray(ysky, "degree") parameters["exp_time"] = self.parameters["fid_exp_time"] parameters["area"] = self.parameters["fid_area"] parameters["sky_center"] = sky_center return EventList(events, parameters)
def simulate_spectrum(spec, instrument, exp_time, out_file, instr_bkgnd=False, foreground=False, ptsrc_bkgnd=False, bkgnd_area=None, absorb_model="wabs", nH=0.05, overwrite=False, prng=None): """ Generate a PI or PHA spectrum from a :class:`~soxs.spectra.Spectrum` by convolving it with responses. To be used if one wants to create a spectrum without worrying about spatial response. Similar to XSPEC's "fakeit". Parameters ---------- spec : :class:`~soxs.spectra.Spectrum` The spectrum to be convolved. If None is supplied, only backgrounds will be simulated (if they are turned on). instrument : string The name of the instrument to use, which picks an instrument specification from the instrument registry. exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. out_file : string The file to write the spectrum to. instr_bkgnd : boolean, optional Whether or not to include the instrumental/particle background. Default: False foreground : boolean, optional Whether or not to include the local foreground. Default: False ptsrc_bkgnd : boolean, optional Whether or not to include the unresolved point-source background. Default: False bkgnd_area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The area on the sky for the background components, in square arcminutes. Default: None, necessary to specify if any of the background components are turned on. absorb_model : string, optional The absorption model to use, "wabs" or "tbabs". Default: "wabs" nH : float, optional The hydrogen column in units of 10**22 atoms/cm**2. Default: 0.05 overwrite : boolean, optional Whether or not to overwrite an existing file. Default: False 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. Examples -------- >>> spec = soxs.Spectrum.from_file("my_spectrum.txt") >>> soxs.simulate_spectrum(spec, "lynx_lxm", 100000.0, ... "my_spec.pi", overwrite=True) """ from soxs.events import _write_spectrum from soxs.instrument import RedistributionMatrixFile, \ AuxiliaryResponseFile from soxs.spectra import ConvolvedSpectrum from soxs.background.foreground import hm_astro_bkgnd from soxs.background.instrument import instrument_backgrounds from soxs.background.spectra import BackgroundSpectrum, \ ConvolvedBackgroundSpectrum prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") try: instrument_spec = instrument_registry[instrument] except KeyError: raise KeyError("Instrument %s is not in the instrument registry!" % instrument) if foreground or instr_bkgnd or ptsrc_bkgnd: if instrument_spec["grating"]: raise NotImplementedError("Backgrounds cannot be included in simulations " "of gratings spectra at this time!") if bkgnd_area is None: raise RuntimeError("The 'bkgnd_area' argument must be set if one wants " "to simulate backgrounds! Specify a value in square " "arcminutes.") bkgnd_area = np.sqrt(parse_value(bkgnd_area, "arcmin**2")) elif spec is None: raise RuntimeError("You have specified no source spectrum and no backgrounds!") arf_file = get_response_path(instrument_spec["arf"]) rmf_file = get_response_path(instrument_spec["rmf"]) arf = AuxiliaryResponseFile(arf_file) rmf = RedistributionMatrixFile(rmf_file) event_params = {} event_params["RESPFILE"] = os.path.split(rmf.filename)[-1] event_params["ANCRFILE"] = os.path.split(arf.filename)[-1] event_params["TELESCOP"] = rmf.header["TELESCOP"] event_params["INSTRUME"] = rmf.header["INSTRUME"] event_params["MISSION"] = rmf.header.get("MISSION", "") out_spec = np.zeros(rmf.n_ch) if spec is not None: cspec = ConvolvedSpectrum(spec, arf) out_spec += rmf.convolve_spectrum(cspec, exp_time, prng=prng) fov = None if bkgnd_area is None else np.sqrt(bkgnd_area) if foreground: mylog.info("Adding in astrophysical foreground.") cspec_frgnd = ConvolvedSpectrum(hm_astro_bkgnd.to_spectrum(fov), arf) out_spec += rmf.convolve_spectrum(cspec_frgnd, exp_time, prng=prng) if instr_bkgnd and instrument_spec["bkgnd"] is not None: mylog.info("Adding in instrumental background.") instr_spec = instrument_backgrounds[instrument_spec["bkgnd"]] cspec_instr = instr_spec.to_scaled_spectrum(fov, instrument_spec["focal_length"]) out_spec += rmf.convolve_spectrum(cspec_instr, exp_time, prng=prng) if ptsrc_bkgnd: mylog.info("Adding in background from unresolved point-sources.") spec_plaw = BackgroundSpectrum.from_powerlaw(1.45, 0.0, 2.0e-7, emin=0.01, emax=10.0, nbins=300000) spec_plaw.apply_foreground_absorption(nH, model=absorb_model) cspec_plaw = ConvolvedBackgroundSpectrum(spec_plaw.to_spectrum(fov), arf) out_spec += rmf.convolve_spectrum(cspec_plaw, exp_time, prng=prng) bins = (np.arange(rmf.n_ch)+rmf.cmin).astype("int32") _write_spectrum(bins, out_spec, exp_time, rmf.header["CHANTYPE"], event_params, out_file, overwrite=overwrite)
def __init__(self, prng=None): self.spectral_norm = None self.redshift = None self.prng = parse_prng(prng)
def make_background(exp_time, instrument, sky_center, foreground=True, ptsrc_bkgnd=True, instr_bkgnd=True, no_dither=False, dither_params=None, roll_angle=0.0, subpixel_res=False, input_sources=None, absorb_model="wabs", nH=0.05, prng=None): """ Make background events. Parameters ---------- exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time to use, in seconds. instrument : string The name of the instrument to use, which picks an instrument specification from the instrument registry. sky_center : array, tuple, or list The center RA, Dec coordinates of the observation, in degrees. foreground : boolean, optional Whether or not to include the Galactic foreground. Default: True instr_bkgnd : boolean, optional Whether or not to include the instrumental background. Default: True no_dither : boolean, optional If True, turn off dithering entirely. Default: False dither_params : array-like of floats, optional The parameters to use to control the size and period of the dither pattern. The first two numbers are the dither amplitude in x and y detector coordinates in arcseconds, and the second two numbers are the dither period in x and y detector coordinates in seconds. Default: [8.0, 8.0, 1000.0, 707.0]. ptsrc_bkgnd : boolean, optional Whether or not to include the point-source background. Default: True Default: 0.05 roll_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The roll angle of the observation in degrees. Default: 0.0 subpixel_res: boolean, optional If True, event positions are not randomized within the pixels within which they are detected. Default: False input_sources : string, optional If set to a filename, input the point source positions, fluxes, and spectral indices from an ASCII table instead of generating them. Default: None absorb_model : string, optional The absorption model to use, "wabs" or "tbabs". Default: "wabs" nH : float, optional The hydrogen column in units of 10**22 atoms/cm**2. Default: 0.05 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. """ from soxs.background import make_instrument_background, \ make_foreground, make_ptsrc_background prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") roll_angle = parse_value(roll_angle, "deg") try: instrument_spec = instrument_registry[instrument] except KeyError: raise KeyError("Instrument %s is not in the instrument registry!" % instrument) if not instrument_spec["imaging"]: raise RuntimeError("Instrument '%s' is not " % instrument_spec["name"] + "designed for imaging observations!") fov = instrument_spec["fov"] input_events = defaultdict(list) arf_file = get_response_path(instrument_spec["arf"]) arf = AuxiliaryResponseFile(arf_file) rmf_file = get_response_path(instrument_spec["rmf"]) rmf = RedistributionMatrixFile(rmf_file) if ptsrc_bkgnd: mylog.info("Adding in point-source background.") ptsrc_events = make_ptsrc_background(exp_time, fov, sky_center, area=1.2*arf.max_area, input_sources=input_sources, absorb_model=absorb_model, nH=nH, prng=prng) for key in ["ra", "dec", "energy"]: input_events[key].append(ptsrc_events[key]) input_events["flux"].append(ptsrc_events["flux"]) input_events["emin"].append(ptsrc_events["energy"].min()) input_events["emax"].append(ptsrc_events["energy"].max()) input_events["sources"].append("ptsrc_bkgnd") events, event_params = generate_events(input_events, exp_time, instrument, sky_center, no_dither=no_dither, dither_params=dither_params, roll_angle=roll_angle, subpixel_res=subpixel_res, prng=prng) mylog.info("Generated %d photons from the point-source background." % len(events["energy"])) else: nx = instrument_spec["num_pixels"] events = defaultdict(list) if not instrument_spec["dither"]: dither_on = False else: dither_on = not no_dither if dither_params is None: dither_params = [8.0, 8.0, 1000.0, 707.0] dither_dict = {"x_amp": dither_params[0], "y_amp": dither_params[1], "x_period": dither_params[2], "y_period": dither_params[3], "dither_on": dither_on, "plate_scale": instrument_spec["fov"]/nx*60.0} event_params = {"exposure_time": exp_time, "fov": instrument_spec["fov"], "num_pixels": nx, "pix_center": np.array([0.5*(2*nx+1)]*2), "channel_type": rmf.header["CHANTYPE"], "sky_center": sky_center, "dither_params": dither_dict, "plate_scale": instrument_spec["fov"]/nx/60.0, "chan_lim": [rmf.cmin, rmf.cmax], "rmf": rmf_file, "arf": arf_file, "telescope": rmf.header["TELESCOP"], "instrument": instrument_spec['name'], "mission": rmf.header.get("MISSION", ""), "nchan": rmf.n_ch, "roll_angle": roll_angle, "aimpt_coords": instrument_spec["aimpt_coords"]} if "chips" not in event_params: event_params["chips"] = instrument_spec["chips"] if foreground: mylog.info("Adding in astrophysical foreground.") bkg_events = make_foreground(event_params, arf, rmf, prng=prng) for key in bkg_events: events[key] = np.concatenate([events[key], bkg_events[key]]) if instr_bkgnd and instrument_spec["bkgnd"] is not None: mylog.info("Adding in instrumental background.") bkg_events = make_instrument_background(instrument_spec["bkgnd"], event_params, rmf, prng=prng) for key in bkg_events: events[key] = np.concatenate([events[key], bkg_events[key]]) return events, event_params
def make_xrb_particles(data_source, age_field, scale_length, sfr_time_range=(1.0, "Gyr"), prng=None): r""" This routine generates an in-memory dataset composed of X-ray binary particles from an input data source containing star particles. Parameters ---------- data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer` The yt data source to obtain the data from, such as a sphere, box, disk, etc. age_field : string or (type, name) field tuple The stellar age field. Must be in some kind of time units. scale_length : string, (ftype, fname) tuple, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The radial length scale over which to scatter the XRB particles from their parent star particle. Can be the name of a smoothing length field for the stars, a (value, unit) tuple, or a YTQuantity. sfr_time_range : string, (ftype, fname) tuple, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`, optional The recent time range over which to calculate the star formation rate from the current time in the dataset. Default: 1.0 Gyr prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ prng = parse_prng(prng) ds = data_source.ds ptype = data_source._determine_fields(age_field)[0][0] t = data_source[age_field].to("Gyr") m = data_source[(ptype, "particle_mass")].to("Msun") sfr_time_range = parse_value(sfr_time_range, "Gyr") recent = t < sfr_time_range n_recent = recent.sum() if n_recent == 0: sfr = 0.0 else: sfr = (m[recent].sum()/sfr_time_range).to("Msun/yr").v mylog.info("%d star particles were formed in the last " % n_recent + "%s for a SFR of %4.1f Msun/yr." % (sfr_time_range, sfr)) mtot = m.sum() npart = m.size scale_field = None if isinstance(scale_length, tuple): if isinstance(scale_length[0], string_types): scale_field = scale_length elif isinstance(scale_length, string_types): scale_field = (ptype, scale_length) if scale_field is None: if isinstance(scale_length, tuple): scale = YTArray([scale_length[0]]*npart, scale_length[1]) elif isinstance(scale_length, YTQuantity): scale = YTArray([scale_length]*npart) else: scale = YTArray([scale_length[0]]*npart, "kpc") else: scale = data_source[scale_length] scale = scale.to('kpc').d N_l = lmxb_cdf(Lcut)*mtot.v*1.0e-11 N_h = hmxb_cdf(Lcut)*sfr N_all = N_l+N_h if N_all == 0.0: raise RuntimeError("There are no X-ray binaries to generate!") # Compute conversion factors from luminosity to count rate lmxb_factor = get_scale_factor(alpha_lmxb, emin_lmxb, emax_lmxb) hmxb_factor = get_scale_factor(alpha_hmxb, emin_hmxb, emax_hmxb) xp = [] yp = [] zp = [] vxp = [] vyp = [] vzp = [] lp = [] rp = [] ap = [] if N_l > 0.0: F_l = np.zeros(nbins+1) for i in range(1, nbins+1): F_l[i] = lmxb_cdf(Lbins[i]) F_l /= F_l[-1] invcdf_l = InterpolatedUnivariateSpline(F_l, logLbins) n_l = prng.poisson(lam=N_l*m/mtot) mylog.info("Number of low-mass X-ray binaries: %s" % n_l.sum()) for i, n in enumerate(n_l): if n > 0: randvec = prng.uniform(size=n) l = YTArray(10**invcdf_l(randvec)*1.0e38, "erg/s") r = YTArray(l.v*lmxb_factor, "photons/s/keV") # Now convert output luminosities to bolometric l *= bc_lmxb x = YTArray(prng.normal(scale=scale[i], size=n), "kpc") y = YTArray(prng.normal(scale=scale[i], size=n), "kpc") z = YTArray(prng.normal(scale=scale[i], size=n), "kpc") x += data_source[ptype, "particle_position_x"][i].to("kpc") y += data_source[ptype, "particle_position_y"][i].to("kpc") z += data_source[ptype, "particle_position_z"][i].to("kpc") vx = YTArray([data_source[ptype, "particle_velocity_x"][i]]*n).to('km/s') vy = YTArray([data_source[ptype, "particle_velocity_y"][i]]*n).to('km/s') vz = YTArray([data_source[ptype, "particle_velocity_z"][i]]*n).to('km/s') xp.append(x) yp.append(y) zp.append(z) vxp.append(vx) vyp.append(vy) vzp.append(vz) lp.append(l) rp.append(r) ap.append(np.array([alpha_lmxb]*n)) if N_h > 0.0: F_h = np.zeros(nbins+1) for i in range(1, nbins+1): F_h[i] = hmxb_cdf(Lbins[i]) F_h /= F_h[-1] invcdf_h = InterpolatedUnivariateSpline(F_h, logLbins) n_h = prng.poisson(lam=N_h*m/mtot) mylog.info("Number of high-mass X-ray binaries: %s" % n_h.sum()) for i, n in enumerate(n_h): if n > 0: randvec = prng.uniform(size=n) l = YTArray(10**invcdf_h(randvec)*1.0e38, "erg/s") r = YTArray(l.v*hmxb_factor, "photons/s/keV") # Now convert output luminosities to bolometric l *= bc_hmxb x = YTArray(prng.normal(scale=scale[i], size=n), "kpc") y = YTArray(prng.normal(scale=scale[i], size=n), "kpc") z = YTArray(prng.normal(scale=scale[i], size=n), "kpc") x += data_source[ptype, "particle_position_x"][i].to("kpc") y += data_source[ptype, "particle_position_y"][i].to("kpc") z += data_source[ptype, "particle_position_z"][i].to("kpc") vx = YTArray([data_source[ptype, "particle_velocity_x"][i]]*n).to('km/s') vy = YTArray([data_source[ptype, "particle_velocity_y"][i]]*n).to('km/s') vz = YTArray([data_source[ptype, "particle_velocity_z"][i]]*n).to('km/s') xp.append(x) yp.append(y) zp.append(z) vxp.append(vx) vyp.append(vy) vzp.append(vz) lp.append(l) rp.append(r) ap.append(np.array([alpha_hmxb]*n)) xp = uconcatenate(xp) yp = uconcatenate(yp) zp = uconcatenate(zp) vxp = uconcatenate(vxp) vyp = uconcatenate(vyp) vzp = uconcatenate(vzp) lp = uconcatenate(lp) rp = uconcatenate(rp) ap = uconcatenate(ap) data = {"particle_position_x": (xp.d, str(xp.units)), "particle_position_y": (yp.d, str(yp.units)), "particle_position_z": (zp.d, str(zp.units)), "particle_velocity_x": (vxp.d, str(vxp.units)), "particle_velocity_y": (vyp.d, str(vyp.units)), "particle_velocity_z": (vzp.d, str(vzp.units)), "particle_luminosity": (lp.d, str(lp.units)), "particle_count_rate": (rp.d, str(rp.units)), "particle_spectral_index": ap} dle = ds.domain_left_edge.to("kpc").v dre = ds.domain_right_edge.to("kpc").v bbox = np.array([[dle[i], dre[i]] for i in range(3)]) new_ds = load_particles(data, bbox=bbox, length_unit="kpc", time_unit="Myr", mass_unit="Msun", velocity_unit="km/s") return new_ds
def simulate_spectrum(spec, instrument, exp_time, out_file, instr_bkgnd=False, foreground=False, ptsrc_bkgnd=False, bkgnd_area=None, absorb_model="wabs", nH=0.05, overwrite=False, prng=None): """ Generate a PI or PHA spectrum from a :class:`~soxs.spectra.Spectrum` by convolving it with responses. To be used if one wants to create a spectrum without worrying about spatial response. Similar to XSPEC's "fakeit". Parameters ---------- spec : :class:`~soxs.spectra.Spectrum` The spectrum to be convolved. If None is supplied, only backgrounds will be simulated (if they are turned on). instrument : string The name of the instrument to use, which picks an instrument specification from the instrument registry. exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. out_file : string The file to write the spectrum to. instr_bkgnd : boolean, optional Whether or not to include the instrumental/particle background. Default: False foreground : boolean, optional Whether or not to include the local foreground. Default: False ptsrc_bkgnd : boolean, optional Whether or not to include the unresolved point-source background. Default: False bkgnd_area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The area on the sky for the background components, in square arcminutes. Default: None, necessary to specify if any of the background components are turned on. absorb_model : string, optional The absorption model to use, "wabs" or "tbabs". Default: "wabs" nH : float, optional The hydrogen column in units of 10**22 atoms/cm**2. Default: 0.05 overwrite : boolean, optional Whether or not to overwrite an existing file. Default: False 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. Examples -------- >>> spec = soxs.Spectrum.from_file("my_spectrum.txt") >>> soxs.simulate_spectrum(spec, "lynx_lxm", 100000.0, ... "my_spec.pi", overwrite=True) """ from soxs.events import _write_spectrum from soxs.response import RedistributionMatrixFile, \ AuxiliaryResponseFile from soxs.spectra import ConvolvedSpectrum from soxs.background.foreground import hm_astro_bkgnd from soxs.background.spectra import BackgroundSpectrum from soxs.background.instrument import InstrumentalBackground prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") try: instrument_spec = instrument_registry[instrument] except KeyError: raise KeyError( f"Instrument {instrument} is not in the instrument registry!") if foreground or instr_bkgnd or ptsrc_bkgnd: if instrument_spec["grating"]: raise NotImplementedError( "Backgrounds cannot be included in simulations " "of gratings spectra at this time!") if bkgnd_area is None: raise RuntimeError( "The 'bkgnd_area' argument must be set if one wants " "to simulate backgrounds! Specify a value in square " "arcminutes.") bkgnd_area = np.sqrt(parse_value(bkgnd_area, "arcmin**2")) elif spec is None: raise RuntimeError( "You have specified no source spectrum and no backgrounds!") arf_file = get_data_file(instrument_spec["arf"]) rmf_file = get_data_file(instrument_spec["rmf"]) arf = AuxiliaryResponseFile(arf_file) rmf = RedistributionMatrixFile(rmf_file) event_params = { "RESPFILE": os.path.split(rmf.filename)[-1], "ANCRFILE": os.path.split(arf.filename)[-1], "TELESCOP": rmf.header["TELESCOP"], "INSTRUME": rmf.header["INSTRUME"], "MISSION": rmf.header.get("MISSION", "") } out_spec = np.zeros(rmf.n_ch) if spec is not None: cspec = ConvolvedSpectrum.convolve(spec, arf) out_spec += rmf.convolve_spectrum(cspec, exp_time, prng=prng) fov = None if bkgnd_area is None else np.sqrt(bkgnd_area) if foreground: mylog.info("Adding in astrophysical foreground.") cspec_frgnd = ConvolvedSpectrum.convolve( hm_astro_bkgnd.to_spectrum(fov), arf) out_spec += rmf.convolve_spectrum(cspec_frgnd, exp_time, prng=prng) if instr_bkgnd and instrument_spec["bkgnd"] is not None: mylog.info("Adding in instrumental background.") bkgnd_spec = instrument_spec["bkgnd"] # Temporary hack for ACIS-S if "aciss" in instrument_spec["name"]: bkgnd_spec = bkgnd_spec[1] bkgnd_spec = InstrumentalBackground.from_filename( bkgnd_spec[0], bkgnd_spec[1], instrument_spec['focal_length']) out_spec += bkgnd_spec.generate_channel_spectrum(exp_time, bkgnd_area, prng=prng) if ptsrc_bkgnd: mylog.info("Adding in background from unresolved point-sources.") spec_plaw = BackgroundSpectrum.from_powerlaw(1.45, 0.0, 2.0e-7, emin=0.01, emax=10.0, nbins=300000) spec_plaw.apply_foreground_absorption(nH, model=absorb_model) cspec_plaw = ConvolvedSpectrum.convolve(spec_plaw.to_spectrum(fov), arf) out_spec += rmf.convolve_spectrum(cspec_plaw, exp_time, prng=prng) bins = (np.arange(rmf.n_ch) + rmf.cmin).astype("int32") _write_spectrum(bins, out_spec, exp_time, rmf.header["CHANTYPE"], event_params, out_file, overwrite=overwrite)
def generate_events(self, area, exp_time, angular_width, source_model, sky_center, parameters=None, velocity_fields=None, absorb_model=None, nH=None, no_shifting=False, sigma_pos=None, prng=None): """ Generate projected events from a light cone simulation. Parameters ---------- area : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity` The collecting area to determine the number of events. If units are not specified, it is assumed to be in cm^2. exp_time : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity` The exposure time to determine the number of events. If units are not specified, it is assumed to be in seconds. angular_width : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity` The angular width of the light cone simulation. If units are not specified, it is assumed to be in degrees. source_model : :class:`~pyxsim.source_models.SourceModel` A source model used to generate the events. sky_center : array-like Center RA, Dec of the events in degrees. parameters : dict, optional A dictionary of parameters to be passed for the source model to use, if necessary. velocity_fields : list of fields The yt fields to use for the velocity. If not specified, the following will be assumed: ['velocity_x', 'velocity_y', 'velocity_z'] for grid datasets ['particle_velocity_x', 'particle_velocity_y', 'particle_velocity_z'] for particle datasets absorb_model : string or :class:`~pyxsim.spectral_models.AbsorptionModel` A model for foreground galactic absorption, to simulate the absorption of events before being detected. This cannot be applied here if you already did this step previously in the creation of the :class:`~pyxsim.photon_list.PhotonList` instance. Known options for strings are "wabs" and "tbabs". nH : float, optional The foreground column density in units of 10^22 cm^{-2}. Only used if absorption is applied. no_shifting : boolean, optional If set, the photon energies will not be Doppler shifted. sigma_pos : float, optional Apply a gaussian smoothing operation to the sky positions of the events. This may be useful when the binned events appear blocky due to their uniform distribution within simulation cells. However, this will move the events away from their originating position on the sky, and so may distort surface brightness profiles and/or spectra. Should probably only be used for visualization purposes. Supply a float here to smooth with a standard deviation with this fraction of the cell size. Default: None prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ prng = parse_prng(prng) area = parse_value(area, "cm**2") exp_time = parse_value(exp_time, "s") aw = parse_value(angular_width, "deg") tot_events = defaultdict(list) for output in self.light_cone_solution: ds = load(output["filename"]) ax = output["projection_axis"] c = output["projection_center"]*ds.domain_width + ds.domain_left_edge le = c.copy() re = c.copy() width = ds.quan(aw*output["box_width_per_angle"], "unitary").to("code_length") depth = ds.domain_width[ax].in_units("code_length")*output["box_depth_fraction"] le[ax] -= 0.5*depth re[ax] += 0.5*depth for off_ax in axes_lookup[ax]: le[off_ax] -= 0.5*width re[off_ax] += 0.5*width reg = ds.box(le, re) photons = PhotonList.from_data_source(reg, output['redshift'], area, exp_time, source_model, parameters=parameters, center=c, velocity_fields=velocity_fields, cosmology=ds.cosmology) if sum(photons["num_photons"]) > 0: events = photons.project_photons("xyz"[ax], sky_center, absorb_model=absorb_model, nH=nH, no_shifting=no_shifting, sigma_pos=sigma_pos, prng=prng) if events.num_events > 0: tot_events["xsky"].append(events["xsky"]) tot_events["ysky"].append(events["ysky"]) tot_events["eobs"].append(events["eobs"]) del events del photons parameters = {"exp_time": exp_time, "area": area, "sky_center": YTArray(sky_center, "deg")} for key in tot_events: tot_events[key] = uconcatenate(tot_events[key]) return EventList(tot_events, parameters)
def generate_events(self, area, exp_time, angular_width, source_model, sky_center, parameters=None, velocity_fields=None, absorb_model=None, nH=None, no_shifting=False, sigma_pos=None, prng=None): """ Generate projected events from a light cone simulation. Parameters ---------- area : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity` The collecting area to determine the number of events. If units are not specified, it is assumed to be in cm^2. exp_time : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity` The exposure time to determine the number of events. If units are not specified, it is assumed to be in seconds. angular_width : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity` The angular width of the light cone simulation. If units are not specified, it is assumed to be in degrees. source_model : :class:`~pyxsim.source_models.SourceModel` A source model used to generate the events. sky_center : array-like Center RA, Dec of the events in degrees. parameters : dict, optional A dictionary of parameters to be passed for the source model to use, if necessary. velocity_fields : list of fields The yt fields to use for the velocity. If not specified, the following will be assumed: ['velocity_x', 'velocity_y', 'velocity_z'] for grid datasets ['particle_velocity_x', 'particle_velocity_y', 'particle_velocity_z'] for particle datasets absorb_model : string or :class:`~pyxsim.spectral_models.AbsorptionModel` A model for foreground galactic absorption, to simulate the absorption of events before being detected. This cannot be applied here if you already did this step previously in the creation of the :class:`~pyxsim.photon_list.PhotonList` instance. Known options for strings are "wabs" and "tbabs". nH : float, optional The foreground column density in units of 10^22 cm^{-2}. Only used if absorption is applied. no_shifting : boolean, optional If set, the photon energies will not be Doppler shifted. sigma_pos : float, optional Apply a gaussian smoothing operation to the sky positions of the events. This may be useful when the binned events appear blocky due to their uniform distribution within simulation cells. However, this will move the events away from their originating position on the sky, and so may distort surface brightness profiles and/or spectra. Should probably only be used for visualization purposes. Supply a float here to smooth with a standard deviation with this fraction of the cell size. Default: None prng : integer or :class:`~numpy.random.RandomState` object 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 to use the :mod:`numpy.random` module. """ prng = parse_prng(prng) area = parse_value(area, "cm**2") exp_time = parse_value(exp_time, "s") aw = parse_value(angular_width, "deg") tot_events = defaultdict(list) for output in self.light_cone_solution: ds = load(output["filename"]) ax = output["projection_axis"] c = output[ "projection_center"] * ds.domain_width + ds.domain_left_edge le = c.copy() re = c.copy() width = ds.quan(aw * output["box_width_per_angle"], "unitary").to("code_length") depth = ds.domain_width[ax].in_units( "code_length") * output["box_depth_fraction"] le[ax] -= 0.5 * depth re[ax] += 0.5 * depth for off_ax in axes_lookup[ax]: le[off_ax] -= 0.5 * width re[off_ax] += 0.5 * width reg = ds.box(le, re) photons = PhotonList.from_data_source( reg, output['redshift'], area, exp_time, source_model, parameters=parameters, center=c, velocity_fields=velocity_fields, cosmology=ds.cosmology) if sum(photons["num_photons"]) > 0: events = photons.project_photons("xyz"[ax], sky_center, absorb_model=absorb_model, nH=nH, no_shifting=no_shifting, sigma_pos=sigma_pos, prng=prng) if events.num_events > 0: tot_events["xsky"].append(events["xsky"]) tot_events["ysky"].append(events["ysky"]) tot_events["eobs"].append(events["eobs"]) del events del photons parameters = { "exp_time": exp_time, "area": area, "sky_center": YTArray(sky_center, "deg") } for key in tot_events: tot_events[key] = uconcatenate(tot_events[key]) return EventList(tot_events, parameters)
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