# satpass

satpass tells you when the ISS, Tiangong, Hubble, Starlink trains and the brightest rocket
bodies will pass over your location, and whether you will be able to see them: the satellite
has to be sunlit while your sky is dark. It works entirely offline from two-line element sets,
using an SGP4/SDP4 propagator written from scratch after Vallado et al. (2006). That propagator
matches the official reference vectors to better than a millimetre. The output can be a pass
table, a live position readout, CSV tracks, SVG sky plots and ground-track maps, a "what's up
tonight" list, or an iCalendar feed.

![CSS pass sky plot](docs/img/css-pass.svg)

## Features

- **SGP4 and SDP4 written from scratch.** Near-Earth and deep-space orbits are both handled,
  including 12-hour and 24-hour resonance, lunar-solar perturbations and the Lyddane fix for
  low inclinations. It uses WGS-72 constants, outputs TEME, and is verified against
  `tcppver.out`.
- **TLE handling.** 2-line and 3-line sets with checksum validation and epoch decoding. You can
  look up a satellite by name, by alias (`ISS`, `CSS`/`TIANGONG`, `HUBBLE`/`HST`) or by NORAD id.
- **CelesTrak updates.** `satpass tle update` downloads into a local cache, refreshes at most
  every 2 hours and warns when elements are stale. Sample TLEs are bundled, so the tool works
  offline straight away.
- **Frames.** TEME→ECEF uses IAU-82 GMST, the observer's position uses WGS-84, and the
  topocentric SEZ frame gives azimuth, elevation, range and range-rate.
- **Visibility.** A low-precision Sun, a conical umbra/penumbra Earth-shadow model (plus a
  cylindrical test), twilight classes, and a diffuse-sphere magnitude estimate.
- **Pass prediction.** A coarse scan, then Brent root-finding for AOS and LOS and Brent
  maximisation for culmination, plus the visible windows inside each pass.
- **Commands:**
  - `passes`: list upcoming passes of one satellite.
  - `now`: the satellite's current position and illumination.
  - `track`: a position table as CSV.
  - `skyplot`: a polar SVG plot of one pass.
  - `groundtrack`: an SVG world map with Natural Earth coastlines.
  - `flyover-list`: every bright pass from a group, for example tonight.
  - `ical`: passes as an iCalendar feed.
  - `tle update|list|info` and `config init|show`: manage TLEs and your location.

## Install

```bash
cd satpass
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
# or: python3.12 -m venv .venv && .venv/bin/pip install -e '.[dev]'
```

The only runtime dependency is `click`. `pytest` and `sgp4` are needed only for the test suite;
`sgp4` serves as the test oracle.

## Quickstart

```bash
# 1. Tell satpass where you are (written to ~/.config/satpass/config.toml)
satpass config init --lat 12.9141 --lon 74.856 --elev 22 --tz Asia/Kolkata --name Mangaluru

# 2. Optional: fetch fresh elements (otherwise the bundled samples are used)
satpass tle update --group visual --group stations

# 3. Ask
satpass passes ISS --days 5 --min-el 10 --visible-only
satpass flyover-list --tonight
```

Every example below runs offline against the bundled TLEs and `examples/config.toml`, with a
fixed `--start` so you get the same output:

```bash
alias sp="satpass --config examples/config.toml"
```

## Usage

### `passes`: upcoming passes of one satellite

```
$ satpass passes CSS --start 2026-10-16T00:00 --days 5 --min-el 10 --visible-only
note: using bundled sample TLEs; run `satpass tle update` for fresh data
CSS (TIANHE) [48274] passes over Mangaluru (12.9141, 74.8560, 22 m) · times in Asia/Kolkata
window 2026-10-16 00:00 + 5 d, min elevation 10°, visible only
 #  Date        AOS       Az    Culmination  El   Az    LOS       Az    Mag   Visible window
 1  2026-10-16  19:35:07  SSW   19:40:12      35°  SE    19:45:17  ENE  -0.7  19:35:07-19:38:26
 2  2026-10-17  05:23:54  NW    05:28:59      36°  NE    05:34:04  ESE  -1.9  05:26:26-05:34:04
 3  2026-10-17  18:36:17  SSW   18:40:49      14°  SE    18:45:21  ENE  -1.6  18:36:17-18:42:32
 4  2026-10-17  20:12:30  WSW   20:17:30      27°  NW    20:22:30  NE    2.1  20:12:30-20:14:48
 5  2026-10-18  04:24:48  NNW   04:29:27      16°  NE    04:34:06  E     1.1  04:30:28-04:34:06
 6  2026-10-18  19:12:46  SW    19:17:59      75°  NW    19:23:12  NE   -3.4  19:12:46-19:18:49
 7  2026-10-19  05:01:34  NW    05:06:46      74°  SW    05:11:58  SE   -3.6  05:06:46-05:11:58
 8  2026-10-19  19:50:56  W     19:55:20      12°  NW    19:59:44  NNE   0.7  19:50:56-19:54:59
 9  2026-10-20  04:02:07  NW    04:07:14      37°  NE    04:12:19  ESE   2.4  04:10:46-04:12:19
10  2026-10-20  05:39:40  W     05:43:51      10°  SW    05:48:04  S    -1.3  05:43:01-05:48:04
11  2026-10-20  18:50:41  WSW   18:55:39      26°  NW    19:00:38  NNE  -1.4  18:50:41-18:58:47
```

- **Mag** is the brightest magnitude during the visible window. Lower is brighter.
- **Visible window** is when the satellite is sunlit and the Sun is below −6°. A 19:35 pass
  that is visible only until 19:38 means the satellite entered Earth's shadow at that point.
- A `<` or `>` after a row means the pass was already in progress at the start of the search
  window, or was still in progress at its end.

Options:

- `--days N` (default 5)
- `--min-el DEG`: minimum culmination elevation (default 10)
- `--visible-only`
- `--start ISO`: a time without an offset is read in your configured time zone.

### `now`: where is it right now?

```
$ satpass now ISS --at 2026-10-16T14:19:20
ISS (ZARYA) [25544] at 2026-10-16 14:19:20 IST
  azimuth        235.35° (SW)
  elevation       66.11°
  range           457.7 km   range-rate -0.079 km/s
  sub-point      12.018° lat    73.542° lon
  altitude        421.0 km
  velocity        7.660 km/s (inertial)
  illumination sunlit (lit fraction 1.00)
  sun altitude    52.41° (day)
  magnitude    2.4
  TLE epoch    2026-10-15 19:43:34 UTC (+0.55 d)
```

### `track`: CSV time series

```
$ satpass track ISS --start 2026-10-16T14:15 --duration 2m --step 30s
time_utc,az_deg,el_deg,range_km,range_rate_km_s,lat_deg,lon_deg,alt_km,sunlit,sun_alt_deg,magnitude
2026-10-16T08:45:00Z,318.166,4.526,1908.813,-6.8542,24.7561,63.0042,423.067,1,53.28,-0.15
2026-10-16T08:45:30Z,317.481,7.032,1703.714,-6.8157,23.3279,64.3172,422.766,1,53.18,-0.31
2026-10-16T08:46:00Z,316.584,9.964,1500.128,-6.7514,21.8865,65.5998,422.479,1,53.08,-0.48
2026-10-16T08:46:30Z,315.367,13.513,1299.036,-6.6459,20.4330,66.8544,422.207,1,52.98,-0.66
2026-10-16T08:47:00Z,313.632,17.994,1102.077,-6.4690,18.9685,68.0832,421.950,1,52.88,-0.83
```

`--duration` and `--step` accept `s`, `m`, `h` and `d` suffixes. `-o file.csv` writes the table
to a file.

### `skyplot`: polar plot of one pass

```
$ satpass skyplot CSS --pass 1 --start 2026-10-16T00:00 --visible-only -o docs/img/css-pass.svg
wrote docs/img/css-pass.svg: CSS (TIANHE) pass 1, 2026-10-16 19:35:07 max 34.7°
```

The pass number refers to the same list `passes` prints with the same options. In the plot, the
zenith is at the centre and the horizon is the outer ring. The path is drawn:

- **yellow** where the satellite is visible
- **blue** where it is sunlit but the sky is too bright
- **dashed grey** where it is in Earth's shadow

Each minute is marked with a tick.

### `groundtrack`: map of the sub-satellite path

```
$ satpass groundtrack ISS --orbits 3 --start 2026-10-16T18:00 -o docs/img/iss-track.svg
wrote docs/img/iss-track.svg: ISS (ZARYA), 3 orbits (279 min)
```

![ISS ground track](docs/img/iss-track.svg)

The map uses an equirectangular projection with embedded Natural Earth 1:110m coastlines (public
domain, simplified to 0.1°). The track is solid where the satellite is sunlit and dashed where it
is in shadow. The track is split cleanly where it crosses the antimeridian.

### `flyover-list`: everything bright tonight

```
$ satpass flyover-list --group visual --tonight --start 2026-10-16T12:00
Bright passes (bundled:visual, 155 satellites) over Mangaluru (12.9141, 74.8560, 22 m) · times in Asia/Kolkata
2026-10-16 18:32 -> 2026-10-17 05:59, min el 20°, mag <= 3.5
Start     End       Mag   VisEl  Dir          Satellite
18:32:40  18:41:37    1.9    79°    S -> N     COSMOS 1953 [19210]
18:33:25  18:43:45    2.7    31°  NNW -> SE    SPACEMOBILE-006 [67232]
18:36:21  18:44:34    1.6    29°  SSE -> N     CZ-2C R/B [54039]
18:37:16  18:50:14    3.4    69°    N -> SSW   SAOCOM 1A [43641]
19:33:24  19:39:35    2.2    72°   SW -> ENE   XRISM [57800]
19:35:07  19:38:26   -0.7    18°  SSW -> ENE   CSS (TIANHE) [48274]
05:02:44  05:15:44    2.5    45°  SSW -> N     SL-14 R/B [17912]
...
05:44:57  05:53:38    3.4    24°  WNW -> SE    XRISM [57800]
```

- `--tonight` searches from the evening's −6° Sun crossing to the morning one. If it is already
  dark, the search starts now.
- Without `--tonight`, `--hours N` sets the length of the window.
- **VisEl** is the highest elevation reached while the satellite is visible.
- Other options: `--min-el` (default 20) and `--max-mag` (default 3.5).
- The whole 155-object group takes under a second.

### `ical`: subscribe in your calendar

```
$ satpass ical CSS --start 2026-10-16T00:00 --days 14 -o css.ics
wrote css.ics: 13 events
```
```
BEGIN:VEVENT
UID:48274-20261016T141012Z@satpass
DTSTART:20261016T140507Z
DTEND:20261016T140826Z
SUMMARY:CSS (TIANHE) pass\, max 35° SE\, mag -0.7
DESCRIPTION:AOS 14:05:07 UTC az 213° (SSW)\nMax 14:10:12 UTC el 34.7° az
 136°\nLOS 14:15:17 UTC az 59° (ENE)\nVisible to the naked eye during thi
 s event.
```

Events cover the visible window. By default only visible passes are exported; add
`--all-passes` to include every pass above `--min-el`. Lines are folded to 75 octets as
RFC 5545 requires.

### `tle`: element sets

```
$ satpass tle update --group visual
visual: 155 element sets downloaded to /home/you/.cache/satpass/visual.tle
$ satpass tle update --group visual
visual: 155 element sets fresh in /home/you/.cache/satpass/visual.tle
$ satpass --tle examples/geo.tle tle info GSAT-30
GSAT-30  NORAD 45026  COSPAR 20005A
1 45026U 20005A   26288.95331432 -.00000164  00000+0  00000+0 0  9990
2 45026   0.0126 241.5401 0002275 287.4500 281.7263  1.00273494 24710
epoch        2026-10-15 22:52:46.357248 UTC
inclination  0.0126°   RAAN 241.5401°
eccentricity 0.0002275   arg perigee 287.4500°
mean motion  1.00273494 rev/day  (period 1436.07 min)
B*           0.00000e+00
model        deep-space (SDP4)
```

- `tle update` takes `--group` more than once to fetch several groups (default: `visual` and
  `stations`).
- `--force` bypasses CelesTrak's courtesy limit of one download per 2 hours.
- `tle list` shows each element set's epoch and age.
- Any command warns when the elements are more than 7 days from the prediction time, and again
  more strongly past 30 days.

## Configuration

The location file is TOML (see `examples/config.toml`):

```toml
[observer]
name = "Mangaluru"
lat = 12.9141      # degrees, north positive
lon = 74.856       # degrees, east positive
elev = 22.0        # metres above the WGS-84 ellipsoid
tz = "Asia/Kolkata"  # IANA zone used for displayed times
```

Lookup order:

1. `--config PATH`
2. `$SATPASS_CONFIG`
3. `$XDG_CONFIG_HOME/satpass/config.toml` (normally `~/.config/satpass/config.toml`)

`--lat/--lon/--elev/--tz` override the file, or replace it if you give both lat and lon.

Where TLEs are read from:

1. `--tle FILE`, which uses only that file.
2. The download cache `$SATPASS_CACHE_DIR`, otherwise `~/.cache/satpass/*.tle`.
3. The bundled samples.

When several sets share a NORAD id, the one with the newest epoch wins.

Global options such as `--config`, `--tle` and `--lat` go **before** the subcommand:
`satpass --tle my.tle passes ISS`.

### Bundled sample TLEs

These were downloaded from CelesTrak on 2026-10-16 and are copied in `examples/` and in the
package:

| File | Sets | Epochs (UTC) |
|---|---|---|
| `visual.tle` | 155 | 2026-10-13 18:28 → 2026-10-16 07:57 |
| `stations.tle` | 17 | 2026-10-15 07:27 → 2026-10-16 07:57 |
| `starlink.tle` | 10 | 2026-10-15 12:54 → 2026-10-16 06:17 |
| `geo.tle` | 9 (GSAT/INSAT/TDRS/GOES/…) | 2026-10-15 14:09 → 2026-10-16 05:25 |

They are good for predictions within about a week of those dates.

## Architecture

| Module | Role |
|---|---|
| `tle.py` | TLE parsing, checksum, epoch → datetime/JD, lazy propagator |
| `sgp4.py` | SGP4 initialisation and propagation (near-Earth core) |
| `deepspace.py` | SDP4 resonance setup (`dsinit`) and integrator (`dspace`) |
| `lunisolar.py` | Lunar-solar coefficients (`dscom`) and periodics (`dpper`, Lyddane) |
| `constants.py` | WGS-72 constants |
| `frames.py` | GMST, TEME↔ECEF, WGS-84 geodetic, SEZ look angles, `Observer` |
| `sun.py` | Sun ephemeris, conical/cylindrical shadow, phase angle, twilight |
| `magnitude.py` | Standard magnitudes and diffuse-sphere brightness |
| `rootfind.py` | Brent root finder and Brent maximiser |
| `passes.py` | `observe()`, pass search, visibility windows, dark interval |
| `svg.py` | Sky plot and ground-track renderers |
| `ical.py` | RFC 5545 export |
| `catalog.py` | Bundled/cached sources, CelesTrak download, name lookup, age warnings |
| `config.py` | TOML location config |
| `cli.py`, `cli_admin.py`, `clictx.py` | Click commands |

[`docs/algorithms.md`](docs/algorithms.md) explains the maths with worked examples:

- SGP4 and SDP4
- the reference frames
- shadow geometry and magnitudes
- the pass search
- accuracy versus TLE age

## Tests

```bash
.venv/bin/pytest -q
```

The suite has 112 tests:

- **Propagator against Vallado.** It is checked against Vallado's reference vectors for 22
  satellites (near-Earth, deep-space and all resonance classes), including error codes 4 and 6.
- **Propagator against the oracle.** Modern TLEs, including GEO, are compared with the `sgp4`
  package at random times.
- **TLE parsing and checksums.**
- **Frames.** GMST against Vallado Example 3-5, the TEME→ECEF example, geodetic round trips and
  look-angle geometry.
- **Sun and shadow.** Sun position against Vallado Example 5-1; the shadow models in umbra,
  penumbra and annular geometry.
- **Magnitudes and root finding.**
- **Pass prediction.** Results are compared with a 1-second brute-force scan driven by the
  oracle (AOS/LOS within 1.5 s, peak elevation within 0.01°), and visibility windows are checked
  for consistency.
- **Output files.** The SVGs are checked to be well-formed and the iCal structure is checked.
- **Cache behaviour.** Uses a stubbed download.
- **The CLI end to end.** Runs offline.

## Limitations

- **Accuracy is limited by the TLE.** Predictions are only as good as the elements: expect tens
  of seconds of error after a week, and much more for objects in low orbit during high solar
  activity. See the accuracy table in the docs.
- **Earth orientation is simplified.** Polar motion is ignored and UT1 is taken as UTC, which
  costs at most a few hundred metres. Atmospheric refraction is ignored too, so it does not lift
  objects near the horizon by about 0.5°.
- **Magnitudes are rough.** Standard magnitudes are built in only for a few objects (ISS,
  Tiangong, HST, a few Earth-observation satellites and Starlink). Everything else uses a
  generic 4.0. Flares and glints are not modelled.
- **Very short passes can be missed.** The coarse scan uses a 60 s step for LEO, so a grazing
  pass that lasts under a minute and stays within a degree or so of the horizon can be skipped.
  Any pass above 10° is found.
- **Always-up satellites are one clipped pass.** A geostationary satellite that never sets is
  reported as a single pass flagged `<>`. Its "visible window" means sunlit against a dark sky,
  which is not the same as visible to the naked eye; GEO satellites are around magnitude 10–12.
- **Only the "improved" SGP4 mode is implemented.** The legacy AFSPC mode differs slightly in
  how GMST is computed at epoch and in some angle wrapping.
