def _write_source(self, filename, extver, img_extver=None, overwrite=False): coldefs, header = self._get_source_hdu() tbhdu = pyfits.BinTableHDU.from_columns(coldefs) tbhdu.name = self.src_type.upper() if self.src_type == "phlist": hduclas1 = "PHOTONS" else: hduclas1 = self.src_type.upper() tbhdu.header["HDUCLASS"] = "HEASARC/SIMPUT" tbhdu.header["HDUCLAS1"] = hduclas1 tbhdu.header["HDUVERS"] = "1.1.0" tbhdu.header.update(header) tbhdu.header["EXTVER"] = extver if self.imhdu is not None: self.imhdu.header["EXTVER"] = img_extver if os.path.exists(filename) and not overwrite: mylog.info(f"Appending this source to {filename}.") with pyfits.open(filename, mode='append') as f: f.append(tbhdu) if self.imhdu is not None: f.append(self.imhdu) f.flush() else: if os.path.exists(filename): mylog.warning(f"Overwriting {filename} with this source.") else: mylog.info(f"Writing source to {filename}.") f = [pyfits.PrimaryHDU(), tbhdu] if self.imhdu is not None: f.append(self.imhdu) pyfits.HDUList(f).writeto(filename, overwrite=overwrite) if self.imhdu is not None: self.imhdu.header["EXTVER"] = 1
def generate_events(source, exp_time, instrument, sky_center, no_dither=False, dither_params=None, roll_angle=0.0, subpixel_res=False, aimpt_shift=None, 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 aimpt_shift : array-like, optional A two-float array-like object which shifts the aimpoint on the detector from the nominal position. Units are in arcseconds. Default: None, which results in no shift from the nominal aimpoint. prng : :class:`~numpy.random.RandomState` object, integer, or None A pseudo-random number generator. Typically will only be specified if you have a reason to generate the same set of random numbers, such as for a test. Default is None, which sets the seed based on the system time. """ exp_time = parse_value(exp_time, "s") roll_angle = parse_value(roll_angle, "deg") prng = parse_prng(prng) if source is None: source_list = [] elif isinstance(source, dict): parameters = {} for key in ["flux", "emin", "emax", "src_names"]: parameters[key] = source[key] source_list = [] for i in range(len(parameters["flux"])): phlist = SimputPhotonList(source["ra"][i], source["dec"][i], source["energy"][i], parameters['flux'][i], parameters['src_names'][i]) source_list.append(phlist) elif isinstance(source, str): # Assume this is a SIMPUT catalog source_list, parameters = read_simput_catalog(source) try: instrument_spec = instrument_registry[instrument] except KeyError: raise KeyError( f"Instrument {instrument} is not in the instrument registry!") if not instrument_spec["imaging"]: raise RuntimeError(f"Instrument '{instrument_spec['name']}' is not " f"designed for imaging observations!") arf_file = get_data_file(instrument_spec["arf"]) rmf_file = get_data_file(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 aimpt_shift is None: aimpt_shift = np.zeros(2) aimpt_shift = ensure_numpy_array(aimpt_shift).astype('float64') aimpt_shift /= plate_scale_arcsec 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 = { "exposure_time": exp_time, "arf": arf.filename, "sky_center": sky_center, "pix_center": np.array([0.5 * (2 * nx + 1)] * 2), "num_pixels": nx, "plate_scale": plate_scale, "rmf": rmf.filename, "channel_type": rmf.chan_type, "telescope": rmf.header["TELESCOP"], "instrument": instrument_spec['name'], "mission": rmf.header.get("MISSION", ""), "nchan": rmf.n_ch, "roll_angle": roll_angle, "fov": instrument_spec["fov"], "chan_lim": [rmf.cmin, rmf.cmax], "chips": instrument_spec["chips"], "dither_params": dither_dict, "aimpt_coords": instrument_spec["aimpt_coords"], "aimpt_shift": aimpt_shift } # Set up WCS 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 # Determine rotation matrix rot_mat = get_rot_mat(roll_angle) # Set up PSF psf_type = instrument_spec["psf"][0] psf_class = psf_model_registry[psf_type] psf = psf_class(instrument_spec, prng=prng) all_events = defaultdict(list) for i, src in enumerate(source_list): mylog.info( f"Detecting events from source {parameters['src_names'][i]}") # Step 1: Use ARF to determine which photons are observed mylog.info(f"Applying energy-dependent effective area from " f"{os.path.split(arf.filename)[-1]}.") refband = [parameters["emin"][i], parameters["emax"][i]] if src.src_type == "phlist": events = arf.detect_events_phlist(src.events.copy(), exp_time, parameters["flux"][i], refband, prng=prng) elif src.src_type.endswith("spectrum"): events = arf.detect_events_spec(src, exp_time, 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] + aimpt_shift[0] dety = det[1, :] + event_params["aimpt_coords"][1] + aimpt_shift[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 mylog.info(f"Scattering events with a {psf}-based PSF.") detx, dety = psf.scatter(detx, dety, events["energy"]) # 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) events["chip_id"] = -np.ones(n_evt, dtype='int') for i, chip in enumerate(event_params["chips"]): rtype = chip[0] args = chip[1:] r, _ = create_region(rtype, args, 0.0, 0.0) inside = r.contains(PixCoord(cx, cy)) events["chip_id"][inside] = i keep = events["chip_id"] > -1 mylog.info(f"{n_evt-keep.sum()} events were rejected because " f"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] - aimpt_shift[0], events["dety"] + y_offset[keep] - event_params["aimpt_coords"][1] - aimpt_shift[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(f"Scattering energies with " f"RMF {os.path.split(rmf.filename)[-1]}.") all_events = rmf.scatter_energies(all_events, prng=prng) return all_events, event_params
def from_xspec(cls, model_string, params, emin=0.01, emax=50.0, nbins=10000): mylog.warning("The 'from_xspec' method has been deprecated: " "use 'from_xspec_model' instead.") cls.from_xspec_model(model_string, params, emin=emin, emax=emax, nbins=nbins)
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 from_xspec(cls, model_string, params, emin, emax, nbins): mylog.warning("The 'from_xspec' method has been deprecated: " "use 'from_xspec_model' instead.") cls.from_xspec_model(model_string, params, emin, emax, nbins)
def make_exposure_map(event_file, expmap_file, energy, weights=None, asol_file=None, normalize=True, overwrite=False, nhistx=16, nhisty=16): """ Make an exposure map for a SOXS event file, and optionally write an aspect solution file. The exposure map will be created by binning an aspect histogram over the range of the aspect solution. Parameters ---------- event_file : string The path to the event file to use for making the exposure map. expmap_file : string The path to write the exposure map file to. energy : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, or NumPy array The energy in keV to use when computing the exposure map, or a set of energies to be used with the *weights* parameter. If providing a set, it must be in keV. weights : array-like, optional The weights to use with a set of energies given in the *energy* parameter. Used to create a more accurate exposure map weighted by a range of energies. Default: None asol_file : string, optional The path to write the aspect solution file to, if desired. Default: None normalize : boolean, optional If True, the exposure map will be divided by the exposure time so that the map's units are cm**2. Default: True overwrite : boolean, optional Whether or not to overwrite an existing file. Default: False nhistx : integer, optional The number of bins in the aspect histogram in the DETX direction. Default: 16 nhisty : integer, optional The number of bins in the aspect histogram in the DETY direction. Default: 16 """ import pyregion._region_filter as rfilter from scipy.ndimage.interpolation import rotate, shift from soxs.instrument import AuxiliaryResponseFile, perform_dither if isinstance(energy, np.ndarray) and weights is None: raise RuntimeError("Must supply a single value for the energy if " "you do not supply weights!") if not isinstance(energy, np.ndarray): energy = parse_value(energy, "keV") f_evt = pyfits.open(event_file) hdu = f_evt["EVENTS"] arf = AuxiliaryResponseFile(hdu.header["ANCRFILE"]) exp_time = hdu.header["EXPOSURE"] nx = int(hdu.header["TLMAX2"] - 0.5) // 2 ny = int(hdu.header["TLMAX3"] - 0.5) // 2 ra0 = hdu.header["TCRVL2"] dec0 = hdu.header["TCRVL3"] xdel = hdu.header["TCDLT2"] ydel = hdu.header["TCDLT3"] x0 = hdu.header["TCRPX2"] y0 = hdu.header["TCRPX3"] xdet0 = 0.5 * (2 * nx + 1) ydet0 = 0.5 * (2 * ny + 1) xaim = hdu.header.get("AIMPT_X", 0.0) yaim = hdu.header.get("AIMPT_Y", 0.0) roll = hdu.header["ROLL_PNT"] instr = instrument_registry[hdu.header["INSTRUME"].lower()] dither_params = {} if "DITHXAMP" in hdu.header: dither_params["x_amp"] = hdu.header["DITHXAMP"] dither_params["y_amp"] = hdu.header["DITHYAMP"] dither_params["x_period"] = hdu.header["DITHXPER"] dither_params["y_period"] = hdu.header["DITHYPER"] dither_params["plate_scale"] = ydel * 3600.0 dither_params["dither_on"] = True else: dither_params["dither_on"] = False f_evt.close() # Create time array for aspect solution dt = 1.0 # Seconds t = np.arange(0.0, exp_time + dt, dt) # Construct WCS w = pywcs.WCS(naxis=2) w.wcs.crval = [ra0, dec0] w.wcs.crpix = [x0, y0] w.wcs.cdelt = [xdel, ydel] w.wcs.ctype = ["RA---TAN", "DEC--TAN"] w.wcs.cunit = ["deg"] * 2 # Create aspect solution if we had dithering. # otherwise just set the offsets to zero if dither_params["dither_on"]: x_off, y_off = perform_dither(t, dither_params) # Make the aspect histogram x_amp = dither_params["x_amp"] / dither_params["plate_scale"] y_amp = dither_params["y_amp"] / dither_params["plate_scale"] x_edges = np.linspace(-x_amp, x_amp, nhistx + 1, endpoint=True) y_edges = np.linspace(-y_amp, y_amp, nhisty + 1, endpoint=True) asphist = np.histogram2d(x_off, y_off, (x_edges, y_edges))[0] asphist *= dt x_mid = 0.5 * (x_edges[1:] + x_edges[:-1]) y_mid = 0.5 * (y_edges[1:] + y_edges[:-1]) # Determine the effective area eff_area = arf.interpolate_area(energy).value if weights is not None: eff_area = np.average(eff_area, weights=weights) if instr["chips"] is None: rtypes = ["Box"] args = [[0.0, 0.0, instr["num_pixels"], instr["num_pixels"]]] else: rtypes = [] args = [] for i, chip in enumerate(instr["chips"]): rtypes.append(chip[0]) args.append(np.array(chip[1:])) tmpmap = np.zeros((2 * nx, 2 * ny)) for rtype, arg in zip(rtypes, args): rfunc = getattr(rfilter, rtype) new_args = parse_region_args(rtype, arg, xdet0 - xaim - 1.0, ydet0 - yaim - 1.0) r = rfunc(*new_args) tmpmap += r.mask(tmpmap).astype("float64") if dither_params["dither_on"]: expmap = np.zeros((2 * nx, 2 * ny)) niter = nhistx * nhisty pbar = tqdm(leave=True, total=niter, desc="Creating exposure map ") for i in range(nhistx): for j in range(nhisty): expmap += shift(tmpmap, (x_mid[i], y_mid[j]), order=0) * asphist[i, j] pbar.update(nhisty) pbar.close() else: expmap = tmpmap * exp_time expmap *= eff_area if normalize: expmap /= exp_time if roll != 0.0: rotate(expmap, roll, output=expmap, reshape=False) map_header = { "EXPOSURE": exp_time, "MTYPE1": "EQPOS", "MFORM1": "RA,DEC", "CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CRVAL1": ra0, "CRVAL2": dec0, "CUNIT1": "deg", "CUNIT2": "deg", "CDELT1": xdel, "CDELT2": ydel, "CRPIX1": x0, "CRPIX2": y0 } map_hdu = pyfits.ImageHDU(expmap, header=pyfits.Header(map_header)) map_hdu.name = "EXPMAP" map_hdu.writeto(expmap_file, overwrite=overwrite) if asol_file is not None: if dither_params["dither_on"]: det = np.array([x_off, y_off]) pix = np.dot(get_rot_mat(roll).T, det) ra, dec = w.wcs_pix2world(pix[0, :] + x0, pix[1, :] + y0, 1) col_t = pyfits.Column(name='time', format='D', unit='s', array=t) col_ra = pyfits.Column(name='ra', format='D', unit='deg', array=ra) col_dec = pyfits.Column(name='dec', format='D', unit='deg', array=dec) coldefs = pyfits.ColDefs([col_t, col_ra, col_dec]) tbhdu = pyfits.BinTableHDU.from_columns(coldefs) tbhdu.name = "ASPSOL" tbhdu.header["EXPOSURE"] = exp_time hdulist = [pyfits.PrimaryHDU(), tbhdu] pyfits.HDUList(hdulist).writeto(asol_file, overwrite=overwrite) else: mylog.warning("Refusing to write an aspect solution file because " "there was no dithering.")
def instrument_simulator(simput_file, out_file, exp_time, instrument, sky_center, clobber=False, dither_shape="square", dither_size=16.0, roll_angle=0.0, astro_bkgnd=True, instr_bkgnd=True, prng=np.random): """ Take unconvolved events in a SIMPUT catalog and create an event file from them. This function does the following: 1. Determines which events are observed using the ARF 2. Pixelizes the events, applying PSF effects and dithering 3. Adds instrumental and astrophysical background events 4. Determines energy channels using the RMF 5. Writes the events to a file Parameters ---------- simput_file : string The SIMPUT catalog file to be used as input. out_file : string The name of the event file to be written. exp_time : float 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. Can also be a JSON file with a new instrument specification. If this is the case, it will be loaded into the instrument registry. sky_center : array, tuple, or list The center RA, Dec coordinates of the observation, in degrees. clobber : boolean, optional Whether or not to clobber an existing file with the same name. Default: False dither_shape : string The shape of the dither. Currently "circle" or "square" Default: "square" dither_size : float The size of the dither in arcseconds. Width of square or radius of circle. Default: 16.0 roll_angle : float The roll angle of the observation in degrees. Default: 0.0 astro_bkgnd : boolean, optional Whether or not to include astrophysical background. Default: True instr_bkgnd : boolean, optional Whether or not to include instrumental/particle background. Default: True 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 -------- >>> instrument_simulator("sloshing_simput.fits", "sloshing_evt.fits", "hdxi_3x10", ... [30., 45.], clobber=True) """ event_list, parameters = read_simput_catalog(simput_file) 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) nx = instrument_spec["num_pixels"] plate_scale = instrument_spec["fov"] / nx / 60. # arcmin to deg plate_scale_arcsec = plate_scale * 3600.0 dsize = dither_size / plate_scale_arcsec event_params = {} event_params["exposure_time"] = exp_time event_params["arf"] = os.path.split(arf.filename)[-1] event_params["sky_center"] = sky_center event_params["pix_center"] = np.array([0.5 * (nx + 1)] * 2) event_params["num_pixels"] = nx event_params["plate_scale"] = plate_scale event_params["rmf"] = os.path.split(rmf.filename)[-1] event_params["channel_type"] = rmf.header["CHANTYPE"] event_params["telescope"] = rmf.header["TELESCOP"] event_params["instrument"] = rmf.header["INSTRUME"] event_params["mission"] = rmf.header.get("MISSION", "") event_params["nchan"] = rmf.ebounds_header["DETCHANS"] event_params["roll_angle"] = roll_angle event_params["fov"] = instrument_spec["fov"] num = 0 for i in range(1, rmf.num_mat_columns + 1): if rmf.header["TTYPE%d" % i] == "F_CHAN": num = i break event_params["chan_lim"] = [ rmf.header["TLMIN%d" % num], rmf.header["TLMAX%d" % num] ] 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 roll_angle = np.deg2rad(roll_angle) rot_mat = np.array([[np.sin(roll_angle), -np.cos(roll_angle)], [-np.cos(roll_angle), -np.sin(roll_angle)]]) all_events = {} first = True for i, evts in enumerate(event_list): mylog.info("Detecting events from source %d" % (i + 1)) # Step 1: Use ARF to determine which photons are observed mylog.info("Applying energy-dependent effective area from %s." % event_params["arf"]) 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 # Dither pixel coordinates x_offset = np.zeros(n_evt) y_offset = np.zeros(n_evt) if dither_shape == "circle": r = dsize * np.sqrt(prng.uniform(size=n_evt)) theta = 2. * np.pi * prng.uniform(size=n_evt) x_offset = r * np.cos(theta) y_offset = r * np.sin(theta) elif dither_shape == "square": x_offset = dsize * prng.uniform(low=-0.5, high=0.5, size=n_evt) y_offset = dsize * prng.uniform(low=-0.5, high=0.5, size=n_evt) xpix -= x_offset ypix -= y_offset # Rotate physical coordinates to detector coordinates det = np.dot(rot_mat, np.array([xpix, ypix])) detx = det[0, :] dety = det[1, :] # PSF scattering of detector coordinates 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 events["chipx"] = np.round(detx + event_params['pix_center'][0]) events["chipy"] = np.round(dety + event_params['pix_center'][1]) # Throw out events that don't fall on the chip keepx = np.logical_and(events["chipx"] >= 1.0, events["chipx"] <= nx) keepy = np.logical_and(events["chipy"] >= 1.0, events["chipy"] <= nx) keep = np.logical_and(keepx, keepy) mylog.info("%d events were rejected because " % (n_evt - keep.sum()) + "they fall outside the field of view.") n_evt = keep.sum() if n_evt == 0: mylog.warning( "No events are within the field of view for this source!!!" ) else: for key in events: events[key] = events[key][keep] # Convert chip coordinates back to detector coordinates events["detx"] = np.round( events["chipx"] - event_params['pix_center'][0] + prng.uniform(low=-0.5, high=0.5, size=n_evt)) events["dety"] = np.round( events["chipy"] - event_params['pix_center'][1] + prng.uniform(low=-0.5, high=0.5, size=n_evt)) # Convert detector coordinates back to pixel coordinates pix = np.dot(rot_mat, np.array([events["detx"], events["dety"]])) events["xpix"] = pix[ 0, :] + event_params['pix_center'][0] + x_offset[keep] events["ypix"] = pix[ 1, :] + event_params['pix_center'][1] + y_offset[keep] if n_evt > 0: for key in events: if first: all_events[key] = events[key] else: all_events[key] = np.concatenate( [all_events[key], events[key]]) first = False if all_events["energy"].size == 0: mylog.warning( "No events from any of the sources in the catalog were detected!") # Step 3: Add astrophysical background if astro_bkgnd: mylog.info("Adding in astrophysical background.") bkg_events = add_background(astro_bkgnd, event_params, rot_mat, prng=prng) for key in all_events: all_events[key] = np.concatenate( [all_events[key], bkg_events[key]]) # Step 4: Add particle background if instr_bkgnd: mylog.info("Adding in instrumental background.") bkg_events = add_background( instrument_spec["bkgnd"], event_params, rot_mat, prng=prng, focal_length=instrument_spec["focal_length"]) for key in all_events: all_events[key] = np.concatenate( [all_events[key], bkg_events[key]]) if all_events["energy"].size == 0: raise RuntimeError("No events were detected!!!") # Step 5: Scatter energies with RMF if all_events["energy"].size > 0: mylog.info("Scattering energies with RMF %s." % event_params['rmf']) all_events = rmf.scatter_energies(all_events, prng=prng) # Step 6: Add times to events all_events['time'] = np.random.uniform(size=all_events["energy"].size, low=0.0, high=event_params["exposure_time"]) write_event_file(all_events, event_params, out_file, clobber=clobber)
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 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_exposure_map(event_file, expmap_file, energy, weights=None, asol_file=None, normalize=True, overwrite=False, reblock=1, nhistx=16, nhisty=16, order=1): """ Make an exposure map for a SOXS event file, and optionally write an aspect solution file. The exposure map will be created by binning an aspect histogram over the range of the aspect solution. Parameters ---------- event_file : string The path to the event file to use for making the exposure map. expmap_file : string The path to write the exposure map file to. energy : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, or NumPy array The energy in keV to use when computing the exposure map, or a set of energies to be used with the *weights* parameter. If providing a set, it must be in keV. weights : array-like, optional The weights to use with a set of energies given in the *energy* parameter. Used to create a more accurate exposure map weighted by a range of energies. Default: None asol_file : string, optional The path to write the aspect solution file to, if desired. Default: None normalize : boolean, optional If True, the exposure map will be divided by the exposure time so that the map's units are cm**2. Default: True overwrite : boolean, optional Whether or not to overwrite an existing file. Default: False reblock : integer, optional Supply an integer power of 2 here to make an exposure map with a different binning. Default: 1 nhistx : integer, optional The number of bins in the aspect histogram in the DETX direction. Default: 16 nhisty : integer, optional The number of bins in the aspect histogram in the DETY direction. Default: 16 order : integer, optional The interpolation order to use when making the exposure map. Default: 1 """ import pyregion._region_filter as rfilter from scipy.ndimage.interpolation import rotate, shift from soxs.instrument import AuxiliaryResponseFile, perform_dither if isinstance(energy, np.ndarray) and weights is None: raise RuntimeError("Must supply a single value for the energy if " "you do not supply weights!") if not isinstance(energy, np.ndarray): energy = parse_value(energy, "keV") f_evt = pyfits.open(event_file) hdu = f_evt["EVENTS"] arf = AuxiliaryResponseFile(hdu.header["ANCRFILE"]) exp_time = hdu.header["EXPOSURE"] nx = int(hdu.header["TLMAX2"]-0.5)//2 ny = int(hdu.header["TLMAX3"]-0.5)//2 ra0 = hdu.header["TCRVL2"] dec0 = hdu.header["TCRVL3"] xdel = hdu.header["TCDLT2"] ydel = hdu.header["TCDLT3"] x0 = hdu.header["TCRPX2"] y0 = hdu.header["TCRPX3"] xdet0 = 0.5*(2*nx+1) ydet0 = 0.5*(2*ny+1) xaim = hdu.header.get("AIMPT_X", 0.0) yaim = hdu.header.get("AIMPT_Y", 0.0) roll = hdu.header["ROLL_PNT"] instr = instrument_registry[hdu.header["INSTRUME"].lower()] dither_params = {} if "DITHXAMP" in hdu.header: dither_params["x_amp"] = hdu.header["DITHXAMP"] dither_params["y_amp"] = hdu.header["DITHYAMP"] dither_params["x_period"] = hdu.header["DITHXPER"] dither_params["y_period"] = hdu.header["DITHYPER"] dither_params["plate_scale"] = ydel*3600.0 dither_params["dither_on"] = True else: dither_params["dither_on"] = False f_evt.close() # Create time array for aspect solution dt = 1.0 # Seconds t = np.arange(0.0, exp_time+dt, dt) # Construct WCS w = pywcs.WCS(naxis=2) w.wcs.crval = [ra0, dec0] w.wcs.crpix = [x0, y0] w.wcs.cdelt = [xdel, ydel] w.wcs.ctype = ["RA---TAN","DEC--TAN"] w.wcs.cunit = ["deg"]*2 # Create aspect solution if we had dithering. # otherwise just set the offsets to zero if dither_params["dither_on"]: x_off, y_off = perform_dither(t, dither_params) # Make the aspect histogram x_amp = dither_params["x_amp"]/dither_params["plate_scale"] y_amp = dither_params["y_amp"]/dither_params["plate_scale"] x_edges = np.linspace(-x_amp, x_amp, nhistx+1, endpoint=True) y_edges = np.linspace(-y_amp, y_amp, nhisty+1, endpoint=True) asphist = np.histogram2d(x_off, y_off, (x_edges, y_edges))[0] asphist *= dt x_mid = 0.5*(x_edges[1:]+x_edges[:-1])/reblock y_mid = 0.5*(y_edges[1:]+y_edges[:-1])/reblock # Determine the effective area eff_area = arf.interpolate_area(energy).value if weights is not None: eff_area = np.average(eff_area, weights=weights) if instr["chips"] is None: rtypes = ["Box"] args = [[0.0, 0.0, instr["num_pixels"], instr["num_pixels"]]] else: rtypes = [] args = [] for i, chip in enumerate(instr["chips"]): rtypes.append(chip[0]) args.append(np.array(chip[1:])) tmpmap = np.zeros((2*nx, 2*ny)) for rtype, arg in zip(rtypes, args): rfunc = getattr(rfilter, rtype) new_args = parse_region_args(rtype, arg, xdet0-xaim-1.0, ydet0-yaim-1.0) r = rfunc(*new_args) tmpmap += r.mask(tmpmap).astype("float64") tmpmap = downsample(tmpmap, reblock) if dither_params["dither_on"]: expmap = np.zeros(tmpmap.shape) niter = nhistx*nhisty pbar = tqdm(leave=True, total=niter, desc="Creating exposure map ") for i in range(nhistx): for j in range(nhisty): expmap += shift(tmpmap, (x_mid[i], y_mid[j]), order=order)*asphist[i, j] pbar.update(nhisty) pbar.close() else: expmap = tmpmap*exp_time expmap *= eff_area if normalize: expmap /= exp_time if roll != 0.0: rotate(expmap, roll, output=expmap, reshape=False) expmap[expmap < 0.0] = 0.0 map_header = {"EXPOSURE": exp_time, "MTYPE1": "EQPOS", "MFORM1": "RA,DEC", "CTYPE1": "RA---TAN", "CTYPE2": "DEC--TAN", "CRVAL1": ra0, "CRVAL2": dec0, "CUNIT1": "deg", "CUNIT2": "deg", "CDELT1": xdel*reblock, "CDELT2": ydel*reblock, "CRPIX1": 0.5*(2.0*nx//reblock+1), "CRPIX2": 0.5*(2.0*ny//reblock+1)} map_hdu = pyfits.ImageHDU(expmap, header=pyfits.Header(map_header)) map_hdu.name = "EXPMAP" map_hdu.writeto(expmap_file, overwrite=overwrite) if asol_file is not None: if dither_params["dither_on"]: det = np.array([x_off, y_off]) pix = np.dot(get_rot_mat(roll).T, det) ra, dec = w.wcs_pix2world(pix[0,:]+x0, pix[1,:]+y0, 1) col_t = pyfits.Column(name='time', format='D', unit='s', array=t) col_ra = pyfits.Column(name='ra', format='D', unit='deg', array=ra) col_dec = pyfits.Column(name='dec', format='D', unit='deg', array=dec) coldefs = pyfits.ColDefs([col_t, col_ra, col_dec]) tbhdu = pyfits.BinTableHDU.from_columns(coldefs) tbhdu.name = "ASPSOL" tbhdu.header["EXPOSURE"] = exp_time hdulist = [pyfits.PrimaryHDU(), tbhdu] pyfits.HDUList(hdulist).writeto(asol_file, overwrite=overwrite) else: mylog.warning("Refusing to write an aspect solution file because " "there was no dithering.")