Пример #1
0
def test_straight_overhead():
    """
    With a precise CIRS<->AltAz transformation this should give Alt=90 exactly

    If the CIRS self-transform breaks it won't, due to improper treatment of aberration
    """
    t = Time('J2010')
    obj = EarthLocation(-1 * u.deg, 52 * u.deg, height=10. * u.km)
    home = EarthLocation(-1 * u.deg, 52 * u.deg, height=0. * u.km)

    # An object that appears straight overhead - FOR A GEOCENTRIC OBSERVER.
    # Note, this won't be overhead for a topocentric observer because of
    # aberration.
    cirs_geo = obj.get_itrs(t).transform_to(CIRS(obstime=t))

    # now get the Geocentric CIRS position of observatory
    obsrepr = home.get_itrs(t).transform_to(CIRS(obstime=t)).cartesian

    # topocentric CIRS position of a straight overhead object
    cirs_repr = cirs_geo.cartesian - obsrepr

    # create a CIRS object that appears straight overhead for a TOPOCENTRIC OBSERVER
    topocentric_cirs_frame = CIRS(obstime=t, location=home)
    cirs_topo = topocentric_cirs_frame.realize_frame(cirs_repr)

    # Check AltAz (though Azimuth can be anything so is not tested).
    aa = cirs_topo.transform_to(AltAz(obstime=t, location=home))
    assert_allclose(aa.alt, 90 * u.deg, atol=1 * u.uas, rtol=0)

    # Check HADec.
    hd = cirs_topo.transform_to(HADec(obstime=t, location=home))
    assert_allclose(hd.ha, 0 * u.hourangle, atol=1 * u.uas, rtol=0)
    assert_allclose(hd.dec, 52 * u.deg, atol=1 * u.uas, rtol=0)
Пример #2
0
def test_regression_simple_5133():
    """
    Simple test to check if alt-az calculations respect height of observer

    Because ITRS is geocentric and includes aberration, an object that
    appears 'straight up' to a geocentric observer (ITRS) won't be
    straight up to a topocentric observer - see

    https://github.com/astropy/astropy/issues/10983

    This is why we construct a topocentric GCRS SkyCoord before calculating AltAz
    """
    t = Time('J2010')
    obj = EarthLocation(-1 * u.deg, 52 * u.deg, height=[10., 0.] * u.km)
    home = EarthLocation(-1 * u.deg, 52 * u.deg, height=5. * u.km)

    obsloc_gcrs, obsvel_gcrs = home.get_gcrs_posvel(t)
    gcrs_geo = obj.get_itrs(t).transform_to(GCRS(obstime=t))
    obsrepr = home.get_itrs(t).transform_to(GCRS(obstime=t)).cartesian
    topo_gcrs_repr = gcrs_geo.cartesian - obsrepr
    topocentric_gcrs_frame = GCRS(obstime=t,
                                  obsgeoloc=obsloc_gcrs,
                                  obsgeovel=obsvel_gcrs)
    gcrs_topo = topocentric_gcrs_frame.realize_frame(topo_gcrs_repr)
    aa = gcrs_topo.transform_to(AltAz(obstime=t, location=home))

    # az is more-or-less undefined for straight up or down
    assert_quantity_allclose(aa.alt, [90, -90] * u.deg, rtol=1e-7)
    assert_quantity_allclose(aa.distance, 5 * u.km)
Пример #3
0
def iod_obs_tod(location: EarthLocation, epoch: Time):
    """
    Get the true of date (tod) position-velocity vector of the observatory

    Parameters
    ----------
    location: `~astropy.coordinates.EarthLocation`
        Position vector in body fixed reference frame.
    epoch: `~astropy.time.Time`

    Returns
    -------
    pos, vel: ~astropy.units.Quantity
        Position and velocity vector of the observation, in tod frame

    """
    from astropy.coordinates.earth import OMEGA_EARTH
    from astropy import _erfa as erfa
    assert isinstance(location, EarthLocation)
    assert isinstance(epoch, Time)

    dut = epoch.get_delta_ut1_utc(iers_table=iers_table)
    ut1 = Time(epoch.mjd + dut.to_value('day'), format='mjd', scale='ut1')
    earth_rotation_angle = erfa.era00(ut1.jd1, ut1.jd2) * u.rad
    loc = location.get_itrs()
    unit = loc.x.unit
    loc = np.array([loc.x.value, loc.y.value, loc.z.value])
    pos = np.matmul(rotation_matrix(-earth_rotation_angle, 'z'), loc)
    vel = [-OMEGA_EARTH.value * pos[1], OMEGA_EARTH.value * pos[0], 0.0]
    acc = [-OMEGA_EARTH.value**2 * pos[0], -OMEGA_EARTH.value**2 * pos[1], 0.0]

    return pos * unit, \
           vel * unit * OMEGA_EARTH.unit, \
           acc * unit * OMEGA_EARTH.unit * OMEGA_EARTH.unit
Пример #4
0
def iod_obs_tod(location: EarthLocation, epoch: Time):
    """
    Get the true of date (tod) position-velocity vector of the observatory

    Parameters
    ----------
    location: `~astropy.coordinates.EarthLocation`
        Position vector in body fixed reference frame.
    epoch: `~astropy.time.Time`

    Returns
    -------
    pos, vel: ~astropy.units.Quantity
        Position and velocity vector of the observation, in tod frame

    """

    from astropy.coordinates.earth import OMEGA_EARTH
    import nessan_fa as fa
    assert isinstance(location, EarthLocation)
    assert isinstance(epoch, Time)
    gmst, gast = fa.greenwich_sidereal_time(epoch)
    loc = location.get_itrs()
    unit = loc.x.unit
    loc = np.array([loc.x.value, loc.y.value, loc.z.value])

    pos = np.matmul(rotation_matrix(-gast, 'z'), loc)
    vel = [-OMEGA_EARTH.value * pos[1], OMEGA_EARTH.value * pos[0], 0.0]
    acc = [-OMEGA_EARTH.value**2 * pos[0], -OMEGA_EARTH.value**2 * pos[1], 0.0]

    return pos * unit, \
           vel * unit * OMEGA_EARTH.unit, \
           acc * unit * OMEGA_EARTH.unit * OMEGA_EARTH.unit
Пример #5
0
def test_regression_simple_5133():
    t = Time('J2010')
    obj = EarthLocation(-1*u.deg, 52*u.deg, height=[100., 0.]*u.km)
    home = EarthLocation(-1*u.deg, 52*u.deg, height=10.*u.km)
    aa = obj.get_itrs(t).transform_to(AltAz(obstime=t, location=home))

    # az is more-or-less undefined for straight up or down
    assert_quantity_allclose(aa.alt, [90, -90]*u.deg, rtol=1e-5)
    assert_quantity_allclose(aa.distance, [90, 10]*u.km)
Пример #6
0
def test_regression_simple_5133():
    t = Time('J2010')
    obj = EarthLocation(-1*u.deg, 52*u.deg, height=[100., 0.]*u.km)
    home = EarthLocation(-1*u.deg, 52*u.deg, height=10.*u.km)
    aa = obj.get_itrs(t).transform_to(AltAz(obstime=t, location=home))

    # az is more-or-less undefined for straight up or down
    assert_quantity_allclose(aa.alt, [90, -90]*u.deg, rtol=1e-5)
    assert_quantity_allclose(aa.distance, [90, 10]*u.km)
Пример #7
0
def geoTopoVector(longitude, latitude, elevation, jd):

    loc = EarthLocation(longitude, latitude, elevation)

    time = Time(jd, scale='utc', format='jd')
    itrs = loc.get_itrs(obstime=time)
    gcrs = itrs.transform_to(GCRS(obstime=time))

    r = gcrs.cartesian

    # convert from m to km
    x = r.x.value / 1000.0
    y = r.y.value / 1000.0
    z = r.z.value / 1000.0

    return x, y, z
Пример #8
0
 def make_map(self):
     # still in beta version, use with caution
     # ref : https://gist.github.com/hayesla/42596c72ab686171fe516f9ab43300e2
     hdu = fits.open(self.fname)
     header = hdu[0].header
     data = np.squeeze(hdu[0].data)
     data = np.squeeze(hdu[0].data)
     obstime = Time(header['date-obs'])
     frequency = header['crval3'] * u.Hz
     reference_coord = SkyCoord(header['crval1'] * u.deg,
                                header['crval2'] * u.deg,
                                frame='gcrs',
                                obstime=obstime,
                                distance=sun_coord.earth_distance(obstime),
                                equinox='J2000')
     lofar_loc = EarthLocation(lat=52.905329712 * u.deg,
                               lon=6.867996528 *
                               u.deg)  # location of the center of LOFAR
     lofar_coord = SkyCoord(lofar_loc.get_itrs(Time(obstime)))
     reference_coord_arcsec = reference_coord.transform_to(
         frames.Helioprojective(observer=lofar_coord))
     cdelt1 = (np.abs(header['cdelt1']) * u.deg).to(u.arcsec)
     cdelt2 = (np.abs(header['cdelt2']) * u.deg).to(u.arcsec)
     P1 = sun_coord.P(obstime)
     new_header = sunpy.map.make_fitswcs_header(
         data,
         reference_coord_arcsec,
         reference_pixel=u.Quantity(
             [header['crpix1'] - 1, header['crpix2'] - 1] * u.pixel),
         scale=u.Quantity([cdelt1, cdelt2] * u.arcsec / u.pix),
         rotation_angle=-P1,
         wavelength=frequency.to(u.MHz),
         observatory='LOFAR')
     lofar_map = sunpy.map.Map(data, new_header)
     lofar_map_rotate = lofar_map.rotate()
     bl = SkyCoord(-2500 * u.arcsec,
                   -2500 * u.arcsec,
                   frame=lofar_map_rotate.coordinate_frame)
     tr = SkyCoord(2500 * u.arcsec,
                   2500 * u.arcsec,
                   frame=lofar_map_rotate.coordinate_frame)
     lofar_submap = lofar_map_rotate.submap(bl, tr)
     return lofar_submap
Пример #9
0
 def setup(self):
     wht = EarthLocation(342.12*u.deg, 28.758333333333333*u.deg, 2327*u.m)
     self.obstime = Time("2013-02-02T23:00")
     self.wht_itrs = wht.get_itrs(obstime=self.obstime)
Пример #10
0
    def _get_components_and_classes(self):

        # The aim of this function is to return whatever is needed for
        # world_axis_object_components and world_axis_object_classes. It's easier
        # to figure it out in one go and then return the values and let the
        # properties return part of it.

        # Since this method might get called quite a few times, we need to cache
        # it. We start off by defining a hash based on the attributes of the
        # WCS that matter here (we can't just use the WCS object as a hash since
        # it is mutable)
        wcs_hash = (self.naxis, list(self.wcs.ctype), list(self.wcs.cunit),
                    self.wcs.radesys, self.wcs.specsys, self.wcs.equinox,
                    self.wcs.dateobs, self.wcs.lng, self.wcs.lat)

        # If the cache is present, we need to check that the 'hash' matches.
        if getattr(self, '_components_and_classes_cache', None) is not None:
            cache = self._components_and_classes_cache
            if cache[0] == wcs_hash:
                return cache[1]
            else:
                self._components_and_classes_cache = None

        # Avoid circular imports by importing here
        from astropy.wcs.utils import wcs_to_celestial_frame
        from astropy.coordinates import SkyCoord, EarthLocation
        from astropy.time.formats import FITS_DEPRECATED_SCALES
        from astropy.time import Time, TimeDelta

        components = [None] * self.naxis
        classes = {}

        # Let's start off by checking whether the WCS has a pair of celestial
        # components

        if self.has_celestial:

            try:
                celestial_frame = wcs_to_celestial_frame(self)
            except ValueError:
                # Some WCSes, e.g. solar, can be recognized by WCSLIB as being
                # celestial but we don't necessarily have frames for them.
                celestial_frame = None
            else:

                kwargs = {}
                kwargs['frame'] = celestial_frame
                kwargs['unit'] = u.deg

                classes['celestial'] = (SkyCoord, (), kwargs)

                components[self.wcs.lng] = ('celestial', 0,
                                            'spherical.lon.degree')
                components[self.wcs.lat] = ('celestial', 1,
                                            'spherical.lat.degree')

        # Next, we check for spectral components

        if self.has_spectral:

            # Find index of spectral coordinate
            ispec = self.wcs.spec
            ctype = self.wcs.ctype[ispec][:4]
            ctype = ctype.upper()

            kwargs = {}

            # Determine observer location and velocity

            # TODO: determine how WCS standard would deal with observer on a
            # spacecraft far from earth. For now assume the obsgeo parameters,
            # if present, give the geocentric observer location.

            if np.isnan(self.wcs.obsgeo[0]):
                observer = None
            else:

                earth_location = EarthLocation(*self.wcs.obsgeo[:3], unit=u.m)
                obstime = Time(self.wcs.mjdobs,
                               format='mjd',
                               scale='utc',
                               location=earth_location)
                observer_location = SkyCoord(
                    earth_location.get_itrs(obstime=obstime))

                if self.wcs.specsys in VELOCITY_FRAMES:
                    frame = VELOCITY_FRAMES[self.wcs.specsys]
                    observer = observer_location.transform_to(frame)
                    if isinstance(frame, str):
                        observer = attach_zero_velocities(observer)
                    else:
                        observer = update_differentials_to_match(
                            observer_location,
                            VELOCITY_FRAMES[self.wcs.specsys],
                            preserve_observer_frame=True)
                elif self.wcs.specsys == 'TOPOCENT':
                    observer = attach_zero_velocities(observer_location)
                else:
                    raise NotImplementedError(
                        f'SPECSYS={self.wcs.specsys} not yet supported')

            # Determine target

            # This is tricker. In principle the target for each pixel is the
            # celestial coordinates of the pixel, but we then need to be very
            # careful about SSYSOBS which is tricky. For now, we set the
            # target using the reference celestial coordinate in the WCS (if
            # any).

            if self.has_celestial and celestial_frame is not None:

                # NOTE: celestial_frame was defined higher up

                # NOTE: we set the distance explicitly to avoid warnings in SpectralCoord

                target = SkyCoord(self.wcs.crval[self.wcs.lng] *
                                  self.wcs.cunit[self.wcs.lng],
                                  self.wcs.crval[self.wcs.lat] *
                                  self.wcs.cunit[self.wcs.lat],
                                  frame=celestial_frame,
                                  distance=1000 * u.kpc)

                target = attach_zero_velocities(target)

            else:

                target = None

            # SpectralCoord does not work properly if either observer or target
            # are not convertible to ICRS, so if this is the case, we (for now)
            # drop the observer and target from the SpectralCoord and warn the
            # user.

            if observer is not None:
                try:
                    observer.transform_to(ICRS())
                except Exception:
                    warnings.warn(
                        'observer cannot be converted to ICRS, so will '
                        'not be set on SpectralCoord', AstropyUserWarning)
                    observer = None

            if target is not None:
                try:
                    target.transform_to(ICRS())
                except Exception:
                    warnings.warn(
                        'target cannot be converted to ICRS, so will '
                        'not be set on SpectralCoord', AstropyUserWarning)
                    target = None

            # NOTE: below we include Quantity in classes['spectral'] instead
            # of SpectralCoord - this is because we want to also be able to
            # accept plain quantities.

            if ctype == 'ZOPT':

                def spectralcoord_from_redshift(redshift):
                    return SpectralCoord((redshift + 1) * self.wcs.restwav,
                                         unit=u.m,
                                         observer=observer,
                                         target=target)

                def redshift_from_spectralcoord(spectralcoord):
                    # TODO: check target is consistent
                    if observer is None:
                        warnings.warn(
                            'No observer defined on WCS, SpectralCoord '
                            'will be converted without any velocity '
                            'frame change', AstropyUserWarning)
                        return spectralcoord.to_value(
                            u.m) / self.wcs.restwav - 1.
                    else:
                        return spectralcoord.in_observer_velocity_frame(
                            observer).to_value(u.m) / self.wcs.restwav - 1.

                classes['spectral'] = (u.Quantity, (), {},
                                       spectralcoord_from_redshift)
                components[self.wcs.spec] = ('spectral', 0,
                                             redshift_from_spectralcoord)

            elif ctype == 'BETA':

                def spectralcoord_from_beta(beta):
                    return SpectralCoord(beta * C_SI,
                                         unit=u.m / u.s,
                                         doppler_convention='relativistic',
                                         doppler_rest=self.wcs.restwav * u.m,
                                         observer=observer,
                                         target=target)

                def beta_from_spectralcoord(spectralcoord):
                    # TODO: check target is consistent
                    doppler_equiv = u.doppler_relativistic(self.wcs.restwav *
                                                           u.m)
                    if observer is None:
                        warnings.warn(
                            'No observer defined on WCS, SpectralCoord '
                            'will be converted without any velocity '
                            'frame change', AstropyUserWarning)
                        return spectralcoord.to_value(u.m / u.s,
                                                      doppler_equiv) / C_SI
                    else:
                        return spectralcoord.in_observer_velocity_frame(
                            observer).to_value(u.m / u.s, doppler_equiv) / C_SI

                classes['spectral'] = (u.Quantity, (), {},
                                       spectralcoord_from_beta)
                components[self.wcs.spec] = ('spectral', 0,
                                             beta_from_spectralcoord)

            else:

                kwargs['unit'] = self.wcs.cunit[ispec]

                if self.wcs.restfrq > 0:
                    if ctype == 'VELO':
                        kwargs['doppler_convention'] = 'relativistic'
                        kwargs['doppler_rest'] = self.wcs.restfrq * u.Hz
                    elif ctype == 'VRAD':
                        kwargs['doppler_convention'] = 'radio'
                        kwargs['doppler_rest'] = self.wcs.restfrq * u.Hz
                    elif ctype == 'VOPT':
                        kwargs['doppler_convention'] = 'optical'
                        kwargs['doppler_rest'] = self.wcs.restwav * u.m

                def spectralcoord_from_value(value):
                    return SpectralCoord(value,
                                         observer=observer,
                                         target=target,
                                         **kwargs)

                def value_from_spectralcoord(spectralcoord):
                    # TODO: check target is consistent
                    if observer is None:
                        warnings.warn(
                            'No observer defined on WCS, SpectralCoord '
                            'will be converted without any velocity '
                            'frame change', AstropyUserWarning)
                        return spectralcoord.to_value(**kwargs)
                    else:
                        return spectralcoord.in_observer_velocity_frame(
                            observer).to_value(**kwargs)

                classes['spectral'] = (u.Quantity, (), {},
                                       spectralcoord_from_value)
                components[self.wcs.spec] = ('spectral', 0,
                                             value_from_spectralcoord)

        # We can then make sure we correctly return Time objects where appropriate
        # (https://www.aanda.org/articles/aa/pdf/2015/02/aa24653-14.pdf)

        if 'time' in self.world_axis_physical_types:

            multiple_time = self.world_axis_physical_types.count('time') > 1

            for i in range(self.naxis):

                if self.world_axis_physical_types[i] == 'time':

                    if multiple_time:
                        name = f'time.{i}'
                    else:
                        name = 'time'

                    # Initialize delta
                    reference_time_delta = None

                    # Extract time scale
                    scale = self.wcs.ctype[i].lower()

                    if scale == 'time':
                        if self.wcs.timesys:
                            scale = self.wcs.timesys.lower()
                        else:
                            scale = 'utc'

                    # Drop sub-scales
                    if '(' in scale:
                        pos = scale.index('(')
                        scale, subscale = scale[:pos], scale[pos + 1:-1]
                        warnings.warn(
                            f'Dropping unsupported sub-scale '
                            f'{subscale.upper()} from scale {scale.upper()}',
                            UserWarning)

                    # TODO: consider having GPS as a scale in Time
                    # For now GPS is not a scale, we approximate this by TAI - 19s
                    if scale == 'gps':
                        reference_time_delta = TimeDelta(19, format='sec')
                        scale = 'tai'

                    elif scale.upper() in FITS_DEPRECATED_SCALES:
                        scale = FITS_DEPRECATED_SCALES[scale.upper()]

                    elif scale not in Time.SCALES:
                        raise ValueError(
                            f'Unrecognized time CTYPE={self.wcs.ctype[i]}')

                    # Determine location
                    trefpos = self.wcs.trefpos.lower()

                    if trefpos.startswith('topocent'):
                        # Note that some headers use TOPOCENT instead of TOPOCENTER
                        if np.any(np.isnan(self.wcs.obsgeo[:3])):
                            warnings.warn(
                                'Missing or incomplete observer location '
                                'information, setting location in Time to None',
                                UserWarning)
                            location = None
                        else:
                            location = EarthLocation(*self.wcs.obsgeo[:3],
                                                     unit=u.m)
                    elif trefpos == 'geocenter':
                        location = EarthLocation(0, 0, 0, unit=u.m)
                    elif trefpos == '':
                        location = None
                    else:
                        # TODO: implement support for more locations when Time supports it
                        warnings.warn(
                            f"Observation location '{trefpos}' is not "
                            "supported, setting location in Time to None",
                            UserWarning)
                        location = None

                    reference_time = Time(np.nan_to_num(self.wcs.mjdref[0]),
                                          np.nan_to_num(self.wcs.mjdref[1]),
                                          format='mjd',
                                          scale=scale,
                                          location=location)

                    if reference_time_delta is not None:
                        reference_time = reference_time + reference_time_delta

                    def time_from_reference_and_offset(offset):
                        if isinstance(offset, Time):
                            return offset
                        return reference_time + TimeDelta(offset, format='sec')

                    def offset_from_time_and_reference(time):
                        return (time - reference_time).sec

                    classes[name] = (Time, (), {},
                                     time_from_reference_and_offset)
                    components[i] = (name, 0, offset_from_time_and_reference)

        # Fallback: for any remaining components that haven't been identified, just
        # return Quantity as the class to use

        for i in range(self.naxis):
            if components[i] is None:
                name = self.wcs.ctype[i].split('-')[0].lower()
                if name == '':
                    name = 'world'
                while name in classes:
                    name += "_"
                classes[name] = (u.Quantity, (), {'unit': self.wcs.cunit[i]})
                components[i] = (name, 0, 'value')

        # Keep a cached version of result
        self._components_and_classes_cache = wcs_hash, (components, classes)

        return components, classes
 def setup(self):
     wht = EarthLocation(342.12*u.deg, 28.758333333333333*u.deg, 2327*u.m)
     self.obstime = Time("2013-02-02T23:00")
     self.wht_itrs = wht.get_itrs(obstime=self.obstime)
Пример #12
0
class Observer():
    __names = []

    def __init__(self, **kwargs):
        """ Defines the observer object

        Parameters:
            name (str): Name for the Observer. (required)
                Observer is uniquely defined (name must be different for each observer).
            code (str): The IAU code for SORA to search for its coordinates in MPC database
            site (EarthLocation): User provides an EarthLocation object.
            lon (str, float): The Longitude of the site in degrees.
                Positive to East. Range (0 to 360) or (-180 to +180)
                User can provide in degrees (float) or hexadecimal (string)
            lat (str, float): The Latitude of the site in degrees.
                Positive North. Range (+90 to -90)
                User can provide in degrees (float) or hexadecimal (string)
            height (int, float): The height of the site in meters above see level.

        Examples:
            User can provide one of the following to define an observer:
            - If user will use the MPC name for the site:
                Observer(code)
            - If user wants to use a different name from the MPC database:
                Observer(name, code)
            - If user wants to use an EarthLocation value:
                EarthLocation(lon, lat, height)
                Observer(name, site)
            - If user wants to give site coordinates directly:
                Observer(name, lon, lat, height)
        """
        input_tests.check_kwargs(
            kwargs,
            allowed_kwargs=['code', 'height', 'lat', 'lon', 'name', 'site'])
        if 'code' in kwargs:
            self.code = kwargs['code']
            try:
                name, self.site = search_code_mpc()[self.code]
                self.__name = kwargs.get('name', name)
            except:
                raise ValueError(
                    'code {} could not be located in MPC database'.format(
                        self.code))
        elif all(i in kwargs for i in ['name', 'site']):
            self.__name = kwargs['name']
            self.site = test_attr(kwargs['site'], EarthLocation, 'site')
        elif all(i in kwargs for i in ['name', 'lon', 'lat', 'height']):
            self.__name = kwargs['name']
            self.site = EarthLocation(kwargs['lon'], kwargs['lat'],
                                      kwargs['height'])
        else:
            raise ValueError('Input parameters could not be determined')
        if self.__name in self.__names:
            raise ValueError(
                "name {} already defined for another Observer object. "
                "Please choose a different one.".format(self.__name))
        self.__names.append(self.__name)

    def get_ksi_eta(self, time, star):
        """ Calculates relative position to star in the orthographic projection.

        Parameters:
            time (str, Time): Reference time to calculate the position.
                It can be a string in the format "yyyy-mm-dd hh:mm:ss.s" or an astropy Time object
            star (str, SkyCoord): The coordinate of the star in the same reference frame as the ephemeris.
                It can be a string in the format "hh mm ss.s +dd mm ss.ss"
                or an astropy SkyCoord object.

        Returns:
            ksi, eta (float): on-sky orthographic projection of the observer relative to a star
                Ksi is in the North-South direction (North positive)
                Eta is in the East-West direction (East positive)
        """
        time = test_attr(time, Time, 'time')
        try:
            star = SkyCoord(star, unit=(u.hourangle, u.deg))
        except:
            raise ValueError(
                'star is not an astropy object or a string in the format "hh mm ss.s +dd mm ss.ss"'
            )

        itrs = self.site.get_itrs(obstime=time)
        gcrs = itrs.transform_to(GCRS(obstime=time))
        rz = rotation_matrix(star.ra, 'z')
        ry = rotation_matrix(-star.dec, 'y')

        cp = gcrs.cartesian.transform(rz).transform(ry)
        return cp.y.to(u.km).value, cp.z.to(u.km).value

    def sidereal_time(self, time, mode='local'):
        """ Calculates the Apparent Sidereal Time at a reference time

        Parameters:
            time (str,Time): Reference time to calculate sidereal time.
            mode (str): local or greenwich
                If 'local': calculates the sidereal time for the coordinates of this object.
                If 'greenwich': calculates the Greenwich Apparent Sidereal Time.

        Returns:
            sidereal_time: An Astropy Longitude object with the Sidereal Time.
        """
        # return local or greenwich sidereal time
        time = test_attr(time, Time, 'time')
        time.location = self.site
        if mode == 'local':
            return time.sidereal_time('apparent')
        elif mode == 'greenwich':
            return time.sidereal_time('apparent', 'greenwich')
        else:
            raise ValueError('mode must be "local" or "greenwich"')

    def altaz(self, time, coord):
        """ Calculates the Altitude and Azimuth at a reference time for a coordinate

        Parameters:
            time (str,Time): Reference time to calculate the sidereal time.
            coord (str, astropy.SkyCoord): Coordinate of the target ICRS.

        Returns:
            altitude (float): object altitude in degrees.
            azimuth (float): object azimuth in degrees.
        """
        time = test_attr(time, Time, 'time')
        if type(coord) == str:
            coord = SkyCoord(coord, unit=(u.hourangle, u.deg))
        ephem_altaz = coord.transform_to(
            AltAz(obstime=time, location=self.site))
        return ephem_altaz.alt.deg, ephem_altaz.az.deg

    @property
    def name(self):
        return self.__name

    @property
    def lon(self):
        return self.site.lon

    @lon.setter
    def lon(self, lon):
        lat = self.site.lat
        height = self.site.height
        site = EarthLocation(lon, lat, height)
        self.site = site

    @property
    def lat(self):
        return self.site.lat

    @lat.setter
    def lat(self, lat):
        lon = self.site.lon
        height = self.site.height
        site = EarthLocation(lon, lat, height)
        self.site = site

    @property
    def height(self):
        return self.site.height

    @height.setter
    def height(self, height):
        lon = self.site.lon
        lat = self.site.lat
        site = EarthLocation(lon, lat, height)
        self.site = site

    def __str__(self):
        """ String representation of the Observer class
        """
        out = ('Site: {}\n'
               'Geodetic coordinates: Lon: {}, Lat: {}, height: {:.3f}'.format(
                   self.name, self.site.lon.__str__(), self.site.lat.__str__(),
                   self.site.height.to(u.km)))
        return out

    def __del__(self):
        """ When this object is deleted, it removes the name from the Class name list.
        """
        try:
            self.__names.remove(self.__name)
        except:
            pass