Esempio n. 1
0
 def __init__(self, filename):
     self.filename = check_file_location(filename, "files")
     self.handle = pyfits.open(self.filename)
     if "MATRIX" in self.handle:
         self.mat_key = "MATRIX"
     elif "SPECRESP MATRIX" in self.handle:
         self.mat_key = "SPECRESP MATRIX"
     else:
         raise RuntimeError("Cannot find the response matrix in the RMF "
                            "file %s! " % filename + "It should be named "
                            "\"MATRIX\" or \"SPECRESP MATRIX\".")
     self.data = self.handle[self.mat_key].data
     self.header = self.handle[self.mat_key].header
     self.num_mat_columns = len(self.handle[self.mat_key].columns)
     self.ebounds = self.handle["EBOUNDS"].data
     self.ebounds_header = self.handle["EBOUNDS"].header
     self.weights = np.array([w.sum() for w in self.data["MATRIX"]])
     self.elo = self.data["ENERG_LO"]
     self.ehi = self.data["ENERG_HI"]
     self.n_de = self.elo.size
     self.n_ch = len(self.ebounds["CHANNEL"])
     num = 0
     for i in range(1, self.num_mat_columns + 1):
         if self.header["TTYPE%d" % i] == "F_CHAN":
             num = i
             break
     self.cmin = self.header["TLMIN%d" % num]
     self.cmax = self.header["TLMAX%d" % num]
Esempio n. 2
0
 def __init__(self, filename):
     self.filename = check_file_location(filename, "files")
     f = pyfits.open(self.filename)
     self.elo = f["SPECRESP"].data.field("ENERG_LO")
     self.ehi = f["SPECRESP"].data.field("ENERG_HI")
     self.emid = 0.5 * (self.elo + self.ehi)
     self.eff_area = np.nan_to_num(
         f["SPECRESP"].data.field("SPECRESP")).astype("float64")
     self.max_area = self.eff_area.max()
     f.close()
Esempio n. 3
0
def tbabs_cross_section(E):
    global _tbabs_emid
    global _tbabs_sigma
    global _tbabs_spline
    if _tbabs_spline is None:
        filename = check_file_location("tbabs_table.h5", "files")
        f = h5py.File(filename, "r")
        _tbabs_sigma = f["cross_section"][:]
        nbins = _tbabs_sigma.size
        ebins = np.linspace(f["emin"].value, f["emax"].value, nbins+1)
        f.close()
        _tbabs_emid = 0.5*(ebins[1:]+ebins[:-1])
        _tbabs_spline = InterpolatedUnivariateSpline(_tbabs_emid,
                                                     _tbabs_sigma, ext=1)
    return _tbabs_spline(E)
Esempio n. 4
0
def add_background(bkgnd_name,
                   event_params,
                   rot_mat,
                   focal_length=None,
                   prng=np.random):

    fov = event_params["fov"]

    bkgnd_spec = background_registry[bkgnd_name]

    # Generate background events

    bkg_events = {}

    # If this is an astrophysical background, detect the events with the ARF used
    # for the observation, otherwise it's a particle background and assume area = 1.
    if bkgnd_spec.bkgnd_type == "astrophysical":
        arf = AuxiliaryResponseFile(
            check_file_location(event_params["arf"], "files"))
        area = arf.interpolate_area(bkgnd_spec.emid).value
    else:
        area = (focal_length / 10.0)**2

    bkg_events["energy"] = bkgnd_spec.generate_energies(
        event_params["exposure_time"], area, fov, prng=prng)

    n_events = bkg_events["energy"].size

    bkg_events['chipx'] = np.round(
        prng.uniform(low=1.0, high=event_params['num_pixels'], size=n_events))
    bkg_events['chipy'] = np.round(
        prng.uniform(low=1.0, high=event_params['num_pixels'], size=n_events))
    bkg_events["detx"] = np.round(
        bkg_events["chipx"] - event_params['pix_center'][0] +
        prng.uniform(low=-0.5, high=0.5, size=n_events))
    bkg_events["dety"] = np.round(
        bkg_events["chipy"] - event_params['pix_center'][1] +
        prng.uniform(low=-0.5, high=0.5, size=n_events))
    pix = np.dot(rot_mat, np.array([bkg_events["detx"], bkg_events["dety"]]))
    bkg_events["xpix"] = pix[0, :] + event_params['pix_center'][0]
    bkg_events["ypix"] = pix[1, :] + event_params['pix_center'][1]

    return bkg_events
Esempio n. 5
0
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)
Esempio n. 6
0
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)
Esempio n. 7
0
def make_background(exp_time,
                    instrument,
                    sky_center,
                    foreground=True,
                    ptsrc_bkgnd=True,
                    instr_bkgnd=True,
                    no_dither=False,
                    dither_params=None,
                    roll_angle=0.0,
                    subpixel_res=False,
                    input_sources=None,
                    absorb_model="wabs",
                    nH=0.05,
                    prng=None):
    """
    Make background events. 

    Parameters
    ----------
    exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The exposure time to use, in seconds. 
    instrument : string
        The name of the instrument to use, which picks an instrument
        specification from the instrument registry. 
    sky_center : array, tuple, or list
        The center RA, Dec coordinates of the observation, in degrees.
    foreground : boolean, optional
        Whether or not to include the Galactic foreground. Default: True
    instr_bkgnd : boolean, optional
        Whether or not to include the instrumental background. Default: True
    no_dither : boolean, optional
        If True, turn off dithering entirely. Default: False
    dither_params : array-like of floats, optional
        The parameters to use to control the size and period of the dither
        pattern. The first two numbers are the dither amplitude in x and y
        detector coordinates in arcseconds, and the second two numbers are
        the dither period in x and y detector coordinates in seconds. 
        Default: [8.0, 8.0, 1000.0, 707.0].
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the point-source background. Default: True
        Default: 0.05
    roll_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The roll angle of the observation in degrees. Default: 0.0
    subpixel_res: boolean, optional
        If True, event positions are not randomized within the pixels 
        within which they are detected. Default: False
    input_sources : string, optional
        If set to a filename, input the point source positions, fluxes,
        and spectral indices from an ASCII table instead of generating
        them. Default: None
    absorb_model : string, optional
        The absorption model to use, "wabs" or "tbabs". Default: "wabs"
    nH : float, optional
        The hydrogen column in units of 10**22 atoms/cm**2. 
        Default: 0.05
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 
    """
    from soxs.background import make_instrument_background, \
        make_foreground, make_ptsrc_background
    prng = parse_prng(prng)
    exp_time = parse_value(exp_time, "s")
    roll_angle = parse_value(roll_angle, "deg")
    try:
        instrument_spec = instrument_registry[instrument]
    except KeyError:
        raise KeyError("Instrument %s is not in the instrument registry!" %
                       instrument)
    fov = instrument_spec["fov"]

    input_events = defaultdict(list)

    arf_file = check_file_location(instrument_spec["arf"], "files")
    arf = AuxiliaryResponseFile(arf_file)
    rmf_file = check_file_location(instrument_spec["rmf"], "files")
    rmf = RedistributionMatrixFile(rmf_file)

    if ptsrc_bkgnd:
        mylog.info("Adding in point-source background.")
        ptsrc_events = make_ptsrc_background(exp_time,
                                             fov,
                                             sky_center,
                                             input_sources=input_sources,
                                             absorb_model=absorb_model,
                                             nH=nH,
                                             prng=prng)
        for key in ["ra", "dec", "energy"]:
            input_events[key].append(ptsrc_events[key])
        input_events["flux"].append(ptsrc_events["flux"])
        input_events["emin"].append(ptsrc_events["energy"].min())
        input_events["emax"].append(ptsrc_events["energy"].max())
        input_events["sources"].append("ptsrc_bkgnd")
        events, event_params = generate_events(input_events,
                                               exp_time,
                                               instrument,
                                               sky_center,
                                               no_dither=no_dither,
                                               dither_params=dither_params,
                                               roll_angle=roll_angle,
                                               subpixel_res=subpixel_res,
                                               prng=prng)
        mylog.info("Generated %d photons from the point-source background." %
                   len(events["energy"]))
    else:
        nx = instrument_spec["num_pixels"]
        events = defaultdict(list)
        if not instrument_spec["dither"]:
            dither_on = False
        else:
            dither_on = not no_dither
        if dither_params is None:
            dither_params = [8.0, 8.0, 1000.0, 707.0]
        dither_dict = {
            "x_amp": dither_params[0],
            "y_amp": dither_params[1],
            "x_period": dither_params[2],
            "y_period": dither_params[3],
            "dither_on": dither_on,
            "plate_scale": instrument_spec["fov"] / nx * 60.0
        }
        event_params = {
            "exposure_time": exp_time,
            "fov": instrument_spec["fov"],
            "num_pixels": nx,
            "pix_center": np.array([0.5 * (2 * nx + 1)] * 2),
            "channel_type": rmf.header["CHANTYPE"],
            "sky_center": sky_center,
            "dither_params": dither_dict,
            "plate_scale": instrument_spec["fov"] / nx / 60.0,
            "chan_lim": [rmf.cmin, rmf.cmax],
            "rmf": rmf_file,
            "arf": arf_file,
            "telescope": rmf.header["TELESCOP"],
            "instrument": instrument_spec['name'],
            "mission": rmf.header.get("MISSION", ""),
            "nchan": rmf.ebounds_header["DETCHANS"],
            "roll_angle": roll_angle,
            "aimpt_coords": instrument_spec["aimpt_coords"]
        }

    if "chips" not in event_params:
        event_params["chips"] = instrument_spec["chips"]

    if foreground:
        mylog.info("Adding in astrophysical foreground.")
        bkg_events = make_foreground(event_params, arf, rmf, prng=prng)
        for key in bkg_events:
            events[key] = np.concatenate([events[key], bkg_events[key]])
    if instr_bkgnd and instrument_spec["bkgnd"] is not None:
        mylog.info("Adding in instrumental background.")
        bkg_events = make_instrument_background(
            instrument_spec["bkgnd"],
            event_params,
            instrument_spec["focal_length"],
            rmf,
            prng=prng)
        for key in bkg_events:
            events[key] = np.concatenate([events[key], bkg_events[key]])

    return events, event_params
Esempio n. 8
0
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)
    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

    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.ebounds_header["DETCHANS"]
    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