How GPKit works

Four steps, each a small set of types.K-11

How data flows through GPKitBytes go to ElementSets.read, which returns an ElementSetFile. Each bad record becomes a Refusal; each good one an ElementSet. Propagator is made from an ElementSet. It gives a StateVector in TEME with state(at:), a Look with look(from:at:), and Passes with passes(over:from:for:): a rise, a culmination and a set, each with a Look.bytesElementSets.read(_:as:)ElementSetFileper bad recordRefusal.elementSetsElementSetPropagator(_:)Propagatorstate(at:)StateVector (TEME)look(from:at:)Lookpasses(over:from:for:)Passes: rise · culmination · set
The same, as text
bytes ──ElementSets.read──▶ ElementSetFile ──▶ ElementSet ──▶ Propagator ─┬─state(at:)─────────────▶ StateVector (TEME)
                                 │                                        ├─look(from:at:)─────────▶ Look
                                 └─▶ Refusal (per bad record)             └─passes(over:from:for:)─▶ Passes

1. Reading: bytes to element sets

ElementSets.read(_:as:) takes bytes, Data or a String, and a format: .csv, .json, .xml, .kvn or .tle. GPKit doesn't guess the format; you say which it is.K-10K-22A-46

It returns an ElementSetFile: one entry per record, each either an ElementSet or a Refusal.K-10K-25

let file = try ElementSets.read(bytes, as: .json)
for set in file.elementSets { set.catalogNumber }      // 25544: an Int, whatever its width
for refusal in file.refusals { print(refusal) }        // NORAD_CAT_ID "A0000" is not an integer of up to nine digits (record 2 of the file)

K-71b

One record, whatever the format. An ElementSet's properties are the OMM's keywords in the OMM's units, and a TLE is read into the same record. The TLE is a way in, not the model.K-12

TLE

Strict: both lines 69 printable ASCII characters, both checksums right, the two catalog fields the same, every separator column a space, and every field what its columns are for. A letter in a numeric field, a short line, a line 1 with no line 2, and a line that belongs to no set are each refused with that reason.K-20

Go deeper: the checksum Sum the first 68 characters, counting each digit as its value and each minus sign as 1; everything else counts 0. The last digit of that sum is the check digit in column 69.K-21

OMM

OMM (CCSDS 502.0-B-3) in four encodings: CSV with a header row, JSON (an array of objects, or one object), XML (NDM/XML, CCSDS 505.0-B-3) and KVN.K-22

What GPKit fills in, and what it lets pass, is written down, because each is a decision made for the readerK-27:

when GPKit
CENTER_NAME, REF_FRAME, TIME_SYSTEM, MEAN_ELEMENT_THEORY are absent (as in CelesTrak's CSV and JSON, and every TLE) fills in EARTH, TEME, UTC, SGP4, and lists each in defaulted
OBJECT_ID or OBJECT_NAME is empty, null or absent nil; nothing is put in its place
a keyword GPKit has no property for keeps it as text in otherKeywords
a byte-order mark; LF or CRLF passes over it
JSON numbers written as strings reads them
XML namespace prefixes, CDATA, entities reads them; a document type declaration is refused

Reading checks the form of a record, not its physics: an eccentricity of 2 is a number, and it's read.K-22

Catalog numbers and Alpha-5

The catalog assigned number 100000 on 11 July 2026. The usable five-digit range had already run out at 69,999 (CelesTrak).G-10G-11

Go deeper: decoding Alpha-5 `value = (index of the letter in "ABCDEFGHJKLMNPQRSTUVWXYZ" + 10) × 10,000 + the last four digits`. Refused: lowercase, `I` or `O`, a space or sign, a letter anywhere but first, and any length but five.K-23 | number | field | | number | field | |---|---|---|---|---| | 100000 | `A0000` | | 229999 | `N9999` | | 179999 | `H9999` | | 230000 | `P0000` | | 180000 | `J0000` | | 339999 | `Z9999` | These are among the test vectors in `ValueTests.swift`.K-23

Refusals

GPKit drops nothing without a word, and guesses nothing.K-25 A Refusal says what was wrong (one of 19 kinds), the field, the text that was there, and the line or record. One bad record costs one record: the ones around it are read.K-25

ElementSets.read throws only when the input as a whole can't be what it's said to be: bytes that aren't UTF-8, empty input, JSON or XML that doesn't parse, or a file that ends inside a record. A file cut short is refused whole, so part of a catalog is never loaded as if it were all of it.K-25K-26

Writing a TLE

TLE.write(set) brings each value to its field's resolution, exactly, in decimal, a half rounding up. eccentricity: .truncated cuts it off instead, as CelesTrak's own lines do. What a TLE can't carry is refused, never approximated: a catalog number that's missing, negative or above 339999, an epoch outside 1957–2056, or a value wider than its columns.K-29

2. Values: exact decimals and integer epochs

3. SGP4

Propagator is SGP4: a port to Swift of the propagation routines of David Vallado's SGP4.cpp, as CelesTrak publishes it.K-30

let propagator = try Propagator(set)            // refuses a set that isn't for SGP4
let state = try propagator.state(at: time)      // TEME: kilometres and kilometres per second
Go deeper: frames, and how it's checked - `state` is in TEME. `position(at:)` turns it with the Earth by Greenwich mean sidereal time and gives the point on the WGS-84 ellipsoid beneath the satellite. UT1 is taken to be UTC (they're within 0.9 s), and the motion of the pole is left out.K-34K-35 - The tests compile Vallado's SGP4.cpp, unmodified, and run it beside the port through the 33 verification cases: 2,349 steps, 24 of the cases deep space, with each set of constants and in both of the C++'s operation modes. The port **agrees with the reference C++ within 1 micrometre**, with the same error code at every step.K-40K-40b - The C++ is compiled with floating-point contraction off, because Clang fuses multiply-adds by default on arm64 and Swift never does.K-41b - [How it's tested →](/testing/)

4. Passes, look angles and Doppler

let home = Observer(latitude: 40.0, longitude: -75.0, height: 100)     // degrees, degrees, metres
let passes = propagator.passes(over: home, from: now, for: .seconds(86_400))
for pass in passes { pass.rise?.time; pass.culmination.look.elevation; pass.set?.look.azimuth }

K-72

What GPKit 1.0.0 doesn't do: it has no sun model, so nothing about whether a pass can be seen, and no horizon that varies by direction. GP Passes adds both on top.K-55 GPKit's main, not yet in a release, adds a sun model and when in a pass the satellite can be seen.K-90K-96

Sources

  1. K-11 Data flow: ElementSets.read → ElementSet → Propagator → TEME state → Earth-fixed → look → passes. Source: GPKit: ElementSets.swift:54-69; OMM.swift:43-104; Propagator.swift:94-128; Observer.swift:102-154; Passes.swift:58-177.
  2. K-10 The public API (signatures). Source: GPKit: docs/public-api.swift (generated by tools/public-api.sh).
  3. K-22 OMM in CSV, JSON, XML (505.0-B-3) and KVN (502.0-B-3 §7). Quirks handled as listed. DOCTYPE refused. Source: GPKit: ElementSets.swift:1-13; XML.swift:1-7; README.md:105-119.
  4. A-46 Import reads OMM in CSV, JSON, XML or KVN, and TLE (including .2le and .3le), detected by content. Source: Passes: FormatDetector.swift:4-44.
  5. K-25 Refusals carry a kind (19 kinds), a message, a field and a line or record. One bad record costs one record. Source: GPKit: Refusal.swift:7-89; README.md:81-83.
  6. K-71b The Reading example (ElementSets.read, elementSets, refusals, the printed refusal). Source: GPKit: README.md:42-62.
  7. K-12 One record type whatever the format; "the TLE is a way in, not the model". Source: GPKit: ElementSet.swift:3-4; README.md:64-65.
  8. K-20 TLE reading is strict: 69 printable ASCII characters, both checksums, agreeing catalog fields, space separators. Year pivot 57. Source: GPKit: TLE.swift:3-5,19-24,51-80.
  9. K-28 Epochs are integers to the attosecond; a leap second at 23:59:60 is kept. Source: GPKit: Epoch.swift:1-7.
  10. K-21 The checksum is verified by default; .ignore skips only the checksum. Source: GPKit: TLE.swift:8-34; README.md:93-96; test TLETests.swift:77-99.
  11. K-27 Values are ExactDecimal. Applied defaults are recorded in defaulted. Source: GPKit: ExactDecimal.swift:1-6; ElementSet.swift:54-76.
  12. G-10 On 11 July 2026 the catalog assigned number 100000, to SARAMAGO. Source: gpconf: README.md:98-99; docs/BRAND.md:59.
  13. G-11 The five-digit range ended at 69,999 (CelesTrak). Source: gpconf: docs/BRAND.md:59; docs/RESEARCH.md:32-36.
  14. K-24 Catalog numbers are an Int; OMM NORAD_CAT_ID takes up to nine digits; Alpha-5 is refused in OMM. Source: GPKit: OMM.swift:4-17; ElementSet.swift:9-10; OMMTests.swift:58-70.
  15. K-23 Alpha-5 as Space-Track defines it: A–Z without I and O = 10–33, ceiling 339999. Test vectors. Source: GPKit: Alpha5.swift:1-46; ValueTests.swift:85-101.
  16. K-26 A file cut short is refused whole. Source: GPKit: README.md:85-87; test OMMTests.swift:120.
  17. K-29 The TLE writer uses exact decimal rounding, Alpha-5 from 100000, and refuses numbers above 339999. Source: GPKit: TLE.swift:242-354; README.md:121-133.
  18. K-34 Positions are TEME; UT1 is taken as UTC (within 0.9 s); polar motion is left out. Source: GPKit: Observer.swift:102-104; README.md:218-219.
  19. K-30 SGP4 is a port of Vallado's SGP4.cpp (CelesTrak/fundamentals-of-astrodynamics 49df0479c950), with names and order of operations kept. Source: GPKit: NOTICE:9-17; SGP4Core.swift:1-15.
  20. K-31 Gravity constants WGS-72 (default), WGS-84 and WGS-72-old. Source: GPKit: SGP4Core.swift:78-108; Propagator.swift:76-94.
  21. K-32 Deep-space terms apply at a period of 225 min or more. Source: GPKit: SGP4Core.swift:44-45.
  22. K-33 Error codes map to PropagationFailure.Kind. Source: GPKit: Propagator.swift:33-48,115-122.
  23. K-36 A Propagator is a value and can be shared between tasks. Source: GPKit: Propagator.swift:71-73; test SGP4Tests.swift:170.
  24. K-37 The TLE epoch is kept exactly; the C++ reader's round trip moves it by up to 20 µs. Source: GPKit: Propagator.swift:91-93; README.md:237-243.
  25. K-35 WGS-84 ellipsoid for the observer. Source: GPKit: Observer.swift:95-100.
  26. K-40 SGP4 vs the unmodified C++: 33 cases (24 deep space), 2,349 steps, equal error codes, agreeing within 1 micrometre (asserted). Source: GPKit: SGP4Tests.swift:74-75,102-126.
  27. K-40b The same comparison with WGS-72-old, WGS-72 and WGS-84, each in modes i and a: equal error codes, within 1 micrometre. Source: GPKit: SGP4Tests.swift:188-209.
  28. K-41b The oracle compiles SGP4.cpp with #pragma STDC FP_CONTRACT OFF: Clang fuses multiply-adds by default on arm64, and Swift never does. Source: GPKit: Tests/SGP4Oracle/oracle.cpp:1-9.
  29. K-72 The Passes example (Observer with height in metres, passes(over:from:for:), rise/culmination/set). Source: GPKit: README.md:190-207.
  30. K-50 Elevation is sampled every 30 s. Source: GPKit: Passes.swift:110.
  31. K-51 Crossings are found by bisection to 0.5 ms; the highest point by golden-section search. Source: GPKit: Passes.swift:77-108.
  32. K-52 Passes shorter than a step are found. Source: GPKit: Passes.swift:147-156; test PassTests.swift:290-303.
  33. K-54b rise is nil when already up at the start and set nil when still up at the end; one culmination a pass, the highest. Source: GPKit: Passes.swift:44-49; README.md:220-222.
  34. K-54 A search stops at the first failure and keeps what it found. Source: GPKit: Passes.swift:29-31,164-175; tests PassTests.swift:313-367.
  35. K-53 No refraction; angles are geometric. Source: GPKit: README.md:216-217.
  36. K-16 Azimuth is degrees clockwise from north (90 is east); elevation is degrees above the horizon, negative below it; range is in km; range rate is in km/s, positive while receding. Source: GPKit: Observer.swift:64-73.
  37. K-60 Doppler: received = f(1 − ṙ/c), transmit = f/(1 − ṙ/c), c = 299,792.458 km/s. Source: GPKit: Observer.swift:75-88.
  38. K-61 437 MHz at 7 km/s approaching comes to about 10.2 kHz. Source: GPKit: test PassTests.swift:125-138.
  39. K-55 GPKit has no sun or visibility model and no azimuth-dependent horizon. Source: GPKit: a grep of Sources for sun/eclipse/sunlit/twilight found nothing; signature passes(over:from:for:above:).
  40. K-90 Merged into GPKit's main since 1.0.0 and not in a release: v1.0.0 is the only tag; each item below is under "Unreleased", as additions ("nothing is removed or changed"); within 1.x, code written for 1.0 keeps compiling. Source: GPKit main 11b7ab4 (PR #25, 2026-11-07): CHANGELOG.md:7-9, :92; README.md (Stability); git tag (v1.0.0 only); GitHub releases (v1.0.0 only).
  41. K-96 Sun (the sun's position and height at a place, after the NOAA calculators' formulas), Sunlight (the sky at a place: daylight, twilight or dark; whether the satellite is sunlit; phase angle; a brightness estimate) and PassVisibility (when in a pass the satellite is sunlit and when it can be seen with the eye). Source: GPKit main 11b7ab4 (PR #25, 2026-11-07): CHANGELOG.md:24-29; docs/public-api.swift:288, :301, :317, :331.