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 if prng is None: self.prng = np.random else: self.prng = prng self.spectral_norm = None self.redshift = None
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 test_parse_value(): t_yt = parse_value(YTQuantity(300.0, "ks"), "s") t_astropy = parse_value(Quantity(300.0, "ks"), "s") t_float = parse_value(300000.0, "s") t_tuple = parse_value((300.0, "ks"), "s") assert t_astropy == t_yt assert t_float == t_yt assert t_tuple == t_yt
def create_empty_list(cls, exp_time, area, wcs, parameters=None): events = {"xpix": np.array([]), "ypix": np.array([]), "eobs": YTArray([], "keV")} if parameters is None: parameters = {} parameters["ExposureTime"] = parse_value(exp_time, "s") parameters["Area"] = parse_value(area, "cm**2") parameters["pix_center"] = wcs.wcs.crpix[:] parameters["sky_center"] = YTArray(wcs.wcs.crval[:], "deg") parameters["dtheta"] = YTQuantity(wcs.wcs.cdelt[0], "deg") return cls(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 if prng is None: self.prng = np.random else: self.prng = prng self.spectral_norm = None self.redshift = None
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 __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_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 write_fits_file(self, fitsfile, fov, nx, overwrite=False): """ Write events to a FITS binary table file. The result is a standard "event file" which can be processed by standard X-ray analysis tools. Parameters ---------- fitsfile : string The name of the event file to write. 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. nx : integer The resolution of the image (number of pixels on a side). overwrite : boolean, optional Set to True to overwrite a previous file. """ from astropy.time import Time, TimeDelta events = communicate_events(self.events) fov = parse_value(fov, "arcmin") if comm.rank == 0: exp_time = float(self.parameters["exp_time"]) t_begin = Time.now() dt = TimeDelta(exp_time, format='sec') t_end = t_begin + dt dtheta = fov.to("deg").v / nx wcs = pywcs.WCS(naxis=2) wcs.wcs.crpix = [0.5 * (nx + 1)] * 2 wcs.wcs.crval = self.parameters["sky_center"].d wcs.wcs.cdelt = [-dtheta, dtheta] wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"] wcs.wcs.cunit = ["deg"] * 2 xx, yy = wcs.wcs_world2pix(self["xsky"].d, self["ysky"].d, 1) keepx = np.logical_and(xx >= 0.5, xx <= float(nx) + 0.5) keepy = np.logical_and(yy >= 0.5, yy <= float(nx) + 0.5) keep = np.logical_and(keepx, keepy) n_events = keep.sum() mylog.info("Threw out %d events because " % (xx.size - n_events) + "they fell outside the field of view.") col_e = pyfits.Column(name='ENERGY', format='E', unit='eV', array=events["eobs"].in_units("eV").d[keep]) col_x = pyfits.Column(name='X', format='D', unit='pixel', array=xx[keep]) col_y = pyfits.Column(name='Y', format='D', unit='pixel', array=yy[keep]) cols = [col_e, col_x, col_y] if "channel_type" in self.parameters: chantype = self.parameters["channel_type"] if chantype == "pha": cunit = "adu" elif chantype == "pi": cunit = "Chan" col_ch = pyfits.Column(name=chantype.upper(), format='1J', unit=cunit, array=events[chantype][keep]) cols.append(col_ch) time = np.random.uniform(size=n_events, low=0.0, high=float( self.parameters["exp_time"])) col_t = pyfits.Column(name="TIME", format='1D', unit='s', array=time) cols.append(col_t) coldefs = pyfits.ColDefs(cols) tbhdu = pyfits.BinTableHDU.from_columns(coldefs) tbhdu.name = "EVENTS" tbhdu.header["MTYPE1"] = "sky" tbhdu.header["MFORM1"] = "x,y" tbhdu.header["MTYPE2"] = "EQPOS" tbhdu.header["MFORM2"] = "RA,DEC" tbhdu.header["TCTYP2"] = "RA---TAN" tbhdu.header["TCTYP3"] = "DEC--TAN" tbhdu.header["TCRVL2"] = float(self.parameters["sky_center"][0]) tbhdu.header["TCRVL3"] = float(self.parameters["sky_center"][1]) tbhdu.header["TCDLT2"] = -dtheta tbhdu.header["TCDLT3"] = dtheta tbhdu.header["TCRPX2"] = 0.5 * (nx + 1) tbhdu.header["TCRPX3"] = 0.5 * (nx + 1) tbhdu.header["TLMIN2"] = 0.5 tbhdu.header["TLMIN3"] = 0.5 tbhdu.header["TLMAX2"] = float(nx) + 0.5 tbhdu.header["TLMAX3"] = float(nx) + 0.5 if "channel_type" in self.parameters: rmf = RedistributionMatrixFile(self.parameters["rmf"]) tbhdu.header["TLMIN4"] = rmf.cmin tbhdu.header["TLMAX4"] = rmf.cmax tbhdu.header["RESPFILE"] = os.path.split( self.parameters["rmf"])[-1] tbhdu.header["PHA_BINS"] = rmf.n_ch tbhdu.header["ANCRFILE"] = os.path.split( self.parameters["arf"])[-1] tbhdu.header["CHANTYPE"] = self.parameters["channel_type"] tbhdu.header["MISSION"] = self.parameters["mission"] tbhdu.header["TELESCOP"] = self.parameters["telescope"] tbhdu.header["INSTRUME"] = self.parameters["instrument"] tbhdu.header["EXPOSURE"] = exp_time tbhdu.header["TSTART"] = 0.0 tbhdu.header["TSTOP"] = exp_time tbhdu.header["AREA"] = float(self.parameters["area"]) tbhdu.header["HDUVERS"] = "1.1.0" tbhdu.header["RADECSYS"] = "FK5" tbhdu.header["EQUINOX"] = 2000.0 tbhdu.header["HDUCLASS"] = "OGIP" tbhdu.header["HDUCLAS1"] = "EVENTS" tbhdu.header["HDUCLAS2"] = "ACCEPTED" tbhdu.header["DATE"] = t_begin.tt.isot tbhdu.header["DATE-OBS"] = t_begin.tt.isot tbhdu.header["DATE-END"] = t_end.tt.isot hdulist = [pyfits.PrimaryHDU(), tbhdu] if "channel_type" in self.parameters: start = pyfits.Column(name='START', format='1D', unit='s', array=np.array([0.0])) stop = pyfits.Column(name='STOP', format='1D', unit='s', array=np.array([exp_time])) tbhdu_gti = pyfits.BinTableHDU.from_columns([start, stop]) tbhdu_gti.name = "STDGTI" tbhdu_gti.header["TSTART"] = 0.0 tbhdu_gti.header["TSTOP"] = exp_time tbhdu_gti.header["HDUCLASS"] = "OGIP" tbhdu_gti.header["HDUCLAS1"] = "GTI" tbhdu_gti.header["HDUCLAS2"] = "STANDARD" tbhdu_gti.header["RADECSYS"] = "FK5" tbhdu_gti.header["EQUINOX"] = 2000.0 tbhdu_gti.header["DATE"] = t_begin.tt.isot tbhdu_gti.header["DATE-OBS"] = t_begin.tt.isot tbhdu_gti.header["DATE-END"] = t_end.tt.isot hdulist.append(tbhdu_gti) pyfits.HDUList(hdulist).writeto(fitsfile, overwrite=overwrite) comm.barrier()
def write_fits_image(self, imagefile, fov, nx, emin=None, emax=None, overwrite=False): r""" Generate a image by binning X-ray counts and write it to a FITS file. Parameters ---------- imagefile : string The name of the image file to write. fov : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The field of view of the image. If units are not provided, they are assumed to be in arcminutes. nx : integer The resolution of the image (number of pixels on a side). emin : float, optional The minimum energy of the photons to put in the image, in keV. emax : float, optional The maximum energy of the photons to put in the image, in keV. overwrite : boolean, optional Set to True to overwrite a previous file. """ fov = parse_value(fov, "arcmin") if emin is None: mask_emin = np.ones(self.num_events, dtype='bool') else: mask_emin = self["eobs"].d > emin if emax is None: mask_emax = np.ones(self.num_events, dtype='bool') else: mask_emax = self["eobs"].d < emax mask = np.logical_and(mask_emin, mask_emax) dtheta = fov.to("deg").v / nx xbins = np.linspace(0.5, float(nx) + 0.5, nx + 1, endpoint=True) ybins = np.linspace(0.5, float(nx) + 0.5, nx + 1, endpoint=True) wcs = pywcs.WCS(naxis=2) wcs.wcs.crpix = [0.5 * (nx + 1)] * 2 wcs.wcs.crval = self.parameters["sky_center"].d wcs.wcs.cdelt = [-dtheta, dtheta] wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"] wcs.wcs.cunit = ["deg"] * 2 xx, yy = wcs.wcs_world2pix(self["xsky"].d, self["ysky"].d, 1) H, xedges, yedges = np.histogram2d(xx[mask], yy[mask], bins=[xbins, ybins]) if parallel_capable: H = comm.comm.reduce(H, root=0) if comm.rank == 0: hdu = pyfits.PrimaryHDU(H.T) hdu.header["MTYPE1"] = "EQPOS" hdu.header["MFORM1"] = "RA,DEC" hdu.header["CTYPE1"] = "RA---TAN" hdu.header["CTYPE2"] = "DEC--TAN" hdu.header["CRPIX1"] = 0.5 * (nx + 1) hdu.header["CRPIX2"] = 0.5 * (nx + 1) hdu.header["CRVAL1"] = float(self.parameters["sky_center"][0]) hdu.header["CRVAL2"] = float(self.parameters["sky_center"][1]) hdu.header["CUNIT1"] = "deg" hdu.header["CUNIT2"] = "deg" hdu.header["CDELT1"] = -dtheta hdu.header["CDELT2"] = dtheta hdu.header["EXPOSURE"] = float(self.parameters["exp_time"]) hdu.writeto(imagefile, overwrite=overwrite) comm.barrier()
def project_photons(self, normal, area_new=None, exp_time_new=None, redshift_new=None, dist_new=None, absorb_model=None, sky_center=None, no_shifting=False, north_vector=None, prng=None): 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] area_new : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`, optional New value for the (constant) collecting area of the detector. If units are not specified, is assumed to be in cm**2. exp_time_new : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`, optional The new value for the exposure time. If units are not specified it is assumed to be in seconds. redshift_new : float, optional The new value for the cosmological redshift. dist_new : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`, optional The new value for the angular diameter distance, used for nearby sources. This may be optionally supplied instead of it being determined from the cosmology. If units are not specified, it is assumed to be in Mpc. To use this, the redshift must be zero. absorb_model : :class:`~pyxsim.spectral_models.AbsorptionModel` A model for foreground galactic absorption. sky_center : array-like, optional Center RA, Dec of the events in degrees. 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. prng : :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. Examples -------- >>> L = np.array([0.1,-0.2,0.3]) >>> events = my_photons.project_photons(L, area_new=10000., ... redshift_new=0.05) """ if prng is None: prng = np.random if redshift_new is not None and dist_new is not None: mylog.error("You may specify a new redshift or distance, " + "but not both!") if sky_center is None: sky_center = YTArray([30., 45.], "degree") else: sky_center = YTArray(sky_center, "degree") dx = self.photons["dx"].d if isinstance(normal, string_types): # if on-axis, just use the maximum width of the plane perpendicular # to that axis w = self.parameters["Width"].copy() w["xyz".index(normal)] = 0.0 ax_idx = np.argmax(w) else: # if off-axis, just use the largest width to make sure we get everything ax_idx = np.argmax(self.parameters["Width"]) nx = self.parameters["Dimension"][ax_idx] dx_min = (self.parameters["Width"] / self.parameters["Dimension"])[ax_idx] 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] n_ph = self.photons["NumberOfPhotons"] n_ph_tot = n_ph.sum() parameters = {} zobs0 = self.parameters["FiducialRedshift"] D_A0 = self.parameters["FiducialAngularDiameterDistance"] scale_factor = 1.0 if (exp_time_new is None and area_new is None and redshift_new is None and dist_new is None): my_n_obs = n_ph_tot zobs = zobs0 D_A = D_A0 else: if exp_time_new is None: Tratio = 1. else: exp_time_new = parse_value(exp_time_new, "s") Tratio = exp_time_new / self.parameters["FiducialExposureTime"] if area_new is None: Aratio = 1. else: area_new = parse_value(area_new, "cm**2") Aratio = area_new / self.parameters["FiducialArea"] if redshift_new is None and dist_new is None: Dratio = 1. zobs = zobs0 D_A = D_A0 else: if dist_new is not None: if redshift_new is not None and redshift_new > 0.0: mylog.warning( "Redshift must be zero for nearby sources. Resetting redshift to 0.0." ) zobs = 0.0 D_A = parse_value(dist_new, "Mpc") else: zobs = redshift_new D_A = self.cosmo.angular_diameter_distance( 0.0, zobs).in_units("Mpc") scale_factor = (1. + zobs0) / (1. + zobs) Dratio = D_A0*D_A0*(1.+zobs0)**3 / \ (D_A*D_A*(1.+zobs)**3) fak = Aratio * Tratio * Dratio if fak > 1: raise ValueError( "This combination of requested parameters results in " "%g%% more photons collected than are " % (100. * (fak - 1.)) + "available in the sample. Please reduce the collecting " "area, exposure time, or increase the distance/redshift " "of the object. Alternatively, generate a larger sample " "of photons.") my_n_obs = np.int64(n_ph_tot * fak) Nn = 4294967294 if my_n_obs == n_ph_tot: if my_n_obs <= Nn: idxs = np.arange(my_n_obs, dtype='uint32') else: idxs = np.arange(my_n_obs, dtype='uint64') else: if n_ph_tot <= Nn: idxs = np.arange(n_ph_tot, dtype='uint32') prng.shuffle(idxs) idxs = idxs[:my_n_obs] else: Nc = np.int32(n_ph_tot / Nn) idxs = np.zeros(my_n_obs, dtype=np.uint64) Nup = np.uint32(my_n_obs / Nc) for i in range(Nc + 1): if (i + 1) * Nc < n_ph_tot: idtm = np.arange(i * Nc, (i + 1) * Nc, dtype='uint64') Nupt = Nup else: idtm = np.arange(i * Nc, n_ph_tot, dtype='uint64') Nupt = my_n_obs - i * Nup prng.shuffle(idtm) idxs[i * Nup, i * Nup + Nupt] = idtm[:Nupt] del (idtm) # idxs = prng.permutation(n_ph_tot)[:my_n_obs].astype("int64") obs_cells = np.searchsorted(self.p_bins, idxs, side='right') - 1 delta = dx[obs_cells] if isinstance(normal, string_types): if self.parameters["DataType"] == "cells": xsky = prng.uniform(low=-0.5, high=0.5, size=my_n_obs) ysky = prng.uniform(low=-0.5, high=0.5, size=my_n_obs) elif self.parameters["DataType"] == "particles": xsky = prng.normal(loc=0.0, scale=1.0, size=my_n_obs) ysky = prng.normal(loc=0.0, scale=1.0, size=my_n_obs) xsky *= delta ysky *= delta xsky += self.photons[axes_lookup[normal][0]].d[obs_cells] ysky += self.photons[axes_lookup[normal][1]].d[obs_cells] if not no_shifting: vz = self.photons["v%s" % normal] else: if self.parameters["DataType"] == "cells": x = prng.uniform(low=-0.5, high=0.5, size=my_n_obs) y = prng.uniform(low=-0.5, high=0.5, size=my_n_obs) z = prng.uniform(low=-0.5, high=0.5, size=my_n_obs) elif self.parameters["DataType"] == "particles": x = prng.normal(loc=0.0, scale=1.0, size=my_n_obs) y = prng.normal(loc=0.0, scale=1.0, size=my_n_obs) z = prng.normal(loc=0.0, scale=1.0, size=my_n_obs) if not no_shifting: vz = self.photons["vx"]*z_hat[0] + \ self.photons["vy"]*z_hat[1] + \ self.photons["vz"]*z_hat[2] x *= delta y *= delta z *= delta x += self.photons["x"].d[obs_cells] y += self.photons["y"].d[obs_cells] z += self.photons["z"].d[obs_cells] xsky = x * x_hat[0] + y * x_hat[1] + z * x_hat[2] ysky = x * y_hat[0] + y * y_hat[1] + z * y_hat[2] del (delta) if no_shifting: eobs = self.photons["Energy"][idxs] else: # shift = -vz.in_cgs()/clight # shift = np.sqrt((1.-shift)/(1.+shift)) # eobs = self.photons["Energy"][idxs]*shift[obs_cells] shift = -vz[obs_cells].in_cgs() / clight shift = np.sqrt((1. - shift) / (1. + shift)) eobs = self.photons["Energy"][idxs] eobs *= shift del (shift) eobs *= scale_factor if absorb_model is None: detected = np.ones(eobs.shape, dtype='bool') else: detected = absorb_model.absorb_photons(eobs, prng=prng) events = {} dtheta = YTQuantity(np.rad2deg(dx_min / D_A), "degree") events["xpix"] = xsky[detected] / dx_min.v + 0.5 * (nx + 1) events["ypix"] = ysky[detected] / dx_min.v + 0.5 * (nx + 1) events["eobs"] = eobs[detected] events = comm.par_combine_object(events, datatype="dict", op="cat") num_events = len(events["xpix"]) if comm.rank == 0: mylog.info("Total number of observed photons: %d" % num_events) if exp_time_new is None: parameters["ExposureTime"] = self.parameters[ "FiducialExposureTime"] else: parameters["ExposureTime"] = exp_time_new if area_new is None: parameters["Area"] = self.parameters["FiducialArea"] else: parameters["Area"] = area_new parameters["Redshift"] = zobs parameters["AngularDiameterDistance"] = D_A.in_units("Mpc") parameters["sky_center"] = sky_center parameters["pix_center"] = np.array([0.5 * (nx + 1)] * 2) parameters["dtheta"] = dtheta return EventList(events, parameters)
def from_data_source(cls, data_source, redshift, area, exp_time, source_model, parameters=None, center=None, dist=None, cosmology=None, velocity_fields=None): r""" Initialize a :class:`~pyxsim.photon_list.PhotonList` from a yt data source. The redshift, collecting area, exposure time, and cosmology are stored in the *parameters* dictionary which is passed to the *source_model* function. Parameters ---------- data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer` The data source from which the photons will be generated. redshift : float The cosmological redshift for the photons. area : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`. 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, or :class:`~yt.units.yt_array.YTQuantity`. The exposure time to determine the number of photons. If units are not specified, it is assumed to be in seconds. source_model : :class:`~pyxsim.source_models.SourceModel` A source model used to generate the photons. parameters : dict, optional A dictionary of parameters to be passed for the source model to use, if necessary. 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. dist : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`, optional The angular diameter distance, used for nearby sources. This may be optionally supplied instead of it being determined from the *redshift* and given *cosmology*. If units are not specified, it is assumed to be in Mpc. To use this, the redshift must be set to zero. 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. 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 Examples -------- >>> thermal_model = ThermalSourceModel(apec_model, Zmet=0.3) >>> redshift = 0.05 >>> area = 6000.0 # assumed here in cm**2 >>> time = 2.0e5 # assumed here in seconds >>> sp = ds.sphere("c", (500., "kpc")) >>> my_photons = PhotonList.from_data_source(sp, redshift, area, ... time, thermal_model) """ ds = data_source.ds if parameters is None: parameters = {} if cosmology is None: if hasattr(ds, 'cosmology'): cosmo = ds.cosmology else: cosmo = Cosmology() else: cosmo = cosmology mylog.info( "Cosmology: h = %g, omega_matter = %g, omega_lambda = %g" % (cosmo.hubble_constant, cosmo.omega_matter, cosmo.omega_lambda)) if dist is None: if redshift <= 0.0: msg = "If redshift <= 0.0, you must specify a distance to the source using the 'dist' argument!" mylog.error(msg) raise ValueError(msg) D_A = cosmo.angular_diameter_distance(0.0, redshift).in_units("Mpc") else: D_A = parse_value(dist, "Mpc") if redshift > 0.0: mylog.warning( "Redshift must be zero for nearby sources. Resetting redshift to 0.0." ) redshift = 0.0 if center == "center" or center == "c": parameters["center"] = ds.domain_center elif center == "max" or center == "m": parameters["center"] = ds.find_max("density")[-1] elif iterable(center): if isinstance(center, YTArray): parameters["center"] = center.in_units("code_length") elif isinstance(center, tuple): if center[0] == "min": parameters["center"] = ds.find_min(center[1])[-1] elif center[0] == "max": parameters["center"] = ds.find_max(center[1])[-1] else: raise RuntimeError else: parameters["center"] = ds.arr(center, "code_length") elif center is None: parameters["center"] = data_source.get_field_parameter("center") parameters["FiducialExposureTime"] = parse_value(exp_time, "s") parameters["FiducialArea"] = parse_value(area, "cm**2") parameters["FiducialRedshift"] = redshift parameters["FiducialAngularDiameterDistance"] = D_A parameters["HubbleConstant"] = cosmo.hubble_constant parameters["OmegaMatter"] = cosmo.omega_matter parameters["OmegaLambda"] = cosmo.omega_lambda D_A = parameters["FiducialAngularDiameterDistance"].in_cgs() dist_fac = 1.0 / (4. * np.pi * D_A.value * D_A.value * (1. + redshift)**2) spectral_norm = parameters["FiducialArea"].v * parameters[ "FiducialExposureTime"].v * dist_fac source_model.setup_model(data_source, redshift, spectral_norm) p_fields, v_fields, w_field = determine_fields( ds, source_model.source_type) if velocity_fields is not None: v_fields = velocity_fields if p_fields[0] == ("index", "x"): parameters["DataType"] = "cells" else: parameters["DataType"] = "particles" if hasattr(data_source, "left_edge"): # Region or grid le = data_source.left_edge re = data_source.right_edge elif hasattr(data_source, "radius") and not hasattr(data_source, "height"): # Sphere le = -data_source.radius + data_source.center re = data_source.radius + data_source.center else: # Compute rough boundaries of the object # DOES NOT WORK for objects straddling periodic # boundaries yet if sum(ds.periodicity) > 0: mylog.warning("You are using a region that is not currently " "supported for straddling periodic boundaries. " "Check to make sure that this is not the case.") le = ds.arr(np.zeros(3), "code_length") re = ds.arr(np.zeros(3), "code_length") for i, ax in enumerate(p_fields): le[i], re[i] = data_source.quantities.extrema(ax) dds_min = get_smallest_dds(ds, parameters["DataType"]) le = np.rint((le - ds.domain_left_edge) / dds_min) * dds_min + ds.domain_left_edge re = ds.domain_right_edge - np.rint( (ds.domain_right_edge - re) / dds_min) * dds_min width = re - le parameters["Dimension"] = np.rint(width / dds_min).astype("int") parameters["Width"] = parameters["Dimension"] * dds_min.in_units("kpc") citer = data_source.chunks([], "io") photons = defaultdict(list) for chunk in parallel_objects(citer): chunk_data = source_model(chunk) if chunk_data is not None: number_of_photons, idxs, energies = chunk_data photons["NumberOfPhotons"].append(number_of_photons) photons["Energy"].append(ds.arr(energies, "keV")) photons["x"].append(chunk[p_fields[0]][idxs].in_units("kpc")) photons["y"].append(chunk[p_fields[1]][idxs].in_units("kpc")) photons["z"].append(chunk[p_fields[2]][idxs].in_units("kpc")) photons["vx"].append(chunk[v_fields[0]][idxs].in_units("km/s")) photons["vy"].append(chunk[v_fields[1]][idxs].in_units("km/s")) photons["vz"].append(chunk[v_fields[2]][idxs].in_units("km/s")) if w_field is None: photons["dx"].append(ds.arr(np.zeros(len(idxs)), "kpc")) else: photons["dx"].append(chunk[w_field][idxs].in_units("kpc")) source_model.cleanup_model() concatenate_photons(photons) # Translate photon coordinates to the source center # Fix photon coordinates for regions crossing a periodic boundary dw = ds.domain_width.to("kpc") for i, ax in enumerate("xyz"): if ds.periodicity[i] and len(photons[ax]) > 0: tfl = photons[ax] < le[i].to('kpc') tfr = photons[ax] > re[i].to('kpc') photons[ax][tfl] += dw[i] photons[ax][tfr] -= dw[i] photons[ax] -= parameters["center"][i].in_units("kpc") mylog.info("Finished generating photons.") mylog.info("Number of photons generated: %d" % int(np.sum(photons["NumberOfPhotons"]))) mylog.info("Number of cells with photons: %d" % len(photons["x"])) return cls(photons, parameters, cosmo)
def from_data_source(cls, data_source, redshift, area, exp_time, source_model, point_sources=False, parameters=None, center=None, dist=None, cosmology=None, velocity_fields=None): r""" Initialize a :class:`~pyxsim.photon_list.PhotonList` from a yt data source. The redshift, collecting area, exposure time, and cosmology are stored in the *parameters* dictionary which is passed to the *source_model* function. Parameters ---------- data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer` The data source from which the photons will be generated. 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. source_model : :class:`~pyxsim.source_models.SourceModel` A source model used to generate the photons. point_sources : boolean, optional If True, the photons will be assumed to be generated from the exact positions of the cells or particles and not smeared around within a volume. Default: False parameters : dict, optional A dictionary of parameters to be passed for the source model to use, if necessary. 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. dist : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The angular diameter distance, used for nearby sources. This may be optionally supplied instead of it being determined from the *redshift* and given *cosmology*. If units are not specified, it is assumed to be in kpc. To use this, the redshift must be set to zero. 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. 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 Examples -------- >>> thermal_model = ThermalSourceModel(apec_model, Zmet=0.3) >>> redshift = 0.05 >>> area = 6000.0 # assumed here in cm**2 >>> time = 2.0e5 # assumed here in seconds >>> sp = ds.sphere("c", (500., "kpc")) >>> my_photons = PhotonList.from_data_source(sp, redshift, area, ... time, thermal_model) """ ds = data_source.ds if parameters is None: parameters = {} if cosmology is None: if hasattr(ds, 'cosmology'): cosmo = ds.cosmology else: cosmo = Cosmology() else: cosmo = cosmology if dist is None: if redshift <= 0.0: msg = "If redshift <= 0.0, you must specify a distance to the " \ "source using the 'dist' argument!" mylog.error(msg) raise ValueError(msg) D_A = cosmo.angular_diameter_distance(0.0, redshift).in_units("Mpc") else: D_A = parse_value(dist, "kpc") if redshift > 0.0: mylog.warning("Redshift must be zero for nearby sources. " "Resetting redshift to 0.0.") redshift = 0.0 if isinstance(center, string_types): if center == "center" or center == "c": parameters["center"] = ds.domain_center elif center == "max" or center == "m": parameters["center"] = ds.find_max("density")[-1] elif iterable(center): if isinstance(center, YTArray): parameters["center"] = center.in_units("code_length") elif isinstance(center, tuple): if center[0] == "min": parameters["center"] = ds.find_min(center[1])[-1] elif center[0] == "max": parameters["center"] = ds.find_max(center[1])[-1] else: raise RuntimeError else: parameters["center"] = ds.arr(center, "code_length") elif center is None: if hasattr(data_source, "left_edge"): parameters["center"] = 0.5*(data_source.left_edge+data_source.right_edge) else: parameters["center"] = data_source.get_field_parameter("center") parameters["fid_exp_time"] = parse_value(exp_time, "s") parameters["fid_area"] = parse_value(area, "cm**2") parameters["fid_redshift"] = redshift parameters["fid_d_a"] = D_A parameters["hubble"] = cosmo.hubble_constant parameters["omega_matter"] = cosmo.omega_matter parameters["omega_lambda"] = cosmo.omega_lambda if redshift > 0.0: mylog.info("Cosmology: h = %g, omega_matter = %g, omega_lambda = %g" % (cosmo.hubble_constant, cosmo.omega_matter, cosmo.omega_lambda)) else: mylog.info("Observing local source at distance %s." % D_A) D_A = parameters["fid_d_a"].in_cgs() dist_fac = 1.0/(4.*np.pi*D_A.value*D_A.value*(1.+redshift)**2) spectral_norm = parameters["fid_area"].v*parameters["fid_exp_time"].v*dist_fac source_model.setup_model(data_source, redshift, spectral_norm) p_fields, v_fields, w_field = determine_fields(ds, source_model.source_type, point_sources) if velocity_fields is not None: v_fields = velocity_fields if p_fields[0] == ("index", "x"): parameters["data_type"] = "cells" else: parameters["data_type"] = "particles" citer = data_source.chunks([], "io") photons = defaultdict(list) for chunk in parallel_objects(citer): chunk_data = source_model(chunk) if chunk_data is not None: ncells, number_of_photons, idxs, energies = chunk_data photons["num_photons"].append(number_of_photons) photons["energy"].append(energies) photons["pos"].append(np.array([chunk[p_fields[0]].d[idxs], chunk[p_fields[1]].d[idxs], chunk[p_fields[2]].d[idxs]])) photons["vel"].append(np.array([chunk[v_fields[0]].d[idxs], chunk[v_fields[1]].d[idxs], chunk[v_fields[2]].d[idxs]])) if w_field is None: photons["dx"].append(np.zeros(ncells)) else: photons["dx"].append(chunk[w_field].d[idxs]) source_model.cleanup_model() photon_units = {"pos": ds.field_info[p_fields[0]].units, "vel": ds.field_info[v_fields[0]].units, "energy": "keV"} if w_field is None: photon_units["dx"] = "kpc" else: photon_units["dx"] = ds.field_info[w_field].units concatenate_photons(ds, photons, photon_units) c = parameters["center"].to("kpc") if sum(ds.periodicity) > 0: # Fix photon coordinates for regions crossing a periodic boundary dw = ds.domain_width.to("kpc") le, re = find_object_bounds(data_source) for i in range(3): if ds.periodicity[i] and photons["pos"].shape[0] > 0: tfl = photons["pos"][:,i] < le[i] tfr = photons["pos"][:,i] > re[i] photons["pos"][tfl,i] += dw[i] photons["pos"][tfr,i] -= dw[i] # Re-center all coordinates if photons["pos"].shape[0] > 0: photons["pos"] -= c mylog.info("Finished generating photons.") mylog.info("Number of photons generated: %d" % int(np.sum(photons["num_photons"]))) mylog.info("Number of cells with photons: %d" % photons["dx"].size) return cls(photons, parameters, cosmo)
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 from_data_source(cls, data_source, redshift, area, exp_time, source_model, point_sources=False, parameters=None, center=None, dist=None, cosmology=None, velocity_fields=None): r""" Initialize a :class:`~pyxsim.photon_list.PhotonList` from a yt data source. The redshift, collecting area, exposure time, and cosmology are stored in the *parameters* dictionary which is passed to the *source_model* function. Parameters ---------- data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer` The data source from which the photons will be generated. 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. source_model : :class:`~pyxsim.source_models.SourceModel` A source model used to generate the photons. point_sources : boolean, optional If True, the photons will be assumed to be generated from the exact positions of the cells or particles and not smeared around within a volume. Default: False parameters : dict, optional A dictionary of parameters to be passed for the source model to use, if necessary. 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. dist : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The angular diameter distance, used for nearby sources. This may be optionally supplied instead of it being determined from the *redshift* and given *cosmology*. If units are not specified, it is assumed to be in kpc. To use this, the redshift must be set to zero. 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. 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 Examples -------- >>> thermal_model = ThermalSourceModel(apec_model, Zmet=0.3) >>> redshift = 0.05 >>> area = 6000.0 # assumed here in cm**2 >>> time = 2.0e5 # assumed here in seconds >>> sp = ds.sphere("c", (500., "kpc")) >>> my_photons = PhotonList.from_data_source(sp, redshift, area, ... time, thermal_model) """ ds = data_source.ds if parameters is None: parameters = {} if cosmology is None: if hasattr(ds, 'cosmology'): cosmo = ds.cosmology else: cosmo = Cosmology() else: cosmo = cosmology if dist is None: if redshift <= 0.0: msg = "If redshift <= 0.0, you must specify a distance to the " \ "source using the 'dist' argument!" mylog.error(msg) raise ValueError(msg) D_A = cosmo.angular_diameter_distance(0.0, redshift).in_units("Mpc") else: D_A = parse_value(dist, "kpc") if redshift > 0.0: mylog.warning("Redshift must be zero for nearby sources. " "Resetting redshift to 0.0.") redshift = 0.0 if isinstance(center, string_types): if center == "center" or center == "c": parameters["center"] = ds.domain_center elif center == "max" or center == "m": parameters["center"] = ds.find_max("density")[-1] elif iterable(center): if isinstance(center, YTArray): parameters["center"] = center.in_units("code_length") elif isinstance(center, tuple): if center[0] == "min": parameters["center"] = ds.find_min(center[1])[-1] elif center[0] == "max": parameters["center"] = ds.find_max(center[1])[-1] else: raise RuntimeError else: parameters["center"] = ds.arr(center, "code_length") elif center is None: if hasattr(data_source, "left_edge"): parameters["center"] = 0.5 * (data_source.left_edge + data_source.right_edge) else: parameters["center"] = data_source.get_field_parameter( "center") parameters["fid_exp_time"] = parse_value(exp_time, "s") parameters["fid_area"] = parse_value(area, "cm**2") parameters["fid_redshift"] = redshift parameters["fid_d_a"] = D_A parameters["hubble"] = cosmo.hubble_constant parameters["omega_matter"] = cosmo.omega_matter parameters["omega_lambda"] = cosmo.omega_lambda if redshift > 0.0: mylog.info( "Cosmology: h = %g, omega_matter = %g, omega_lambda = %g" % (cosmo.hubble_constant, cosmo.omega_matter, cosmo.omega_lambda)) else: mylog.info("Observing local source at distance %s." % D_A) D_A = parameters["fid_d_a"].in_cgs() dist_fac = 1.0 / (4. * np.pi * D_A.value * D_A.value * (1. + redshift)**2) spectral_norm = parameters["fid_area"].v * parameters[ "fid_exp_time"].v * dist_fac source_model.setup_model(data_source, redshift, spectral_norm) p_fields, v_fields, w_field = determine_fields( ds, source_model.source_type, point_sources) if velocity_fields is not None: v_fields = velocity_fields if p_fields[0] == ("index", "x"): parameters["data_type"] = "cells" else: parameters["data_type"] = "particles" citer = data_source.chunks([], "io") photons = defaultdict(list) for chunk in parallel_objects(citer): chunk_data = source_model(chunk) if chunk_data is not None: ncells, number_of_photons, idxs, energies = chunk_data photons["num_photons"].append(number_of_photons) photons["energy"].append(energies) photons["pos"].append( np.array([ chunk[p_fields[0]].d[idxs], chunk[p_fields[1]].d[idxs], chunk[p_fields[2]].d[idxs] ])) photons["vel"].append( np.array([ chunk[v_fields[0]].d[idxs], chunk[v_fields[1]].d[idxs], chunk[v_fields[2]].d[idxs] ])) if w_field is None: photons["dx"].append(np.zeros(ncells)) else: photons["dx"].append(chunk[w_field].d[idxs]) source_model.cleanup_model() photon_units = { "pos": ds.field_info[p_fields[0]].units, "vel": ds.field_info[v_fields[0]].units, "energy": "keV" } if w_field is None: photon_units["dx"] = "kpc" else: photon_units["dx"] = ds.field_info[w_field].units concatenate_photons(ds, photons, photon_units) c = parameters["center"].to("kpc") if sum(ds.periodicity) > 0: # Fix photon coordinates for regions crossing a periodic boundary dw = ds.domain_width.to("kpc") le, re = find_object_bounds(data_source) for i in range(3): if ds.periodicity[i] and photons["pos"].shape[0] > 0: tfl = photons["pos"][:, i] < le[i] tfr = photons["pos"][:, i] > re[i] photons["pos"][tfl, i] += dw[i] photons["pos"][tfr, i] -= dw[i] # Re-center all coordinates if photons["pos"].shape[0] > 0: photons["pos"] -= c mylog.info("Finished generating photons.") mylog.info("Number of photons generated: %d" % int(np.sum(photons["num_photons"]))) mylog.info("Number of cells with photons: %d" % photons["dx"].size) return cls(photons, parameters, cosmo)
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 write_fits_file(self, fitsfile, fov, nx, overwrite=False): """ Write events to a FITS binary table file. The result is a standard "event file" which can be processed by standard X-ray analysis tools. Parameters ---------- fitsfile : string The name of the event file to write. 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. nx : integer The resolution of the image (number of pixels on a side). overwrite : boolean, optional Set to True to overwrite a previous file. """ from astropy.time import Time, TimeDelta events = communicate_events(self.events) fov = parse_value(fov, "arcmin") if comm.rank == 0: exp_time = float(self.parameters["exp_time"]) t_begin = Time.now() dt = TimeDelta(exp_time, format='sec') t_end = t_begin + dt dtheta = fov.to("deg").v / nx wcs = pywcs.WCS(naxis=2) wcs.wcs.crpix = [0.5*(nx+1)]*2 wcs.wcs.crval = self.parameters["sky_center"].d wcs.wcs.cdelt = [-dtheta, dtheta] wcs.wcs.ctype = ["RA---TAN", "DEC--TAN"] wcs.wcs.cunit = ["deg"] * 2 xx, yy = wcs.wcs_world2pix(self["xsky"].d, self["ysky"].d, 1) keepx = np.logical_and(xx >= 0.5, xx <= float(nx)+0.5) keepy = np.logical_and(yy >= 0.5, yy <= float(nx)+0.5) keep = np.logical_and(keepx, keepy) n_events = keep.sum() mylog.info("Threw out %d events because " % (xx.size-n_events) + "they fell outside the field of view.") col_e = pyfits.Column(name='ENERGY', format='E', unit='eV', array=events["eobs"].in_units("eV").d[keep]) col_x = pyfits.Column(name='X', format='D', unit='pixel', array=xx[keep]) col_y = pyfits.Column(name='Y', format='D', unit='pixel', array=yy[keep]) cols = [col_e, col_x, col_y] if "channel_type" in self.parameters: chantype = self.parameters["channel_type"] if chantype == "pha": cunit = "adu" elif chantype == "pi": cunit = "Chan" col_ch = pyfits.Column(name=chantype.upper(), format='1J', unit=cunit, array=events[chantype][keep]) cols.append(col_ch) time = np.random.uniform(size=n_events, low=0.0, high=float(self.parameters["exp_time"])) col_t = pyfits.Column(name="TIME", format='1D', unit='s', array=time) cols.append(col_t) coldefs = pyfits.ColDefs(cols) tbhdu = pyfits.BinTableHDU.from_columns(coldefs) tbhdu.name = "EVENTS" tbhdu.header["MTYPE1"] = "sky" tbhdu.header["MFORM1"] = "x,y" tbhdu.header["MTYPE2"] = "EQPOS" tbhdu.header["MFORM2"] = "RA,DEC" tbhdu.header["TCTYP2"] = "RA---TAN" tbhdu.header["TCTYP3"] = "DEC--TAN" tbhdu.header["TCRVL2"] = float(self.parameters["sky_center"][0]) tbhdu.header["TCRVL3"] = float(self.parameters["sky_center"][1]) tbhdu.header["TCDLT2"] = -dtheta tbhdu.header["TCDLT3"] = dtheta tbhdu.header["TCRPX2"] = 0.5*(nx+1) tbhdu.header["TCRPX3"] = 0.5*(nx+1) tbhdu.header["TLMIN2"] = 0.5 tbhdu.header["TLMIN3"] = 0.5 tbhdu.header["TLMAX2"] = float(nx)+0.5 tbhdu.header["TLMAX3"] = float(nx)+0.5 if "channel_type" in self.parameters: rmf = RedistributionMatrixFile(self.parameters["rmf"]) tbhdu.header["TLMIN4"] = rmf.cmin tbhdu.header["TLMAX4"] = rmf.cmax tbhdu.header["RESPFILE"] = os.path.split(self.parameters["rmf"])[-1] tbhdu.header["PHA_BINS"] = rmf.n_ch tbhdu.header["ANCRFILE"] = os.path.split(self.parameters["arf"])[-1] tbhdu.header["CHANTYPE"] = self.parameters["channel_type"] tbhdu.header["MISSION"] = self.parameters["mission"] tbhdu.header["TELESCOP"] = self.parameters["telescope"] tbhdu.header["INSTRUME"] = self.parameters["instrument"] tbhdu.header["EXPOSURE"] = exp_time tbhdu.header["TSTART"] = 0.0 tbhdu.header["TSTOP"] = exp_time tbhdu.header["AREA"] = float(self.parameters["area"]) tbhdu.header["HDUVERS"] = "1.1.0" tbhdu.header["RADECSYS"] = "FK5" tbhdu.header["EQUINOX"] = 2000.0 tbhdu.header["HDUCLASS"] = "OGIP" tbhdu.header["HDUCLAS1"] = "EVENTS" tbhdu.header["HDUCLAS2"] = "ACCEPTED" tbhdu.header["DATE"] = t_begin.tt.isot tbhdu.header["DATE-OBS"] = t_begin.tt.isot tbhdu.header["DATE-END"] = t_end.tt.isot hdulist = [pyfits.PrimaryHDU(), tbhdu] if "channel_type" in self.parameters: start = pyfits.Column(name='START', format='1D', unit='s', array=np.array([0.0])) stop = pyfits.Column(name='STOP', format='1D', unit='s', array=np.array([exp_time])) tbhdu_gti = pyfits.BinTableHDU.from_columns([start,stop]) tbhdu_gti.name = "STDGTI" tbhdu_gti.header["TSTART"] = 0.0 tbhdu_gti.header["TSTOP"] = exp_time tbhdu_gti.header["HDUCLASS"] = "OGIP" tbhdu_gti.header["HDUCLAS1"] = "GTI" tbhdu_gti.header["HDUCLAS2"] = "STANDARD" tbhdu_gti.header["RADECSYS"] = "FK5" tbhdu_gti.header["EQUINOX"] = 2000.0 tbhdu_gti.header["DATE"] = t_begin.tt.isot tbhdu_gti.header["DATE-OBS"] = t_begin.tt.isot tbhdu_gti.header["DATE-END"] = t_end.tt.isot hdulist.append(tbhdu_gti) pyfits.HDUList(hdulist).writeto(fitsfile, overwrite=overwrite) comm.barrier()
def write_fits_image(self, imagefile, fov, nx, emin=None, emax=None, overwrite=False): r""" Generate a image by binning X-ray counts and write it to a FITS file. Parameters ---------- imagefile : string The name of the image file to write. fov : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity` The field of view of the image. If units are not provided, they are assumed to be in arcminutes. nx : integer The resolution of the image (number of pixels on a side). emin : float, optional The minimum energy of the photons to put in the image, in keV. emax : float, optional The maximum energy of the photons to put in the image, in keV. overwrite : boolean, optional Set to True to overwrite a previous file. """ fov = parse_value(fov, "arcmin") if emin is None: mask_emin = np.ones(self.num_events, dtype='bool') else: mask_emin = self["eobs"].d > emin if emax is None: mask_emax = np.ones(self.num_events, dtype='bool') else: mask_emax = self["eobs"].d < emax mask = np.logical_and(mask_emin, mask_emax) dtheta = fov.to("deg").v/nx xbins = np.linspace(0.5, float(nx)+0.5, nx+1, endpoint=True) ybins = np.linspace(0.5, float(nx)+0.5, nx+1, endpoint=True) wcs = pywcs.WCS(naxis=2) wcs.wcs.crpix = [0.5*(nx+1)]*2 wcs.wcs.crval = self.parameters["sky_center"].d wcs.wcs.cdelt = [-dtheta, dtheta] wcs.wcs.ctype = ["RA---TAN","DEC--TAN"] wcs.wcs.cunit = ["deg"]*2 xx, yy = wcs.wcs_world2pix(self["xsky"].d, self["ysky"].d, 1) H, xedges, yedges = np.histogram2d(xx[mask], yy[mask], bins=[xbins, ybins]) if parallel_capable: H = comm.comm.reduce(H, root=0) if comm.rank == 0: hdu = pyfits.PrimaryHDU(H.T) hdu.header["MTYPE1"] = "EQPOS" hdu.header["MFORM1"] = "RA,DEC" hdu.header["CTYPE1"] = "RA---TAN" hdu.header["CTYPE2"] = "DEC--TAN" hdu.header["CRPIX1"] = 0.5*(nx+1) hdu.header["CRPIX2"] = 0.5*(nx+1) hdu.header["CRVAL1"] = float(self.parameters["sky_center"][0]) hdu.header["CRVAL2"] = float(self.parameters["sky_center"][1]) hdu.header["CUNIT1"] = "deg" hdu.header["CUNIT2"] = "deg" hdu.header["CDELT1"] = -dtheta hdu.header["CDELT2"] = dtheta hdu.header["EXPOSURE"] = float(self.parameters["exp_time"]) hdu.writeto(imagefile, overwrite=overwrite) comm.barrier()
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)