# Changelog
Only recent releases are listed. Older entries are in this file's git history (`git show vX.Y.Z:CHANGELOG.md`) and on the [GitHub Releases](https://github.com/ssmichael1/satkit/releases) page.
## 0.22.0 - 2026-09-12
### Added
- Kepler: checked constructor `Kepler::try_new` / `Kepler::validate` (Python: the constructor and the `a`/`eccen`/`inclination`/`mu` setters raise `ValueError` for a non-finite value, `a <= 0`, `eccen` outside [0, 1), `incl` outside [0, π] or `mu <= 0` instead of producing NaN); per-instance gravitational parameter `mu` (`Kepler::with_mu`, `from_pv_with_mu`; Python `kepler(..., mu=)`, `kepler.mu`, `from_pv(..., mu=)`) so lunar and heliocentric elements have a correct period, `propagate` and `to_pv`; derived quantities `periapsis`, `apoapsis`, `specific_energy`, `angular_momentum`, `flight_path_angle`, `argument_of_latitude`, `true_longitude`; `SatState::from_kepler` / `satstate.from_kepler(time, kepler)`; a one-line `repr` for Python `kepler`; `Kepler` derives `PartialEq` and serde (`mu` defaults to Earth's when absent), `Anomaly` derives `Copy`/`PartialEq` ([#168](https://github.com/ssmichael1/satkit/pull/168))
- `propsettings.initial_step_secs` and `propresult.next_step_secs` (Rust: `PropSettings::initial_step_secs`,
`PropagationResult::next_step_secs`). The adaptive integrators no longer use numeris' starting-step heuristic,
which is scale sensitive and started `rkv98` at a fraction of a millisecond for an orbit in metres and seconds —
about half the force evaluations of a one-hour arc at 1e-9 tolerance were spent growing that first step
([#175](https://github.com/ssmichael1/satkit/pull/175) discussion). The default first step is now derived from the
initial state, the tolerances and the integrator order (`1.5·|r|/|v|·tol^(1/(p+1))`, within ~2.5× of the settled
stride across the RK integrators from 1e-6 to 1e-12); `initial_step_secs` overrides it, and `next_step_secs` reports the
integrator's working stride at the end of an arc so a follow-on arc can warm-start at full stride
(`ps.initial_step_secs = res.next_step_secs`). Requires numeris 0.6 ([#178](https://github.com/ssmichael1/satkit/pull/178))
- `integrator.rkv98` with `enable_interp=False` now runs the 16-stage `rkv98_nointerp` tableau automatically:
the five extra stages of the 21-stage tableau exist only to build the interpolant, so this is the same order
and error control at 24% fewer force evaluations per step. Results change at the tolerance level for that
combination (a different tableau takes different steps) ([#178](https://github.com/ssmichael1/satkit/pull/178))
### Distribution
- **Breaking (Python packaging):** macOS wheels are arm64 (Apple silicon) only; the `x86_64-apple-darwin` wheels are no longer built, matching SciPy/Polars and the arm64-only GitHub runners, and the macOS deployment target moves from 10.12 to 11.0. Intel-Mac users install from source (`pip install --no-binary satkit satkit`, stable Rust toolchain required) or from conda-forge, which builds `osx-64` ([#172](https://github.com/ssmichael1/satkit/pull/172))
### Fixed
- Space-weather records are indexed by UTC calendar day. The lookup counted continuous days from the first record, and since `Instant` counts leap seconds the last 37 seconds of each UTC day read the *next* day's record (F10.7, Ap, and the 3-hourly Ap history fed to NRLMSISE-00). Density, and hence drag, in that window changes very slightly (contributed by @scottshambaugh, [#176](https://github.com/ssmichael1/satkit/pull/176))
- `sgp4()` rejects SGP4-XP element sets (TLE ephemeris type 4, OMM `EPHEMERIS_TYPE` 4) with a clear error instead of propagating them: an SGP4-XP line 1 stores agom and a B term in the columns a classic TLE uses for nddot and B*, so the old behaviour ran classic SGP4 on the wrong inputs and returned a plausible but wrong state ([#174](https://github.com/ssmichael1/satkit/pull/174))
- `satkit.density.nrlmsise(altitude_m, latitude_rad, longitude_rad, time)` converts its radian latitude/longitude to the degrees the model takes; the values were passed through unchanged, so a caller following the stub at 60° N was evaluated at 1.05° N (the `itrfcoord` overload and `nrlmsise00(latitude_deg=...)` were already correct) ([#171](https://github.com/ssmichael1/satkit/pull/171))
- A corrupt or truncated `tab5.2*.txt` in a data directory no longer panics the first frame transform: satkit warns and uses the compiled-in copy of the same IERS table (exact, not an approximation), and the parser now rejects text with no table header or fewer rows than declared — an HTML notice page saved under the table's name previously loaded as six empty series and silently dropped the nutation terms ([#166](https://github.com/ssmichael1/satkit/pull/166))
### Changed
- **Breaking (Rust):** `Kepler.w` is renamed `Kepler.argp`, the struct gains a `mu` field (struct-literal construction must supply it — prefer `Kepler::new(...).with_mu(...)`), and `kepler::Error` is `#[non_exhaustive]` (new `InvalidElement` variant). Python: `argp` is the constructor parameter and property name; `w` still works as a constructor keyword and as a property (kept indefinitely) but emits `DeprecationWarning`. **Breaking (Python):** `kepler.from_pv` raises `ValueError` instead of `RuntimeError` for a hyperbolic/parabolic or rectilinear state, and every element setter (`a`, `eccen`, `inclination`, `raan`, `argp`, `nu`, `mu`) raises `ValueError` for an out-of-domain or non-finite value instead of accepting it ([#168](https://github.com/ssmichael1/satkit/pull/168))
- OMM interface overhaul: `omm_from_url` returns every field the source provided (metadata, `COMMENT`, Space-Track extras such as `OBJECT_TYPE`/`RCS_SIZE`/`TLE_LINE1`, XML `USER_DEFINED` parameters) instead of 17 keys; new `omm_from_file` / `omm_from_text` loaders (JSON or XML, detected from content) replace the `xmltodict` workaround; `TLE.from_omm` / `TLE.to_omm` convert between the representations; `sgp4` dict inputs go through the same serde parser as Rust (quoted numbers, `null`/empty optionals, case-insensitive metadata, any level of an xmltodict tree, `EPOCH` as `time`/`datetime`); propagation rejects `EPHEMERIS_TYPE` 4 (SGP4-XP) instead of running classic SGP4 on it; stubs gain the `OMMDict` type. Rust: `OMM.epoch` is an `Instant` (was a string; `epoch_instant()` deprecated), `OMM` derives `Serialize`, gains `from_mean_elements`, `from_tle`/`to_tle`, `from_json_value`, `from_text`/`from_file`, `reset_cache`; `from_json_string` accepts a bare object; `omm::Error` is `#[non_exhaustive]` with a single `InvalidField` variant; `Default` is removed ([#173](https://github.com/ssmichael1/satkit/pull/173))
- Propagation step time is ~3x faster (LEO, 40x40 field, drag, rkv98): the spherical-harmonic gravity kernels dispatch at runtime to a copy compiled with FMA on x86-64 (their `mul_add` calls were libm function calls on the baseline x86-64 that wheels target; ~5x on the 40x40 field, no change on arm64 where FMA is native), NRLMSISE-00 computes its latitude, longitude and local-time terms once per evaluation instead of in each of its 14 inner calls, the ITRF-to-geodetic conversion uses a single Bowring refinement (double precision from 1000 km below the surface to beyond lunar distance), gravity-model parsing skips coefficients above the requested degree, and `satkit.__version__` is read from the extension module instead of an `importlib.metadata` lookup at import time (contributed by @scottshambaugh, [#175](https://github.com/ssmichael1/satkit/pull/175))
- `update_datafiles()` no longer downloads files that are compiled into the library: the IERS tables and gravity models are `default: false` in the manifest (still pinned and fetchable by name), and the unused `leap-seconds.list` (nothing ever read it — the runtime leap-second table is a compiled-in constant) is removed from the manifest entirely. The only static download left is the JPL ephemeris, alongside the daily EOP / space-weather / solar-cycle refreshes ([#163](https://github.com/ssmichael1/satkit/pull/163))
### Docs
- Every Python docstring (`.pyi` stubs and runtime `__doc__`) states units of measure for dimensioned arguments, returns and attributes (SI: meters, m/s, m/s², radians unless the name ends in `_deg`); `sgp4` outputs explicitly noted as meters / m/s in the TEME frame, not the km / km/s of other SGP4 libraries ([#169](https://github.com/ssmichael1/satkit/pull/169))
## 0.21.2 - 2026-08-30
### Changed
- **Breaking (Rust):** `utils::download::Error` and its field-carrying variants are `#[non_exhaustive]`: downstream matches need a wildcard arm and struct patterns a `..`, so future variants and fields (this release alone added `Request`, `ContentRejected`, and `AllSourcesFailed.hint`) land without breaking compiles ([#161](https://github.com/ssmichael1/satkit/pull/161))
### Fixed
- Downloads verify servers against the operating system's trust store (macOS keychain, Windows certificate store, `/etc/ssl` on Unix) instead of the Mozilla root list compiled into the HTTP client: on a network whose TLS is inspected by a corporate proxy every download failed with `io: invalid peer certificate: UnknownIssuer`, because such a proxy re-signs traffic with a private CA that can only ever live in the system store. `SATKIT_CA_BUNDLE` overrides the choice with a PEM file, `platform`, or `webpki` (the compiled-in roots, for a container with no system store); `SSL_CERT_FILE` is deliberately not consulted. Download errors also name the URL that failed and, for a certificate failure, say what to do about it once (`download::Error` gains a `Request` variant, and `AllSourcesFailed` a `hint` field) ([#160](https://github.com/ssmichael1/satkit/pull/160))
- The daily CelesTrak files are checked before they replace the copy on disk: `EOP-All.csv` and `SW-All.csv` are run through the parser that will later read them, and any unverified download is rejected if it opens with an HTML document. A filtering proxy or captive portal that answers `200 OK` with a notice page can no longer overwrite a good EOP table with a web page — the partial download is discarded and the existing file left in place (`download::Error::ContentRejected`) ([#160](https://github.com/ssmichael1/satkit/pull/160))
### Docs
- GMAT validation page: removed the "What the corpus found" note ([#159](https://github.com/ssmichael1/satkit/pull/159))
- GMAT validation page and README describe the drag corpus: drag orbits, constant/file-driven force models, measured agreement against the drag-only displacement, anomalous-oxygen and F10.7-timing floors ([#157](https://github.com/ssmichael1/satkit/pull/157))
### CI
- Build workflow runs once per change: pull requests build on the PR event only (branch pushes without a PR no longer build), `main` builds on the merge commit, the redundant run on PR close is gone, and a new push to a PR cancels its superseded run ([#158](https://github.com/ssmichael1/satkit/pull/158))
## 0.21.1 - 2026-08-30
### CI
- Build/test workflow skips pure documentation changes: `mkdocs.yml`, any `*.md`, `LICENSE*`, `recipes/**` and the docs workflow file, in addition to `docs/**` ([#153](https://github.com/ssmichael1/satkit/pull/153))
- Docs build uses an absolute `SATKIT_DATA` and no longer installs the `satkit-data` bundle: notebooks executed from `docs/tutorials/` could not resolve the relative path and fell back to the bundle's frozen `EOP-All.csv` (ending 2026-08-23), which produced the stale-EOP warning on satkit.dev ([#148](https://github.com/ssmichael1/satkit/pull/148))
- `on_disk_file_is_verified_once_via_sidecar_marker` test sets the restored file's mtime explicitly; Windows file-time granularity (~1–15 ms) let a rewrite reuse the marker's mtime and fail intermittently ([#149](https://github.com/ssmichael1/satkit/pull/149))
- CI refreshes `EOP-All.csv` / `SW-All.csv` on every run (also on an `astro-data` cache hit) via `download_data.py --refresh-only`, so docs and tests no longer run on a stale EOP table; a failed refresh keeps the cached copy instead of failing the job ([#147](https://github.com/ssmichael1/satkit/pull/147))
- GitHub Actions updated to current major versions (checkout v7, setup-python v7, cache v6, upload-artifact v7, download-artifact v8, upload-pages-artifact v5, deploy-pages v5, sccache-action v0.0.11, cibuildwheel v4.2.0; Windows wheel repair explicitly kept off) ([#145](https://github.com/ssmichael1/satkit/pull/145))
### Changed
- NRLMSISE-00 is fed the 7-element 3-hourly geomagnetic ap history from `SW-All.csv` (model switch 9 = −1: current-day daily Ap, the current and three preceding 3-hourly ap, the 12–33 h and 36–57 h means; falls back to the daily Ap when the history is incomplete) instead of the daily Ap alone, so file-driven densities follow storms within hours — along the ISS orbit after the 2023-02-27 storm the 3-day drag residual against GMAT drops from 6.8 km to 0.3 km (gates tightened accordingly); `satkit.density.nrlmsise(itrfcoord, ...)` passed latitude/longitude in radians to the degree-taking model ([#154](https://github.com/ssmichael1/satkit/pull/154))
### Fixed
- TLE, OMM and Optical Observations tutorials: the CelesTrak-throttling note is a plain blockquote (mkdocs-jupyter renders notebook markdown with nbconvert, which does not support `!!! note` admonitions) ([#152](https://github.com/ssmichael1/satkit/pull/152))
- An EOP query at exactly the last table epoch is no longer treated as extrapolation (it returned the right values but printed the out-of-range warning); the Coordinate Frames tutorial ends its rotation sweep at the last EOP entry instead of 2030, so the docs build no longer emits that warning ([#151](https://github.com/ssmichael1/satkit/pull/151))
- HTTP requests send a descriptive `satkit/<version>` User-Agent; a CelesTrak 503/403 from `TLE.from_url` / `omm_from_url` now explains CelesTrak's throttling of repeated identical queries instead of a bare status code; the TLE, OMM and Optical Observations tutorials fall back to a pinned element set when the live fetch is unavailable, so the docs build no longer depends on CelesTrak ([#144](https://github.com/ssmichael1/satkit/pull/144))
- Python `kepler.mean_anomaly` setter no longer hangs on NaN or `eccen >= 1` (delegates to the core capped solver); `from_pv` extracts inclination and the anomalies with `atan2` (exact down to i = 1e-9 rad, e ≤ 0.999); constructor accepts keyword arguments matching the stub (`a, eccen, incl, raan, w, nu`), `propagate` accepts `int` seconds, and a new [Keplerian Elements guide](https://satkit.dev/guide/kepler/) ([#146](https://github.com/ssmichael1/satkit/pull/146))
- Drag: NRLMSISE-00 was given geodetic latitude/longitude in radians instead of degrees (pointwise density error up to +200 %, ~6 % of the 3-day drag displacement at ISS altitude); the space-weather feed now follows the NRLMSISE-00 interface (observed rather than 1 AU-adjusted F10.7, current-day daily Ap); Python `propagate(..., satproperties=None)` is accepted; eight 3-day drag cases (constant and CelesTrak-file space weather, 250–550 km) added to the GMAT regression corpus with the measured floors documented in `tests/gmat/README.md` ([#150](https://github.com/ssmichael1/satkit/pull/150))
## 0.21.0 - 2026-08-30
### Added
- **Experimental:** ECOM (Empirical CODE Orbit Model) solar-radiation-pressure model — reduced/ECOM1/ECOM2 coefficients in the DYB frame, Rust (`EcomParams`, `SatProperties::srp_ecom`) and Python (`ecomparams`, `satproperties(ecom=...)`), with a GPS SP3 fit/prediction tutorial; the interface may change in a minor release ([#131](https://github.com/ssmichael1/satkit/pull/131))
- Static data files are downloaded from a manifest pinned to the release (`data/manifest.json`, SHA-256 verified), trying GitHub release assets, then the origin servers (JPL, IERS), then the GCS bucket; `SATKIT_DATA_URL` overrides the source for mirrors ([#137](https://github.com/ssmichael1/satkit/pull/137))
- GMAT regression corpus: 17 seven-day reference trajectories (LEO to cislunar; `j2`/`full`/`gr` force models) replayed and gated under `cargo test` and `pytest`; regenerate with `tests/gmat/generate.py` ([#127](https://github.com/ssmichael1/satkit/pull/127))
- EOP coverage is visible and enforceable: `earth_orientation_params::{coverage, status}` (Python `frametransform.eop_coverage()` / `eop_status()`), one-time warnings past the table end or with no table, and `PropSettings::require_eop_coverage` ([#133](https://github.com/ssmichael1/satkit/pull/133))
- `frametransform::ierstable::preload()`; a missing `tab5.2*.txt` now raises `RuntimeError` in Python instead of `PanicException` ([#133](https://github.com/ssmichael1/satkit/pull/133))
- Python test pinning the EME2000 frame bias (23.1 mas) against IERS 2010 values ([#132](https://github.com/ssmichael1/satkit/pull/132))
### Changed
- **Breaking (Python packaging):** core data (IERS tables, gravity models to degree 70) is compiled in and the JPL ephemeris downloads on first use (SHA-256 verified) into the platform user-data directory, so `satkit-data` is no longer a dependency (optional: `pip install satkit[data]`); `SATKIT_OFFLINE=1` / `utils.set_offline()` forbid network access, and read-only or missing data locations are typed errors ([#139](https://github.com/ssmichael1/satkit/pull/139))
- Propagator's GCRF→ITRF table now uses the full IAU 2006/2000A chain instead of the ~1″ IAU-76 approximation; removes an inclination-dependent drift of ~50 m over 7 days at LEO versus GMAT ([#127](https://github.com/ssmichael1/satkit/pull/127))
- Relativistic correction now includes geodesic precession and Lense–Thirring (all three IERS 2010 Eq. 10.12 terms); shifts results by ≤ 1 m over 7 days at 200,000 km, cm at LEO. **Breaking (Rust):** `Precomputed::interp` returns the named struct `InterpSample` instead of a tuple ([#129](https://github.com/ssmichael1/satkit/pull/129))
- **Breaking:** gravity degree/order above 40 is rejected (`Error::InvalidGravityDegree`, Python `ValueError`) instead of being silently evaluated at 40 ([#130](https://github.com/ssmichael1/satkit/pull/130))
- `Precomputed` table size is capped (`Error::PrecomputeTooLarge`) and non-finite padding rejected, instead of allocating gigabytes ([#130](https://github.com/ssmichael1/satkit/pull/130))
- A propagation with no EOP table loaded fails with `Error::EopUnavailable` instead of running with zero polar motion and UT1−UTC ([#133](https://github.com/ssmichael1/satkit/pull/133))
- Data downloads use `https://celestrak.org` and validate manifest paths and URLs ([#130](https://github.com/ssmichael1/satkit/pull/130))
- numeris 0.5.14 → 0.5.18: the adaptive Runge–Kutta integrators no longer abort at shadow-boundary kinks on eclipsing arcs ([#128](https://github.com/ssmichael1/satkit/pull/128)) ([#135](https://github.com/ssmichael1/satkit/pull/135))
- **Cannonball SRP acts along the satellite→Sun line** rather than the
geocentric Sun direction (a ~1e-4 rad difference at LEO). `test_gps`
residual 1.7997 → 1.7868 m.
### Fixed
- SP3 epochs are read as GPS time, not UTC (18 s error) in `test_gps`, `sp3file.py` and the validation script — `test_gps` residual 1.80 → 1.21 m; cannonball SRP now acts along the satellite→Sun line; `jgm3`/`itugrace16` documented as zero-tide models ([#131](https://github.com/ssmichael1/satkit/pull/131))
- `import satkit` no longer fails when the optional `satkit_data` bundle is installed as a namespace package (the conda layout, no `__init__.py`): its `data/` directory is discovered via `__path__` ([#140](https://github.com/ssmichael1/satkit/pull/140))
- References page rebuilt as a full bibliography and every guide, API page and tutorial now cites its primary source; wrong SGP4 (AIAA 2006-6753), JGM-3 DOI and box-wing citations corrected; drag gate, Lambert multi-revolution, leap-second and GR descriptions brought in line with the code; RK stage counts corrected (RKV98 is 21 stages) ([#136](https://github.com/ssmichael1/satkit/pull/136))
- Frame-bias docs: `EME2000` is 23.1 mas from GCRF (docs said 17); GMAT's `EarthMJ2000Eq` is an IAU-76 realization ~44 mas from ICRF, not the constant bias ([#132](https://github.com/ssmichael1/satkit/pull/132))
- Doc fixes: Gauss–Jackson dense-output note, dual licence in crate docs, CONTRIBUTING versions/paths, gravity degree limit in `docs/index.md`, leap-second table described as compiled in ([#130](https://github.com/ssmichael1/satkit/pull/130))
### CI
- Published wheels are import-tested by cibuildwheel; the NOAA network test is `#[ignore]`d and run explicitly; `doc = false` on the extension crate so `cargo doc --workspace` builds ([#130](https://github.com/ssmichael1/satkit/pull/130))
## 0.20.4 - 2026-08-28
### Added
- **`scale=` keyword on the `time(...)` constructor** (Python): Gregorian
date/time components passed to `satkit.time(year, month, day[, hour, minute,
second])` can now be interpreted in an explicit time scale, e.g.
`satkit.time(2020, 1, 1, scale=satkit.timescale.TAI)`, mirroring the existing
`time.from_mjd(..., scale=...)` / `time.from_jd(..., scale=...)` API. The
keyword defaults to `satkit.timescale.UTC`, so all existing calls are
unchanged. Backed by a new scale-aware core constructor
`Instant::from_datetime_with_scale`.
- **`SGP4InitArgs::from_mean_elements`** — a constructor that performs the
rev/day → rad/min and degree → radian conversions from catalog units. Both
the `TLE` and CCSDS `OMM` SGP4 sources now build their init args through it,
so the conversion factors are defined in one place.
- **`time` and `duration` are now hashable.** Both define `__eq__` but
previously lacked `__hash__`, which made them unhashable (unusable as `dict`
keys or `set` members). The hash is derived from the underlying microsecond
count, so it is consistent with equality.
- **Equality and `repr` on more value types.** `TLE`, `kepler`, and `itrfcoord`
now implement `__eq__`; `TLE`, `kepler`, `satstate`, and `propsettings` now
implement `__repr__` (delegating to their `str` form). The three float-backed
types that gained `__eq__` are intentionally left unhashable — a failed
`hash()` is clearer than silently-wrong float-keyed lookups.
- **`mypy.stubtest` in CI.** The `python bindings test` job now verifies that
the hand-written `.pyi` type stubs match the compiled PyO3 bindings. Two CLI
flags (`--ignore-positional-only`, `--ignore-disjoint-bases`) plus
`python/stubtest_allowlist.txt` suppress systematic PyO3 idioms (constructors,
final classes, native submodules); everything else must agree.
### Fixed
- **Canonical physical constants corrected and documented.** The lunar
gravitational parameter now uses the JPL DE440 value
(`4.902800118e12 m³/s²`) instead of the erroneous `4.9048695e12`; duplicate
lunar-GM literals in point-gravity tests now reference the shared constant.
The solar GM, WGS 84 flattening and rotation rate, nominal solar radius,
DE440 Earth–Moon mass ratio, and derived geosynchronous radius were updated
to their canonical values. `JGM3_J2` now uses the conventional positive `J2`
sign. Every constant in `consts.rs` now identifies its authoritative source.
- **Relativity documentation now describes the Schwarzschild correction
accurately.** Removed misleading fixed position-drift-per-day estimates and
clarified that, for a state satisfying the Newtonian circular-orbit
relation, the post-Newtonian correction points radially outward while the
much larger Newtonian acceleration points inward. The corresponding test
name and Python type-stub documentation were corrected as well.
- **Type stubs now type-check and match the runtime.** The `.pyi` files had
never been checked and contained illegal overload-implementation blocks, a
`time.__add__` overload group split by other methods, and `time`-as-annotation
shadowing. Filled in stub gaps (`itrfcoord.height`, `time.add_utc_days`,
`weekday.Invalid`) and removed/fixed a phantom `time.as_gregorian(scale=)`
parameter and a mis-declared `sgp4_opsmode.improved` property.
- **Panic hardening: malformed and edge-case input now returns errors instead
of panicking** (PR #124), following a codebase-wide audit:
- *TLE parsing*: day-of-year is range-checked, negative satellite numbers
are rejected, and non-finite implied-exponent fields (bstar, nddot) error
at parse time instead of overflowing later in epoch math, `Display`, or
`to_2line`. `Display` also falls back to the raw satellite number rather
than unwrapping a failed alpha5 conversion.
- *Time*: `from_rfc3339` no longer panics on non-ASCII input while scanning
for a timezone offset; `from_datetime` errors on extreme years (new
`InstantError::InvalidYear`); MJD conversions, `from_gps_week_and_second`,
and leap-second folding saturate at the i64 boundaries; chrono `DateTime`
conversion saturates to `MIN_UTC`/`MAX_UTC`; `strftime` `%B`/`%b` handle
out-of-range months.
- *JPL ephemerides*: querying exactly at the file's end epoch (`jd_stop`)
now clamps to the last Chebyshev record instead of indexing past it, and
the parser validates header fields and declared sizes (with checked
arithmetic, before allocating) so a corrupt or crafted file errors
cleanly. Unpopulated bodies error instead of underflowing.
- *Gravity models*: `Gravity::parse` rejects `.gfc` lines with order >
degree (previously a panic for large orders and **silent coefficient
aliasing** for moderate ones), and the evaluators clamp the requested
degree to the loaded model's table so a custom low-degree model cannot be
indexed out of bounds. A spec-conformant bare `end_of_head` line now
terminates the header instead of silently yielding an all-zero model.
- *Orbit propagation*: the force model falls back to direct computation
when an adaptive integrator probes outside the precomputed interp table
(previously an unwrap panic for high-altitude states);
`propagate::<C>` with unsupported column counts, zero/negative/non-finite
`Precomputed` steps, and invalid thrust frames constructed via pub
fields/`Deserialize` all surface as errors instead of panics.
- *Utilities*: `datadir()` no longer panics (and permanently poisons its
singleton mutex) if a candidate directory cannot be stat-ed; download
helpers return an error for paths/URLs with no valid file name.
- **Python bindings: invalid input raises clean exceptions instead of
`PanicException`**, and non-contiguous numpy input now works:
- Wrong-size/shape arrays to `propagate`, frame transforms,
`kepler.from_pv`, `satstate.add_maneuver`, and `thrust.constant` raise
`ValueError`/`RuntimeError` (via shape validation in `py_to_smatrix`).
- Strided views and Fortran-order arrays are now accepted by `itrfcoord`,
`gravity`, `gravity_and_partials`, and the `satstate`
covariance/uncertainty setters (previously `as_slice().unwrap()` panics).
- `TLE.from_lines`/`from_url` raise on input containing no valid TLEs;
`TLE.from_file` handles non-UTF-8 files; `sgp4` rejects empty TLE
lists/time arrays; `kepler(...)` rejects non-numeric positional args;
`quaternion.from_axis_angle` validates axis length;
`planets.heliocentric_pos` raises for Sun/Moon and out-of-range times
(previously a panic with the GIL released); `time` arithmetic with ints
too large for f64 raises `OverflowError`; `datetime.timestamp()` failures
propagate; `utils.datadir()` handles non-UTF-8 paths.
### Changed
- **BREAKING: `sgp4::Error::SatRecInit` now carries a typed `SGP4Error`
instead of a raw `i32`.** The raw Vallado init error code is mapped to the
corresponding `SGP4Error` variant (eccentricity, mean motion, perturbed
eccentricity, semi-latus rectum, orbit decay) at construction, so the
`Display` output is now a description rather than a bare number.
Additionally, the `SGP4Error` enum has moved from `sgp4::sgp4_impl` to
`sgp4::error`; it remains re-exported at `satkit::sgp4::SGP4Error`, so the
public path is unchanged.
- **`LambertError` renamed to `lambert::Error`** to match the per-module
`module::Error` convention used everywhere else in the crate. The old name
remains as a `#[deprecated]` type alias, so existing code compiles with a
deprecation warning; update `satkit::lambert::LambertError` →
`satkit::lambert::Error`. A `lambert::Result<T>` alias was also added.
- **`itrfcoord(...)`, `sgp4(...)`, and `satstate.propagate(...)` now reject
unknown keyword arguments** instead of silently ignoring them. Previously a
typo such as `itrfcoord(..., alttiude=100)` was dropped, leaving the ground
station at 0 m altitude; it now raises `ValueError`. Callers passing only
documented keywords are unaffected.
- **Renamed keyword arguments on several bindings** so the accepted Python
keyword matches its documentation (the stubs previously advertised names the
runtime rejected). Positional calls are unaffected; only callers passing these
by the *old* keyword must update:
- `duration.from_hours` / `from_minutes` / `from_seconds`: `d` → `hours` / `minutes` / `seconds`
- `time.from_string`: `s` → `string`; `time.from_unixtime`: `t` → `unixtime`
- `time.from_rfc3339`: `s` → `rfc3339`; `time.from_datetime`: `tm` → `dt`
- `time.strftime`: `fmt` → `format`; `time.strptime`: `(s, fmt)` → `(date_string, format)`
- `kepler.from_pv`: `(r, v)` → `(pos, vel)`