How GPKit is tested
Five kinds of evidence, each with what it covers and what it doesn't.
1. SGP4 against the C++ it was ported from
GPKit's tests compile David Vallado's SGP4.cpp, unmodified, and run it beside GPKit's port on the same inputs.K-30K-40
- The 33 verification cases published with "Revisiting Spacetrack Report #3": near-Earth and deep-space orbits, the half-day and one-day resonances, and the runs that end in each error. Every time of every case, 2,349 steps; 24 of the cases are deep space.K-40
- Result, asserted by the tests: at every step the port agrees with the reference C++ within 1 micrometre in position (and within 1 micrometre per second in velocity), with the same error code.K-40
- Every option: the same holds with WGS-72-old, WGS-72 and WGS-84, each in both of the C++'s operation modes. K-40b
- Fairness: the C++ is compiled with floating-point contraction off. Clang fuses multiply-adds by default on arm64 and Swift never does, so the two would otherwise differ for a reason that is neither's arithmetic.K-41b
- From the lines: with each side reading the lines itself, the agreement at the C++ reader's epoch is the same 1 micrometre. At GPKit's exact epoch, the C++ reader's epoch round trip (up to 20 µs) shows up, and the tests bound it: under 5 mm for case 23333, a deep-space orbit of eccentricity 0.97, and under 1 mm for every other case. K-37K-43b
2. Passes and positions against Skyfield
Skyfield 1.55, an independent Python library, computed the passes of five public element sets over 40° N, 75° W for two days each. Its answers are committed to GPKit's tests, and CI compares against them.K-42
| largest difference (recorded run, 2026-10-07) | |
|---|---|
| 49 passes: 95 rises and sets | 0.166 s |
| 47 culminations | 0.349 s |
| elevation, azimuth (329 times) | 0.0044°, 0.0100° |
| range, range rate | 84.0 m, 1.25 m/s |
| with UT1 applied as Skyfield does | elevation within 0.000001°, range within 0.017 m |
K-42 The gap closes when the Earth is turned by UT1 rather than UTC, which Skyfield applies and GPKit doesn't (GPKit takes UT1 to be UTC).K-34K-42 Skyfield's own search stops within half a second of an event.K-42
The two count culminations differently. Skyfield gives every peak of the elevation, and GPKit gives the highest of a pass. The comparison allows for that, pass for pass.K-42
3. SGP4 against python-sgp4, over real catalogs
Every element set of a gpconf corpus copy: 14,411 sets in CSV, JSON, TLE and XML files, each read by each library's own reader and propagated to 0, 90, 720, 1,440, 4,320 and 10,080 minutes from its epoch. Over 86,466 states the largest difference in position is 0.0024 mm, and no error code differs. Recorded 2026-10-07 with python-sgp4 2.27.K-43
This one isn't in CI: it needs a corpus copy that holds the provider files, which isn't in the repository.K-43
4. GPKit's own tests
68 tests in 14 suites, in Swift Testing. They cover:
- the readers (TLE, CSV, JSON, XML, KVN), refusals, exact decimals and epochs;
- Alpha-5, with Space-Track's examples;
- the TLE writer, including that what was read is written back byte for byte;
- SGP4, passes, look angles and Doppler.
The README's examples are compiled and held to the words the README prints.K-44
CI builds and tests on macOS and Linux, builds for iOS, tvOS and visionOS, and checks that the core compiles with no Foundation.K-14K-04
What isn't here: no fuzzing, mutation testing or property-based tests were found in the repository.K-45
5. gpconf
gpconf is a free test kit that tells you whether your satellite software handles catalog numbers above 99,999 — built from real CelesTrak data, with every expected answer traced to its source.G-01
GPKit ships an adapter for gpconf's runner (Sources/gpconf-adapter). CI runs gpconf on every push, offline: the five
cases that need no provider file. A hosted runner's address is shared, and the corpus asks that its fetch not run in a
loop or in CI.K-82 In gpconf's own words, a run like that is "5 of 18 cases graded offline; not a full conformance
result."G-21b
The result: gpconf 0.8.0
17 of 18 cases pass, each on every item exactly. None fails, none is skipped, and none passes only within a
tolerance. The eighteenth, satcat-70000-cutoff, checks data and runs no parser, so it is not exercised.K-83
| gpconf | 0.8.0 from PyPI, corpus 0.8.0, run 5 November 2026 (00:36 UTC) |
| GPKit | 1229da2, the code of 1.0.0, debug build |
| platform | macOS 27.2 arm64, Apple Swift 6.4.2, Python 3.14.6 |
| data | the maintainer's provider files, fetched 21 September 2026: 128 files. The network was closed to the run |
| cases | 18: pass 17, pass-tolerance 0, fail 0, skip 0, not-fetched 0, not-available 0, not-exercised 1 |
| items | pass 367, info 106, not-exercised 1 |
| the gate, this month's launches | TLE: 256 of 256 loaded; CSV: 256 of 256 loaded. None refused, misidentified or dropped |
| report | SHA-256 f858f93f4fa0f23dbe04ecb5313e63bcc2303cc8653853ffded393b510250a62 |
Every case
| case | verdict | items | |---|---|---| | `epoch-year-19xx` | pass | pass 34, info 9 | | `baseline-iss-five-formats` | pass | pass 27, info 9 | | `six-digit-omm-saramago` | pass | pass 28, info 10 | | `tle-omits-six-digit-objects` | pass | pass 6, info 3 | | `analyst-objects` | pass | pass 66, info 14 | | `nine-digit-supgp-launch-nominals` | pass | pass 26, info 6 | | `supgp-celestrak-classification-c` | pass | pass 24, info 6 | | `bstar-and-derivative-forms` | pass | pass 24, info 8 | | `satcat-70000-cutoff` | not-exercised | not-exercised 1, info 6 | | `csv-json-omitted-mandatory-fields` | pass | pass 26 | | `mean-motion-derivative-convention` | pass | pass 6 | | `tle-vs-omm-precision-loss` | pass | pass 6 | | `omm-xml-schema` | pass | pass 16, info 8 | | `alpha5-encoding-vectors` | pass | pass 5 | | `alpha5-tle-derived` | pass | pass 18, info 12 | | `kvn-syntax-variants` | pass | pass 29, info 6 | | `tle-writer-alpha5` | pass | pass 5, info 2 | | `corrupt-input` | pass | pass 21, info 7 | K-83Run it yourself
The commands, from GPKit's own record of the runK-83:
python3 -m venv venv && ./venv/bin/python -m pip install gpconf==0.8.0
swift build
./venv/bin/gpconf run --data DATA --no-fetch-hint -v \
--cmd "'$PWD/.build/debug/gpkit-gpconf' parse {fmt}" \
--vectors-cmd "'$PWD/.build/debug/gpkit-gpconf' vectors" \
--write-cmd "'$PWD/.build/debug/gpkit-gpconf' write" \
--json report.json
python3 tools/check_gpconf.py report.json --gate
DATA is a folder holding fixtures/<case>/raw/, as gpconf fetch writes it. A later fetch reads other records,
so the counts of items can differ; the verdicts should not.K-83 gpconf's own published counts were measured with
launch-window files that a new fetch can't obtain.G-26 The provider files' fingerprint is in GPKit's record, with
statuses and counts only: an item's detail can quote provider values, so the full report isn't kept.K-83
Reading a gpconf result
| status | means |
|---|---|
pass |
every check passed exactly |
pass-tolerance |
passed only because of a tolerance (1e-12 relative for a binary float, 2 µs for an epoch), reported apart from an exact pass |
fail |
a check failed |
skip |
the adapter said it can't read that format. That's the adapter's claim, reported and never verified |
not-exercised |
a data check that runs no parser, or data that didn't occur |
not-fetched |
a provider file that isn't on disk |
not-available |
a launch-window capture that no fetch requests |
G-21G-22G-23G-24 gpconf's own rule applies to every count: "a count is a result against that version on that date, not a verdict on the project."G-25
Sources
K-30SGP4 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.K-40SGP4 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.K-40bThe same comparison with WGS-72-old, WGS-72 and WGS-84, each in modesianda: equal error codes, within 1 micrometre. Source: GPKit:SGP4Tests.swift:188-209.K-41bThe 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.K-37The 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.K-43bFrom the lines: at the C++ reader's epoch within 1 µm; at GPKit's own epoch, case 23333 under 5 mm and every other under 1 mm (asserted); the two epochs within 25 µs. Source: GPKit:SGP4Tests.swift:128-167.K-42Skyfield 1.55: five public element sets over 40° N, 75° W, two days each; 49 passes; 95 rises and sets within 0.166 s; 47 culminations within 0.349 s; look-angle bounds. Source: GPKit:conformance/skyfield-2026-10-07.txt:1-19; testPassTests.swift:168-248.K-34Positions 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.K-43python-sgp4 2.27: 14,411 element sets, 86,466 states (0, 90, 720, 1,440, 4,320 and 10,080 min), largest difference 0.0024 mm, no differing error codes. Not run in CI. Source: GPKit:conformance/python-sgp4-crosscheck-2026-10-07.txt:5-10;CrossCheckTests.swift:5-14.K-4468 tests in 14 suites; the README examples are compiled by tests. Source: GPKit: grep of@Testand@Suite;ReadmeTests.swift:5-6.K-23Alpha-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.K-29bWhat was read is written back byte for byte (test). Source: GPKit:TLETests.swift:155-160.K-14No Foundation outside Date.swift, enforced by a script in CI. Source: GPKit:Package.swift:14-15;tools/check-no-foundation.sh;ci.yml:24-31.K-04Platforms: macOS 13, iOS 16, tvOS 16, visionOS 1. Linux is built in CI. Source: GPKit:Package.swift:9;.github/workflows/ci.yml:16.K-45No fuzz, mutation or property tests. Source: GPKit: a grep that found none.G-01The written sentence (quoted exactly). Source: gpconf:docs/BRAND.md:40.K-82CI runs gpconf's 5 offline cases; 13 need provider files that gpconf fetches and does not ship. Source: GPKit:README.md:153-155;ci.yml:33-56;tools/check_gpconf.py.G-21bgpconf's run line for an offline run: "5 of 18 cases graded offline; not a full conformance result.". Source: gpconf:docs/ADAPTERS.md:688-690.K-83gpconf 0.8.0 (PyPI), corpus 0.8.0, run 2026-11-05 00:36:31Z: GPKit 1229da2 (1.0.0's code), debug, macOS 27.2 arm64, Swift 6.4.2, Python 3.14.6; the maintainer's provider files of 2026-09-21 (128), network closed; cases 18, pass 17, pass-tolerance 0, fail 0, skip 0, not-fetched 0, not-available 0, not-exercised 1 (satcat-70000-cutoff); items pass 367, info 106, not-exercised 1; gate 256/256 TLE and CSV; report SHA-256 f858f93f…0a62; the commands; per-case verdicts and item counts. Source: GPKit:conformance/gpconf-0.8.0-2026-11-05.txton main at 6d6ce58 (PR #1, merged 2026-11-05T00:42:49Z; first read at 216459a).G-26Published counts were measured with launch-window files that a new user's fetch can't obtain. Source: gpconf:README.md:380-382.G-21How a case's status is chosen (first of fail, pass-tolerance, pass, not-exercised, not-fetched, not-available, skip). Source: gpconf:docs/ADAPTERS.md:677-680.G-22pass-toleranceis reported apart from an exactpass; tolerances are 1e-12 relative and 2 µs. Source: gpconf:README.md:348-355.G-23A skip is the adapter's claim, reported and never verified. Source: gpconf:docs/ADAPTERS.md:66-69.G-24not-fetched and not-available definitions. Source: gpconf:README.md:359-371.G-25"a count is a result against that version on that date, not a verdict on the project". Source: gpconf:README.md:242.