Troubleshooting & FAQ¶
Symptom-first answers to the problems people most often hit with satkit.
Each entry gives the cause and the fix in a few lines and links to the page
that covers the subject in full.
When something about data files looks wrong, start by asking satkit what it is using:
import satkit as sk
print(sk.__version__)
print(sk.utils.datadir()) # where downloads are written
print(sk.utils.data_search_dirs()) # where files are looked up, in order
print(sk.utils.is_offline()) # downloads forbidden?
print(sk.frametransform.eop_coverage()) # (first, last_observed, last) or None
print(sk.spaceweather.coverage()) # (first, last_observed, last_daily, last) or None
Warnings about Earth orientation and space weather¶
"EOP data ends at … held constant"¶
EOP data ends at 2027-10-02T00:00:00.000000Z (MJD 61680); the request for MJD UTC = 62502 and all later epochs use the last entry's values held constant. ...
Cause. An Earth-fixed frame transform (or a propagation, or gmst/gast)
was asked for an epoch past the last row of the Earth-orientation (EOP)
table. Either the epoch is genuinely in the future beyond the IERS
predictions (about a year ahead), or the file on disk is old: satkit fetches
the EOP file on first use but does not refresh it on its own after that.
Impact. Polar motion and \(\Delta UT1\) are held at their last values. They drift by ~0.1″ and ~10 ms over a few months, i.e. metres of position error at LEO.
Fix. Refresh the file, which also reloads the table in the running process:
Check an epoch before relying on it with sk.frametransform.eop_status(t),
and pass sk.propsettings(require_eop_coverage=True) to make propagate
raise instead of extrapolating (or, before 1973-01-02, instead of using zero
EOP). See EOP coverage.
"no Earth Orientation Parameters (EOP) table is loaded"¶
Cause. No finals2000A.all is in any search directory
and the first-use download failed (no network, SATKIT_OFFLINE=1, a proxy,
a read-only data directory). A data directory copied from satkit 0.23 or
earlier may hold only CelesTrak's EOP-All.csv, which satkit no longer reads;
it does not count as an EOP file.
Impact. Polar motion, \(\Delta UT1\) and the celestial-pole offsets are
treated as zero. \(\Delta UT1\) is kept within 0.9 s (up to ~13″ of Earth
rotation) and polar motion is up to ~0.6″, so Earth-fixed frame transforms are
off by up to ~12″ over the file's history (~10″ since 2000, typically ~3″),
i.e. hundreds of metres at LEO.
propagate() refuses to run rather than integrate with a mis-oriented
gravity field (RuntimeError: no Earth Orientation Parameters (EOP) table is loaded ...).
Fix. Run sk.utils.update_datafiles() on a machine with network access,
or point SATKIT_DATA at a directory that holds the file. If the download
itself fails, the entries under Data files and downloads
below cover the usual reasons.
"EOP data not available for MJD UTC = … (too early)"¶
EOP data not available for MJD UTC = 40000 (too early): the loaded table starts at 1973-01-02T00:00:00.000000Z (MJD 41684), and polar motion, UT1-UTC and nutation corrections are treated as zero (UT1 = UTC) before it.
finals2000A.all has no EOP data before 1973-01-02; refreshing the data files does not change this.
Cause. The epoch is before the start of the loaded EOP table. The IERS
finals2000A.all file begins on 1973-01-02, so refreshing the data does
not move the start of the table.
Impact. Before 1973 polar motion and the celestial-pole offsets are zero and UT1 = UTC, as in ERFA when no EOP are supplied.
If the warning names a start other than 1973-01-02, the table was loaded by
hand (Rust earth_orientation_params::init_from_path / init_from_bytes)
or the file is truncated; it
carries no advice line then.
Space-weather warnings¶
| message begins | cause | fix |
|---|---|---|
no space-weather table is loaded |
no space-weather files on disk and the first-use download failed. NRLMSISE-00 runs on \(F_{10.7} = F_{10.7A} = 150\), \(A_p = 4\), which can be wrong by a factor of two in density | sk.utils.update_datafiles() with network access, or SATKIT_DATA pointing at a directory with the files |
the space-weather table ends at … |
the epoch is past the last row; that row's values are used unchanged | the warning says which case applies. "The table ends in the past, so it is out of date": an old file or a table loaded by hand — refresh with sk.utils.update_datafiles(). "The table already reaches past today to the end of its long-range forecast": the epoch is beyond the NASA MSAFE forecast (about 15 years ahead) and no refresh can help |
the space-weather record for … is a monthly prediction |
the record carries \(F_{10.7}\) but no \(K_p\)/\(a_p\) (a CelesTrak SW-All.csv left in a data directory by satkit 0.22 or earlier, or loaded by hand), so NRLMSISE-00 uses a quiet-time \(A_p = 4\) |
sk.utils.update_datafiles() fetches the GFZ / SWPC / MSAFE files, whose monthly rows do carry \(A_p\) |
sk.spaceweather.get(t) itself raises RuntimeError: No space weather record found for date
when no table is loaded. See Space weather coverage
for sk.spaceweather.status(t) and what each block of the table can tell the density model.
Warnings and logging: how do I silence or redirect these warnings?¶
satkit's warnings (these, and the ones about data files, downloads and
SATKIT_CA_BUNDLE) go through the standard logging system of each language,
under the module that raised them: satkit.earth_orientation_params,
satkit.spaceweather, satkit.utils.download, and so on. Most are shown once
per process. They are not Python warnings, so warnings.filterwarnings(...)
and python -W ignore have no effect.
Python. The records go to the logging module, as children of the
satkit logger. With logging not configured, WARNING records reach stderr
through logging's last-resort handler (the message alone; in Jupyter a
stderr block under the cell), as they did before 0.24. To silence them all, or
only those of one module:
import logging
logging.getLogger("satkit").setLevel(logging.ERROR)
logging.getLogger("satkit.spaceweather").setLevel(logging.ERROR)
To send them elsewhere, configure logging as usual, e.g.
logging.basicConfig(format="%(levelname)s %(name)s: %(message)s") or a
handler on the satkit logger. The first-use JPL ephemeris download is
announced as a warning, so it shows by default; the per-file "Downloading …"
notices from utils.download_file are logged at INFO and appear only when
logging is configured at that level: logging.basicConfig(level=logging.INFO).
Rust. The records go through the log facade with
the module path as target. With no logger installed they are printed to
stderr (Warning: …, or satkit: … for a notice). Install any log backend
(env_logger, tracing-subscriber with its log bridge, …) to take them
over, and filter by target, e.g. RUST_LOG=satkit=error with env_logger.
Either language. The EOP and space-weather warnings can also be turned off at the source (each warning's "To disable" line names the function):
and SATKIT_QUIET=1 still suppresses the gravity-model and IERS-table
warnings. Silencing a warning does not change the result: prefer refreshing
the data, or checking eop_status(t) / spaceweather.status(t) explicitly.
The Earth-orientation or space-weather data is old, although I have network access¶
Cause. satkit downloads these files on first use only when no copy exists
in any search directory, and never refreshes them on its own. A copy that
is already there — from an earlier run, a legacy ~/.satkit-data, a directory
copied from another machine — is used as it is, and it
ages from the day it was written.
Fix. Run sk.utils.update_datafiles() periodically (it is cheap: see
below).
The fresh files go to sk.utils.datadir(). When several copies of one of
these files exist across the search directories, satkit uses the one whose
table runs latest (for finals2000A.all, the latest observed row), so an old
copy elsewhere does not shadow the refreshed one. The GFZ / SWPC / MSAFE files
are also preferred over an older CelesTrak SW-All.csv. See
Data Directories.
Data files and downloads¶
Where are my data files, and which copy is being used?¶
Downloads are written to exactly one directory, sk.utils.datadir():
SATKIT_DATA if set, else the directory given to sk.utils.set_datadir(),
else the platform user-data directory (~/Library/Application Support/satkit-data
on macOS, ~/.local/share/satkit-data on Linux, %LOCALAPPDATA%\satkit-data
on Windows). Files are looked up across sk.utils.data_search_dirs() and used
from the first directory that contains them — except the Earth-orientation and
space-weather files, where the copy whose data runs latest is used, so a stale
copy in an earlier directory (an add_search_dir() directory, say) does not
shadow a refreshed one in datadir(). Nothing is ever written inside site-packages. Full table: Data Directories.
What needs no data directory at all: the IERS nutation tables and the EGM96 / EGM2008 / JGM-2 / JGM-3 gravity models are compiled in, so SGP4, time scales, gravity, Keplerian propagation and Lambert targeting work straight after install. See Data Files.
The first propagate() or ephemeris query takes a long time¶
The first call that needs the JPL ephemeris downloads it (DE440, 102 MB,
SHA-256 verified), once. Run sk.utils.update_datafiles() to take the hit up
front, or set SATKIT_JPLEPHEM_FILE=lnxp1900p2053.421 for the 14 MB DE421
(1900–2053). See Selecting a JPL ephemeris file.
The download is announced with a warning on the satkit.jplephem logger, so it
is shown by default (see Warnings and logging).
update_datafiles() says "no request made" — how do I force a refresh?¶
This is intended: the Earth-orientation and space-weather files are only
re-requested once their publication cadence has passed, and then
conditionally, so calling update_datafiles() at the top of every script is
fine. update_datafiles(overwrite=True) forces a transfer of everything
(including the 102 MB ephemeris); deleting a file's <name>.http-cache
sidecar in datadir() forces just that file. overwrite and dir are
keyword-only; anything else is rejected
(TypeError: update_datafiles() got an unexpected keyword argument 'force').
See How often EOP and space weather are refreshed.
"No writeable data directory" or "Read-only file system" (containers, shared installs)¶
RuntimeError: No writeable data directory: /data/satkit could not be created or is not writable (...). Set SATKIT_DATA to a directory satkit may write to
RuntimeError: Data directory /data/satkit is not writable (Read-only file system (os error 30)). Pass a writable directory (Python: update_datafiles(dir=...)), or set the environment variable SATKIT_DATA to one and restart
satkit only writes to datadir(). Point SATKIT_DATA at a writable volume,
or call sk.utils.set_datadir(path) with an existing directory before the
first data access (SATKIT_DATA takes precedence). Read-only directories are
fine as search locations: provision the files once
and they are read from there. See
Data Directories.
Downloads fail with invalid peer certificate: UnknownIssuer¶
RuntimeError: could not fetch https://www-app3.gfz-potsdam.de/kp_index/Kp_ap_Ap_SN_F107_since_1932.txt: io: invalid peer certificate: UnknownIssuer
Typically a corporate TLS-inspecting proxy re-signed the connection with a
private CA that is not in the operating system's trust store, which is what
satkit verifies against (SSL_CERT_FILE and REQUESTS_CA_BUNDLE are
ignored). Install the CA system-wide, or set SATKIT_CA_BUNDLE to a PEM file
holding it together with the public roots; SATKIT_CA_BUNDLE=webpki covers
a container with no trust store. See
Downloads behind a TLS-inspecting proxy.
Downloads have to go through an HTTP proxy¶
The standard HTTPS_PROXY / HTTP_PROXY / ALL_PROXY / NO_PROXY
variables are honoured for every download. Where outbound access is blocked,
SATKIT_DATA_URL can name an internal mirror for the hash-pinned files, but
not for Earth orientation and space weather: copy a data directory
instead. See Environment variables and API.
HTTP 503 (or 403) from CelesTrak¶
CelesTrak returned HTTP 503 for https://celestrak.org/NORAD/elements/gp.php?... CelesTrak throttles repeated identical GP queries ...
Cause. CelesTrak rate-limits clients that fetch the same element sets
repeatedly — typically a script or notebook that calls sk.TLE.from_url(...)
or sk.omm_from_url(...) on every run. It asks for at most one request per
object every ~2 hours. A 403 can also come from a filtering proxy.
Fix. Do not retry in a loop. Save the response text once and parse the
saved copy with sk.TLE.from_file(...) / sk.TLE.from_lines(...) or
sk.omm_from_file(...) / sk.omm_from_text(...), re-fetching only when you
need newer elements. satkit's own data refreshes do not contribute to this:
they go to the IERS, GFZ, NOAA and NASA servers and are rate-limited to each
file's publication cadence.
Running offline or air-gapped¶
Everything compiled in works with no network. The JPL ephemeris, the EOP and
space-weather tables, and ITU_GRACE16 (if selected) are files: fill a data
directory with update_datafiles() on a connected machine, copy it across and
point SATKIT_DATA at it, optionally with SATKIT_OFFLINE=1 so a missing
file fails fast. The steps are in Provisioning up front.
The EOP and space-weather files still age offline: repeat the copy
periodically.
"… is not present and cannot be downloaded (SATKIT_OFFLINE is set)" (or "offline mode was turned on …")¶
RuntimeError: ... linux_p1550p2650.440 is not present and cannot be downloaded (SATKIT_OFFLINE is set). Provide it in the data directory (SATKIT_DATA); sources: https://github.com/ssmichael1/satkit-data/releases/download/data-v1/linux_p1550p2650.440, ...
Downloads are forbidden and the file is in none of the search directories; the
parenthesis says whether SATKIT_OFFLINE or set_offline(True) is the cause.
Put the file (the message lists its URLs) in datadir() or any search
directory, or check sk.utils.is_offline(): a leftover SATKIT_OFFLINE in a
CI environment is a common cause. update_datafiles() checks first and fails
without downloading anything or creating a directory:
RuntimeError: update_datafiles cannot run: downloads are forbidden (SATKIT_OFFLINE is set); nothing was fetched
Installation¶
pip install satkit tries to compile, or fails asking for Rust¶
Pre-built wheels exist for CPython 3.11–3.15 on Linux x86_64 and aarch64
(glibc), macOS on Apple silicon (arm64), and Windows x86_64. Anywhere else pip
builds the source distribution, which needs a stable Rust toolchain
(rustup): Intel Macs (use conda-forge, or
pip install --no-binary satkit satkit), Alpine and other musl images, and
Python 3.9 or older, which is not supported. See Installation.
Should I use pip or conda?¶
Either: the conda-forge package is built from the same source distribution and behaves identically, and it also builds for Intel Macs. See Conda.
API changes that look like errors¶
DeprecationWarning: time.as_mjd() is deprecated since 0.23 ...¶
In 0.23 the Python as_* conversion methods were renamed to_*, to pair with
the from_* constructors. The old names still work but warn. The deprecated
names below that are removed in 0.25:
| deprecated | use |
|---|---|
time.as_date(), as_gregorian(), as_datetime(), as_mjd(), as_jd(), as_unixtime(), as_iso8601(), as_rfc3339() |
to_date(), to_gregorian(), to_datetime(), to_mjd(), to_jd(), to_unixtime(), to_iso8601(), to_rfc3339() |
quaternion.as_rotation_matrix(), as_euler() |
to_rotation_matrix(), to_euler() |
frametransform.eop_source() (deprecated in 0.24; the table is always finals2000A.all) |
frametransform.eop_coverage(), which is None when no table is loaded |
These warn but have no removal date:
| deprecated | use |
|---|---|
time.datetime() |
time.to_datetime() |
kepler.w (property) |
kepler.argp (w is kept indefinitely, with a warning attributed to your line) |
Python hides DeprecationWarning outside __main__ by default; run with
python -W error::DeprecationWarning to find every remaining call.
TypeError from satproperties(...) or a misspelt keyword¶
TypeError: satproperties() takes keyword arguments only: cdaoverm=, craoverm=, thrusts=, ecom= (2 positional arguments given; ...)
TypeError: propsettings.__new__() got an unexpected keyword argument 'gravity_degre'
Since 0.24 satproperties() is keyword-only: write
sk.satproperties(cdaoverm=..., craoverm=...). Through 0.23 two positional
arguments were bound as (craoverm, cdaoverm), the reverse of the docs, so
results from old positional calls had drag and radiation pressure swapped and
are worth re-checking. The keyword arguments of duration(), propsettings(),
itrfcoord(), sgp4(), propagate(), satstate.propagate(), gravity(),
gravity_and_partials() and nrlmsise00() are real Python keyword arguments
too, so a misspelt one raises TypeError (it was ValueError through 0.23).
See Migrating to 0.24.
AttributeError: 'list' object has no attribute 'epoch' after loading a TLE¶
TLE.from_lines(), TLE.from_file() and TLE.from_url() return a list,
even for one element set (since 0.24). Take the first one:
tle = sk.TLE.from_lines(lines)[0]. See
Loading TLEs.
TypeError: 'float' object is not callable (or 'int', or 'satkit.weekday')¶
A zero-argument member that describes the object is a property — call it
without parentheses. Since 0.22.1 this includes quaternion.norm,
time.day_of_year, time.weekday and tlefitstatus.converged, alongside
quaternion.angle and propresult.can_interp:
q = sk.quaternion.rotz(0.1)
t = sk.time(2024, 1, 1)
q.norm, t.day_of_year, t.weekday # not q.norm(), t.day_of_year(), t.weekday()
The reverse also changed: quaternion.conj and quaternion.conjugate are
methods (q.conj()), like q.inverse(). Without the parentheses you get
the bound method, and using it fails with
TypeError: unsupported operand type(s) for *: 'builtin_function_or_method' and ....
The rule: a property tells you something about the object; a method gives you
something to use instead of it.
Results that look wrong¶
Times are off by 69.184 s (or 37 s, or 18 s)¶
satkit.time values are unambiguous instants, but constructors and
conversions interpret numbers as UTC unless told otherwise, and times
print in UTC. TT − UTC is 69.184 s, TAI − UTC 37 s, GPS − UTC 18 s (since 2017).
Pass the scale explicitly:
t = sk.time(2024, 1, 1, 12, 0, 0, scale=sk.timescale.TT)
print(t) # 2024-01-01T11:58:50.816000Z (printed in UTC)
t.to_mjd(sk.timescale.TT) # to_mjd() alone means UTC
sk.time.from_mjd(60310.0, sk.timescale.TT)
See the Time Systems tutorial.
Adding a number to a time jumps by days¶
time + float adds days: t + 60 is two months later, not one minute.
Use a duration for anything else:
RuntimeError: Must pass in year, month, day or year, month, day, hour, min, sec¶
The time constructor takes either the date alone or all six fields:
sk.time(2024, 1, 1) or sk.time(2024, 1, 1, 12, 0, 0), not
sk.time(2024, 1, 1, 12).
A datetime without a time zone is read as local time¶
sk.time.from_datetime(dt) (and SGP4 given a list of datetimes) converts the
instant the datetime represents, and Python treats a naive datetime as
local time. On a machine in New York, datetime(2024, 1, 1, 12) becomes
17:00 UTC. This is intentional and matches Python's own convention
(datetime.timestamp(), datetime.now()), and to_datetime(utc=False)
returns a naive local-time datetime that round-trips. For UTC, attach a time
zone (tzinfo=datetime.timezone.utc) or construct sk.time directly; avoid
datetime.utcnow(), which returns a naive UTC value (and is deprecated since
Python 3.12).
SGP4 positions are 1000 times larger than expected, or in the wrong frame¶
sk.sgp4() returns metres and metres/second — like every other
position in satkit, not the kilometres of most SGP4 libraries — in the
TEME frame. Rotate before comparing with anything in GCRF or ITRF, and
before using an SGP4 state as the initial state of propagate (which works
in GCRF):
import numpy as np
tle = sk.TLE.from_lines([
"1 25544U 98067A 24001.50000000 .00016717 00000-0 30306-3 0 9999",
"2 25544 51.6416 247.4627 0006703 130.5360 325.0288 15.49815367432041",
])[0]
p_teme, v_teme = sk.sgp4(tle, tle.epoch)
q = sk.frametransform.qteme2gcrf(tle.epoch)
state_gcrf = np.concatenate([q * p_teme, q * v_teme])
res = sk.propagate(state_gcrf, tle.epoch, duration_days=1.0)
sk.frametransform.rotation(from_frame=sk.frame.TEME, to_frame=sk.frame.ITRF, tm=t)
gives the rotation to the Earth-fixed frame. Remember that a TLE holds SGP4
mean elements, which are not osculating Keplerian elements. See
TLEs, SGP4 & OMMs.
ValueError: invalid Keplerian element incl = 51.6: inclination must be in [0, π] radians¶
sk.kepler takes angles in radians and distances in metres. TLE
fields are in degrees (tle.inclination, tle.raan, …), so copying them
across needs math.radians(); only the inclination is range-checked, so a
RAAN or anomaly in degrees is accepted silently. A semi-major axis in
kilometres is accepted too (it is positive), and describes an orbit inside the
Earth. sk.itrfcoord has separate latitude_deg= / latitude_rad= keywords
and takes altitude in metres.
sgp4() returns nan¶
SGP4 failed for that element set and time, most often because the satellite
has decayed or the time is far from the TLE epoch. Pass errflag=True to also get
the error code for each output, as an int32 NumPy array that compares
element-wise with sk.sgp4_error values (err == sk.sgp4_error.orbit_decay):
A TLE with ephemeris type 4 raises
RuntimeError: Ephemeris type 4 (SGP4-XP) is not supported ... instead:
satkit implements classic SGP4 only.
My propagation disagrees with GMAT, STK or Orekit¶
Check the force model before suspecting the integrator. The propsettings
defaults are a quick, not a high-fidelity, configuration:
- Gravity is 4×4 by default (
gravity_degree,gravity_order; maximum 70). For precision LEO work use tens of degrees. The default model is EGM2008 since 0.23 (EGM96 before);gravity_model=sk.gravmodel.egm96reproduces older results. - Drag and solar radiation pressure are only applied when you pass
satproperties=sk.satproperties(cdaoverm=..., craoverm=...); without it neither force is included. Drag is skipped above ~1,000 km altitude. - Space weather and EOP follow the tables above: past their coverage the inputs are forecasts or held constant.
See the Force Model guide and Validation: GMAT Comparison for measured agreement.