Skip to main content

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 an Elements object.
  • A Satellite is a satellite using a TLE, instantiated with a TwoLineElement 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

MIT

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sattrack-0.4.1.tar.gz (127.1 kB view hashes)

Uploaded Source

Built Distribution

sattrack-0.4.1-cp311-cp311-win_amd64.whl (105.4 kB view hashes)

Uploaded CPython 3.11 Windows x86-64

Supported by

AWS AWS Cloud computing and Security Sponsor Datadog Datadog Monitoring Fastly Fastly CDN Google Google Download Analytics Microsoft Microsoft PSF Sponsor Pingdom Pingdom Monitoring Sentry Sentry Error logging StatusPage StatusPage Status page