Umbau auf server
This commit is contained in:
@@ -0,0 +1,373 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""An interface between Skyfield and the Python ``sgp4`` library."""
|
||||
|
||||
from numpy import (
|
||||
array, concatenate, identity, multiply, ones_like, repeat,
|
||||
)
|
||||
from sgp4 import omm
|
||||
from sgp4.api import SGP4_ERRORS, Satrec
|
||||
|
||||
from .constants import AU_KM, DAY_S, T0, tau
|
||||
from .functions import _T, mxm, mxv, rot_x, rot_y, rot_z
|
||||
from .searchlib import _find_discrete, find_maxima
|
||||
from .timelib import compute_calendar_date
|
||||
from .vectorlib import VectorFunction
|
||||
|
||||
_identity = identity(3)
|
||||
|
||||
class EarthSatellite(VectorFunction):
|
||||
"""An Earth satellite loaded from a TLE file and propagated with SGP4.
|
||||
|
||||
An earth satellite object is a Skyfield vector function, so you can
|
||||
either call its ``at()`` method to generate its position in the sky
|
||||
or else use addition and subtraction to combine it with other
|
||||
vectors.
|
||||
|
||||
Satellite parameters are generally only accurate for a week or two
|
||||
around the *epoch* of the parameters, the date for which they were
|
||||
generated, which is available as an attribute:
|
||||
|
||||
``epoch``
|
||||
A Skyfield :class:`~skyfield.timelib.Time` giving the exact
|
||||
epoch moment for these satellite orbit parameters.
|
||||
``name``
|
||||
Satellite name
|
||||
|
||||
When building a satellite, use the arguments ``line1`` and ``line2``
|
||||
to provide the two data lines from a TLE file as separate strings.
|
||||
Optional ``name`` lets you give a name to the satellite, accessible
|
||||
later through the ``name`` attribute. ``ts`` is a
|
||||
:class:`~skyfield.timelib.Timescale` object, used to generate the
|
||||
``epoch`` value; if it is not provided, the satellite will use a
|
||||
built in ``Timescale`` object.
|
||||
|
||||
If you are interested in the catalog entry details, the SGP4 model
|
||||
parameters for a particular satellite can be accessed through its
|
||||
``model`` attribute:
|
||||
|
||||
``model.satnum``
|
||||
The unique satellite NORAD catalog number given in the TLE file.
|
||||
``model.classification``
|
||||
Satellite classification, or else ``'U'`` for “Unknown”
|
||||
``model.intldesg``
|
||||
International designator
|
||||
``model.epochyr``
|
||||
Full four-digit year of this element set's epoch moment.
|
||||
``model.epochdays``
|
||||
Fractional days into the year of the epoch moment.
|
||||
``model.jdsatepoch``
|
||||
Julian date of the epoch (computed from ``epochyr`` and ``epochdays``).
|
||||
``model.ndot``
|
||||
First time derivative of the mean motion (ignored by SGP4).
|
||||
``model.nddot``
|
||||
Second time derivative of the mean motion (ignored by SGP4).
|
||||
``model.bstar``
|
||||
Ballistic drag coefficient B* in inverse earth radii.
|
||||
``model.ephtype``
|
||||
Ephemeris type (ignored by SGP4 as determination now automatic)
|
||||
``model.elnum``
|
||||
Element number
|
||||
``model.inclo``
|
||||
Inclination in radians.
|
||||
``model.nodeo``
|
||||
Right ascension of ascending node in radians.
|
||||
``model.ecco``
|
||||
Eccentricity.
|
||||
``model.argpo``
|
||||
Argument of perigee in radians.
|
||||
``model.mo``
|
||||
Mean anomaly in radians.
|
||||
``model.no_kozai``
|
||||
Mean motion in radians per minute.
|
||||
``model.revnum``
|
||||
Revolution number at epoch [Revs]
|
||||
|
||||
"""
|
||||
center = 399
|
||||
ts = None # see __init__()
|
||||
|
||||
def __init__(self, line1, line2, name=None, ts=None):
|
||||
if ts is None:
|
||||
ts = self.ts
|
||||
if ts is None:
|
||||
from .api import load # avoid import loop
|
||||
ts = EarthSatellite.ts = load.timescale()
|
||||
|
||||
self.name = None if name is None else name.strip()
|
||||
satrec = Satrec.twoline2rv(line1, line2)
|
||||
self.model = satrec
|
||||
|
||||
two_digit_year = satrec.epochyr
|
||||
if two_digit_year < 57:
|
||||
year = two_digit_year + 2000
|
||||
else:
|
||||
year = two_digit_year + 1900
|
||||
|
||||
self.epoch = ts.utc(year, 1, satrec.epochdays)
|
||||
|
||||
self._setup(satrec)
|
||||
|
||||
def _setup(self, satrec):
|
||||
# If only I had not made __init__() specific to TLE lines, but
|
||||
# had put them in an alternate construtor instead, this would
|
||||
# simply have lived in __init__(). Alas! I was so young then.
|
||||
|
||||
self.target = -100000 - satrec.satnum
|
||||
|
||||
@classmethod
|
||||
def from_satrec(cls, satrec, ts):
|
||||
"""Build an EarthSatellite from a raw sgp4 Satrec object.
|
||||
|
||||
This lets you provide raw numeric orbital elements instead of
|
||||
the text of a TLE set. See :ref:`from-satrec` for detais.
|
||||
|
||||
"""
|
||||
self = cls.__new__(cls)
|
||||
self.model = satrec
|
||||
self.name = None
|
||||
|
||||
# TODO: once sgp4 starts filling in epochyr and epochdays in
|
||||
# sgp4init(), the separate epoch code here and in __init__() can
|
||||
# be unified to always use epochyr and epochdays.
|
||||
whole, fraction = divmod(satrec.jdsatepoch, 1.0)
|
||||
year, month, day = compute_calendar_date(whole)
|
||||
day += 0.5 # convert integer Julian day into Julian date float
|
||||
self.epoch = ts.utc(year, month, day + fraction + satrec.jdsatepochF)
|
||||
|
||||
self._setup(satrec)
|
||||
return self
|
||||
|
||||
@classmethod
|
||||
def from_omm(cls, ts, element_dict):
|
||||
"""Build an EarthSatellite from OMM text fields.
|
||||
|
||||
Provide a ``ts`` timescale object, and a Python dict of OMM
|
||||
field names and values. The timescale is used to build the
|
||||
satellite's ``.epoch`` time.
|
||||
|
||||
"""
|
||||
self = cls.__new__(cls)
|
||||
self.name = element_dict.get('OBJECT_NAME', None)
|
||||
self.model = satrec = Satrec()
|
||||
omm.initialize(satrec, element_dict)
|
||||
self.epoch = ts._utc_jd(satrec.jdsatepoch, satrec.jdsatepochF)
|
||||
self._setup(satrec)
|
||||
return self
|
||||
|
||||
def __str__(self):
|
||||
return self.target_name
|
||||
|
||||
@property
|
||||
def target_name(self):
|
||||
return '{0}{1}catalog #{2} epoch {3}'.format(
|
||||
self.name or '',
|
||||
' ' if self.name else '',
|
||||
self.model.satnum,
|
||||
self.epoch.utc_strftime(),
|
||||
)
|
||||
|
||||
def _position_and_velocity_TEME_km(self, t):
|
||||
"""Return the raw true equator mean equinox (TEME) vectors from SGP4.
|
||||
|
||||
Returns a tuple of NumPy arrays ``([x y z], [xdot ydot zdot])``
|
||||
expressed in kilometers and kilometers per second. Note that we
|
||||
assume the TLE epoch to be a UTC date, per AIAA 2006-6753.
|
||||
|
||||
"""
|
||||
sat = self.model
|
||||
jd = t.whole
|
||||
fraction = t.tai_fraction - t._leap_seconds() / DAY_S
|
||||
|
||||
if getattr(jd, 'shape', None):
|
||||
e, r, v = sat.sgp4_array(jd, fraction)
|
||||
messages = [SGP4_ERRORS[error] if error else None for error in e]
|
||||
return r.T, v.T, messages
|
||||
else:
|
||||
error, position, velocity = sat.sgp4(jd, fraction)
|
||||
message = SGP4_ERRORS[error] if error else None
|
||||
return array(position), array(velocity), message
|
||||
|
||||
def ITRF_position_velocity_error(self, t):
|
||||
"""Deprecated: use the TEME and ITRS frame objects instead."""
|
||||
# TODO: can we teach frame objects to figure out that the
|
||||
# transform TEME -> ITRS can not only skip the t.M rotation, but
|
||||
# can also subtract the angles of their two competing z-axis
|
||||
# rotations and call rot_z() only once instead of twice?
|
||||
rTEME, vTEME, error = self._position_and_velocity_TEME_km(t)
|
||||
rTEME /= AU_KM
|
||||
vTEME /= AU_KM
|
||||
vTEME *= DAY_S
|
||||
rITRF, vITRF = TEME_to_ITRF(t.whole, rTEME, vTEME, 0.0, 0.0,
|
||||
t.ut1_fraction)
|
||||
return rITRF, vITRF, error
|
||||
|
||||
def _at(self, t):
|
||||
"""Compute this satellite's GCRS position and velocity at time `t`."""
|
||||
r, v, error = self._position_and_velocity_TEME_km(t)
|
||||
r /= AU_KM
|
||||
v /= AU_KM
|
||||
v *= DAY_S
|
||||
R = _T(TEME.rotation_at(t))
|
||||
r = mxv(R, r)
|
||||
v = mxv(R, v)
|
||||
return r, v, None, error
|
||||
|
||||
def find_events(self, topos, t0, t1, altitude_degrees=0.0):
|
||||
"""Return the times at which the satellite rises, culminates, and sets.
|
||||
|
||||
Searches between ``t0`` and ``t1``, which should each be a
|
||||
Skyfield :class:`~skyfield.timelib.Time` object, for passes of
|
||||
this satellite above the location ``topos`` that reach at least
|
||||
``altitude_degrees`` above the horizon.
|
||||
|
||||
Returns a tuple ``(t, events)`` whose first element is a
|
||||
:class:`~skyfield.timelib.Time` array and whose second element
|
||||
is an array of events:
|
||||
|
||||
* 0 — Satellite rose above ``altitude_degrees``.
|
||||
* 1 — Satellite culminated and started to descend again.
|
||||
* 2 — Satellite fell below ``altitude_degrees``.
|
||||
|
||||
Note that multiple culminations in a row are possible when,
|
||||
without setting, the satellite reaches a second peak altitude
|
||||
after descending partway down the sky from the first one.
|
||||
|
||||
"""
|
||||
# First, we find the moments of maximum altitude over the time
|
||||
# period. Some of these maxima will be negative, meaning the
|
||||
# satellite failed to crest the horizon.
|
||||
|
||||
ts = t0.ts
|
||||
at = (self - topos).at
|
||||
half_second = 0.5 / DAY_S
|
||||
orbits_per_minute = self.model.no_kozai / tau
|
||||
orbits_per_day = 24 * 60 * orbits_per_minute
|
||||
|
||||
# Note the protection against zero orbits_per_day.
|
||||
# TODO: why isn't 3 samples per orbit enough?
|
||||
step_days = 0.05 / max(orbits_per_day, 1.0)
|
||||
|
||||
# Long-period satellites might rise each day not because of
|
||||
# their own motion, but because the Earth rotates under them, so
|
||||
# check position at least each quarter-day. We might need to
|
||||
# tighten this even further if experience someday shows it
|
||||
# missing a pass of a particular satellite.
|
||||
if step_days > 0.25:
|
||||
step_days = 0.25
|
||||
|
||||
def cheat(t):
|
||||
"""Avoid computing expensive values that cancel out anyway."""
|
||||
t.gast = t.tt * 0.0
|
||||
t.M = t.MT = _identity
|
||||
|
||||
def altitude_at(t):
|
||||
cheat(t)
|
||||
return at(t).altaz()[0].degrees
|
||||
|
||||
altitude_at.step_days = step_days
|
||||
tmax, altitude = find_maxima(t0, t1, altitude_at, half_second, 12)
|
||||
|
||||
# Next, filter out the maxima that are not high enough.
|
||||
|
||||
keepers = altitude >= altitude_degrees
|
||||
jdmax = tmax.tt[keepers]
|
||||
ones = ones_like(jdmax, 'uint8')
|
||||
|
||||
# Finally, find the rising and setting that bracket each maximum
|
||||
# altitude. We guess that the satellite will be back below the
|
||||
# horizon in between each pair of adjancent maxima.
|
||||
|
||||
def below_horizon_at(t):
|
||||
cheat(t)
|
||||
return at(t).altaz()[0].degrees < altitude_degrees
|
||||
|
||||
# The `jdo` array are the times of maxima, with their averages
|
||||
# in between them. The start and end times are thrown in too,
|
||||
# in case a rising or setting is lingering out between a maxima
|
||||
# and the ends of our range. Could this perhaps still miss a
|
||||
# stubborn rising or setting near the ends?
|
||||
doublets = repeat(concatenate(((t0.tt,), jdmax, (t1.tt,))), 2)
|
||||
jdo = (doublets[:-1] + doublets[1:]) / 2.0
|
||||
|
||||
trs, rs = _find_discrete(t0.ts, jdo, below_horizon_at, half_second, 8)
|
||||
|
||||
jd = concatenate((jdmax, trs.tt))
|
||||
v = concatenate((ones, rs * 2))
|
||||
|
||||
i = jd.argsort()
|
||||
return ts.tt_jd(jd[i]), v[i]
|
||||
|
||||
class TEME(object):
|
||||
"""The satellite-specific True Equator Mean Equinox frame of reference.
|
||||
|
||||
This TEME frame is used to measure right ascension and declination,
|
||||
and is the reference frame of the SGP4 Earth satellite orbit model.
|
||||
It is a bit quirky. Instead measuring right ascension from the true
|
||||
vernal equinox point, it uses the ‘mean’ equniox that considers only
|
||||
precession but not nutation (the same equinox used for Greenwich
|
||||
Mean Sidereal Time). This made the reference frame more tractable
|
||||
for the 1970s computers that first implemented SGP4.
|
||||
|
||||
Defined in AIAA 2006-6753 Appendix C. See :ref:`reference_frames`
|
||||
for a guide to using Skyfield reference frames like this one.
|
||||
|
||||
"""
|
||||
@staticmethod
|
||||
def rotation_at(t):
|
||||
theta, theta_dot = theta_GMST1982(t.whole, t.ut1_fraction)
|
||||
angle = theta - t.gast / 24.0 * tau
|
||||
return mxm(rot_z(angle), t.M)
|
||||
|
||||
# TODO: Are there any applications that will need us to include the
|
||||
# tiny affect on velocity of the rate of change of the difference
|
||||
# between GMST1982 and GAST?
|
||||
|
||||
def theta_GMST1982(jd_ut1, fraction_ut1=0.0):
|
||||
"""Return the angle of Greenwich Mean Standard Time 1982 given the JD.
|
||||
|
||||
This angle defines the difference between the idiosyncratic True
|
||||
Equator Mean Equinox (TEME) frame of reference used by SGP4 and the
|
||||
more standard Pseudo Earth Fixed (PEF) frame of reference. The UT1
|
||||
time should be provided as a Julian date. Theta is returned in
|
||||
radians, and its velocity in radians per day of UT1 time.
|
||||
|
||||
From AIAA 2006-6753 Appendix C.
|
||||
|
||||
"""
|
||||
t = (jd_ut1 - T0 + fraction_ut1) / 36525.0
|
||||
g = 67310.54841 + (8640184.812866 + (0.093104 + (-6.2e-6) * t) * t) * t
|
||||
dg = 8640184.812866 + (0.093104 * 2.0 + (-6.2e-6 * 3.0) * t) * t
|
||||
theta = (jd_ut1 % 1.0 + fraction_ut1 + g / DAY_S % 1.0) % 1.0 * tau
|
||||
theta_dot = (1.0 + dg / (DAY_S * 36525.0)) * tau
|
||||
return theta, theta_dot
|
||||
|
||||
_zero_zero_minus_one = array((0.0, 0.0, -1.0))
|
||||
_cross120 = array((1,2,0))
|
||||
_cross201 = array((2,0,1))
|
||||
|
||||
def _cross(a, b):
|
||||
# Nearly 4x speedup over numpy cross(). TODO: Maybe move to .functions?
|
||||
return a[_cross120] * b[_cross201] - a[_cross201] * b[_cross120]
|
||||
|
||||
def TEME_to_ITRF(jd_ut1, rTEME, vTEME, xp=0.0, yp=0.0, fraction_ut1=0.0):
|
||||
"""Deprecated: use the TEME and ITRS frame objects instead."""
|
||||
theta, theta_dot = theta_GMST1982(jd_ut1, fraction_ut1)
|
||||
angular_velocity = multiply.outer(_zero_zero_minus_one, theta_dot)
|
||||
|
||||
R = rot_z(-theta)
|
||||
|
||||
if len(rTEME.shape) == 1:
|
||||
rPEF = (R).dot(rTEME)
|
||||
vPEF = (R).dot(vTEME) + _cross(angular_velocity, rPEF)
|
||||
else:
|
||||
rPEF = mxv(R, rTEME)
|
||||
vPEF = mxv(R, vTEME) + _cross(angular_velocity, rPEF)
|
||||
|
||||
if xp == 0.0 and yp == 0.0:
|
||||
rITRF = rPEF
|
||||
vITRF = vPEF
|
||||
else:
|
||||
W = (rot_x(yp)).dot(rot_y(xp))
|
||||
rITRF = (W).dot(rPEF)
|
||||
vITRF = (W).dot(vPEF)
|
||||
return rITRF, vITRF
|
||||
Reference in New Issue
Block a user