Skip to main content

meteor-shower-forecast

Location meteor-shower rate estimates, not the inflated, best case, Zenithal Hourly Rate (ZHR), but what a naked-eye observer at a specific place and time should actually expect to see.

A shower's published ZHR assumes the radiant is at the zenith and the sky is dark enough to see magnitude 6.5 stars. Neither is usually true, so this combines three corrections:

  1. Day-decayed ZHR -- an asymmetric log-linear falloff around the shower's peak date (showers typically ramp up faster than they decline).
  2. Radiant altitude -- computed from the observer's coordinates and time; a radiant near the horizon is heavily foreshortened (rate ∝ sin(radiant altitude)). A radiant up to ~10 degrees below the local horizon can still produce visible meteors (they ablate ~100km up, well above the radiant's own point on the horizon); that ~10 degrees is a derived geometric estimate, not a value from meteor-science literature -- see forecast.HORIZON_DIP_DEG.
  3. Sky brightness -- the site's Bortle class, combined with the actual moon illumination on the given date, degrades the naked-eye limiting magnitude, which disproportionately hides a shower's fainter meteors according to its population index.

Both the Bortle class and the moon illumination are estimated automatically (from coordinates and date/time, respectively) unless you override them.

The rate-estimation formulas (the day-decay/radiant-altitude/sky- brightness combination) are adapted from the DarkHours project (MIT licensed). DarkHours has no PyPI package, so this reimplements the relevant math directly (see the module docstrings in src/meteor_shower_forecast/ for exactly what was ported from where) rather than vendoring its much larger, service-oriented codebase.

The ~38-shower catalog (names, IAU codes, radiants, activity, ZHR, population index) is transcribed from the International Meteor Organization's Meteor Shower Calendar, Table 5, "Working List of Visual Meteor Showers" -- the set of showers with a real, currently-maintained ZHR behind them. publishActivity decay rates -- required, alongside ZHR/population index, for an actual rate estimate -- come from the Global Meteor Network's operational flux-monitoring data (21 showers), Jenniskens (1994, Astronomy & Astrophysics, 287, 990) for a few more, and for the remaining minor showers an estimate from each one's IMO activity period (see "Activity decay rates" below). Parent bodies are cross-referenced from the IAU Meteor Data Center's List of Established Showers; per its citation request, work using it should cite Jenniskens et al. 2020 (Planetary and Space Science, 182, 104821) and Jopek & Kanuchova 2017 (Planetary and Space Science, 143, 3-6) -- see catalog.py's docstring for both citations and exact source editions.

Runtime dependencies are kept minimal: bortlefinder (coordinates -> Bortle class/SQM, extracted from this project's own earlier coordinate-based Bortle estimation and published standalone) and timezonefinder (coordinates -> IANA timezone, for local-time display).

Install

pip install meteor-shower-forecast

Usage

With just coordinates, the CLI reports the next upcoming shower, evaluated at this observer's best upcoming viewing opportunity -- not the shower's catalog peak. The catalog peak is a global, radiant-independent instant (when Earth crosses the dust trail most densely); it says nothing about whether the radiant is even above your horizon then. A bracketed search across the shower's active window (restricted to astronomical night, so it never picks a daytime moment) finds the actual best time. peak is still reported alongside best for reference. Sky brightness and moon illumination are estimated automatically, and dates/times are shown in local time with the 3-letter timezone abbreviation (EDT, PST, JST, ...) looked up from the coordinates:

meteor-shower-forecast --lat 35.7796 --lon -78.6382
Orionids (ORI)
  parent body:         1P/Halley
  best:                Wed, Oct 21, 2026, 5:47 AM EDT
  peak:                Wed, Oct 21, 2026, 11:13 AM EDT
  days from peak:      -0.2
  effective ZHR:       18.8
  radiant altitude:    70.0° (az 188.6°)
  sky brightness:      Bortle 8 (City sky, SQM 17.75) (estimated from coordinates)
  moon:                78% illuminated (estimated for this date/time)
  limiting magnitude:  3.67
  faint-meteor factor: 0.075
  ~ estimated rate:    1.3 meteors/hour
  note: moonlight brightens the sky by 0.10 mag/arcsec²
  note: +1.0 more meteors/hour than observing at the shower's catalog peak from this location

This matters most when the catalog peak itself is a bad time to look -- e.g. the Quadrantids' 2027 peak falls with the radiant just below the horizon from this location (an estimated 3.4 meteors/hour), but a few hours later, once the radiant has climbed well clear of the horizon, the same shower produces over five times the rate:

meteor-shower-forecast --lat 35.0 --lon -78.6 --shower Quadrantids
Quadrantids (QUA)
  parent body:         2003 EH1
  best:                Mon, Jan 4, 2027, 5:51 AM EST
  peak:                Sun, Jan 3, 2027, 7:21 PM EST
  days from peak:      +0.4
  effective ZHR:       34.8
  radiant altitude:    56.4° (az 52.3°)
  ...
  ~ estimated rate:    18.8 meteors/hour
  note: +15.4 more meteors/hour than observing at the shower's catalog peak from this location

Pass --year YYYY to look at a different year's occurrence instead of the next upcoming one (past years work too) -- it still searches for that year's best time, unlike --when, which pins one exact instant (the two are mutually exclusive):

meteor-shower-forecast --lat 35.0 --lon -78.6 --shower Quadrantids --year 2020

--shower NAME without --when works the same way: it searches that shower's own upcoming active window for the best time. Pass --when explicitly to evaluate one specific date and time instead (e.g. checking a shower a day past peak) -- the field is then labeled time, not best, since it's exactly what you asked for rather than a search result.

Override the automatic moon estimate, or pass 0 to ignore moonlight entirely:

meteor-shower-forecast --lat 35.0 --lon -78.6 \
  --shower Perseids --when 2026-08-12T05:00:00 --moon-illumination 80

Compare every shower's realistic best-case rate, each at its own best upcoming observing time, sorted chronologically by (catalog) peak date (pass --when to instead compare what's active on one specific night):

meteor-shower-forecast --lat 35.0 --lon -78.6 --shower all

Pass --bortle <1-9> or --sqm <value> to override the coordinate-based sky-brightness estimate with a known Bortle class or a direct sky quality meter reading (mag/arcsec²).

List the known showers (all ~38 on the IMO Working List, each with its IAU code and parent body when known; look one up by name, prefix, or IAU code -- e.g. --shower SDA):

meteor-shower-forecast --list-showers
QUA   Quadrantids                      peak 01-04  ZHR 80, parent 2003 EH1
GUM   gamma-Ursae Minorids             peak 01-18  ZHR 3 (estimated decay rate)
...
PPU   pi-Puppids                       peak 04-23  no rate data (variable/outburst shower), parent 26P/Grigg-Skjellerup

Activity decay rates

A rate estimate needs ZHR, population index, and an activity decay rate -- how fast ZHR rises toward and falls away from the peak. IMO's calendar publishes the first two for every shower on its Working List, but not decay rates, so they come from elsewhere, and each shower's source is recorded as ShowerDef.decay_source (and the CSV's decay_source column).

The model

Activity follows the asymmetric log-linear profile used by both GMN (RMS/Formats/Showers.py, Shower.computeZHRFloat) and Jenniskens (1994):

ZHR(t) = ZHR_peak * 10^(-B * |t - t_peak|)

with B = b_rise before the peak and B = b_decline after it. Both sources define B per degree of solar longitude; this package applies it per day (the Sun moves ~0.986 degrees/day, so ~1.5% error most of the year).

Sources, in order of preference

decay_source Showers What it is
gmn 21 Measured: Global Meteor Network flux-monitoring fits
jenniskens1994 EGE, ACE Measured: Jenniskens (1994) visual-observation fits
imo_activity_estimate GUM, ELY, JPE, GDR, ERI, SLY, OCT, NOO, AND, PUP, COM Estimated from the IMO activity period
(none) PPU, JBO, AMO, PHO Variable/outburst-only: no baseline ZHR to decay from

1. Global Meteor Network. Transcribed from share/flux_showers.csv in CroatianMeteorNetwork/RMS, the "NASA Meteoroid Environment Office" section only. The file's other sections use an undifferentiated Bp = Bm = 0.2 placeholder rather than a per-shower fit, so they're not used. Cite:

  • Vida, D.; Segon, D.; Gural, P.S.; et al. 2021, "The Global Meteor Network -- Methodology and First Results", MNRAS 506(4), 5046-5074.
  • Vida, D.; Blaauw Erskine, R.C.; Brown, P.G.; et al. 2022, "Computing optical meteor flux using Global Meteor Network data", MNRAS 515(2), 2322-2339.

2. Jenniskens (1994). Measured B values for many minor annual showers, from multi-year visual observations by Dutch Meteor Society and Australian observers:

  • Jenniskens, P. 1994, "Meteor stream activity I. The annual streams", Astronomy & Astrophysics 287, 990-1013.

The values here are transcribed from the Dutch Meteor Society's abridged edition of its data table, not the paper itself -- verify against the original before citing. It gives one symmetric B per shower. GMN's NASA-MEO set appears to derive largely from this paper -- for the showers both cover, many values are identical, so the two sources are consistent with each other:

Shower Jenniskens B GMN b_rise / b_decline
Taurids 0.026 0.026 / 0.026 (STA and NTA)
kappa-Cygnids 0.069 0.069 / 0.069
sigma-Hydrids 0.10 0.10 / 0.10
Orionids 0.12 0.120 / 0.119
Monocerotids 0.25 0.25 / 0.25

Where they differ, it's mostly major showers (Perseids 0.20 vs 0.35; Geminids 0.39/0.72 vs 0.15/0.462), which Jenniskens fit as a main peak plus a separate broad background component.

Only two of the showers GMN lacks are covered: epsilon-Geminids (B 0.082, ZHR 2.9 vs IMO's 3) and alpha-Centaurids (B 0.18, ZHR 7.3 vs IMO's 6). Jenniskens' Puppid-Velids value (0.034) is deliberately not used: his PUP is a broader multi-radiant complex (ZHR 4.5, maximum at 251 degrees) than IMO's (ZHR 10, and IMO's 255 degrees is a radiant reference date only), so his slope doesn't fit IMO's peak.

3. Estimate from the IMO activity period. For the remaining minor showers, B is derived from the activity period (start and end dates) that IMO's Table 5 lists for every shower. The assumption: those limits mark where a shower stops being distinguishable, which happens at roughly the same absolute ZHR for every minor shower. So on each side of the peak:

B_side = log10(ZHR_peak / ZHR_edge) / (days from peak to activity limit)

ZHR_edge is calibrated from the GMN-measured showers -- the median, over both sides of each, of ZHR_peak * 10^(-B * days to limit). It currently comes out at 0.46, and scripts/build_shower_catalog.py recomputes it on every run, so it tracks updated GMN or IMO data.

The calibration uses only the 12 GMN showers with ZHR <= 10 (AUR, CAP, DRA, DSX, HYD, KCG, LMI, MON, NTA, SPE, STA, URS), since every shower being estimated is in that range. Major showers' IMO activity limits trace a broad, low-level background rather than the core peak GMN's B fits: the Perseids' listed activity spans 38 days, but GMN's B = 0.35 takes them from ZHR 110 to 1 in about 6.

Validation. Each GMN shower was left out in turn, its B predicted from the rest, and compared with GMN's measured value (both sides of each shower, so 24 sides for the minor set, 42 for all 21):

Edge assumption Calibrated on Median error Within 2x
Fixed absolute ZHR (used) ZHR <= 10 0.20 dex (1.6x) 67%
Fixed fraction of peak ZHR ZHR <= 10 0.22 dex (1.6x) 58%
Fixed absolute ZHR all 21 0.28 dex (1.9x) 57%
Fixed fraction of peak ZHR all 21 0.35 dex (2.2x) 43%

As an independent check against the Jenniskens values (not used in the calibration):

Shower Estimated b_rise / b_decline Jenniskens B
epsilon-Geminids 0.176 / 0.086 0.082
alpha-Centaurids 0.128 / 0.090 0.18
Puppid-Velids 0.210 / 0.154 0.034

EGE and ACE land within about 2x of the measured value; PUP is off by about 5x, consistent with the definitional mismatch above. Where a measured value exists it's used instead of the estimate.

Alternatives considered and rejected:

  • One B for every minor shower (e.g. the median of the minor GMN showers, 0.1) -- simplest, but measured minor-shower values span 0.026 (Taurids) to 0.25 (Monocerotids), so one number would be badly wrong for many showers.
  • GMN's 0.2 placeholder -- not shower-specific; GMN itself doesn't present it as a fit.

Details of the estimate:

  • Activity dates are taken as whole days: the start date from 00:00 UTC and the end date through 24:00 UTC.
  • Each side is at least 1 day long. Without this, October Camelopardalids (listed activity Oct 5-6, peaking on the 6th) would get an arbitrarily steep decline driven by timing noise; it still gets B ~1.0, consistent with it being a brief shower.
  • A lopsided activity period gives a lopsided B. September Lyncids (3 days before the peak, 25 after) gets 0.27 / 0.031, and Andromedids (24 before, 4 after) gets 0.042 / 0.223. That's what IMO's data implies, not a measured profile.

Every rate estimate for a shower with an estimated decay rate says so in its notes, and --list-showers marks it "(estimated decay rate)".

How much the decay rate matters

For a minor shower, B barely affects the peak-night forecast -- at the peak, the rate depends only on ZHR, radiant altitude, and sky brightness. A ZHR-3 shower gives about one meteor an hour at the peak whatever B is, against a sporadic background of ~5-10 an hour.

What B does decide is how many nights around the peak a shower counts as active, since anything below ZHR 2 (forecast.ZHR_DECAY_FLOOR) is treated as indistinguishable from the sporadic background. For a ZHR-3 shower that's log10(3 / 2) / B days either side of the peak: about +/-7 days at B = 0.026, +/-1.8 days at B = 0.1, and +/-0.7 days at B = 0.25. A ~1.6x error in B gives a proportional error in that window.

Showers with no rate estimate

The variable/outburst-only showers (e.g. alpha-Monocerotids) have no baseline ZHR, so they get no decay rate and no rate estimate. Looking one up by name/code still reports its radiant, population index, parent body, and next peak date, with estimated_rate_per_hour (and the fields that feed it) None.

Estimating Bortle class from coordinates

By default, sky brightness is derived from --lat/--lon via the bortlefinder package, which reads a small, locally-fetched grid based on the Falchi et al. (2016) New World Atlas of Artificial Night Sky Brightness (doi.org/10.5880/GFZ.1.4.2016.001, CC BY-NC 4.0) -- built primarily from the VIIRS Day/Night Band instrument aboard the Suomi NPP satellite (a joint NASA/NOAA mission). Fetch it once (downloads ~684MB, reduced to a ~19MB local grid; needs the build extra):

pip install meteor-shower-forecast[build]
bortlefinder-fetch-grid

This derivation is approximate: satellite atlases measure zenith brightness, while the Bortle scale is a subjective whole-sky rating dominated by horizon light domes. David Lorenz's own validation against paired dark-sky observations found real disagreement -- often a full class, worse in the Bortle 5-7 range (source). Treat it as a starting estimate, not a substitute for a real SQM meter reading -- every result marks whether its Bortle class was estimated or given directly. See bortlefinder's own README for more on its accuracy and licensing (the fetched grid data is CC BY-NC 4.0, non-commercial; the package code itself is MIT).

Moon illumination is likewise a low-precision approximation (mean lunar elongation, no ephemeris) good to within a few percent -- see astro.moon_illumination_pct.

The best-time search only considers astronomical night (Sun below 18 degrees altitude), matching the fully-dark-sky assumption already baked into the Bortle/SQM model, and never proposes a time before now. If a shower's whole active window at a given location never reaches astronomical darkness (e.g. high-latitude summer), it falls back to the least-bright moment found and says so in a note.

Library

from meteor_shower_forecast import estimate_shower_rate, estimate_all_showers, next_shower

estimate = estimate_shower_rate("Perseids", lat=35.0, lon=-78.6, bortle=4)
print(estimate.iau_code, estimate.parent_body, estimate.estimated_rate_per_hour)

for e in estimate_all_showers(lat=35.0, lon=-78.6, bortle=4):
    print(e.shower, e.estimated_rate_per_hour)

upcoming = next_shower()
print(upcoming.name)

# Any of the ~38 IMO-listed showers can be looked up by name/code;
# estimated_rate_per_hour is None for the variable/outburst-only showers,
# which have no baseline ZHR (e.g. alpha-Monocerotids).
monocerotids = estimate_shower_rate("AMO", lat=35.0, lon=-78.6, bortle=4)
print(monocerotids.radiant_alt_deg, monocerotids.estimated_rate_per_hour)

# reference anchors the best-time search to a given year (or any datetime)
# instead of now -- past years work too. Ignored if when_utc is passed.
from datetime import datetime, timezone
past = estimate_shower_rate(
    "Geminids", lat=35.0, lon=-78.6, bortle=4, reference=datetime(2020, 1, 1, tzinfo=timezone.utc)
)
print(past.when_utc, past.estimated_rate_per_hour)

Development

git clone https://github.com/rtphokie/meteor-shower-forecast
cd meteor-shower-forecast
uv sync

The shower catalog (src/meteor_shower_forecast/data/shower_catalog.csv) is generated, not hand-edited -- see scripts/build_shower_catalog.py's docstring to regenerate it from a newer IMO calendar or updated GMN data. Rerunning it also recalibrates the activity-period decay estimates.

Metadata

Release files for meteor-shower-forecast 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for meteor-shower-forecast 0.2.0
File Size Uploaded Provenance Actions
meteor_shower_forecast-0.2.0.tar.gz 28.0 kB Without provenance Details for meteor_shower_forecast-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for meteor-shower-forecast 0.2.0
File Interpreter ABI Platform Actions
meteor_shower_forecast-0.2.0-py3-none-any.whl Python 3 none any Details for meteor_shower_forecast-0.2.0-py3-none-any.whl

Total release size: 59.8 kB

Release files / meteor_shower_forecast-0.2.0.tar.gz

Download URL meteor_shower_forecast-0.2.0.tar.gz
Size 28.0 kB
Tags Source
SHA-256 checksum
How to use checksums
1341de0341b45e9683e1fea7bda9b244e834b35dc460e5fb6781890f52e02a86
BLAKE2b-256 checksum
How to use checksums
102f6fb2620109a4598553cbd854233eb99c1a10deb432e1878a2068ad7737af
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Release files / meteor_shower_forecast-0.2.0-py3-none-any.whl

Download URL meteor_shower_forecast-0.2.0-py3-none-any.whl
Size 31.9 kB
Tags Python 3
SHA-256 checksum
How to use checksums
ca43c71674a28d11ca0afb938c695ad530b54c7cc162fe6b2b584c123657ec64
BLAKE2b-256 checksum
How to use checksums
09152cffb0444a2ece4ec4641bc6b7387fedd1449f16b64c0d49fb03e04e7c69
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.11

Security for meteor-shower-forecast 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page