SatelliteKit
SatelliteKit is a library, written in Swift, implementing the SGP4/SDP4 earth-orbiting satellite propagation algorithms first published in the SpaceTrack Report #3 and later refined by Vallado et al in Revisiting Spacetrack Report #3.
Install / Use
/learn @gavineadie/SatelliteKitREADME
SatelliteKit
Satellite Prediction Library
SatelliteKit is a library, written in Swift, implementing the SGP4/SDP4 earth-orbiting satellite
propagation algorithms first published in the
SpaceTrack Report #3
and later refined by Vallado et al in
Revisiting Spacetrack Report #3.
The code of this library is derived from Orekit which implements
the above published algorithms as a small part of it's extensive capabilities.
Test output from SatelliteKit agrees, to meaninglessly high precision, with Orekit
test output and the test output in the above published paper [1].
[1] "Vallado, David A.; Paul Crawford; Richard Hujsak; T. S. Kelso, (August 2006), Revisiting Spacetrack Report #3".
Some people will be surprised by some of my source code comment format; it is a style I inherited from a systems programming language I used long ago and it is really not appropriate for publicly released code in the modern age (especially since Swift has markup built in).
Also note that there is extensive use of Unicode characters in property names and other places. This attempts to match, as much as is reasonable, the mathematical notation and Greek characters usage in the original 1980 Spacetrack Report.
Change Notes
At the end of the README. Lastest change: Version/Tag 2.1.0 -- (2025 Aug 03)
Important Changes • 2025 Aug 03
Significant contributed changes. Source compatibility maintained.
Important Changes • 2025 Feb 16
The temporary changes to accommodate throwing in the position and velocity functions has
been reversed which is a source-incompatible change. Any use of position_throwz() can be
replaced with position() though the throwz versions remain available.
Important Changes • 2024 Sep 12
The core of SatelliteKit operates to generate a 6-D vector of the orbiting object's
position (x, y, z) and velocity (ẋ, ẏ, ż) at the given time. That 6-D vector is the
struct PVCoordinates and it is derived in the (public) getPVCoordinates function
which calls the (private) computePVCoordinates function. When a propagation anomaly
is detected within computePVCoordinates an error is thrown and getPVCoordinates
throws it into the public API.
The programmer can catch errors from getPVCoordinates and take appropriate action,
however, the two functions Satellite.position and Satellite.velocity (which make
direct calls to getPVCoordinates) are more commonly used to obtain an object's
position and velocity and they do not process the errors gracefully -- instead
they cause an immediate failure and exit from the running program.
Some of the possible errors are not outlandish (for examples, a satellite in a low orbit may decay, elements may be loaded that contain errors, or elements way past their sell-by date may be unpropagatable).
Obviously, this is a bad experience for the user and needs to be corrected. Since
merely making Satellite.position and Satellite.velocity throw errors would break
existing code, new versions (same basic purpose, but with different names) have been added
to catch and re-throw the propagation errors, giving the programmer the ability to dodge a
failure and/or display an error alert to the user. The non-throwing versions have also
been marked as deprecated so the compiler can issue an advisory message.
In the Sample Usage below, the line
let posInKms = sat.position(minsAfterEpoch: 10.0)
should be recoded as
do {
let posInKms = try sat.position_throwz(minsAfterEpoch: 10.0)
} catch {
// code to process the error ..
}
(This workaround will be temporary -- the next MAJOR release of SatelliteKit will remove
the non-throwing functions.)
Elements
The Elements structure is initialized from the three lines of elements in a traditional TLE set.
Some sources of TLEs provide no first line (which would contain the object's informal name) and,
in that case, it is OK to pass a null String into the initializer.
public init(_ line0: String, _ line1: String, _ line2: String) throws
The public properties that are exposed from in the Elements structure are:
public let commonName: String // line zero name (if any) [eg: ISS (ZARYA)]
public let noradIndex: UInt // The satellite number [eg: 25544]
public let launchName: String // International designation [eg: 1998-067A]
public let t₀: Double // the TLE t=0 epoch time (days since 1950)
public let e₀: Double // TLE .. eccentricity
public let i₀: Double // TLE .. inclination (radians).
public let ω₀: Double // Argument of perigee (radians).
public let Ω₀: Double // Right Ascension of Ascending node (radians).
public let M₀: Double // Mean anomaly (radians).
public var n₀: Double = 0.0 // Mean motion (radians/min) << [un'Kozai'd]
public var a₀: Double = 0.0 // semi-major axis (Eᵣ) << [un'Kozai'd]
public let ephemType: Int // Type of ephemeris.
public let tleClass: String // Classification (U for unclassified).
public let tleNumber: Int // Element number.
public let revNumber: Int // Revolution number at epoch.
Note that the operation to "un Kozai" the element data is performed inside the initialization because both SGP4 and SDP4 need that adjustment.
The initializer will throw an exception if the numeric parsing of the element data fails, however, it will not do so if the record checksum fails. More complete correctness of the element record can be verified by:
public func formatOK(_ line1: String, _ line2: String) -> Bool
which will return true if the lines are 69 characters long, format is valid, and checksums are good.
Note that line0 doesn't take part in the check so is omitted for this function, and that formatOK will
emit explicit errors into the log.
Other data formats
There has been concern for some time that the three line element sets will become limited, not least of all because they only allow 5 digits for a object's unique NORAD numeric identifier. It has been proposed to provide other, less constricted, data formats. More information on this move will be found at A New Way to Obtain GP Data (aka TLEs)
SatelliteKit has been changed to allow the ingestion of GP data in a JSON form .. for example, given JSON
data, this would decode an array of Elements structures (I'm not catching errors in the example, but you should):
let jsonDecoder = JSONDecoder()
jsonDecoder.dateDecodingStrategy = .formatted(DateFormatter.iso8601Micros)
let tleArray = try jsonDecoder.decode([Elements].self, from: jsonData)
print(Satellite(withTLE: tleArray[0]).debugDescription())
print(Satellite(withTLE: tleArray[1]).debugDescription())
print(Satellite(withTLE: tleArray[2]).debugDescription())
The Elements structure also implements debugDescription which will generate this formatted String
┌─[elements : 0.66 days old]]──────────────────────────────────────────
│ ISS (ZARYA) 25544 = 1998-067A rev#:09857 tle#:0999
│ t₀: 2018-02-08 22:51:49 +0000 +24876.95265046 days after 1950
│
│ inc: 51.6426° aop: 86.7895° mot: 15.53899203 (rev/day)
│ raan: 297.9871° anom: 100.1959° ecc: 0.0003401
│ drag: +3.2659e-05
└───────────────────────────────────────────────────────────────────────
Satellite
Having obtained the Elements for a satellite (a struct which holds only a description of the orbital
elements), it is used to initialize a Satellite struct to manage the propagation of the object's
position and velocity as time is varied from the epochal t₀=0 of the element set.
Whether the object requires the "deep space" propagator, or not, is determined within the Satellite initialization.
The Satellite initializers are:
public init(_: String, _: String, _: String) // three TLE lines ..
public init(elements: Elements) // an Elements struct ..
The Satellite struct offers some public properties and some public functions.
The properties provide some naming information and a "grab bag" directory for whatever you want.
public let tle: Elements // make TLE accessible
public let commonName: String // "COSMOS .."
public let noradIdent: String // "21332"
public let t₀Days1950: Double // days since 1950
public var e: Double { return propagator.e } //### these vary slowly over time ..
public var i: Double { return propagator.i } //###
public var ω: Double { return propagator.ω } //###
public var Ω: Double { return propagator.Ω } //###
public var extraInfo = [String: AnyObject]() // the "grab bag" dictionary ..
The functions accept a time argument, either minutes after the satellite's TLE epoch, or Julian Days, and provide celestial postion (Kilometers) and velocity (Kms/sec) state vectors as output.
public func position(minsAfterEpoch: Double) -> Vector
public func velocity(minsAfterEpoch: Double) -> Vector
public func position(julianDays: Double) -> Vector
public func velocity(julianDays: Double) -> Vector
Sample Usage
This is a simple invocation of the above:
do {
let elements = try Elements("ISS (ZARYA)",
"1 25544U 98067A 18039.95265046 .00001678 00000-0 32659-4 0 9999",
