Package: sattrack
Sattrack 0.4.1Sattrack 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 generatedtwo-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'sPassFinderclass 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 usesPyevspace, 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. TheJulianDateclass 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-indatetimemodule. TheGeoPositionclass enables painless rotation of vector quantities to and from a topocentric reference frame.Sattrack Web AppAn online version of Sattrack can be foundhere. 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.InstallYou can install Sattrack fromPyPion the command line with:pipinstallsattrackThe source code can be found ongithub, and can be cloned using:gitclonehttps://github.com/qbizzle68/sattrack.gitThe 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).UsageTimeTiming is handled using the Julian date. According to thewikipedia 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.whereThe 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 theGregorian 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 toUniversal 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.TheJulianDateclass 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. TheJulianDateclass also supports interfacing with Python'sdatetimemodule via theJulianDate.fromDatetime()class method, and thetoDateTime()instance method.ExamplesAJulianDateobject can be instantiated in various ways:fromsattrack.apiimportJulianDate,nowfromdatetimeimportdatetime,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:fromsattrack.apiimportJulianDate# Differences in time measured in solar days.oneHour=1/24oneMinute=1/1440oneSecond=1/86400currentTime=JulianDate(2024,2,17,21,35,0,-6)dt=5*oneHour+4*oneMinute+3*oneSecondfutureTime=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-pastTimeprint(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-futureTimeprint(timeSinceFuture)# prints '-0.4222916666666667'TheJulianDateclass also supports all comparison operators:fromsattrack.apiimportJulianDatefirstTime=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<secondTImethirdTime>=firstTimesecondTime!=thirdTime# These all return False:firstTime>secondTimethirdTime<firstTimefirstTime==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==timeInGermanySince times usually represent unique events, it makes sense they may be usable as keys values, meaning
they should be hashable:fromsattrack.apiimportJulianDatefirstTime=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'CoordinatesThere are several coordinate types in Sattrack, most inheriting from the abstract base classCoordinates,
the most useful of which is probably theGeoPositionclass. This class represents the geo-position of a user,
and is used for computingtopocentricvalues. 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 allCoordinatesobjects use
radians, but abstract that away from the user, who will instantiate them and see them printed with degree units.fromsattrack.apiimportGeoPosition# 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)TheGeoPositionclass 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.fromsattrack.apiimportGeoPosition,JulianDate,SIDEREAL_PER_SOLARdetroit=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]'OrbitablesIn Sattrack, an orbitable object is an object that can orbit a celestial body. TheOrbitableclass 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 theElementsclass, is implemented with
theOrbitclass. An orbitable that is described by a TLE, via theTwoLineElementclass, is implemented
with theSatelliteclass. This may create some confusion, as outside Sattrack, anOrbitobject 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:AnOrbitis a satellite using only orbital elements, instantiated with anElementsobject.ASatelliteis a satellite using a TLE, instantiated with aTwoLineElementobject.Both classes implement most of the same methods (via theOrbitableinterface) so that other modules/classes
in the package do not need to distinguish between the two types.OrbitThe simplest and most customizable satellite in Sattrack is anOrbitobject. This is why theOrbitclass 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 anElementsobject, then instantiate anOrbitfrom the elements.fromsattrack.apiimportElements,Orbit,now# Create a JulianDate object from the current timetime=now()# The regular constructor takes it's arguments in radians, not degreeselements=Elements.fromDegrees(90,# right-ascension of the ascending node51.6,# inclination0,# argument of periapsis0.001,# eccentricity6700,# semi-major axis in kilometers180,# mean anomaly, currently at apoapsistime)# time satellite is at mean anomalyorbit=Orbit(elements,'custom-satellite')SatelliteThe more complex satellite type is aSatelliteobject. The complexity comes from the SGP4 library needed to
propagate the satellite, however this distinction is hidden away by theTwoLineElementclass and Sattrack'ssgp4extension module. Thanks to theOrbitableinterface, aSatelliteobject can be used, for the most
part, just like anOrbitobject.fromsattrack.apiimportTwoLineElement,Satellitetle=TwoLineElement('''ISS (ZARYA)1 25544U 98067A   24048.87310782  .00022368  00000+0  39798-3 0  99932 25544  51.6400 191.6020 0001723 274.9313 221.5487 15.50057742439897''')satellite=Satellite(tle)AccuracyIt 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 theOrbitclass, 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 DataAt this point whether you have anOrbitorSatelliteobject is irrelevant for the most part outside of
accuracy, as all operations shown here can be done with either class.fromsattrack.apiimportGeoPosition,JulianDate,TwoLineElement,Satellite,toTopocentric,toTopocentricOffsetgeo=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  99932 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 PassesOverviewOne 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 thePassFinderclass. ThePassFindermethodscomputeNextPass()andcomputePassList()methods return either a single orlistofSatellitePassobjects respectively.Satellite PassTheSatellitePassis container ofPositionInfoinstances. EachPositionInfoobject 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 printedSatellitePasslooks 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    |  TrueEach 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 isilluminatedby 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 areunobscuredby the Sun. In Sattrack terms, a satellite is visible if and only if it is illuminatedandunobscured.# other code used to produce our example SatellitePass aboveriseInfo=satellitePass.riseInfoprint(riseInfo.illuminated)# prints Falseprint(riseInfo.unobscured)# prints Trueprint(riseInfo.visible)# prints FalseThere are also visibility values for an entireSatellitePass. 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 PassesPasses can be easily found using thePassFinderclass, which required anOrbitableandGeoPositionto
instantiate, and a list ofSatellitePassobjects can be computed with thecomputePassList()method.tle=TwoLineElement('''ISS (ZARYA)1 25544U 98067A   24048.87310782  .00022368  00000+0  39798-3 0  99932 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.forsatellitePassinpassList:print(satellitePass)# prints a long list of SatellitePass objectsvisibleList=[satellitePassforsatellitePassinpassListifsatellitePass.visible]print(len(visibleLlist))# prints 17# Compute a list of passes that are visible and are higher than 70 degrees.highPasses=[satellitePassforsatellitePassinvisibleListifsatellitePass.maxInfo.altitude>70]print(len(highPasses))# prints 2Pass EventsEach 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 aPositionInfoinstance, otherwise it will beNone. Some events may be
the samePositionInfoinstance. For example, in a pass that is visible during its entirety, themaxInfo,maxVisibleInfo,maxIlluminatedInfo, andmaxUnobscuredInfoattributes will be the samePositionInfoinstance. Similarly, all first info's will be the same asriseInfo, and all last info's will be the same
assetInfo. All possible event attribute names are:riseInfo,setInfo,maxInfo,firstIlluminatedInfo,lastIlluminatedInfo,maxIlluminatedInfo,firstUnobscuredInfo,lastUnobscuredInfo,maxUnobscuredInfo,firstVisibleInfo,lastVisibleInfo, andmaxVisibleInfo.LicenseMIT