Compute positioning and angle data for satellites in orbit.
Project description
Sattrack 0.4.1
Sattrack is a Python package that simplifies computations of celestial mechanics. While Sattrack
supports user defined satellites and celestial bodies, it was designed for real satellites in Earth orbit,
specifically those with a recently generated two-line element set. Sattrack makes it easy to
produce an array of data, from SGP4 state vectors, to orbital elements and anomalies at a given time. You
can generate the exact data to solve your specific problem, or use Sattrack's PassFinder
class to find
the time and details of the next overhead pass of the International Space Station, as well as a list of
all overhead passes for the next several days.
Sattrack uses Pyevspace, a fast and lightweight Euclidean vector space package for representing vector
quantities and rotating them between reference frames. We implement our own time and geo-position types
with simple interfaces for ease of use and accurate results. The JulianDate
class allows quick and easy
computations of time differences, while abstracting away the complexities of using Gregorian calendar dates,
as well as timezone differences. There is also support for use with Python's built-in datetime
module. The
GeoPosition
class enables painless rotation of vector quantities to and from a topocentric reference frame.
Sattrack Web App
An online version of Sattrack can be found here. It is a Python Flask app that wraps around Sattrack, and allows you to use some of Sattrack's tools wherever you have access to the internet. This is handy for those who want the convenience of not needing to use the package yourselves, or as an example of the possibilities the package can do for you.
Install
You can install Sattrack from PyPi on the command line with:
pip install sattrack
The source code can be found on github, and can be cloned using:
git clone https://github.com/qbizzle68/sattrack.git
The SGP4 module in sattrack is an extension module that uses the Python C-API to wrap around an SGP4 library written in C. This means if you're using an operating system that doesn't have an already built wheel on PyPi, or you make changes to the source code and (re-)install the package, you'll need a C compiler installed on your machine (most people won't need to worry about this).
Usage
Time
Timing is handled using the Julian date. According to the wikipedia page on the Julian day:
The Julian date (JD) of any instant is the Julian day number plus the fraction of a day since the preceding noon in Universal Time.
where
The Julian day is the continuous count of days since the beginning of the Julian period.
The lower level details of the Julian date are out of the scope of these docs, but the key takeaways are:
- Each Julian date has a Julian day number, an integer, that increments every day at 12:00:00 UT.
- The current Julian period started counting on November 24, 4714 BC, in the Gregorian calendar.
- The Julian date is the Julian day number plus the fraction of the day following 12:00:00 UT.
- The Julian date is not relative to each time zone, but to Universal Time, however time zones are still significant when converting between Julian dates and Gregorian calendar dates.
- January 1, 2000 12:00:00 UT has a Julian date of 2451545.0, and is known as the J2000 epoch.
The JulianDate
class handles all the complexities of the Julian date computations for you. It will convert
between Gregorian calendar elements and Julian date values, compute future/past dates, printing various
formats, all with varying time zones. The JulianDate
class also supports interfacing with Python's datetime
module via the JulianDate.fromDatetime()
class method, and the toDateTime()
instance method.
Examples
A JulianDate
object can be instantiated in various ways:
from sattrack.api import JulianDate, now
from datetime import datetime, timezone, timedelta
# Assuming this was in a Python script that was ran on February 17, 2024 at 21:35:00 -6 UTC, these would
# all produce approximately the same Julian dates, give or take a few milliseconds.
# Explicit construction of Julian dates:
jdFromComponents = JulianDate(2024, 2, 17, 21, 35, 0, -6)
jdFromValue = JulianDate.fromNumber(2460358.642361111, -6)
# Via Python's datetime module:
offset = timedelta(hours=-6)
tz = timezone(offset)
datetimeInstance = datetime(2024, 2, 17, 21, 35, 0, 0, tzinfo=tz)
jdFromDatetime = JulianDate.fromDatetime(datetimeInstance)
# Get current time and timezone (implicitly via time.localtime().tm_gmtoff, or explicitly):
currentJDImplicit = now()
currentJDExplicit = now(-6)
The complexities in computing future/past dates as well as differences in times are completely abstracted away:
from sattrack.api import JulianDate
# Differences in time measured in solar days.
oneHour = 1 / 24
oneMinute = 1 / 1440
oneSecond = 1/86400
currentTime = JulianDate(2024, 2, 17, 21, 35, 0, -6)
dt = 5 * oneHour + 4 * oneMinute + 3 * oneSecond
futureTime = currentTime.future(dt)
print(futureTime)
# prints '2460358.860451 --- 2024/02/18 02:39:03.0 -6 UTC'
# Negative values move into past.
pastTime = currentTime.future(-dt)
print(pastTime)
# prints '2460358.43816 --- 2024/02/17 16:30:57.0 -6 UTC'
timeSincePast = futureTime - pastTime
print(timeSincePast)
# prints '0.4222916666666667'
print(timeSincePast == (2 * dt))
# prints 'True'
# Order here matters, the sign of the time difference is measured relative to the second parameter.
timeSinceFuture = pastTime - futureTime
print(timeSinceFuture)
# prints '-0.4222916666666667'
The JulianDate
class also supports all comparison operators:
from sattrack.api import JulianDate
firstTime = JulianDate(2024, 2, 17, 21, 35, 0, -6)
secondTime = JulianDate(2024, 2, 17, 22, 35, 0, -6)
thirdTime = JulianDate(2024, 2, 17, 23, 35, 0, -6)
# These all return True:
firstTime < secondTIme
thirdTime >= firstTime
secondTime != thirdTime
# These all return False:
firstTime > secondTime
thirdTime < firstTime
firstTime == secondTIme
# Time zones are just different representations of the same moment in time:
timeInUSA = JulianDate(2024, 2, 17, 12, 0, 0, -6)
timeInGermany = JulianDate(2024, 2, 17, 19, 0, 0, +1)
# Notice the Julian date number is the same.
print(timeInUSA)
# prints '2460358.25 --- 2024/02/17 12:00:00.0 -6 UTC'
print(timeInGermany)
# prints '2460358.25 --- 2024/02/17 19:00:00.0 +1 UTC'
# Returns True:
timeInUSA == timeInGermany
Since times usually represent unique events, it makes sense they may be usable as keys values, meaning they should be hashable:
from sattrack.api import JulianDate
firstTime = JulianDate(2024, 2, 17, 21, 35, 0, -6)
secondTime = JulianDate(2024, 2, 17, 22, 35, 0, -6)
thirdTime = JulianDate(2024, 2, 17, 23, 35, 0, -6)
# Since JulianDate objects are hashable, they can be used as keys in dicts, or in sets.
events = {firstTime: 'first event', secondTime: 'seconds event', thirdTime: 'third event'}
earliestTime = min(events)
print('The earliest event is', events[earliestTime])
# prints 'The earliest event is first event'
Coordinates
There are several coordinate types in Sattrack, most inheriting from the abstract base class Coordinates
,
the most useful of which is probably the GeoPosition
class. This class represents the geo-position of a user,
and is used for computing topocentric values. It takes a latitude and longitude angle as constructor
arguments, and also supports an elevation value, which can be used while computing a geo-position's radius or
geocentric position vector.
Almost all angle values in Sattrack are in radians. This is because, as a computationally intensive and data based
package, radians are the most useful to use. This is a rule that is kept as consistent across the package as
possible for ease of use, however geo-positions are almost exclusively described using degrees, therefore the only
consistent part of the coordinates objects is how they break this rule. Internally all Coordinates
objects use
radians, but abstract that away from the user, who will instantiate them and see them printed with degree units.
from sattrack.api import GeoPosition
# Arguments are latitude then longitude, think of the colloquialism lat/long.
detroit = GeoPosition(42.331429, -83.045753)
# Elevation is in kilometers, Detroit's is 200 meters.
detroitWithElevation = GeoPosition(42.331429, -83.045753, 0.200)
The GeoPosition
class also supports several methods, specifically for computing position data with regard to a
geo-position. For example, since the Earth is rotating relative to the celestial reference frame, the vector
pointing from the center of Earth to a geo-position is dependent on time.
from sattrack.api import GeoPosition, JulianDate, SIDEREAL_PER_SOLAR
detroit = GeoPosition(42.331429, -83.045753)
time = JulianDate(2024, 2, 17, 21, 35, 0, -6)
positionVector = detroit.getPositionVector(time)
print(positionVector)
# prints '[-2239.8, 4157.31, 4272.89]'
# Time exactly one earth rotation later (one sidereal day).
futureTime = time.future(SIDEREAL_PER_SOLAR)
print(futureTime.date())
# prints '2024/02/18 21:31:04.091 -6 UTC'
futurePositionVector = detroit.getPositionVector(futureTime)
print(futurePositionVector)
# prints '[-2239.8, 4157.31, 4272.89]'
Orbitables
In Sattrack, an orbitable object is an object that can orbit a celestial body. The Orbitable
class is an
abstract base class that provides an interface for different types of orbiting objects. In Sattrack terms, a
satellite that is described using a set of orbital elements, via the Elements
class, is implemented with
the Orbit
class. An orbitable that is described by a TLE, via the TwoLineElement
class, is implemented
with the Satellite
class. This may create some confusion, as outside Sattrack, an Orbit
object would
also be called a satellite, however this is how they are differentiated within Sattrack.
This distinction should be clear enough to avoid any ambiguities in this documentation. To restate it simply:
- An
Orbit
is a satellite using only orbital elements, instantiated with anElements
object. - A
Satellite
is a satellite using a TLE, instantiated with aTwoLineElement
object.
Both classes implement most of the same methods (via the Orbitable
interface) so that other modules/classes
in the package do not need to distinguish between the two types.
Orbit
The simplest and most customizable satellite in Sattrack is an Orbit
object. This is why the Orbit
class was
created, to allow us to explicitly create specific satellite conditions whenever we want. To create a satellite
from a set of orbital elements, first create an Elements
object, then instantiate an Orbit
from the elements.
from sattrack.api import Elements, Orbit, now
# Create a JulianDate object from the current time
time = now()
# The regular constructor takes it's arguments in radians, not degrees
elements = Elements.fromDegrees(90, # right-ascension of the ascending node
51.6, # inclination
0, # argument of periapsis
0.001, # eccentricity
6700, # semi-major axis in kilometers
180, # mean anomaly, currently at apoapsis
time) # time satellite is at mean anomaly
orbit = Orbit(elements, 'custom-satellite')
Satellite
The more complex satellite type is a Satellite
object. The complexity comes from the SGP4 library needed to
propagate the satellite, however this distinction is hidden away by the TwoLineElement
class and Sattrack's
sgp4
extension module. Thanks to the Orbitable
interface, a Satellite
object can be used, for the most
part, just like an Orbit
object.
from sattrack.api import TwoLineElement, Satellite
tle = TwoLineElement('''ISS (ZARYA)
1 25544U 98067A 24048.87310782 .00022368 00000+0 39798-3 0 9993
2 25544 51.6400 191.6020 0001723 274.9313 221.5487 15.50057742439897''')
satellite = Satellite(tle)
Accuracy
It should be quickly noted here that no real satellite in orbit maintains constant orbital elements due
to the myriad of perturbing forces including but not limited to atmospheric drag, oblateness of the parent
body, solar radiation pressure and gravitational effects from the sun, moon, and other planetary bodies.
This means using orbital elements over a non-zero period of time with the Orbit
class, should really
only be used when an idealized case is sufficient, such as demonstrating orbital principles or when a
patched conic approximation is needed such as while playing Kerbal Space Program.
A two-line element set is one of the most accurate ways to approximate a satellite's position using the SGP4 algorithm. With that being said, it should also be noted that the use of two-line element sets and the SGP4 algorithm is, by definition, an approximation. Therefore, without direct measurement of a satellite, there is no way to know its exact position or velocity. A TLE will stay with a few kilometers of accuracy within several days, so you should use a TLE generated close to the time of the data you wish to compute. An error of several kilometers may sound large, however low earth satellites usually have a radius over 6,700 kilometers, so the error is relatively small and reasonable for most cases.
Generating Data
At this point whether you have an Orbit
or Satellite
object is irrelevant for the most part outside of
accuracy, as all operations shown here can be done with either class.
from sattrack.api import GeoPosition, JulianDate, TwoLineElement, Satellite, toTopocentric, toTopocentricOffset
geo = GeoPosition(42.331429, -83.045753)
time = JulianDate(2024, 2, 17, 21, 35, 0, -6)
# Most current TLE for the International Space Station for the time used here.
tle = TwoLineElement('''ISS (ZARYA)
1 25544U 98067A 24048.87310782 .00022368 00000+0 39798-3 0 9993
2 25544 51.6400 191.6020 0001723 274.9313 221.5487 15.50057742439897''')
satellite = Satellite(tle)
position, velocity = satellite.getState(time)
print(position, velocity)
# prints '[2825.37, 4178.21, -4557.74] [-6.87903, 1.25872, -3.11139]'
# Transform state vectors to the SEZ reference frame.
topocentricPosition = toTopocentricOffset(position, geo, time)
topocentricVelocity = toTopocentric(velocity, geo, time)
print(topocentricPosition, topocentricVelocity)
# prints '[4922.7, -4469.09, -7709.13] [5.24356, 5.45901, 1.13596]'
Overhead Satellite Passes
Overview
One of the most intriguing parts of celestial mechanics that even a lay person can enjoy is when a satellite
visibly passes overhead. Sattrack shields the complexities for quickly and accurately finding the next time
a satellite will pass overhead with the PassFinder
class. The PassFinder
methods computeNextPass()
and
computePassList()
methods return either a single or list
of SatellitePass
objects respectively.
Satellite Pass
The SatellitePass
is container of PositionInfo
instances. Each PositionInfo
object contains information
about a specific event that occurs during a pass such as rise time, maximum altitude time and set time (all
passes have these three events). A printed SatellitePass
looks like the following:
Pass details for ISS (ZARYA), at 2024/02/18 04:30:59.791 -6 UTC
instance | time | altitude | azimuth | illuminated | unobscured | visible
-------------------------------------------------------------------------------------------------
rise | 04:25:48.838 | 0.00 | 211.73 (SSW) | False | True | False
-------------------------------------------------------------------------------------------------
first illuminated | 04:30:26.189 | 29.34 | 159.59 (SSE) | True | True | True
-------------------------------------------------------------------------------------------------
max | 04:30:59.791 | 31.64 | 138.45 (SE) | True | True | True
-------------------------------------------------------------------------------------------------
set | 04:36:16.96 | 0.00 | 62.78 (ENE) | True | True | True
Each column corresponds to a different event described by the instance value, and has a time, altitude angle, and azimuth angle. The azimuth angle refers to a compass heading to look towards at ground level, and the altitude angle refers to how high above that point the satellite is at that time (90 degrees is straight up). Each event also has visibility information: illuminated, unobscured and visible. Illuminated refers to whether the satellite is illuminated by sunlight at that time. Satellites do not give off their own light, and can only be seen when they reflect sunlight (just like the moon). Unobscured refers to the fact that the Sun is too bright for us to see any other celestial objects in the sky (except for the bright Moon). Therefore, celestial objects can almost always only be seen after the Sun has set and before it rises, when they are unobscured by the Sun. In Sattrack terms, a satellite is visible if and only if it is illuminated and unobscured.
# other code used to produce our example SatellitePass above
riseInfo = satellitePass.riseInfo
print(riseInfo.illuminated)
# prints False
print(riseInfo.unobscured)
# prints True
print(riseInfo.visible)
# prints False
There are also visibility values for an entire SatellitePass
. If any event of a pass is illuminated, the
illuminated attribute of the pass is True, otherwise it is False. The same logic follows for the unobscured
and visible attributes.
Finding Passes
Passes can be easily found using the PassFinder
class, which required an Orbitable
and GeoPosition
to
instantiate, and a list of SatellitePass
objects can be computed with the computePassList()
method.
tle = TwoLineElement('''ISS (ZARYA)
1 25544U 98067A 24048.87310782 .00022368 00000+0 39798-3 0 9993
2 25544 51.6400 191.6020 0001723 274.9313 221.5487 15.50057742439897''')
satellite = Satellite(tle)
geo = GeoPosition(42.331429, -83.045753)
time = JulianDate(2024, 2, 17, 21, 35, 0, -6)
finder = PassFinder(satellite, geo)
# Compute passes for the next seven days.
passList = finder.computePassList(time, 7)
You can either print and visually inspect each pass, or refine the list however you wish.
for satellitePass in passList:
print(satellitePass)
# prints a long list of SatellitePass objects
visibleList = [satellitePass for satellitePass in passList if satellitePass.visible]
print(len(visibleLlist))
# prints 17
# Compute a list of passes that are visible and are higher than 70 degrees.
highPasses = [satellitePass for satellitePass in visibleList if satellitePass.maxInfo.altitude > 70]
print(len(highPasses))
# prints 2
Pass Events
Each satellite pass can have several events besides the default rise, set and maximum. These events are determined by where in the pass a satellite (if at all) becomes illuminated, unobscured, or visible. A satellite that enters or exits the Earth's shadow during a pass will have a 'first illuminated' or a 'last illuminated' event respectively. A satellite pass that occurs during sunrise or sunset will have a 'last unobscured' or a 'first unobscured' event respectively. Events like these will only be displayed when the illuminated or unobscured value changes at a single point during the pass. For example a satellite pass that takes place in the middle of the day will have no first or last unobscured event, as it's unobscured for the entire pass.
There are several other events that may provide utility in searching for specific pass types. If the event is
applicable, its value will be a PositionInfo
instance, otherwise it will be None
. Some events may be
the same PositionInfo
instance. For example, in a pass that is visible during its entirety, the maxInfo
,
maxVisibleInfo
, maxIlluminatedInfo
, and maxUnobscuredInfo
attributes will be the same PositionInfo
instance. Similarly, all first info's will be the same as riseInfo
, and all last info's will be the same
as setInfo
. All possible event attribute names are: riseInfo
, setInfo
, maxInfo
, firstIlluminatedInfo
,
lastIlluminatedInfo
, maxIlluminatedInfo
, firstUnobscuredInfo
, lastUnobscuredInfo
, maxUnobscuredInfo
,
firstVisibleInfo
, lastVisibleInfo
, and maxVisibleInfo
.
License
Project details
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
File details
Details for the file sattrack-0.4.1.tar.gz
.
File metadata
- Download URL: sattrack-0.4.1.tar.gz
- Upload date:
- Size: 127.1 kB
- Tags: Source
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.0.0 CPython/3.11.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | f8fd73c40f4d5096874847246c5afc50e43c91767b2bcb5154e77a19be38b3ed |
|
MD5 | 44e6c3e9c6f71842f3ac90a551d55a50 |
|
BLAKE2b-256 | ee1d2dc5d02e7c5b212a28a6f6c082e19751d0a69588558a323e93a83f46345b |
File details
Details for the file sattrack-0.4.1-cp311-cp311-win_amd64.whl
.
File metadata
- Download URL: sattrack-0.4.1-cp311-cp311-win_amd64.whl
- Upload date:
- Size: 105.4 kB
- Tags: CPython 3.11, Windows x86-64
- Uploaded using Trusted Publishing? No
- Uploaded via: twine/5.0.0 CPython/3.11.6
File hashes
Algorithm | Hash digest | |
---|---|---|
SHA256 | 2df84f6a8c2be499916165240037e6214d10601c3e4657664ea7a8caa7cdb4cc |
|
MD5 | a92e59c776bc4339f426ca7141eadf01 |
|
BLAKE2b-256 | 482f8552c1c34e42ad81ca4966157198966319de23959da427ce65ee36b12b47 |