Ejemplo n.º 1
0
def make_diffuse_background(bkg_events, event_params, rmf, prng=None):
    from soxs.instrument import perform_dither

    n_e = bkg_events["energy"].size

    bkg_events['time'] = prng.uniform(size=n_e,
                                      low=0.0,
                                      high=event_params["exposure_time"])

    x_offset, y_offset = perform_dither(bkg_events["time"],
                                        event_params["dither_params"])

    rot_mat = get_rot_mat(event_params["roll_angle"])

    det = np.array([
        bkg_events["detx"] + x_offset - event_params["aimpt_coords"][0],
        bkg_events["dety"] + y_offset - event_params["aimpt_coords"][1]
    ])
    pix = np.dot(rot_mat.T, det)

    bkg_events["xpix"] = pix[0, :] + event_params['pix_center'][0]
    bkg_events["ypix"] = pix[1, :] + event_params['pix_center'][1]

    mylog.info("Scattering energies with RMF %s." %
               os.path.split(rmf.filename)[-1])
    bkg_events = rmf.scatter_energies(bkg_events, prng=prng)

    return bkg_events
Ejemplo n.º 2
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)
Ejemplo n.º 3
0
def add_background_from_file(events, event_params, bkg_file):
    f = pyfits.open(bkg_file)

    hdu = f["EVENTS"]

    sexp = event_params["exposure_time"]
    bexp = hdu.header["EXPOSURE"]

    if event_params["exposure_time"] > hdu.header["EXPOSURE"]:
        raise RuntimeError(
            "The background file does not have sufficient exposure! Source "
            "exposure time %g, background exposure time %g." % (sexp, bexp))

    for k1, k2 in key_map.items():
        if event_params[k1] != hdu.header[k2]:
            raise RuntimeError("'%s' keyword does not match! %s vs. %s" %
                               (k1, event_params[k1], hdu.header[k2]))
    rmf1 = os.path.split(event_params["rmf"])[-1]
    rmf2 = hdu.header["RESPFILE"]
    arf1 = os.path.split(event_params["arf"])[-1]
    arf2 = hdu.header["ANCRFILE"]
    if rmf1 != rmf2:
        raise RuntimeError("RMFs do not match! %s vs. %s" % (rmf1, rmf2))
    if arf1 != arf2:
        raise RuntimeError("ARFs do not match! %s vs. %s" % (arf1, arf2))

    idxs = hdu.data["TIME"] < sexp

    mylog.info("Adding %d background events from %s." % (idxs.sum(), bkg_file))

    if event_params["roll_angle"] == hdu.header["ROLL_PNT"]:
        xpix = hdu.data["X"][idxs]
        ypix = hdu.data["Y"][idxs]
    else:
        rot_mat = get_rot_mat(event_params["roll_angle"])
        xpix, ypix = np.dot(
            rot_mat.T,
            np.array([hdu.data["DETX"][idxs], hdu.data["DETY"][idxs]]))
        xpix += hdu.header["TCRPX2"]
        ypix += hdu.header["TCRPX3"]

    all_events = {}
    for key in [
            "detx", "dety", "time", "ccd_id", event_params["channel_type"]
    ]:
        all_events[key] = np.concatenate(
            [events[key], hdu.data[key.upper()][idxs]])
    all_events["xpix"] = np.concatenate([events["xpix"], xpix])
    all_events["ypix"] = np.concatenate([events["ypix"], ypix])
    all_events["energy"] = np.concatenate(
        [events["energy"], hdu.data["ENERGY"][idxs] / 1000.0])

    f.close()

    return all_events
Ejemplo n.º 4
0
def _generate_energies(spec, t_exp, rate, prng, quiet=False):
    cumspec = spec.cumspec
    n_ph = prng.poisson(t_exp*rate)
    if not quiet:
        mylog.info("Creating %d energies from this spectrum." % n_ph)
    randvec = prng.uniform(size=n_ph)
    randvec.sort()
    e = np.interp(randvec, cumspec, spec.ebins.value)
    if not quiet:
        mylog.info("Finished creating energies.")
    return e
Ejemplo n.º 5
0
def _generate_energies(spec, t_exp, rate, prng, quiet=False):
    cumspec = spec.cumspec
    n_ph = prng.poisson(t_exp*rate)
    if not quiet:
        mylog.info("Creating %d energies from this spectrum." % n_ph)
    randvec = prng.uniform(size=n_ph)
    randvec.sort()
    e = np.interp(randvec, cumspec, spec.ebins.value)
    if not quiet:
        mylog.info("Finished creating energies.")
    return e
Ejemplo n.º 6
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)
Ejemplo n.º 7
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
Ejemplo n.º 8
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)
Ejemplo n.º 9
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}
Ejemplo n.º 10
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)
Ejemplo n.º 11
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
Ejemplo n.º 12
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)
Ejemplo n.º 13
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)
Ejemplo n.º 14
0
    def write_catalog(self, overwrite=False):
        """
        Write the SIMPUT catalog and associated photon lists to disk.

        Parameters
        ----------
        overwrite : boolean, optional
            Whether or not to overwrite an existing file with 
            the same name. Default: False
        """
        for i, phlist in enumerate(self.photon_lists):
            if i == 0:
                append = False
                mylog.info("Writing SIMPUT catalog file %s_simput.fits." % self.name)
            else:
                append = True
            mylog.info("Writing SIMPUT photon list file %s_phlist.fits." % phlist.name)
            phlist.write_photon_list(self.name, append=append, overwrite=overwrite)
Ejemplo n.º 15
0
    def generate_energies(self, t_exp, area, prng=None):
        """
        Generate photon energies from this spectrum given an
        exposure time and effective area.

        Parameters
        ----------
        t_exp : float
            The exposure time in seconds.
        area : float or NumPy array
            The effective area in cm**2, or the effective area multiplied by the field of view
            area in cm**2*arcmin**2 if the spectrum has units of intensity. 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 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.
        """
        if "arcmin" in self._units:
            A = u.Quantity(area, "cm**2*arcmin**2")
        else:
            A = u.Quantity(area, "cm**2")
        if prng is None:
            prng = np.random
        if isinstance(area, np.ndarray):
            rate_arr = A*self.flux*self.de
            rate = rate_arr.sum()
            cumspec = np.cumsum(rate_arr.value)
            cumspec = np.insert(cumspec, 0, 0.0)
            cumspec /= cumspec[-1]
        else:
            rate = A*self.total_flux
            cumspec = self.cumspec
        n_ph = np.modf(t_exp*rate.value)
        n_ph = np.int64(n_ph[1]) + np.int64(n_ph[0] >= prng.uniform())
        mylog.info("Creating %d events from this spectrum." % n_ph)
        randvec = prng.uniform(size=n_ph)
        randvec.sort()
        energy = np.interp(randvec, cumspec, self.ebins.value)
        flux = np.sum(energy)*erg_per_keV/t_exp/area
        energies = Energies(energy, flux)
        return energies
Ejemplo n.º 16
0
    def _write_source(self,
                      filename,
                      extver,
                      img_extver=None,
                      overwrite=False):
        coldefs, header = self._get_source_hdu()

        tbhdu = pyfits.BinTableHDU.from_columns(coldefs)
        tbhdu.name = self.src_type.upper()

        if self.src_type == "phlist":
            hduclas1 = "PHOTONS"
        else:
            hduclas1 = self.src_type.upper()
        tbhdu.header["HDUCLASS"] = "HEASARC/SIMPUT"
        tbhdu.header["HDUCLAS1"] = hduclas1
        tbhdu.header["HDUVERS"] = "1.1.0"
        tbhdu.header.update(header)

        tbhdu.header["EXTVER"] = extver
        if self.imhdu is not None:
            self.imhdu.header["EXTVER"] = img_extver

        if os.path.exists(filename) and not overwrite:
            mylog.info(f"Appending this source to {filename}.")
            with pyfits.open(filename, mode='append') as f:
                f.append(tbhdu)
                if self.imhdu is not None:
                    f.append(self.imhdu)
                f.flush()
        else:
            if os.path.exists(filename):
                mylog.warning(f"Overwriting {filename} with this source.")
            else:
                mylog.info(f"Writing source to {filename}.")
            f = [pyfits.PrimaryHDU(), tbhdu]
            if self.imhdu is not None:
                f.append(self.imhdu)
            pyfits.HDUList(f).writeto(filename, overwrite=overwrite)

        if self.imhdu is not None:
            self.imhdu.header["EXTVER"] = 1
Ejemplo n.º 17
0
def add_instrument_to_registry(inst_spec):
    """
    Add an instrument specification to the registry, contained
    in either a dictionary or a JSON file.

    The *inst_spec* must have the structure as shown below. 
    The order is not important. If you use a JSON file, the
    structure is the same, but the file cannot include comments.

    >>> {
    ...     "name": "hdxi_3x10", # The short name of the instrument
    ...     "arf": "xrs_hdxi_3x10.arf", # The file containing the ARF
    ...     "rmf": "xrs_hdxi.rmf" # The file containing the RMF
    ...     "bkgnd": "acisi" # The name of the particle background
    ...     "fov": 20.0, # The field of view in arcminutes
    ...     "focal_length": 10.0, # The focal length in meters
    ...     "num_pixels": 4096, # The number of pixels on a side in the FOV
    ...     "psf": ["gaussian", 0.5] # The type of PSF and its FWHM
    ... }
    """
    if isinstance(inst_spec, dict):
        inst = inst_spec
    elif os.path.exists(inst_spec):
        f = open(inst_spec, "r")
        inst = json.load(f)
        f.close()
    name = inst["name"]
    if name in instrument_registry:
        raise KeyError(
            "The instrument with name %s is already in the registry! Assign a different name!"
            % name)
    # Catch old JSON files with plate scale
    if "plate_scale" in inst:
        inst["fov"] = inst["num_pixels"] * inst["plate_scale"] / 60.0
        inst.pop("plate_scale")
    instrument_registry[name] = inst
    mylog.info(
        "The %s instrument specification has been added to the instrument registry."
        % name)
    return name
Ejemplo n.º 18
0
    def fetch_files(self, key, loc=None):
        """
        A handy method to fetch ARF, RMF, background,
        and PSF files to a location of one's choice.
        Files are only actually downloaded if they are 
        not present already.

        Parameters
        ----------
        key : string
            The instrument specification to download
            the files for.
        loc : string, optional
            The path to download the files to. If not 
            specified, it will download them to the 
            current working directory.
        """
        inst_spec = self[key]
        if loc is None:
            loc = os.getcwd()
        dog = PoochHandle(cache_dir=loc)
        log_msg = f"Downloading %s \"%s\" for instrument \"{key}\"."
        fns = [inst_spec['arf'], inst_spec['rmf']]
        logs = ["ARF", "RMF"]
        if inst_spec['bkgnd'] is not None:
            bkgnd = inst_spec['bkgnd'][0]
            if isinstance(bkgnd, list):
                for b in inst_spec['bkgnd']:
                    fns.append(b[0])
            else:
                fns.append(bkgnd)
            logs.append("instrumental background model")
        if inst_spec['psf'] is not None:
            if "image" in inst_spec['psf'][0]:
                fns.append(inst_spec['psf'][1])
                logs.append("PSF model")
        for fn, log in zip(fns, logs):
            mylog.info(log_msg % (log, fn))
            dog.fetch(fn)
Ejemplo n.º 19
0
    def write_catalog(self, overwrite=False):
        """
        Write the SIMPUT catalog and associated photon lists to disk.

        Parameters
        ----------
        overwrite : boolean, optional
            Whether or not to overwrite an existing file with 
            the same name. Default: False
        """
        for i, phlist in enumerate(self.photon_lists):
            if i == 0:
                append = False
                mylog.info("Writing SIMPUT catalog file %s_simput.fits." %
                           self.name)
            else:
                append = True
            mylog.info("Writing SIMPUT photon list file %s_phlist.fits." %
                       phlist.name)
            phlist.write_photon_list(self.name,
                                     append=append,
                                     overwrite=overwrite)
Ejemplo n.º 20
0
def make_diffuse_background(bkg_events, event_params, rmf, prng=None):
    from soxs.instrument import perform_dither

    n_e = bkg_events["energy"].size

    bkg_events['time'] = prng.uniform(size=n_e, low=0.0,
                                      high=event_params["exposure_time"])

    x_offset, y_offset = perform_dither(bkg_events["time"],
                                        event_params["dither_params"])

    rot_mat = get_rot_mat(event_params["roll_angle"])

    det = np.array([bkg_events["detx"] + x_offset - event_params["aimpt_coords"][0],
                    bkg_events["dety"] + y_offset - event_params["aimpt_coords"][1]])
    pix = np.dot(rot_mat.T, det)

    bkg_events["xpix"] = pix[0, :] + event_params['pix_center'][0]
    bkg_events["ypix"] = pix[1, :] + event_params['pix_center'][1]

    mylog.info("Scattering energies with RMF %s." % os.path.split(rmf.filename)[-1])
    bkg_events = rmf.scatter_energies(bkg_events, prng=prng)

    return bkg_events
Ejemplo n.º 21
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(fov, sky_center, prng=prng)
        num_sources = fluxes.size
    else:
        mylog.info(f"Reading in point-source properties from {input_sources}.")
        t = ascii.read(input_sources)
        ra0 = t["RA"].data
        dec0 = t["Dec"].data
        fluxes = t["flux_0.5_2.0_keV"].data
        ind = t["index"].data
        num_sources = fluxes.size

    mylog.debug(f"Generating spectra from {num_sources} sources.")

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

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

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

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

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

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

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

    mylog.debug("Finished generating spectra.")

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

    all_nph = all_energies.size

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

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

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

    return output_events
Ejemplo n.º 22
0
def add_background_from_file(events, event_params, bkg_file):
    from soxs.instrument import perform_dither
    f = pyfits.open(bkg_file)

    hdu = f["EVENTS"]

    dither_params = {}
    if "DITHXAMP" in hdu.header:
        dither_params["x_amp"] = hdu.header["DITHXAMP"]
        dither_params["y_amp"] = hdu.header["DITHYAMP"]
        dither_params["x_period"] = hdu.header["DITHXPER"]
        dither_params["y_period"] = hdu.header["DITHYPER"]
        dither_params["plate_scale"] = hdu.header["TCDLT3"] * 3600.0
        dither_params["dither_on"] = True
    else:
        dither_params["dither_on"] = False

    sexp = event_params["exposure_time"]
    bexp = hdu.header["EXPOSURE"]

    if event_params["exposure_time"] > hdu.header["EXPOSURE"]:
        raise RuntimeError(
            "The background file does not have sufficient exposure! Source "
            "exposure time %g, background exposure time %g." % (sexp, bexp))

    for k1, k2 in key_map.items():
        if event_params[k1] != hdu.header[k2]:
            raise RuntimeError("'%s' keyword does not match! %s vs. %s" %
                               (k1, event_params[k1], hdu.header[k2]))
    rmf1 = os.path.split(event_params["rmf"])[-1]
    rmf2 = hdu.header["RESPFILE"]
    arf1 = os.path.split(event_params["arf"])[-1]
    arf2 = hdu.header["ANCRFILE"]
    if rmf1 != rmf2:
        raise RuntimeError("RMFs do not match! %s vs. %s" % (rmf1, rmf2))
    if arf1 != arf2:
        raise RuntimeError("ARFs do not match! %s vs. %s" % (arf1, arf2))

    idxs = hdu.data["TIME"] < sexp

    mylog.info("Adding %d background events from %s." % (idxs.sum(), bkg_file))

    if event_params["roll_angle"] == hdu.header["ROLL_PNT"]:
        xpix = hdu.data["X"][idxs]
        ypix = hdu.data["Y"][idxs]
    else:
        rot_mat = get_rot_mat(event_params["roll_angle"])
        if dither_params["dither_on"]:
            t = hdu.data["TIME"][idxs]
            x_off, y_off = perform_dither(t, dither_params)
        else:
            x_off = 0.0
            y_off = 0.0
        det = np.array([
            hdu.data["DETX"][idxs] + x_off - event_params["aimpt_coords"][0],
            hdu.data["DETY"][idxs] + y_off - event_params["aimpt_coords"][1]
        ])
        xpix, ypix = np.dot(rot_mat.T, det)

        xpix += hdu.header["TCRPX2"]
        ypix += hdu.header["TCRPX3"]

    all_events = {}
    for key in [
            "detx", "dety", "time", "ccd_id", event_params["channel_type"]
    ]:
        all_events[key] = np.concatenate(
            [events[key], hdu.data[key.upper()][idxs]])
    all_events["xpix"] = np.concatenate([events["xpix"], xpix])
    all_events["ypix"] = np.concatenate([events["ypix"], ypix])
    all_events["energy"] = np.concatenate(
        [events["energy"], hdu.data["ENERGY"][idxs] / 1000.0])

    f.close()

    return all_events
Ejemplo n.º 23
0
def write_event_file(events, parameters, filename, overwrite=False):
    from astropy.time import Time, TimeDelta
    mylog.info("Writing events to file %s." % filename)

    t_begin = Time.now()
    dt = TimeDelta(parameters["exposure_time"], format='sec')
    t_end = t_begin + dt

    col_x = pyfits.Column(name='X', format='D', unit='pixel', array=events["xpix"])
    col_y = pyfits.Column(name='Y', format='D', unit='pixel', array=events["ypix"])
    col_e = pyfits.Column(name='ENERGY', format='E', unit='eV', array=events["energy"]*1000.)
    col_dx = pyfits.Column(name='DETX', format='D', unit='pixel', array=events["detx"])
    col_dy = pyfits.Column(name='DETY', format='D', unit='pixel', array=events["dety"])
    col_id = pyfits.Column(name='CCD_ID', format='D', unit='pixel', array=events["ccd_id"])

    chantype = parameters["channel_type"]
    if chantype == "PHA":
        cunit = "adu"
    elif chantype == "PI":
        cunit = "Chan"
    col_ch = pyfits.Column(name=chantype.upper(), format='1J', unit=cunit, array=events[chantype])

    col_t = pyfits.Column(name="TIME", format='1D', unit='s', array=events['time'])

    cols = [col_e, col_x, col_y, col_ch, col_t, col_dx, col_dy, col_id]

    coldefs = pyfits.ColDefs(cols)
    tbhdu = pyfits.BinTableHDU.from_columns(coldefs)
    tbhdu.name = "EVENTS"

    tbhdu.header["MTYPE1"] = "sky"
    tbhdu.header["MFORM1"] = "x,y"
    tbhdu.header["MTYPE2"] = "EQPOS"
    tbhdu.header["MFORM2"] = "RA,DEC"
    tbhdu.header["TCTYP2"] = "RA---TAN"
    tbhdu.header["TCTYP3"] = "DEC--TAN"
    tbhdu.header["TCRVL2"] = parameters["sky_center"][0]
    tbhdu.header["TCRVL3"] = parameters["sky_center"][1]
    tbhdu.header["TCDLT2"] = -parameters["plate_scale"]
    tbhdu.header["TCDLT3"] = parameters["plate_scale"]
    tbhdu.header["TCRPX2"] = parameters["pix_center"][0]
    tbhdu.header["TCRPX3"] = parameters["pix_center"][1]
    tbhdu.header["TCUNI2"] = "deg"
    tbhdu.header["TCUNI3"] = "deg"
    tbhdu.header["TLMIN2"] = 0.5
    tbhdu.header["TLMIN3"] = 0.5
    tbhdu.header["TLMAX2"] = 2.0*parameters["num_pixels"]+0.5
    tbhdu.header["TLMAX3"] = 2.0*parameters["num_pixels"]+0.5
    tbhdu.header["TLMIN4"] = parameters["chan_lim"][0]
    tbhdu.header["TLMAX4"] = parameters["chan_lim"][1]
    tbhdu.header["TLMIN6"] = -0.5*parameters["num_pixels"]
    tbhdu.header["TLMAX6"] = 0.5*parameters["num_pixels"]
    tbhdu.header["TLMIN7"] = -0.5*parameters["num_pixels"]
    tbhdu.header["TLMAX7"] = 0.5*parameters["num_pixels"]
    tbhdu.header["EXPOSURE"] = parameters["exposure_time"]
    tbhdu.header["TSTART"] = 0.0
    tbhdu.header["TSTOP"] = parameters["exposure_time"]
    tbhdu.header["HDUVERS"] = "1.1.0"
    tbhdu.header["RADECSYS"] = "FK5"
    tbhdu.header["EQUINOX"] = 2000.0
    tbhdu.header["HDUCLASS"] = "OGIP"
    tbhdu.header["HDUCLAS1"] = "EVENTS"
    tbhdu.header["HDUCLAS2"] = "ACCEPTED"
    tbhdu.header["DATE"] = t_begin.tt.isot
    tbhdu.header["DATE-OBS"] = t_begin.tt.isot
    tbhdu.header["DATE-END"] = t_end.tt.isot
    tbhdu.header["RESPFILE"] = os.path.split(parameters["rmf"])[-1]
    tbhdu.header["PHA_BINS"] = parameters["nchan"]
    tbhdu.header["ANCRFILE"] = os.path.split(parameters["arf"])[-1]
    tbhdu.header["CHANTYPE"] = parameters["channel_type"]
    tbhdu.header["MISSION"] = parameters["mission"]
    tbhdu.header["TELESCOP"] = parameters["telescope"]
    tbhdu.header["INSTRUME"] = parameters["instrument"]
    tbhdu.header["RA_PNT"] = parameters["sky_center"][0]
    tbhdu.header["DEC_PNT"] = parameters["sky_center"][1]
    tbhdu.header["ROLL_PNT"] = parameters["roll_angle"]
    tbhdu.header["AIMPT_X"] = parameters["aimpt_coords"][0]
    tbhdu.header["AIMPT_Y"] = parameters["aimpt_coords"][1]
    if parameters["dither_params"]["dither_on"]:
        tbhdu.header["DITHXAMP"] = parameters["dither_params"]["x_amp"]
        tbhdu.header["DITHYAMP"] = parameters["dither_params"]["y_amp"]
        tbhdu.header["DITHXPER"] = parameters["dither_params"]["x_period"]
        tbhdu.header["DITHYPER"] = parameters["dither_params"]["y_period"]

    start = pyfits.Column(name='START', format='1D', unit='s',
                          array=np.array([0.0]))
    stop = pyfits.Column(name='STOP', format='1D', unit='s',
                         array=np.array([parameters["exposure_time"]]))

    tbhdu_gti = pyfits.BinTableHDU.from_columns([start,stop])
    tbhdu_gti.name = "STDGTI"
    tbhdu_gti.header["TSTART"] = 0.0
    tbhdu_gti.header["TSTOP"] = parameters["exposure_time"]
    tbhdu_gti.header["HDUCLASS"] = "OGIP"
    tbhdu_gti.header["HDUCLAS1"] = "GTI"
    tbhdu_gti.header["HDUCLAS2"] = "STANDARD"
    tbhdu_gti.header["RADECSYS"] = "FK5"
    tbhdu_gti.header["EQUINOX"] = 2000.0
    tbhdu_gti.header["DATE"] = t_begin.tt.isot
    tbhdu_gti.header["DATE-OBS"] = t_begin.tt.isot
    tbhdu_gti.header["DATE-END"] = t_end.tt.isot

    hdulist = [pyfits.PrimaryHDU(), tbhdu, tbhdu_gti]

    pyfits.HDUList(hdulist).writeto(filename, overwrite=overwrite)
Ejemplo n.º 24
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
Ejemplo n.º 25
0
 def __init__(self, emin, emax, nbins, var_elem=None, apec_root=None,
              apec_vers=None, broadening=True, nolines=False,
              abund_table=None, nei=False):
     if apec_vers is None:
         filedir = os.path.join(os.path.dirname(__file__), 'files')
         cfile = glob.glob("%s/apec_*_coco.fits" % filedir)[0]
         apec_vers = cfile.split("/")[-1].split("_")[1][1:]
     mylog.info("Using APEC version %s." % apec_vers)
     if nei and apec_root is None:
         raise RuntimeError("The NEI APEC tables are not supplied with "
                            "SOXS! Download them from http://www.atomdb.org "
                            "and set 'apec_root' to their location.")
     if nei and var_elem is None:
         raise RuntimeError("For NEI spectra, you must specify which elements "
                            "you want to vary using the 'var_elem' argument!")
     self.nei = nei
     emin = parse_value(emin, "keV")
     emax = parse_value(emax, 'keV')
     self.emin = emin
     self.emax = emax
     self.nbins = nbins
     self.ebins = np.linspace(self.emin, self.emax, nbins+1)
     self.de = np.diff(self.ebins)
     self.emid = 0.5*(self.ebins[1:]+self.ebins[:-1])
     if apec_root is None:
         apec_root = soxs_files_path
     if nei:
         neistr = "_nei"
         ftype = "comp"
     else:
         neistr = ""
         ftype = "coco"
     self.cocofile = os.path.join(apec_root, "apec_v%s%s_%s.fits" % (apec_vers, neistr, ftype))
     self.linefile = os.path.join(apec_root, "apec_v%s%s_line.fits" % (apec_vers, neistr))
     if not os.path.exists(self.cocofile) or not os.path.exists(self.linefile):
         raise IOError("Cannot find the APEC files!\n %s\n, %s" % (self.cocofile,
                                                                   self.linefile))
     mylog.info("Using %s for generating spectral lines." % os.path.split(self.linefile)[-1])
     mylog.info("Using %s for generating the continuum." % os.path.split(self.cocofile)[-1])
     self.nolines = nolines
     self.wvbins = hc/self.ebins[::-1]
     self.broadening = broadening
     try:
         self.line_handle = pyfits.open(self.linefile)
     except IOError:
         raise IOError("Line file %s does not exist" % self.linefile)
     try:
         self.coco_handle = pyfits.open(self.cocofile)
     except IOError:
         raise IOError("Continuum file %s does not exist" % self.cocofile)
     self.Tvals = self.line_handle[1].data.field("kT")
     self.nT = len(self.Tvals)
     self.dTvals = np.diff(self.Tvals)
     self.minlam = self.wvbins.min()
     self.maxlam = self.wvbins.max()
     self.var_elem_names = []
     self.var_ion_names = []
     if var_elem is None:
         self.var_elem = np.empty((0,1), dtype='int')
     else:
         self.var_elem = []
         if len(var_elem) != len(set(var_elem)):
             raise RuntimeError("Duplicates were found in the \"var_elem\" list! %s" % var_elem)
         for elem in var_elem:
             if "^" in elem:
                 if not self.nei:
                     raise RuntimeError("Cannot use different ionization states with a "
                                        "CIE plasma!")
                 el = elem.split("^")
                 e = el[0]
                 ion = int(el[1])
             else:
                 if self.nei:
                     raise RuntimeError("Variable elements must include the ionization "
                                        "state for NEI plasmas!")
                 e = elem
                 ion = 0
             self.var_elem.append([elem_names.index(e), ion])
         self.var_elem.sort(key=lambda x: (x[0], x[1]))
         self.var_elem = np.array(self.var_elem, dtype='int')
         self.var_elem_names = [elem_names[e[0]] for e in self.var_elem]
         self.var_ion_names = ["%s^%d" % (elem_names[e[0]], e[1]) for e in self.var_elem]
     self.num_var_elem = len(self.var_elem)
     if self.nei:
         self.cosmic_elem = [elem for elem in [1, 2]
                             if elem not in self.var_elem[:, 0]]
         self.metal_elem = []
     else:
         self.cosmic_elem = [elem for elem in cosmic_elem 
                             if elem not in self.var_elem[:,0]]
         self.metal_elem = [elem for elem in metal_elem
                            if elem not in self.var_elem[:,0]]
     if abund_table is None:
         abund_table = soxs_cfg.get("soxs", "abund_table")
     if not isinstance(abund_table, string_types):
         if len(abund_table) != 30:
             raise RuntimeError("User-supplied abundance tables "
                                "must be 30 elements long!")
         self.atable = np.concatenate([[0.0], np.array(abund_table)])
     else:
         self.atable = abund_tables[abund_table].copy()
     self._atable = self.atable.copy()
     self._atable[1:] /= abund_tables["angr"][1:]
Ejemplo n.º 26
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)
Ejemplo n.º 27
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, write_regions=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
    write_regions : string, optional
        If set to a filename, output circle ds9 regions corresponding to the
        positions of the halos with radii corresponding to their R500 
        projected on the sky.  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_comoving_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)
    xhi = (xc+1.1*0.5*fov)
    ylo = (yc-1.1*0.5*fov)
    yhi = (yc+1.1*0.5*fov)

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

    halo_x = halo_data["x"][()].astype("float64")/(h0*scale.value)
    halo_y = halo_data["y"][()].astype("float64")/(h0*scale.value)

    fov_idxs = (halo_x >= xlo) & (halo_x <= xhi)
    fov_idxs = (halo_y >= ylo) & (halo_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
    # We need to compute proper scales here
    s = scale[fov_idxs].to("Mpc/arcsec").value/(1.0+z)
    ra0, dec0 = w.wcs_pix2world((halo_x[fov_idxs]-xc)*60.0,
                                (halo_y[fov_idxs]-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)

    if write_regions is not None:
        from regions import CircleSkyRegion, write_ds9
        from astropy.coordinates import Angle, SkyCoord
        regs = []
        for halo in range(n_halos):
            c = SkyCoord(ra0[halo], dec0[halo], unit=("deg", "deg"), frame='fk5')
            scale = cosmo.kpc_proper_per_arcmin(z[halo]).to("kpc/deg")
            r500c = r500_kpc / scale.value
            r = Angle(r500c, 'deg')
            reg = CircleSkyRegion(c, r)
            regs.append(reg)
        write_ds9(regs, write_regions)

    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
Ejemplo n.º 28
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
Ejemplo n.º 29
0
def instrument_simulator(input_events, out_file, exp_time, instrument,
                         sky_center, overwrite=False, instr_bkgnd=True, 
                         foreground=True, ptsrc_bkgnd=True, 
                         bkgnd_file=None, no_dither=False, 
                         dither_params=None, roll_angle=0.0, 
                         subpixel_res=False, prng=None):
    """
    Take unconvolved events and create an event file from them. This
    function calls generate_events to do 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

    and then calls make_background to add instrumental and astrophysical
    backgrounds, unless a background file is provided, in which case
    the background events are read from this file. The events are
    then written out to a file.

    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.
    overwrite : boolean, optional
        Whether or not to overwrite an existing file with the same name.
        Default: False
    instr_bkgnd : boolean, optional
        Whether or not to include the instrumental/particle background. 
        Default: True
    foreground : boolean, optional
        Whether or not to include the local foreground. 
        Default: True
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the point-source background. 
        Default: True
    bkgnd_file : string, optional
        If set, backgrounds will be loaded from this file and not generated
        on the fly. Default: None
    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. 

    Examples
    --------
    >>> instrument_simulator("sloshing_simput.fits", "sloshing_evt.fits", 
    ...                      300000.0, "hdxi_3x10", [30., 45.], overwrite=True)
    """
    from soxs.background import add_background_from_file
    if not out_file.endswith(".fits"):
        out_file += ".fits"
    mylog.info("Making observation of source in %s." % out_file)
    # Make the source first
    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)
    # If the user wants backgrounds, either make the background or add an already existing
    # background event file. It may be necessary to reproject events to a new coordinate system.
    if bkgnd_file is None:
        if not instr_bkgnd and not ptsrc_bkgnd and not foreground:
            mylog.info("No backgrounds will be added to this observation.")
        else:
            mylog.info("Adding background events.")
            bkg_events, _ = make_background(exp_time, instrument, sky_center,
                                            foreground=foreground, instr_bkgnd=instr_bkgnd, 
                                            no_dither=no_dither, dither_params=dither_params, 
                                            ptsrc_bkgnd=ptsrc_bkgnd, prng=prng, 
                                            subpixel_res=subpixel_res, roll_angle=roll_angle)
            for key in events:
                events[key] = np.concatenate([events[key], bkg_events[key]])
    else:
        mylog.info("Adding background events from the file %s." % bkgnd_file)
        if not os.path.exists(bkgnd_file):
            raise IOError("Cannot find the background event file %s!" % bkgnd_file)
        events = add_background_from_file(events, event_params, bkgnd_file)
    if len(events["energy"]) == 0:
        raise RuntimeError("No events were detected from source or background!!")
    write_event_file(events, event_params, out_file, overwrite=overwrite)
    mylog.info("Observation complete.")
Ejemplo n.º 30
0
def add_background_from_file(events, event_params, bkg_file):
    from soxs.instrument import perform_dither
    f = pyfits.open(bkg_file)

    hdu = f["EVENTS"]

    dither_params = {}
    if "DITHXAMP" in hdu.header:
        dither_params["x_amp"] = hdu.header["DITHXAMP"]
        dither_params["y_amp"] = hdu.header["DITHYAMP"]
        dither_params["x_period"] = hdu.header["DITHXPER"]
        dither_params["y_period"] = hdu.header["DITHYPER"]
        dither_params["plate_scale"] = hdu.header["TCDLT3"]*3600.0
        dither_params["dither_on"] = True
    else:
        dither_params["dither_on"] = False

    sexp = event_params["exposure_time"]
    bexp = hdu.header["EXPOSURE"]

    if event_params["exposure_time"] > hdu.header["EXPOSURE"]:
        raise RuntimeError("The background file does not have sufficient exposure! Source "
                           "exposure time %g, background exposure time %g." % (sexp, bexp))

    for k1, k2 in key_map.items():
        if event_params[k1] != hdu.header[k2]:
            raise RuntimeError("'%s' keyword does not match! %s vs. %s" % (k1, event_params[k1],
                                                                           hdu.header[k2]))
    rmf1 = os.path.split(event_params["rmf"])[-1]
    rmf2 = hdu.header["RESPFILE"]
    arf1 = os.path.split(event_params["arf"])[-1]
    arf2 = hdu.header["ANCRFILE"]
    if rmf1 != rmf2:
        raise RuntimeError("RMFs do not match! %s vs. %s" % (rmf1, rmf2))
    if arf1 != arf2:
        raise RuntimeError("ARFs do not match! %s vs. %s" % (arf1, arf2))

    idxs = hdu.data["TIME"] < sexp

    mylog.info("Adding %d background events from %s." % (idxs.sum(), bkg_file))

    if event_params["roll_angle"] == hdu.header["ROLL_PNT"]:
        xpix = hdu.data["X"][idxs]
        ypix = hdu.data["Y"][idxs]
    else:
        rot_mat = get_rot_mat(event_params["roll_angle"])
        if dither_params["dither_on"]:
            t = hdu.data["TIME"][idxs]
            x_off, y_off = perform_dither(t, dither_params)
        else:
            x_off = 0.0
            y_off = 0.0
        det = np.array([hdu.data["DETX"][idxs] + x_off - event_params["aimpt_coords"][0],
                        hdu.data["DETY"][idxs] + y_off - event_params["aimpt_coords"][1]])
        xpix, ypix = np.dot(rot_mat.T, det)

        xpix += hdu.header["TCRPX2"]
        ypix += hdu.header["TCRPX3"]

    all_events = {}
    for key in ["detx", "dety", "time", "ccd_id", event_params["channel_type"]]:
        all_events[key] = np.concatenate([events[key], hdu.data[key.upper()][idxs]])
    all_events["xpix"] = np.concatenate([events["xpix"], xpix])
    all_events["ypix"] = np.concatenate([events["ypix"], ypix])
    all_events["energy"] = np.concatenate([events["energy"], hdu.data["ENERGY"][idxs]/1000.0])

    f.close()

    return all_events
Ejemplo n.º 31
0
 def __init__(self,
              emin,
              emax,
              nbins,
              var_elem=None,
              apec_root=None,
              apec_vers=None,
              broadening=True,
              nolines=False,
              abund_table=None,
              nei=False):
     if apec_vers is None:
         filedir = os.path.join(os.path.dirname(__file__), 'files')
         cfile = glob.glob("%s/apec_*_coco.fits" % filedir)[0]
         apec_vers = cfile.split("/")[-1].split("_")[1][1:]
     mylog.info("Using APEC version %s." % apec_vers)
     if nei and apec_root is None:
         raise RuntimeError(
             "The NEI APEC tables are not supplied with "
             "SOXS! Download them from http://www.atomdb.org "
             "and set 'apec_root' to their location.")
     if nei and var_elem is None:
         raise RuntimeError(
             "For NEI spectra, you must specify which elements "
             "you want to vary using the 'var_elem' argument!")
     self.nei = nei
     emin = parse_value(emin, "keV")
     emax = parse_value(emax, 'keV')
     self.emin = emin
     self.emax = emax
     self.nbins = nbins
     self.ebins = np.linspace(self.emin, self.emax, nbins + 1)
     self.de = np.diff(self.ebins)
     self.emid = 0.5 * (self.ebins[1:] + self.ebins[:-1])
     if apec_root is None:
         apec_root = soxs_files_path
     if nei:
         neistr = "_nei"
         ftype = "comp"
     else:
         neistr = ""
         ftype = "coco"
     self.cocofile = os.path.join(
         apec_root, "apec_v%s%s_%s.fits" % (apec_vers, neistr, ftype))
     self.linefile = os.path.join(
         apec_root, "apec_v%s%s_line.fits" % (apec_vers, neistr))
     if not os.path.exists(self.cocofile) or not os.path.exists(
             self.linefile):
         raise IOError("Cannot find the APEC files!\n %s\n, %s" %
                       (self.cocofile, self.linefile))
     mylog.info("Using %s for generating spectral lines." %
                os.path.split(self.linefile)[-1])
     mylog.info("Using %s for generating the continuum." %
                os.path.split(self.cocofile)[-1])
     self.nolines = nolines
     self.wvbins = hc / self.ebins[::-1]
     self.broadening = broadening
     try:
         self.line_handle = pyfits.open(self.linefile)
     except IOError:
         raise IOError("Line file %s does not exist" % self.linefile)
     try:
         self.coco_handle = pyfits.open(self.cocofile)
     except IOError:
         raise IOError("Continuum file %s does not exist" % self.cocofile)
     self.Tvals = self.line_handle[1].data.field("kT")
     self.nT = len(self.Tvals)
     self.dTvals = np.diff(self.Tvals)
     self.minlam = self.wvbins.min()
     self.maxlam = self.wvbins.max()
     self.var_elem_names = []
     self.var_ion_names = []
     if var_elem is None:
         self.var_elem = np.empty((0, 1), dtype='int')
     else:
         self.var_elem = []
         if len(var_elem) != len(set(var_elem)):
             raise RuntimeError(
                 "Duplicates were found in the \"var_elem\" list! %s" %
                 var_elem)
         for elem in var_elem:
             if "^" in elem:
                 if not self.nei:
                     raise RuntimeError(
                         "Cannot use different ionization states with a "
                         "CIE plasma!")
                 el = elem.split("^")
                 e = el[0]
                 ion = int(el[1])
             else:
                 if self.nei:
                     raise RuntimeError(
                         "Variable elements must include the ionization "
                         "state for NEI plasmas!")
                 e = elem
                 ion = 0
             self.var_elem.append([elem_names.index(e), ion])
         self.var_elem.sort(key=lambda x: (x[0], x[1]))
         self.var_elem = np.array(self.var_elem, dtype='int')
         self.var_elem_names = [elem_names[e[0]] for e in self.var_elem]
         self.var_ion_names = [
             "%s^%d" % (elem_names[e[0]], e[1]) for e in self.var_elem
         ]
     self.num_var_elem = len(self.var_elem)
     if self.nei:
         self.cosmic_elem = [
             elem for elem in [1, 2] if elem not in self.var_elem[:, 0]
         ]
         self.metal_elem = []
     else:
         self.cosmic_elem = [
             elem for elem in cosmic_elem if elem not in self.var_elem[:, 0]
         ]
         self.metal_elem = [
             elem for elem in metal_elem if elem not in self.var_elem[:, 0]
         ]
     if abund_table is None:
         abund_table = soxs_cfg.get("soxs", "abund_table")
     if not isinstance(abund_table, string_types):
         if len(abund_table) != 30:
             raise RuntimeError("User-supplied abundance tables "
                                "must be 30 elements long!")
         self.atable = np.concatenate([[0.0], np.array(abund_table)])
     else:
         self.atable = abund_tables[abund_table].copy()
     self._atable = self.atable.copy()
     self._atable[1:] /= abund_tables["angr"][1:]
Ejemplo n.º 32
0
def make_mosaic_events(pointing_list,
                       input_source,
                       out_prefix,
                       exp_time,
                       instrument,
                       overwrite=False,
                       instr_bkgnd=True,
                       foreground=True,
                       ptsrc_bkgnd=True,
                       bkgnd_file=None,
                       no_dither=False,
                       dither_params=None,
                       subpixel_res=False,
                       aimpt_shift=None,
                       prng=None):
    """
    Observe a source from many different pointings. 

    Parameters
    ----------
    pointing_list : list of tuples or str
        Either a list of tuples or a two-column ASCII table, containing 
        RA and Dec pointings for each mock observation.
    input_source : string
        The path to the SIMPUT catalog file which contains the input
        source(s).
    out_prefix : string
        The prefix for the event files which will be generated. 
    exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The exposure time in seconds.
    instrument : string
        The name of the instrument to use, which picks an instrument
        specification from the instrument registry.
    overwrite : boolean, optional
        Whether or not to overwrite an existing file with the same name.
        Default: False
    instr_bkgnd : boolean, optional
        Whether or not to include the instrumental/particle background. 
        Default: True
    foreground : boolean, optional
        Whether or not to include the local foreground. 
        Default: True
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the point-source background. 
        Default: True
    bkgnd_file : string, optional
        If set, backgrounds will be loaded from this file and not generated
        on the fly. Default: None
    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
    aimpt_shift : array-like, optional
        A two-float array-like object which shifts the aimpoint on the 
        detector from the nominal position. Units are in arcseconds.
        Default: None, which results in no shift from the nominal aimpoint. 
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 
    """
    if isinstance(pointing_list, str):
        t = ascii.read(pointing_list,
                       format='commented_header',
                       guess=False,
                       header_start=0,
                       delimiter="\t")
    elif not isinstance(pointing_list, Table):
        t = Table(np.array(pointing_list), names=["ra", "dec"])
    out_list = []
    for i, row in enumerate(t):
        out_file = f"{out_prefix}_{i}_evt.fits"
        out_list.append(out_file)
        instrument_simulator(input_source,
                             out_file,
                             exp_time,
                             instrument, (row["ra"], row["dec"]),
                             overwrite=overwrite,
                             instr_bkgnd=instr_bkgnd,
                             foreground=foreground,
                             ptsrc_bkgnd=ptsrc_bkgnd,
                             bkgnd_file=bkgnd_file,
                             no_dither=no_dither,
                             dither_params=dither_params,
                             subpixel_res=subpixel_res,
                             aimpt_shift=aimpt_shift,
                             prng=prng)
    t["evtfile"] = out_list
    outfile = f"{out_prefix}_event_mosaic.dat"
    mylog.info(f"Writing mosaic information to {outfile}.")
    t.write(outfile,
            overwrite=overwrite,
            delimiter="\t",
            format='ascii.commented_header')
    return outfile
Ejemplo n.º 33
0
def instrument_simulator(input_events,
                         out_file,
                         exp_time,
                         instrument,
                         sky_center,
                         overwrite=False,
                         instr_bkgnd=True,
                         foreground=True,
                         ptsrc_bkgnd=True,
                         bkgnd_file=None,
                         no_dither=False,
                         dither_params=None,
                         roll_angle=0.0,
                         subpixel_res=False,
                         aimpt_shift=None,
                         prng=None):
    """
    Take unconvolved events and create an event file from them. This
    function calls generate_events to do 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

    and then calls make_background to add instrumental and astrophysical
    backgrounds, unless a background file is provided, in which case
    the background events are read from this file. The events are
    then written out to a file.

    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.
    overwrite : boolean, optional
        Whether or not to overwrite an existing file with the same name.
        Default: False
    instr_bkgnd : boolean, optional
        Whether or not to include the instrumental/particle background. 
        Default: True
    foreground : boolean, optional
        Whether or not to include the local foreground. 
        Default: True
    ptsrc_bkgnd : boolean, optional
        Whether or not to include the point-source background. 
        Default: True
    bkgnd_file : string, optional
        If set, backgrounds will be loaded from this file and not generated
        on the fly. Default: None
    no_dither : boolean, optional
        If True, turn off dithering entirely. Default: False
    dither_params : array-like of floats, optional
        The parameters to use to control the size and period of the dither
        pattern. The first two numbers are the dither amplitude in x and y
        detector coordinates in arcseconds, and the second two numbers are
        the dither period in x and y detector coordinates in seconds. 
        Default: [8.0, 8.0, 1000.0, 707.0].
    roll_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The roll angle of the observation in degrees. Default: 0.0
    subpixel_res: boolean, optional
        If True, event positions are not randomized within the pixels 
        within which they are detected. Default: False
    aimpt_shift : array-like, optional
        A two-float array-like object which shifts the aimpoint on the 
        detector from the nominal position. Units are in arcseconds.
        Default: None, which results in no shift from the nominal aimpoint. 
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 

    Examples
    --------
    >>> instrument_simulator("sloshing_simput.fits", "sloshing_evt.fits", 
    ...                      300000.0, "lynx_hdxi", [30., 45.], overwrite=True)
    """
    from soxs.background import add_background_from_file
    if not out_file.endswith(".fits"):
        out_file += ".fits"
    mylog.info(f"Making observation of source in {out_file}.")
    # Make the source first
    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,
                                           aimpt_shift=aimpt_shift,
                                           prng=prng)
    # If the user wants backgrounds, either make the background or add an already existing
    # background event file. It may be necessary to reproject events to a new coordinate system.
    if bkgnd_file is None:
        if not instr_bkgnd and not ptsrc_bkgnd and not foreground:
            mylog.info("No backgrounds will be added to this observation.")
        else:
            mylog.info("Adding background events.")
            bkg_events, _ = make_background(exp_time,
                                            instrument,
                                            sky_center,
                                            foreground=foreground,
                                            instr_bkgnd=instr_bkgnd,
                                            no_dither=no_dither,
                                            dither_params=dither_params,
                                            ptsrc_bkgnd=ptsrc_bkgnd,
                                            prng=prng,
                                            subpixel_res=subpixel_res,
                                            roll_angle=roll_angle,
                                            aimpt_shift=aimpt_shift)
            for key in events:
                events[key] = np.concatenate([events[key], bkg_events[key]])
    else:
        mylog.info(f"Adding background events from the file {bkgnd_file}.")
        if not os.path.exists(bkgnd_file):
            raise IOError(
                f"Cannot find the background event file {bkgnd_file}!")
        events = add_background_from_file(events, event_params, bkgnd_file)
    if len(events["energy"]) == 0:
        raise RuntimeError(
            "No events were detected from source or background!!")
    write_event_file(events, event_params, out_file, overwrite=overwrite)
    mylog.info("Observation complete.")
Ejemplo n.º 34
0
def generate_events(source,
                    exp_time,
                    instrument,
                    sky_center,
                    no_dither=False,
                    dither_params=None,
                    roll_angle=0.0,
                    subpixel_res=False,
                    aimpt_shift=None,
                    prng=None):
    """
    Take unconvolved events and convolve them with instrumental responses. This 
    function does the following:

    1. Determines which events are observed using the ARF
    2. Pixelizes the events, applying PSF effects and dithering
    3. Determines energy channels using the RMF

    This function is not meant to be called by the end-user but is used by
    the :func:`~soxs.instrument.instrument_simulator` function.

    Parameters
    ----------
    input_events : string, dict, or None
        The unconvolved events to be used as input. Can be one of the
        following:
        1. The name of a SIMPUT catalog file.
        2. A Python dictionary containing the following items:
        "ra": A NumPy array of right ascension values in degrees.
        "dec": A NumPy array of declination values in degrees.
        "energy": A NumPy array of energy values in keV.
        "flux": The flux of the entire source, in units of erg/cm**2/s.
    out_file : string
        The name of the event file to be written.
    exp_time : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`
        The exposure time to use, in seconds. 
    instrument : string
        The name of the instrument to use, which picks an instrument
        specification from the instrument registry. 
    sky_center : array, tuple, or list
        The center RA, Dec coordinates of the observation, in degrees.
    no_dither : boolean, optional
        If True, turn off dithering entirely. Default: False
    dither_params : array-like of floats, optional
        The parameters to use to control the size and period of the dither
        pattern. The first two numbers are the dither amplitude in x and y
        detector coordinates in arcseconds, and the second two numbers are
        the dither period in x and y detector coordinates in seconds. 
        Default: [8.0, 8.0, 1000.0, 707.0].
    roll_angle : float, (value, unit) tuple, or :class:`~astropy.units.Quantity`, optional
        The roll angle of the observation in degrees. Default: 0.0
    subpixel_res : boolean, optional
        If True, event positions are not randomized within the pixels 
        within which they are detected. Default: False
    aimpt_shift : array-like, optional
        A two-float array-like object which shifts the aimpoint on the 
        detector from the nominal position. Units are in arcseconds.
        Default: None, which results in no shift from the nominal aimpoint. 
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 
    """
    exp_time = parse_value(exp_time, "s")
    roll_angle = parse_value(roll_angle, "deg")
    prng = parse_prng(prng)
    if source is None:
        source_list = []
    elif isinstance(source, dict):
        parameters = {}
        for key in ["flux", "emin", "emax", "src_names"]:
            parameters[key] = source[key]
        source_list = []
        for i in range(len(parameters["flux"])):
            phlist = SimputPhotonList(source["ra"][i], source["dec"][i],
                                      source["energy"][i],
                                      parameters['flux'][i],
                                      parameters['src_names'][i])
            source_list.append(phlist)
    elif isinstance(source, str):
        # Assume this is a SIMPUT catalog
        source_list, parameters = read_simput_catalog(source)

    try:
        instrument_spec = instrument_registry[instrument]
    except KeyError:
        raise KeyError(
            f"Instrument {instrument} is not in the instrument registry!")
    if not instrument_spec["imaging"]:
        raise RuntimeError(f"Instrument '{instrument_spec['name']}' is not "
                           f"designed for imaging observations!")

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

    nx = instrument_spec["num_pixels"]
    plate_scale = instrument_spec["fov"] / nx / 60.  # arcmin to deg
    plate_scale_arcsec = plate_scale * 3600.0

    if aimpt_shift is None:
        aimpt_shift = np.zeros(2)
    aimpt_shift = ensure_numpy_array(aimpt_shift).astype('float64')
    aimpt_shift /= plate_scale_arcsec

    if not instrument_spec["dither"]:
        dither_on = False
    else:
        dither_on = not no_dither
    if dither_params is None:
        dither_params = [8.0, 8.0, 1000.0, 707.0]
    dither_dict = {
        "x_amp": dither_params[0],
        "y_amp": dither_params[1],
        "x_period": dither_params[2],
        "y_period": dither_params[3],
        "dither_on": dither_on,
        "plate_scale": plate_scale_arcsec
    }

    event_params = {
        "exposure_time": exp_time,
        "arf": arf.filename,
        "sky_center": sky_center,
        "pix_center": np.array([0.5 * (2 * nx + 1)] * 2),
        "num_pixels": nx,
        "plate_scale": plate_scale,
        "rmf": rmf.filename,
        "channel_type": rmf.chan_type,
        "telescope": rmf.header["TELESCOP"],
        "instrument": instrument_spec['name'],
        "mission": rmf.header.get("MISSION", ""),
        "nchan": rmf.n_ch,
        "roll_angle": roll_angle,
        "fov": instrument_spec["fov"],
        "chan_lim": [rmf.cmin, rmf.cmax],
        "chips": instrument_spec["chips"],
        "dither_params": dither_dict,
        "aimpt_coords": instrument_spec["aimpt_coords"],
        "aimpt_shift": aimpt_shift
    }

    # Set up WCS

    w = pywcs.WCS(naxis=2)
    w.wcs.crval = event_params["sky_center"]
    w.wcs.crpix = event_params["pix_center"]
    w.wcs.cdelt = [-plate_scale, plate_scale]
    w.wcs.ctype = ["RA---TAN", "DEC--TAN"]
    w.wcs.cunit = ["deg"] * 2

    # Determine rotation matrix
    rot_mat = get_rot_mat(roll_angle)

    # Set up PSF
    psf_type = instrument_spec["psf"][0]
    psf_class = psf_model_registry[psf_type]
    psf = psf_class(instrument_spec, prng=prng)

    all_events = defaultdict(list)

    for i, src in enumerate(source_list):

        mylog.info(
            f"Detecting events from source {parameters['src_names'][i]}")

        # Step 1: Use ARF to determine which photons are observed

        mylog.info(f"Applying energy-dependent effective area from "
                   f"{os.path.split(arf.filename)[-1]}.")
        refband = [parameters["emin"][i], parameters["emax"][i]]
        if src.src_type == "phlist":
            events = arf.detect_events_phlist(src.events.copy(),
                                              exp_time,
                                              parameters["flux"][i],
                                              refband,
                                              prng=prng)
        elif src.src_type.endswith("spectrum"):
            events = arf.detect_events_spec(src, exp_time, refband, prng=prng)

        n_evt = events["energy"].size

        if n_evt == 0:
            mylog.warning("No events were observed for this source!!!")
        else:

            # Step 2: Assign pixel coordinates to events. Apply dithering and
            # PSF. Clip events that don't fall within the detection region.

            mylog.info("Pixeling events.")

            # Convert RA, Dec to pixel coordinates
            xpix, ypix = w.wcs_world2pix(events["ra"], events["dec"], 1)

            xpix -= event_params["pix_center"][0]
            ypix -= event_params["pix_center"][1]

            events.pop("ra")
            events.pop("dec")

            n_evt = xpix.size

            # Rotate physical coordinates to detector coordinates

            det = np.dot(rot_mat, np.array([xpix, ypix]))
            detx = det[0, :] + event_params["aimpt_coords"][0] + aimpt_shift[0]
            dety = det[1, :] + event_params["aimpt_coords"][1] + aimpt_shift[1]

            # Add times to events
            events['time'] = prng.uniform(size=n_evt,
                                          low=0.0,
                                          high=event_params["exposure_time"])

            # Apply dithering

            x_offset, y_offset = perform_dither(events["time"], dither_dict)

            detx -= x_offset
            dety -= y_offset

            # PSF scattering of detector coordinates

            mylog.info(f"Scattering events with a {psf}-based PSF.")
            detx, dety = psf.scatter(detx, dety, events["energy"])

            # Convert detector coordinates to chip coordinates.
            # Throw out events that don't fall on any chip.

            cx = np.trunc(detx) + 0.5 * np.sign(detx)
            cy = np.trunc(dety) + 0.5 * np.sign(dety)

            events["chip_id"] = -np.ones(n_evt, dtype='int')
            for i, chip in enumerate(event_params["chips"]):
                rtype = chip[0]
                args = chip[1:]
                r, _ = create_region(rtype, args, 0.0, 0.0)
                inside = r.contains(PixCoord(cx, cy))
                events["chip_id"][inside] = i
            keep = events["chip_id"] > -1

            mylog.info(f"{n_evt-keep.sum()} events were rejected because "
                       f"they do not fall on any CCD.")
            n_evt = keep.sum()

            if n_evt == 0:
                mylog.warning("No events are within the field "
                              "of view for this source!!!")
            else:

                # Keep only those events which fall on a chip

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

                # Convert chip coordinates back to detector coordinates,
                # unless the user has specified that they want subpixel
                # resolution

                if subpixel_res:
                    events["detx"] = detx[keep]
                    events["dety"] = dety[keep]
                else:
                    events["detx"] = cx[keep] + \
                                     prng.uniform(low=-0.5, high=0.5, size=n_evt)
                    events["dety"] = cy[keep] + \
                                     prng.uniform(low=-0.5, high=0.5, size=n_evt)

                # Convert detector coordinates back to pixel coordinates by
                # adding the dither offsets back in and applying the rotation
                # matrix again

                det = np.array([
                    events["detx"] + x_offset[keep] -
                    event_params["aimpt_coords"][0] - aimpt_shift[0],
                    events["dety"] + y_offset[keep] -
                    event_params["aimpt_coords"][1] - aimpt_shift[1]
                ])
                pix = np.dot(rot_mat.T, det)

                events["xpix"] = pix[0, :] + event_params['pix_center'][0]
                events["ypix"] = pix[1, :] + event_params['pix_center'][1]

        if n_evt > 0:
            for key in events:
                all_events[key] = np.concatenate(
                    [all_events[key], events[key]])

    if len(all_events["energy"]) == 0:
        mylog.warning("No events from any of the sources in "
                      "the catalog were detected!")
        for key in [
                "xpix", "ypix", "detx", "dety", "time", "chip_id",
                event_params["channel_type"]
        ]:
            all_events[key] = np.array([])
    else:
        # Step 4: Scatter energies with RMF
        mylog.info(f"Scattering energies with "
                   f"RMF {os.path.split(rmf.filename)[-1]}.")
        all_events = rmf.scatter_energies(all_events, prng=prng)

    return all_events, event_params
Ejemplo n.º 35
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
Ejemplo n.º 36
0
Archivo: events.py Proyecto: eblur/soxs
def write_event_file(events, parameters, filename, overwrite=False):
    from astropy.time import Time, TimeDelta
    mylog.info("Writing events to file %s." % filename)

    t_begin = Time.now()
    dt = TimeDelta(parameters["exposure_time"], format='sec')
    t_end = t_begin + dt

    col_x = pyfits.Column(name='X',
                          format='D',
                          unit='pixel',
                          array=events["xpix"])
    col_y = pyfits.Column(name='Y',
                          format='D',
                          unit='pixel',
                          array=events["ypix"])
    col_e = pyfits.Column(name='ENERGY',
                          format='E',
                          unit='eV',
                          array=events["energy"] * 1000.)
    col_dx = pyfits.Column(name='DETX',
                           format='D',
                           unit='pixel',
                           array=events["detx"])
    col_dy = pyfits.Column(name='DETY',
                           format='D',
                           unit='pixel',
                           array=events["dety"])
    col_id = pyfits.Column(name='CCD_ID',
                           format='D',
                           unit='pixel',
                           array=events["ccd_id"])

    chantype = parameters["channel_type"]
    if chantype == "PHA":
        cunit = "adu"
    elif chantype == "PI":
        cunit = "Chan"
    col_ch = pyfits.Column(name=chantype.upper(),
                           format='1J',
                           unit=cunit,
                           array=events[chantype])

    col_t = pyfits.Column(name="TIME",
                          format='1D',
                          unit='s',
                          array=events['time'])

    cols = [col_e, col_x, col_y, col_ch, col_t, col_dx, col_dy, col_id]

    coldefs = pyfits.ColDefs(cols)
    tbhdu = pyfits.BinTableHDU.from_columns(coldefs)
    tbhdu.name = "EVENTS"

    tbhdu.header["MTYPE1"] = "sky"
    tbhdu.header["MFORM1"] = "x,y"
    tbhdu.header["MTYPE2"] = "EQPOS"
    tbhdu.header["MFORM2"] = "RA,DEC"
    tbhdu.header["TCTYP2"] = "RA---TAN"
    tbhdu.header["TCTYP3"] = "DEC--TAN"
    tbhdu.header["TCRVL2"] = parameters["sky_center"][0]
    tbhdu.header["TCRVL3"] = parameters["sky_center"][1]
    tbhdu.header["TCDLT2"] = -parameters["plate_scale"]
    tbhdu.header["TCDLT3"] = parameters["plate_scale"]
    tbhdu.header["TCRPX2"] = parameters["pix_center"][0]
    tbhdu.header["TCRPX3"] = parameters["pix_center"][1]
    tbhdu.header["TCUNI2"] = "deg"
    tbhdu.header["TCUNI3"] = "deg"
    tbhdu.header["TLMIN2"] = 0.5
    tbhdu.header["TLMIN3"] = 0.5
    tbhdu.header["TLMAX2"] = 2.0 * parameters["num_pixels"] + 0.5
    tbhdu.header["TLMAX3"] = 2.0 * parameters["num_pixels"] + 0.5
    tbhdu.header["TLMIN4"] = parameters["chan_lim"][0]
    tbhdu.header["TLMAX4"] = parameters["chan_lim"][1]
    tbhdu.header["TLMIN6"] = -0.5 * parameters["num_pixels"]
    tbhdu.header["TLMAX6"] = 0.5 * parameters["num_pixels"]
    tbhdu.header["TLMIN7"] = -0.5 * parameters["num_pixels"]
    tbhdu.header["TLMAX7"] = 0.5 * parameters["num_pixels"]
    tbhdu.header["EXPOSURE"] = parameters["exposure_time"]
    tbhdu.header["TSTART"] = 0.0
    tbhdu.header["TSTOP"] = parameters["exposure_time"]
    tbhdu.header["HDUVERS"] = "1.1.0"
    tbhdu.header["RADECSYS"] = "FK5"
    tbhdu.header["EQUINOX"] = 2000.0
    tbhdu.header["HDUCLASS"] = "OGIP"
    tbhdu.header["HDUCLAS1"] = "EVENTS"
    tbhdu.header["HDUCLAS2"] = "ACCEPTED"
    tbhdu.header["DATE"] = t_begin.tt.isot
    tbhdu.header["DATE-OBS"] = t_begin.tt.isot
    tbhdu.header["DATE-END"] = t_end.tt.isot
    tbhdu.header["RESPFILE"] = os.path.split(parameters["rmf"])[-1]
    tbhdu.header["PHA_BINS"] = parameters["nchan"]
    tbhdu.header["ANCRFILE"] = os.path.split(parameters["arf"])[-1]
    tbhdu.header["CHANTYPE"] = parameters["channel_type"]
    tbhdu.header["MISSION"] = parameters["mission"]
    tbhdu.header["TELESCOP"] = parameters["telescope"]
    tbhdu.header["INSTRUME"] = parameters["instrument"]
    tbhdu.header["RA_PNT"] = parameters["sky_center"][0]
    tbhdu.header["DEC_PNT"] = parameters["sky_center"][1]
    tbhdu.header["ROLL_PNT"] = parameters["roll_angle"]
    tbhdu.header["AIMPT_X"] = parameters["aimpt_coords"][0]
    tbhdu.header["AIMPT_Y"] = parameters["aimpt_coords"][1]
    if parameters["dither_params"]["dither_on"]:
        tbhdu.header["DITHXAMP"] = parameters["dither_params"]["x_amp"]
        tbhdu.header["DITHYAMP"] = parameters["dither_params"]["y_amp"]
        tbhdu.header["DITHXPER"] = parameters["dither_params"]["x_period"]
        tbhdu.header["DITHYPER"] = parameters["dither_params"]["y_period"]

    start = pyfits.Column(name='START',
                          format='1D',
                          unit='s',
                          array=np.array([0.0]))
    stop = pyfits.Column(name='STOP',
                         format='1D',
                         unit='s',
                         array=np.array([parameters["exposure_time"]]))

    tbhdu_gti = pyfits.BinTableHDU.from_columns([start, stop])
    tbhdu_gti.name = "STDGTI"
    tbhdu_gti.header["TSTART"] = 0.0
    tbhdu_gti.header["TSTOP"] = parameters["exposure_time"]
    tbhdu_gti.header["HDUCLASS"] = "OGIP"
    tbhdu_gti.header["HDUCLAS1"] = "GTI"
    tbhdu_gti.header["HDUCLAS2"] = "STANDARD"
    tbhdu_gti.header["RADECSYS"] = "FK5"
    tbhdu_gti.header["EQUINOX"] = 2000.0
    tbhdu_gti.header["DATE"] = t_begin.tt.isot
    tbhdu_gti.header["DATE-OBS"] = t_begin.tt.isot
    tbhdu_gti.header["DATE-END"] = t_end.tt.isot

    hdulist = [pyfits.PrimaryHDU(), tbhdu, tbhdu_gti]

    pyfits.HDUList(hdulist).writeto(filename, overwrite=overwrite)
Ejemplo n.º 37
0
def make_uniform_background(energy, event_params, rmf, prng=None):
    from soxs.instrument import perform_dither
    import pyregion._region_filter as rfilter

    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

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

    n_e = bkg_events["energy"].size

    bkg_events['time'] = prng.uniform(size=n_e,
                                      low=0.0,
                                      high=event_params["exposure_time"])

    x_offset, y_offset = perform_dither(bkg_events["time"],
                                        event_params["dither_params"])

    rot_mat = get_rot_mat(event_params["roll_angle"])

    det = np.array([
        bkg_events["detx"] + x_offset - event_params["aimpt_coords"][0],
        bkg_events["dety"] + y_offset - event_params["aimpt_coords"][1]
    ])
    pix = np.dot(rot_mat.T, det)

    bkg_events["xpix"] = pix[0, :] + event_params['pix_center'][0]
    bkg_events["ypix"] = pix[1, :] + event_params['pix_center'][1]

    mylog.info("Scattering energies with RMF %s." %
               os.path.split(rmf.filename)[-1])
    bkg_events = rmf.scatter_energies(bkg_events, prng=prng)

    return bkg_events
Ejemplo n.º 38
0
def make_instrument_background(bkgnd_name, event_params, rmf, prng=None):
    import pyregion._region_filter as rfilter

    prng = parse_prng(prng)

    if event_params["chips"] is None:
        bkgnd_spec = [instrument_backgrounds[bkgnd_name]]
    else:
        if isinstance(bkgnd_name, string_types):
            nchips = len(event_params["chips"])
            bkgnd_names = [bkgnd_name] * nchips
        else:
            bkgnd_names = bkgnd_name
        bkgnd_spec = []
        for name in bkgnd_names:
            spec = instrument_backgrounds[name].new_spec_from_band(
                rmf.elo[0], rmf.ehi[-1])
            bkgnd_spec.append(spec)

    bkg_events = {}

    nx = event_params["num_pixels"]

    if event_params["chips"] is None:
        bkg_events["energy"] = bkgnd_spec[0].generate_energies(
            event_params["exposure_time"],
            event_params["fov"],
            prng=prng,
            quiet=True).value
        n_events = bkg_events["energy"].size
        bkg_events["chip_id"] = np.zeros(n_events, dtype='int')
        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)
    else:
        bkg_events["energy"] = []
        bkg_events["detx"] = []
        bkg_events["dety"] = []
        bkg_events["chip_id"] = []
        for i, chip in enumerate(event_params["chips"]):
            e = bkgnd_spec[i].generate_energies(event_params["exposure_time"],
                                                event_params["fov"],
                                                prng=prng,
                                                quiet=True).value
            n_events = e.size
            detx = prng.uniform(low=-0.5 * nx, high=0.5 * nx, size=n_events)
            dety = prng.uniform(low=-0.5 * nx, high=0.5 * nx, size=n_events)
            thisc = np.ones(n_events, dtype='bool')
            rtype = chip[0]
            args = chip[1:]
            r = getattr(rfilter, rtype)(*args)
            inside = r.inside(detx, dety)
            thisc = np.logical_and(thisc, inside)
            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(thisc.sum()))
        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("Making %d events from the instrumental background." %
                   bkg_events["energy"].size)

    return make_diffuse_background(bkg_events, event_params, rmf, prng=prng)
Ejemplo n.º 39
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
Ejemplo n.º 40
0
 def __init__(self, emin, emax, nbins, var_elem=None, apec_root=None,
              apec_vers=None, broadening=True, nolines=False,
              abund_table=None, nei=False):
     if apec_vers is None:
         apec_vers = default_apec_vers
     mylog.info(f"Using APEC version {apec_vers}.")
     if nei and var_elem is None:
         raise RuntimeError("For NEI spectra, you must specify which elements "
                            "you want to vary using the 'var_elem' argument!")
     self.nei = nei
     emin = parse_value(emin, "keV")
     emax = parse_value(emax, 'keV')
     self.emin = emin
     self.emax = emax
     self.nbins = nbins
     self.ebins = np.linspace(self.emin, self.emax, nbins+1)
     self.de = np.diff(self.ebins)
     self.emid = 0.5*(self.ebins[1:]+self.ebins[:-1])
     if nei:
         neistr = "_nei"
         ftype = "comp"
     else:
         neistr = ""
         ftype = "coco"
     cocofile = f"apec_v{apec_vers}{neistr}_{ftype}.fits"
     linefile = f"apec_v{apec_vers}{neistr}_line.fits"
     if apec_root is None:
         self.cocofile = get_data_file(cocofile)
         self.linefile = get_data_file(linefile)
     else:
         self.cocofile = os.path.join(apec_root, cocofile)
         self.linefile = os.path.join(apec_root, linefile)
     if not os.path.exists(self.cocofile) or not os.path.exists(self.linefile):
         raise IOError(f"Cannot find the APEC files!\n {self.cocofile}\n, "
                       f"{self.linefile}")
     mylog.info(f"Using {cocofile} for generating spectral lines.")
     mylog.info(f"Using {linefile} for generating the continuum.")
     self.nolines = nolines
     self.wvbins = hc/self.ebins[::-1]
     self.broadening = broadening
     self.line_handle = pyfits.open(self.linefile)
     self.coco_handle = pyfits.open(self.cocofile)
     self.nT = self.line_handle[1].data.shape[0]
     self.Tvals = self.line_handle[1].data.field("kT")
     self.dTvals = np.diff(self.Tvals)
     self.minlam = self.wvbins.min()
     self.maxlam = self.wvbins.max()
     self.var_elem_names = []
     self.var_ion_names = []
     if var_elem is None:
         self.var_elem = np.empty((0, 1), dtype='int')
     else:
         self.var_elem = []
         if len(var_elem) != len(set(var_elem)):
             raise RuntimeError("Duplicates were found in the \"var_elem\" list! %s" % var_elem)
         for elem in var_elem:
             if "^" in elem:
                 if not self.nei:
                     raise RuntimeError("Cannot use different ionization states with a "
                                        "CIE plasma!")
                 el = elem.split("^")
                 e = el[0]
                 ion = int(el[1])
             else:
                 if self.nei:
                     raise RuntimeError("Variable elements must include the ionization "
                                        "state for NEI plasmas!")
                 e = elem
                 ion = 0
             self.var_elem.append([elem_names.index(e), ion])
         self.var_elem.sort(key=lambda x: (x[0], x[1]))
         self.var_elem = np.array(self.var_elem, dtype='int')
         self.var_elem_names = [elem_names[e[0]] for e in self.var_elem]
         self.var_ion_names = ["%s^%d" % (elem_names[e[0]], e[1]) for e in self.var_elem]
     self.num_var_elem = len(self.var_elem)
     if self.nei:
         self.cosmic_elem = [elem for elem in [1, 2]
                             if elem not in self.var_elem[:, 0]]
         self.metal_elem = []
     else:
         self.cosmic_elem = [elem for elem in cosmic_elem 
                             if elem not in self.var_elem[:,0]]
         self.metal_elem = [elem for elem in metal_elem
                            if elem not in self.var_elem[:,0]]
     if abund_table is None:
         abund_table = soxs_cfg.get("soxs", "abund_table")
     if not isinstance(abund_table, str):
         if len(abund_table) != 30:
             raise RuntimeError("User-supplied abundance tables "
                                "must be 30 elements long!")
         self.atable = np.concatenate([[0.0], np.array(abund_table)])
     else:
         self.atable = abund_tables[abund_table].copy()
     self._atable = self.atable.copy()
     self._atable[1:] /= abund_tables["angr"][1:]
Ejemplo n.º 41
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,
                    aimpt_shift=None,
                    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
    aimpt_shift : array-like, optional
        A two-float array-like object which shifts the aimpoint on the 
        detector from the nominal position. Units are in arcseconds.
        Default: None, which results in no shift from the nominal aimpoint. 
    prng : :class:`~numpy.random.RandomState` object, integer, or None
        A pseudo-random number generator. Typically will only 
        be specified if you have a reason to generate the same 
        set of random numbers, such as for a test. Default is None, 
        which sets the seed based on the system time. 
    """
    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(f"Instrument {instrument} is not in the "
                       f"instrument registry!")
    if not instrument_spec["imaging"]:
        raise RuntimeError(f"Instrument '{instrument_spec['name']}' is not "
                           f"designed for imaging observations!")
    fov = instrument_spec["fov"]

    input_events = defaultdict(list)

    arf_file = get_data_file(instrument_spec["arf"])
    arf = AuxiliaryResponseFile(arf_file)
    rmf_file = get_data_file(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["src_names"].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,
                                               aimpt_shift=aimpt_shift,
                                               prng=prng)
        mylog.info(f"Generated {events['energy'].size} photons from "
                   f"the point-source background.")
    else:
        nx = instrument_spec["num_pixels"]
        plate_scale = instrument_spec["fov"] / nx / 60.0
        plate_scale_arcsec = plate_scale * 3600.0
        if aimpt_shift is None:
            aimpt_shift = np.zeros(2)
        aimpt_shift = ensure_numpy_array(aimpt_shift).astype('float64')
        aimpt_shift /= plate_scale_arcsec
        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": plate_scale,
            "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"],
            "aimpt_shift": aimpt_shift
        }

    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,
                                                event_params,
                                                rmf,
                                                prng=prng)
        for key in bkg_events:
            events[key] = np.concatenate([events[key], bkg_events[key]])

    return events, event_params
Ejemplo n.º 42
0
def instrument_simulator(simput_file,
                         out_file,
                         exp_time,
                         instrument,
                         sky_center,
                         clobber=False,
                         dither_shape="square",
                         dither_size=16.0,
                         roll_angle=0.0,
                         astro_bkgnd=True,
                         instr_bkgnd=True,
                         prng=np.random):
    """
    Take unconvolved events in a SIMPUT catalog and create an event
    file from them. This function does the following:

    1. Determines which events are observed using the ARF
    2. Pixelizes the events, applying PSF effects and dithering
    3. Adds instrumental and astrophysical background events
    4. Determines energy channels using the RMF
    5. Writes the events to a file

    Parameters
    ----------
    simput_file : string
        The SIMPUT catalog file to be used as input.
    out_file : string
        The name of the event file to be written.
    exp_time : float
        The exposure time to use, in seconds. 
    instrument : string
        The name of the instrument to use, which picks an instrument
        specification from the instrument registry. Can also be a JSON
        file with a new instrument specification. If this is the case,
        it will be loaded into the instrument registry. 
    sky_center : array, tuple, or list
        The center RA, Dec coordinates of the observation, in degrees.
    clobber : boolean, optional
        Whether or not to clobber an existing file with the same name.
        Default: False
    dither_shape : string
        The shape of the dither. Currently "circle" or "square" 
        Default: "square"
    dither_size : float
        The size of the dither in arcseconds. Width of square or radius
        of circle. Default: 16.0
    roll_angle : float
        The roll angle of the observation in degrees. Default: 0.0
    astro_bkgnd : boolean, optional
        Whether or not to include astrophysical background. Default: True
    instr_bkgnd : boolean, optional
        Whether or not to include instrumental/particle background. Default: True
    prng : :class:`~numpy.random.RandomState` object or :mod:`~numpy.random`, optional
        A pseudo-random number generator. Typically will only be specified
        if you have a reason to generate the same set of random numbers, such as for a
        test. Default is the :mod:`numpy.random` module.

    Examples
    --------
    >>> instrument_simulator("sloshing_simput.fits", "sloshing_evt.fits", "hdxi_3x10",
    ...                      [30., 45.], clobber=True)
    """
    event_list, parameters = read_simput_catalog(simput_file)

    try:
        instrument_spec = instrument_registry[instrument]
    except KeyError:
        raise KeyError("Instrument %s is not in the instrument registry!" %
                       instrument)
    arf_file = check_file_location(instrument_spec["arf"], "files")
    rmf_file = check_file_location(instrument_spec["rmf"], "files")
    arf = AuxiliaryResponseFile(arf_file)
    rmf = RedistributionMatrixFile(rmf_file)

    nx = instrument_spec["num_pixels"]
    plate_scale = instrument_spec["fov"] / nx / 60.  # arcmin to deg
    plate_scale_arcsec = plate_scale * 3600.0
    dsize = dither_size / plate_scale_arcsec

    event_params = {}
    event_params["exposure_time"] = exp_time
    event_params["arf"] = os.path.split(arf.filename)[-1]
    event_params["sky_center"] = sky_center
    event_params["pix_center"] = np.array([0.5 * (nx + 1)] * 2)
    event_params["num_pixels"] = nx
    event_params["plate_scale"] = plate_scale
    event_params["rmf"] = os.path.split(rmf.filename)[-1]
    event_params["channel_type"] = rmf.header["CHANTYPE"]
    event_params["telescope"] = rmf.header["TELESCOP"]
    event_params["instrument"] = rmf.header["INSTRUME"]
    event_params["mission"] = rmf.header.get("MISSION", "")
    event_params["nchan"] = rmf.ebounds_header["DETCHANS"]
    event_params["roll_angle"] = roll_angle
    event_params["fov"] = instrument_spec["fov"]
    num = 0
    for i in range(1, rmf.num_mat_columns + 1):
        if rmf.header["TTYPE%d" % i] == "F_CHAN":
            num = i
            break
    event_params["chan_lim"] = [
        rmf.header["TLMIN%d" % num], rmf.header["TLMAX%d" % num]
    ]

    w = pywcs.WCS(naxis=2)
    w.wcs.crval = event_params["sky_center"]
    w.wcs.crpix = event_params["pix_center"]
    w.wcs.cdelt = [-plate_scale, plate_scale]
    w.wcs.ctype = ["RA---TAN", "DEC--TAN"]
    w.wcs.cunit = ["deg"] * 2

    roll_angle = np.deg2rad(roll_angle)
    rot_mat = np.array([[np.sin(roll_angle), -np.cos(roll_angle)],
                        [-np.cos(roll_angle), -np.sin(roll_angle)]])

    all_events = {}

    first = True

    for i, evts in enumerate(event_list):

        mylog.info("Detecting events from source %d" % (i + 1))

        # Step 1: Use ARF to determine which photons are observed

        mylog.info("Applying energy-dependent effective area from %s." %
                   event_params["arf"])
        refband = [parameters["emin"][i], parameters["emax"][i]]
        events = arf.detect_events(evts,
                                   exp_time,
                                   parameters["flux"][i],
                                   refband,
                                   prng=prng)

        n_evt = events["energy"].size

        if n_evt == 0:
            mylog.warning("No events were observed for this source!!!")
        else:

            # Step 2: Assign pixel coordinates to events. Apply dithering and
            # PSF. Clip events that don't fall within the detection region.

            mylog.info("Pixeling events.")

            # Convert RA, Dec to pixel coordinates
            xpix, ypix = w.wcs_world2pix(events["ra"], events["dec"], 1)

            xpix -= event_params["pix_center"][0]
            ypix -= event_params["pix_center"][1]

            events.pop("ra")
            events.pop("dec")

            n_evt = xpix.size

            # Dither pixel coordinates

            x_offset = np.zeros(n_evt)
            y_offset = np.zeros(n_evt)
            if dither_shape == "circle":
                r = dsize * np.sqrt(prng.uniform(size=n_evt))
                theta = 2. * np.pi * prng.uniform(size=n_evt)
                x_offset = r * np.cos(theta)
                y_offset = r * np.sin(theta)
            elif dither_shape == "square":
                x_offset = dsize * prng.uniform(low=-0.5, high=0.5, size=n_evt)
                y_offset = dsize * prng.uniform(low=-0.5, high=0.5, size=n_evt)

            xpix -= x_offset
            ypix -= y_offset

            # Rotate physical coordinates to detector coordinates

            det = np.dot(rot_mat, np.array([xpix, ypix]))
            detx = det[0, :]
            dety = det[1, :]

            # PSF scattering of detector coordinates

            psf_type, psf_spec = instrument_spec["psf"]
            if psf_type == "gaussian":
                sigma = psf_spec / sigma_to_fwhm / plate_scale_arcsec
                detx += prng.normal(loc=0.0, scale=sigma, size=n_evt)
                dety += prng.normal(loc=0.0, scale=sigma, size=n_evt)
            else:
                raise NotImplementedError("PSF type %s not implemented!" %
                                          psf_type)

            # Convert detector coordinates to chip coordinates

            events["chipx"] = np.round(detx + event_params['pix_center'][0])
            events["chipy"] = np.round(dety + event_params['pix_center'][1])

            # Throw out events that don't fall on the chip

            keepx = np.logical_and(events["chipx"] >= 1.0,
                                   events["chipx"] <= nx)
            keepy = np.logical_and(events["chipy"] >= 1.0,
                                   events["chipy"] <= nx)
            keep = np.logical_and(keepx, keepy)
            mylog.info("%d events were rejected because " %
                       (n_evt - keep.sum()) +
                       "they fall outside the field of view.")
            n_evt = keep.sum()

            if n_evt == 0:
                mylog.warning(
                    "No events are within the field of view for this source!!!"
                )
            else:

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

                # Convert chip coordinates back to detector coordinates

                events["detx"] = np.round(
                    events["chipx"] - event_params['pix_center'][0] +
                    prng.uniform(low=-0.5, high=0.5, size=n_evt))
                events["dety"] = np.round(
                    events["chipy"] - event_params['pix_center'][1] +
                    prng.uniform(low=-0.5, high=0.5, size=n_evt))

                # Convert detector coordinates back to pixel coordinates

                pix = np.dot(rot_mat,
                             np.array([events["detx"], events["dety"]]))

                events["xpix"] = pix[
                    0, :] + event_params['pix_center'][0] + x_offset[keep]
                events["ypix"] = pix[
                    1, :] + event_params['pix_center'][1] + y_offset[keep]

        if n_evt > 0:
            for key in events:
                if first:
                    all_events[key] = events[key]
                else:
                    all_events[key] = np.concatenate(
                        [all_events[key], events[key]])
            first = False

    if all_events["energy"].size == 0:
        mylog.warning(
            "No events from any of the sources in the catalog were detected!")

    # Step 3: Add astrophysical background

    if astro_bkgnd:
        mylog.info("Adding in astrophysical background.")
        bkg_events = add_background(astro_bkgnd,
                                    event_params,
                                    rot_mat,
                                    prng=prng)
        for key in all_events:
            all_events[key] = np.concatenate(
                [all_events[key], bkg_events[key]])

    # Step 4: Add particle background

    if instr_bkgnd:
        mylog.info("Adding in instrumental background.")
        bkg_events = add_background(
            instrument_spec["bkgnd"],
            event_params,
            rot_mat,
            prng=prng,
            focal_length=instrument_spec["focal_length"])
        for key in all_events:
            all_events[key] = np.concatenate(
                [all_events[key], bkg_events[key]])

    if all_events["energy"].size == 0:
        raise RuntimeError("No events were detected!!!")

    # Step 5: Scatter energies with RMF

    if all_events["energy"].size > 0:
        mylog.info("Scattering energies with RMF %s." % event_params['rmf'])
        all_events = rmf.scatter_energies(all_events, prng=prng)

    # Step 6: Add times to events

    all_events['time'] = np.random.uniform(size=all_events["energy"].size,
                                           low=0.0,
                                           high=event_params["exposure_time"])

    write_event_file(all_events, event_params, out_file, clobber=clobber)
Ejemplo n.º 43
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)
Ejemplo n.º 44
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