def generate_events(input_events, exp_time, instrument, sky_center, no_dither=False, dither_params=None, roll_angle=0.0, subpixel_res=False, prng=None): """ Take unconvolved events and convolve them with instrumental responses. This function does the following: 1. Determines which events are observed using the ARF 2. Pixelizes the events, applying PSF effects and dithering 3. Determines energy channels using the RMF This function is not meant to be called by the end-user but is used by the :func:`~soxs.instrument.instrument_simulator` function. Parameters ---------- input_events : string, dict, or None The unconvolved events to be used as input. Can be one of the following: 1. The name of a SIMPUT catalog file. 2. A Python dictionary containing the following items: "ra": A NumPy array of right ascension values in degrees. "dec": A NumPy array of declination values in degrees. "energy": A NumPy array of energy values in keV. "flux": The flux of the entire source, in units of erg/cm**2/s. out_file : string The name of the event file to be written. exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time to use, in seconds. instrument : string The name of the instrument to use, which picks an instrument specification from the instrument registry. sky_center : array, tuple, or list The center RA, Dec coordinates of the observation, in degrees. no_dither : boolean, optional If True, turn off dithering entirely. Default: False dither_params : array-like of floats, optional The parameters to use to control the size and period of the dither pattern. The first two numbers are the dither amplitude in x and y detector coordinates in arcseconds, and the second two numbers are the dither period in x and y detector coordinates in seconds. Default: [8.0, 8.0, 1000.0, 707.0]. roll_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional The roll angle of the observation in degrees. Default: 0.0 subpixel_res: boolean, optional If True, event positions are not randomized within the pixels within which they are detected. Default: False prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. """ import pyregion._region_filter as rfilter exp_time = parse_value(exp_time, "s") roll_angle = parse_value(roll_angle, "deg") prng = parse_prng(prng) if isinstance(input_events, dict): parameters = {} for key in ["flux", "emin", "emax", "sources"]: parameters[key] = input_events[key] event_list = [] for i in range(len(parameters["flux"])): edict = {} for key in ["ra", "dec", "energy"]: edict[key] = input_events[key][i] event_list.append(edict) elif isinstance(input_events, string_types): # Assume this is a SIMPUT catalog event_list, parameters = read_simput_catalog(input_events) try: instrument_spec = instrument_registry[instrument] except KeyError: raise KeyError("Instrument %s is not in the instrument registry!" % instrument) if not instrument_spec["imaging"]: raise RuntimeError("Instrument '%s' is not " % instrument_spec["name"] + "designed for imaging observations!") arf_file = get_response_path(instrument_spec["arf"]) rmf_file = get_response_path(instrument_spec["rmf"]) arf = AuxiliaryResponseFile(arf_file) rmf = RedistributionMatrixFile(rmf_file) nx = instrument_spec["num_pixels"] plate_scale = instrument_spec["fov"]/nx/60. # arcmin to deg plate_scale_arcsec = plate_scale * 3600.0 if not instrument_spec["dither"]: dither_on = False else: dither_on = not no_dither if dither_params is None: dither_params = [8.0, 8.0, 1000.0, 707.0] dither_dict = {"x_amp": dither_params[0], "y_amp": dither_params[1], "x_period": dither_params[2], "y_period": dither_params[3], "dither_on": dither_on, "plate_scale": plate_scale_arcsec} event_params = {} event_params["exposure_time"] = exp_time event_params["arf"] = arf.filename event_params["sky_center"] = sky_center event_params["pix_center"] = np.array([0.5*(2*nx+1)]*2) event_params["num_pixels"] = nx event_params["plate_scale"] = plate_scale event_params["rmf"] = rmf.filename event_params["channel_type"] = rmf.header["CHANTYPE"] event_params["telescope"] = rmf.header["TELESCOP"] event_params["instrument"] = instrument_spec['name'] event_params["mission"] = rmf.header.get("MISSION", "") event_params["nchan"] = rmf.n_ch event_params["roll_angle"] = roll_angle event_params["fov"] = instrument_spec["fov"] event_params["chan_lim"] = [rmf.cmin, rmf.cmax] event_params["chips"] = instrument_spec["chips"] event_params["dither_params"] = dither_dict event_params["aimpt_coords"] = instrument_spec["aimpt_coords"] w = pywcs.WCS(naxis=2) w.wcs.crval = event_params["sky_center"] w.wcs.crpix = event_params["pix_center"] w.wcs.cdelt = [-plate_scale, plate_scale] w.wcs.ctype = ["RA---TAN","DEC--TAN"] w.wcs.cunit = ["deg"]*2 rot_mat = get_rot_mat(roll_angle) all_events = defaultdict(list) for i, evts in enumerate(event_list): mylog.info("Detecting events from source %s." % parameters["sources"][i]) # Step 1: Use ARF to determine which photons are observed mylog.info("Applying energy-dependent effective area from %s." % os.path.split(arf.filename)[-1]) refband = [parameters["emin"][i], parameters["emax"][i]] events = arf.detect_events(evts, exp_time, parameters["flux"][i], refband, prng=prng) n_evt = events["energy"].size if n_evt == 0: mylog.warning("No events were observed for this source!!!") else: # Step 2: Assign pixel coordinates to events. Apply dithering and # PSF. Clip events that don't fall within the detection region. mylog.info("Pixeling events.") # Convert RA, Dec to pixel coordinates xpix, ypix = w.wcs_world2pix(events["ra"], events["dec"], 1) xpix -= event_params["pix_center"][0] ypix -= event_params["pix_center"][1] events.pop("ra") events.pop("dec") n_evt = xpix.size # Rotate physical coordinates to detector coordinates det = np.dot(rot_mat, np.array([xpix, ypix])) detx = det[0,:] + event_params["aimpt_coords"][0] dety = det[1,:] + event_params["aimpt_coords"][1] # Add times to events events['time'] = prng.uniform(size=n_evt, low=0.0, high=event_params["exposure_time"]) # Apply dithering x_offset, y_offset = perform_dither(events["time"], dither_dict) detx -= x_offset dety -= y_offset # PSF scattering of detector coordinates if instrument_spec["psf"] is not None: psf_type, psf_spec = instrument_spec["psf"] if psf_type == "gaussian": sigma = psf_spec/sigma_to_fwhm/plate_scale_arcsec detx += prng.normal(loc=0.0, scale=sigma, size=n_evt) dety += prng.normal(loc=0.0, scale=sigma, size=n_evt) else: raise NotImplementedError("PSF type %s not implemented!" % psf_type) # Convert detector coordinates to chip coordinates. # Throw out events that don't fall on any chip. cx = np.trunc(detx)+0.5*np.sign(detx) cy = np.trunc(dety)+0.5*np.sign(dety) if event_params["chips"] is None: events["chip_id"] = np.zeros(n_evt, dtype='int') keepx = np.logical_and(cx >= -0.5*nx, cx <= 0.5*nx) keepy = np.logical_and(cy >= -0.5*nx, cy <= 0.5*nx) keep = np.logical_and(keepx, keepy) else: events["chip_id"] = -np.ones(n_evt, dtype='int') for i, chip in enumerate(event_params["chips"]): thisc = np.ones(n_evt, dtype='bool') rtype = chip[0] args = chip[1:] r = getattr(rfilter, rtype)(*args) inside = r.inside(cx, cy) thisc = np.logical_and(thisc, inside) events["chip_id"][thisc] = i keep = events["chip_id"] > -1 mylog.info("%d events were rejected because " % (n_evt-keep.sum()) + "they do not fall on any CCD.") n_evt = keep.sum() if n_evt == 0: mylog.warning("No events are within the field of view for this source!!!") else: # Keep only those events which fall on a chip for key in events: events[key] = events[key][keep] # Convert chip coordinates back to detector coordinates, unless the # user has specified that they want subpixel resolution if subpixel_res: events["detx"] = detx[keep] events["dety"] = dety[keep] else: events["detx"] = cx[keep] + prng.uniform(low=-0.5, high=0.5, size=n_evt) events["dety"] = cy[keep] + prng.uniform(low=-0.5, high=0.5, size=n_evt) # Convert detector coordinates back to pixel coordinates by # adding the dither offsets back in and applying the rotation # matrix again det = np.array([events["detx"] + x_offset[keep] - event_params["aimpt_coords"][0], events["dety"] + y_offset[keep] - event_params["aimpt_coords"][1]]) pix = np.dot(rot_mat.T, det) events["xpix"] = pix[0,:] + event_params['pix_center'][0] events["ypix"] = pix[1,:] + event_params['pix_center'][1] if n_evt > 0: for key in events: all_events[key] = np.concatenate([all_events[key], events[key]]) if len(all_events["energy"]) == 0: mylog.warning("No events from any of the sources in the catalog were detected!") for key in ["xpix", "ypix", "detx", "dety", "time", "chip_id", event_params["channel_type"]]: all_events[key] = np.array([]) else: # Step 4: Scatter energies with RMF mylog.info("Scattering energies with RMF %s." % os.path.split(rmf.filename)[-1]) all_events = rmf.scatter_energies(all_events, prng=prng) return all_events, event_params
def simulate_spectrum(spec, instrument, exp_time, out_file, overwrite=False, prng=None): """ Generate a PI or PHA spectrum from a :class:`~soxs.spectra.Spectrum` by convolving it with responses. To be used if one wants to create a spectrum without worrying about spatial response. Similar to XSPEC's "fakeit". Parameters ---------- spec : :class:`~soxs.spectra.Spectrum` The spectrum to be convolved. instrument : string The name of the instrument to use, which picks an instrument specification from the instrument registry. exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity` The exposure time in seconds. out_file : string The file to write the spectrum to. overwrite : boolean, optional Whether or not to overwrite an existing file. Default: False prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. Examples -------- >>> spec = soxs.Spectrum.from_file("my_spectrum.txt") >>> soxs.simulate_spectrum(spec, "mucal", 100000.0, ... "my_spec.pi", overwrite=True) """ from soxs.events import write_spectrum from soxs.instrument import RedistributionMatrixFile, \ AuxiliaryResponseFile from soxs.spectra import ConvolvedSpectrum prng = parse_prng(prng) exp_time = parse_value(exp_time, "s") try: instrument_spec = instrument_registry[instrument] except KeyError: raise KeyError("Instrument %s is not in the instrument registry!" % instrument) arf_file = check_file_location(instrument_spec["arf"], "files") rmf_file = check_file_location(instrument_spec["rmf"], "files") arf = AuxiliaryResponseFile(arf_file) rmf = RedistributionMatrixFile(rmf_file) cspec = ConvolvedSpectrum(spec, arf) events = {} events["energy"] = cspec.generate_energies(exp_time, prng=prng).value events = rmf.scatter_energies(events, prng=prng) events["arf"] = arf.filename events["rmf"] = rmf.filename events["exposure_time"] = exp_time events["channel_type"] = rmf.header["CHANTYPE"] events["telescope"] = rmf.header["TELESCOP"] events["instrument"] = rmf.header["INSTRUME"] events["mission"] = rmf.header.get("MISSION", "") write_spectrum(events, out_file, overwrite=overwrite)