# skywatch

**skywatch** answers the amateur astronomer's eternal question — *what's up tonight?* — completely offline.
Give it a location and a date and it computes twilight times, the Moon's phase and rise/set, a planet table,
the moonless dark-sky window and a ranked list of the best targets, and it draws an all-sky chart and
altitude-vs-time plots as SVG. Every number comes from textbook algorithms implemented from scratch
(Jean Meeus, *Astronomical Algorithms*; JPL approximate planetary elements; Roman 1987 constellation
boundaries) — no skyfield, astropy or ephem, no network, no API keys.

## Features

- **Time & coordinates**: Julian Day, ΔT (Espenak–Meeus polynomials), mean/apparent sidereal time,
  ecliptic↔equatorial↔horizontal transforms, rigorous precession, low-precision nutation, Bennett refraction
  (pressure-scaled for site elevation).
- **Sun**: apparent position (Meeus ch. 25), equation of time, sunrise/sunset and civil/nautical/astronomical
  twilight by bracketing + bisection on altitude crossings.
- **Moon**: position from the main terms of Meeus ch. 47, illuminated fraction, phase name/emoji, rise/set,
  exact times of new/first-quarter/full/last-quarter (Meeus ch. 49).
- **Planets** Mercury–Neptune: JPL Keplerian elements with rates → geocentric with light-time, magnitude
  (simplified Mallama & Hilton 2018, with Saturn ring tilt), elongation, apparent diameter.
- **Rise/transit/set** for any object; circumpolar / never-rises detection.
- **Catalogs** (embedded CSV): 925 stars to V 4.5 (288 with proper names), all 110 Messier objects + 50 bright
  NGC/IC objects, constellation figures for 36 constellations, the full Roman (1987) boundary table.
- **Constellation identification** for any RA/Dec using the IAU boundaries.
- **Commands**: `tonight`, `where`, `chart`, `altitude`, `calendar`, `ical`, `config`.
- **Calendar**: lunar phases, conjunctions (< 3°, including the Moon), greatest elongations, oppositions,
  meteor-shower peaks (solar-longitude based), with iCalendar export.

## Install

```bash
cd skywatch
uv venv .venv && uv pip install -p .venv/bin/python -e '.[dev]'
source .venv/bin/activate          # or call .venv/bin/skywatch directly
```

Requires Python ≥ 3.11 (`tomllib`, `zoneinfo`); runtime dependencies are only `click` and `rich`.

## Quickstart

```bash
skywatch config set-location --lat 18.52 --lon 73.86 --elev 560 --tz Asia/Kolkata --name Pune
skywatch config set-location --lat 51.5074 --lon -0.1278 --elev 11 --tz Europe/London --name London
skywatch tonight --date 2026-10-16
skywatch where Saturn --at "2026-10-16 23:30"
skywatch chart --date 2026-10-16 --at 22:00 -o sky.svg
skywatch altitude M31 M42 Saturn Jupiter --date 2026-10-16 -o alt.svg
skywatch calendar --month 2026-11
skywatch ical --month 2026-11 -o november.ics
```

## Usage

All observing commands accept `--location/-l NAME` (default: the configured default location) or an ad-hoc
`--lat/--lon/--tz/--elev`. Longitudes are **east positive**. Times are shown in the location's time zone.

### `skywatch tonight [--date YYYY-MM-DD] [--targets N]`

The "night of" a date runs from local noon to local noon the next day.

```
$ skywatch tonight --date 2026-10-16
skywatch · Friday 16 October 2026 · Pune (+18.52°, +73.86°, Asia/Kolkata)

Sun & twilight                                      
┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━┓
┃ Event                        ┃ Evening ┃ Morning ┃
┡━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━┩
│ Sunset / sunrise             │ 18:12   │ 06:29   │
│ Civil twilight (−6°)         │ 18:34   │ 06:07   │
│ Nautical twilight (−12°)     │ 18:59   │ 05:41   │
│ Astronomical twilight (−18°) │ 19:24   │ 05:16   │
└──────────────────────────────┴─────────┴─────────┘

Moon 🌒 Waxing Crescent, 33% illuminated · rise — · transit 16:51 · set 22:21
Dark-sky window (astronomical night, Moon down): 22:24–05:16 = 6.9 h

Planets (max altitude between sunset and sunrise)                                         
┏━━━━━━━━━┳━━━━━━━┳━━━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━┳━━━━━━┳━━━━━━━┳━━━━━━━┳━━━━━━━━━━━━━━━┓
┃ Planet  ┃ Rise  ┃ Transit ┃ Set   ┃ Max alt     ┃ Mag  ┃ Size  ┃ Elong ┃ Constellation ┃
┡━━━━━━━━━╇━━━━━━━╇━━━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━╇━━━━━━╇━━━━━━━╇━━━━━━━╇━━━━━━━━━━━━━━━┩
│ Mercury │ 08:17 │ 13:52   │ 19:26 │ 16° @ 18:12 │ +0.1 │ 7.3"  │ 25°   │ Libra         │
│ Venus   │ 07:17 │ 12:56   │ 18:29 │ 3° @ 18:12  │ -4.2 │ 59.5" │ 14°   │ Virgo         │
│ Mars    │ 01:18 │ 07:47   │ 14:17 │ 72° @ 06:29 │ +1.0 │ 6.0"  │ 73°   │ Cancer        │
│ Jupiter │ 02:10 │ 08:32   │ 14:57 │ 60° @ 06:29 │ -1.9 │ 34.4" │ 61°   │ Leo           │
│ Saturn  │ 17:33 │ 23:36   │ 05:40 │ 73° @ 23:35 │ +0.4 │ 19.6" │ 167°  │ Cetus         │
│ Uranus  │ 20:35 │ 03:06   │ 09:37 │ 88° @ 03:07 │ +5.7 │ 3.7"  │ 138°  │ Taurus        │
│ Neptune │ 17:04 │ 23:05   │ 05:06 │ 71° @ 23:04 │ +7.7 │ 2.3"  │ 159°  │ Pisces        │
└─────────┴───────┴─────────┴───────┴─────────────┴──────┴───────┴───────┴───────────────┘
Top 15 targets (moonless astronomical night)                                                                              
┏━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━┳━━━━━┳━━━━━━━━━━━┳━━━━━┳━━━━━┳━━━━━━━━━━━━━━━┳━━━━━━━━━━┳━━━━━━━┓
┃ #  ┃ Target                          ┃ Type           ┃ Mag ┃ Best time ┃ Alt ┃ Dir ┃ Constellation ┃ Moon sep ┃ Score ┃
┡━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━╇━━━━━╇━━━━━━━━━━━╇━━━━━╇━━━━━╇━━━━━━━━━━━━━━━╇━━━━━━━━━━╇━━━━━━━┩
│ 1  │ M45 Pleiades                    │ open cluster   │ 1.2 │ 02:43     │ 84° │ N   │ Taurus        │ 146°     │ 100   │
│ 2  │ M31 Andromeda Galaxy            │ galaxy         │ 3.4 │ 23:41     │ 67° │ N   │ Andromeda     │ 114°     │ 84    │
│ 3  │ M42 Great Orion Nebula          │ cluster+nebula │ 4.0 │ 04:30     │ 66° │ S   │ Orion         │ 145°     │ 78    │
│ 4  │ M44 Beehive                     │ open cluster   │ 3.1 │ 05:16     │ 57° │ E   │ Cancer        │ 148°     │ 78    │
│ 5  │ NGC 1499 California Nebula      │ nebula         │ 5.0 │ 02:59     │ 72° │ N   │ Perseus       │ 150°     │ 78    │
│ 6  │ M33 Triangulum Galaxy           │ galaxy         │ 5.8 │ 00:26     │ 78° │ N   │ Triangulum    │ 120°     │ 69    │
│ 7  │ NGC 1980 Lower Sword            │ cluster+nebula │ 2.5 │ 04:30     │ 66° │ S   │ Orion         │ 145°     │ 67    │
│ 8  │ NGC 7000 North America Nebula   │ nebula         │ 4.0 │ 22:24     │ 49° │ NW  │ Cygnus        │ 82°      │ 67    │
│ 9  │ NGC 2264 Christmas Tree Cluster │ cluster+nebula │ 3.9 │ 05:16     │ 80° │ SSE │ Monoceros     │ 162°     │ 66    │
│ 10 │ M35                             │ open cluster   │ 5.1 │ 05:01     │ 84° │ N   │ Gemini        │ 176°     │ 66    │
│ 11 │ NGC 752                         │ open cluster   │ 5.7 │ 00:57     │ 71° │ N   │ Andromeda     │ 126°     │ 63    │
│ 12 │ Saturn                          │ planet         │ 0.4 │ 23:41     │ 73° │ S   │ Cetus         │ 98°      │ 63    │
│ 13 │ NGC 2232                        │ open cluster   │ 3.9 │ 05:16     │ 67° │ S   │ Monoceros     │ 148°     │ 60    │
│ 14 │ M34                             │ open cluster   │ 5.2 │ 01:43     │ 66° │ N   │ Perseus       │ 135°     │ 59    │
│ 15 │ NGC 1981 Upper Sword            │ cluster+nebula │ 4.2 │ 04:30     │ 67° │ S   │ Orion         │ 146°     │ 58    │
└────┴─────────────────────────────────┴────────────────┴─────┴───────────┴─────┴─────┴───────────────┴──────────┴───────┘
```

The dark-sky window is astronomical night minus the time the Moon is above the horizon. When there is no
such window (e.g. London in June) targets are ranked over the darkest period available instead, and the
table title says which. A `—` rise time means the body was already up at the start of the noon-to-noon window.

### `skywatch where NAME [--at TIME] [--date DATE]`

`NAME` can be `Sun`, `Moon`, a planet, a Messier/NGC/IC id (`M42`, `NGC 869`, `ngc0869`), a common name
(`Pleiades`) or a star name (`Sirius`). `--at` accepts `now` (default), `HH:MM` or `"YYYY-MM-DD HH:MM"`.

```
$ skywatch where Saturn --at "2026-10-16 23:30"
Saturn — Fri 16 Oct 23:30 Asia/Kolkata from Pune
  Altitude   +73.06°  (above horizon)
  Azimuth    174.78° (S)
  RA/Dec     00h42m35.7s +01°38′10″ (apparent, of date)
  RA/Dec     00h41m13.2s +01°29′22″ (J2000)
  Constellation  Cetus (Cet)
  Distance   8.4570 au (1265.1 million km), light time 70.3 min
  Magnitude  +0.40   diameter 19.6"   elongation 166.8°   phase 100% lit
  Next rise  Sat 17 Oct 17:28   next transit Fri 16 Oct 23:36 (alt 73°)   next set Sat 17 Oct 05:40

$ skywatch where M31 -l London --at "2026-10-16 23:00"
M31 Andromeda Galaxy — Fri 16 Oct 23:00 Europe/London from London
  Altitude   +75.25°  (above horizon)
  Azimuth    126.96° (SE)
  RA/Dec     00h44m12.8s +41°25′00″ (apparent, of date)
  RA/Dec     00h42m44.4s +41°16′13″ (J2000)
  Constellation  Andromeda (And)
  Type       galaxy, mag 3.4, size 177.8′
  Rise/set   circumpolar — never sets from this latitude
```

### `skywatch chart [--at TIME] [--date DATE] [--mag 4.5] -o sky.svg`

Zenith-centred stereographic all-sky chart: horizon, 30°/60° altitude rings, cardinal points (east on the
left, as when looking up), stars sized by magnitude, constellation figures and names, Messier objects,
planets and the Moon. Example: [`examples/sky-pune-2026-10-16.svg`](examples/sky-pune-2026-10-16.svg).

![Sky chart](examples/sky-pune-2026-10-16.svg)

### `skywatch altitude OBJ... [--date DATE] -o alt.svg`

Altitude-vs-local-time curves from one hour before sunset to one hour after sunrise, on a background shaded
by day / civil / nautical / astronomical twilight / night, with the Moon as a dashed reference line.

![Altitude plot](examples/altitude-pune-2026-10-16.svg)

### `skywatch calendar [--month YYYY-MM]`

```
$ skywatch calendar --month 2026-11
Sky calendar · November 2026 · times in Asia/Kolkata                                                       
┏━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ When             ┃ Event                                           ┃ Details                            ┃
┡━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ Mon 02 Nov 01:58 │ ☾ Last Quarter                                  │                                    │
│ Mon 02 Nov 19:09 │ ☌ Moon–Mars conjunction                         │ separation 0.97°                   │
│ Tue 03 Nov 04:19 │ ☌ Moon–Jupiter conjunction                      │ separation 0.49°                   │
│ Fri 06 Nov 00:27 │ ☄ Southern Taurids meteor shower peak           │ ZHR ~5, radiant RA 2.1h Dec +9°    │
│ Sat 07 Nov 16:13 │ ☌ Moon–Venus conjunction                        │ separation 0.95°                   │
│ Mon 09 Nov 12:32 │ ☾ New Moon                                      │                                    │
│ Thu 12 Nov 23:44 │ ☄ Northern Taurids meteor shower peak           │ ZHR ~5, radiant RA 3.9h Dec +22°   │
│ Mon 16 Nov 09:36 │ ☌ Mars–Jupiter conjunction                      │ separation 1.19°                   │
│ Tue 17 Nov 17:18 │ ☾ First Quarter                                 │                                    │
│ Wed 18 Nov 05:19 │ ☄ Leonids meteor shower peak                    │ ZHR ~15, radiant RA 10.1h Dec +22° │
│ Sat 21 Nov 05:01 │ ∡ Mercury at greatest elongation west (morning) │ 19.6° from the Sun                 │
│ Tue 24 Nov 20:23 │ ☾ Full Moon                                     │                                    │
│ Thu 26 Nov 04:00 │ ☍ Uranus at opposition                          │ mag 5.7, 18.439 au                 │
│ Mon 30 Nov 13:58 │ ☌ Moon–Jupiter conjunction                      │ separation 1.06°                   │
│ Mon 30 Nov 22:25 │ ☌ Moon–Mars conjunction                         │ separation 3.00°                   │
└──────────────────┴─────────────────────────────────────────────────┴────────────────────────────────────┘
```

### `skywatch ical [--month YYYY-MM] [-o file.ics]`

Writes the same events as RFC 5545 iCalendar (UTC timestamps, CRLF line endings, folded lines); without
`-o` it prints to stdout. Example: [`examples/skywatch-2026-11.ics`](examples/skywatch-2026-11.ics).

### `skywatch config ...`

| Command | Purpose |
| --- | --- |
| `config set-location --lat --lon [--elev] --tz --name [--default]` | add/update a named location (the first one becomes default) |
| `config list` | show saved locations (`*` marks the default) |
| `config use NAME` | change the default |
| `config remove NAME` | delete a location |
| `config path` | print the config file path |

## Configuration

Locations are stored as TOML in `$XDG_CONFIG_HOME/skywatch/config.toml` (normally
`~/.config/skywatch/config.toml`). Set `SKYWATCH_CONFIG=/path/to/file.toml` to use another file; the sample
[`examples/config.toml`](examples/config.toml) holds Pune and London:

```toml
default = "Pune"

[locations."Pune"]
lat = 18.52
lon = 73.86
elev = 560.0
tz = "Asia/Kolkata"
```

Elevation scales atmospheric refraction by the standard-atmosphere pressure ratio.

## Architecture

| Module | Responsibility |
| --- | --- |
| `astrotime.py` | Julian Day, ΔT, GMST/GAST/LST, datetime ↔ JD |
| `coords.py` | nutation, obliquity, precession, frame transforms, Bennett refraction, separations, formatting |
| `sun.py` | solar position (Meeus 25), equation of time (Meeus 28) |
| `moon.py` | lunar position (Meeus 47), phase angle and illumination (48), phase instants (49) |
| `planets.py` | JPL approximate elements → apparent geocentric positions, magnitudes, sizes |
| `catalog.py` | CSV catalog loading, constellation names, Roman (1987) constellation lookup |
| `bodies.py` | `Observer`/`Target` model, name resolution, topocentric alt/az |
| `riseset.py` | crossing search (bracket + bisection), rise/transit/set, twilight, interval sampling |
| `planner.py` | `tonight` report: dark window, planet table, target scoring |
| `events.py` | monthly events: phases, conjunctions, elongations/oppositions, meteor showers |
| `charts.py` | SVG sky chart and altitude plot |
| `ical.py` | iCalendar export |
| `config.py` | TOML location store |
| `cli.py` | click commands and rich tables |
| `data/*.csv` | embedded catalogs, regenerated by `tools/build_catalogs.py` from HYG v3.8, OpenNGC, d3-celestial and CDS VI/42 |

See [`docs/algorithms.md`](docs/algorithms.md) for formulas, worked examples, the accuracy table and sources.

## Tests

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

70 tests: Meeus worked examples (7.a, 12.a/b, 13.b, 15.a, 16.a, 21.b, 22.a, 25.a, 28.b, 33.a, 47.a, 48.a,
49.a/b), sunrise/sunset for London and Pune against published tables (±2 min), twilight, circumpolar
detection, the 2020 Jupiter–Saturn great conjunction (0.10°), USNO lunar phase times, the 2020 Mars
opposition and Venus greatest elongation, the Roman (1987) reference points, catalog lookups, the scoring
function, and end-to-end CLI runs including SVG well-formedness and iCalendar format.

## Limitations

- Planet positions use JPL's *approximate* elements: typically ≤ 1′ for the inner planets and a few
  arcminutes for the outer ones, valid 1800–2050. Event instants from slow geometry (e.g. a planet–planet
  conjunction) can be off by several hours even when the separation is right.
- The Moon uses the ~60 largest longitude/distance and 30 largest latitude terms of Meeus ch. 47 (≈ 10″).
  Only the altitude is corrected for lunar parallax; RA/Dec are geocentric.
- Annual aberration (≤ 20″) and stellar proper motion are ignored.
- ΔT's 2005–2050 polynomial overshoots the observed value by ~2 s in the 2020s (irrelevant at minute precision).
- Rise/set uses the standard −0.833° (Sun) / −0.567° horizon; no terrain or horizon dip from elevation.
- Polar locations work, but on days without sunset/sunrise many cells simply show `—`.
- Eclipses, comets, asteroids and satellites are out of scope.
