Example #1
0
    def get_rstar(self, df, source):

        # print(df.epo_x.values)
        # exit()

        cstar = SkyCoord(
            ra=source.cat.ra.values * u.rad,
            dec=source.cat.dec.values * u.rad,
            # distance = 1/(rad2arcsec*source.cat.par.values) * u.pc,frame='icrs')
            pm_ra_cosdec=source.cat.mu_a.values * u.rad / u.yr *
            np.cos(source.cat.dec.values),
            pm_dec=source.cat.mu_d.values * u.rad / u.yr,
            distance=1 / (rad2arcsec * source.cat.par.values) * u.pc,
            frame='icrs')
        cstar = cstar.apply_space_motion(dt=df.epo_x.values * u.year)

        if debug:
            print("cstar radec + pm =", cstar)

        cstar.representation_type = 'cartesian'
        rstar = np.column_stack(
            [cstar.x.to(u.m),
             cstar.y.to(u.m),
             cstar.z.to(u.m)])

        if debug:
            print("cstar =", cstar)
            print("cstar =", rstar, np.linalg.norm(rstar))
            print("cstar normalized =", rstar / np.linalg.norm(rstar))

        return rstar
Example #2
0
def image_gaia_query(image,
                     *args,
                     limit=3000,
                     correct_pm=True,
                     wcs=True,
                     circular=True,
                     fov=None):

    if wcs:
        center = image.wcs.pixel_to_world(*(np.array(image.shape) / 2)[::-1])
    else:
        center = image.skycoord

    if fov is None:
        fov = image.fov

    table = gaia_query(center, fov, "*", limit=limit, circular=circular)

    if correct_pm:
        skycoord = SkyCoord(ra=table['ra'].quantity,
                            dec=table['dec'].quantity,
                            pm_ra_cosdec=table['pmra'].quantity,
                            pm_dec=table['pmdec'].quantity,
                            distance=table['parallax'].quantity,
                            obstime=Time('2015-06-01 00:00:00.0'))
        with warnings.catch_warnings():
            warnings.simplefilter("ignore")
            skycoord = skycoord.apply_space_motion(Time(image.date))

        table["ra"] = skycoord.ra
        table["dec"] = skycoord.dec

    return table
Example #3
0
def _correct_with_proper_motion(ra, dec, pm_ra, pm_dec, equinox, new_time):
    """Return proper-motion corrected RA / Dec.
       It also return whether proper motion correction is applied or not."""
    # all parameters have units

    if ra is None or dec is None or \
       pm_ra is None or pm_dec is None or (np.all(pm_ra == 0) and np.all(pm_dec == 0)) or \
       equinox is None:
        return ra, dec, False

    # To be more accurate, we should have supplied distance to SkyCoord
    # in theory, for Gaia DR2 data, we can infer the distance from the parallax provided.
    # It is not done for 2 reasons:
    # 1. Gaia DR2 data has negative parallax values occasionally. Correctly handling them could be tricky. See:
    #    https://www.cosmos.esa.int/documents/29201/1773953/Gaia+DR2+primer+version+1.3.pdf/a4459741-6732-7a98-1406-a1bea243df79
    # 2. For our purpose (ploting in various interact usage) here, the added distance does not making
    #    noticeable significant difference. E.g., applying it to Proxima Cen, a target with large parallax
    #    and huge proper motion, does not change the result in any noticeable way.
    #
    c = SkyCoord(ra,
                 dec,
                 pm_ra_cosdec=pm_ra,
                 pm_dec=pm_dec,
                 frame='icrs',
                 obstime=equinox)

    # Suppress ErfaWarning temporarily as a workaround for:
    #   https://github.com/astropy/astropy/issues/11747
    with warnings.catch_warnings():
        # the same warning appears both as an ErfaWarning and a astropy warning
        # so we filter by the message instead
        warnings.filterwarnings("ignore", message="ERFA function")
        new_c = c.apply_space_motion(new_obstime=new_time)
    return new_c.ra, new_c.dec, True
Example #4
0
def coordinate_propagator(ra, dec, parallax, pmra, pmdec, ref_epoch,
                          target_epoch, tic_id):
    '''Authors:
		Patrick Tamburo, Boston University, July 2020
	Purpose:
        Given a target's position, parallax, proper motion, and reference epoch, propagate space motion to target epoch. 
        See https://docs.astropy.org/en/stable/coordinates/apply_space_motion.html for more examples. 
	Inputs:
        ra (float): The target's DECIMAL RA at your reference epoch
        dec (float): The target's DECIMAL Dec at your reference epoch 
        parallax (float): The target's parallax in milliarcsec
        pmra (flaot): The target's RA proper motion in milliarcsec/year
        pmdec (float): The target's Dec proper motion in millarcsec/year
        ref_epoch (float): Decimal year representing the epoch when coordinates, parallax, and proper motions were measured (e.g., 2015.5 for Gaia measurements)
        target_epoch (str): A string representing the epoch you want to propagate motion to (e.g., '2019-09-25')
        tic_id (str): The target's TIC ID (e.g., 'TIC 326109505')
    Outputs:
        new_ra, new_dec (float): The propagated position of the target at target_epoch.
	TODO:
    '''
    c = SkyCoord(ra=ra * u.deg,
                 dec=dec * u.deg,
                 distance=Distance(parallax=parallax * u.mas),
                 pm_ra_cosdec=pmra * u.mas / u.yr,
                 pm_dec=pmdec * u.mas / u.yr,
                 obstime=Time(ref_epoch, format='decimalyear'))
    target_obstime = Time(target_epoch)
    c_target_epoch = c.apply_space_motion(
        target_obstime)  #Apply space motion to TESS epoch
    new_ra = c_target_epoch.ra.value
    new_dec = c_target_epoch.dec.value
    return new_ra, new_dec
    def properMotion(self, SkyCoo, properMotion, dt):
        c = SkyCoord(ra=skyCoo[0] * u.degree,
                     dec=skyCoo[1] * u.degree,
                     pm_l_cosb=properMotion[0] * u.mas / u.yr,
                     pm_b=properMotion[1] * u.mas / u.yr,
                     frame='galactic',
                     obstime=Time('1999-01-01T00:00:00.123'))

        return c.apply_space_motion(dt=dt * u.year)
Example #6
0
def vizierLocationForTarget(exp, target, doMotionCorrection):
    """Get the target location from Vizier optionally correction motion.

    Parameters
    ----------
    target : `str`
        The name of the target, e.g. 'HD 55852'

    Returns
    -------
    targetLocation : `lsst.geom.SpherePoint` or `None`
        Location of the target object, optionally corrected for
        proper motion and parallax.

    Raises
    ------
    ValueError
        If object not found in Hipparcos2 via Vizier.
        This is quite common, even for bright objects.
    """
    # do not import at the module level - tests crash due to a race
    # condition with directory creation
    from astroquery.vizier import Vizier

    result = Vizier.query_object(
        target)  # result is an empty table list for an unknown target
    try:
        star = result['I/311/hip2']
    except TypeError:  # if 'I/311/hip2' not in result (result doesn't allow easy checking without a try)
        raise ValueError

    epoch = "J1991.25"
    coord = SkyCoord(
        ra=star[0]['RArad'] * u.Unit(star['RArad'].unit),
        dec=star[0]['DErad'] * u.Unit(star['DErad'].unit),
        obstime=epoch,
        pm_ra_cosdec=star[0]['pmRA'] *
        u.Unit(star['pmRA'].unit),  # NB contains cosdec already
        pm_dec=star[0]['pmDE'] * u.Unit(star['pmDE'].unit),
        distance=Distance(parallax=star[0]['Plx'] * u.Unit(star['Plx'].unit)))

    if doMotionCorrection:
        expDate = exp.getInfo().getVisitInfo().getDate()
        obsTime = astropy.time.Time(expDate.get(expDate.EPOCH),
                                    format='jyear',
                                    scale='tai')
        newCoord = coord.apply_space_motion(new_obstime=obsTime)
    else:
        newCoord = coord

    raRad, decRad = newCoord.ra.rad, newCoord.dec.rad
    ra = geom.Angle(raRad)
    dec = geom.Angle(decRad)
    targetLocation = geom.SpherePoint(ra, dec)
    return targetLocation
Example #7
0
def test_regression_10092():
    """
    Check that we still get a proper motion even for SkyCoords without distance
    """
    c = SkyCoord(l=10*u.degree, b=45*u.degree,
                 pm_l_cosb=34*u.mas/u.yr, pm_b=-117*u.mas/u.yr,
                 frame='galactic',
                 obstime=Time('1988-12-18 05:11:23.5'))

    with pytest.warns(ErfaWarning, match='ERFA function "pmsafe" yielded .*'):
        # expect ErfaWarning here
        newc = c.apply_space_motion(dt=10*u.year)
    assert_quantity_allclose(newc.pm_l_cosb, 33.99980714*u.mas/u.yr,
                             atol=1.0e-5*u.mas/u.yr)
Example #8
0
def precess_gaia_coordinates(gaia_df, new_epoch=2000.0):
    '''
    Precesses target coordinates from Gaia (i.e., using a parallax) from the
    original epoch to a new epoch. Radial velocities are assumed to be zero
    (because they're measured poorly). "The new position of the source is
    determined assuming that it moves in a straight line with constant velocity
    in an inertial frame."

    Adapted from the astropy docs:
        https://docs.astropy.org/en/stable/coordinates/apply_space_motion.html

    [Note that the *equinox* is a currently obsolete concept, formerly tied to
    equatorial coordinate systems centered on the Earth. The ICRS reference
    system, which Gaia uses, has an origin at the solar system barycenter and
    kinematic axes that are fixed and non-rotating. The nutation of the Earth
    does not affect these coordinates. See
    https://www.cosmos.esa.int/web/gaia/faqs#ICRSICRF]

    Parameters
    ----------

    gaia_df: pandas DataFrame of Gaia data.
        It's assumed that you're starting from Gaia data (any data release).
        Keys must include 'ra', 'dec', 'parallax', 'pmra', 'pmdec',
        'ref_epoch'. This can also be a dict.

    new_epoch: float
        Target epoch in Julian year format to precess from origin epoch. This
        is a float, like: 2000.0, 2021.0, etc.

    Returns
    -------

    c, c_new_epoch: astropy Coordinate
        Original and precessed astropy coordinate objects corresponding to new_epoch.
    '''

    c = SkyCoord(ra=nparr(gaia_df['ra']) * u.deg,
                 dec=nparr(gaia_df['dec']) * u.deg,
                 distance=Distance(parallax=nparr(gaia_df['parallax']) * u.mas),
                 pm_ra_cosdec=nparr(gaia_df['pmra']) * u.mas/u.yr,
                 pm_dec=nparr(gaia_df['pmdec']) * u.mas/u.yr,
                 obstime=Time(nparr(gaia_df['ref_epoch']), format='jyear'))

    new_epoch = Time(new_epoch, format='jyear')

    c_new_epoch = c.apply_space_motion(new_epoch)

    return c, c_new_epoch
Example #9
0
def propagate_pm(ticstars):
    ''' propagate proper motions to target epoch, re-compute the radial
        distances and sort the array by distances again
    '''
    nrstars = len(ticstars)
    epochs = np.ndarray(nrstars, dtype='object')
    epochs.fill(ticepoch)
    mask = ~np.isnan(ticstars['pmRA'].data.data) & ~np.isnan(
        ticstars['pmDEC'].data.data)
    gd = np.where(mask)
    ngd = nrw(gd)
    if ngd == 0:
        # nothing to do
        return

    # assume 1000 pc distance for stars without known distance
    dist = ticstars['d']
    bd = np.where(np.isnan(dist.data.data))
    dist[bd] = 1000.0

    # setup astropy coordinates, apply space motion and correct coordinates
    coords = SkyCoord(ra=ticstars['ra'][gd] * u.deg,
                      dec=ticstars['dec'][gd] * u.deg,
                      pm_ra_cosdec=ticstars['pmRA'][gd] * u.mas / u.yr,
                      pm_dec=ticstars['pmDEC'][gd] * u.mas / u.yr,
                      obstime=ticepoch,
                      distance=dist[gd] * u.pc)
    newcoords = coords.apply_space_motion(Time(options.targetepoch))
    newra = newcoords.ra.degree
    newdec = newcoords.dec.degree
    oldra = deepcopy(ticstars['ra'])
    olddec = deepcopy(ticstars['dec'])
    ticstars['ra'][gd] = newra
    ticstars['dec'][gd] = newdec

    # recompute the angular distances with new coordinates
    c0 = SkyCoord(ra=ticstars['ra'][0] * u.deg, dec=ticstars['dec'][0] * u.deg)
    for i in range(1, nrstars):  # skip the target star itself
        cs = SkyCoord(ra=ticstars['ra'][i] * u.deg,
                      dec=ticstars['dec'][i] * u.deg)
        rdist = c0.separation(cs)
        ticstars['dstArcSec'][i] = rdist.arcsecond

    # resort table by new radial distance
    ticstars.sort(['dstArcSec'])
Example #10
0
    def get_gaia_sources(self):
        """ Get the source table from gaia"""

        # Find the center of the first frame:
        #        ra, dec = self.wcs[0].all_pix2world(self.center[:, None].T, 0).T
        #        ra, dec = ra[0], dec[0]
        #        ra2, dec2 = self.wcs[0].all_pix2world(np.asarray([[self.center[0] + self.shape[2]/2, self.center[1] + self.shape[1]/2]]), 0)[0]

        ra, dec = self.wcs[0].all_pix2world(
            np.vstack([self.X.ravel(), self.Y.ravel()]).T, 0).T
        #        height = ((np.max(dec) - np.min(dec)) * 1.01)*u.deg
        #        width = ((np.max(ra) - np.min(ra))/4)*u.deg
        radius = np.hypot((ra.max() - ra.min()) / 2,
                          (dec.max() - dec.min()) / 2)
        #        r = np.hypot(ra - ra2, dec - dec2)
        #        print(ra, dec, r)
        sources = get_sources(
            ra.mean(),
            dec.mean(),
            radius=radius,  #height=height, width=width,
            epoch=self.time[0],
            magnitude_limit=self.magnitude_limit).reset_index(drop=True)

        # Use gaia space motion to correct for any drifts in time
        dist = Distance(parallax=np.asarray(sources['Plx']) * u.mas,
                        allow_negative=True)
        coords = SkyCoord(
            ra=np.asarray(sources['RA_ICRS']) * u.deg,
            dec=np.asarray(sources['DE_ICRS']) * u.deg,
            pm_ra_cosdec=np.nan_to_num(sources['pmRA']) * u.mas / u.year,
            pm_dec=np.nan_to_num(sources['pmDE']) * u.mas / u.year,
            distance=dist,
            obstime='J2015.05',
            radial_velocity=np.nan_to_num(sources['RV']) * u.km / u.s)
        cs = coords.apply_space_motion(self.time[0])
        locs = self.wcs[0].wcs_world2pix(
            np.atleast_2d((cs.ra.deg, cs.dec.deg)).T, 0)

        # Trim out any sources that are outside the image
        lmask = (locs[:, 0] >= -1) & (locs[:, 0] <= self.shape[1] + 1) & (
            locs[:, 1] >= -1) & (locs[:, 1] <= self.shape[2] + 1)
        sources, cs, locs = sources[lmask].reset_index(
            drop=True), cs[lmask], locs[lmask]
        return sources, cs, locs
Example #11
0
def correct_gaia_to_epoch(gaia_cat: Union[str, table.QTable],
                          new_epoch: time.Time):
    gaia_cat = load_catalogue(cat_name="gaia", cat=gaia_cat)
    epochs = list(map(lambda y: f"J{y}", gaia_cat['ref_epoch']))
    gaia_coords = SkyCoord(ra=gaia_cat["ra"],
                           dec=gaia_cat["dec"],
                           pm_ra_cosdec=gaia_cat["pmra"],
                           pm_dec=gaia_cat["pmdec"],
                           obstime=epochs)
    u.debug_print(2, "astrometry.correct_gaia_to_epoch(): new_epoch ==",
                  new_epoch)
    gaia_coords_corrected = gaia_coords.apply_space_motion(
        new_obstime=new_epoch)
    gaia_cat_corrected = copy.deepcopy(gaia_cat)
    gaia_cat_corrected["ra"] = gaia_coords_corrected.ra
    gaia_cat_corrected["dec"] = gaia_coords_corrected.dec
    new_epoch.format = "jyear"
    gaia_cat_corrected["ref_epoch"] = new_epoch.value
    return gaia_cat_corrected
Example #12
0
def find_in_gaia(ra, dec):
    # First do a large search using 30 arcsec
    coord = SkyCoord(ra=ra, dec=dec, unit=(u.degree, u.degree), frame='icrs')
    radius = u.Quantity(30.0, u.arcsec)
    q = Gaia.cone_search_async(coord, radius)
    gaia = q.get_results()
    gaia = gaia[np.nan_to_num(gaia['parallax']) > 0]
    warning = (len(gaia) == 0)

    # Then propagate the Gaia coordinates to 2000, and find the best match to the
    # input coordinates
    if not warning:
        ra2015 = np.array(gaia['ra']) * u.deg
        dec2015 = np.array(gaia['dec']) * u.deg
        parallax = np.array(gaia['parallax']) * u.mas
        pmra = np.array(gaia['pmra']) * u.mas / u.yr
        pmdec = np.array(gaia['pmdec']) * u.mas / u.yr
        c2015 = SkyCoord(ra=ra2015,
                         dec=dec2015,
                         distance=Distance(parallax=parallax,
                                           allow_negative=True),
                         pm_ra_cosdec=pmra,
                         pm_dec=pmdec,
                         obstime=Time(2015.5, format='decimalyear'))
        c2000 = c2015.apply_space_motion(dt=-15.5 * u.year)

        idx, sep, _ = coord.match_to_catalog_sky(c2000)

        # The best match object
        best = gaia[idx]
        gaia_id = best['source_id']

        MG = 5 + 5 * np.log10(
            best['parallax'] / 1000) + best['phot_g_mean_mag']
        bprp = best['bp_rp']

        gaia_id = np.int(gaia_id)
        G = np.float(best['phot_g_mean_mag'])
        MG = np.float(MG)
        bprp = np.float(bprp)

        return gaia_id, G, MG, bprp
Example #13
0
 def get_radec_position_after_pm(self, date_obs):
     target_pmra = self.simbad[0]['PMRA'] * u.mas / u.yr
     if np.isnan(target_pmra):
         target_pmra = 0 * u.mas / u.yr
     target_pmdec = self.simbad[0]['PMDEC'] * u.mas / u.yr
     if np.isnan(target_pmdec):
         target_pmdec = 0 * u.mas / u.yr
     target_parallax = self.simbad[0]['PLX_VALUE'] * u.mas
     if target_parallax == 0 * u.mas:
         target_parallax = 1e-4 * u.mas
     target_coord = SkyCoord(ra=self.radec_position.ra,
                             dec=self.radec_position.dec,
                             distance=Distance(parallax=target_parallax),
                             pm_ra_cosdec=target_pmra,
                             pm_dec=target_pmdec,
                             frame='icrs',
                             equinox="J2000",
                             obstime="J2000")
     self.radec_position_after_pm = target_coord.apply_space_motion(
         new_obstime=Time(date_obs))
     return self.radec_position_after_pm
Example #14
0
def best_gaia_entry(ra: float, dec: float, mjd: float, entries: Table):
    """
    Using the originally supplied ra and dec choose the closest
    entries (propagating all entries in time to match current ra and dec
    at time = 'mjd')

    :param ra: float, the right ascension in degrees
    :param dec: float, the declination in degrees
    :param mjd: float, the modified julien date for observation
    :param entries:
    :return:
    """
    # get the original coords in SkyCoord
    ocoord = SkyCoord(ra, dec, unit='deg')
    # get gaia time and observation time
    gaia_time = Time('2015.5', format='decimalyear')
    obs_time = Time(mjd, format='mjd')
    # get entries as numpy arrays (with units)
    ra_arr = np.array(entries['ra']) * uu.deg
    dec_arr = np.array(entries['dec']) * uu.deg
    pmra_arr = np.array(entries['pmra']) * uu.mas / uu.yr
    pmde_arr = np.array(entries['pmde']) * uu.mas / uu.yr
    plx_arr = np.array(entries['plx']) * uu.mas
    # propagate all entries ra and dec to mjd
    coords0 = SkyCoord(ra_arr,
                       dec_arr,
                       pm_ra_cosdec=pmra_arr,
                       pm_dec=pmde_arr,
                       distance=Distance(parallax=plx_arr),
                       obstime=gaia_time)
    # apply space motion
    coords1 = coords0.apply_space_motion(obs_time)
    # crossmatch with ra and dec and keep closest
    separation = coords1.separation(ocoord)
    # find the position of the minimum separated value
    position = np.argmin(separation.value)
    # return the position
    return position
Example #15
0
    def wrapper(*args, _skip_decorator=False, **kwargs):
        """Wrapper docstring.

        Other Parameters
        ----------------
        _skip_decorator : bool, optional
            Whether to skip the decorator.
            default {_skip_decorator}

        Notes
        -----
        The wrapper

        """
        # whether to skip decorator or keep going
        if _skip_decorator:
            return function(*args, **kwargs)
        # else:

        ba = sig.bind_partial_with_defaults(*args, **kwargs)

        obstime = ba.arguments.get("obstime", None)
        # print("obstime: ", obstime)

        # Catalog munging
        for name, info in catalogs.items():
            catalog = ba.arguments[name]

            # get obstime if not provided or determined
            if obstime is None and hasattr(catalog, "obstime"):
                obstime = getattr(catalog, "obstime")
                # print(obstime)  # TODO report this somehow

            # Step 1, convert to correct output type
            # TODO use registry for this
            cat_dtype = info["dtype"]  # output type

            # TODO support passing multiple types
            if cat_dtype is BCFrame:

                if isinstance(  # note, works if subclass
                        catalog, (BCFrame, SkyCoord)):
                    pass
                elif isinstance(catalog, Table):
                    catalog = xdg.get_transform(Table, SkyCoord)(catalog)

                # elif:

                else:
                    raise TypeError((f"Unknown conversion {type(catalog)} "
                                     "-> BaseCoordinateFrame."))

            elif isinstance(cat_dtype, SkyCoord):
                if obstime is None and hasattr(catalog, "obstime"):
                    obstime = getattr(catalog, "obstime")

                if isinstance(catalog, SkyCoord):  # note, works if subclass
                    pass
                elif isinstance(catalog, BCFrame):
                    catalog = xdg.get_transform(BCFrame, SkyCoord)(catalog)

                elif isinstance(catalog, Table):
                    catalog = xdg.get_transform(Table, SkyCoord)(catalog)

            # elif isinstance(cat_dtype, BCFrame):  # a specific Frame class

            else:
                raise Exception("TODO")

            # Step 2, evolve observations by `obstime`
            try:
                new_catalog = SkyCoord.apply_space_motion(catalog, obstime)
            except Exception as e:  # can't evolve
                # need to check that this is not a problem
                if hasattr(catalog, "obstime"):
                    if catalog.obstime != obstime:  # Uh oh!, it is
                        raise Exception((f"epoch mismatch: catalog {name}'s "
                                         "coordinates cannot be transformed "
                                         f"to match the epoch {obstime} "
                                         f"(Exception {e})."))
                    else:
                        pass  # the epochs match

                # TODO, do as warning instead
                # print(f"Not adjusting catalog {name} ({e})")
                new_catalog = catalog

            # reassign
            ba.arguments[name] = new_catalog

        # /def

        # ----------------------------------

        return_ = function(*ba.args, **ba.kwargs)

        # replace_catalogs = {}  # TODO implement when have "return" working
        # for name, info in catalogs.items():
        #     if info.get("return", False):  # get munged data
        #         replace_catalogs[name] = ba.arguments[name]

        # if replace_catalogs:  # it's not empty
        #     return return_, replace_catalogs

        return return_
Example #16
0
def find_object_in_catalog(image, db_address, gaia_class, simbad_class):
    """
    Find the object in external catalogs. Update the ra and dec if found. Also add an initial classification if found.
    :return:
    """

    # Assume that the equinox and input epoch are both j2000.
    # Gaia uses an equinox of 2000, but epoch of 2015.5 for the proper motion
    coordinate = SkyCoord(ra=image.ra,
                          dec=image.dec,
                          unit=(units.deg, units.deg),
                          frame='icrs',
                          pm_ra_cosdec=image.pm_ra * units.mas / units.year,
                          pm_dec=image.pm_dec * units.mas / units.year,
                          equinox='j2000',
                          obstime=Time(2000.0, format='decimalyear'))
    transformed_coordinate = coordinate.apply_space_motion(
        new_obstime=Time(2015.5, format='decimalyear'))

    with warnings.catch_warnings():
        warnings.simplefilter("ignore")
        # 10 arcseconds should be a large enough radius to capture bright objects.
        gaia = import_utils.import_attribute(gaia_class)
        gaia_connection = gaia()
        gaia_connection.ROW_LIMIT = 200
        results = gaia_connection.query_object(
            coordinate=transformed_coordinate, radius=10.0 * units.arcsec)

    # Filter out objects fainter than r=12 and brighter than r = 5.
    # There is at least one case (gamma cas) that is in gaia but does not have a complete catalog record like proper
    # motions and effective temperatures.
    results = results[np.logical_and(results['phot_rp_mean_mag'] < 12.0,
                                     results['phot_rp_mean_mag'] > 5.0)]
    if len(results) > 0:
        # convert the luminosity from the LSun units that Gaia provides to cgs units
        results[0]['lum_val'] *= constants.L_sun.to('erg / s').value
        image.classification = dbs.get_closest_HR_phoenix_models(
            db_address, results[0]['teff_val'], results[0]['lum_val'])
        # Update the ra and dec to the catalog coordinates as those are basically always better than a user enters
        # manually.
        image.ra, image.dec = results[0]['ra'], results[0]['dec']
        if results[0]['pmra'] is not np.ma.masked:
            image.pm_ra, image.pm_dec = results[0]['pmra'], results[0]['pmdec']
    # If nothing in Gaia fall back to simbad. This should only be for stars that are brighter than mag = 3
    else:
        # IMPORTANT NOTE:
        # During e2e tests we do not import astroquery.simbad.Simbad. We import a mocked simbad call
        # which can be found in banzai_nres.tests.utils.MockSimbad . This returns a simbad table that is
        # truncated. If you add a new votable field, you will need to add it to the mocked table as well.
        simbad = import_utils.import_attribute(simbad_class)
        simbad_connection = simbad()
        simbad_connection.add_votable_fields('pmra', 'pmdec', 'fe_h', 'otype')
        try:
            results = simbad_connection.query_region(coordinate,
                                                     radius='0d0m10s')
        except astroquery.exceptions.TableParseError:
            response = simbad_connection.last_response.content
            logger.error(
                f'Error querying SIMBAD. Response from SIMBAD: {response}',
                image=image)
            results = []
        if results:
            results = remove_planets_from_simbad(results)
            results = results[0]  # get the closest source.
            image.classification = dbs.get_closest_phoenix_models(
                db_address, results['Fe_H_Teff'], results['Fe_H_log_g'])[0]
            # note that we always assume the proper motions are in mas/yr... which they should be.
            if results['PMRA'] is not np.ma.masked:
                image.pm_ra, image.pm_dec = results['PMRA'], results['PMDEC']
            # Update the ra and dec to the catalog coordinates as those will be consistent across observations.
            # Simbad always returns h:m:s, d:m:s, for ra, dec. If for some reason simbad does not, these coords will be
            # very wrong and barycenter correction will be very wrong.
            coord = SkyCoord(results['RA'],
                             results['DEC'],
                             unit=(units.hourangle, units.deg))
            image.ra, image.dec = coord.ra.deg, coord.dec.deg
Example #17
0
def match_kc19_to_2rxs(max_sep=12 * u.arcsec):
    """
    2RXS has RA/dec of ROSAT sources in J2000.

    Cut on the ROSAT detection likelihood to be >=9. Following the
    Boller et al (2016) abstract, this is "conservative".
    Cut on the "Sflag' screening flag (0=good, not equal to 0 = screening flag
    set).

    https://ui.adsabs.harvard.edu/abs/2016A%26A...588A.103B/abstract
    """

    csvpath = '../data/kc19_to_2rxs_within_{}_arcsec.csv'.format(max_sep.value)

    if not os.path.exists(csvpath):

        #
        # Get 2RXS sources. From Boller+2016, they were taken with the PSPC between
        # June 1990 and Aug 1991.
        #
        with fits.open("../data/Boller_2016_2RXS_ROSAT_sources.fits") as hdul:
            t = Table(hdul[1].data)

        sel = (t['ExiML'] >= 9) & (t['Sflag'] == 0)

        t = t[sel]

        rosat_coord = SkyCoord(nparr(t['_RAJ2000']),
                               nparr(t['_DEJ2000']),
                               frame='icrs',
                               unit=(u.deg, u.deg),
                               equinox=Time(2000.0, format='jyear'),
                               obstime=Time(1991.0, format='jyear'))

        #
        # Get Kounkel & Covey 2019 sources with gaia info crossmatched. Set an
        # annoyingly high 1" absolute tolerance on the ra/dec match between what I
        # got from gaiadr2.source table, and what was listed in KC19 table 1.  Use
        # the gaiadr2.source ra/dec (in this case, "_x", not "_y"). rtol is set as
        # what it needed to be to pass. Set the correct equinox and estimate of
        # observing time by the Gaia satellite, and include the proper motions.
        #
        df = pd.read_csv('../data/kounkel_table1_sourceinfo.csv')

        for k_x, k_y in zip(['ra_x', 'dec_x'], ['ra_y', 'dec_y']):
            np.testing.assert_allclose(nparr(df[k_x]),
                                       nparr(df[k_y]),
                                       rtol=3e-2,
                                       atol=1 / 3600)

        kc19_coord = SkyCoord(nparr(df.ra_x),
                              nparr(df.dec_x),
                              frame='icrs',
                              unit=(u.deg, u.deg),
                              pm_ra_cosdec=nparr(df.pmra) * u.mas / u.yr,
                              pm_dec=nparr(df.pmdec) * u.mas / u.yr,
                              equinox=Time(2015.5, format='jyear'),
                              obstime=Time(2015.5, format='jyear'))

        kc19_coord_rosat_epoch = kc19_coord.apply_space_motion(
            new_obstime=Time(1991.0, format='jyear'))

        #
        # For each ROSAT coordinate, get the nearest KC2019 match. Ensure they are
        # each in the same equinox system (J2000.0).  Examine the separation
        # distribution of the match.
        #

        idx, d2d, _ = rosat_coord.match_to_catalog_sky(
            kc19_coord_rosat_epoch.transform_to(rosat_coord))

        sel = (d2d < 1000 * u.arcsec)  # ~1/3rd of a degree

        bins = np.logspace(-2, 3, 11)

        plt.close('all')
        f, ax = plt.subplots(figsize=(4, 3))
        ax.hist(d2d[sel].to(u.arcsec).value,
                bins=bins,
                cumulative=False,
                color='black',
                fill=False,
                linewidth=0.5)

        format_ax(ax)

        ax.set_xlabel('2RXS to nearest KC19 source [arcsec]')
        ax.set_ylabel('Number per bin')
        ax.set_xscale('log')
        ax.set_yscale('log')

        f.tight_layout(pad=0.2)
        outpath = '../results/crossmatching/kc19_to_2rxs_separation.png'
        savefig(f, outpath)

        #
        # The above shows a hint of a bump at like ~10 arcsec. This is roughly the
        # level of positional uncertainty expected from ROSAT (Ayres+2004,
        # https://ui.adsabs.harvard.edu/abs/2004ApJ...608..957A/abstract).  6" was
        # the design specification accuracy. Seems like 12" (2x the expected
        # positional uncertainty) is a decent place to set the cut. It will give
        # ~1.5k matches.
        #

        sep_constraint = (d2d < max_sep)

        rosat_matches = t[sep_constraint]
        kc19_match_df = df.iloc[idx[sep_constraint]]

        kc19_match_df['has_2RXS_match'] = 1

        kc19_match_df['2RXS_match_dist_arcsec'] = (d2d[sep_constraint].to(
            u.arcsec).value)

        kc19_match_df['2RXS_name'] = rosat_matches['_2RXS']

        kc19_match_df['2RXS_ExiML'] = rosat_matches['ExiML']

        kc19_match_df.to_csv(csvpath, index=False)
        print('made {}'.format(csvpath))

    else:
        kc19_match_df = pd.read_csv(csvpath)
        df = pd.read_csv('../data/kounkel_table1_sourceinfo.csv')

    return kc19_match_df, df
Example #18
0
def run_astrometry(image2d,
                   mask2d,
                   saturpix,
                   header,
                   no_reuse_gaia,
                   maxfieldview_arcmin,
                   fieldfactor,
                   pvalues,
                   nightdir,
                   output_fname,
                   setupdata,
                   interactive,
                   logfile,
                   debug=False):
    """
    Compute astrometric solution of image.

    Note that the input parameter header is modified in output.

    Parameters
    ----------
    image2d : numpy 2D array
        Image to be calibrated.
    mask2d : numpy 2D array
        Useful region mask.
    saturpix : numpy 2D array
        Array storing the location of saturated pixels in the raw data.
    header: astropy header
        Initial header of the image prior to the astrometric
        calibration.
    no_reuse_gaia : bool
        If True, previous GAIA data is not reused to perform the
        initial astrometric calibration with Astrometry.net.
    maxfieldview_arcmin : float
        Maximum field of view. This is necessary to retrieve the GAIA
        data for the astrometric calibration.
    fieldfactor : float
        Multiplicative factor to enlarge the required field of view
        in order to facilitate the reuse of the downloaded GAIA data.
    pvalues : list of int
        Possible P values for build-astrometry-index (scale number)
        in the order to be employed (if one fails, the next one is used).
        See help of build-astrometry-index for details.
    nightdir : str or None
        Directory where the reduced images will be stored.
    output_fname : str or None
        Output file name.
    setupdata : dict
        Setup data stored as a Python dictionary.
    interactive : bool or None
        If True, enable interactive execution (e.g. plots,...).
    logfile : instance of ToLogFile
        Logfile to store reduction information.
    debug : bool or None
        Display additional debugging information.

    Returns
    -------
    ierr_astr : int
        Error status value. 0: no error. 1: error while performing
        astrometric calibration.
    astrsumm1 : instance of AstrSumm
        Summary of the astrometric calibration with Astronomy.net.
    astrsumm2 : instance of AstrSumm
        Summary of the astrometric calibration with AstrOmatic.net.
    """

    ierr_astr = 0
    astrsumm1 = None
    astrsumm2 = None

    # creating work subdirectory
    workdir = nightdir + '/work'

    if not os.path.isdir(workdir):
        os.makedirs(workdir)
    else:
        filelist = glob.glob('{}/*'.format(workdir))
        logfile.print('\nRemoving previous files: {}'.format(filelist))
        for filepath in filelist:
            try:
                os.remove(filepath)
            except:
                logfile.print("Error while deleting file : ", filepath)

    # define ToLogFile object
    logfile.print('\nAstrometric calibration of {}'.format(output_fname))

    # define CmdExecute object
    cmd = CmdExecute(logfile)

    # generate myastrometry.cfg
    cfgfile = '{}/myastrometry.cfg'.format(workdir)
    with open(cfgfile, 'wt') as f:
        f.write('add_path .\nindex index-image')
        logfile.print('Creating configuration file {}'.format(cfgfile))

    # remove deprecated WCS keywords:
    for kwd in ['pc001001', 'pc001002', 'pc002001', 'pc002002']:
        if kwd in header:
            del header[kwd]
    # rename deprecated RADECSYS as RADESYS
    if 'RADECSYS' in header:
        header.rename_keyword('RADECSYS', 'RADESYS')

    # RA, DEC, and DATE-OBS from the image header
    ra_initial = header['ra']
    dec_initial = header['dec']
    dateobs = header['date-obs']
    c_fk5_dateobs = SkyCoord(ra=ra_initial * u.degree,
                             dec=dec_initial * u.degree,
                             frame='fk5',
                             equinox=Time(dateobs))
    c_fk5_j2000 = c_fk5_dateobs.transform_to(FK5(equinox='J2000'))
    logfile.print('Central coordinates:')
    logfile.print(str(c_fk5_dateobs))
    logfile.print(str(c_fk5_j2000))
    ra_center = c_fk5_j2000.ra.deg * np.pi / 180
    dec_center = c_fk5_j2000.dec.deg * np.pi / 180
    xj2000 = np.cos(ra_center) * np.cos(dec_center)
    yj2000 = np.sin(ra_center) * np.cos(dec_center)
    zj2000 = np.sin(dec_center)

    # read JSON file with central coordinates of fields already calibrated
    jsonfname = '{}/central_pointings.json'.format(nightdir)
    if os.path.exists(jsonfname):
        with open(jsonfname) as jfile:
            ccbase = json.load(jfile)
    else:
        ccbase = dict()

    # decide whether new GAIA data is needed
    retrieve_new_gaia_data = True
    indexid = None
    if no_reuse_gaia:
        logfile.print(
            '-> Forcing downloading of GAIA catalogue close the field pointing'
        )
    else:
        nindices = len(ccbase)
        if nindices > 0:
            dist_arcmin_min = None
            for i, ikey in enumerate(ccbase):
                x = ccbase[ikey]['x']
                y = ccbase[ikey]['y']
                z = ccbase[ikey]['z']
                search_radius_arcmin = ccbase[ikey]['search_radius_arcmin']
                # angular distance (radians)
                dotprodcut = x * xj2000 + y * yj2000 + z * zj2000
                if abs(
                        dotprodcut
                ) > 1:  # avoid RuntimeWarning when dotproduct = 1.0000000000000002
                    dist_rad = 0.0
                else:
                    dist_rad = np.arccos(x * xj2000 + y * yj2000 + z * zj2000)
                # angular distance (arcmin)
                dist_arcmin = dist_rad * 180 / np.pi * 60
                if (maxfieldview_arcmin /
                        2) + dist_arcmin < search_radius_arcmin:
                    if dist_arcmin_min is None:
                        dist_arcmin_min = dist_arcmin
                        indexid = int(ikey[-6:])
                    else:
                        if dist_arcmin < dist_arcmin_min:
                            dist_arcmin_min = dist_arcmin
                            indexid = int(ikey[-6:])
        if indexid is not None:
            logfile.print(
                '-> Reusing previously downloaded GAIA catalogue (indexid={})'.
                format(indexid))
            retrieve_new_gaia_data = False
        else:
            logfile.print(
                '-> No previous GAIA catalogue found close the field pointing')

    if retrieve_new_gaia_data:
        indexid = len(ccbase) + 1

    # create index subdir
    subdir = 'index{:06d}'.format(indexid)
    # create path to subdir
    newsubdir = nightdir + '/' + subdir

    if retrieve_new_gaia_data:
        # check that directory for the new index does not exist
        if not os.path.isdir(newsubdir):
            logfile.print(
                'Subdirectory {} not found. Creating it!'.format(newsubdir))
            os.makedirs(newsubdir)
        else:
            msg = 'ERROR: subdirectory {} already exists'.format(newsubdir)
            raise SystemError(msg)
        # generate additional logfile for retrieval of GAIA data
        loggaianame = '{}/gaialog.log'.format(newsubdir)
        loggaia = open(loggaianame, 'wt')
        logfile.print('-> Creating {}'.format(loggaianame))
        loggaia.write('Querying GAIA data...\n')
        # generate query for GAIA
        search_radius_arcmin = fieldfactor * (maxfieldview_arcmin / 2)
        search_radius_degree = search_radius_arcmin / 60
        # loop in phot_g_mean_mag
        # ---
        mag_minimum = 0
        gaia_query_line, tap_result = retrieve_gaia(c_fk5_j2000.ra.deg,
                                                    c_fk5_j2000.dec.deg,
                                                    search_radius_degree,
                                                    mag_minimum, loggaia)
        if tap_result is None:
            nobjects_mag_minimum = 0
        else:
            nobjects_mag_minimum = len(tap_result)
        logfile.print('-> Gaia data: magnitude, nobjects: {:.3f}, {}'.format(
            mag_minimum, nobjects_mag_minimum))
        if nobjects_mag_minimum >= NMAXGAIA:
            raise SystemError('Unexpected')
        # ---
        mag_maximum = 30
        gaia_query_line, tap_result = retrieve_gaia(c_fk5_j2000.ra.deg,
                                                    c_fk5_j2000.dec.deg,
                                                    search_radius_degree,
                                                    mag_maximum, loggaia)
        if tap_result is None:
            nobjects_mag_maximum = 0
        else:
            nobjects_mag_maximum = len(tap_result)
        logfile.print('-> Gaia data: magnitude, nobjects: {:.3f}, {}'.format(
            mag_maximum, nobjects_mag_maximum))
        if nobjects_mag_maximum < NMAXGAIA:
            loop_in_gaia = False
        else:
            loop_in_gaia = True
        # ---
        niter = 0
        nitermax = 50
        while loop_in_gaia:
            niter += 1
            loggaia.write('Iteration {}\n'.format(niter))
            mag_medium = (mag_minimum + mag_maximum) / 2
            gaia_query_line, tap_result = retrieve_gaia(
                c_fk5_j2000.ra.deg, c_fk5_j2000.dec.deg, search_radius_degree,
                mag_medium, loggaia)
            if tap_result is None:
                msg = 'WARNING: unable to retrieve GAIA data (tap_result is None)'
                logfile.print(msg)
            else:
                nobjects = len(tap_result)
                logfile.print(
                    '-> Gaia data: magnitude, nobjects: {:.3f}, {}'.format(
                        mag_medium, nobjects))
                if nobjects < NMAXGAIA:
                    if mag_maximum - mag_minimum < 0.1:
                        loop_in_gaia = False
                    else:
                        mag_minimum = mag_medium
                else:
                    mag_maximum = mag_medium
            if niter > nitermax:
                loggaia.write(
                    'ERROR: nitermax reached while retrieving GAIA data')
                loop_in_gaia = False

        if tap_result is None:
            raise SystemError(
                'FATAL ERROR: unable to retrieve GAIA data (tap_result is None; check http connection)'
            )

        loggaia.write(str(tap_result.to_table()) + '\n')
        loggaia.close()

        logfile.print('Querying GAIA data: {} objects found'.format(
            len(tap_result)))

        # proper motion correction
        logfile.print('-> Applying proper motion correction...')
        source_id = []
        ra_corrected = []
        dec_corrected = []
        phot_g_mean_mag = []
        for irecord, record in enumerate(tap_result):
            source_id.append(record['source_id'])
            phot_g_mean_mag.append(record['phot_g_mean_mag'])
            ra, dec = record['ra'], record['dec']
            pmra, pmdec = record['pmra'], record['pmdec']
            ref_epoch = record['ref_epoch']
            if not np.isnan(pmra) and not np.isnan(pmdec):
                t0 = Time(ref_epoch, format='decimalyear')
                c = SkyCoord(ra=ra * u.degree,
                             dec=dec * u.degree,
                             pm_ra_cosdec=pmra * u.mas / u.yr,
                             pm_dec=pmdec * u.mas / u.yr,
                             obstime=t0)
                dt = Time(dateobs) - t0
                c_corrected = c.apply_space_motion(dt=dt.jd * u.day)
                if debug:
                    print(irecord, ra, c_corrected.ra.value, dec,
                          c_corrected.dec.value)
                ra_corrected.append(c_corrected.ra.value)
                dec_corrected.append(c_corrected.dec.value)
            else:
                ra_corrected.append(ra)
                dec_corrected.append(dec)

        # save GAIA objects in FITS binary table
        hdr = fits.Header()
        hdr.add_history('GAIA objets selected with following query:')
        hdr.add_history(gaia_query_line)
        hdr.add_history('---')
        hdr.add_history(
            'Note that RA and DEC have been corrected from proper motion')
        primary_hdu = fits.PrimaryHDU(header=hdr)
        col1 = fits.Column(name='source_id', format='K', array=source_id)
        col2 = fits.Column(name='ra', format='D', array=ra_corrected)
        col3 = fits.Column(name='dec', format='D', array=dec_corrected)
        col4 = fits.Column(name='phot_g_mean_mag',
                           format='E',
                           array=phot_g_mean_mag)
        hdu = fits.BinTableHDU.from_columns([col1, col2, col3, col4])
        hdul = fits.HDUList([primary_hdu, hdu])
        outfname = nightdir + '/' + subdir + '/GaiaDR2-query.fits'
        hdul.writeto(outfname, overwrite=True)
        logfile.print('-> Saving {}'.format(outfname))

        # update JSON file with central coordinates of fields already calibrated
        ccbase[subdir] = {
            'ra': c_fk5_j2000.ra.degree,
            'dec': c_fk5_j2000.dec.degree,
            'x': xj2000,
            'y': yj2000,
            'z': zj2000,
            'search_radius_arcmin': search_radius_arcmin
        }
        with open(jsonfname, 'w') as outfile:
            json.dump(ccbase, outfile, indent=2)

    else:
        # check that directory with the old index does exist
        if os.path.isdir(newsubdir):
            logfile.print('Subdirectory {} found'.format(newsubdir))
        else:
            msg = 'ERROR: subdirectory {} does not exist!'
            raise SystemError(msg)

    command = 'cp {}/{}/GaiaDR2-query.fits {}/work/'.format(
        nightdir, subdir, nightdir)
    cmd.run(command)

    # image dimensions
    naxis2, naxis1 = image2d.shape

    # save temporary FITS file
    tmpfname = '{}/xxx.fits'.format(workdir)
    header.add_history('--Computing Astrometry.net WCS solution--')
    hdu = fits.PrimaryHDU(image2d.astype(np.float32), header)
    hdu.writeto(tmpfname, overwrite=True)
    logfile.print('\nGenerating reduced image {}/xxx.fits (after bias '
                  'subtraction and flatfielding)\n'.format(workdir))

    logfile.print('\n*** Using Astrometry.net tools ***')
    ip = 0
    loop = True
    while loop:
        # generate index file with GAIA data
        command = 'build-astrometry-index -i GaiaDR2-query.fits'
        command += ' -o index-image.fits'
        command += ' -A ra -D dec -S phot_g_mean_mag'
        command += ' -P {}'.format(pvalues[ip])
        command += ' -E -I {}'.format(indexid)
        cmd.run(command, cwd=workdir)

        # solve fieldmormo
        command = 'solve-field -p'
        command += ' --config myastrometry.cfg --overwrite'
        command += ' --ra ' + str(c_fk5_j2000.ra.degree)
        command += ' --dec ' + str(c_fk5_j2000.dec.degree)
        command += ' --radius {}'.format(maxfieldview_arcmin / 120)
        command += ' xxx.fits'
        cmd.run(command, cwd=workdir)

        # check that the field solved
        if not os.path.isfile('{}/xxx.solved'.format(workdir)):
            logfile.print('WARNING: field did not solve.')
            if ip < len(pvalues) - 1:
                logfile.print('WARNING: trying with new P value.')
                ip += 1
            else:
                ierr_astr = 1
                msg = 'Unable to solve the field with Astrometry.net'
                logfile.print(msg)
                header.add_history(msg)
                hdu = fits.PrimaryHDU(image2d.astype(np.float32), header)
                hdu.writeto(output_fname, overwrite=True)
                logfile.print('-> file {} created'.format(output_fname))
                save_auxfiles(output_fname=output_fname,
                              nightdir=nightdir,
                              workdir=workdir,
                              logfile=logfile)
                return ierr_astr, astrsumm1, astrsumm2
        else:
            loop = False

    # check for saturated objects
    with fits.open('{}/xxx.axy'.format(workdir), 'update') as hdul_table:
        tbl = hdul_table[1].data
        isaturated = []
        for i in range(tbl.shape[0]):
            ix = int(tbl['X'][i] + 0.5)
            iy = int(tbl['Y'][i] + 0.5)
            if saturpix[iy - 1, ix - 1]:
                isaturated.append(i)
        logfile.print('Checking file: {}/xxx.axy'.format(workdir))
        logfile.print('Number of saturated objects found: {}/{}'.format(
            len(isaturated), tbl.shape[0]))
        if len(isaturated) > 0:
            for i in isaturated:
                logfile.print('Saturated object: {}'.format(tbl[i]))
        if len(isaturated) > 0:
            hdul_table[1].data = np.delete(tbl, isaturated)
            logfile.print('File: {}/xxx.axy updated\n'.format(workdir))

    if len(isaturated) > 0:
        # rerun code
        command = 'solve-field -p'
        command += ' --config myastrometry.cfg --continue'
        command += ' --width {} --height {}'.format(naxis1, naxis2)
        command += ' --x-column X --y-column Y --sort-column FLUX'
        command += ' --ra ' + str(c_fk5_j2000.ra.degree)
        command += ' --dec ' + str(c_fk5_j2000.dec.degree)
        command += ' --radius {}'.format(maxfieldview_arcmin / 120)
        command += ' xxx.axy'
        cmd.run(command, cwd=workdir)

        # check that the field solved
        if not os.path.isfile('{}/xxx.solved'.format(workdir)):
            ierr_astr = 1
            msg = 'Unable to solve the field with Astrometry.net'
            logfile.print(msg)
            header.add_history(msg)
            hdu = fits.PrimaryHDU(image2d.astype(np.float32), header)
            hdu.writeto(output_fname, overwrite=True)
            logfile.print('-> file {} created'.format(output_fname))
            save_auxfiles(output_fname=output_fname,
                          nightdir=nightdir,
                          workdir=workdir,
                          logfile=logfile)
            return ierr_astr, astrsumm1, astrsumm2

        # insert new WCS into image header
        command = 'new-wcs -i xxx.fits -w xxx.wcs -o xxx.new -d'
        cmd.run(command, cwd=workdir)

    # read GaiaDR2 table and convert RA, DEC to X, Y
    # (note: the same result can be accomplished using the command-line program:
    # $ wcs-rd2xy -w xxx.wcs -i GaiaDR2-query.fits -o gaia-xy.fits)
    with fits.open('{}/GaiaDR2-query.fits'.format(workdir)) as hdul_table:
        gaiadr2 = hdul_table[1].data
    with fits.open('{}/xxx.new'.format(workdir)) as hdul:
        w = WCS(hdul[0].header)
    try:
        xgaia, ygaia = w.all_world2pix(gaiadr2.ra, gaiadr2.dec, 1)
    except NoConvergence:
        msg = 'WARNING: NoConvergence exception in WCS.all_world2pix() call (using quiet=True instead)'
        print(msg)
        xgaia, ygaia = w.all_world2pix(gaiadr2.ra, gaiadr2.dec, 1, quiet=True)
    # compute pixel scale (mean in both axis) in arcsec/pix
    pixel_scales_arcsec_pix = proj_plane_pixel_scales(w) * 3600
    logfile.print('astrometry.net> pixel scales (arcsec/pix): {}'.format(
        pixel_scales_arcsec_pix))

    # load corr file
    corrfname = '{}/xxx.corr'.format(workdir)
    with fits.open(corrfname) as hdul_table:
        tcorr = hdul_table[1].data

    # generate plots
    astrsumm1 = plot_astrometry(
        output_fname=output_fname,
        image2d=image2d,
        mask2d=mask2d,
        peak_x=tcorr.field_x,
        peak_y=tcorr.field_y,
        pred_x=tcorr.index_x,
        pred_y=tcorr.index_y,
        xcatag=xgaia,
        ycatag=ygaia,
        pixel_scales_arcsec_pix=pixel_scales_arcsec_pix,
        workdir=workdir,
        interactive=interactive,
        logfile=logfile,
        suffix='net')

    # open result and update header
    result_fname = '{}/xxx.new'.format(workdir)
    with fits.open(result_fname) as hdul:
        newheader = hdul[0].header

    # copy configuration files for astrometric.net
    logfile.print('\n*** Using AstrOmatic.net tools ***')
    conffiles = ['default.param', 'config.sex', 'config.scamp']
    for fname in conffiles:
        keyfname = fname.replace('.', '_')
        if keyfname in setupdata:
            initfname = setupdata[keyfname]
            if initfname[0] != '/':
                initfname = os.getcwd() + '/' + initfname
            if os.path.exists(initfname):
                command = 'cp {} {}/'.format(initfname, workdir)
                cmd.run(command)
            else:
                raise SystemError(
                    'The file {} given in setup_filabres.yaml does not exist!'.
                    format(initfname))
        else:
            dumdata = pkgutil.get_data('filabres.astromatic', fname)
            txtfname = '{}/{}'.format(workdir, fname)
            logfile.print('Generating {}'.format(txtfname))
            with open(txtfname, 'wt') as f:
                f.write(str(dumdata.decode('utf8')))
    logfile.print(' ')

    # run sextractor
    command = 'sex xxx.new -c config.sex -CATALOG_NAME xxx.ldac'
    cmd.run(command, cwd=workdir)

    # run scamp
    command = 'scamp xxx.ldac -c config.scamp'
    cmd.run(command, cwd=workdir)

    # check there is a useful result
    if os.path.exists('{}/xxx.head'.format(workdir)):
        with open('{}/xxx.head'.format(workdir)) as fdum:
            singleline = fdum.read()
        if 'PV2_10' not in singleline:
            ierr_astr = 2
    else:
        ierr_astr = 2
    if ierr_astr == 2:
        msg = 'Unable to solve the field with AstrOmatic.net'
        logfile.print(msg)
        newheader.add_history(msg)
        newheader[
            'history'] = '-------------------------------------------------------'
        newheader[
            'history'] = 'Summary of astrometric calibration with Astrometry.net:'
        newheader['history'] = '- pixscale: {}'.format(astrsumm1.pixscale)
        newheader['history'] = '- ntargets: {}'.format(astrsumm1.ntargets)
        newheader['history'] = '- meanerr: {}'.format(astrsumm1.meanerr)
        newheader[
            'history'] = '-------------------------------------------------------'
        hdu = fits.PrimaryHDU(image2d.astype(np.float32), newheader)
        hdu.writeto(output_fname, overwrite=True)
        logfile.print('-> file {} created'.format(output_fname))
        save_auxfiles(output_fname=output_fname,
                      nightdir=nightdir,
                      workdir=workdir,
                      logfile=logfile)
        return ierr_astr, astrsumm1, astrsumm2

    # remove SIP parameters in newheader
    newheader['history'] = '--Deleting SIP from Astrometry.net WCS solution--'
    newheader.add_comment('--Deleted SIP from Astrometry.net WCS solution--')
    sip_param = []
    for p in ['', 'P']:
        for c in ['A', 'B']:
            sip_param += ['{}{}_ORDER'.format(c, p)]
            sip_param += [
                '{}{}_{}_{}'.format(c, p, i, j) for i in range(3)
                for j in range(3) if i + j < 3
            ]
    for kwd in sip_param:
        kwd_value = newheader[kwd]
        kwd_comment = newheader.comments[kwd]
        newheader['comment'] = 'deleted {:8} = {:20} / {}'.format(
            kwd, kwd_value, kwd_comment)
        del newheader[kwd]
    # remove HISTORY and COMMENT entries from astrometry.net
    with fits.open('{}/xxx.wcs'.format(workdir)) as hdul:
        oldheader = hdul[0].header
    for kwd in ['HISTORY', 'COMMENT']:
        for itemval in oldheader[kwd]:
            try:
                idel = list(newheader.values()).index(itemval)
            except ValueError:
                idel = -1
            if idel > -1:
                del newheader[idel]
    # delete additional comment lines
    tobedeleted = [
        'Original key: "END"', '--Start of Astrometry.net WCS solution--',
        '--Put in by the new-wcs program--', '--End of Astrometry.net WCS--',
        '--(Put in by the new-wcs program)--'
    ]
    for item in tobedeleted:
        try:
            idel = list(newheader.values()).index(item)
        except ValueError:
            idel = -1
        if idel > -1:
            del newheader[idel]
    # remove blank COMMENTS
    idel = 0
    while idel > -1:
        try:
            idel = list(newheader.values()).index('')
        except ValueError:
            idel = -1
        if idel > -1:
            del newheader[idel]

    # set the TPV solution obtained with sextractor+scamp
    newheader['history'] = '--Computing new solution with SEXTRACTOR+SCAMP--'
    with open('{}/xxx.head'.format(workdir)) as tpvfile:
        tpvheader = tpvfile.readlines()
    for line in tpvheader:
        kwd = line[:8].strip()
        if kwd.find('END') > -1:
            break
        if kwd == 'COMMENT':
            pass  # Avoid problem with non-standard ASCII characters
        elif kwd == 'HISTORY':
            newheader[kwd] = line[10:].rstrip()
        else:
            # note the blank spaces to avoid problem with "S/N"
            kwd_value, kwd_comment = line[11:].split(' / ')
            try:
                value = float(kwd_value.replace('\'', ' '))
            except ValueError:
                value = kwd_value.replace('\'', ' ')
            newheader[kwd] = (value, kwd_comment.rstrip())

    # set CTYPE1 and CTYPE2 from 'RA---TAN' and 'DEC--TAN' to 'RA---TPV' and 'DEC--TPV'
    newheader['CTYPE1'] = 'RA---TPV'
    newheader['CTYPE2'] = 'DEC--TPV'

    # load WCS computed with SCAMP
    w = WCS(newheader)
    # compute pixel scale (mean in both axis) in arcsec/pix
    pixel_scales_arcsec_pix = proj_plane_pixel_scales(w) * 3600
    logfile.print('astrometry> pixel scales (arcsec/pix): {}'.format(
        pixel_scales_arcsec_pix))

    # load peak location from catalogue
    peak_x, peak_y = load_scamp_cat('full', workdir, logfile)
    peak_ra, peak_dec = load_scamp_cat('merged', workdir, logfile)
    pred_x, pred_y = w.wcs_world2pix(peak_ra, peak_dec, 1)

    # predict expected location of GAIA data
    with fits.open('{}/GaiaDR2-query.fits'.format(workdir)) as hdul_table:
        gaiadr2 = hdul_table[1].data
    xgaia, ygaia = w.wcs_world2pix(gaiadr2.ra, gaiadr2.dec, 1)

    # generate plots
    astrsumm2 = plot_astrometry(
        output_fname=output_fname,
        image2d=image2d,
        mask2d=mask2d,
        peak_x=peak_x,
        peak_y=peak_y,
        pred_x=pred_x,
        pred_y=pred_y,
        xcatag=xgaia,
        ycatag=ygaia,
        pixel_scales_arcsec_pix=pixel_scales_arcsec_pix,
        workdir=workdir,
        interactive=interactive,
        logfile=logfile,
        suffix='scamp')

    # store astrometric summaries in history
    newheader[
        'history'] = '-------------------------------------------------------'
    newheader[
        'history'] = 'Summary of astrometric calibration with Astrometry.net:'
    newheader['history'] = '- pixscale: {}'.format(astrsumm1.pixscale)
    newheader['history'] = '- ntargets: {}'.format(astrsumm1.ntargets)
    newheader['history'] = '- meanerr: {}'.format(astrsumm1.meanerr)
    newheader[
        'history'] = '-------------------------------------------------------'
    newheader[
        'history'] = 'Summary of astrometric calibration with AstrOmatic.net:'
    newheader['history'] = '- pixscale: {}'.format(astrsumm2.pixscale)
    newheader['history'] = '- ntargets: {}'.format(astrsumm2.ntargets)
    newheader['history'] = '- meanerr: {}'.format(astrsumm2.meanerr)
    newheader[
        'history'] = '-------------------------------------------------------'

    # save result
    hdu = fits.PrimaryHDU(image2d.astype(np.float32), newheader)
    hdu.writeto(output_fname, overwrite=True)
    logfile.print('-> file {} created'.format(output_fname))

    # storing relevant files in corresponding subdirectory
    save_auxfiles(output_fname=output_fname,
                  nightdir=nightdir,
                  workdir=workdir,
                  logfile=logfile)

    return ierr_astr, astrsumm1, astrsumm2
Example #19
0
def get_nearby_offset_stars(source_ra,
                            source_dec,
                            source_name,
                            how_many=3,
                            radius_degrees=2 / 60.,
                            mag_limit=18.0,
                            mag_min=10.0,
                            min_sep_arcsec=5,
                            starlist_type='Keck',
                            obstime=None,
                            use_source_pos_in_starlist=True,
                            allowed_queries=2,
                            queries_issued=0):
    """Finds good list of nearby offset stars for spectroscopy
       and returns info about those stars, including their
       offsets calculated to the source of interest

    Parameters
    ----------
    source_ra : float
        Right ascension (J2000) of the source
    source_dec : float
        Declination (J2000) of the source
    source_name : str
        Name of the source
    how_many : int, optional
        How many offset stars to try to find
    radius_degrees : float, optional
        Search radius from the source position in arcmin
    mag_limit : float, optional
        How faint should we search for offset stars?
    mag_min : float, optional
        What is the brightest offset star we will allow?
    min_sep_arcsec : float, optional
        What is the closest offset star allowed to the source?
    starlist_type : str, optional
        What starlist format should we use?
    obstime : str, optional
        What datetime (in isoformat) should we assume for the observation
        (to calculate proper motions)?
    use_source_pos_in_starlist : bool, optional
        Return the source itself for in starlist?
    allowed_queries : int, optional
        How many times should we query (with looser and looser criteria)
        before giving up on getting the number of offset stars we desire?
    queries_issued : int, optional
        How many times have we issued a query? Bookkeeping parameter.

    Returns
    -------
    (list, str, int, int)
        Return a tuple which contains: a list of dictionaries for each object
        in the star list, the query issued, the number of queries issues, and
        the length of the star list (not including the source itself)
    """

    if queries_issued >= allowed_queries:
        raise Exception(
            'Number of offsets queries needed exceeds what is allowed')

    if not obstime:
        source_obstime = Time(datetime.datetime.utcnow().isoformat())
    else:
        # TODO: check the obstime format
        source_obstime = Time(obstime)

    gaia_obstime = "J2015.5"

    center = SkyCoord(source_ra,
                      source_dec,
                      unit=(u.degree, u.degree),
                      frame='icrs',
                      obstime=source_obstime)
    # get three times as many stars as requested for now
    # and go fainter as well
    fainter_diff = 2.0  # mag
    search_multipler = 10
    query_string = f"""
                  SELECT TOP {how_many*search_multipler} DISTANCE(
                    POINT('ICRS', ra, dec),
                    POINT('ICRS', {source_ra}, {source_dec})) AS
                    dist, source_id, ra, dec, ref_epoch,
                    phot_rp_mean_mag, pmra, pmdec, parallax
                  FROM gaiadr2.gaia_source
                  WHERE 1=CONTAINS(
                    POINT('ICRS', ra, dec),
                    CIRCLE('ICRS', {source_ra}, {source_dec},
                           {radius_degrees}))
                  AND phot_rp_mean_mag < {mag_limit + fainter_diff}
                  AND phot_rp_mean_mag > {mag_min}
                  AND parallax < 250
                  ORDER BY phot_rp_mean_mag ASC
                """
    # TODO possibly: save the offset data (cache)
    job = Gaia.launch_job(query_string)
    r = job.get_results()
    queries_issued += 1

    catalog = SkyCoord.guess_from_table(r)

    # star needs to be this far away
    # from another star
    min_sep = min_sep_arcsec * u.arcsec
    good_list = []
    for source in r:
        c = SkyCoord(ra=source["ra"],
                     dec=source["dec"],
                     unit=(u.degree, u.degree),
                     pm_ra_cosdec=(np.cos(source["dec"] * np.pi / 180.0) *
                                   source['pmra'] * u.mas / u.yr),
                     pm_dec=source["pmdec"] * u.mas / u.yr,
                     frame='icrs',
                     distance=min(abs(1 / source["parallax"]), 10) * u.kpc,
                     obstime=gaia_obstime)

        d2d = c.separation(catalog)  # match it to the catalog
        if sum(d2d < min_sep) == 1 and source["phot_rp_mean_mag"] <= mag_limit:
            # this star is not near another star and is bright enough
            # precess it's position forward to the source obstime and
            # get offsets suitable for spectroscopy
            # TODO: put this in geocentric coords to account for parallax
            cprime = c.apply_space_motion(new_obstime=source_obstime)
            dra, ddec = cprime.spherical_offsets_to(center)
            good_list.append((source["dist"], c, source, dra.to(u.arcsec),
                              ddec.to(u.arcsec)))

    good_list.sort()

    # if we got less than we asked for, relax the criteria
    if (len(good_list) < how_many) and (queries_issued < allowed_queries):
        return get_nearby_offset_stars(
            source_ra,
            source_dec,
            source_name,
            how_many=how_many,
            radius_degrees=radius_degrees * 1.3,
            mag_limit=mag_limit + 1.0,
            mag_min=mag_min - 1.0,
            min_sep_arcsec=min_sep_arcsec / 2.0,
            starlist_type=starlist_type,
            obstime=obstime,
            use_source_pos_in_starlist=use_source_pos_in_starlist,
            queries_issued=queries_issued,
            allowed_queries=allowed_queries)

    # default to keck star list
    sep = ' '  # 'fromunit'
    commentstr = "#"
    giveoffsets = True
    maxname_size = 16
    # truncate the source_name if we need to
    if len(source_name) > 10:
        basename = source_name[0:3] + ".." + source_name[-6:]
    else:
        basename = source_name

    if starlist_type == 'Keck':
        pass

    elif starlist_type == 'P200':
        sep = ':'  # 'fromunit'
        commentstr = "!"
        giveoffsets = False
        maxname_size = 20

        # truncate the source_name if we need to
        if len(source_name) > 15:
            basename = source_name[0:3] + ".." + source_name[-11:]
        else:
            basename = source_name

    else:
        print("Warning: Do not recognize this starlist format. Using Keck.")

    basename = basename.strip().replace(" ", "")
    space = " "
    star_list_format = (
        f"{basename:{space}<{maxname_size}} " +
        f"{center.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}"
        + f" 2000.0 {commentstr}")

    star_list = []
    if use_source_pos_in_starlist:
        star_list.append({
            "str": star_list_format,
            "ra": float(source_ra),
            "dec": float(source_dec),
            "name": basename
        })

    for i, (dist, c, source, dra, ddec) in enumerate(good_list[:how_many]):

        dras = f"{dra.value:<0.03f}\" E" if dra > 0 else f"{abs(dra.value):<0.03f}\" W"
        ddecs = f"{ddec.value:<0.03f}\" N" if ddec > 0 else f"{abs(ddec.value):<0.03f}\" S"

        if giveoffsets:
            offsets = \
                f"raoffset={dra.value:<0.03f} decoffset={ddec.value:<0.03f}"
        else:
            offsets = ""

        name = f"{basename}_off{i+1}"

        star_list_format = (
            f"{name:{space}<{maxname_size}} " +
            f"{c.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}"
            + f" 2000.0 {offsets} " +
            f" {commentstr} dist={3600*dist:<0.02f}\"; {source['phot_rp_mean_mag']:<0.02f} mag"
            + f"; {dras}, {ddecs} " + f" ID={source['source_id']}")

        star_list.append({
            "str": star_list_format,
            "ra": float(source["ra"]),
            "dec": float(source["dec"]),
            "name": name,
            "dras": dras,
            "ddecs": ddecs,
            "mag": float(source["phot_rp_mean_mag"])
        })

    # send back the starlist in
    return (star_list, query_string.replace("\n", " "), queries_issued,
            len(star_list) - 1)
Example #20
0
def make_catalog(ra: float, dec: float, radius: float, year: float,
                 outfile=None, return_data=False
                 ) -> Union[int, Tuple[int, Table]]:
    """
    Make a catalogue of Gaia/2MASS point sources based on a circle of center
    "ra" and "dec" of "radius" [arcsec]

    Output table has following columns
    index, ra, dec, kmag, kmag, kmag

    and is saved in "outfile"

    :param ra: float, the Right Ascension in degrees
    :param dec: float, the Declination in degrees
    :param radius: float, the field radius (in arc seconds)
    :param year: float, the decimal year (to propagate ra/dec with proper
                 motion/plx)

    :return: returns position of target in source list table (if return_data is
             False else returns a tuple 1. the position of target in source
             list table, 2. the Table of sources centered on the ra/dec
    """
    print('='*50)
    print('Field Catalog Generator')
    print('='*50)

    # log input parameters
    print('\nMaking catalog for field centered on:')
    print('\t RA: {0}'.format(ra))
    print('\t DEC: {0}'.format(ra))
    print('\n\tradius = {0} arcsec'.format(radius))
    print('\tObservation date: {0}'.format(year))

    # get observation time
    with warnings.catch_warnings(record=True) as _:
        obs_time = Time(year, format='decimalyear')
    # get center as SkyCoord
    coord_cent = SkyCoord(ra * uu.deg, dec * uu.deg)

    # -------------------------------------------------------------------------
    # Query Gaia - need proper motion etc
    # -------------------------------------------------------------------------
    # construct gaia query
    gaia_query = GAIA_QUERY.format(RA=ra, DEC=dec, RADIUS=radius/3600.0,
                                   GAIA_TWOMASS_ID=GAIA_TWOMASS_ID)
    # define gaia time
    gaia_time = Time('2015.5', format='decimalyear')
    # run Gaia query
    print('\nQuerying Gaia field\n')
    gaia_table = tap_query(GAIA_URL, gaia_query)
    # -------------------------------------------------------------------------
    # Query 2MASS - need to get J, H and Ks mag
    # -------------------------------------------------------------------------
    jmag, hmag, kmag, tmass_id = [], [], [], []
    # now get 2mass magnitudes for each entry
    for row in range(len(gaia_table)):
        # log progress
        pargs = [row + 1, len(gaia_table)]
        print('Querying 2MASS source {0} / {1}'.format(*pargs))
        # query 2MASS for magnitudes
        tmass_query = TWOMASS_QUERY.format(ID=gaia_table[GAIA_TWOMASS_ID][row],
                                           TWOMASS_ID=TWOMASS_ID)
        # run 2MASS query
        tmass_table = tap_query(TWOMASS_URL, tmass_query)
        # deal with no entry
        if tmass_query is None:
            jmag.append(np.nan)
            hmag.append(np.nan)
            kmag.append(np.nan)
            tmass_id.append('NULL')
        else:
            jmag.append(tmass_table['jmag'][0])
            hmag.append(tmass_table['hmag'][0])
            kmag.append(tmass_table['kmag'][0])
            tmass_id.append(tmass_table[TWOMASS_ID][0])
    # add columns to table
    gaia_table['JMAG'] = jmag
    gaia_table['HMAG'] = hmag
    gaia_table['KMAG'] = kmag
    gaia_table[TWOMASS_ID] = tmass_id
    # -------------------------------------------------------------------------
    # Clean up table - remove all entries without 2MASS
    # -------------------------------------------------------------------------
    # remove rows with NaNs in 2MASS magnitudes
    mask = np.isfinite(gaia_table['JMAG'])
    mask &= np.isfinite(gaia_table['HMAG'])
    mask &= np.isfinite(gaia_table['KMAG'])
    # mask table
    cat_table = gaia_table[mask]
    # -------------------------------------------------------------------------
    # Apply space motion
    # -------------------------------------------------------------------------
    # get entries as numpy arrays (with units)
    ra_arr = np.array(cat_table['ra']) * uu.deg
    dec_arr = np.array(cat_table['dec']) * uu.deg
    pmra_arr = np.array(cat_table['pmra']) * uu.mas/uu.yr
    pmde_arr = np.array(cat_table['pmde']) * uu.mas/uu.yr
    plx_arr = np.array(cat_table['plx']) * uu.mas
    # Get sky coords instance
    coords0 = SkyCoord(ra_arr, dec_arr,
                       pm_ra_cosdec=pmra_arr, pm_dec=pmde_arr,
                       distance=Distance(parallax=plx_arr),
                       obstime=gaia_time)
    # apply space motion
    with warnings.catch_warnings(record=True) as _:
        coords1 = coords0.apply_space_motion(obs_time)
    # find our target source (closest to input)
    separation = coord_cent.separation(coords0)
    # sort rest by brightness
    order = np.argsort(cat_table['KMAG'])
    # get the source position (after ordering separation)
    # assume our source is closest to the center
    source_pos = int(np.argmin(separation[order]))
    # -------------------------------------------------------------------------
    # make final table
    # -------------------------------------------------------------------------
    # start table instance
    final_table = Table()
    # index column
    final_table['index'] = np.arange(len(coords1))
    # ra column
    final_table[RA_OUTCOL] = coords1.ra.value[order]
    # dec column
    final_table[DEC_OUTCOL] = coords1.dec.value[order]
    # mag columns
    final_table[F380M_OUTCOL] = cat_table['KMAG'][order]
    final_table[F430M_OUTCOL] = cat_table['KMAG'][order]
    final_table[F480M_OUTCOL] = cat_table['KMAG'][order]
    # -------------------------------------------------------------------------
    # deal with return data
    if return_data:
        return source_pos, final_table
    # -------------------------------------------------------------------------
    # write file
    write_catalog(final_table, outfile)
    # return the position closest to the input coordinates
    return source_pos
    if np.abs(starPmTot) / starPmTotE < 1.5:
        pmOK = False
    if pmOK:
        print('TIC has proper motion data.  Getting 2015.5 position for GAIA')
        ctic = SkyCoord(
            ra=starRa * u.deg,
            dec=starDec * u.deg,
            pm_ra_cosdec=starPmRa * u.mas / u.yr,
            pm_dec=starPmDec * u.mas / u.yr,
            obstime=Time('J2000.0'),
            distance=1000.0 *
            u.pc,  # Use fake distance just for skycoord functionality
            radial_velocity=0.0 * u.km / u.s)  # Use fake rv "
        # convert position to GAIA DR2 2015.5
        gaiaEpc = Time('J2015.5')
        ctic_gaia_epoch = ctic.apply_space_motion(gaiaEpc)
        gaiaPredRa = ctic_gaia_epoch.ra.degree
        gaiaPredDec = ctic_gaia_epoch.dec.degree
    else:
        gaiaPredRa = starRa
        gaiaPredDec = starDec
    gaiaPos = 'Predicted GAIA position {0:.6f} {1:.6f} [J2000.0; epoch 2015.5]'.format(
        gaiaPredRa, gaiaPredDec)

    # We are go for GAIA Cone search
    ADSQL_Str = "SELECT \
       DISTANCE( POINT('ICRS', ra, dec),\
       POINT('ICRS', {0}, {1}) ) AS dist, phot_g_mean_mag, teff_val, teff_percentile_lower, \
       teff_percentile_upper, radius_val, radius_percentile_lower, radius_percentile_upper,\
       astrometric_gof_al, astrometric_excess_noise_sig, phot_bp_mean_mag, phot_rp_mean_mag, bp_rp, \
       a_g_val, e_bp_min_rp_val, parallax, \
print(len(result[0]))

###############################################################################
# Now we load all stars into an array coordinate.  The reference epoch for the
# star positions is J2015.5, # so we update these positions to the date of the
# COR2 observation using :meth:`astropy.coordinates.SkyCoord.apply_space_motion`.

tbl_crds = SkyCoord(ra=result[0]['RA_ICRS'],
                    dec=result[0]['DE_ICRS'],
                    distance=Distance(parallax=u.Quantity(result[0]['Plx'])),
                    pm_ra_cosdec=result[0]['pmRA'],
                    pm_dec=result[0]['pmDE'],
                    radial_velocity=result[0]['RV'],
                    frame='icrs',
                    obstime=Time(result[0]['Epoch'], format='jyear'))
tbl_crds = tbl_crds.apply_space_motion(new_obstime=cor2.date)

###############################################################################
# One of the bright features is actually Mars, so let's also get that coordinate.

mars = get_body_heliographic_stonyhurst('mars', cor2.date, observer=cor2.observer_coordinate)

###############################################################################
# Let's plot the results.  The coordinates will be transformed automatically
# when plotted using :meth:`~astropy.visualization.wcsaxes.WCSAxes.plot_coord`.

ax = plt.subplot(projection=cor2)

# Let's tweak the axis to show in degrees instead of arcsec
lon, lat = ax.coords
lon.set_major_formatter('d.dd')
Example #23
0
    def __init__(self, filename, search_radius = 5, target_ra = np.nan, target_dec = np.nan, dist = np.nan,
                 boxsize = 13, hires_scale = 1, rotate_to = np.nan, normalize = False, query_simbad = False):
        """Load in an image, store some important parameters and perform initial image processing."""

        with fits.open(filename) as fitsfile:
            self.image = fitsfile['image'].data * 1000              #image in mJy/pixel
            self.pfov = fitsfile['image'].header['CDELT2'] * 3600   #pixel FOV in arcsec
            self.wav = int(fitsfile['PRIMARY'].header['WAVELNTH'])  #wavelength of observations
            self.level = int(fitsfile['PRIMARY'].header['LEVEL'])   #processing level (20 or 25)
            self.name = fitsfile['PRIMARY'].header['OBJECT']        #target name
            self.angle = fitsfile['PRIMARY'].header['POSANGLE']     #pointing position angle

            #extract the obsid; the appropriate keyword seemingly depends on the processing level
            try:
                self.obsid = fitsfile['PRIMARY'].header['OBSID001'] #this works for level 2.5
            except KeyError:
                self.obsid = fitsfile['PRIMARY'].header['OBS_ID']   #and this for level 2

            #get the expected star coordinates in pixels, if RA and dec were provided;
            #otherwise, assume it's at the centre of the image
            if np.isnan(target_ra) or np.isnan(target_dec):
                star_expected = [i / 2 for i in self.image.shape]
            else:
                wcs = WCS(fitsfile['image'].header)
                star_expected = np.flip(wcs.wcs_world2pix([[target_ra, target_dec]], 0)[0])

            #extract coverage level, so that we can estimate the rms flux in a suitable region
            cov = fitsfile['coverage'].data

        #refuse to analyse 160 micron data (70/100 is always available and generally at higher S/N)
        if self.wav != 70 and self.wav != 100:
            raise Exception(f"Please provide a 70 or 100 μm image ({filename} is at {self.wav} μm)")

        #factors to correct for flux lost during high-pass filtering (see Kennedy et al. 2012)
        if self.wav == 70:
            self.flux_factor = 1.16
        elif self.wav == 100:
            self.flux_factor = 1.19

        #if no distance is supplied, simply set d = 1 pc so that separations will be in arcsec, not au;
        #in_au can be stored in any saved output for future reference, and plots can be annotated with sep_unit,
        #which is intended to be embedded in a LaTeX string
        if np.isnan(dist):
            distance_provided = False
            dist = 1
            self.sep_unit = r'^{\prime\prime}'
            self.in_au = False
        else:
            distance_provided = True
            self.sep_unit = r'\mathrm{au}'
            self.in_au = True

        #au per pixel at the distance of the target
        self.aupp = self.pfov * dist

        #clean up NaN pixels
        self.image[np.isnan(self.image)] = 0
        cov[np.isnan(cov)] = 0

        #find the coordinates of the brightest pixel within search_radius arcsec
        #of the specified RA and dec (or simply the centre)
        brightest_pix = self._find_brightest(search_radius, star_expected)

        #estimate the rms flux in a region defined by two conditions: coverage is above a
        #specified level, and projected separation from the brightest pixel is above a certain level.
        #NOTE: if the provided RA/dec are far from the image centre, the region defined by these
        #conditions may not be the most appropriate (however, it's unlikely that we will be trying
        #to fit a source near the edge of a map)

        cov_threshold_rms = 0.6 #fraction of max coverage
        sep_threshold_rms = 15 #arcsec
        sky_separation = self._projected_sep_array(brightest_pix)

        self.rms = self._estimate_background((cov > cov_threshold_rms * np.max(cov)) &
                                             (sky_separation > sep_threshold_rms))

        #need to scale up uncertainties since noise is correlated
        natural_pixsize = 3.2 #always the case for PACS 70/100 micron images
        self.uncert = self.rms * self._correlated_noise_factor(natural_pixsize)


        if np.isnan(rotate_to):
            #no rotation requested; simply crop down to the requested size
            self._crop_image(brightest_pix,  boxsize)

        else:
            #cut out a portion of the image with the brightest pixel at the centre - this step is necessary
            #because after the rotation the brightest_pix coordinates will no longer be correct
            self._crop_image(brightest_pix, 2 * boxsize)

            #rotate to the requested position angle (necessary if using image as a PSF)
            self.image = rotate(self.image, self.angle - rotate_to)

            #now cut down to the requested size; note that we again look for the brightest pixel
            #and put this in the centre, since the rotation may have introduced a small offset
            self._crop_image(self._find_brightest(2 * self.pfov, [i / 2 for i in self.image.shape]), boxsize)

        #normalize if requested
        if normalize: self.image /= np.sum(self.image)

        #rebin to a higher resolution if requested
        self.hires_scale = hires_scale
        if self.hires_scale >= 1:
            self.image_hires = congrid(self.image,
                                      [i * self.hires_scale for i in self.image.shape],
                                      minusone = True)

            #ensure that flux is conserved
            self.image_hires *= np.sum(self.image) / np.sum(self.image_hires)

        else:
            raise Exception(f"hires_scale should be an integer >= 1")


        if query_simbad:
            if not np.isnan(rotate_to):
                warnings.warn(f"SIMBAD source overplotting for rotated images is"
                              " not supported. Skipping query.", stacklevel = 2)

            else:
                self.source_coords=[]
                self.source_names=[]
                Simbad.add_votable_fields('pm','plx')
                with fits.open(filename) as fitsfile:
                    qra = fitsfile['PRIMARY'].header['RA'] if np.isnan(target_ra) else target_ra
                    qdec = fitsfile['PRIMARY'].header['DEC'] if np.isnan(target_dec) else target_dec
                    coord = SkyCoord(ra = qra, dec = qdec, unit = (u.degree, u.degree), frame = 'icrs')

                    #find sources within a circle whose radius is half the cutout side length
                    r = Simbad.query_region(coordinates = coord, radius = boxsize * self.pfov * u.arcsec)

                    if len(r) > 0:
                        wcs = WCS(fitsfile['image'].header)
                        for i in range(len(r)):
                            #assume a distance of 50pc if none is available
                            qdist = 50 * u.pc

                            #preferentially use the supplied distance
                            if distance_provided: qdist = dist * u.pc

                            #otherwise, try to get one from SIMBAD
                            elif r[i]['PLX_VALUE'] > 0: distance = (1e3 / r[i]['PLX_VALUE']) * u.pc

                            if np.isfinite(r[i]['PMRA']) and np.isfinite(r[i]['PMDEC']):
                                #J2000 sky coordinates
                                s2000 = SkyCoord(r[i]['RA'].replace(' ',':')+' '+r[i]['DEC'].replace(' ',':'),
                                                 unit = (u.hour, u.degree), distance = qdist,
                                                 pm_ra_cosdec = r[i]['PMRA'] * u.mas / u.yr,
                                                 pm_dec = r[i]['PMDEC'] * u.mas / u.yr,
                                                 obstime = Time(2451545.0,format='jd'))

                                #apply proper motion correction to observation date
                                s = s2000.apply_space_motion(new_obstime = Time(fitsfile['PRIMARY'].header['DATE-OBS']))

                            else:
                                s = SkyCoord(r[i]['RA'].replace(' ',':')+' '+r[i]['DEC'].replace(' ',':'),
                                             unit = (u.hour, u.degree))

                            #find the pixel corresponding to source i
                            coord = np.flip(wcs.wcs_world2pix([[s.ra.deg, s.dec.deg]], 0)[0])

                            #translate into image cutout coordinates
                            coord -= np.array(brightest_pix) - boxsize

                            #store the coordinates and source name as an attribute
                            append = True

                            #don't append duplicate coordinates (i.e. planets)
                            for c in self.source_coords:
                                if np.isclose(c, coord).all():
                                    append = False

                            if append:
                                self.source_coords.append(coord)
                                self.source_names.append(r[i]['MAIN_ID'].decode())
Example #24
0
def get_nearby_offset_stars(
    source_ra,
    source_dec,
    source_name,
    how_many=3,
    radius_degrees=2 / 60.0,
    mag_limit=18.0,
    mag_min=10.0,
    min_sep_arcsec=2,
    starlist_type='Keck',
    obstime=None,
    use_source_pos_in_starlist=True,
    allowed_queries=2,
    queries_issued=0,
    use_ztfref=True,
    required_ztfref_source_distance=60,
):
    """Finds good list of nearby offset stars for spectroscopy
       and returns info about those stars, including their
       offsets calculated to the source of interest

    Parameters
    ----------
    source_ra : float
        Right ascension (J2000) of the source
    source_dec : float
        Declination (J2000) of the source
    source_name : str
        Name of the source
    how_many : int, optional
        How many offset stars to try to find
    radius_degrees : float, optional
        Search radius from the source position in arcmin
    mag_limit : float, optional
        How faint should we search for offset stars?
    mag_min : float, optional
        What is the brightest offset star we will allow?
    min_sep_arcsec : float, optional
        What is the closest offset star allowed to the source?
    starlist_type : str, optional
        What starlist format should we use?
    obstime : str, optional
        What datetime (in isoformat) should we assume for the observation
        (to calculate proper motions)?
    use_source_pos_in_starlist : bool, optional
        Return the source itself for in starlist?
    allowed_queries : int, optional
        How many times should we query (with looser and looser criteria)
        before giving up on getting the number of offset stars we desire?
    queries_issued : int, optional
        How many times have we issued a query? Bookkeeping parameter.
    use_ztfref : boolean, optional
        Use the ZTFref catalog for offset star positions if possible
    required_ztfref_source_distance : float, optional
        If there are zero ZTF ref stars within this distance in arcsec,
        then do not use the ztfref catalog even if asked. This probably
        means that the source is at the end of the ref catalog.

    Returns
    -------
    (list, str, int, int, bool)
        Return a tuple which contains: a list of dictionaries for each object
        in the star list, the query issued, the number of queries issues,
        the length of the star list (not including the source itself),
        and whether the ZTFref catalog was used for source positions or not.
    """

    if queries_issued >= allowed_queries:
        raise Exception(
            'Number of offsets queries needed exceeds what is allowed')

    if not obstime:
        source_obstime = Time(datetime.datetime.utcnow().isoformat())
    else:
        # TODO: check the obstime format
        source_obstime = Time(obstime)

    center = SkyCoord(
        source_ra,
        source_dec,
        unit=(u.degree, u.degree),
        frame='icrs',
        obstime=source_obstime,
    )
    # get three times as many stars as requested for now
    # and go fainter as well
    fainter_diff = 1.5  # mag
    search_multipler = 20
    min_distance = 5.0 / 3600.0  # min distance from source for offset star
    source_in_catalog_dist = 0.5 / 3600.0  # min distance from source for offset star
    query_string = f"""
                  SELECT TOP {how_many*search_multipler} DISTANCE(
                    POINT('ICRS', ra, dec),
                    POINT('ICRS', {source_ra}, {source_dec})) AS
                    dist, source_id, ra, dec, ref_epoch,
                    phot_rp_mean_mag, pmra, pmdec, parallax
                  FROM {{main_db}}.gaia_source
                  WHERE 1=CONTAINS(
                    POINT('ICRS', ra, dec),
                    CIRCLE('ICRS', {source_ra}, {source_dec},
                           {radius_degrees}))
                  AND phot_rp_mean_mag < {mag_limit + fainter_diff}
                  AND phot_rp_mean_mag > {mag_min}
                  AND parallax < 250
                  ORDER BY dist ASC
                """

    g = GaiaQuery()
    r = g.query(query_string)

    # get brighter stars at top:
    r.sort("phot_rp_mean_mag")

    potential_source_in_gaia_query = r[r["dist"] < source_in_catalog_dist]
    if len(potential_source_in_gaia_query) > 0:
        # try to find offset stars brighter than the catalog brightness of the
        # source.
        source_catalog_mag = potential_source_in_gaia_query["phot_rp_mean_mag"]
        offset_brightness_limit = source_catalog_mag
        for _ in range(3):
            temp_r = r[r["phot_rp_mean_mag"] <= offset_brightness_limit]
            if len(temp_r) > how_many + 2:
                r = temp_r
                break
            offset_brightness_limit += 0.5

    # filter out stars near the source (and the source itself)
    # since we do not want waste an offset star on very nearby sources
    r = r[r["dist"] > min_distance]

    queries_issued += 1

    catalog = SkyCoord.guess_from_table(r)
    if use_ztfref:
        ztfcatalog = get_ztfcatalog(source_ra, source_dec)
        if ztfcatalog is None:
            log('Warning: Could not find the ZTF reference catalog'
                f' at position {source_ra} {source_dec}')
        else:
            if (sum(
                    center.separation(ztfcatalog) <
                    required_ztfref_source_distance * u.arcsec) == 0):
                ztfcatalog = None
                log('Warning: The ZTF reference catalog is empty near'
                    f' position {source_ra} {source_dec}. This probably means'
                    ' that the source is at the edge of the ref catalog.')
                use_ztfref = False

    # star needs to be this far away
    # from another star
    min_sep = min_sep_arcsec * u.arcsec
    good_list = []
    for source in r:
        c = SkyCoord(
            ra=source["ra"],
            dec=source["dec"],
            unit=(u.degree, u.degree),
            pm_ra_cosdec=source['pmra'] * u.mas / u.yr,
            pm_dec=source["pmdec"] * u.mas / u.yr,
            frame='icrs',
            distance=min(abs(1 / source["parallax"]), 10) * u.kpc,
            obstime=Time(source['ref_epoch'], format='jyear'),
        )

        d2d = c.separation(catalog)  # match it to the catalog
        if sum(d2d < min_sep) == 1 and source["phot_rp_mean_mag"] <= mag_limit:
            # this star is not near another star and is bright enough

            # if there's a close match to ZTF reference position then use
            #  ZTF position for this source instead of the gaia/motion data
            if use_ztfref and ztfcatalog is not None:
                idx, ztfdist, _ = c.match_to_catalog_sky(ztfcatalog)
                if ztfdist < 0.5 * u.arcsec:
                    cprime = SkyCoord(
                        ra=ztfcatalog[idx].ra.value,
                        dec=ztfcatalog[idx].dec.value,
                        unit=(u.degree, u.degree),
                        frame='icrs',
                        obstime=source_obstime,
                    )

                    dra, ddec = cprime.spherical_offsets_to(center)
                    pa = cprime.position_angle(center).degree
                    # use the RA, DEC from ZTF here
                    source["ra"] = ztfcatalog[idx].ra.value
                    source["dec"] = ztfcatalog[idx].dec.value
                    good_list.append((
                        source["dist"],
                        cprime,
                        source,
                        dra.to(u.arcsec),
                        ddec.to(u.arcsec),
                        pa,
                    ))
            else:
                # precess it's position forward to the source obstime and
                # get offsets suitable for spectroscopy
                # TODO: put this in geocentric coords to account for parallax
                cprime = c.apply_space_motion(new_obstime=source_obstime)
                dra, ddec = cprime.spherical_offsets_to(center)
                pa = cprime.position_angle(center).degree
                good_list.append((
                    source["dist"],
                    cprime,
                    source,
                    dra.to(u.arcsec),
                    ddec.to(u.arcsec),
                    pa,
                ))

    good_list.sort()

    # if we got less than we asked for, relax the criteria
    if (len(good_list) < how_many) and (queries_issued < allowed_queries):
        return get_nearby_offset_stars(
            source_ra,
            source_dec,
            source_name,
            how_many=how_many,
            radius_degrees=radius_degrees * 1.3,
            mag_limit=mag_limit + 1.0,
            mag_min=mag_min - 1.0,
            min_sep_arcsec=min_sep_arcsec / 2.0,
            starlist_type=starlist_type,
            obstime=obstime,
            use_source_pos_in_starlist=use_source_pos_in_starlist,
            queries_issued=queries_issued,
            allowed_queries=allowed_queries,
            use_ztfref=use_ztfref,
            required_ztfref_source_distance=required_ztfref_source_distance,
        )

    starlist_format = starlist_formats.get(starlist_type)
    if starlist_format is None:
        log("Warning: Do not recognize this starlist format. Using Keck.")
        starlist_format = starlist_formats["Keck"]

    sep = starlist_format["sep"]
    commentstr = starlist_format["commentstr"]
    giveoffsets = starlist_format["giveoffsets"]
    maxname_size = starlist_format["maxname_size"]
    first_line = starlist_format["first_line"]

    basename = source_name.strip().replace(" ", "")
    if len(basename) > maxname_size:
        basename = basename[3:]

    abrev_basename = source_name.strip().replace(" ", "")
    if len(abrev_basename) > maxname_size - 3:
        abrev_basename = basename[3:maxname_size]

    space = " "
    star_list_format = (
        f"{basename:{space}<{maxname_size}} " +
        f"{center.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}"
        + f" 2000.0  {commentstr} source_name={source_name}")

    star_list = [{"str": first_line}] if first_line else []
    if use_source_pos_in_starlist:
        star_list.append({
            "str": star_list_format,
            "ra": float(source_ra),
            "dec": float(source_dec),
            "name": basename,
        })

    for i, (dist, c, source, dra, ddec, pa) in enumerate(good_list[:how_many]):
        dras = f"{dra.value:<0.03f}\" E" if dra > 0 else f"{abs(dra.value):<0.03f}\" W"
        ddecs = (f"{ddec.value:<0.03f}\" N"
                 if ddec > 0 else f"{abs(ddec.value):<0.03f}\" S")

        if giveoffsets:
            offsets = f"raoffset={dra.value:<0.03f} decoffset={ddec.value:<0.03f}"
        else:
            offsets = ""

        name = f"{abrev_basename}_o{i+1}"

        star_list_format = (
            f"{name:{space}<{maxname_size}} " +
            f"{c.to_string('hmsdms', sep=sep, decimal=False, precision=2, alwayssign=True)[1:]}"
            + f" 2000.0 {offsets}" +
            f" {commentstr} dist={3600*dist:<0.02f}\"; {source['phot_rp_mean_mag']:<0.02f} mag"
            + f"; {dras}, {ddecs} PA={pa:<0.02f} deg" +
            f" ID={source['source_id']}")

        star_list.append({
            "str": star_list_format,
            "ra": c.ra.value,
            "dec": c.dec.value,
            "name": name,
            "dras": dras,
            "ddecs": ddecs,
            "mag": float(source["phot_rp_mean_mag"]),
            "pa": pa,
        })

    # send back the starlist in
    return (
        star_list,
        query_string.replace("\n", " "),
        queries_issued,
        len(star_list) - 1,
        use_ztfref,
    )
Example #25
0
# Then propagate the Gaia coordinates to 2000, and find the best match to the
# input coordinates
if not warning:
    ra2015 = np.array(gaia['ra']) * u.deg
    dec2015 = np.array(gaia['dec']) * u.deg
    parallax = np.array(gaia['parallax']) * u.mas
    pmra = np.array(gaia['pmra']) * u.mas / u.yr
    pmdec = np.array(gaia['pmdec']) * u.mas / u.yr
    c2015 = SkyCoord(ra=ra2015,
                     dec=dec2015,
                     distance=Distance(parallax=parallax, allow_negative=True),
                     pm_ra_cosdec=pmra,
                     pm_dec=pmdec,
                     obstime=Time(2015.5, format='decimalyear'))
    c2000 = c2015.apply_space_motion(dt=-15.5 * u.year)

    idx, sep, _ = coord.match_to_catalog_sky(c2000)

    # All objects
    id_all = gaia['source_id']
    plx_all = np.array(gaia['parallax'])
    g_all = np.array(gaia['phot_g_mean_mag'])
    MG_all = 5 + 5 * np.log10(plx_all / 1000) + g_all
    bprp_all = np.array(gaia['bp_rp'])

    id_all = np.array(id_all)
    g_all = np.array(gaia['phot_g_mean_mag'])
    MG_all = np.array(MG_all)
    bprp_all = np.array(bprp_all)