def generate_fluxes(exp_time, area, fov, prng): from soxs.data import cdf_fluxes, cdf_gal, cdf_agn exp_time = parse_value(exp_time, "s") area = parse_value(area, "cm**2") fov = parse_value(fov, "arcmin") logf = np.log10(cdf_fluxes) n_gal = np.rint(cdf_gal[-1]) n_agn = np.rint(cdf_agn[-1]) F_gal = cdf_gal / cdf_gal[-1] F_agn = cdf_agn / cdf_agn[-1] f_gal = InterpolatedUnivariateSpline(F_gal, logf) f_agn = InterpolatedUnivariateSpline(F_agn, logf) eph_mean_erg = 1.0*erg_per_keV S_min_obs = eph_mean_erg/(exp_time*area) mylog.debug("Flux of %g erg/cm^2/s gives roughly " "one photon during exposure." % S_min_obs) fov_area = fov**2 n_gal = int(n_gal*fov_area/3600.0) n_agn = int(n_agn*fov_area/3600.0) mylog.debug("%d AGN, %d galaxies in the FOV." % (n_agn, n_gal)) randvec1 = prng.uniform(size=n_agn) agn_fluxes = 10**f_agn(randvec1) randvec2 = prng.uniform(size=n_gal) gal_fluxes = 10**f_gal(randvec2) return agn_fluxes, gal_fluxes
def generate_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_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(fov, sky_center, prng=prng) num_sources = fluxes.size else: mylog.info(f"Reading in point-source properties from {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(f"Generating spectra from {num_sources} 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( f"{all_nph} photons remain after foreground galactic absorption.") all_flux = np.sum(all_energies) * erg_per_keV / (exp_time * area) output_events = { "ra": all_ra, "dec": all_dec, "energy": all_energies, "flux": all_flux } return output_events
def add_instrument_to_registry(inst_spec): """ Add an instrument specification to the registry, contained in either a dictionary or a JSON file. The *inst_spec* must have the structure as shown below. The order is not important. If you use a JSON file, the structure is the same, but the file cannot include comments, and use "null" instead of "None", and "true" or "false" instead of "True" or "False". For the "chips" entry, "None" means no chips and the detector field of view is a single square. If you want to have multiple chips, they must be specified in a format described in the online documentation. >>> { ... "name": "lynx_hdxi", # The short name of the instrument ... "arf": "xrs_hdxi_3x10.arf", # The file containing the ARF ... "rmf": "xrs_hdxi.rmf", # The file containing the RMF ... "bkgnd": ["lynx_hdxi_particle_bkgnd.pha", 1.0], # The name of the particle background file and the area of extraction ... "fov": 20.0, # The field of view in arcminutes ... "focal_length": 10.0, # The focal length in meters ... "num_pixels": 4096, # The number of pixels on a side in the FOV ... "dither": True, # Whether or not to dither the instrument ... "psf": ["image", "chandra_psf.fits", 6], # The type of PSF and associated parameters ... "chips": [["Box", 0, 0, 4096, 4096]], # The specification for the chips ... "aimpt_coords": [0.0, 0.0], # The detector coordinates of the aimpoint ... "imaging": True # Whether or not this is a imaging instrument ... "grating": False # Whether or not this is a grating instrument ... } """ if isinstance(inst_spec, dict): inst = inst_spec elif os.path.exists(inst_spec): with open(inst_spec, "r") as f: inst = json.load(f) name = inst["name"] if name in instrument_registry: raise KeyError(f"The instrument with name {name} is already in the " f"registry! Assign a different name!") # Catch older JSON files which don't distinguish between imagings # and non-imagings if "imaging" not in inst: mylog.warning("Instrument specifications must now include an 'imaging' " "item, which determines whether or not this instrument " "specification supports imaging. Default is True.") inst["imaging"] = True if "grating" not in inst: mylog.warning("Instrument specifications must now include an 'grating' " "item, which determines whether or not this instrument " "specification corresponds to a gratings instrument. " "Default is False.") inst["grating"] = False if inst["grating"] and inst["imaging"]: raise RuntimeError("Currently, gratings instrument specifications cannot " "have 'imaging' == True!") if inst['imaging']: default_set = {"name", "arf", "rmf", "bkgnd", "fov", "chips", "aimpt_coords", "focal_length", "num_pixels", "dither", "psf", "imaging", "grating"} else: default_set = {"name", "arf", "rmf", "bkgnd", "focal_length", "imaging", "grating"} my_keys = set(inst.keys()) if my_keys != default_set: missing = default_set.difference(my_keys) raise RuntimeError(f"One or more items is missing from the instrument " f"specification!\nItems needed: {missing}") instrument_registry[name] = inst mylog.debug(f"The {name} instrument specification has been added " f"to the instrument registry.") return name
def add_instrument_to_registry(inst_spec): """ Add an instrument specification to the registry, contained in either a dictionary or a JSON file. The *inst_spec* must have the structure as shown below. The order is not important. If you use a JSON file, the structure is the same, but the file cannot include comments, and use "null" instead of "None", and "true" or "false" instead of "True" or "False". For the "chips" entry, "None" means no chips and the detector field of view is a single square. If you want to have multiple chips, they must be specified in a format described in the online documentation. >>> { ... "name": "lynx_hdxi", # The short name of the instrument ... "arf": "xrs_hdxi_3x10.arf", # The file containing the ARF ... "rmf": "xrs_hdxi.rmf", # The file containing the RMF ... "bkgnd": "acisi", # The name of the particle background ... "fov": 20.0, # The field of view in arcminutes ... "focal_length": 10.0, # The focal length in meters ... "num_pixels": 4096, # The number of pixels on a side in the FOV ... "dither": True, # Whether or not to dither the instrument ... "psf": ["gaussian", 0.5], # The type of PSF and its HPD ... "chips": None, # The specification for the chips ... "aimpt_coords": [0.0, 0.0], # The detector coordinates of the aimpoint ... "imaging": True # Whether or not this is a imaging instrument ... "grating": False # Whether or not this is a grating instrument ... } """ if isinstance(inst_spec, dict): inst = inst_spec elif os.path.exists(inst_spec): f = open(inst_spec, "r") inst = json.load(f) f.close() name = inst["name"] if name in instrument_registry: raise KeyError( "The instrument with name %s is already in the registry! Assign a different name!" % name) # Catch older JSON files which don't distinguish between imagings and non-imagings if "imaging" not in inst: mylog.warning( "Instrument specifications must now include an 'imaging' item, which " "determines whether or not this instrument specification supports " "imaging. Default is True.") inst["imaging"] = True if "grating" not in inst: mylog.warning( "Instrument specifications must now include an 'grating' item, which " "determines whether or not this instrument specification corresponds " "to a gratings instrument. Default is False.") inst["grating"] = False if inst["grating"] and inst["imaging"]: raise RuntimeError( "Currently, gratings instrument specifications cannot have " "'imaging' == True!") if inst['imaging']: # Catch older JSON files without chip definitions if "chips" not in inst: mylog.warning( "Instrument specifications must now include a 'chips' item, which details " "the layout of the chips if there are more that one. Assuming None for " "one chip that covers the entire field of view.") inst["chips"] = None # Catch older JSON files without aimpoint coordinates if "aimpt_coords" not in inst: mylog.warning( "Instrument specifications must now include a 'aimpt_coords' item, which " "details the position in detector coordinates of the nominal aimpoint. " "Assuming [0.0, 0.0].") inst["aimpt_coords"] = [0.0, 0.0] default_set = { "name", "arf", "rmf", "bkgnd", "fov", "chips", "aimpt_coords", "focal_length", "num_pixels", "dither", "psf", "imaging", "grating" } else: default_set = { "name", "arf", "rmf", "bkgnd", "focal_length", "imaging", "grating" } my_keys = set(inst.keys()) # Don't check things we don't need if "dep_name" in my_keys: my_keys.remove("dep_name") if my_keys != default_set: missing = default_set.difference(my_keys) raise RuntimeError( "One or more items is missing from the instrument specification!\n" "Items needed: %s" % missing) instrument_registry[name] = inst mylog.debug( "The %s instrument specification has been added to the instrument registry." % name) return name
def add_instrument_to_registry(inst_spec): """ Add an instrument specification to the registry, contained in either a dictionary or a JSON file. The *inst_spec* must have the structure as shown below. The order is not important. If you use a JSON file, the structure is the same, but the file cannot include comments, and use "null" instead of "None", and "true" or "false" instead of "True" or "False". For the "chips" entry, "None" means no chips and the detector field of view is a single square. If you want to have multiple chips, they must be specified in a format described in the online documentation. >>> { ... "name": "lynx_hdxi", # The short name of the instrument ... "arf": "xrs_hdxi_3x10.arf", # The file containing the ARF ... "rmf": "xrs_hdxi.rmf", # The file containing the RMF ... "bkgnd": "acisi", # The name of the particle background ... "fov": 20.0, # The field of view in arcminutes ... "focal_length": 10.0, # The focal length in meters ... "num_pixels": 4096, # The number of pixels on a side in the FOV ... "dither": True, # Whether or not to dither the instrument ... "psf": ["gaussian", 0.5], # The type of PSF and its HPD ... "chips": None, # The specification for the chips ... "aimpt_coords": [0.0, 0.0], # The detector coordinates of the aimpoint ... "imaging": True # Whether or not this is a imaging instrument ... "grating": False # Whether or not this is a grating instrument ... } """ if isinstance(inst_spec, dict): inst = inst_spec elif os.path.exists(inst_spec): f = open(inst_spec, "r") inst = json.load(f) f.close() name = inst["name"] if name in instrument_registry: raise KeyError("The instrument with name %s is already in the registry! Assign a different name!" % name) # Catch older JSON files which don't distinguish between imagings and non-imagings if "imaging" not in inst: mylog.warning("Instrument specifications must now include an 'imaging' item, which " "determines whether or not this instrument specification supports " "imaging. Default is True.") inst["imaging"] = True if "grating" not in inst: mylog.warning("Instrument specifications must now include an 'grating' item, which " "determines whether or not this instrument specification corresponds " "to a gratings instrument. Default is False.") inst["grating"] = False if inst["grating"] and inst["imaging"]: raise RuntimeError("Currently, gratings instrument specifications cannot have " "'imaging' == True!") if inst['imaging']: # Catch older JSON files without chip definitions if "chips" not in inst: mylog.warning("Instrument specifications must now include a 'chips' item, which details " "the layout of the chips if there are more that one. Assuming None for " "one chip that covers the entire field of view.") inst["chips"] = None # Catch older JSON files without aimpoint coordinates if "aimpt_coords" not in inst: mylog.warning("Instrument specifications must now include a 'aimpt_coords' item, which " "details the position in detector coordinates of the nominal aimpoint. " "Assuming [0.0, 0.0].") inst["aimpt_coords"] = [0.0, 0.0] default_set = {"name", "arf", "rmf", "bkgnd", "fov", "chips", "aimpt_coords", "focal_length", "num_pixels", "dither", "psf", "imaging", "grating"} else: default_set = {"name", "arf", "rmf", "bkgnd", "focal_length", "imaging", "grating"} my_keys = set(inst.keys()) # Don't check things we don't need my_keys.remove("dep_name") if my_keys != default_set: missing = default_set.difference(my_keys) raise RuntimeError("One or more items is missing from the instrument specification!\n" "Items needed: %s" % missing) instrument_registry[name] = inst mylog.debug("The %s instrument specification has been added to the instrument registry." % name) return name
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