Example #1
0
    def generate_energies(self, t_exp, area, prng=None, quiet=False):
        """
        Generate photon energies from this spectrum 
        given an exposure time and effective area.

        Parameters
        ----------
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The effective area in cm**2. If one is creating 
            events for a SIMPUT file, a constant should be 
            used and it must be large enough so that a 
            sufficiently large sample is drawn for the ARF.
        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. 
        quiet : boolean, optional
            If True, log messages will not be displayed when 
            creating energies. Useful if you have to loop over 
            a lot of spectra. Default: False
        """
        t_exp = parse_value(t_exp, "s")
        area = parse_value(area, "cm**2")
        prng = parse_prng(prng)
        rate = area*self.total_flux.value
        energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet)
        flux = np.sum(energy)*erg_per_keV/t_exp/area
        energies = Energies(energy, flux)
        return energies
Example #2
0
    def from_models(cls, name, spectral_model, spatial_model,
                    t_exp, area, prng=None):
        """
        Generate a single photon list from a spectral and a spatial
        model. 

        Parameters
        ----------
        name : string
            The name of the photon list. This will also be the prefix 
            of any photon list file that is written from this photon list.
        spectral_model : :class:`~soxs.spectra.Spectrum`
            The spectral model to use to generate the event energies.
        spatial_model : :class:`~soxs.spatial.SpatialModel`
            The spatial model to use to generate the event coordinates.
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The effective area in cm**2. If one is creating 
            events for a SIMPUT file, a constant should be 
            used and it must be large enough so that a 
            sufficiently large sample is drawn for the ARF.
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only 
            be specified if you have a reason to generate the same 
            set of random numbers, such as for a test. Default is None, 
            which sets the seed based on the system time. 
        """
        prng = parse_prng(prng)
        t_exp = parse_value(t_exp, "s")
        area = parse_value(area, "cm**2")
        e = spectral_model.generate_energies(t_exp, area, prng=prng)
        ra, dec = spatial_model.generate_coords(e.size, prng=prng)
        return cls(name, ra, dec, e, e.flux)
Example #3
0
def generate_fluxes(fov, prng):
    from soxs.data import cdf_fluxes, cdf_gal, cdf_agn
    prng = parse_prng(prng)

    fov = parse_value(fov, "arcmin")

    logf = np.log10(cdf_fluxes)

    n_gal = np.rint(cdf_gal[-1])
    n_agn = np.rint(cdf_agn[-1])
    F_gal = cdf_gal / cdf_gal[-1]
    F_agn = cdf_agn / cdf_agn[-1]
    f_gal = InterpolatedUnivariateSpline(F_gal, logf)
    f_agn = InterpolatedUnivariateSpline(F_agn, logf)

    fov_area = fov**2

    n_gal = int(n_gal * fov_area / 3600.0)
    n_agn = int(n_agn * fov_area / 3600.0)
    mylog.debug(f"{n_agn} AGN, {n_gal} galaxies in the FOV.")

    randvec1 = prng.uniform(size=n_agn)
    agn_fluxes = 10**f_agn(randvec1)

    randvec2 = prng.uniform(size=n_gal)
    gal_fluxes = 10**f_gal(randvec2)

    return agn_fluxes, gal_fluxes
Example #4
0
def make_instrument_background(bkgnd_name,
                               event_params,
                               focal_length,
                               rmf,
                               prng=None):
    prng = parse_prng(prng)

    bkgnd_spec = instrument_backgrounds[bkgnd_name]

    # Generate background events

    energy = bkgnd_spec.generate_energies(event_params["exposure_time"],
                                          event_params["fov"],
                                          focal_length=focal_length,
                                          prng=prng,
                                          quiet=True).value

    if energy.size == 0:
        raise RuntimeError(
            "No instrumental background events were detected!!!")
    else:
        mylog.info("Making %d events from the instrumental background." %
                   energy.size)

    return make_uniform_background(energy, event_params, rmf, prng=prng)
Example #5
0
    def absorb_photons(self, eobs, prng=None):
        r"""
        Determine which photons will be absorbed by foreground
        galactic absorption.

        Parameters
        ----------
        eobs : array_like
            The energies of the photons in keV.
        prng : integer, :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.
        """
        prng = parse_prng(prng)
        n_events = eobs.size
        if n_events == 0:
            return np.array([], dtype='bool')
        detected = np.zeros(n_events, dtype='bool')
        nchunk = n_events // 100
        if nchunk == 0:
            nchunk = n_events
        k = 0
        pbar = get_pbar("Absorbing photons", n_events)
        while k < n_events:
            absorb = self.get_absorb(eobs[k:k+nchunk])
            nabs = absorb.size
            randvec = prng.uniform(size=nabs)
            detected[k:k+nabs] = randvec < absorb
            k += nabs
            pbar.update(k)
        pbar.finish()
        return detected
Example #6
0
    def generate_energies(self, t_exp, area, prng=None, quiet=False):
        """
        Generate photon energies from this spectrum 
        given an exposure time and effective area.

        Parameters
        ----------
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The effective area in cm**2. If one is creating 
            events for a SIMPUT file, a constant should be 
            used and it must be large enough so that a 
            sufficiently large sample is drawn for the ARF.
        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. 
        quiet : boolean, optional
            If True, log messages will not be displayed when 
            creating energies. Useful if you have to loop over 
            a lot of spectra. Default: False
        """
        t_exp = parse_value(t_exp, "s")
        area = parse_value(area, "cm**2")
        prng = parse_prng(prng)
        rate = area*self.total_flux.value
        energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet)
        flux = np.sum(energy)*erg_per_keV/t_exp/area
        energies = Energies(energy, flux)
        return energies
Example #7
0
    def absorb_photons(self, eobs, prng=None):
        r"""
        Determine which photons will be absorbed by foreground
        galactic absorption.

        Parameters
        ----------
        eobs : array_like
            The energies of the photons in keV.
        prng : integer, :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.
        """
        prng = parse_prng(prng)
        n_events = eobs.size
        if n_events == 0:
            return np.array([], dtype='bool')
        detected = np.zeros(n_events, dtype='bool')
        nchunk = n_events // 100
        if nchunk == 0:
            nchunk = n_events
        k = 0
        pbar = get_pbar("Absorbing photons", n_events)
        while k < n_events:
            absorb = self.get_absorb(eobs[k:k + nchunk])
            nabs = absorb.size
            randvec = prng.uniform(size=nabs)
            detected[k:k + nabs] = randvec < absorb
            k += nabs
            pbar.update(k)
        pbar.finish()
        return detected
Example #8
0
 def convolve_spectrum(self, cspec, exp_time, noisy=True, prng=None):
     prng = parse_prng(prng)
     exp_time = parse_value(exp_time, "s")
     counts = cspec.flux.value * exp_time * cspec.de.value
     spec = np.histogram(cspec.emid.value, self.ebins, weights=counts)[0]
     conv_spec = np.zeros(self.n_ch)
     pbar = tqdm(leave=True, total=self.n_e, desc="Convolving spectrum ")
     if np.all(self.data["N_GRP"] == 1):
         # We can do things a bit faster if there is only one group each
         f_chan = ensure_numpy_array(np.nan_to_num(self.data["F_CHAN"]))
         n_chan = ensure_numpy_array(np.nan_to_num(self.data["N_CHAN"]))
         mat = np.nan_to_num(np.float64(self.data["MATRIX"]))
         mat_size = np.minimum(n_chan, self.n_ch-f_chan)
         for k in range(self.n_e):
             conv_spec[f_chan[k]:f_chan[k]+n_chan[k]] += spec[k]*mat[k,:mat_size[k]]
             pbar.update()
     else:
         # Otherwise, we have to go step-by-step
         for k in range(self.n_e):
             f_chan = ensure_numpy_array(np.nan_to_num(self.data["F_CHAN"][k]))
             n_chan = ensure_numpy_array(np.nan_to_num(self.data["N_CHAN"][k]))
             mat = np.nan_to_num(np.float64(self.data["MATRIX"][k]))
             mat_size = np.minimum(n_chan, self.n_ch-f_chan)
             for i, f in enumerate(f_chan):
                 conv_spec[f:f+n_chan[i]] += spec[k]*mat[:mat_size[i]]
             pbar.update()
     pbar.close()
     if noisy:
         return prng.poisson(lam=conv_spec)
     else:
         return conv_spec
Example #9
0
    def generate_energies(self, t_exp, prng=None, quiet=False):
        """
        Generate photon energies from this convolved spectrum given an
        exposure time.

        Parameters
        ----------
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        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.
        quiet : boolean, optional
            If True, log messages will not be displayed when 
            creating energies. Useful if you have to loop over 
            a lot of spectra. Default: False
        """
        t_exp = parse_value(t_exp, "s")
        prng = parse_prng(prng)
        rate = self.total_flux.value
        energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet)
        earea = self.arf.interpolate_area(energy).value
        flux = np.sum(energy)*erg_per_keV/t_exp/earea.sum()
        energies = Energies(energy, flux)
        return energies
Example #10
0
    def generate_energies(self, t_exp, prng=None, quiet=False):
        """
        Generate photon energies from this convolved spectrum given an
        exposure time.

        Parameters
        ----------
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        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.
        quiet : boolean, optional
            If True, log messages will not be displayed when 
            creating energies. Useful if you have to loop over 
            a lot of spectra. Default: False
        """
        t_exp = parse_value(t_exp, "s")
        prng = parse_prng(prng)
        rate = self.total_flux.value
        energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet)
        earea = self.arf.interpolate_area(energy).value
        flux = np.sum(energy)*erg_per_keV/t_exp/earea.sum()
        energies = Energies(energy, flux)
        return energies
Example #11
0
def generate_sources(fov, sky_center, prng=None):
    r"""
    Make a catalog of point sources.

    Parameters
    ----------
    fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The field of view in arcminutes.
    sky_center : array-like
        The center RA, Dec of the field of view in degrees.
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 
    """
    prng = parse_prng(prng)

    fov = parse_value(fov, "arcmin")

    agn_fluxes, gal_fluxes = generate_fluxes(fov, prng)

    fluxes = np.concatenate([agn_fluxes, gal_fluxes])

    ind = np.concatenate([
        get_agn_index(np.log10(agn_fluxes)),
        gal_index * np.ones(gal_fluxes.size)
    ])

    ra0, dec0 = generate_positions(fluxes.size, fov, sky_center, prng)

    return ra0, dec0, fluxes, ind
Example #12
0
 def ch_to_eb(self, channels, prng=None):
     prng = parse_prng(prng)
     emin = self.ebounds_data["E_MIN"]
     emax = self.ebounds_data["E_MAX"]
     de = emax-emin
     ch = channels - self.cmin
     e = emin[ch] + prng.uniform(size=channels.size)*de[ch]
     return e
Example #13
0
def make_foreground(event_params, arf, rmf, prng=None):
    import pyregion._region_filter as rfilter

    prng = parse_prng(prng)

    conv_frgnd_spec = ConvolvedBackgroundSpectrum(hm_astro_bkgnd, arf)

    energy = conv_frgnd_spec.generate_energies(event_params["exposure_time"],
                                               event_params["fov"], prng=prng, 
                                               quiet=True).value

    prng = parse_prng(prng)

    bkg_events = {}

    n_events = energy.size

    nx = event_params["num_pixels"]
    bkg_events["detx"] = prng.uniform(low=-0.5*nx, high=0.5*nx, size=n_events)
    bkg_events["dety"] = prng.uniform(low=-0.5*nx, high=0.5*nx, size=n_events)
    bkg_events["energy"] = energy

    if event_params["chips"] is None:
        bkg_events["chip_id"] = np.zeros(n_events, dtype='int')
    else:
        bkg_events["chip_id"] = -np.ones(n_events, dtype='int')
        for i, chip in enumerate(event_params["chips"]):
            thisc = np.ones(n_events, dtype='bool')
            rtype = chip[0]
            args = chip[1:]
            r = getattr(rfilter, rtype)(*args)
            inside = r.inside(bkg_events["detx"], bkg_events["dety"])
            thisc = np.logical_and(thisc, inside)
            bkg_events["chip_id"][thisc] = i

    keep = bkg_events["chip_id"] > -1

    if keep.sum() == 0:
        raise RuntimeError("No astrophysical foreground events were detected!!!")
    else:
        mylog.info("Making %d events from the astrophysical foreground." % keep.sum())

    for key in bkg_events:
        bkg_events[key] = bkg_events[key][keep]

    return make_diffuse_background(bkg_events, event_params, rmf, prng=prng)
Example #14
0
def make_foreground(event_params, arf, rmf, prng=None):
    import pyregion._region_filter as rfilter

    prng = parse_prng(prng)

    conv_frgnd_spec = ConvolvedBackgroundSpectrum(hm_astro_bkgnd, arf)

    energy = conv_frgnd_spec.generate_energies(event_params["exposure_time"],
                                               event_params["fov"], prng=prng, 
                                               quiet=True).value

    prng = parse_prng(prng)

    bkg_events = {}

    n_events = energy.size

    nx = event_params["num_pixels"]
    bkg_events["detx"] = prng.uniform(low=-0.5*nx, high=0.5*nx, size=n_events)
    bkg_events["dety"] = prng.uniform(low=-0.5*nx, high=0.5*nx, size=n_events)
    bkg_events["energy"] = energy

    if event_params["chips"] is None:
        bkg_events["chip_id"] = np.zeros(n_events, dtype='int')
    else:
        bkg_events["chip_id"] = -np.ones(n_events, dtype='int')
        for i, chip in enumerate(event_params["chips"]):
            thisc = np.ones(n_events, dtype='bool')
            rtype = chip[0]
            args = chip[1:]
            r = getattr(rfilter, rtype)(*args)
            inside = r.inside(bkg_events["detx"], bkg_events["dety"])
            thisc = np.logical_and(thisc, inside)
            bkg_events["chip_id"][thisc] = i

    keep = bkg_events["chip_id"] > -1

    if keep.sum() == 0:
        raise RuntimeError("No astrophysical foreground events were detected!!!")
    else:
        mylog.info("Making %d events from the astrophysical foreground." % keep.sum())

    for key in bkg_events:
        bkg_events[key] = bkg_events[key][keep]

    return make_diffuse_background(bkg_events, event_params, rmf, prng=prng)
Example #15
0
def make_point_sources(area, exp_time, positions, sky_center,
                       spectra, prng=None):
    r"""
    Create a new :class:`~pyxsim.event_list.EventList` which contains
    point sources.

    Parameters
    ----------
    area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The collecting area to determine the number of events. If units are
        not specified, it is assumed to be in cm^2.
    exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The exposure time to determine the number of events. If units are
        not specified, it is assumed to be in seconds.
    positions : array of source positions, shape 2xN
        The positions of the point sources in RA, Dec, where N is the
        number of point sources. Coordinates should be in degrees.
    sky_center : array-like
        Center RA, Dec of the events in degrees.
    spectra : list (size N) of :class:`~soxs.spectra.Spectrum` objects
        The spectra for the point sources, where N is the number 
        of point sources. Assumed to be in the observer frame.
    prng : integer or :class:`~numpy.random.RandomState` object 
        A pseudo-random number generator. Typically will only be specified
        if you have a reason to generate the same set of random numbers, such as for a
        test. Default is to use the :mod:`numpy.random` module.
    """
    prng = parse_prng(prng)

    spectra = ensure_list(spectra)
    positions = ensure_list(positions)

    area = parse_value(area, "cm**2")
    exp_time = parse_value(exp_time, "s")

    t_exp = exp_time.value/comm.size

    x = []
    y = []
    e = []

    for pos, spectrum in zip(positions, spectra):
        eobs = spectrum.generate_energies(t_exp, area.value, prng=prng)
        ne = eobs.size
        x.append(YTArray([pos[0]] * ne, "deg"))
        y.append(YTArray([pos[1]] * ne, "deg"))
        e.append(YTArray.from_astropy(eobs))

    parameters = {"sky_center": YTArray(sky_center, "degree"),
                  "exp_time": exp_time,
                  "area": area}

    events = {}
    events["xsky"] = uconcatenate(x)
    events["ysky"] = uconcatenate(y)
    events["eobs"] = uconcatenate(e)

    return EventList(events, parameters)
Example #16
0
def make_point_sources(area, exp_time, positions, sky_center,
                       spectra, prng=None):
    r"""
    Create a new :class:`~pyxsim.event_list.EventList` which contains
    point sources.

    Parameters
    ----------
    area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The collecting area to determine the number of events. If units are
        not specified, it is assumed to be in cm^2.
    exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The exposure time to determine the number of events. If units are
        not specified, it is assumed to be in seconds.
    positions : array of source positions, shape 2xN
        The positions of the point sources in RA, Dec, where N is the
        number of point sources. Coordinates should be in degrees.
    sky_center : array-like
        Center RA, Dec of the events in degrees.
    spectra : list (size N) of :class:`~soxs.spectra.Spectrum` objects
        The spectra for the point sources, where N is the number 
        of point sources. Assumed to be in the observer frame.
    prng : integer or :class:`~numpy.random.RandomState` object 
        A pseudo-random number generator. Typically will only be specified
        if you have a reason to generate the same set of random numbers, such as for a
        test. Default is to use the :mod:`numpy.random` module.
    """
    prng = parse_prng(prng)

    spectra = ensure_list(spectra)
    positions = ensure_list(positions)

    area = parse_value(area, "cm**2")
    exp_time = parse_value(exp_time, "s")

    t_exp = exp_time.value/comm.size

    x = []
    y = []
    e = []

    for pos, spectrum in zip(positions, spectra):
        eobs = spectrum.generate_energies(t_exp, area.value, prng=prng)
        ne = eobs.size
        x.append(YTArray([pos[0]] * ne, "degree"))
        y.append(YTArray([pos[1]] * ne, "degree"))
        e.append(YTArray.from_astropy(eobs))

    parameters = {"sky_center": YTArray(sky_center, "degree"),
                  "exp_time": exp_time,
                  "area": area}

    events = {}
    events["eobs"] = uconcatenate(e)
    events["xsky"] = uconcatenate(x)
    events["ysky"] = uconcatenate(y)

    return EventList(events, parameters)
Example #17
0
 def __init__(self, e0, emin, emax, emission_field, alpha, prng=None):
     self.e0 = parse_value(e0, "keV")
     self.emin = parse_value(emin, "keV")
     self.emax = parse_value(emax, "keV")
     self.emission_field = emission_field
     self.alpha = alpha
     self.prng = parse_prng(prng)
     self.spectral_norm = None
     self.redshift = None
Example #18
0
 def __init__(self, e0, emin, emax, emission_field, alpha, prng=None):
     self.e0 = parse_value(e0, "keV")
     self.emin = parse_value(emin, "keV")
     self.emax = parse_value(emax, "keV")
     self.emission_field = emission_field
     self.alpha = alpha
     self.prng = parse_prng(prng)
     self.spectral_norm = None
     self.redshift = None
Example #19
0
    def scatter_energies(self, events, prng=None):
        """
        Scatter photon energies with the RMF and produce the 
        corresponding channel values.

        Parameters
        ----------
        events : dict of np.ndarrays
            The energies and positions of the photons. 
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only 
            be specified if you have a reason to generate the same 
            set of random numbers, such as for a test. Default is None, 
            which sets the seed based on the system time. 
        """
        prng = parse_prng(prng)
        eidxs = np.argsort(events["energy"])
        sorted_e = events["energy"][eidxs]

        detectedChannels = []

        # run through all photon energies and find which bin they go in
        fcurr = 0
        last = sorted_e.shape[0]

        emin = sorted_e[0]
        emax = sorted_e[-1]

        pbar = tqdm(leave=True, total=last, desc="Scattering energies ")
        for (k, low), high in zip(enumerate(self.elo), self.ehi):
            if high < emin or low > emax:
                continue
            e = sorted_e[fcurr:last]
            nn = np.logical_and(low <= e, e < high).sum()
            if nn == 0:
                continue
            # weight function for probabilities from RMF
            weights = np.nan_to_num(np.float64(self.data["MATRIX"][k]))
            weights /= weights.sum()
            trueChannel = self._make_channels(k)
            if len(trueChannel) > 0:
                channelInd = prng.choice(len(weights), size=nn, p=weights)
                detectedChannels.append(trueChannel[channelInd])
                fcurr += nn
                pbar.update(nn)

        pbar.close()

        for key in events:
            events[key] = events[key][eidxs]
        events[self.header["CHANTYPE"]] = np.concatenate(detectedChannels)

        return events
Example #20
0
def make_instrument_background(inst_spec, event_params, rmf, prng=None):
    from collections import defaultdict

    prng = parse_prng(prng)

    bkgnd_spec = inst_spec["bkgnd"]

    if isinstance(bkgnd_spec[0], str):
        nchips = len(event_params["chips"])
        bkgnd_spec = [bkgnd_spec] * nchips

    bkg_events = defaultdict(list)
    pixel_area = (event_params["plate_scale"] * 60.0)**2
    for i, chip in enumerate(event_params["chips"]):
        rtype = chip[0]
        args = chip[1:]
        r, bounds = create_region(rtype, args, 0.0, 0.0)
        sa = (bounds[1] - bounds[0]) * (bounds[3] - bounds[2]) * pixel_area
        bspec = InstrumentalBackground.from_filename(bkgnd_spec[i][0],
                                                     bkgnd_spec[i][1],
                                                     inst_spec['focal_length'])
        chan = bspec.generate_channels(event_params["exposure_time"],
                                       sa,
                                       prng=prng)
        n_events = chan.size
        detx = prng.uniform(low=bounds[0], high=bounds[1], size=n_events)
        dety = prng.uniform(low=bounds[2], high=bounds[3], size=n_events)
        if rtype in ["Box", "Rectangle"]:
            thisc = slice(None, None, None)
            n_det = n_events
        else:
            thisc = r.contains(PixCoord(detx, dety))
            n_det = thisc.sum()
        ch = chan[thisc].astype('int')
        e = rmf.ch_to_eb(ch, prng=prng)
        bkg_events["energy"].append(e)
        bkg_events[rmf.chan_type].append(ch)
        bkg_events["detx"].append(detx[thisc])
        bkg_events["dety"].append(dety[thisc])
        bkg_events["chip_id"].append(i * np.ones(n_det))
    for key in bkg_events:
        bkg_events[key] = np.concatenate(bkg_events[key])

    if bkg_events["energy"].size == 0:
        raise RuntimeError(
            "No instrumental background events were detected!!!")
    else:
        mylog.info(f"Making {bkg_events['energy'].size} events "
                   f"from the instrumental background.")

    return make_diffuse_background(bkg_events, event_params, rmf, prng=prng)
Example #21
0
    def detect_events(self, events, exp_time, flux, refband, prng=None):
        """
        Use the ARF to determine a subset of photons which 
        will be detected. Returns a boolean NumPy array 
        which is the same is the same size as the number 
        of photons, wherever it is "true" means those photons 
        have been detected.

        Parameters
        ----------
        events : dict of np.ndarrays
            The energies and positions of the photons. 
        exp_time : float
            The exposure time in seconds.
        flux : float
            The total flux of the photons in erg/s/cm^2. 
        refband : array_like
            A two-element array or list containing the limits 
            of the energy band which the flux was computed in. 
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only 
            be specified if you have a reason to generate the same 
            set of random numbers, such as for a test. Default is None, 
            which sets the seed based on the system time. 
        """
        prng = parse_prng(prng)
        energy = events["energy"]
        if energy.size == 0:
            return events
        earea = self.interpolate_area(energy).value
        idxs = np.logical_and(energy >= refband[0], energy <= refband[1])
        rate = flux / (energy[idxs].sum() * erg_per_keV) * earea[idxs].sum()
        n_ph = prng.poisson(lam=rate * exp_time)
        fak = float(n_ph) / energy.size
        if fak > 1.0:
            mylog.error(
                "Number of events in sample: %d, Number of events wanted: %d" %
                (energy.size, n_ph))
            raise ValueError(
                "This combination of exposure time and effective area "
                "will result in more photons being drawn than are available "
                "in the sample!!!")
        w = earea / self.max_area
        randvec = prng.uniform(size=energy.size)
        eidxs = prng.permutation(
            np.where(randvec < w)[0])[:n_ph].astype("int64")
        mylog.info("%s events detected." % n_ph)
        for key in events:
            events[key] = events[key][eidxs]
        return events
Example #22
0
def make_background(area, exp_time, fov, sky_center, spectrum, prng=None):
    r"""
    Create a new :class:`~pyxsim.event_list.EventList` which is filled
    uniformly with background events. 

    Parameters
    ----------
    area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The collecting area to determine the number of events. If units are
        not specified, it is assumed to be in cm^2.
    exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The exposure time to determine the number of events. If units are
        not specified, it is assumed to be in seconds.
    fov : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The field of view of the event file. If units are not 
        provided, they are assumed to be in arcminutes.
    sky_center : array-like
        Center RA, Dec of the events in degrees.
    spectrum : :class:`~soxs.spectra.Spectrum`
        The spectrum for the background.
    prng : integer or :class:`~numpy.random.RandomState` object 
        A pseudo-random number generator. Typically will only be specified
        if you have a reason to generate the same set of random numbers, such as for a
        test. Default is to use the :mod:`numpy.random` module.
    """
    prng = parse_prng(prng)

    fov = parse_value(fov, "arcmin")
    exp_time = parse_value(exp_time, "s")
    area = parse_value(area, "cm**2")

    t_exp = exp_time.value / comm.size

    e = spectrum.generate_energies(t_exp, area.value, prng=prng)
    fov_model = FillFOVModel(sky_center[0], sky_center[1], fov.value)
    ra, dec = fov_model.generate_coords(e.size, prng=prng)

    parameters = {
        "sky_center": YTArray(sky_center, "degree"),
        "exp_time": exp_time,
        "area": area
    }

    events = {}
    events["xsky"] = YTArray(ra.value, "degree")
    events["ysky"] = YTArray(dec.value, "degree")
    events["eobs"] = YTArray(e.value, "keV")

    return EventList(events, parameters)
Example #23
0
def make_xrb_photons(ds, redshift, area, exp_time, emin, emax,
                     center="c", cosmology=None, prng=None):
    r"""
    Take a dataset produced by 
    :func:`~pyxsim.source_generators.xray_binaries.make_xrb_particles`
    and produce a :class:`~pyxsim.photon_list.PhotonList`.

    Parameters
    ----------
    ds : :class:`~yt.data_objects.static_output.Dataset`
        The dataset of XRB particles to use to make the photons.
    redshift : float
        The cosmological redshift for the photons.
    area : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The collecting area to determine the number of photons. If units are
        not specified, it is assumed to be in cm^2.
    exp_time : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The exposure time to determine the number of photons. If units are
        not specified, it is assumed to be in seconds.
    emin : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The minimum energy of the photons to be generated, in the rest frame of
        the source. If units are not given, they are assumed to be in keV.
    emax : float, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The maximum energy of the photons to be generated, in the rest frame of
        the source. If units are not given, they are assumed to be in keV.
    center : string or array_like, optional
        The origin of the photon spatial coordinates. Accepts "c", "max", or a coordinate. 
        If not specified, pyxsim attempts to use the "center" field parameter of the data_source. 
    cosmology : :class:`~yt.utilities.cosmology.Cosmology`, optional
        Cosmological information. If not supplied, we try to get
        the cosmology from the dataset. Otherwise, LCDM with
        the default yt parameters is assumed.
    prng : integer or :class:`~numpy.random.RandomState` object 
        A pseudo-random number generator. Typically will only be specified
        if you have a reason to generate the same set of random numbers, such as for a
        test. Default is to use the :mod:`numpy.random` module.
    """
    dd = ds.all_data()
    e0 = (1.0, "keV")
    prng = parse_prng(prng)
    xrb_model = PowerLawSourceModel(e0, emin, emax, 
                                    "particle_count_rate",
                                    "particle_spectral_index", prng=prng)
    photons = PhotonList.from_data_source(dd, redshift, area, exp_time,
                                          xrb_model, center=center,
                                          point_sources=True,
                                          cosmology=cosmology)
    return photons
Example #24
0
 def __init__(self, spectral_model, emin, emax, nchan,
              temperature_field=None, emission_measure_field=None,
              kT_min=0.008, kT_max=64.0, n_kT=10000,
              kT_scale="linear", Zmet=0.3, var_elem=None,
              method="invert_cdf", thermal_broad=True, 
              model_root=None, model_vers=None, nei=False,
              nolines=False, abund_table="angr", prng=None):
     if isinstance(spectral_model, string_types):
         if spectral_model not in thermal_models:
             raise KeyError("%s is not a known thermal spectral model!" % spectral_model)
         spectral_model = thermal_models[spectral_model]
     self.temperature_field = temperature_field
     self.Zmet = Zmet
     self.nei = nei
     if var_elem is None:
         var_elem = {}
         var_elem_keys = None
         self.num_var_elem = 0
     else:
         var_elem_keys = list(var_elem.keys())
         self.num_var_elem = len(var_elem_keys)
     self.var_elem = var_elem
     self.spectral_model = spectral_model(emin, emax, nchan,
                                          var_elem=var_elem_keys,
                                          thermal_broad=thermal_broad,
                                          model_root=model_root,
                                          model_vers=model_vers,
                                          nolines=nolines, nei=nei,
                                          abund_table=abund_table)
     self.var_elem_keys = self.spectral_model.var_elem_names
     self.var_ion_keys = self.spectral_model.var_ion_names
     self.method = method
     self.prng = parse_prng(prng)
     self.kT_min = kT_min
     self.kT_max = kT_max
     self.kT_scale = kT_scale
     self.n_kT = n_kT
     self.spectral_norm = None
     self.redshift = None
     self.pbar = None
     self.kT_bins = None
     self.dkT = None
     self.emission_measure_field = emission_measure_field
     self.Zconvert = 1.0
     self.abund_table = abund_table
     self.atable = self.spectral_model.atable
     self.mconvert = {}
Example #25
0
    def detect_events(self, events, exp_time, flux, refband, prng=None):
        """
        Use the ARF to determine a subset of photons which 
        will be detected. Returns a boolean NumPy array 
        which is the same is the same size as the number 
        of photons, wherever it is "true" means those photons 
        have been detected.

        Parameters
        ----------
        events : dict of np.ndarrays
            The energies and positions of the photons. 
        exp_time : float
            The exposure time in seconds.
        flux : float
            The total flux of the photons in erg/s/cm^2. 
        refband : array_like
            A two-element array or list containing the limits 
            of the energy band which the flux was computed in. 
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only 
            be specified if you have a reason to generate the same 
            set of random numbers, such as for a test. Default is None, 
            which sets the seed based on the system time. 
        """
        prng = parse_prng(prng)
        energy = events["energy"]
        if energy.size == 0:
            return events
        earea = self.interpolate_area(energy).value
        idxs = np.logical_and(energy >= refband[0], energy <= refband[1])
        rate = flux/(energy[idxs].sum()*erg_per_keV)*earea[idxs].sum()
        n_ph = prng.poisson(lam=rate*exp_time)
        fak = float(n_ph)/energy.size
        if fak > 1.0:
            mylog.error("Number of events in sample: %d, Number of events wanted: %d" % (energy.size, n_ph))
            raise ValueError("This combination of exposure time and effective area "
                             "will result in more photons being drawn than are available "
                             "in the sample!!!")
        w = earea / self.max_area
        randvec = prng.uniform(size=energy.size)
        eidxs = prng.permutation(np.where(randvec < w)[0])[:n_ph].astype("int64")
        mylog.info("%s events detected." % n_ph)
        for key in events:
            events[key] = events[key][eidxs]
        return events
Example #26
0
 def convolve_spectrum(self, cspec, exp_time, prng=None):
     prng = parse_prng(prng)
     exp_time = parse_value(exp_time, "s")
     counts = cspec.flux.value * exp_time * cspec.de.value
     spec = np.histogram(cspec.emid.value, self.ebins, weights=counts)[0]
     conv_spec = np.zeros(self.n_ch)
     pbar = tqdm(leave=True, total=self.n_e, desc="Convolving spectrum ")
     for k in range(self.n_e):
         f_chan = ensure_numpy_array(np.nan_to_num(self.data["F_CHAN"][k]))
         n_chan = ensure_numpy_array(np.nan_to_num(self.data["N_CHAN"][k]))
         mat = np.nan_to_num(np.float64(self.data["MATRIX"][k]))
         for f, n in zip(f_chan, n_chan):
             mat_size = min(n, self.n_ch-f)
             conv_spec[f:f+n] += spec[k]*mat[:mat_size]
         pbar.update()
     pbar.close()
     return prng.poisson(lam=conv_spec)
Example #27
0
 def detect_events_spec(self, src, exp_time, refband, prng=None):
     from soxs.spectra import ConvolvedSpectrum
     prng = parse_prng(prng)
     cspec = ConvolvedSpectrum.convolve(
         src.spec, self).new_spec_from_band(refband[0], refband[1])
     energy = cspec.generate_energies(exp_time, quiet=True, prng=prng).value
     if getattr(src, "imhdu", None):
         x, y = image_pos(src.imhdu.data, energy.size, prng)
         w = pywcs.WCS(header=src.imhdu.header)
         w.wcs.crval = [src.ra, src.dec]
         ra, dec = w.wcs_pix2world(x, y, 1)
     else:
         pones = np.ones_like(energy)
         ra = src.ra*pones
         dec = src.dec*pones
     mylog.info(f"{energy.size} events detected.")
     return {"energy": energy, "ra": ra, "dec": dec}
Example #28
0
    def scatter_energies(self, events, prng=None):
        """
        Scatter photon energies with the RMF and produce the 
        corresponding channel values.

        Parameters
        ----------
        events : dict of np.ndarrays
            The energies and positions of the photons. 
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only 
            be specified if you have a reason to generate the same 
            set of random numbers, such as for a test. Default is None, 
            which sets the seed based on the system time. 
        """
        prng = parse_prng(prng)
        eidxs = np.argsort(events["energy"])
        sorted_e = events["energy"][eidxs]

        detectedChannels = []

        # run through all photon energies and find which bin they go in
        fcurr = 0
        last = sorted_e.shape[0]

        pbar = tqdm(leave=True, total=last, desc="Scattering energies ")
        for (k, low), high in zip(enumerate(self.elo), self.ehi):
            # weight function for probabilities from RMF
            weights = np.nan_to_num(np.float64(self.data["MATRIX"][k]))
            weights /= weights.sum()
            trueChannel = self._make_channels(k)
            if len(trueChannel) > 0:
                e = sorted_e[fcurr:last]
                nn = np.logical_and(low <= e, e < high).sum()
                channelInd = prng.choice(len(weights), size=nn, p=weights)
                detectedChannels.append(trueChannel[channelInd])
                fcurr += nn
                pbar.update(nn)

        pbar.close()

        for key in events:
            events[key] = events[key][eidxs]
        events[self.header["CHANTYPE"]] = np.concatenate(detectedChannels)

        return events
Example #29
0
    def generate_energies(self,
                          t_exp,
                          fov,
                          focal_length=None,
                          prng=None,
                          quiet=False):
        """
        Generate photon energies from this instrumental 
        background spectrum given an exposure time, 
        effective area, and field of view.

        Parameters
        ----------
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The width of the field of view on a side in 
            arcminutes.
        focal_length : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
            The focal length in meters. Default is to use
            the default focal length of the instrument
            configuration.
        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. 
        quiet : boolean, optional
            If True, log messages will not be displayed when 
            creating energies. Useful if you have to loop over 
            a lot of spectra. Default: False
        """
        t_exp = parse_value(t_exp, "s")
        fov = parse_value(fov, "arcmin")
        prng = parse_prng(prng)
        if focal_length is None:
            focal_length = self.default_focal_length
        else:
            focal_length = parse_value(focal_length, "m")
        rate = fov * fov * self.total_flux.value
        rate *= (focal_length / self.default_focal_length)**2
        energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet)
        flux = np.sum(energy) * erg_per_keV / t_exp
        energies = Energies(energy, flux)
        return energies
Example #30
0
def make_foreground(event_params, arf, rmf, prng=None):

    prng = parse_prng(prng)

    conv_frgnd_spec = ConvolvedBackgroundSpectrum.convolve(hm_astro_bkgnd, arf)

    bkg_events = {"energy": [], "detx": [], "dety": [], "chip_id": []}
    pixel_area = (event_params["plate_scale"]*60.0)**2
    for i, chip in enumerate(event_params["chips"]):
        rtype = chip[0]
        args = chip[1:]
        r, bounds = create_region(rtype, args, 0.0, 0.0)
        fov = np.sqrt((bounds[1]-bounds[0])*(bounds[3]-bounds[2])*pixel_area)
        e = conv_frgnd_spec.generate_energies(event_params["exposure_time"],
                                              fov, prng=prng, quiet=True).value
        n_events = e.size
        detx = prng.uniform(low=bounds[0], high=bounds[1], size=n_events)
        dety = prng.uniform(low=bounds[2], high=bounds[3], size=n_events)
        if rtype in ["Box", "Rectangle"]:
            thisc = slice(None, None, None)
            n_det = n_events
        else:
            thisc = r.contains(PixCoord(detx, dety))
            n_det = thisc.sum()
        bkg_events["energy"].append(e[thisc])
        bkg_events["detx"].append(detx[thisc])
        bkg_events["dety"].append(dety[thisc])
        bkg_events["chip_id"].append(i*np.ones(n_det))

    for key in bkg_events:
        bkg_events[key] = np.concatenate(bkg_events[key])

    if bkg_events["energy"].size == 0:
        raise RuntimeError("No astrophysical foreground events "
                           "were detected!!!")
    else:
        mylog.info(f"Making {bkg_events['energy'].size} events from the "
                   f"astrophysical foreground.")

    bkg_events = make_diffuse_background(bkg_events, 
                                         event_params, rmf, prng=prng)
    mylog.info(f"Scattering energies with "
               f"RMF {os.path.split(rmf.filename)[-1]}.")

    return rmf.scatter_energies(bkg_events, prng=prng)
Example #31
0
    def __call__(self, events, prng=None):
        """
        Calling method for :class:`~pyxsim.instruments.InstrumentSimulator`.

        Parameters
        ----------
        events : :class:`~pyxsim.events.EventList`
            An EventList instance of unconvolved events.
        prng : integer or :class:`~numpy.random.RandomState` object 
            A pseudo-random number generator. Typically will only be specified
            if you have a reason to generate the same set of random numbers, such as for a
            test. Default is to use the :mod:`numpy.random` module.
        """
        issue_deprecation_warning("The pyXSIM built-in instrument simulators "
                                  "have been deprecated and will be removed "
                                  "in a future release!")
        if "pi" in events or "pha" in events:
            raise RuntimeError(
                "These events have already been convolved with a response!!")
        prng = parse_prng(prng)
        flux = np.sum(events["eobs"]).to("erg") / \
               events.parameters["exp_time"]/events.parameters["area"]
        exp_time = events.parameters["exp_time"]
        emin = events["eobs"].min().value
        emax = events["eobs"].max().value
        new_events = {}
        new_events.update(events.events)
        new_events["energy"] = new_events.pop("eobs")
        new_events = self.arf.detect_events(new_events,
                                            exp_time,
                                            flux, [emin, emax],
                                            prng=prng)
        new_events = self.rmf.scatter_energies(new_events, prng=prng)
        new_events["eobs"] = new_events.pop("energy")
        chantype = self.rmf.header["CHANTYPE"].lower()
        new_events[chantype] = new_events.pop(self.rmf.header["CHANTYPE"])
        parameters = {}
        parameters.update(events.parameters)
        parameters["channel_type"] = chantype
        parameters["mission"] = self.rmf.header.get("MISSION", "")
        parameters["instrument"] = self.rmf.header["INSTRUME"]
        parameters["telescope"] = self.rmf.header["TELESCOP"]
        parameters["arf"] = self.arf.filename
        parameters["rmf"] = self.rmf.filename
        return ConvolvedEventList(new_events, parameters)
Example #32
0
def make_foreground(event_params, arf, rmf, prng=None):
    prng = parse_prng(prng)

    conv_bkgnd_spec = ConvolvedBackgroundSpectrum(hm_astro_bkgnd, arf)

    energy = conv_bkgnd_spec.generate_energies(event_params["exposure_time"],
                                               event_params["fov"],
                                               prng=prng,
                                               quiet=True).value

    if energy.size == 0:
        raise RuntimeError(
            "No astrophysical foreground events were detected!!!")
    else:
        mylog.info("Making %d events from the astrophysical foreground." %
                   energy.size)

    return make_uniform_background(energy, event_params, rmf, prng=prng)
Example #33
0
def generate_sources(exp_time, fov, sky_center, area=40000.0, prng=None):
    r"""
    Make a catalog of point sources.

    Parameters
    ----------
    exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The exposure time of the observation in seconds.
    fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The field of view in arcminutes.
    sky_center : array-like
        The center RA, Dec of the field of view in degrees.
    area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The effective area in cm**2. It must be large enough 
        so that a sufficiently large sample is drawn for the 
        ARF. Default: 40000.
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 
    """
    prng = parse_prng(prng)

    exp_time = parse_value(exp_time, "s")
    fov = parse_value(fov, "arcmin")
    area = parse_value(area, "cm**2")

    agn_fluxes, gal_fluxes = generate_fluxes(exp_time, area, fov, prng)

    fluxes = np.concatenate([agn_fluxes, gal_fluxes])

    ind = np.concatenate([get_agn_index(np.log10(agn_fluxes)),
                          gal_index * np.ones(gal_fluxes.size)])

    dec_scal = np.fabs(np.cos(sky_center[1] * np.pi / 180))
    ra_min = sky_center[0] - fov / (2.0 * 60.0 * dec_scal)
    dec_min = sky_center[1] - fov / (2.0 * 60.0)

    ra0 = prng.uniform(size=fluxes.size) * fov / (60.0 * dec_scal) + ra_min
    dec0 = prng.uniform(size=fluxes.size) * fov / 60.0 + dec_min

    return ra0, dec0, fluxes, ind
Example #34
0
    def generate_coords(self, num_events, prng=None):
        """
        Generate a sample of photon positions from this 
        spatial model. 

        Parameters
        ----------
        num_events : integer
            The number of events to generate.
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only
            be specified if you have a reason to generate the same
            set of random numbers, such as for a test. Default is None,
            which sets the seed based on the system time.
        """
        prng = parse_prng(prng)
        x, y = self._generate_coords(num_events, prng)
        ra, dec = self.w.wcs_pix2world(x, y, 1)
        return u.Quantity(ra, "deg"), u.Quantity(dec, "deg")
Example #35
0
    def generate_coords(self, num_events, prng=None):
        """
        Generate a sample of photon positions from this 
        spatial model. 

        Parameters
        ----------
        num_events : integer
            The number of events to generate.
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only
            be specified if you have a reason to generate the same
            set of random numbers, such as for a test. Default is None,
            which sets the seed based on the system time.
        """
        prng = parse_prng(prng)
        x, y = self._generate_coords(num_events, prng)
        ra, dec = self.w.wcs_pix2world(x, y, 1)
        return u.Quantity(ra, "deg"), u.Quantity(dec, "deg")
Example #36
0
    def __call__(self, events, prng=None):
        """
        Calling method for :class:`~pyxsim.instruments.InstrumentSimulator`.

        Parameters
        ----------
        events : :class:`~pyxsim.events.EventList`
            An EventList instance of unconvolved events.
        prng : integer or :class:`~numpy.random.RandomState` object 
            A pseudo-random number generator. Typically will only be specified
            if you have a reason to generate the same set of random numbers, such as for a
            test. Default is to use the :mod:`numpy.random` module.
        """
        issue_deprecation_warning("The pyXSIM built-in instrument simulators "
                                  "have been deprecated and will be removed "
                                  "in a future release!")
        if "pi" in events or "pha" in events:
            raise RuntimeError("These events have already been convolved with a response!!")
        prng = parse_prng(prng)
        flux = np.sum(events["eobs"]).to("erg") / \
               events.parameters["exp_time"]/events.parameters["area"]
        exp_time = events.parameters["exp_time"]
        emin = events["eobs"].min().value
        emax = events["eobs"].max().value
        new_events = {}
        new_events.update(events.events)
        new_events["energy"] = new_events.pop("eobs")
        new_events = self.arf.detect_events(new_events, exp_time, flux,
                                            [emin, emax], prng=prng)
        new_events = self.rmf.scatter_energies(new_events, prng=prng)
        new_events["eobs"] = new_events.pop("energy")
        chantype = self.rmf.header["CHANTYPE"].lower()
        new_events[chantype] = new_events.pop(self.rmf.header["CHANTYPE"])
        parameters = {}
        parameters.update(events.parameters)
        parameters["channel_type"] = chantype
        parameters["mission"] = self.rmf.header.get("MISSION", "")
        parameters["instrument"] = self.rmf.header["INSTRUME"]
        parameters["telescope"] = self.rmf.header["TELESCOP"]
        parameters["arf"] = self.arf.filename
        parameters["rmf"] = self.rmf.filename
        return ConvolvedEventList(new_events, parameters)
Example #37
0
 def __init__(self,
              ra0,
              dec0,
              width,
              height,
              num_events,
              theta=0.0,
              prng=None):
     prng = parse_prng(prng)
     ra0 = parse_value(ra0, "deg")
     dec0 = parse_value(dec0, "deg")
     width = parse_value(width, "arcsec")
     height = parse_value(height, "arcsec")
     w = construct_wcs(ra0, dec0)
     x = prng.uniform(low=-0.5 * width, high=0.5 * width, size=num_events)
     y = prng.uniform(low=-0.5 * height, high=0.5 * height, size=num_events)
     coords = rotate_xy(theta, x, y)
     ra, dec = w.wcs_pix2world(coords[0, :], coords[1, :], 1)
     super(RectangleModel, self).__init__(ra, dec, coords[0, :],
                                          coords[1, :], w)
Example #38
0
    def generate_energies(self, t_exp, fov, focal_length=None, 
                          prng=None, quiet=False):
        """
        Generate photon energies from this instrumental 
        background spectrum given an exposure time, 
        effective area, and field of view.

        Parameters
        ----------
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The width of the field of view on a side in 
            arcminutes.
        focal_length : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
            The focal length in meters. Default is to use
            the default focal length of the instrument
            configuration.
        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. 
        quiet : boolean, optional
            If True, log messages will not be displayed when 
            creating energies. Useful if you have to loop over 
            a lot of spectra. Default: False
        """
        t_exp = parse_value(t_exp, "s")
        fov = parse_value(fov, "arcmin")
        prng = parse_prng(prng)
        if focal_length is None:
            focal_length = self.default_focal_length
        else:
            focal_length = parse_value(focal_length, "m")
        rate = fov*fov*self.total_flux.value
        rate *= (focal_length/self.default_focal_length)**2
        energy = _generate_energies(self, t_exp, rate, prng, quiet=quiet)
        flux = np.sum(energy)*erg_per_keV/t_exp
        energies = Energies(energy, flux)
        return energies
Example #39
0
 def __init__(self, e0, emission_field, sigma=None, prng=None):
     self.e0 = parse_value(e0, "keV")
     if isinstance(sigma, (float, YTQuantity)) or (isinstance(sigma, tuple) and isinstance(sigma[0], float)):
         # The broadening is constant
         try:
             self.sigma = parse_value(sigma, "keV")
         except YTUnitConversionError:
             try:
                 self.sigma = parse_value(sigma, "km/s")
                 self.sigma *= self.e0/clight
                 self.sigma.convert_to_units("keV")
             except YTUnitConversionError:
                 raise RuntimeError("Units for sigma must either be in dimensions of "
                                    "energy or velocity! sigma = %s" % sigma)
     else:
         # Either no broadening or a field name
         self.sigma = sigma
     self.emission_field = emission_field
     self.prng = parse_prng(prng)
     self.spectral_norm = None
     self.redshift = None
Example #40
0
 def __init__(self,
              ra0,
              dec0,
              func,
              num_events,
              theta=0.0,
              ellipticity=1.0,
              prng=None):
     prng = parse_prng(prng)
     ra0 = parse_value(ra0, "deg")
     dec0 = parse_value(dec0, "deg")
     theta = parse_value(theta, "deg")
     x, y = generate_radial_events(num_events,
                                   func,
                                   prng,
                                   ellipticity=ellipticity)
     w = construct_wcs(ra0, dec0)
     coords = rotate_xy(theta, x, y)
     ra, dec = w.wcs_pix2world(coords[0, :], coords[1, :], 1)
     super(RadialFunctionModel, self).__init__(ra, dec, coords[0, :],
                                               coords[1, :], w)
Example #41
0
    def from_models(cls,
                    name,
                    spectral_model,
                    spatial_model,
                    t_exp,
                    area,
                    prng=None):
        """
        Generate a single photon list from a spectral and a spatial
        model. 

        Parameters
        ----------
        name : string
            The name of the photon list. This will also be the prefix 
            of any photon list file that is written from this photon list.
        spectral_model : :class:`~soxs.spectra.Spectrum`
            The spectral model to use to generate the event energies.
        spatial_model : :class:`~soxs.spatial.SpatialModel`
            The spatial model to use to generate the event coordinates.
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The effective area in cm**2. If one is creating 
            events for a SIMPUT file, a constant should be 
            used and it must be large enough so that a 
            sufficiently large sample is drawn for the ARF.
        prng : :class:`~numpy.random.RandomState` object, integer, or None
            A pseudo-random number generator. Typically will only 
            be specified if you have a reason to generate the same 
            set of random numbers, such as for a test. Default is None, 
            which sets the seed based on the system time. 
        """
        prng = parse_prng(prng)
        t_exp = parse_value(t_exp, "s")
        area = parse_value(area, "cm**2")
        e = spectral_model.generate_energies(t_exp, area, prng=prng)
        ra, dec = spatial_model.generate_coords(e.size, prng=prng)
        return cls(name, ra, dec, e, e.flux)
Example #42
0
 def __init__(self, e0, emission_field, sigma=None, prng=None):
     self.e0 = parse_value(e0, "keV")
     if isinstance(sigma, (float, YTQuantity)) or (isinstance(
             sigma, tuple) and isinstance(sigma[0], float)):
         # The broadening is constant
         try:
             self.sigma = parse_value(sigma, "keV")
         except YTUnitConversionError:
             try:
                 self.sigma = parse_value(sigma, "km/s")
                 self.sigma *= self.e0 / clight
                 self.sigma.convert_to_units("keV")
             except YTUnitConversionError:
                 raise RuntimeError(
                     "Units for sigma must either be in dimensions of "
                     "energy or velocity! sigma = %s" % sigma)
     else:
         # Either no broadening or a field name
         self.sigma = sigma
     self.emission_field = emission_field
     self.prng = parse_prng(prng)
     self.spectral_norm = None
     self.redshift = None
Example #43
0
    def generate_channel_spectrum(self,
                                  t_exp,
                                  solid_angle,
                                  focal_length=None,
                                  prng=None):
        """
        Generate photon energy channels from this instrumental
        background spectrum given an exposure time,
        effective area, and solid angle.

        Parameters
        ----------
        t_exp : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The exposure time in seconds.
        solid_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
            The solid angle in arcmin**2.
        focal_length : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
            The focal length in meters. Default is to use
            the default focal length of the instrument
            configuration.
        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. 
        """
        t_exp = parse_value(t_exp, "s")
        solid_angle = parse_value(solid_angle, "arcmin**2")
        prng = parse_prng(prng)
        if focal_length is None:
            focal_length = self.default_focal_length
        else:
            focal_length = parse_value(focal_length, "m")
        fac = t_exp * solid_angle  # Backgrounds are normalized to 1 arcmin**2
        fac *= (focal_length / self.default_focal_length)**2
        return prng.poisson(lam=self.count_rate * fac)
Example #44
0
def make_cosmological_sources(exp_time, fov, sky_center, cat_center=None,
                              absorb_model="wabs", nH=0.05, area=40000.0, 
                              output_sources=None, prng=None):
    r"""
    Make an X-ray source made up of contributions from
    galaxy clusters, galaxy groups, and galaxies. 

    Parameters
    ----------
    exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The exposure time of the observation in seconds.
    fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The field of view in arcminutes.
    sky_center : array-like
        The center RA, Dec of the field of view in degrees.
    cat_center : array-like
        The center of the field in the coordinates of the
        halo catalog, which range from -5.0 to 5.0 in 
        degrees in both directions. If None is given, a 
        center will be randomly chosen.
    absorb_model : string, optional
        The absorption model to use, "wabs" or "tbabs". Default: "wabs"
    nH : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The hydrogen column in units of 10**22 atoms/cm**2. 
        Default: 0.05
    area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The effective area in cm**2. It must be large enough 
        so that a sufficiently large sample is drawn for the 
        ARF. Default: 40000.
    output_sources : string, optional
        If set to a filename, output the properties of the sources
        within the field of view to a file. Default: None
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time.
    """
    exp_time = parse_value(exp_time, "s")
    fov = parse_value(fov, "arcmin")
    if nH is not None:
        nH = parse_value(nH, "1.0e22*cm**-2")
    area = parse_value(area, "cm**2")
    prng = parse_prng(prng)
    cosmo = FlatLambdaCDM(H0=100.0*h0, Om0=omega_m)
    agen = ApecGenerator(0.1, 10.0, 10000, broadening=False)

    mylog.info("Creating photons from cosmological sources.")

    mylog.info("Loading halo data from catalog: %s" % halos_cat_file)
    halo_data = h5py.File(halos_cat_file, "r")

    scale = cosmo.kpc_proper_per_arcmin(halo_data["redshift"]).to("Mpc/arcmin")

    # 600. arcmin = 10 degrees (total FOV of catalog = 100 deg^2)
    fov_cat = 10.0*60.0
    w = construct_wcs(*sky_center)

    cat_min = -0.5*fov_cat
    cat_max = 0.5*fov_cat

    if cat_center is None:
        xc, yc = prng.uniform(low=cat_min+0.5*fov, high=cat_max-0.5*fov, size=2)
    else:
        xc, yc = cat_center
        xc *= 60.0
        yc *= 60.0
        xc, yc = np.clip([xc, yc], cat_min+0.5*fov, cat_max-0.5*fov)

    mylog.info("Coordinates of the FOV within the catalog are (%g, %g) deg." %
               (xc/60.0, yc/60.0))

    xlo = (xc-1.1*0.5*fov)*scale.value*h0
    xhi = (xc+1.1*0.5*fov)*scale.value*h0
    ylo = (yc-1.1*0.5*fov)*scale.value*h0
    yhi = (yc+1.1*0.5*fov)*scale.value*h0

    mylog.info("Selecting halos in the FOV.")

    fov_idxs = (halo_data["x"] >= xlo) & (halo_data["x"] <= xhi)
    fov_idxs = (halo_data["y"] >= ylo) & (halo_data["y"] <= yhi) & fov_idxs

    n_halos = fov_idxs.sum()

    mylog.info("Number of halos in the field of view: %d" % n_halos)

    # Now select the specific halos which are in the FOV
    z = halo_data["redshift"][fov_idxs].astype("float64")
    m = halo_data["M500c"][fov_idxs].astype("float64")/h0
    s = scale[fov_idxs].to("Mpc/arcsec").value
    ra0, dec0 = w.wcs_pix2world(halo_data["x"][fov_idxs]/(h0*s)-xc*60.0,
                                halo_data["y"][fov_idxs]/(h0*s)-yc*60.0, 1)

    # Close the halo catalog file
    halo_data.close()

    # Some cosmological stuff
    rho_crit = cosmo.critical_density(z).to("Msun/Mpc**3").value

    # halo temperature and k-corrected flux
    kT = Tx(m, z)
    flux_kcorr = 1.0e-14*lum(m, z)/flux2lum(kT, z)

    # halo scale radius
    r500 = (3.0*m/(4.0*np.pi*500*rho_crit))**(1.0/3.0)
    r500_kpc = r500 * 1000.0
    rc_kpc = r500/conc * 1000.0
    rc = r500/conc/s

    # Halo slope parameter
    beta = prng.normal(loc=0.666, scale=0.05, size=n_halos)
    beta[beta < 0.5] = 0.5

    # Halo ellipticity
    ellip = prng.normal(loc=0.85, scale=0.15, size=n_halos)
    ellip[ellip < 0.0] = 1.0e-3

    # Halo orientation
    theta = 360.0*prng.uniform(size=n_halos)

    # If requested, output the source properties to a file
    if output_sources is not None:
        t = Table([ra0, dec0, rc_kpc, beta, ellip, theta, m, r500_kpc, kT, z, flux_kcorr],
                  names=('RA', 'Dec', 'r_c', 'beta', 'ellipticity',
                         'theta', 'M500c', 'r500', 'kT', 'redshift', 'flux_0.5_2.0_keV'))
        t["RA"].unit = "deg"
        t["Dec"].unit = "deg"
        t["flux_0.5_2.0_keV"].unit = "erg/(cm**2*s)"
        t["r_c"].unit = "kpc"
        t["theta"].unit = "deg"
        t["M500c"].unit = "solMass"
        t["r500"].unit = "kpc"
        t["kT"].unit = "kT"
        t.write(output_sources, format='ascii.ecsv', overwrite=True)

    tot_flux = 0.0
    ee = []
    ra = []
    dec = []

    pbar = tqdm(leave=True, total=n_halos, desc="Generating photons from halos ")
    for halo in range(n_halos):
        spec = agen.get_spectrum(kT[halo], abund, z[halo], 1.0)
        spec.rescale_flux(flux_kcorr[halo], emin=emin, emax=emax, flux_type="energy")
        if nH is not None:
            spec.apply_foreground_absorption(nH, model=absorb_model)
        e = spec.generate_energies(exp_time, area, prng=prng, quiet=True)
        beta_model = BetaModel(ra0[halo], dec0[halo], rc[halo], beta[halo], 
                               ellipticity=ellip[halo], theta=theta[halo])
        xsky, ysky = beta_model.generate_coords(e.size, prng=prng)
        tot_flux += e.flux
        ee.append(e.value)
        ra.append(xsky.value)
        dec.append(ysky.value)
        pbar.update()
    pbar.close()

    ra = np.concatenate(ra)
    dec = np.concatenate(dec)
    ee = np.concatenate(ee)

    mylog.info("Created %d photons from cosmological sources." % ee.size)

    output_events = {"ra": ra, "dec": dec, "energy": ee, 
                     "flux": tot_flux.value}

    return output_events
Example #45
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)
    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
Example #46
0
def make_background_file(out_file,
                         exp_time,
                         instrument,
                         sky_center,
                         overwrite=False,
                         foreground=True,
                         instr_bkgnd=True,
                         ptsrc_bkgnd=True,
                         no_dither=False,
                         dither_params=None,
                         subpixel_res=False,
                         input_sources=None,
                         absorb_model="wabs",
                         nH=0.05,
                         prng=None):
    """
    Make an event file consisting entirely of background events. This will be 
    useful for creating backgrounds that can be added to simulations of sources.

    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.
    overwrite : boolean, optional
        Whether or not to overwrite an existing file with the same name.
        Default: False
    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
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the point-source 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].
    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. 
    """
    prng = parse_prng(prng)
    events, event_params = make_background(exp_time,
                                           instrument,
                                           sky_center,
                                           ptsrc_bkgnd=ptsrc_bkgnd,
                                           foreground=foreground,
                                           instr_bkgnd=instr_bkgnd,
                                           no_dither=no_dither,
                                           dither_params=dither_params,
                                           subpixel_res=subpixel_res,
                                           input_sources=input_sources,
                                           absorb_model=absorb_model,
                                           nH=nH,
                                           prng=prng)
    write_event_file(events, event_params, out_file, overwrite=overwrite)
Example #47
0
    def project_photons(self, normal, sky_center, absorb_model=None,
                        nH=None, no_shifting=False, north_vector=None,
                        sigma_pos=None, kernel="top_hat", prng=None,
                        **kwargs):
        r"""
        Projects photons onto an image plane given a line of sight.
        Returns a new :class:`~pyxsim.event_list.EventList`.

        Parameters
        ----------
        normal : character or array-like
            Normal vector to the plane of projection. If "x", "y", or "z", will
            assume to be along that axis (and will probably be faster). Otherwise,
            should be an off-axis normal vector, e.g [1.0, 2.0, -3.0]
        sky_center : array-like
            Center RA, Dec of the events in degrees.
        absorb_model : string or :class:`~pyxsim.spectral_models.AbsorptionModel`
            A model for foreground galactic absorption, to simulate the
            absorption of events before being detected. This cannot be applied
            here if you already did this step previously in the creation of the
            :class:`~pyxsim.photon_list.PhotonList` instance. Known options for
            strings are "wabs" and "tbabs".
        nH : float, optional
            The foreground column density in units of 10^22 cm^{-2}. Only used
            if absorption is applied.
        no_shifting : boolean, optional
            If set, the photon energies will not be Doppler shifted.
        north_vector : a sequence of floats
            A vector defining the "up" direction. This option sets the
            orientation of the plane of projection. If not set, an arbitrary
            grid-aligned north_vector is chosen. Ignored in the case where a
            particular axis (e.g., "x", "y", or "z") is explicitly specified.
        sigma_pos : float, optional
            Apply a gaussian smoothing operation to the sky positions of the
            events. This may be useful when the binned events appear blocky due
            to their uniform distribution within simulation cells. However, this
            will move the events away from their originating position on the
            sky, and so may distort surface brightness profiles and/or spectra.
            Should probably only be used for visualization purposes. Supply a
            float here to smooth with a standard deviation with this fraction
            of the cell size. Default: None
        kernel : string, optional
            The kernel used when smoothing positions of X-rays originating from
            SPH particles, "gaussian" or "top_hat". Default: "top_hat".
        prng : integer or :class:`~numpy.random.RandomState` object 
            A pseudo-random number generator. Typically will only be specified
            if you have a reason to generate the same set of random numbers,
            such as for a test. Default is to use the :mod:`numpy.random`
            module.

        Examples
        --------
        >>> L = np.array([0.1,-0.2,0.3])
        >>> events = my_photons.project_photons(L, [30., 45.])
        """
        prng = parse_prng(prng)

        scale_shift = -1.0/clight.to("km/s")

        if "smooth_positions" in kwargs:
            issue_deprecation_warning("'smooth_positions' has been renamed to "
                                      "'sigma_pos' and the former is deprecated!")
            sigma_pos = kwargs["smooth_positions"]

        if "redshift_new" in kwargs or "area_new" in kwargs or \
            "exp_time_new" in kwargs or "dist_new" in kwargs:
            issue_deprecation_warning("Changing the redshift, distance, area, or "
                                      "exposure time has been deprecated in "
                                      "project_photons!")

        if sigma_pos is not None and self.parameters["data_type"] == "particles":
            raise RuntimeError("The 'smooth_positions' argument should not be used with "
                               "particle-based datasets!")

        if isinstance(absorb_model, string_types):
            if absorb_model not in absorb_models:
                raise KeyError("%s is not a known absorption model!" % absorb_model)
            absorb_model = absorb_models[absorb_model]
        if absorb_model is not None:
            if nH is None:
                raise RuntimeError("You specified an absorption model, but didn't "
                                   "specify a value for nH!")
            absorb_model = absorb_model(nH)

        sky_center = YTArray(sky_center, "degree")

        n_ph = self.photons["num_photons"]

        if not isinstance(normal, string_types):
            L = np.array(normal)
            orient = Orientation(L, north_vector=north_vector)
            x_hat = orient.unit_vectors[0]
            y_hat = orient.unit_vectors[1]
            z_hat = orient.unit_vectors[2]
        else:
            x_hat = np.zeros(3)
            y_hat = np.zeros(3)
            z_hat = np.zeros(3)

        parameters = {}

        D_A = self.parameters["fid_d_a"]

        events = {}

        eobs = self.photons["energy"].v

        if not no_shifting:
            if comm.rank == 0:
                mylog.info("Doppler-shifting photon energies.")
            if isinstance(normal, string_types):
                shift = self.photons["vel"][:,"xyz".index(normal)]*scale_shift
            else:
                shift = np.dot(self.photons["vel"], z_hat)*scale_shift
            doppler_shift(shift, n_ph, eobs)

        if absorb_model is None:
            det = np.ones(eobs.size, dtype='bool')
            num_det = eobs.size
        else:
            if comm.rank == 0:
                mylog.info("Foreground galactic absorption: using "
                           "the %s model and nH = %g." % (absorb_model._name, nH))
            det = absorb_model.absorb_photons(eobs, prng=prng)
            num_det = det.sum()

        events["eobs"] = YTArray(eobs[det], "keV")

        num_events = comm.mpi_allreduce(num_det)

        if comm.rank == 0:
            mylog.info("%d events have been detected." % num_events)

        if num_det > 0:

            if comm.rank == 0:
                mylog.info("Assigning positions to events.")

            if isinstance(normal, string_types):
                norm = "xyz".index(normal)
            else:
                norm = normal

            xsky, ysky = scatter_events(norm, prng, kernel,
                                        self.parameters["data_type"],
                                        num_det, det, self.photons["num_photons"],
                                        self.photons["pos"].d, self.photons["dx"].d,
                                        x_hat, y_hat)

            if self.parameters["data_type"] == "cells" and sigma_pos is not None:
                if comm.rank == 0:
                    mylog.info("Optionally smoothing sky positions.")
                sigma = sigma_pos*np.repeat(self.photons["dx"].d, n_ph)[det]
                xsky += sigma * prng.normal(loc=0.0, scale=1.0, size=num_det)
                ysky += sigma * prng.normal(loc=0.0, scale=1.0, size=num_det)

            d_a = D_A.to("kpc").v
            xsky /= d_a
            ysky /= d_a

            if comm.rank == 0:
                mylog.info("Converting pixel to sky coordinates.")

            pixel_to_cel(xsky, ysky, sky_center)

        else:

            xsky = []
            ysky = []

        events["xsky"] = YTArray(xsky, "degree")
        events["ysky"] = YTArray(ysky, "degree")

        parameters["exp_time"] = self.parameters["fid_exp_time"]
        parameters["area"] = self.parameters["fid_area"]
        parameters["sky_center"] = sky_center

        return EventList(events, parameters)
Example #48
0
def simulate_spectrum(spec, instrument, exp_time, out_file,
                      instr_bkgnd=False, foreground=False,
                      ptsrc_bkgnd=False, bkgnd_area=None,
                      absorb_model="wabs", nH=0.05,
                      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. If None is supplied, only backgrounds
        will be simulated (if they are turned on).
    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.
    instr_bkgnd : boolean, optional
        Whether or not to include the instrumental/particle background. 
        Default: False
    foreground : boolean, optional
        Whether or not to include the local foreground.
        Default: False
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the unresolved point-source background. 
        Default: False
    bkgnd_area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The area on the sky for the background components, in square arcminutes.
        Default: None, necessary to specify if any of the background components
        are turned on. 
    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
    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, "lynx_lxm", 100000.0, 
    ...                        "my_spec.pi", overwrite=True)
    """
    from soxs.events import _write_spectrum
    from soxs.instrument import RedistributionMatrixFile, \
        AuxiliaryResponseFile
    from soxs.spectra import ConvolvedSpectrum
    from soxs.background.foreground import hm_astro_bkgnd
    from soxs.background.instrument import instrument_backgrounds
    from soxs.background.spectra import BackgroundSpectrum, \
        ConvolvedBackgroundSpectrum
    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)
    if foreground or instr_bkgnd or ptsrc_bkgnd:
        if instrument_spec["grating"]:
            raise NotImplementedError("Backgrounds cannot be included in simulations "
                                      "of gratings spectra at this time!")
        if bkgnd_area is None:
            raise RuntimeError("The 'bkgnd_area' argument must be set if one wants "
                               "to simulate backgrounds! Specify a value in square "
                               "arcminutes.")
        bkgnd_area = np.sqrt(parse_value(bkgnd_area, "arcmin**2"))
    elif spec is None:
        raise RuntimeError("You have specified no source spectrum and no backgrounds!")
    arf_file = get_response_path(instrument_spec["arf"])
    rmf_file = get_response_path(instrument_spec["rmf"])
    arf = AuxiliaryResponseFile(arf_file)
    rmf = RedistributionMatrixFile(rmf_file)

    event_params = {}
    event_params["RESPFILE"] = os.path.split(rmf.filename)[-1]
    event_params["ANCRFILE"] = os.path.split(arf.filename)[-1]
    event_params["TELESCOP"] = rmf.header["TELESCOP"]
    event_params["INSTRUME"] = rmf.header["INSTRUME"]
    event_params["MISSION"] = rmf.header.get("MISSION", "")

    out_spec = np.zeros(rmf.n_ch)

    if spec is not None:
        cspec = ConvolvedSpectrum(spec, arf)
        out_spec += rmf.convolve_spectrum(cspec, exp_time, prng=prng)

    fov = None if bkgnd_area is None else np.sqrt(bkgnd_area)

    if foreground:
        mylog.info("Adding in astrophysical foreground.")
        cspec_frgnd = ConvolvedSpectrum(hm_astro_bkgnd.to_spectrum(fov), arf)
        out_spec += rmf.convolve_spectrum(cspec_frgnd, exp_time, prng=prng)
    if instr_bkgnd and instrument_spec["bkgnd"] is not None:
        mylog.info("Adding in instrumental background.")
        instr_spec = instrument_backgrounds[instrument_spec["bkgnd"]]
        cspec_instr = instr_spec.to_scaled_spectrum(fov,
                                                    instrument_spec["focal_length"])
        out_spec += rmf.convolve_spectrum(cspec_instr, exp_time, prng=prng)
    if ptsrc_bkgnd:
        mylog.info("Adding in background from unresolved point-sources.")
        spec_plaw = BackgroundSpectrum.from_powerlaw(1.45, 0.0, 2.0e-7, emin=0.01,
                                                     emax=10.0, nbins=300000)
        spec_plaw.apply_foreground_absorption(nH, model=absorb_model)
        cspec_plaw = ConvolvedBackgroundSpectrum(spec_plaw.to_spectrum(fov), arf)
        out_spec += rmf.convolve_spectrum(cspec_plaw, exp_time, prng=prng)

    bins = (np.arange(rmf.n_ch)+rmf.cmin).astype("int32")

    _write_spectrum(bins, out_spec, exp_time, rmf.header["CHANTYPE"], 
                    event_params, out_file, overwrite=overwrite)
Example #49
0
 def __init__(self, prng=None):
     self.spectral_norm = None
     self.redshift = None
     self.prng = parse_prng(prng)
Example #50
0
def make_background_file(out_file, exp_time, instrument, sky_center,
                         overwrite=False, foreground=True, instr_bkgnd=True,
                         ptsrc_bkgnd=True, no_dither=False, dither_params=None,
                         subpixel_res=False, input_sources=None, 
                         absorb_model="wabs", nH=0.05, prng=None):
    """
    Make an event file consisting entirely of background events. This will be 
    useful for creating backgrounds that can be added to simulations of sources.

    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.
    overwrite : boolean, optional
        Whether or not to overwrite an existing file with the same name.
        Default: False
    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
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the point-source 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].
    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. 
    """
    prng = parse_prng(prng)
    events, event_params = make_background(exp_time, instrument, sky_center, 
                                           ptsrc_bkgnd=ptsrc_bkgnd, 
                                           foreground=foreground, 
                                           instr_bkgnd=instr_bkgnd,
                                           no_dither=no_dither,
                                           dither_params=dither_params, 
                                           subpixel_res=subpixel_res,
                                           input_sources=input_sources,
                                           absorb_model=absorb_model,
                                           nH=nH, prng=prng)
    write_event_file(events, event_params, out_file, overwrite=overwrite)
Example #51
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)
    if not instrument_spec["imaging"]:
        raise RuntimeError("Instrument '%s' is not " % instrument_spec["name"] +
                           "designed for imaging observations!")
    fov = instrument_spec["fov"]

    input_events = defaultdict(list)

    arf_file = get_response_path(instrument_spec["arf"])
    arf = AuxiliaryResponseFile(arf_file)
    rmf_file = get_response_path(instrument_spec["rmf"])
    rmf = RedistributionMatrixFile(rmf_file)

    if ptsrc_bkgnd:
        mylog.info("Adding in point-source background.")
        ptsrc_events = make_ptsrc_background(exp_time, fov, sky_center,
                                             area=1.2*arf.max_area,
                                             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.n_ch,
                        "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, rmf, prng=prng)
        for key in bkg_events:
            events[key] = np.concatenate([events[key], bkg_events[key]])

    return events, event_params
Example #52
0
def make_xrb_particles(data_source, age_field, scale_length, 
                       sfr_time_range=(1.0, "Gyr"), prng=None):
    r"""
    This routine generates an in-memory dataset composed of X-ray binary particles
    from an input data source containing star particles. 

    Parameters
    ----------
    data_source : :class:`~yt.data_objects.data_containers.YTSelectionContainer`
        The yt data source to obtain the data from, such as a sphere, box, disk, 
        etc.
    age_field : string or (type, name) field tuple
        The stellar age field. Must be in some kind of time units. 
    scale_length : string, (ftype, fname) tuple, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`
        The radial length scale over which to scatter the XRB particles
        from their parent star particle. Can be the name of a smoothing
        length field for the stars, a (value, unit) tuple, or a YTQuantity.
    sfr_time_range : string, (ftype, fname) tuple, (value, unit) tuple, :class:`~yt.units.yt_array.YTQuantity`, or :class:`~astropy.units.Quantity`, optional
        The recent time range over which to calculate the star formation rate from
        the current time in the dataset. Default: 1.0 Gyr
    prng : integer or :class:`~numpy.random.RandomState` object 
        A pseudo-random number generator. Typically will only be specified
        if you have a reason to generate the same set of random numbers, such as for a
        test. Default is to use the :mod:`numpy.random` module.
    """
    prng = parse_prng(prng)

    ds = data_source.ds

    ptype = data_source._determine_fields(age_field)[0][0]

    t = data_source[age_field].to("Gyr")
    m = data_source[(ptype, "particle_mass")].to("Msun")

    sfr_time_range = parse_value(sfr_time_range, "Gyr")

    recent = t < sfr_time_range

    n_recent = recent.sum()

    if n_recent == 0:
        sfr = 0.0
    else:
        sfr = (m[recent].sum()/sfr_time_range).to("Msun/yr").v

    mylog.info("%d star particles were formed in the last " % n_recent +
               "%s for a SFR of %4.1f Msun/yr." % (sfr_time_range, sfr))

    mtot = m.sum()

    npart = m.size

    scale_field = None
    if isinstance(scale_length, tuple):
        if isinstance(scale_length[0], string_types):
            scale_field = scale_length
    elif isinstance(scale_length, string_types):
        scale_field = (ptype, scale_length)

    if scale_field is None:
        if isinstance(scale_length, tuple):
            scale = YTArray([scale_length[0]]*npart, scale_length[1])
        elif isinstance(scale_length, YTQuantity):
            scale = YTArray([scale_length]*npart)
        else:
            scale = YTArray([scale_length[0]]*npart, "kpc")
    else:
        scale = data_source[scale_length]

    scale = scale.to('kpc').d

    N_l = lmxb_cdf(Lcut)*mtot.v*1.0e-11
    N_h = hmxb_cdf(Lcut)*sfr

    N_all = N_l+N_h

    if N_all == 0.0:
        raise RuntimeError("There are no X-ray binaries to generate!")

    # Compute conversion factors from luminosity to count rate

    lmxb_factor = get_scale_factor(alpha_lmxb, emin_lmxb, emax_lmxb)
    hmxb_factor = get_scale_factor(alpha_hmxb, emin_hmxb, emax_hmxb)

    xp = []
    yp = []
    zp = []
    vxp = []
    vyp = []
    vzp = []
    lp = []
    rp = []
    ap = []

    if N_l > 0.0:

        F_l = np.zeros(nbins+1)
        for i in range(1, nbins+1):
            F_l[i] = lmxb_cdf(Lbins[i]) 
        F_l /= F_l[-1]
        invcdf_l = InterpolatedUnivariateSpline(F_l, logLbins)

        n_l = prng.poisson(lam=N_l*m/mtot)

        mylog.info("Number of low-mass X-ray binaries: %s" % n_l.sum())

        for i, n in enumerate(n_l):
            if n > 0:
                randvec = prng.uniform(size=n)
                l = YTArray(10**invcdf_l(randvec)*1.0e38, "erg/s")
                r = YTArray(l.v*lmxb_factor, "photons/s/keV")
                # Now convert output luminosities to bolometric
                l *= bc_lmxb
                x = YTArray(prng.normal(scale=scale[i], size=n), "kpc")
                y = YTArray(prng.normal(scale=scale[i], size=n), "kpc")
                z = YTArray(prng.normal(scale=scale[i], size=n), "kpc")
                x += data_source[ptype, "particle_position_x"][i].to("kpc")
                y += data_source[ptype, "particle_position_y"][i].to("kpc")
                z += data_source[ptype, "particle_position_z"][i].to("kpc")
                vx = YTArray([data_source[ptype, "particle_velocity_x"][i]]*n).to('km/s')
                vy = YTArray([data_source[ptype, "particle_velocity_y"][i]]*n).to('km/s')
                vz = YTArray([data_source[ptype, "particle_velocity_z"][i]]*n).to('km/s')
                xp.append(x)
                yp.append(y)
                zp.append(z)
                vxp.append(vx)
                vyp.append(vy)
                vzp.append(vz)
                lp.append(l)
                rp.append(r)
                ap.append(np.array([alpha_lmxb]*n))

    if N_h > 0.0:

        F_h = np.zeros(nbins+1)
        for i in range(1, nbins+1):
            F_h[i] = hmxb_cdf(Lbins[i])
        F_h /= F_h[-1]
        invcdf_h = InterpolatedUnivariateSpline(F_h, logLbins)

        n_h = prng.poisson(lam=N_h*m/mtot)

        mylog.info("Number of high-mass X-ray binaries: %s" % n_h.sum())

        for i, n in enumerate(n_h):
            if n > 0:
                randvec = prng.uniform(size=n)
                l = YTArray(10**invcdf_h(randvec)*1.0e38, "erg/s")
                r = YTArray(l.v*hmxb_factor, "photons/s/keV")
                # Now convert output luminosities to bolometric
                l *= bc_hmxb
                x = YTArray(prng.normal(scale=scale[i], size=n), "kpc")
                y = YTArray(prng.normal(scale=scale[i], size=n), "kpc")
                z = YTArray(prng.normal(scale=scale[i], size=n), "kpc")
                x += data_source[ptype, "particle_position_x"][i].to("kpc")
                y += data_source[ptype, "particle_position_y"][i].to("kpc")
                z += data_source[ptype, "particle_position_z"][i].to("kpc")
                vx = YTArray([data_source[ptype, "particle_velocity_x"][i]]*n).to('km/s')
                vy = YTArray([data_source[ptype, "particle_velocity_y"][i]]*n).to('km/s')
                vz = YTArray([data_source[ptype, "particle_velocity_z"][i]]*n).to('km/s')
                xp.append(x)
                yp.append(y)
                zp.append(z)
                vxp.append(vx)
                vyp.append(vy)
                vzp.append(vz)
                lp.append(l)
                rp.append(r)
                ap.append(np.array([alpha_hmxb]*n))

    xp = uconcatenate(xp)
    yp = uconcatenate(yp)
    zp = uconcatenate(zp)
    vxp = uconcatenate(vxp)
    vyp = uconcatenate(vyp)
    vzp = uconcatenate(vzp)
    lp = uconcatenate(lp)
    rp = uconcatenate(rp)
    ap = uconcatenate(ap)

    data = {"particle_position_x": (xp.d, str(xp.units)),
            "particle_position_y": (yp.d, str(yp.units)),
            "particle_position_z": (zp.d, str(zp.units)),
            "particle_velocity_x": (vxp.d, str(vxp.units)),
            "particle_velocity_y": (vyp.d, str(vyp.units)),
            "particle_velocity_z": (vzp.d, str(vzp.units)),
            "particle_luminosity": (lp.d, str(lp.units)),
            "particle_count_rate": (rp.d, str(rp.units)),
            "particle_spectral_index": ap}

    dle = ds.domain_left_edge.to("kpc").v
    dre = ds.domain_right_edge.to("kpc").v

    bbox = np.array([[dle[i], dre[i]] for i in range(3)])

    new_ds = load_particles(data, bbox=bbox, length_unit="kpc",
                            time_unit="Myr", mass_unit="Msun", 
                            velocity_unit="km/s")

    return new_ds
Example #53
0
def simulate_spectrum(spec,
                      instrument,
                      exp_time,
                      out_file,
                      instr_bkgnd=False,
                      foreground=False,
                      ptsrc_bkgnd=False,
                      bkgnd_area=None,
                      absorb_model="wabs",
                      nH=0.05,
                      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. If None is supplied, only backgrounds
        will be simulated (if they are turned on).
    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.
    instr_bkgnd : boolean, optional
        Whether or not to include the instrumental/particle background. 
        Default: False
    foreground : boolean, optional
        Whether or not to include the local foreground.
        Default: False
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the unresolved point-source background. 
        Default: False
    bkgnd_area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The area on the sky for the background components, in square arcminutes.
        Default: None, necessary to specify if any of the background components
        are turned on. 
    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
    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, "lynx_lxm", 100000.0, 
    ...                        "my_spec.pi", overwrite=True)
    """
    from soxs.events import _write_spectrum
    from soxs.response import RedistributionMatrixFile, \
        AuxiliaryResponseFile
    from soxs.spectra import ConvolvedSpectrum
    from soxs.background.foreground import hm_astro_bkgnd
    from soxs.background.spectra import BackgroundSpectrum
    from soxs.background.instrument import InstrumentalBackground
    prng = parse_prng(prng)
    exp_time = parse_value(exp_time, "s")
    try:
        instrument_spec = instrument_registry[instrument]
    except KeyError:
        raise KeyError(
            f"Instrument {instrument} is not in the instrument registry!")
    if foreground or instr_bkgnd or ptsrc_bkgnd:
        if instrument_spec["grating"]:
            raise NotImplementedError(
                "Backgrounds cannot be included in simulations "
                "of gratings spectra at this time!")
        if bkgnd_area is None:
            raise RuntimeError(
                "The 'bkgnd_area' argument must be set if one wants "
                "to simulate backgrounds! Specify a value in square "
                "arcminutes.")
        bkgnd_area = np.sqrt(parse_value(bkgnd_area, "arcmin**2"))
    elif spec is None:
        raise RuntimeError(
            "You have specified no source spectrum and no backgrounds!")
    arf_file = get_data_file(instrument_spec["arf"])
    rmf_file = get_data_file(instrument_spec["rmf"])
    arf = AuxiliaryResponseFile(arf_file)
    rmf = RedistributionMatrixFile(rmf_file)

    event_params = {
        "RESPFILE": os.path.split(rmf.filename)[-1],
        "ANCRFILE": os.path.split(arf.filename)[-1],
        "TELESCOP": rmf.header["TELESCOP"],
        "INSTRUME": rmf.header["INSTRUME"],
        "MISSION": rmf.header.get("MISSION", "")
    }

    out_spec = np.zeros(rmf.n_ch)

    if spec is not None:
        cspec = ConvolvedSpectrum.convolve(spec, arf)
        out_spec += rmf.convolve_spectrum(cspec, exp_time, prng=prng)

    fov = None if bkgnd_area is None else np.sqrt(bkgnd_area)

    if foreground:
        mylog.info("Adding in astrophysical foreground.")
        cspec_frgnd = ConvolvedSpectrum.convolve(
            hm_astro_bkgnd.to_spectrum(fov), arf)
        out_spec += rmf.convolve_spectrum(cspec_frgnd, exp_time, prng=prng)
    if instr_bkgnd and instrument_spec["bkgnd"] is not None:
        mylog.info("Adding in instrumental background.")
        bkgnd_spec = instrument_spec["bkgnd"]
        # Temporary hack for ACIS-S
        if "aciss" in instrument_spec["name"]:
            bkgnd_spec = bkgnd_spec[1]
        bkgnd_spec = InstrumentalBackground.from_filename(
            bkgnd_spec[0], bkgnd_spec[1], instrument_spec['focal_length'])
        out_spec += bkgnd_spec.generate_channel_spectrum(exp_time,
                                                         bkgnd_area,
                                                         prng=prng)
    if ptsrc_bkgnd:
        mylog.info("Adding in background from unresolved point-sources.")
        spec_plaw = BackgroundSpectrum.from_powerlaw(1.45,
                                                     0.0,
                                                     2.0e-7,
                                                     emin=0.01,
                                                     emax=10.0,
                                                     nbins=300000)
        spec_plaw.apply_foreground_absorption(nH, model=absorb_model)
        cspec_plaw = ConvolvedSpectrum.convolve(spec_plaw.to_spectrum(fov),
                                                arf)
        out_spec += rmf.convolve_spectrum(cspec_plaw, exp_time, prng=prng)

    bins = (np.arange(rmf.n_ch) + rmf.cmin).astype("int32")

    _write_spectrum(bins,
                    out_spec,
                    exp_time,
                    rmf.header["CHANTYPE"],
                    event_params,
                    out_file,
                    overwrite=overwrite)
Example #54
0
    def generate_events(self, area, exp_time, angular_width,
                        source_model, sky_center, parameters=None,
                        velocity_fields=None, absorb_model=None,
                        nH=None, no_shifting=False, sigma_pos=None,
                        prng=None):
        """
        Generate projected events from a light cone simulation. 

        Parameters
        ----------
        area : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`
            The collecting area to determine the number of events. If units are
            not specified, it is assumed to be in cm^2.
        exp_time : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`
            The exposure time to determine the number of events. If units are
            not specified, it is assumed to be in seconds.
        angular_width : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`
            The angular width of the light cone simulation. If units are not
            specified, it is assumed to be in degrees.
        source_model : :class:`~pyxsim.source_models.SourceModel`
            A source model used to generate the events.
        sky_center : array-like
            Center RA, Dec of the events in degrees.
        parameters : dict, optional
            A dictionary of parameters to be passed for the source model to use,
            if necessary.
        velocity_fields : list of fields
            The yt fields to use for the velocity. If not specified, the following will
            be assumed:
            ['velocity_x', 'velocity_y', 'velocity_z'] for grid datasets
            ['particle_velocity_x', 'particle_velocity_y', 'particle_velocity_z'] for particle datasets
        absorb_model : string or :class:`~pyxsim.spectral_models.AbsorptionModel` 
            A model for foreground galactic absorption, to simulate the absorption
            of events before being detected. This cannot be applied here if you 
            already did this step previously in the creation of the 
            :class:`~pyxsim.photon_list.PhotonList` instance. Known options for 
            strings are "wabs" and "tbabs".
        nH : float, optional
            The foreground column density in units of 10^22 cm^{-2}. Only used if
            absorption is applied.
        no_shifting : boolean, optional
            If set, the photon energies will not be Doppler shifted.
        sigma_pos : float, optional
            Apply a gaussian smoothing operation to the sky positions of the
            events. This may be useful when the binned events appear blocky due
            to their uniform distribution within simulation cells. However, this
            will move the events away from their originating position on the
            sky, and so may distort surface brightness profiles and/or spectra.
            Should probably only be used for visualization purposes. Supply a
            float here to smooth with a standard deviation with this fraction
            of the cell size. Default: None
        prng : integer or :class:`~numpy.random.RandomState` object
            A pseudo-random number generator. Typically will only be specified
            if you have a reason to generate the same set of random numbers, such as for a
            test. Default is to use the :mod:`numpy.random` module.
        """
        prng = parse_prng(prng)

        area = parse_value(area, "cm**2")
        exp_time = parse_value(exp_time, "s")
        aw = parse_value(angular_width, "deg")

        tot_events = defaultdict(list)

        for output in self.light_cone_solution:
            ds = load(output["filename"])
            ax = output["projection_axis"]
            c = output["projection_center"]*ds.domain_width + ds.domain_left_edge
            le = c.copy()
            re = c.copy()
            width = ds.quan(aw*output["box_width_per_angle"], "unitary").to("code_length")
            depth = ds.domain_width[ax].in_units("code_length")*output["box_depth_fraction"]
            le[ax] -= 0.5*depth
            re[ax] += 0.5*depth
            for off_ax in axes_lookup[ax]:
                le[off_ax] -= 0.5*width
                re[off_ax] += 0.5*width
            reg = ds.box(le, re)
            photons = PhotonList.from_data_source(reg, output['redshift'], area,
                                                  exp_time, source_model,
                                                  parameters=parameters,
                                                  center=c,
                                                  velocity_fields=velocity_fields,
                                                  cosmology=ds.cosmology)
            if sum(photons["num_photons"]) > 0:
                events = photons.project_photons("xyz"[ax], sky_center,
                                                 absorb_model=absorb_model, nH=nH,
                                                 no_shifting=no_shifting, 
                                                 sigma_pos=sigma_pos,
                                                 prng=prng)
                if events.num_events > 0:
                    tot_events["xsky"].append(events["xsky"])
                    tot_events["ysky"].append(events["ysky"])
                    tot_events["eobs"].append(events["eobs"])
                del events

            del photons

        parameters = {"exp_time": exp_time,
                      "area": area, 
                      "sky_center": YTArray(sky_center, "deg")}

        for key in tot_events:
            tot_events[key] = uconcatenate(tot_events[key])

        return EventList(tot_events, parameters)
Example #55
0
    def generate_events(self,
                        area,
                        exp_time,
                        angular_width,
                        source_model,
                        sky_center,
                        parameters=None,
                        velocity_fields=None,
                        absorb_model=None,
                        nH=None,
                        no_shifting=False,
                        sigma_pos=None,
                        prng=None):
        """
        Generate projected events from a light cone simulation. 

        Parameters
        ----------
        area : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`
            The collecting area to determine the number of events. If units are
            not specified, it is assumed to be in cm^2.
        exp_time : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`
            The exposure time to determine the number of events. If units are
            not specified, it is assumed to be in seconds.
        angular_width : float, (value, unit) tuple, or :class:`~yt.units.yt_array.YTQuantity`
            The angular width of the light cone simulation. If units are not
            specified, it is assumed to be in degrees.
        source_model : :class:`~pyxsim.source_models.SourceModel`
            A source model used to generate the events.
        sky_center : array-like
            Center RA, Dec of the events in degrees.
        parameters : dict, optional
            A dictionary of parameters to be passed for the source model to use,
            if necessary.
        velocity_fields : list of fields
            The yt fields to use for the velocity. If not specified, the following will
            be assumed:
            ['velocity_x', 'velocity_y', 'velocity_z'] for grid datasets
            ['particle_velocity_x', 'particle_velocity_y', 'particle_velocity_z'] for particle datasets
        absorb_model : string or :class:`~pyxsim.spectral_models.AbsorptionModel` 
            A model for foreground galactic absorption, to simulate the absorption
            of events before being detected. This cannot be applied here if you 
            already did this step previously in the creation of the 
            :class:`~pyxsim.photon_list.PhotonList` instance. Known options for 
            strings are "wabs" and "tbabs".
        nH : float, optional
            The foreground column density in units of 10^22 cm^{-2}. Only used if
            absorption is applied.
        no_shifting : boolean, optional
            If set, the photon energies will not be Doppler shifted.
        sigma_pos : float, optional
            Apply a gaussian smoothing operation to the sky positions of the
            events. This may be useful when the binned events appear blocky due
            to their uniform distribution within simulation cells. However, this
            will move the events away from their originating position on the
            sky, and so may distort surface brightness profiles and/or spectra.
            Should probably only be used for visualization purposes. Supply a
            float here to smooth with a standard deviation with this fraction
            of the cell size. Default: None
        prng : integer or :class:`~numpy.random.RandomState` object
            A pseudo-random number generator. Typically will only be specified
            if you have a reason to generate the same set of random numbers, such as for a
            test. Default is to use the :mod:`numpy.random` module.
        """
        prng = parse_prng(prng)

        area = parse_value(area, "cm**2")
        exp_time = parse_value(exp_time, "s")
        aw = parse_value(angular_width, "deg")

        tot_events = defaultdict(list)

        for output in self.light_cone_solution:
            ds = load(output["filename"])
            ax = output["projection_axis"]
            c = output[
                "projection_center"] * ds.domain_width + ds.domain_left_edge
            le = c.copy()
            re = c.copy()
            width = ds.quan(aw * output["box_width_per_angle"],
                            "unitary").to("code_length")
            depth = ds.domain_width[ax].in_units(
                "code_length") * output["box_depth_fraction"]
            le[ax] -= 0.5 * depth
            re[ax] += 0.5 * depth
            for off_ax in axes_lookup[ax]:
                le[off_ax] -= 0.5 * width
                re[off_ax] += 0.5 * width
            reg = ds.box(le, re)
            photons = PhotonList.from_data_source(
                reg,
                output['redshift'],
                area,
                exp_time,
                source_model,
                parameters=parameters,
                center=c,
                velocity_fields=velocity_fields,
                cosmology=ds.cosmology)
            if sum(photons["num_photons"]) > 0:
                events = photons.project_photons("xyz"[ax],
                                                 sky_center,
                                                 absorb_model=absorb_model,
                                                 nH=nH,
                                                 no_shifting=no_shifting,
                                                 sigma_pos=sigma_pos,
                                                 prng=prng)
                if events.num_events > 0:
                    tot_events["xsky"].append(events["xsky"])
                    tot_events["ysky"].append(events["ysky"])
                    tot_events["eobs"].append(events["eobs"])
                del events

            del photons

        parameters = {
            "exp_time": exp_time,
            "area": area,
            "sky_center": YTArray(sky_center, "deg")
        }

        for key in tot_events:
            tot_events[key] = uconcatenate(tot_events[key])

        return EventList(tot_events, parameters)
Example #56
0
def make_ptsrc_background(exp_time, fov, sky_center, absorb_model="wabs", 
                          nH=0.05, area=40000.0, input_sources=None, 
                          output_sources=None, prng=None):
    r"""
    Make a point-source background.

    Parameters
    ----------
    exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The exposure time of the observation in seconds.
    fov : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The field of view in arcminutes.
    sky_center : array-like
        The center RA, Dec of the field of view in degrees.
    absorb_model : string, optional
        The absorption model to use, "wabs" or "tbabs". Default: "wabs"
    nH : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The hydrogen column in units of 10**22 atoms/cm**2. 
        Default: 0.05
    area : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The effective area in cm**2. It must be large enough 
        so that a sufficiently large sample is drawn for the 
        ARF. Default: 40000.
    input_sources : string, optional
        If set to a filename, input the source positions, fluxes,
        and spectral indices from an ASCII table instead of generating
        them. Default: None
    output_sources : string, optional
        If set to a filename, output the properties of the sources
        within the field of view to a file. Default: None
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 
    """
    prng = parse_prng(prng)

    exp_time = parse_value(exp_time, "s")
    fov = parse_value(fov, "arcmin")
    if nH is not None:
        nH = parse_value(nH, "1.0e22*cm**-2")
    area = parse_value(area, "cm**2")
    if input_sources is None:
        ra0, dec0, fluxes, ind = generate_sources(exp_time, fov, sky_center,
                                                  area=area, prng=prng)
        num_sources = fluxes.size
    else:
        mylog.info("Reading in point-source properties from %s." % input_sources)
        t = ascii.read(input_sources)
        ra0 = t["RA"].data
        dec0 = t["Dec"].data
        fluxes = t["flux_0.5_2.0_keV"].data
        ind = t["index"].data
        num_sources = fluxes.size

    mylog.debug("Generating spectra from %d sources." % num_sources)

    # If requested, output the source properties to a file
    if output_sources is not None:
        t = Table([ra0, dec0, fluxes, ind],
                  names=('RA', 'Dec', 'flux_0.5_2.0_keV', 'index'))
        t["RA"].unit = "deg"
        t["Dec"].unit = "deg"
        t["flux_0.5_2.0_keV"].unit = "erg/(cm**2*s)"
        t["index"].unit = ""
        t.write(output_sources, format='ascii.ecsv', overwrite=True)

    # Pre-calculate for optimization
    eratio = spec_emax/spec_emin
    oma = 1.0-ind
    invoma = 1.0/oma
    invoma[oma == 0.0] = 1.0
    fac1 = spec_emin**oma
    fac2 = spec_emax**oma-fac1

    fluxscale = get_flux_scale(ind, fb_emin, fb_emax, spec_emin, spec_emax)

    # Using the energy flux, determine the photon flux by simple scaling
    ref_ph_flux = fluxes*fluxscale*keV_per_erg
    # Now determine the number of photons we will generate
    n_photons = prng.poisson(ref_ph_flux*exp_time*area)

    all_energies = []
    all_ra = []
    all_dec = []

    for i, nph in enumerate(n_photons):
        if nph > 0:
            # Generate the energies in the source frame
            u = prng.uniform(size=nph)
            if ind[i] == 1.0:
                energies = spec_emin*(eratio**u)
            else:
                energies = fac1[i] + u*fac2[i]
                energies **= invoma[i]
            # Assign positions for this source
            ra = ra0[i]*np.ones(nph)
            dec = dec0[i]*np.ones(nph)

            all_energies.append(energies)
            all_ra.append(ra)
            all_dec.append(dec)

    mylog.debug("Finished generating spectra.")

    all_energies = np.concatenate(all_energies)
    all_ra = np.concatenate(all_ra)
    all_dec = np.concatenate(all_dec)

    all_nph = all_energies.size

    # Remove some of the photons due to Galactic foreground absorption.
    # We will throw a lot of stuff away, but this is more general and still
    # faster. 
    if nH is not None:
        if absorb_model == "wabs":
            absorb = get_wabs_absorb(all_energies, nH)
        elif absorb_model == "tbabs":
            absorb = get_tbabs_absorb(all_energies, nH)
        randvec = prng.uniform(size=all_energies.size)
        all_energies = all_energies[randvec < absorb]
        all_ra = all_ra[randvec < absorb]
        all_dec = all_dec[randvec < absorb]
        all_nph = all_energies.size
    mylog.debug("%d photons remain after foreground galactic absorption." % all_nph)

    all_flux = np.sum(all_energies)*erg_per_keV/(exp_time*area)

    output_events = {"ra": all_ra, "dec": all_dec, 
                     "energy": all_energies, "flux": all_flux}

    return output_events