MPC NEA Observation Planning Aid: a Python Convenience Wrapper¶
This tutorial wraps the MPC's NEA Observation Planning Aid web form so it can be driven from Python, returning the list of near-Earth asteroids worth observing as a pandas table.¶
The NEA Observation Planning Aid generates, for a given date and observatory, a filtered list of known near-Earth asteroids worth observing, i.e. those needing astrometric follow-up, rankable by positional uncertainty, brightness, sky position, and more.
It is a web form, not a REST API: there is no JSON endpoint. This tutorial wraps it the same way our WhatsUp tutorial wraps the Observing Target List: we POST the form fields a browser would send, then parse the returned fixed-format text list into a pandas DataFrame.
How this differs from WhatsUp:
- the Observing Target List answers "what is bright right now?" for casual or outreach observing,
- the NEA Observation Planning Aid targets follow-up astrometry — near-Earth asteroids whose orbits need improving, ranked by their positional uncertainty.
Some important framing before we start:
- This is an unofficial convenience — if the page layout changes, the wrapper may need updating.
- Please be considerate: this drives the same production service as the web page, so keep query volumes modest.
- If you need this kind of query programmatically at scale, tell the MPC via the Jira Helpdesk — demand is what motivates a real API.
Import Packages¶
Here we import the standard Python packages we use in this tutorial. In addition to requests, this tutorial needs pandas (pip install requests pandas).
import re
import requests
import pandas as pd
How the Form Works¶
The wrapper is simple: a single POST to https://cgi.minorplanetcenter.net/cgi-bin/neaobs_getlist.cgi with the form fields is all that is needed.
The response is an HTML page whose <pre> block holds one fixed-format text line per object; the wrapper parses each line with a regular expression.
One convention to be aware of: the form's RA window is given in degrees relative to opposition (the anti-solar point), not in absolute RA. For example, ra_opp_range=(-15, 15) selects a window of plus or minus one hour of RA centred on the opposition point.
Query Parameters¶
| Parameter | Description |
|---|---|
obs_date |
'YYYYMMDD' string; must be within roughly a year of today |
obscode |
MPC observatory code, used for the MPES ephemeris links the service embeds |
ra_opp_range |
RA window in degrees relative to opposition (the anti-solar point) |
dec_range |
Declination window in degrees |
mag_range |
Predicted V magnitude window (bright limit, faint limit) |
motion_range |
Apparent motion window in arcsec/min |
elong_range |
Solar elongation window in degrees |
min_gal_lat |
Minimum absolute galactic latitude in degrees (to avoid crowded fields) |
unc_range, unc_sigma |
Sky-plane positional uncertainty window in arcsec, quoted at 1, 2 or 3 sigma |
not_seen_for_days |
Only return objects not observed for at least this many days |
types |
Subset of "V" (VIs), "P" (PHAs), "t" (Atens), "p" (Apollos), "m" (Amors) |
include_numbered |
Whether to include numbered objects |
sort_by |
"designation", "uncertainty", "magnitude", "dec" or "ra" |
descending |
Sort direction |
The Wrapper Function¶
The function below performs the POST-and-parse sequence described above and returns the results as a pandas DataFrame.
URL = "https://cgi.minorplanetcenter.net/cgi-bin/neaobs_getlist.cgi"
LINE_RE = re.compile(
r"^\s*(?P<desig>.+?)\s+Pos = \((?P<ra>\d\d \d\d), (?P<dec>[+-]\d+\.\d)\), "
r"V = (?P<V>[\d.]+), El\. = *(?P<elong>[\d.]+), "
r"\d-sig unc\. = *(?P<unc>[\d.]+)\", b = (?P<galb>[+-][\d.]+), "
r"Mot\. = *(?P<motion>[\d.]+)\"/(?P<motunit>\w+), (?P<notes>.*)$")
def get_nea_planning_list(obs_date, obscode="500",
ra_opp_range=(-120, 120), dec_range=(-90, 90),
mag_range=(1.0, 21.0), motion_range=(0.0, 5.0),
elong_range=(60, 180), min_gal_lat=0,
unc_range=(0, 1800), unc_sigma=3,
not_seen_for_days=0,
types=("V", "P", "t", "p", "m"),
include_numbered=True,
sort_by="uncertainty", descending=True):
"""Query the MPC NEA Observation Planning Aid; return a DataFrame.
obs_date : 'YYYYMMDD' string (must be within about a year of today)
obscode : MPC observatory code (used for the embedded MPES links)
ra_opp_range : RA window in DEGREES RELATIVE TO OPPOSITION (the
anti-solar point), e.g. (-15, 15) is +/- 1 hour of RA
types : subset of V (VIs), P (PHAs), t (Atens), p (Apollos),
m (Amors)
sort_by : designation | uncertainty | magnitude | dec | ra
"""
sort_codes = {"designation": "1", "uncertainty": "2",
"magnitude": "3", "dec": "4", "ra": "5"}
payload = {
"date": str(obs_date),
"ralo": str(ra_opp_range[0]), "rahi": str(ra_opp_range[1]),
"declo": str(dec_range[0]), "dechi": str(dec_range[1]),
"magbr": str(mag_range[0]), "magfa": str(mag_range[1]),
"motlo": str(motion_range[0]), "mothi": str(motion_range[1]),
"mtype": "m", # motion cut in "/min
"elolo": str(elong_range[0]), "elohi": str(elong_range[1]),
"gallat": str(min_gal_lat),
"unclo": str(unc_range[0]), "unchi": str(unc_range[1]),
"uncsig": str(unc_sigma),
"dayssince": str(not_seen_for_days),
"sort": sort_codes[sort_by],
"dirsort": "2" if descending else "1",
"oc": obscode, "ndate": "1", "ephint": "1", "ephunit": "h",
"raty": "a", "motty": "t", "motun": "m",
}
type_fields = {"V": "typ1", "P": "typ2", "t": "typ3", "p": "typ4", "m": "typ5"}
for t in types:
payload[type_fields[t]] = t
for field, val in [("stat1", "N"), ("stat2", "M"), ("stat3", "1"), ("stat4", "P")]:
if field == "stat1" and not include_numbered:
continue
payload[field] = val
r = requests.post(URL, data=payload, timeout=180)
r.raise_for_status()
m = re.search(r"<pre>(.*?)</pre>", r.text, re.S | re.I)
if not m:
raise ValueError("No object list found in the response")
rows = []
for line in re.sub(r"<[^>]+>", "", m.group(1)).splitlines():
lm = LINE_RE.match(line)
if lm:
d = lm.groupdict()
rows.append({
"Designation": d["desig"].strip(),
"RA (h m)": d["ra"], "Dec (deg)": float(d["dec"]),
"V": float(d["V"]), "Elong (deg)": float(d["elong"]),
f"{payload['uncsig']}-sig unc (arcsec)": float(d["unc"]),
"Gal b (deg)": float(d["galb"]),
'Motion ("/min)': float(d["motion"]),
"Notes": d["notes"].strip(),
})
return pd.DataFrame(rows)
Example 1: Follow-Up Targets for a Night¶
Here we ask for follow-up targets for the night of 2026 September 20 as seen from ATLAS-HKO, Haleakala (observatory code T05), keeping objects brighter than V = 20.5. With the default sort_by="uncertainty", the objects most in need of follow-up appear first.
Note that obs_date must be close to the current date: the service offers a rolling window roughly one year wide, so re-running this notebook later needs a fresh 'YYYYMMDD' value.
df = get_nea_planning_list("20260920", obscode="T05", mag_range=(1.0, 20.5))
print(len(df), "objects")
df
143 objects
| Designation | RA (h m) | Dec (deg) | V | Elong (deg) | 3-sig unc (arcsec) | Gal b (deg) | Motion ("/min) | Notes | |
|---|---|---|---|---|---|---|---|---|---|
| 0 | 2026 RM | 23 35 | 23.7 | 20.4 | 154.8 | 33.9 | -36.0 | 4.95 | 1-opp Amor, fading |
| 1 | 2026 RR | 22 54 | 19.7 | 20.3 | 155.2 | 16.5 | -35.2 | 2.27 | 1-opp Apollo, fading |
| 2 | 2019 GK5 | 20 37 | -11.6 | 19.1 | 131.6 | 10.6 | -28.6 | 4.02 | Amor, fading |
| 3 | 2026 PC16 | 23 10 | 5.9 | 20.1 | 168.1 | 8.1 | -48.8 | 3.72 | 1-opp Apollo, fading |
| 4 | 2026 QR2 | 23 55 | 0.2 | 18.2 | 177.7 | 5.8 | -59.5 | 3.36 | 1-opp Aten, fading |
| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
| 138 | (412995) | 20 10 | -74.4 | 20.4 | 100.3 | 0.0 | -31.3 | 0.80 | Amor, fading |
| 139 | (136993) | 23 01 | -50.1 | 18.3 | 130.2 | 0.0 | -58.9 | 1.67 | Apollo, brightening |
| 140 | (88264) | 16 24 | -26.2 | 19.3 | 71.9 | 0.0 | 16.1 | 2.37 | Amor |
| 141 | (89355) | 19 07 | -38.9 | 19.4 | 106.2 | 0.0 | -19.5 | 0.55 | Amor, fading |
| 142 | (112221) | 19 45 | 63.8 | 19.8 | 101.2 | 0.0 | 18.6 | 0.86 | Amor |
143 rows × 9 columns
Example 2: Bright PHAs, Ranked by Brightness¶
The same night, but now restricted to Potentially Hazardous Asteroids (types=("P",)), with a slightly fainter magnitude cut, sorted brightest first. We display a subset of the columns.
df_pha = get_nea_planning_list("20260920", obscode="T05", types=("P",),
mag_range=(1.0, 21.5), sort_by="magnitude",
descending=False)
df_pha[["Designation", "RA (h m)", "Dec (deg)", "V", "Elong (deg)",
'Motion ("/min)', "Notes"]]
| Designation | RA (h m) | Dec (deg) | V | Elong (deg) | Motion ("/min) | Notes | |
|---|---|---|---|---|---|---|---|
| 0 | (111253) | 16 16 | -12.8 | 16.8 | 68.1 | 3.56 | PHA, Apollo, fading |
| 1 | (267729) | 23 14 | 0.1 | 17.9 | 171.3 | 1.57 | PHA, Apollo |
| 2 | (221455) | 22 34 | -11.3 | 17.9 | 159.1 | 0.77 | PHA, Apollo, fading |
| 3 | (164216) | 20 24 | 24.8 | 18.0 | 124.2 | 1.11 | PHA, Apollo, fading |
| 4 | 2017 BP31 | 22 43 | -14.6 | 18.4 | 159.1 | 0.82 | PHA, Apollo, fading |
| 5 | (679786) | 21 26 | -1.4 | 18.9 | 144.7 | 2.10 | PHA, Apollo, brightening |
| 6 | (4015) | 16 16 | -20.1 | 19.0 | 69.0 | 1.36 | PHA, Apollo, brightening |
| 7 | (363790) | 15 50 | -3.9 | 19.1 | 60.7 | 1.36 | PHA, Apollo |
| 8 | (678976) | 23 16 | -10.3 | 19.2 | 168.1 | 1.80 | PHA, Apollo, brightening |
| 9 | 2019 OQ2 | 20 08 | -9.6 | 19.4 | 124.8 | 3.63 | PHA, Apollo, fading |
| 10 | (756316) | 15 55 | -14.4 | 19.5 | 63.1 | 2.81 | PHA, Apollo, brightening |
| 11 | (275677) | 21 29 | -8.5 | 19.6 | 144.8 | 1.69 | PHA, Apollo, fading |
| 12 | (163051) | 20 22 | -4.4 | 19.7 | 128.5 | 0.31 | PHA, Apollo, fading |
| 13 | 2020 DR2 | 18 23 | -9.1 | 19.9 | 98.9 | 1.60 | PHA, Apollo |
| 14 | (398188) | 16 05 | -36.3 | 20.1 | 70.5 | 1.30 | PHA, Aten, fading |
| 15 | (422787) | 16 06 | -33.9 | 20.2 | 69.9 | 1.27 | PHA, Apollo, brightening |
| 16 | 2016 LN1 | 16 58 | -15.9 | 20.2 | 78.3 | 2.66 | PHA, Apollo, brightening |
| 17 | 2012 QC8 | 19 08 | -31.0 | 20.2 | 107.8 | 0.49 | PHA, Apollo |
| 18 | 2016 NV | 17 40 | 34.9 | 20.2 | 87.7 | 0.31 | PHA, Apollo, brightening |
| 19 | (668949) | 16 25 | -10.1 | 20.4 | 69.8 | 0.29 | PHA, Apollo |
| 20 | (140158) | 22 51 | -10.9 | 20.5 | 163.0 | 1.07 | PHA, Apollo, fading |
| 21 | (363344) | 20 41 | -10.2 | 20.6 | 132.9 | 1.49 | PHA, Apollo, fading |
| 22 | 2018 BA7 | 16 09 | -26.6 | 20.7 | 68.7 | 1.57 | PHA, Apollo, brightening |
| 23 | (214869) | 19 42 | -21.1 | 20.7 | 117.0 | 0.15 | PHA, Apollo, fading |
| 24 | (16960) | 20 24 | -0.3 | 20.7 | 129.1 | 0.37 | PHA, Apollo, fading |
| 25 | (163364) | 21 58 | -3.2 | 20.8 | 152.6 | 0.84 | PHA, Apollo, fading |
| 26 | (163818) | 19 19 | -3.9 | 20.8 | 112.9 | 0.43 | PHA, Apollo, fading |
| 27 | (518810) | 17 53 | -32.2 | 20.9 | 91.9 | 1.89 | PHA, Apollo, fading |
| 28 | (412983) | 23 32 | 2.8 | 20.9 | 174.2 | 1.17 | PHA, Amor, fading |
| 29 | (478574) | 23 21 | -31.6 | 20.9 | 149.1 | 1.43 | PHA, Apollo, fading |
| 30 | (873893) | 17 06 | -39.0 | 21.0 | 82.8 | 3.10 | PHA, Amor, fading |
| 31 | (620082) | 20 28 | -16.2 | 21.0 | 128.6 | 1.32 | PHA, Apollo, fading |
| 32 | (85713) | 19 59 | -34.4 | 21.0 | 117.4 | 0.21 | PHA, Apollo, fading |
| 33 | 2018 UA1 | 16 08 | -40.1 | 21.0 | 72.0 | 2.47 | PHA, Apollo, brightening |
| 34 | 2017 WG2 | 19 07 | -52.6 | 21.1 | 102.9 | 2.75 | PHA, Apollo, fading |
| 35 | 2024 NO5 | 17 01 | -22.0 | 21.2 | 79.7 | 1.45 | PHA, Apollo, fading |
| 36 | (523650) | 21 03 | 16.9 | 21.2 | 135.5 | 1.85 | PHA, Apollo, fading |
| 37 | (448003) | 23 09 | -39.9 | 21.2 | 140.4 | 0.99 | PHA, Amor, fading |
| 38 | (612786) | 22 49 | -2.4 | 21.2 | 165.3 | 0.84 | PHA, Apollo, fading |
| 39 | 2024 BR4 | 18 37 | -5.0 | 21.3 | 102.3 | 3.07 | PHA, Apollo, fading |
| 40 | (171839) | 22 38 | 22.2 | 21.3 | 150.9 | 1.09 | PHA, Amor, fading |
| 41 | (26663) | 19 53 | -13.7 | 21.4 | 120.7 | 0.15 | PHA, Apollo, fading |
| 42 | (526878) | 16 28 | -22.8 | 21.4 | 72.2 | 3.22 | PHA, Apollo, fading |
| 43 | 2026 EA2 | 19 36 | 12.9 | 21.4 | 116.0 | 2.18 | 1-opp PHA, Amor, fading |
| 44 | 2024 RR14 | 22 17 | 17.6 | 21.4 | 150.7 | 1.45 | PHA, Apollo |
| 45 | (5011) | 17 05 | -30.5 | 21.5 | 81.4 | 0.68 | PHA, Apollo, fading |
| 46 | 2018 YH | 21 13 | 4.8 | 21.5 | 140.8 | 0.43 | PHA, Apollo, fading |
| 47 | (90416) | 20 06 | -20.2 | 21.5 | 122.8 | 0.46 | PHA, Apollo, fading |
| 48 | 2020 RB | 16 54 | 77.1 | 21.5 | 85.7 | 1.26 | PHA, Apollo, fading |
Reading the Results¶
Each row is one near-Earth asteroid meeting the requested filters on the requested night:
- Designation — the object's designation (on the web page this links to an MPES ephemeris for your observatory code).
- RA (h m) and Dec (deg) — the predicted position (J2000).
- V — the predicted visual magnitude.
- Elong (deg) — the solar elongation.
- 3-sig unc (arcsec) — the sky-plane positional uncertainty (the column name follows your
unc_sigmachoice). This is the key follow-up metric: a large uncertainty means a poorly known orbit, and hence a high-value target. - Gal b (deg) — the galactic latitude; low absolute values mean crowded star fields.
- Motion ("/min) — the apparent rate of motion.
- Notes — the object class (VI, PHA, Aten, Apollo, Amor) plus whether it is brightening or fading.
The follow-up logic is explicit in the sorting: an object with high positional uncertainty that is observable from your site is a high-priority target — observing it is what shrinks the uncertainty.
Caveats¶
- This wrapper is unofficial: it automates the public web form, so results are identical to the website, but if the page layout or output format changes, the wrapper may break and need updating.
obs_datemust fall inside the service's rolling window, which is roughly a year wide; hard-coded dates go stale.- The RA window is expressed in degrees relative to opposition, not absolute RA — this is the form's own convention.
- A
"No object list found"error means either no objects matched your constraints, or the page layout changed.
Summary¶
This tutorial demonstrated how to drive the MPC NEA Observation Planning Aid web form from Python:
- Form URL:
https://cgi.minorplanetcenter.net/cgi-bin/neaobs_getlist.cgi(a single POST of the form fields — no token needed) - Parsing: each fixed-format line of the returned
<pre>block is regex-parsed into a row of a pandas DataFrame - Filtering: by date, magnitude, motion, elongation, galactic latitude, positional uncertainty, days since last observation, and NEA class
- Sorting: by designation, uncertainty, magnitude, declination or RA, in either direction
Further Resources¶
- NEA Observation Planning Aid
- WhatsUp wrapper tutorial
- NEOCP Observations API tutorial
- MPC API tutorials page
For questions or feedback, contact the MPC via the Jira Helpdesk.