New Upstream Release - sgp4
Ready changes
Summary
Merged new upstream version: 2.21 (was: 2.15).
Resulting package
Built on 2022-05-21T02:15 (took 2m32s)
The resulting binary packages can be installed (if you have the apt repository enabled) by running one of:
apt install -t fresh-releases python3-sgp4
Lintian Result
Diff
diff --git a/PKG-INFO b/PKG-INFO
index d1daddb..718181d 100644
--- a/PKG-INFO
+++ b/PKG-INFO
@@ -1,7 +1,7 @@
Metadata-Version: 1.1
Name: sgp4
-Version: 2.15
-Summary: Track earth satellite TLE orbits using up-to-date 2020 version of SGP4
+Version: 2.21
+Summary: Track Earth satellites given TLE data, using up-to-date 2020 SGP4 routines.
Home-page: https://github.com/brandon-rhodes/python-sgp4
Author: Brandon Rhodes
Author-email: brandon@rhodesmill.org
@@ -17,14 +17,17 @@ Description:
satellites themselves deviate from the ideal orbits described in TLE
files.
- * If your platform supports it, this package compiles the verbatim
- source code from the official C++ version of SGP4. You can call the
- routine directly, or through an array API that loops over arrays of
- satellites and arrays of times with machine code instead of Python.
+ * If your platform supports it, this package compiles and uses the
+ verbatim source code from the official C++ version of SGP4.
* Otherwise, a slower but reliable Python implementation of SGP4 is used
instead.
+ * If, instead of asking for the position of a single satellite at a
+ single time, you supply this library with an array of satellites and
+ an array of times, then the arrays can be processed using machine code
+ instead of requiring you to run a slow Python loop over them.
+
Note that the SGP4 propagator returns raw *x,y,z* Cartesian coordinates
in a “True Equator Mean Equinox” (TEME) reference frame that’s centered
on the Earth but does not rotate with it — an “Earth centered inertial”
@@ -243,9 +246,9 @@ Description:
so if you only have one date, be sure to provide NumPy arrays of length
one. Here is a sample computation for 2 satellites and 4 dates:
- >>> s = '1 20580U 90037B 19342.88042116 .00000361 00000-0 11007-4 0 9996'
- >>> t = '2 20580 28.4682 146.6676 0002639 185.9222 322.7238 15.09309432427086'
- >>> satellite2 = Satrec.twoline2rv(s, t)
+ >>> u = '1 20580U 90037B 19342.88042116 .00000361 00000-0 11007-4 0 9996'
+ >>> w = '2 20580 28.4682 146.6676 0002639 185.9222 322.7238 15.09309432427086'
+ >>> satellite2 = Satrec.twoline2rv(u, w)
>>> from sgp4.api import SatrecArray
>>> a = SatrecArray([satellite, satellite2])
@@ -276,45 +279,6 @@ Description:
[-3.85 6.28 -1.85]
[-3.91 6.25 -1.83]]]
- Attributes
- ----------
-
- The attributes of a ``Satrec`` object carry the data loaded from the TLE
- entry.
- Most of this class's hundred-plus attributes are intermediate values
- of interest only to the propagation algorithm itself. Here are the
- attributes set by ``sgp4.io.twoline2rv()`` in which users are likely
- to be interested:
-
- ``satnum``
- Unique satellite number given in the TLE file.
- ``epochyr``
- Full four-digit year of this element set's epoch moment.
- ``epochdays``
- Fractional days into the year of the epoch moment.
- ``jdsatepoch``
- Julian date of the epoch (computed from ``epochyr`` and ``epochdays``).
- ``ndot``
- First time derivative of the mean motion (ignored by SGP4).
- ``nddot``
- Second time derivative of the mean motion (ignored by SGP4).
- ``bstar``
- Ballistic drag coefficient B* in inverse earth radii.
- ``inclo``
- Inclination in radians.
- ``nodeo``
- Right ascension of ascending node in radians.
- ``ecco``
- Eccentricity.
- ``argpo``
- Argument of perigee in radians.
- ``mo``
- Mean anomaly in radians.
- ``no_kozai``
- Mean motion in radians per minute.
-
- Look at the class's documentation for details.
-
Export
------
@@ -328,8 +292,14 @@ Description:
>>> line2
'2 25544 51.6439 211.2001 0007417 17.6667 85.6398 15.50103472202482'
- And another that produces the fields defined by the new OMM format (see
- the “OMM” section above):
+ Happily, these are exactly the two TLE lines that we used to create this
+ satellite object:
+
+ >>> (s == line1) and (t == line2)
+ True
+
+ Another export routine is available that produces the fields defined by
+ the new OMM format (see the “OMM” section above):
>>> from pprint import pprint
>>> fields = exporter.export_omm(satellite, 'ISS (ZARYA)')
@@ -380,42 +350,200 @@ Description:
Providing your own elements
---------------------------
- If instead of parsing a TLE you want to provide your own orbital
- elements, you can call the ``sgp4init()`` method of any existing
- satellite object to reset it to those new elements.
-
- >>> sat = Satrec()
- >>> sat.sgp4init(
- ... WGS72, # gravity model
- ... 'i', # 'a' = old AFSPC mode, 'i' = improved mode
- ... 5, # satnum: Satellite number
- ... 18441.785, # epoch: days since 1949 December 31 00:00 UT
- ... 2.8098e-05, # bstar: drag coefficient (/earth radii)
- ... 6.969196665e-13, # ndot: ballistic coefficient (revs/day)
- ... 0.0, # nddot: second derivative of mean motion (revs/day^3)
- ... 0.1859667, # ecco: eccentricity
- ... 5.7904160274885, # argpo: argument of perigee (radians)
- ... 0.5980929187319, # inclo: inclination (radians)
- ... 0.3373093125574, # mo: mean anomaly (radians)
- ... 0.0472294454407, # no_kozai: mean motion (radians/minute)
- ... 6.0863854713832, # nodeo: right ascension of ascending node (radians)
+ If instead of parsing a TLE you want to specify orbital elements
+ directly, you can pass them as floating point numbers to a satellite
+ object’s ``sgp4init()`` method. For example, here’s how to build the
+ same International Space Station orbit that we loaded from a TLE in the
+ first code example above:
+
+ >>> satellite2 = Satrec()
+ >>> satellite2.sgp4init(
+ ... WGS72, # gravity model
+ ... 'i', # 'a' = old AFSPC mode, 'i' = improved mode
+ ... 25544, # satnum: Satellite number
+ ... 25545.69339541, # epoch: days since 1949 December 31 00:00 UT
+ ... 3.8792e-05, # bstar: drag coefficient (1/earth radii)
+ ... 0.0, # ndot: ballistic coefficient (revs/day)
+ ... 0.0, # nddot: mean motion 2nd derivative (revs/day^3)
+ ... 0.0007417, # ecco: eccentricity
+ ... 0.3083420829620822, # argpo: argument of perigee (radians)
+ ... 0.9013560935706996, # inclo: inclination (radians)
+ ... 1.4946964807494398, # mo: mean anomaly (radians)
+ ... 0.06763602333248933, # no_kozai: mean motion (radians/minute)
+ ... 3.686137125541276, # nodeo: R.A. of ascending node (radians)
... )
- To compute the “epoch” value, simply take a normal Julian date and
- subtract ``2433281.5`` days.
+ These numbers don’t look the same as the numbers in the TLE, because the
+ underlying ``sgp4init()`` routine uses different units: radians rather
+ than degrees. But this is the same orbit and will produce the same
+ positions.
+
+ Note that ``ndot`` and ``nddot`` are ignored by the SGP4 propagator, so
+ you can leave them ``0.0`` without any effect on the resulting satellite
+ positions. But they do at least get saved to the satellite object, and
+ written out if you write the parameters to a TLE or OMM file (see the
+ “Export” section, above).
+
+ To compute the “epoch” argument, take the epoch’s Julian date and
+ subtract 2433281.5 days.
- In addition to setting the attributes natively set by the underlying
- ``sgp4init()`` routine, this library also goes ahead and sets the date
- fields ``epochyr``, ``epochdays``, ``jdsatepoch``, and ``jdsatepochF``.
+ While the underlying ``sgp4init()`` routine leaves the attributes
+ ``epochyr``, ``epochdays``, ``jdsatepoch``, and ``jdsatepochF`` unset,
+ this library goes ahead and sets them anyway for you, using the epoch
+ you provided.
- The character provided as the second argument can be ``'a'`` to run the
- computations so that they are compatible with the old Air Force Space
- Command edition of the library, or ``'i'`` to run the new and improved
- version of the SGP4 algorithm.
+ See the next section for the complete list of attributes that are
+ available from the satellite record once it has been initialized.
+
+ Attributes
+ ----------
- You can also directly access a satellite’s orbital parameters by asking
- for the attributes ``sat.epoch``, ``sat.bstar``, and so forth, using the
- names given in the comments above.
+ There are several dozen ``Satrec`` attributes that expose data from the
+ underlying C++ SGP4 record. They fall into the following categories.
+
+ *Identification*
+
+ These are copied directly from the TLE record but aren’t used by the
+ propagation math.
+
+ | ``satnum`` — Unique number assigned to the satellite.
+ | ``classification`` — ``'U'``, ``'C'``, or ``'S'``
+ indicating the element set is Unclassified, Classified, or Secret.
+ | ``ephtype`` — Integer “ephemeris type”, used internally by space
+ agencies to mark element sets that are not ready for publication;
+ this field should always be ``0`` in published TLEs.
+ | ``elnum`` — Element set number.
+ | ``revnum`` — Satellite’s revolution number at the moment of the epoch,
+ presumably counting from 1 following launch.
+
+ *Orbital Elements*
+
+ These are the orbital parameters, copied verbatim from the text of the
+ TLE record. They describe the orbit at the moment of the TLE’s epoch
+ and so remain constant even as the satellite record is used over and
+ over again to propagate positions for different times.
+
+ | ``epochyr`` — Epoch date: the last two digits of the year.
+ | ``epochdays`` — Epoch date: the number of days into the year,
+ including a decimal fraction for the UTC time of day.
+ | ``ndot`` — First time derivative of the mean motion
+ (loaded from the TLE, but otherwise ignored).
+ | ``nddot`` — Second time derivative of the mean motion
+ (loaded from the TLE, but otherwise ignored).
+ | ``bstar`` — Ballistic drag coefficient B* (1/earth radii).
+ | ``inclo`` — Inclination (radians).
+ | ``nodeo`` — Right ascension of ascending node (radians).
+ | ``ecco`` — Eccentricity.
+ | ``argpo`` — Argument of perigee (radians).
+ | ``mo`` — Mean anomaly (radians).
+ | ``no_kozai`` — Mean motion (radians/minute).
+ | ``no`` — Alias for ``no_kozai``, for compatibility with old code.
+
+ You can also access the epoch as a Julian date:
+
+ | ``jdsatepoch`` — Whole part of the epoch’s Julian date.
+ | ``jdsatepochF`` — Fractional part of the epoch’s Julian date.
+
+ *Computed Orbit Properties*
+
+ These are computed when the satellite is first loaded,
+ as a convenience for callers who might be interested in them.
+ They aren’t used by the SGP4 propagator itself.
+
+ | ``a`` — Semi-major axis (earth radii).
+ | ``altp`` — Altitude of the satellite at perigee
+ (earth radii, assuming a spherical Earth).
+ | ``alta`` — Altitude of the satellite at apogee
+ (earth radii, assuming a spherical Earth).
+ | ``argpdot`` — Rate at which the argument of perigee is changing
+ (radians/minute).
+ | ``gsto`` — Greenwich Sidereal Time at the satellite’s epoch (radians).
+ | ``mdot`` — Rate at which the mean anomaly is changing (radians/minute)
+ | ``nodedot`` — Rate at which the right ascension of the ascending node
+ is changing (radians/minute).
+
+ *Propagator Mode*
+
+ | ``operationmode`` — A single character that directs SGP4
+ to either operate in its modern ``'i'`` improved mode
+ or in its legacy ``'a'`` AFSPC mode.
+ | ``method`` — A single character, chosen automatically
+ when the orbital elements were loaded, that indicates whether SGP4
+ has chosen to use its built-in ``'n'`` Near Earth
+ or ``'d'`` Deep Space mode for this satellite.
+
+ *Result of Most Recent Propagation*
+
+ | ``t`` —
+ The time you gave when you most recently asked SGP4
+ to compute this satellite’s position,
+ measured in minutes before (negative) or after (positive)
+ the satellite’s epoch.
+ | ``error`` —
+ Error code produced by the most recent SGP4 propagation
+ you performed with this element set.
+
+ The possible ``error`` codes are:
+
+ 0. No error.
+ 1. Mean eccentricity is outside the range 0 ≤ e < 1.
+ 2. Mean motion has fallen below zero.
+ 3. Perturbed eccentricity is outside the range 0 ≤ e ≤ 1.
+ 4. Length of the orbit’s semi-latus rectum has fallen below zero.
+ 5. (No longer used.)
+ 6. Orbit has decayed: the computed position is underground.
+ (The position is still returned, in case the vector is helpful
+ to software that might be searching for the moment of re-entry.)
+
+ *Mean Elements From Most Recent Propagation*
+
+ Partway through each propagation, the SGP4 routine saves a set of
+ “singly averaged mean elements” that describe the orbit’s shape at the
+ moment for which a position is being computed. They are averaged with
+ respect to the mean anomaly and include the effects of secular gravity,
+ atmospheric drag, and — in Deep Space mode — of those pertubations from
+ the Sun and Moon that SGP4 averages over an entire revolution of each of
+ those bodies. They omit both the shorter-term and longer-term periodic
+ pertubations from the Sun and Moon that SGP4 applies right before
+ computing each position.
+
+ | ``am`` — Average semi-major axis (earth radii).
+ | ``em`` — Average eccentricity.
+ | ``im`` — Average inclination (radians).
+ | ``Om`` — Average right ascension of ascending node (radians).
+ | ``om`` — Average argument of perigee (radians).
+ | ``mm`` — Average mean anomaly (radians).
+ | ``nm`` — Average mean motion (radians/minute).
+
+ *Gravity Model Parameters*
+
+ When the satellite record is initialized, your choice of gravity model
+ results in a slate of eight constants being copied in:
+
+ | ``tumin`` — Minutes in one “time unit”.
+ | ``xke`` — The reciprocal of ``tumin``.
+ | ``mu`` — Earth’s gravitational parameter (km³/s²).
+ | ``radiusearthkm`` — Radius of the earth (km).
+ | ``j2``, ``j3``, ``j4`` — Un-normalized zonal harmonic values J₂, J₃, and J₄.
+ | ``j3oj2`` — The ratio J₃/J₂.
+
+ Printing satellite attributes
+ -----------------------------
+
+ If you want to print out a satellite, this library provides a convenient
+ “attribute dump” routine that takes a satellite and generates lines that
+ list its attributes::
+
+ from sys import stdout
+ from sgp4.conveniences import dump_satrec
+
+ stdout.writelines(dump_satrec(satellite))
+
+ If you want to compare two satellites, then simply pass a second
+ argument; the second satellite’s attributes will be printed in a second
+ column next to those of the first. ::
+
+ stdout.writelines(dump_satrec(satellite, satellite2))
Validation against the official algorithm
-----------------------------------------
@@ -463,6 +591,53 @@ Description:
Changelog
---------
+ 2022-04-06 — 2.21
+
+ * Added ``dump_satrec()`` to the ``sgp4.conveniences`` module.
+
+ * Fixed the ``Satrec`` attribute ``.error``, which was previously
+ building a nonsense integer from the wrong data in memory.
+
+ * Removed ``.whichconst`` from Python ``Satrec``, to help users avoid
+ writing code that will break when the C++ extension is available.
+
+ 2021-07-01 — 2.20
+
+ * Taught ``sgp4init()`` to round both ``epochdays`` and ``jdsatepochF``
+ to the same 8 decimal places used for the date fraction in a TLE, if
+ the user-supplied ``epoch`` itself has 8 or fewer digits behind the
+ decimal point. This should make it easier to build satellites that
+ round-trip to TLE format with perfect accuracy.
+
+ * Fixed how ``export_tle()`` formats the BSTAR field when its value, if
+ written in scientific notation, has a positive exponent.
+
+ * Fixed the ``epochyr`` assigned by ``sgp4init()`` so years before 2000
+ have two digits instead of three (for example, so that 1980 produces
+ an ``epochyr`` of 80 instead of 980).
+
+ 2021-04-22 — 2.19
+
+ * Extended the documentation on the Python Package Index and in the
+ module docstring so it lists every ``Satrec`` attribute that this
+ library exposes; even the more obscure ones might be useful to folks
+ working to analyze satellite orbits.
+
+ 2021-03-08 — 2.18
+
+ * If a TLE satellite number lacks the required 5 digits,
+ ``twoline2rv()`` now gives the underlying C++ library a little help so
+ it can still parse the classification and international designator
+ correctly.
+
+ * The ``Satrec`` attributes ``jdsatepoch``, ``jdsatepochF``,
+ ``epochyr``, and ``epochdays`` are now writeable, so users can adjust
+ their values manually — which should make up for the fact that the
+ ``sgp4init()`` method can’t set them with full floating point
+ precision.
+
+ | 2021-02-17 — 2.17 — Fixed where in the output array the ``sgp4_array()`` method writes NaN values when an SGP4 propagation fails.
+ | 2021-02-12 — 2.16 — Fixed ``days2mdhms()`` rounding to always match TLE epoch.
| 2021-01-08 — 2.15 — Fixed parsing of the ``satnum`` TLE field in the Python fallback code, when the field has a leading space; added OMM export routine.
| 2020-12-16 — 2.14 — New data formats: added OMM message support for both XML and CSV, and added support for the new Alpha-5 extension to TLE files.
| 2020-10-14 — 2.13 — Enhanced ``sgp4init()`` with custom code that also sets the ``epochdays`` and ``epochyr`` satellite attributes.
diff --git a/debian/changelog b/debian/changelog
index c358e89..8ab5337 100644
--- a/debian/changelog
+++ b/debian/changelog
@@ -1,3 +1,9 @@
+sgp4 (2.21-1) UNRELEASED; urgency=low
+
+ * New upstream release.
+
+ -- Debian Janitor <janitor@jelmer.uk> Sat, 21 May 2022 02:12:48 -0000
+
sgp4 (2.15-0.1) unstable; urgency=medium
* Non-maintainer upload.
diff --git a/extension/wrapper.cpp b/extension/wrapper.cpp
index be7327d..191be7b 100644
--- a/extension/wrapper.cpp
+++ b/extension/wrapper.cpp
@@ -74,12 +74,13 @@ _vectorized_sgp4(PyObject *args, elsetrec *raw_satrec_array, int imax)
for (Py_ssize_t j=0; j < jmax; j++) {
double t = (jd[j] - satrec.jdsatepoch) * 1440.0
+ (fr[j] - satrec.jdsatepochF) * 1440.0;
- Py_ssize_t k = i * jmax + j;
- SGP4Funcs::sgp4(satrec, t, r + k*3, v + k*3);
+ Py_ssize_t k1 = i * jmax + j;
+ Py_ssize_t k3 = 3 * k1;
+ SGP4Funcs::sgp4(satrec, t, r + k3, v + k3);
+ e[k1] = (uint8_t) satrec.error;
if (satrec.error && satrec.error < 6) {
- r[k] = r[k+1] = r[k+2] = v[k] = v[k+1] = v[k+2] = NAN;
+ r[k3] = r[k3+1] = r[k3+2] = v[k3] = v[k3+1] = v[k3+2] = NAN;
}
- e[k] = (uint8_t) satrec.error;
}
}
}
@@ -119,6 +120,11 @@ Satrec_twoline2rv(PyTypeObject *cls, PyObject *args)
line1[68] = '\0';
line2[68] = '\0';
+ /* Allocate the new object. */
+ SatrecObject *self = (SatrecObject*) cls->tp_alloc(cls, 0);
+ if (!self)
+ return NULL;
+
/* Correct for locales that use a comma as the decimal point, since
users report that the scanf() function on macOS is sensitive to
locale when parsing floats. This operation is not thread-safe,
@@ -131,10 +137,18 @@ Satrec_twoline2rv(PyTypeObject *cls, PyObject *args)
if (switch_locale)
old_locale = setlocale(LC_NUMERIC, "C");
- SatrecObject *self = (SatrecObject*) cls->tp_alloc(cls, 0);
- if (!self)
- return NULL;
+ /* Leading spaces in a catalog number make scanf() in the official
+ code consume the Classification letter as part of the catalog
+ number. (The first character of the International Designator
+ then gets consumed as the Classification instead.) But no
+ parsing error is reported, which is bad for users, so let's avoid
+ the situation by adding leading zeros ourselves. */
+ for (int i=2; i<7; i++) {
+ if (line1[i] == ' ') line1[i] = '0';
+ if (line2[i] == ' ') line2[i] = '0';
+ }
+ /* Call the official routine. */
SGP4Funcs::twoline2rv(line1, line2, ' ', ' ', 'i', whichconst,
dummy, dummy, dummy, self->satrec);
@@ -192,14 +206,27 @@ Satrec_sgp4init(PyObject *self, PyObject *args)
nodeo, satrec);
/* Populate date fields that SGP4Funcs::twoline2rv would set. */
+ double whole;
+ double fraction = modf(epoch, &whole);
+ double whole_jd = whole + 2433281.5;
+
+ /* Go out on a limb: if `epoch` has no decimal digits past the 8
+ decimal places stored in a TLE, then assume the user is trying
+ to specify an exact decimal fraction. */
+ double epoch8 = epoch * 1e8;
+ if (round(epoch8) == epoch8) {
+ fraction = round(fraction * 1e8) / 1e8;
+ }
+
+ satrec.jdsatepoch = whole_jd;
+ satrec.jdsatepochF = fraction;
+
int y, m, d, H, M;
double S, jan0jd, jan0fr /* always comes out 0.0 */;
- SGP4Funcs::invjday_SGP4(2433281.5, epoch, y, m, d, H, M, S);
+ SGP4Funcs::invjday_SGP4(2433281.5, whole, y, m, d, H, M, S);
SGP4Funcs::jday_SGP4(y, 1, 0, 0, 0, 0.0, jan0jd, jan0fr);
- satrec.epochyr = y % 1000;
- satrec.epochdays = 2433281.5 - jan0jd + epoch;
- satrec.jdsatepochF = modf(epoch, &satrec.jdsatepoch);
- satrec.jdsatepoch += 2433281.5;
+ satrec.epochyr = y % 100;
+ satrec.epochdays = whole_jd - jan0jd + fraction;
/* Return true as sgp4init does, satrec.error contains any error codes */
@@ -268,16 +295,16 @@ static PyMemberDef Satrec_members[] = {
{"operationmode", T_CHAR, O(operationmode), READONLY,
PyDoc_STR("Operation mode: 'a' legacy AFSPC, or 'i' improved.")},
- {"jdsatepoch", T_DOUBLE, O(jdsatepoch), READONLY,
+ {"jdsatepoch", T_DOUBLE, O(jdsatepoch), 0,
PyDoc_STR("Julian date of epoch, day number (see jdsatepochF).")},
- {"jdsatepochF", T_DOUBLE, O(jdsatepochF), READONLY,
+ {"jdsatepochF", T_DOUBLE, O(jdsatepochF), 0,
PyDoc_STR("Julian date of epoch, fraction of day (see jdsatepoch).")},
{"classification", T_CHAR, O(classification), 0,
"Usually U=Unclassified, C=Classified, or S=Secret."},
/* intldesg: inline character array; see Satrec_getset. */
- {"epochyr", T_INT, O(epochyr), READONLY,
+ {"epochyr", T_INT, O(epochyr), 0,
PyDoc_STR("Year of this element set's epoch (see epochdays). Not set by sgp4init().")},
- {"epochdays", T_DOUBLE, O(epochdays), READONLY,
+ {"epochdays", T_DOUBLE, O(epochdays), 0,
PyDoc_STR("Day of the year of this element set's epoch (see epochyr). Not set by sgp4init().")},
{"ndot", T_DOUBLE, O(ndot), READONLY,
PyDoc_STR("Ballistic Coefficient in revs/day.")},
@@ -314,7 +341,7 @@ static PyMemberDef Satrec_members[] = {
{"method", T_CHAR, O(method), READONLY,
PyDoc_STR("Method, either 'n' near earth or 'd' deep space.")},
- {"error", T_INT, O(method), READONLY,
+ {"error", T_INT, O(error), READONLY,
PyDoc_STR("Error code (1-6) documented in sgp4()")},
{"a", T_DOUBLE, O(a), READONLY,
PyDoc_STR("semi-major axis")},
diff --git a/setup.py b/setup.py
index 3a573e3..9e9f82a 100644
--- a/setup.py
+++ b/setup.py
@@ -1,13 +1,9 @@
import os
import sys
from distutils.core import setup, Extension
-from textwrap import dedent
-
-import sgp4, sgp4.model
+import sgp4
description, long_description = sgp4.__doc__.split('\n', 1)
-satdoc = dedent(sgp4.model.Satellite.__doc__.split('\n', 1)[1])
-long_description = long_description.replace('entry.', 'entry.' + satdoc)
# Force compilation on Travis CI + Python 3 to make sure it keeps working.
optional = True
@@ -31,10 +27,19 @@ if sys.version_info[0] == 3:
# multiple processors when available?
# extra_compile_args=['-fopenmp'],
# extra_link_args=['-fopenmp'],
+ extra_compile_args=['-ffloat-store'],
))
+# Read the package's "__version__" without importing it.
+path = 'sgp4/__init__.py'
+with open(path, 'rb') as f:
+ text = f.read().decode('utf-8')
+text = text.replace('-*- coding: utf-8 -*-', '') # for Python 2.7
+namespace = {}
+eval(compile(text, path, 'exec'), namespace)
+
setup(name = 'sgp4',
- version = '2.15',
+ version = namespace['__version__'],
description = description,
long_description = long_description,
license = 'MIT',
diff --git a/sgp4/__init__.py b/sgp4/__init__.py
index 33a71fc..95207de 100644
--- a/sgp4/__init__.py
+++ b/sgp4/__init__.py
@@ -1,5 +1,5 @@
# -*- coding: utf-8 -*-
-"""Track earth satellite TLE orbits using up-to-date 2020 version of SGP4
+"""Track Earth satellites given TLE data, using up-to-date 2020 SGP4 routines.
This Python package computes the position and velocity of an
earth-orbiting satellite, given the satellite's TLE orbital elements
@@ -11,14 +11,17 @@ the algorithm. This error is far less than the 1–3 km/day by which
satellites themselves deviate from the ideal orbits described in TLE
files.
-* If your platform supports it, this package compiles the verbatim
- source code from the official C++ version of SGP4. You can call the
- routine directly, or through an array API that loops over arrays of
- satellites and arrays of times with machine code instead of Python.
+* If your platform supports it, this package compiles and uses the
+ verbatim source code from the official C++ version of SGP4.
* Otherwise, a slower but reliable Python implementation of SGP4 is used
instead.
+* If, instead of asking for the position of a single satellite at a
+ single time, you supply this library with an array of satellites and
+ an array of times, then the arrays can be processed using machine code
+ instead of requiring you to run a slow Python loop over them.
+
Note that the SGP4 propagator returns raw *x,y,z* Cartesian coordinates
in a “True Equator Mean Equinox” (TEME) reference frame that’s centered
on the Earth but does not rotate with it — an “Earth centered inertial”
@@ -237,9 +240,9 @@ dates, build a ``SatrecArray`` from several individual satellites. Its
so if you only have one date, be sure to provide NumPy arrays of length
one. Here is a sample computation for 2 satellites and 4 dates:
->>> s = '1 20580U 90037B 19342.88042116 .00000361 00000-0 11007-4 0 9996'
->>> t = '2 20580 28.4682 146.6676 0002639 185.9222 322.7238 15.09309432427086'
->>> satellite2 = Satrec.twoline2rv(s, t)
+>>> u = '1 20580U 90037B 19342.88042116 .00000361 00000-0 11007-4 0 9996'
+>>> w = '2 20580 28.4682 146.6676 0002639 185.9222 322.7238 15.09309432427086'
+>>> satellite2 = Satrec.twoline2rv(u, w)
>>> from sgp4.api import SatrecArray
>>> a = SatrecArray([satellite, satellite2])
@@ -270,12 +273,6 @@ one. Here is a sample computation for 2 satellites and 4 dates:
[-3.85 6.28 -1.85]
[-3.91 6.25 -1.83]]]
-Attributes
-----------
-
-The attributes of a ``Satrec`` object carry the data loaded from the TLE
-entry. Look at the class's documentation for details.
-
Export
------
@@ -289,8 +286,14 @@ file, there’s an export routine that will turn it back into a TLE:
>>> line2
'2 25544 51.6439 211.2001 0007417 17.6667 85.6398 15.50103472202482'
-And another that produces the fields defined by the new OMM format (see
-the “OMM” section above):
+Happily, these are exactly the two TLE lines that we used to create this
+satellite object:
+
+>>> (s == line1) and (t == line2)
+True
+
+Another export routine is available that produces the fields defined by
+the new OMM format (see the “OMM” section above):
>>> from pprint import pprint
>>> fields = exporter.export_omm(satellite, 'ISS (ZARYA)')
@@ -341,42 +344,200 @@ constants as were used to generate the TLE.
Providing your own elements
---------------------------
-If instead of parsing a TLE you want to provide your own orbital
-elements, you can call the ``sgp4init()`` method of any existing
-satellite object to reset it to those new elements.
-
->>> sat = Satrec()
->>> sat.sgp4init(
-... WGS72, # gravity model
-... 'i', # 'a' = old AFSPC mode, 'i' = improved mode
-... 5, # satnum: Satellite number
-... 18441.785, # epoch: days since 1949 December 31 00:00 UT
-... 2.8098e-05, # bstar: drag coefficient (/earth radii)
-... 6.969196665e-13, # ndot: ballistic coefficient (revs/day)
-... 0.0, # nddot: second derivative of mean motion (revs/day^3)
-... 0.1859667, # ecco: eccentricity
-... 5.7904160274885, # argpo: argument of perigee (radians)
-... 0.5980929187319, # inclo: inclination (radians)
-... 0.3373093125574, # mo: mean anomaly (radians)
-... 0.0472294454407, # no_kozai: mean motion (radians/minute)
-... 6.0863854713832, # nodeo: right ascension of ascending node (radians)
+If instead of parsing a TLE you want to specify orbital elements
+directly, you can pass them as floating point numbers to a satellite
+object’s ``sgp4init()`` method. For example, here’s how to build the
+same International Space Station orbit that we loaded from a TLE in the
+first code example above:
+
+>>> satellite2 = Satrec()
+>>> satellite2.sgp4init(
+... WGS72, # gravity model
+... 'i', # 'a' = old AFSPC mode, 'i' = improved mode
+... 25544, # satnum: Satellite number
+... 25545.69339541, # epoch: days since 1949 December 31 00:00 UT
+... 3.8792e-05, # bstar: drag coefficient (1/earth radii)
+... 0.0, # ndot: ballistic coefficient (revs/day)
+... 0.0, # nddot: mean motion 2nd derivative (revs/day^3)
+... 0.0007417, # ecco: eccentricity
+... 0.3083420829620822, # argpo: argument of perigee (radians)
+... 0.9013560935706996, # inclo: inclination (radians)
+... 1.4946964807494398, # mo: mean anomaly (radians)
+... 0.06763602333248933, # no_kozai: mean motion (radians/minute)
+... 3.686137125541276, # nodeo: R.A. of ascending node (radians)
... )
-To compute the “epoch” value, simply take a normal Julian date and
-subtract ``2433281.5`` days.
+These numbers don’t look the same as the numbers in the TLE, because the
+underlying ``sgp4init()`` routine uses different units: radians rather
+than degrees. But this is the same orbit and will produce the same
+positions.
+
+Note that ``ndot`` and ``nddot`` are ignored by the SGP4 propagator, so
+you can leave them ``0.0`` without any effect on the resulting satellite
+positions. But they do at least get saved to the satellite object, and
+written out if you write the parameters to a TLE or OMM file (see the
+“Export” section, above).
+
+To compute the “epoch” argument, take the epoch’s Julian date and
+subtract 2433281.5 days.
-In addition to setting the attributes natively set by the underlying
-``sgp4init()`` routine, this library also goes ahead and sets the date
-fields ``epochyr``, ``epochdays``, ``jdsatepoch``, and ``jdsatepochF``.
+While the underlying ``sgp4init()`` routine leaves the attributes
+``epochyr``, ``epochdays``, ``jdsatepoch``, and ``jdsatepochF`` unset,
+this library goes ahead and sets them anyway for you, using the epoch
+you provided.
-The character provided as the second argument can be ``'a'`` to run the
-computations so that they are compatible with the old Air Force Space
-Command edition of the library, or ``'i'`` to run the new and improved
-version of the SGP4 algorithm.
+See the next section for the complete list of attributes that are
+available from the satellite record once it has been initialized.
+
+Attributes
+----------
-You can also directly access a satellite’s orbital parameters by asking
-for the attributes ``sat.epoch``, ``sat.bstar``, and so forth, using the
-names given in the comments above.
+There are several dozen ``Satrec`` attributes that expose data from the
+underlying C++ SGP4 record. They fall into the following categories.
+
+*Identification*
+
+These are copied directly from the TLE record but aren’t used by the
+propagation math.
+
+| ``satnum`` — Unique number assigned to the satellite.
+| ``classification`` — ``'U'``, ``'C'``, or ``'S'``
+ indicating the element set is Unclassified, Classified, or Secret.
+| ``ephtype`` — Integer “ephemeris type”, used internally by space
+ agencies to mark element sets that are not ready for publication;
+ this field should always be ``0`` in published TLEs.
+| ``elnum`` — Element set number.
+| ``revnum`` — Satellite’s revolution number at the moment of the epoch,
+ presumably counting from 1 following launch.
+
+*Orbital Elements*
+
+These are the orbital parameters, copied verbatim from the text of the
+TLE record. They describe the orbit at the moment of the TLE’s epoch
+and so remain constant even as the satellite record is used over and
+over again to propagate positions for different times.
+
+| ``epochyr`` — Epoch date: the last two digits of the year.
+| ``epochdays`` — Epoch date: the number of days into the year,
+ including a decimal fraction for the UTC time of day.
+| ``ndot`` — First time derivative of the mean motion
+ (loaded from the TLE, but otherwise ignored).
+| ``nddot`` — Second time derivative of the mean motion
+ (loaded from the TLE, but otherwise ignored).
+| ``bstar`` — Ballistic drag coefficient B* (1/earth radii).
+| ``inclo`` — Inclination (radians).
+| ``nodeo`` — Right ascension of ascending node (radians).
+| ``ecco`` — Eccentricity.
+| ``argpo`` — Argument of perigee (radians).
+| ``mo`` — Mean anomaly (radians).
+| ``no_kozai`` — Mean motion (radians/minute).
+| ``no`` — Alias for ``no_kozai``, for compatibility with old code.
+
+You can also access the epoch as a Julian date:
+
+| ``jdsatepoch`` — Whole part of the epoch’s Julian date.
+| ``jdsatepochF`` — Fractional part of the epoch’s Julian date.
+
+*Computed Orbit Properties*
+
+These are computed when the satellite is first loaded,
+as a convenience for callers who might be interested in them.
+They aren’t used by the SGP4 propagator itself.
+
+| ``a`` — Semi-major axis (earth radii).
+| ``altp`` — Altitude of the satellite at perigee
+ (earth radii, assuming a spherical Earth).
+| ``alta`` — Altitude of the satellite at apogee
+ (earth radii, assuming a spherical Earth).
+| ``argpdot`` — Rate at which the argument of perigee is changing
+ (radians/minute).
+| ``gsto`` — Greenwich Sidereal Time at the satellite’s epoch (radians).
+| ``mdot`` — Rate at which the mean anomaly is changing (radians/minute)
+| ``nodedot`` — Rate at which the right ascension of the ascending node
+ is changing (radians/minute).
+
+*Propagator Mode*
+
+| ``operationmode`` — A single character that directs SGP4
+ to either operate in its modern ``'i'`` improved mode
+ or in its legacy ``'a'`` AFSPC mode.
+| ``method`` — A single character, chosen automatically
+ when the orbital elements were loaded, that indicates whether SGP4
+ has chosen to use its built-in ``'n'`` Near Earth
+ or ``'d'`` Deep Space mode for this satellite.
+
+*Result of Most Recent Propagation*
+
+| ``t`` —
+ The time you gave when you most recently asked SGP4
+ to compute this satellite’s position,
+ measured in minutes before (negative) or after (positive)
+ the satellite’s epoch.
+| ``error`` —
+ Error code produced by the most recent SGP4 propagation
+ you performed with this element set.
+
+The possible ``error`` codes are:
+
+0. No error.
+1. Mean eccentricity is outside the range 0 ≤ e < 1.
+2. Mean motion has fallen below zero.
+3. Perturbed eccentricity is outside the range 0 ≤ e ≤ 1.
+4. Length of the orbit’s semi-latus rectum has fallen below zero.
+5. (No longer used.)
+6. Orbit has decayed: the computed position is underground.
+ (The position is still returned, in case the vector is helpful
+ to software that might be searching for the moment of re-entry.)
+
+*Mean Elements From Most Recent Propagation*
+
+Partway through each propagation, the SGP4 routine saves a set of
+“singly averaged mean elements” that describe the orbit’s shape at the
+moment for which a position is being computed. They are averaged with
+respect to the mean anomaly and include the effects of secular gravity,
+atmospheric drag, and — in Deep Space mode — of those pertubations from
+the Sun and Moon that SGP4 averages over an entire revolution of each of
+those bodies. They omit both the shorter-term and longer-term periodic
+pertubations from the Sun and Moon that SGP4 applies right before
+computing each position.
+
+| ``am`` — Average semi-major axis (earth radii).
+| ``em`` — Average eccentricity.
+| ``im`` — Average inclination (radians).
+| ``Om`` — Average right ascension of ascending node (radians).
+| ``om`` — Average argument of perigee (radians).
+| ``mm`` — Average mean anomaly (radians).
+| ``nm`` — Average mean motion (radians/minute).
+
+*Gravity Model Parameters*
+
+When the satellite record is initialized, your choice of gravity model
+results in a slate of eight constants being copied in:
+
+| ``tumin`` — Minutes in one “time unit”.
+| ``xke`` — The reciprocal of ``tumin``.
+| ``mu`` — Earth’s gravitational parameter (km³/s²).
+| ``radiusearthkm`` — Radius of the earth (km).
+| ``j2``, ``j3``, ``j4`` — Un-normalized zonal harmonic values J₂, J₃, and J₄.
+| ``j3oj2`` — The ratio J₃/J₂.
+
+Printing satellite attributes
+-----------------------------
+
+If you want to print out a satellite, this library provides a convenient
+“attribute dump” routine that takes a satellite and generates lines that
+list its attributes::
+
+ from sys import stdout
+ from sgp4.conveniences import dump_satrec
+
+ stdout.writelines(dump_satrec(satellite))
+
+If you want to compare two satellites, then simply pass a second
+argument; the second satellite’s attributes will be printed in a second
+column next to those of the first. ::
+
+ stdout.writelines(dump_satrec(satellite, satellite2))
Validation against the official algorithm
-----------------------------------------
@@ -424,6 +585,53 @@ https://pypi.org/project/sgp4/1.4/
Changelog
---------
+2022-04-06 — 2.21
+
+* Added ``dump_satrec()`` to the ``sgp4.conveniences`` module.
+
+* Fixed the ``Satrec`` attribute ``.error``, which was previously
+ building a nonsense integer from the wrong data in memory.
+
+* Removed ``.whichconst`` from Python ``Satrec``, to help users avoid
+ writing code that will break when the C++ extension is available.
+
+2021-07-01 — 2.20
+
+* Taught ``sgp4init()`` to round both ``epochdays`` and ``jdsatepochF``
+ to the same 8 decimal places used for the date fraction in a TLE, if
+ the user-supplied ``epoch`` itself has 8 or fewer digits behind the
+ decimal point. This should make it easier to build satellites that
+ round-trip to TLE format with perfect accuracy.
+
+* Fixed how ``export_tle()`` formats the BSTAR field when its value, if
+ written in scientific notation, has a positive exponent.
+
+* Fixed the ``epochyr`` assigned by ``sgp4init()`` so years before 2000
+ have two digits instead of three (for example, so that 1980 produces
+ an ``epochyr`` of 80 instead of 980).
+
+2021-04-22 — 2.19
+
+* Extended the documentation on the Python Package Index and in the
+ module docstring so it lists every ``Satrec`` attribute that this
+ library exposes; even the more obscure ones might be useful to folks
+ working to analyze satellite orbits.
+
+2021-03-08 — 2.18
+
+* If a TLE satellite number lacks the required 5 digits,
+ ``twoline2rv()`` now gives the underlying C++ library a little help so
+ it can still parse the classification and international designator
+ correctly.
+
+* The ``Satrec`` attributes ``jdsatepoch``, ``jdsatepochF``,
+ ``epochyr``, and ``epochdays`` are now writeable, so users can adjust
+ their values manually — which should make up for the fact that the
+ ``sgp4init()`` method can’t set them with full floating point
+ precision.
+
+| 2021-02-17 — 2.17 — Fixed where in the output array the ``sgp4_array()`` method writes NaN values when an SGP4 propagation fails.
+| 2021-02-12 — 2.16 — Fixed ``days2mdhms()`` rounding to always match TLE epoch.
| 2021-01-08 — 2.15 — Fixed parsing of the ``satnum`` TLE field in the Python fallback code, when the field has a leading space; added OMM export routine.
| 2020-12-16 — 2.14 — New data formats: added OMM message support for both XML and CSV, and added support for the new Alpha-5 extension to TLE files.
| 2020-10-14 — 2.13 — Enhanced ``sgp4init()`` with custom code that also sets the ``epochdays`` and ``epochyr`` satellite attributes.
@@ -447,3 +655,4 @@ Changelog
| 2012-08-27 — 1.0 — Initial release
"""
+__version__ = '2.21'
diff --git a/sgp4/api.py b/sgp4/api.py
index af76ec5..5e83a79 100644
--- a/sgp4/api.py
+++ b/sgp4/api.py
@@ -1,7 +1,9 @@
"""Public API that tries to import C++ module, but falls back to Python."""
-__all__ = ('Satrec', 'SatrecArray', 'WGS72OLD', 'WGS72', 'WGS84',
- 'jday', 'days2mdhms')
+__all__ = (
+ 'SGP4_ERRORS', 'Satrec', 'SatrecArray', 'WGS72OLD', 'WGS72', 'WGS84',
+ 'accelerated', 'jday', 'days2mdhms',
+)
from .functions import jday, days2mdhms
diff --git a/sgp4/conveniences.py b/sgp4/conveniences.py
index 69c9d9b..84e1e0e 100644
--- a/sgp4/conveniences.py
+++ b/sgp4/conveniences.py
@@ -6,6 +6,7 @@ itself, native Python datetime handling could be convenient.
"""
import datetime as dt
+import sgp4
from .functions import days2mdhms, jday
class _UTC(dt.tzinfo):
@@ -71,3 +72,38 @@ def sat_epoch_datetime(sat):
second = int(second)
micro = int(fraction * 1e6)
return dt.datetime(year, month, day, hour, minute, second, micro, UTC)
+
+_ATTRIBUTES = None
+
+def dump_satrec(sat, sat2=None):
+ """Yield lines that list the attributes of one or two satellites."""
+
+ global _ATTRIBUTES
+ if _ATTRIBUTES is None:
+ _ATTRIBUTES = []
+ for line in sgp4.__doc__.splitlines():
+ if line.endswith('*'):
+ title = line.strip('*')
+ _ATTRIBUTES.append(title)
+ elif line.startswith('| ``'):
+ pieces = line.split('``')
+ _ATTRIBUTES.append(pieces[1])
+ i = 2
+ while pieces[i] == ', ':
+ _ATTRIBUTES.append(pieces[i+1])
+ i += 2
+
+ for item in _ATTRIBUTES:
+ if item[0].isupper():
+ title = item
+ yield '\n'
+ yield '# -------- {0} --------\n'.format(title)
+ else:
+ name = item
+ value = getattr(sat, item, '(not set)')
+ line = '{0} = {1!r}\n'.format(item, value)
+ if sat2 is not None:
+ value2 = getattr(sat2, name, '(not set)')
+ verdict = '==' if (value == value2) else '!='
+ line = '{0:39} {1} {2!r}\n'.format(line[:-1], verdict, value2)
+ yield line
diff --git a/sgp4/exporter.py b/sgp4/exporter.py
index 00ebed1..dd9b84e 100644
--- a/sgp4/exporter.py
+++ b/sgp4/exporter.py
@@ -42,14 +42,11 @@ def export_tle(satrec):
# Add First Time Derivative of the Mean Motion (don't use "+")
append("{0: 8.8f}".format(satrec.ndot * (_xpdotp * 1440.0)).replace("0", "", 1) + " ")
- # Add Second Time Derivative of Mean Motion (don't use "+")
- # Multiplication with 10 is a hack to get the exponent right
- append("{0: 4.4e}".format((satrec.nddot * (_xpdotp * 1440.0 * 1440)) * 10).replace(".", "")
- .replace("e+00", "-0").replace("e-0", "-") + " ")
+ # Add Second Time Derivative of Mean Motion
+ append(_abbreviate_rate(satrec.nddot * _xpdotp * 20736000.0, '-0'))
# Add BSTAR
- # Multiplication with 10 is a hack to get the exponent right
- append("{0: 4.4e}".format(satrec.bstar * 10).replace(".", "").replace("e+00", "+0").replace("e-0", "-") + " ")
+ append(_abbreviate_rate(satrec.bstar * 10.0, '+0'))
# Add Ephemeris Type and Element Number
ephtype = getattr(satrec, 'ephtype', 0)
@@ -105,6 +102,15 @@ def export_tle(satrec):
return line1, line2
+def _abbreviate_rate(value, zero_exponent_string):
+ return (
+ '{0: 4.4e} '.format(value)
+ .replace('.', '')
+ .replace('e+00', zero_exponent_string)
+ .replace('e-0', '-')
+ .replace('e+0', '+')
+ )
+
def export_omm(satrec, object_name):
launch_year = int(satrec.intldesg[:2])
launch_year += 1900 + (launch_year < 57) * 100
diff --git a/sgp4/functions.py b/sgp4/functions.py
index 9adb47b..619e802 100644
--- a/sgp4/functions.py
+++ b/sgp4/functions.py
@@ -5,8 +5,6 @@ modules to offer simple date handling, so this small module holds the
routines instead.
"""
-from math import trunc
-
def jday(year, mon, day, hr, minute, sec):
"""Return two floats that, when added, produce the specified Julian date.
@@ -43,9 +41,9 @@ def jday(year, mon, day, hr, minute, sec):
def days2mdhms(year, days, round_to_microsecond=6):
"""Convert a float point number of days into the year into date and time.
- Given the integer year plus the "day of the year" (where 1.0 means
+ Given the integer year plus the "day of the year" where 1.0 means
the beginning of January 1, 2.0 means the beginning of January 2,
- and so forth), returns the Gregorian calendar month, day, hour,
+ and so forth, return the Gregorian calendar month, day, hour,
minute, and floating point seconds.
>>> days2mdhms(2000, 1.0) # January 1
@@ -55,28 +53,28 @@ def days2mdhms(year, days, round_to_microsecond=6):
>>> days2mdhms(2000, 366.0) # December 31, since 2000 was a leap year
(12, 31, 0, 0, 0.0)
+ The floating point seconds are rounded to an even number of
+ microseconds if ``round_to_microsecond`` is true.
+
"""
- whole, fraction = divmod(days, 1.0)
+ second = days * 86400.0
+ if round_to_microsecond:
+ second = round(second, round_to_microsecond)
+
+ minute, second = divmod(second, 60.0)
+ if round_to_microsecond:
+ second = round(second, round_to_microsecond)
+
+ minute = int(minute)
+ hour, minute = divmod(minute, 60)
+ day_of_year, hour = divmod(hour, 24)
is_leap = year % 400 == 0 or (year % 4 == 0 and year % 100 != 0)
- month, day = _day_of_year_to_month_day(int(whole), is_leap)
+ month, day = _day_of_year_to_month_day(day_of_year, is_leap)
if month == 13: # behave like the original in case of overflow
month = 12
day += 31
- # The 8 digits of floating point day specified in the TLE have a
- # resolution of exactly 1e-8 * 24 * 3600 * 1e6 = 864 microseconds,
- # so round off any floating-point noise beyond the microsecond.
- if round_to_microsecond:
- fraction += 0.5 / 86400e6
-
- second = fraction * 86400.0
- minute, second = divmod(second, 60.0)
- hour, minute = divmod(minute, 60.0)
-
- if round_to_microsecond:
- second = trunc(second * 1e6) / 1e6
-
return month, day, int(hour), int(minute), second
def _day_of_year_to_month_day(day_of_year, is_leap):
diff --git a/sgp4/io.py b/sgp4/io.py
index 57ac594..46e5ece 100644
--- a/sgp4/io.py
+++ b/sgp4/io.py
@@ -126,7 +126,6 @@ def twoline2rv(longstr1, longstr2, whichconst, opsmode='i', satrec=None):
satrec = Satellite()
satrec.error = 0;
- satrec.whichconst = whichconst # Python extension: remembers its consts
line = longstr1.rstrip()
diff --git a/sgp4/model.py b/sgp4/model.py
index 3cb0ba9..ad256ec 100644
--- a/sgp4/model.py
+++ b/sgp4/model.py
@@ -33,7 +33,7 @@ class Satrec(object):
'pho', 'pinco', 'plo', 'radiusearthkm', 'revnum', 'satnum', 'se2',
'se3', 'sgh2', 'sgh3', 'sgh4', 'sh2', 'sh3', 'si2', 'si3', 'sinmao',
'sl2', 'sl3', 'sl4', 't', 't2cof', 't3cof', 't4cof', 't5cof', 'tumin',
- 'whichconst', 'x1mth2', 'x7thm1', 'xfact', 'xgh2', 'xgh3', 'xgh4',
+ 'x1mth2', 'x7thm1', 'xfact', 'xgh2', 'xgh3', 'xgh4',
'xh2', 'xh3', 'xi2', 'xi3', 'xke', 'xl2', 'xl3', 'xl4', 'xlamo',
'xlcof', 'xli', 'xmcof', 'xni', 'zmol', 'zmos',
'jdsatepochF'
@@ -73,14 +73,22 @@ class Satrec(object):
def sgp4init(self, whichconst, opsmode, satnum, epoch, bstar,
ndot, nddot, ecco, argpo, inclo, mo, no_kozai, nodeo):
whichconst = gravity_constants[whichconst]
+ whole, fraction = divmod(epoch, 1.0)
+ whole_jd = whole + 2433281.5
- y, m, d, H, M, S = invjday(epoch + 2433281.5)
- jan0epoch = jday(y, 1, 0, 0, 0, 0.0) - 2433281.5
+ # Go out on a limb: if `epoch` has no decimal digits past the 8
+ # decimal places stored in a TLE, then assume the user is trying
+ # to specify an exact decimal fraction.
+ if round(epoch, 8) == epoch:
+ fraction = round(fraction, 8)
- self.epochyr = y % 1000
- self.epochdays = epoch - jan0epoch
- self.jdsatepoch, self.jdsatepochF = divmod(epoch, 1.0)
- self.jdsatepoch += 2433281.5
+ self.jdsatepoch = whole_jd
+ self.jdsatepochF = fraction
+
+ y, m, d, H, M, S = invjday(whole_jd)
+ jan0 = jday(y, 1, 0, 0, 0, 0.0)
+ self.epochyr = y % 100
+ self.epochdays = whole_jd - jan0 + fraction
sgp4init(whichconst, opsmode, satnum, epoch, bstar, ndot, nddot,
ecco, argpo, inclo, mo, no_kozai, nodeo, self)
@@ -172,41 +180,7 @@ class SatrecArray(object):
return e, r, v
class Satellite(object):
- """The old Satellite object for compatibility with sgp4 1.x.
-
- Most of this class's hundred-plus attributes are intermediate values
- of interest only to the propagation algorithm itself. Here are the
- attributes set by ``sgp4.io.twoline2rv()`` in which users are likely
- to be interested:
-
- ``satnum``
- Unique satellite number given in the TLE file.
- ``epochyr``
- Full four-digit year of this element set's epoch moment.
- ``epochdays``
- Fractional days into the year of the epoch moment.
- ``jdsatepoch``
- Julian date of the epoch (computed from ``epochyr`` and ``epochdays``).
- ``ndot``
- First time derivative of the mean motion (ignored by SGP4).
- ``nddot``
- Second time derivative of the mean motion (ignored by SGP4).
- ``bstar``
- Ballistic drag coefficient B* in inverse earth radii.
- ``inclo``
- Inclination in radians.
- ``nodeo``
- Right ascension of ascending node in radians.
- ``ecco``
- Eccentricity.
- ``argpo``
- Argument of perigee in radians.
- ``mo``
- Mean anomaly in radians.
- ``no_kozai``
- Mean motion in radians per minute.
-
- """
+ """The old Satellite object, for compatibility with sgp4 1.x."""
jdsatepochF = 0.0 # for compatibility with new Satrec; makes tests simpler
# TODO: only offer this on legacy class we no longer document
diff --git a/sgp4/tests.py b/sgp4/tests.py
index fba2b6a..c08263a 100644
--- a/sgp4/tests.py
+++ b/sgp4/tests.py
@@ -19,6 +19,8 @@ try:
except ImportError:
from StringIO import StringIO
+import numpy as np
+
from sgp4.api import WGS72OLD, WGS72, WGS84, Satrec, jday, accelerated
from sgp4.earth_gravity import wgs72
from sgp4.ext import invjday, newtonnu, rv2coe
@@ -47,7 +49,9 @@ VANGUARD_ATTRS = {
'operationmode': 'i',
# Time
'epochyr': 0,
+ 'epochdays': 179.78495062,
'jdsatepoch': 2451722.5,
+ 'jdsatepochF': 0.78495062,
# Orbit
'bstar': 2.8098e-05,
'ndot': 6.96919666594958e-13,
@@ -59,7 +63,7 @@ VANGUARD_ATTRS = {
'no_kozai': 0.04722944544077857,
'nodeo': 6.08638547138321,
}
-VANGUARD_EPOCH = 18441.7849506199999894
+VANGUARD_EPOCH = 18441.78495062
# Handle deprecated assertRaisesRegexp, but allow its use Python 2.6 and 2.7
if sys.version_info[:2] == (2, 7) or sys.version_info[:2] == (2, 6):
@@ -78,7 +82,6 @@ def test_legacy_built_with_twoline2rv():
verify_vanguard_1(sat, legacy=True)
def test_satrec_initialized_with_sgp4init():
- # epochyr and epochdays are not set by sgp4init
sat = Satrec()
sat.sgp4init(
WGS72,
@@ -108,6 +111,21 @@ def test_legacy_initialized_with_sgp4init():
)
verify_vanguard_1(sat, legacy=True)
+# ------------------------------------------------------------------------
+# Test array API
+
+def test_whether_array_logic_writes_nan_values_to_correct_row():
+ # https://github.com/brandon-rhodes/python-sgp4/issues/87
+ l1 = "1 44160U 19006AX 20162.79712247 +.00816806 +19088-3 +34711-2 0 9997"
+ l2 = "2 44160 095.2472 272.0808 0216413 032.6694 328.7739 15.58006382062511"
+ sat = Satrec.twoline2rv(l1, l2)
+ jd0 = np.array([2459054.5, 2459055.5])
+ jd1 = np.array([0.79712247, 0.79712247])
+ e, r, v = sat.sgp4_array(jd0, jd1)
+ assert list(e) == [6, 1]
+ assert np.isnan(r).tolist() == [[False, False, False], [True, True, True]]
+ assert np.isnan(v).tolist() == [[False, False, False], [True, True, True]]
+
# ------------------------------------------------------------------------
# Other Officially Supported Routines
#
@@ -224,6 +242,16 @@ def test_export_tle_raises_error_for_out_of_range_angles():
)
assertRaises(ValueError, export_tle, sat)
+def test_tle_import_export_round_trips():
+ for line1, line2 in [(
+ '1 44542U 19061A 21180.78220369 -.00000015 00000-0 -66561+1 0 9997',
+ '2 44542 54.7025 244.1098 0007981 318.8601 283.5781 1.86231125 12011',
+ )]:
+ sat = Satrec.twoline2rv(line1, line2)
+ outline1, outline2 = export_tle(sat)
+ assertEqual(line1, outline1)
+ assertEqual(line2, outline2)
+
def test_all_three_gravity_models_with_twoline2rv():
# The numbers below are those produced by Vallado's C++ code.
# (Why does the Python version not produce the same values to
@@ -257,13 +285,9 @@ GRAVITY_DIGITS = (
# Why don't Python and C agree more closely?
4 if not accelerated
- # Insist on very high precision on my Linux laptop, to signal me if
- # some future adjustment subtlely changes the library's results.
- else 12 if platform.system() == 'Linux' and platform.machine() == 'x86_64'
-
- # Otherwise, reduce our expectations. Note that at least 6 digits
- # past the decimal point are necessary to let the test distinguish
- # between WSG72OLD and WGS72. See:
+ # Otherwise, try 10 digits. Note that at least 6 digits past the
+ # decimal point are necessary to let the test distinguish between
+ # WSG72OLD and WGS72. See:
# https://github.com/conda-forge/sgp4-feedstock/pull/19
# https://github.com/brandon-rhodes/python-sgp4/issues/69
else 10
@@ -293,10 +317,13 @@ def assert_wgs84(sat):
def test_satnum_leading_spaces():
# https://github.com/brandon-rhodes/python-sgp4/issues/81
+ # https://github.com/brandon-rhodes/python-sgp4/issues/90
l1 = '1 4859U 21001A 21007.63955392 .00000000 00000+0 00000+0 0 9990'
l2 = '2 4859 000.0000 000.0000 0000000 000.0000 000.0000 01.00000000 09'
sat = Satrec.twoline2rv(l1, l2)
assertEqual(sat.satnum, 4859)
+ assertEqual(sat.classification, 'U')
+ assertEqual(sat.intldesg, '21001A')
def test_satnum_alpha5_encoding():
def make_sat(satnum_string):
@@ -333,6 +360,17 @@ def test_intldesg_with_7_characters():
)
assertEqual(sat.intldesg, '13066AE')
+def test_1990s_satrec_initialized_with_sgp4init():
+ sat = Satrec()
+ sat.sgp4init(
+ WGS72,
+ 'i',
+ VANGUARD_ATTRS['satnum'],
+ VANGUARD_EPOCH - 365.0, # change year 2000 to 1999
+ *sgp4init_args(VANGUARD_ATTRS)
+ )
+ assertEqual(sat.epochyr, 99)
+
def test_setters():
sat = Satrec()
@@ -459,7 +497,9 @@ def verify_vanguard_1(sat, legacy=False):
if legacy:
attrs = attrs.copy()
del attrs['epochyr']
+ del attrs['epochdays']
del attrs['jdsatepoch']
+ del attrs['jdsatepochF']
for name, value in attrs.items():
try:
@@ -469,10 +509,6 @@ def verify_vanguard_1(sat, legacy=False):
e.args = ('for attribute %s, %s' % (name, message),)
raise e
- if not legacy:
- assertAlmostEqual(sat.epochdays, 179.78495062, delta=3e-14)
- assertAlmostEqual(sat.jdsatepochF, 0.78495062, delta=1e-13)
-
def sgp4init_args(d):
"""Given a dict of orbital parameters, return them in sgp4init order."""
return (d['bstar'], d['ndot'], d['nddot'], d['ecco'], d['argpo'],
@@ -493,6 +529,7 @@ def test_satrec_against_tcppver_using_julian_dates():
jd = satrec.jdsatepoch + whole
fr = satrec.jdsatepochF + fraction
e, r, v = satrec.sgp4(jd, fr)
+ assert e == satrec.error
return e, r, v
run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6])
@@ -501,6 +538,7 @@ def test_satrec_against_tcppver_using_tsince():
def invoke(satrec, tsince):
e, r, v = satrec.sgp4_tsince(tsince)
+ assert e == satrec.error
return e, r, v
run_satellite_against_tcppver(Satrec.twoline2rv, invoke, [1,1,6,6,4,3,6])
@@ -536,6 +574,7 @@ def run_satellite_against_tcppver(twoline2rv, invoke, expected_errors):
# output in tcppver.out.
data = get_data(__name__, 'tcppver.out')
+ data = data.replace(b'\r', b'')
tcppver_lines = data.decode('ascii').splitlines(True)
error_list = []
@@ -722,23 +761,15 @@ def test_omm_csv_matches_old_tle():
assert_satellites_match(sat1, sat2)
def assert_satellites_match(sat1, sat2):
- julian_fractions = {'epochdays', 'jdsatepochF'}
- todo = {'whichconst'}
-
for attr in dir(sat1):
if attr.startswith('_'):
continue
- if attr in todo:
- continue
value1 = getattr(sat1, attr, None)
if value1 is None:
continue
if callable(value1):
continue
value2 = getattr(sat2, attr)
- if attr in julian_fractions:
- value1 = round(value1, 10)
- value2 = round(value2, 10)
assertEqual(value1, value2, '%s %r != %r' % (attr, value1, value2))
# Live example of OMM:
Debdiff
[The following lists of changes regard files as different if they have different names, permissions or owners.]
Files in second set of .debs but not in first
-rw-r--r-- root/root /usr/lib/python3/dist-packages/sgp4-2.21.egg-info
Files in first set of .debs but not in second
-rw-r--r-- root/root /usr/lib/python3/dist-packages/sgp4-2.15.egg-info/PKG-INFO -rw-r--r-- root/root /usr/lib/python3/dist-packages/sgp4-2.15.egg-info/dependency_links.txt -rw-r--r-- root/root /usr/lib/python3/dist-packages/sgp4-2.15.egg-info/top_level.txt
No differences were encountered in the control files