Files
skyview.astronomiemuseum.de/public/py/vendor/skyfield/toposlib.py
T

339 lines
13 KiB
Python
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""Classes that represent a ‘topocentric’ position on the Earth’s surface."""
from numpy import arctan2, array, array2string, cos, exp, sin, sqrt
from .constants import ANGVEL, DAY_S, RAD2DEG, T0, pi, tau
from .earthlib import refract
from .framelib import itrs
from .functions import (
_T, angular_velocity_matrix, mxm, mxv, rot_y, rot_z,
)
from .descriptorlib import reify
from .units import Angle, Distance, _ltude
from .vectorlib import VectorFunction
_EARTH_ANGULAR_VELOCITY_VECTOR = array((0, 0, DAY_S * ANGVEL))
_lat_options = {'precision': 4, 'floatmode': 'fixed', 'sign': '+',
'threshold': 5, 'edgeitems': 2}
_lon_options = {'precision': 4, 'floatmode': 'fixed',
'threshold': 5, 'edgeitems': 2}
_elev_options = {'precision': 1, 'floatmode': 'fixed',
'threshold': 5, 'edgeitems': 2}
class ITRSPosition(VectorFunction):
"""An |xyz| position in the Earth-centered Earth-fixed (ECEF) ITRS frame."""
center = 399
def __init__(self, itrs_xyz):
self.itrs_xyz = itrs_xyz
x, y, z = itrs_xyz.au
self._velocity_au_per_d = ANGVEL * DAY_S * array((-y, x, 0.0 * z))
@property
def target(self):
# When used as a vector function, this Earth geographic location
# computes positions from the Earth's center to itself. (This
# is a property, rather than an attribute, to avoid a circular
# reference that delays garbage collection.)
return self
def _at(self, t):
"""Compute GCRS position and velocity at time `t`."""
r = self.itrs_xyz.au
v = self._velocity_au_per_d
RT = _T(itrs.rotation_at(t))
r = mxv(RT, r)
v = mxv(RT, v)
return r, v, None, None
class GeographicPosition(ITRSPosition):
"""A latitude-longitude-elevation position on Earth.
Each instance of this class holds an |xyz| vector for a geographic
position on, above, or below the Earth’s surface, in the ITRS
reference frame: the international standard for an Earth-centered
Earth-fixed (ECEF) reference frame. Instead of instantiating this
class directly, Skyfield users usually give a reference geoid the
longitude and latitude they are interested in::
from skyfield.api import wgs84
topos = wgs84.latlon(37.3414, -121.6429)
Once a geographic position has been created, here are its attributes
and methods:
"""
vector_name = 'Geodetic'
def __init__(self, model, latitude, longitude, elevation, itrs_xyz):
super(GeographicPosition, self).__init__(itrs_xyz)
self.model = model
self.latitude = latitude
self.longitude = longitude
self.elevation = elevation
self._R_lat = _R_lat = rot_y(latitude.radians)[::-1]
self._R_latlon = mxm(_R_lat, rot_z(-longitude.radians))
@reify # not @property, so users have the option of overwriting it
def target_name(self):
return '{0} latitude {1} N longitude {2} E elevation {3} m'.format(
self.model.name,
array2string(self.latitude.degrees, **_lat_options),
array2string(self.longitude.degrees, **_lon_options),
array2string(self.elevation.m, **_elev_options))
def lst_hours_at(self, t):
"""Return the Local Apparent Sidereal Time, in hours, at time ``t``.
This location’s Local Apparent Sidereal Time (LAST) is the right
ascension of the zenith at the time ``t``, as measured against
the “true” Earth equator and equinox (rather than the fictional
“mean” equator and equinox, which ignore the Earth’s nutation).
"""
sprime = -47.0e-6 * (t.whole - T0 + t.tdb_fraction) / 36525.0
return (t.gast + self.longitude.hours + sprime / 54000.0) % 24.0
def refract(self, altitude_degrees, temperature_C, pressure_mbar):
"""Predict how the atmosphere will refract a position.
Given a body that is standing ``altitude_degrees`` above the
true horizon, return an ``Angle`` predicting its apparent
altitude given the supplied temperature and pressure, either of
which can be the string ``'standard'`` to use 10°C and a
pressure of 1010 mbar adjusted for the elevation of this
geographic location.
"""
if temperature_C == 'standard':
temperature_C = 10.0
if pressure_mbar == 'standard':
pressure_mbar = 1010.0 * exp(-self.elevation.m / 9.1e3)
alt = refract(altitude_degrees, temperature_C, pressure_mbar)
return Angle(degrees=alt)
def rotation_at(self, t):
"""Compute rotation from GCRS to this location’s altazimuth system."""
return mxm(self._R_latlon, itrs.rotation_at(t))
def _dRdt_times_RT_at(self, t):
# TODO: taking the derivative of the instantaneous angular
# velocity would provide a more accurate transform.
R = mxv(self._R_lat, _EARTH_ANGULAR_VELOCITY_VECTOR)
return angular_velocity_matrix(R)
class Geoid(object):
"""An Earth ellipsoid: maps latitudes and longitudes to |xyz| positions.
Instead of creating their own geoid object, most Skyfield users
simply use the `wgs84` object that comes built-in.
The math for turning a position into latitude and longitude is based
on Dr. T.S. Kelso's quite helpful article `Orbital Coordinate
Systems, Part III <https://www.celestrak.org/columns/v02n03/>`_.
"""
def __init__(self, name, radius_m, inverse_flattening):
self.name = name
self.radius = Distance(m=radius_m)
self.inverse_flattening = inverse_flattening
omf = (inverse_flattening - 1.0) / inverse_flattening
self._one_minus_flattening_squared = omf * omf
f = 1.0 / inverse_flattening
self._e2 = 2.0*f - f*f
@reify
def polar_radius(self):
"""The Earth’s polar radius, as a :class:`~skyfield.units.Distance`."""
return Distance(self.radius.au * (1.0 - 1.0 / self.inverse_flattening))
def latlon(self, latitude_degrees, longitude_degrees, elevation_m=0.0,
cls=GeographicPosition):
"""Return a `GeographicPosition` for a given latitude and longitude.
The longitude and latitude should both be specified in degrees.
If no elevation in meters is supplied, the returned position
will lie on the surface of the ellipsoid. Longitude is positive
towards the east, so supply a negative number for west::
from skyfield.api import wgs84
observatory = wgs84.latlon(37.3414, -121.6429) # 121.6° West
You can avoid remembering which directions are negative by using
Skyfield’s compass direction constants, which have the values +1
and −1::
from skyfield.api import N, S, E, W
observatory = wgs84.latlon(37.3414 * N, 121.6429 * W)
"""
latitude = Angle(degrees=latitude_degrees)
longitude = Angle(degrees=longitude_degrees)
elevation = Distance(m=elevation_m)
lat = latitude.radians
lon = longitude.radians
radius_au = self.radius.au
elevation_au = elevation.au
sinphi = sin(lat)
cosphi = cos(lat)
omf2 = self._one_minus_flattening_squared
c = 1.0 / sqrt(cosphi * cosphi + sinphi * sinphi * omf2)
s = omf2 * c
# At equator: 6378 km, the Earth's actual radius at the equator.
# At the pole: 6399 km, the Earth's radius of curvature at the pole.
radius_xy = radius_au * c
xy = (radius_xy + elevation_au) * cosphi
x = xy * cos(lon)
y = xy * sin(lon)
# At equator: 6335 km, the Earth's radius of curvature at the equator.
# At the pole: 6357 km, the Earth's actual radius at the pole.
radius_z = radius_au * s
z = (radius_z + elevation_au) * sinphi
r = array((x, y, z))
return cls(self, latitude, longitude, elevation, Distance(r))
def latlon_of(self, position):
"""Return the latitude and longitude of a ``position``.
The position’s ``.center`` must be 399, the center of the Earth.
Geodetic latitude and longitude are returned as a pair of
:class:`~skyfield.units.Angle` objects.
"""
xyz_au, x, y, R, aC, hyp, lat = self._compute_latitude(position)
lon = (arctan2(y, x) - pi) % tau - pi
return Angle(radians=lat), Angle(radians=lon)
def height_of(self, position):
"""Return the height above the Earth’s ellipsoid of a ``position``.
The position’s ``.center`` must be 399, the center of the Earth.
A :class:`~skyfield.units.Distance` is returned giving the
position’s geodetic height above the Earth’s surface.
"""
xyz_au, x, y, R, aC, hyp, lat = self._compute_latitude(position)
height_au = sqrt(hyp * hyp + R * R) - aC
return Distance(height_au)
def geographic_position_of(self, position):
"""Return the `GeographicPosition` of a ``position``.
The position’s ``.center`` must be 399, the center of the Earth.
A `GeographicPosition` is returned giving the position’s
geodetic ``latitude`` and ``longitude``, and an ``elevation``
above or below the surface of the ellipsoid.
"""
xyz_au, x, y, R, aC, hyp, lat = self._compute_latitude(position)
lon = (arctan2(y, x) - pi) % tau - pi
height_au = sqrt(hyp * hyp + R * R) - aC
return GeographicPosition(
latitude=Angle(radians=lat),
longitude=Angle(radians=lon),
elevation=Distance(height_au),
itrs_xyz=Distance(xyz_au),
model=self,
)
def subpoint_of(self, position):
"""Return the point on the ellipsoid directly below a ``position``.
The position’s ``.center`` must be 399, the center of the Earth.
Returns a `GeographicPosition` giving the geodetic ``latitude``
and ``longitude`` that lie directly below the input position,
and an ``elevation`` above the ellipsoid of zero.
"""
xyz_au, x, y, R, aC, hyp, lat = self._compute_latitude(position)
lon = (arctan2(y, x) - pi) % tau - pi
return self.latlon(lat * RAD2DEG, lon * RAD2DEG)
def _compute_latitude(self, position):
if position.center != 399:
raise ValueError(
'you can only calculate a geographic position from a'
' position which is geocentric (center=399), but this'
' position has a center of {0}'.format(position.center)
)
xyz_au = position.frame_xyz(itrs).au
x, y, z = xyz_au
a = self.radius.au
e2 = self._e2
R = sqrt(x*x + y*y)
lat = arctan2(z, R)
for iteration in 0,1,2:
sin_lat = sin(lat)
e2_sin_lat = e2 * sin_lat
# At 0°, aC = 6378 km, Earth's actual radius at the equator.
# At 90°, aC = 6399 km, Earth's radius of curvature at the pole.
aC = a / sqrt(1.0 - e2_sin_lat * sin_lat)
hyp = z + aC * e2_sin_lat
lat = arctan2(hyp, R)
return xyz_au, x, y, R, aC, hyp, lat
subpoint = geographic_position_of # deprecated method name
wgs84 = Geoid('WGS84', 6378137.0, 298.257223563)
iers2010 = Geoid('IERS2010', 6378136.6, 298.25642)
wgs84.__doc__ = """World Geodetic System 1984 `Geoid`.
This is the standard geoid used by the GPS system,
and is likely the standard that’s intended
if you are supplied a latitude and longitude
that don’t specify an alternative geoid.
"""
iers2010.__doc__ = 'International Earth Rotation Service 2010 `Geoid`.'
# Compatibility with old versions of Skyfield:
class Topos(GeographicPosition):
"""Deprecated: use ``wgs84.latlon()`` or ``iers2010.latlon()`` instead."""
def __init__(self, latitude=None, longitude=None, latitude_degrees=None,
longitude_degrees=None, elevation_m=0.0, x=0.0, y=0.0):
if latitude_degrees is not None:
pass
elif isinstance(latitude, Angle):
latitude_degrees = latitude.degrees
elif isinstance(latitude, (str, float, tuple)):
latitude_degrees = _ltude(latitude, 'latitude', 'N', 'S')
else:
raise TypeError('please provide either latitude_degrees=<float>'
' or latitude=<skyfield.units.Angle object>'
' with north being positive')
if longitude_degrees is not None:
pass
elif isinstance(longitude, Angle):
longitude_degrees = longitude.degrees
elif isinstance(longitude, (str, float, tuple)):
longitude_degrees = _ltude(longitude, 'longitude', 'E', 'W')
else:
raise TypeError('please provide either longitude_degrees=<float>'
' or longitude=<skyfield.units.Angle object>'
' with east being positive')
# Sneaky: the model thinks it's creating an object when really
# it's just calling our superclass __init__() for us. Alas, the
# crimes committed to avoid duplicating code! (This is actually
# quite clean compared to the other alternatives I tried.)
iers2010.latlon(latitude_degrees, longitude_degrees, elevation_m,
super(Topos, self).__init__)
self.R_lat = self._R_lat # On this old class, it was public.
def itrf_xyz(self):
return self.itrs_xyz