Пример #1
0
def cirs_to_gcrs(time):
    # compute the polar motion p-matrix
    xp, yp = get_polar_motion(time)
    sp = erfa.sp00(*get_jd12(time, "tt"))
    pmmat = erfa.pom00(xp, yp, sp)

    # now determine the Earth Rotation Angle for the input obstime
    # era00 accepts UT1, so we convert if need be
    era = erfa.era00(*get_jd12(time, "ut1"))

    # c2tcio expects a GCRS->CIRS matrix, but we just set that to an I-matrix
    # because we're already in CIRS
    return erfa.c2tcio(np.eye(3), era, pmmat)
Пример #2
0
def apparent_longitude(t='now'):
    """
    Returns the Sun's apparent longitude, referred to the true equinox of date.  Corrections for
    nutation and aberration (for Earth motion) are included.

    Parameters
    ----------
    t : {parse_time_types}
        A time (usually the start time) specified as a parse_time-compatible
        time string, number, or a datetime object.

    Notes
    -----
    The nutation model is IAU 2000A nutation with adjustments to match IAU 2006 precession.
    """
    time = parse_time(t)
    sun = SkyCoord(0*u.deg, 0*u.deg, 0*u.AU, frame='hcrs', obstime=time)
    coord = sun.transform_to(GeocentricMeanEcliptic(equinox=time))

    # Astropy's GeocentricMeanEcliptic already includes aberration, so only add nutation
    jd1, jd2 = get_jd12(time, 'tt')
    nut_lon, _ = erfa.nut06a(jd1, jd2)*u.radian
    lon = coord.lon + nut_lon

    return Longitude(lon)
Пример #3
0
def delta_obs_ra_to_cirs_ra(mjd, longitude, latitude):
    """
  difference between radio astronomers observed coords and CIRS coords
  
  Args
  ====
    mjd:       (float) modified Julian date
    longitude: (float) in degrees or as astropy Angle
    latitude:  (float) in degrees or as astropy Angle
  
  Return
  ======
    hour angle as astropy.coordinates.Angle
  """
    from astropy import _erfa as erfa
    from astropy.coordinates.builtin_frames.utils import get_jd12
    # get the Earth Rotation Angle
    era = erfa.era00(*get_jd12(APt.Time(mjd, format='mjd'), 'ut1'))
    logger.debug("delta_obs_ra_to_cirs_ra: ERA = %f rad", era)
    logger.debug("delta_obs_ra_to_cirs_ra: ERA = %s",
                 format_angles(era * 12 / pi))
    theta_earth = APc.Angle(era, unit='rad')
    # get the time of observation
    obs_time = APt.Time(mjd,
                        format='mjd',
                        location=(APc.Angle(longitude, unit="deg"),
                                  APc.Angle(latitude, unit="deg")))
    # Greenwich apparent sidereal time of observation
    gast = obs_time.sidereal_time('apparent', longitude=0)  # Greenwich ST
    logger.debug("delta_obs_ra_to_cirs_ra: GAST = %s", gast)
    logger.debug("delta_obs_ra_to_cirs_ra: delta = %s", gast - theta_earth)
    return (gast - theta_earth)
Пример #4
0
    def velocity_of_Earth(full_BJD):
        """
        Calculate 3D velocity of Earth for given epoch.

        If you need velocity projected on the plane of the sky, then use
        :py:func:`~MulensModel.coordinates.Coordinates.v_Earth_projected`

        Parameters :
            full_BJD: *float*
                Are we fitting for blending flux? If not then blending flux is
                fixed to 0.  Default is the same as
                :py:func:`MulensModel.fit.Fit.fit_fluxes()`.

        Returns :
            velocity: *np.ndarray* (*float*, size of (3,))
                3D velocity in km/s. The frame follows *Astropy* conventions.
        """
        # The 4 lines below, that calculate velocity for given epoch,
        # are based on astropy 1.3 code:
        # https://github.com/astropy/astropy/blob/master/astropy/
        # coordinates/solar_system.py
        time = Time(full_BJD, format='jd', scale='tdb')
        (jd1, jd2) = get_jd12(time, 'tdb')
        (earth_pv_helio, earth_pv_bary) = erfa.epv00(jd1, jd2)
        factor = 1731.45683  # This scales AU/day to km/s.
        # The returned values are of np.ndarray type in astropy v1 and v2,
        # but np.void in v3. The np.asarray() works in both cases.
        velocity = np.asarray(earth_pv_bary[1]) * factor
        return velocity
Пример #5
0
def _cirs_to_tee_ra(cirs_ra, time):
    """
    Convert from CIRS frame to the true equator & equinox frame.

    The frame radio astronomers call the apparent or current epoch is the
    "true equator & equinox" frame, notated E_upsilon in the USNO circular
    astropy doesn't have this frame but it's pretty easy to adapt the CIRS frame
    by modifying the ra to reflect the difference between
    GAST (Grenwich Apparent Sidereal Time) and the earth rotation angle (theta)

    Parameters
    ----------
    cirs_ra : :class:`astropy.Angle`
        CIRS RA.
    time : :class:`astropy.Time`
        Time object for time to convert to the "true equator & equinox" frame.
    """
    era = erfa.era00(*get_jd12(time, "ut1"))
    theta_earth = Angle(era, unit="rad")

    assert isinstance(time, Time)
    assert isinstance(cirs_ra, Angle)
    gast = time.sidereal_time("apparent", longitude=0)
    tee_ra = cirs_ra + (gast - theta_earth)
    return tee_ra
Пример #6
0
 def test_tete_quick(self):
     # Following copied from intermediate_rotation_transforms.gcrs_to_tete
     rbpn = erfa.pnm06a(*get_jd12(self.obstime, 'tt'))
     loc_gcrs_frame = get_location_gcrs(
         self.loc, self.obstime, tete_to_itrs_mat(self.obstime, rbpn=rbpn),
         rbpn)
     self.check_obsgeo(loc_gcrs_frame.obsgeoloc, loc_gcrs_frame.obsgeovel)
Пример #7
0
def _obliquity_rotation_value(equinox):
    """
    Function to calculate obliquity of the earth.
    This uses obl06 of erfa.
    """
    jd1, jd2 = get_jd12(equinox, "tt")
    obl = erfa.obl06(jd1, jd2) * u.radian
    return obl.to(u.deg)
Пример #8
0
def _obliquity_rotation_value(equinox):
    """
    Function to calculate obliquity of the earth.
    This uses obl06 of erfa.
    """
    jd1, jd2 = get_jd12(equinox, "tt")
    obl = erfa.obl06(jd1, jd2) * u.radian
    return obl.to(u.deg)
Пример #9
0
def _apparent_position_in_true_coordinates(skycoord):
    """
    Convert Skycoord in GCRS frame into one in which RA and Dec
    are defined w.r.t to the true equinox and poles of the Earth
    """
    jd1, jd2 = get_jd12(skycoord.obstime, 'tt')
    _, _, _, _, _, _, _, rbpn = erfa.pn00a(jd1, jd2)
    return SkyCoord(
        skycoord.frame.realize_frame(skycoord.cartesian.transform(rbpn)))
Пример #10
0
def test_atciqz_aticq(t, pos):
    """Check replacements against erfa versions for consistency."""
    jd1, jd2 = get_jd12(t, 'tdb')
    astrom, _ = erfa.apci13(jd1, jd2)

    ra = pos.lon.to_value(u.rad)
    dec = pos.lat.to_value(u.rad)
    assert_allclose(erfa.atciqz(ra, dec, astrom), atciqz(pos, astrom))
    assert_allclose(erfa.aticq(ra, dec, astrom), aticq(pos, astrom))
Пример #11
0
def test_atciqz_aticq(st):
    """Check replacements against erfa versions for consistency."""
    t, pos = st
    jd1, jd2 = get_jd12(t, 'tdb')
    astrom, _ = erfa.apci13(jd1, jd2)

    ra, dec = pos
    ra = ra.value
    dec = dec.value
    assert_allclose(erfa.atciqz(ra, dec, astrom), atciqz(ra, dec, astrom))
    assert_allclose(erfa.aticq(ra, dec, astrom), aticq(ra, dec, astrom))
Пример #12
0
def cirs_to_tee_ra(cirs_ra, time):
    from astropy import _erfa as erfa
    from astropy.coordinates.builtin_frames.utils import get_jd12
    era = erfa.era00(*get_jd12(time, 'ut1'))
    theta_earth = Angle(era, unit='rad')

    assert (isinstance(time, Time))
    assert (isinstance(cirs_ra, Angle))
    gast = time.sidereal_time('apparent', longitude=0)
    tee_ra = cirs_ra + (gast - theta_earth)
    return tee_ra
Пример #13
0
def test_atciqz_aticq(st):
    """Check replacements against erfa versions for consistency."""
    t, pos = st
    jd1, jd2 = get_jd12(t, 'tdb')
    astrom, _ = erfa.apci13(jd1, jd2)

    ra, dec = pos
    ra = ra.value
    dec = dec.value
    assert_allclose(erfa.atciqz(ra, dec, astrom), atciqz(ra, dec, astrom))
    assert_allclose(erfa.aticq(ra, dec, astrom), aticq(ra, dec, astrom))
def test_icrs_altaz_moonish(testframe):
    """
    Check that something expressed in *ICRS* as being moon-like goes to the
    right AltAz distance
    """
    # we use epv00 instead of get_sun because get_sun includes aberration
    earth_pv_helio, earth_pv_bary = epv00(*get_jd12(testframe.obstime, 'tdb'))
    earth_icrs_xyz = earth_pv_bary[0]*u.au
    moonoffset = [0, 0, MOONDIST.value]*MOONDIST.unit
    moonish_icrs = ICRS(CartesianRepresentation(earth_icrs_xyz + moonoffset))
    moonaa = moonish_icrs.transform_to(testframe)

    # now check that the distance change is similar to earth radius
    assert 1000*u.km < np.abs(moonaa.distance - MOONDIST).to(u.au) < 7000*u.km
Пример #15
0
def mean_obliquity_of_ecliptic(t='now'):
    """
    Returns the mean obliquity of the ecliptic, using the IAU 2006 definition.  No correction for
    nutation is included.

    Parameters
    ----------
    t : {parse_time_types}
        Time to use in a parse-time-compatible format
    """
    time = parse_time(t)
    jd1, jd2 = get_jd12(time, 'tt')
    obl = erfa.obl06(jd1, jd2) * u.radian
    return Angle(obl, u.arcsec)
def test_icrs_altaz_moonish(testframe):
    """
    Check that something expressed in *ICRS* as being moon-like goes to the
    right AltAz distance
    """
    # we use epv00 instead of get_sun because get_sun includes aberration
    earth_pv_helio, earth_pv_bary = erfa.epv00(*get_jd12(testframe.obstime, 'tdb'))
    earth_icrs_xyz = earth_pv_bary[0]*u.au
    moonoffset = [0, 0, MOONDIST.value]*MOONDIST.unit
    moonish_icrs = ICRS(CartesianRepresentation(earth_icrs_xyz + moonoffset))
    moonaa = moonish_icrs.transform_to(testframe)

    # now check that the distance change is similar to earth radius
    assert 1000*u.km < np.abs(moonaa.distance - MOONDIST).to(u.au) < 7000*u.km
Пример #17
0
def mean_obliquity_of_ecliptic(t='now'):
    """
    Returns the mean obliquity of the ecliptic, using the IAU 2006 definition.  No correction for
    nutation is included.

    Parameters
    ----------
    t : {parse_time_types}
        A time (usually the start time) specified as a parse_time-compatible
        time string, number, or a datetime object.
    """
    time = parse_time(t)
    jd1, jd2 = get_jd12(time, 'tt')
    obl = erfa.obl06(jd1, jd2)*u.radian
    return Angle(obl, u.arcsec)
Пример #18
0
def _gmst82_angle(obstime):
    """
    Universal Time to Greenwich mean sidereal time (IAU 1982 model).

    Parameters
    ----------
    obstime : Time
        time at which the polar motion should be calculated.
    Returns
    -------
    float
        Greenwich mean sidereal time (radians)
    """
    # Get GMST82 angle in rad
    gmst82 = erfa.gmst82(*get_jd12(obstime, 'ut1')) * u.rad

    return gmst82
Пример #19
0
def true_obliquity_of_ecliptic(t='now'):
    """
    Returns the true obliquity of the ecliptic, using the IAU 2006 definition.  Correction for
    nutation is included.

    Parameters
    ----------
    t : {parse_time_types}
        Time to use in a parse-time-compatible format

    Notes
    -----
    The nutation model is IAU 2000A nutation with adjustments to match IAU 2006 precession.
    """
    time = parse_time(t)
    jd1, jd2 = get_jd12(time, 'tt')
    obl = erfa.obl06(jd1, jd2) * u.radian
    _, nut_obl = erfa.nut06a(jd1, jd2) * u.radian
    obl += nut_obl
    return Angle(obl, u.arcsec)
Пример #20
0
def _polar_mot_matrix(obstime):
    """
    Form the matrix of polar motion for a given date, IAU 2000.

    The matrix operates in the sense V(TRS) = rpom * V(CIP), meaning that it is the final rotation when computing the
    pointing direction to a celestial source.

    Parameters
    ----------
    obstime : Time
        time at which the polar motion should be calculated.
    Returns
    -------
        3x3 rotation matrix due to polar motion
    """
    # compute the polar motion p-matrix
    xp, yp = get_polar_motion(obstime)
    sp = erfa.sp00(*get_jd12(obstime, 'tt'))
    polar_mot_mat = erfa.pom00(xp, yp, sp)

    return polar_mot_mat
Пример #21
0
def true_obliquity_of_ecliptic(t='now'):
    """
    Returns the true obliquity of the ecliptic, using the IAU 2006 definition.  Correction for
    nutation is included.

    Parameters
    ----------
    t : {parse_time_types}
        A time (usually the start time) specified as a parse_time-compatible
        time string, number, or a datetime object.

    Notes
    -----
    The nutation model is IAU 2000A nutation with adjustments to match IAU 2006 precession.
    """
    time = parse_time(t)
    jd1, jd2 = get_jd12(time, 'tt')
    obl = erfa.obl06(jd1, jd2)*u.radian
    _, nut_obl = erfa.nut06a(jd1, jd2)*u.radian
    obl += nut_obl
    return Angle(obl, u.arcsec)
Пример #22
0
def _rotation_matrix_obliquity(time):
    """
    Return the rotation matrix from Earth equatorial to ecliptic coordinates
    """
    return rotation_matrix(erfa.obl06(*get_jd12(time, 'tt'))*u.radian, 'x')
Пример #23
0
def _ecliptic_rotation_matrix():
    jd1, jd2 = get_jd12(J2000, J2000.scale)
    obl = _erfa.obl80(jd1, jd2) * u.radian
    assert obl.to(u.arcsec).value == 84381.448
    return rotation_matrix(obl, "x")
Пример #24
0
def _ecliptic_rotation_matrix():
    jd1, jd2 = get_jd12(J2000, J2000.scale)
    obl = erfa.obl80(jd1, jd2) * u.radian
    assert obl.to(u.arcsec).value == 84381.448
    return rotation_matrix(obl, "x")
Пример #25
0
def test_iau_fullstack(fullstack_icrs, fullstack_fiducial_altaz,
                       fullstack_times, fullstack_locations,
                       fullstack_obsconditions):
    """
    Test the full transform from ICRS <-> AltAz
    """

    # create the altaz frame
    altazframe = AltAz(obstime=fullstack_times,
                       location=fullstack_locations,
                       pressure=fullstack_obsconditions[0],
                       temperature=fullstack_obsconditions[1],
                       relative_humidity=fullstack_obsconditions[2],
                       obswl=fullstack_obsconditions[3])

    aacoo = fullstack_icrs.transform_to(altazframe)

    # compare aacoo to the fiducial AltAz - should always be different
    assert np.all(
        np.abs(aacoo.alt - fullstack_fiducial_altaz.alt) > 50 *
        u.milliarcsecond)
    assert np.all(
        np.abs(aacoo.az - fullstack_fiducial_altaz.az) > 50 * u.milliarcsecond)

    # if the refraction correction is included, we *only* do the comparisons
    # where altitude >5 degrees.  The SOFA guides imply that below 5 is where
    # where accuracy gets more problematic, and testing reveals that alt<~0
    # gives garbage round-tripping, and <10 can give ~1 arcsec uncertainty
    if fullstack_obsconditions[0].value == 0:
        # but if there is no refraction correction, check everything
        msk = slice(None)
        tol = 5 * u.microarcsecond
    else:
        msk = aacoo.alt > 5 * u.deg
        # most of them aren't this bad, but some of those at low alt are offset
        # this much.  For alt > 10, this is always better than 100 masec
        tol = 750 * u.milliarcsecond

    # now make sure the full stack round-tripping works
    icrs2 = aacoo.transform_to(ICRS)

    adras = np.abs(fullstack_icrs.ra - icrs2.ra)[msk]
    addecs = np.abs(fullstack_icrs.dec - icrs2.dec)[msk]
    assert np.all(adras < tol), 'largest RA change is {} mas, > {}'.format(
        np.max(adras.arcsec * 1000), tol)
    assert np.all(addecs < tol), 'largest Dec change is {} mas, > {}'.format(
        np.max(addecs.arcsec * 1000), tol)

    # check that we're consistent with the ERFA alt/az result
    iers_tab = iers.earth_orientation_table.get()
    xp, yp = u.Quantity(iers_tab.pm_xy(fullstack_times)).to_value(u.radian)
    lon = fullstack_locations.geodetic[0].to_value(u.radian)
    lat = fullstack_locations.geodetic[1].to_value(u.radian)
    height = fullstack_locations.geodetic[2].to_value(u.m)
    jd1, jd2 = get_jd12(fullstack_times, 'utc')
    pressure = fullstack_obsconditions[0].to_value(u.hPa)
    temperature = fullstack_obsconditions[1].to_value(u.deg_C)
    # Relative humidity can be a quantity or a number.
    relative_humidity = u.Quantity(fullstack_obsconditions[2], u.one).value
    obswl = fullstack_obsconditions[3].to_value(u.micron)
    astrom, eo = erfa.apco13(jd1, jd2, fullstack_times.delta_ut1_utc, lon, lat,
                             height, xp, yp, pressure, temperature,
                             relative_humidity, obswl)
    erfadct = _erfa_check(fullstack_icrs.ra.rad, fullstack_icrs.dec.rad,
                          astrom)
    npt.assert_allclose(erfadct['alt'], aacoo.alt.radian, atol=1e-7)
    npt.assert_allclose(erfadct['az'], aacoo.az.radian, atol=1e-7)
Пример #26
0
def _get_body_barycentric_posvel(body,
                                 time,
                                 ephemeris=None,
                                 get_velocity=True):
    """Calculate the barycentric position (and velocity) of a solar system body.

    Parameters
    ----------
    body : str or other
        The solar system body for which to calculate positions.  Can also be a
        kernel specifier (list of 2-tuples) if the ``ephemeris`` is a JPL
        kernel.
    time : `~astropy.time.Time`
        Time of observation.
    ephemeris : str, optional
        Ephemeris to use.  By default, use the one set with
        ``astropy.coordinates.solar_system_ephemeris.set``
    get_velocity : bool, optional
        Whether or not to calculate the velocity as well as the position.

    Returns
    -------
    position : `~astropy.coordinates.CartesianRepresentation` or tuple
        Barycentric (ICRS) position or tuple of position and velocity.

    Notes
    -----
    No velocity can be calculated with the built-in ephemeris for the Moon.

    Whether or not velocities are calculated makes little difference for the
    built-in ephemerides, but for most JPL ephemeris files, the execution time
    roughly doubles.
    """

    if ephemeris is None:
        ephemeris = solar_system_ephemeris.get()
        if ephemeris is None:
            raise ValueError(_EPHEMERIS_NOTE)
        kernel = solar_system_ephemeris.kernel
    else:
        kernel = _get_kernel(ephemeris)

    jd1, jd2 = get_jd12(time, 'tdb')
    if kernel is None:
        body = body.lower()
        earth_pv_helio, earth_pv_bary = erfa.epv00(jd1, jd2)
        if body == 'earth':
            body_pv_bary = earth_pv_bary

        elif body == 'moon':
            if get_velocity:
                raise KeyError("the Moon's velocity cannot be calculated with "
                               "the '{0}' ephemeris.".format(ephemeris))
            return calc_moon(time).cartesian

        else:
            sun_pv_bary = erfa.pvmpv(earth_pv_bary, earth_pv_helio)
            if body == 'sun':
                body_pv_bary = sun_pv_bary
            else:
                try:
                    body_index = PLAN94_BODY_NAME_TO_PLANET_INDEX[body]
                except KeyError:
                    raise KeyError(
                        "{0}'s position and velocity cannot be "
                        "calculated with the '{1}' ephemeris.".format(
                            body, ephemeris))
                body_pv_helio = erfa.plan94(jd1, jd2, body_index)
                body_pv_bary = erfa.pvppv(body_pv_helio, sun_pv_bary)

        body_pos_bary = CartesianRepresentation(body_pv_bary['p'],
                                                unit=u.au,
                                                xyz_axis=-1,
                                                copy=False)
        if get_velocity:
            body_vel_bary = CartesianRepresentation(body_pv_bary['v'],
                                                    unit=u.au / u.day,
                                                    xyz_axis=-1,
                                                    copy=False)

    else:
        if isinstance(body, str):
            # Look up kernel chain for JPL ephemeris, based on name
            try:
                kernel_spec = BODY_NAME_TO_KERNEL_SPEC[body.lower()]
            except KeyError:
                raise KeyError("{0}'s position cannot be calculated with "
                               "the {1} ephemeris.".format(body, ephemeris))
        else:
            # otherwise, assume the user knows what their doing and intentionally
            # passed in a kernel chain
            kernel_spec = body

        # jplephem cannot handle multi-D arrays, so convert to 1D here.
        jd1_shape = getattr(jd1, 'shape', ())
        if len(jd1_shape) > 1:
            jd1, jd2 = jd1.ravel(), jd2.ravel()
        # Note that we use the new jd1.shape here to create a 1D result array.
        # It is reshaped below.
        body_posvel_bary = np.zeros((2 if get_velocity else 1, 3) +
                                    getattr(jd1, 'shape', ()))
        for pair in kernel_spec:
            spk = kernel[pair]
            if spk.data_type == 3:
                # Type 3 kernels contain both position and velocity.
                posvel = spk.compute(jd1, jd2)
                if get_velocity:
                    body_posvel_bary += posvel.reshape(body_posvel_bary.shape)
                else:
                    body_posvel_bary[0] += posvel[:4]
            else:
                # spk.generate first yields the position and then the
                # derivative. If no velocities are desired, body_posvel_bary
                # has only one element and thus the loop ends after a single
                # iteration, avoiding the velocity calculation.
                for body_p_or_v, p_or_v in zip(body_posvel_bary,
                                               spk.generate(jd1, jd2)):
                    body_p_or_v += p_or_v

        body_posvel_bary.shape = body_posvel_bary.shape[:2] + jd1_shape
        body_pos_bary = CartesianRepresentation(body_posvel_bary[0],
                                                unit=u.km,
                                                copy=False)
        if get_velocity:
            body_vel_bary = CartesianRepresentation(body_posvel_bary[1],
                                                    unit=u.km / u.day,
                                                    copy=False)

    return (body_pos_bary, body_vel_bary) if get_velocity else body_pos_bary
Пример #27
0
def test_iau_fullstack(fullstack_icrs, fullstack_fiducial_altaz,
                       fullstack_times, fullstack_locations,
                       fullstack_obsconditions):
    """
    Test the full transform from ICRS <-> AltAz
    """

    # create the altaz frame
    altazframe = AltAz(obstime=fullstack_times, location=fullstack_locations,
                       pressure=fullstack_obsconditions[0],
                       temperature=fullstack_obsconditions[1],
                       relative_humidity=fullstack_obsconditions[2],
                       obswl=fullstack_obsconditions[3])

    aacoo = fullstack_icrs.transform_to(altazframe)

    # compare aacoo to the fiducial AltAz - should always be different
    assert np.all(np.abs(aacoo.alt - fullstack_fiducial_altaz.alt) > 50*u.milliarcsecond)
    assert np.all(np.abs(aacoo.az - fullstack_fiducial_altaz.az) > 50*u.milliarcsecond)

    # if the refraction correction is included, we *only* do the comparisons
    # where altitude >5 degrees.  The SOFA guides imply that below 5 is where
    # where accuracy gets more problematic, and testing reveals that alt<~0
    # gives garbage round-tripping, and <10 can give ~1 arcsec uncertainty
    if fullstack_obsconditions[0].value == 0:
        # but if there is no refraction correction, check everything
        msk = slice(None)
        tol = 5*u.microarcsecond
    else:
        msk = aacoo.alt > 5*u.deg
        # most of them aren't this bad, but some of those at low alt are offset
        # this much.  For alt > 10, this is always better than 100 masec
        tol = 750*u.milliarcsecond

    # now make sure the full stack round-tripping works
    icrs2 = aacoo.transform_to(ICRS)

    adras = np.abs(fullstack_icrs.ra - icrs2.ra)[msk]
    addecs = np.abs(fullstack_icrs.dec - icrs2.dec)[msk]
    assert np.all(adras < tol), 'largest RA change is {0} mas, > {1}'.format(np.max(adras.arcsec*1000), tol)
    assert np.all(addecs < tol), 'largest Dec change is {0} mas, > {1}'.format(np.max(addecs.arcsec*1000), tol)

    # check that we're consistent with the ERFA alt/az result
    xp, yp = u.Quantity(iers.IERS_Auto.open().pm_xy(fullstack_times)).to_value(u.radian)
    lon = fullstack_locations.geodetic[0].to_value(u.radian)
    lat = fullstack_locations.geodetic[1].to_value(u.radian)
    height = fullstack_locations.geodetic[2].to_value(u.m)
    jd1, jd2 = get_jd12(fullstack_times, 'utc')
    pressure = fullstack_obsconditions[0].to_value(u.hPa)
    temperature = fullstack_obsconditions[1].to_value(u.deg_C)
    # Relative humidity can be a quantity or a number.
    relative_humidity = u.Quantity(fullstack_obsconditions[2], u.one).value
    obswl = fullstack_obsconditions[3].to_value(u.micron)
    astrom, eo = erfa.apco13(jd1, jd2,
                             fullstack_times.delta_ut1_utc,
                             lon, lat, height,
                             xp, yp,
                             pressure, temperature, relative_humidity,
                             obswl)
    erfadct = _erfa_check(fullstack_icrs.ra.rad, fullstack_icrs.dec.rad, astrom)
    npt.assert_allclose(erfadct['alt'], aacoo.alt.radian, atol=1e-7)
    npt.assert_allclose(erfadct['az'], aacoo.az.radian, atol=1e-7)