Preparing to merge this repository into a combined astrophotography repo. session-scripts/ becomes pipeline/ because the scripts import layout.py from their own directory and must stay together, and because 'pipeline' says what it is rather than how it came about. observing/ stays at the top level: observing plans are not processing code.
191 lines
7.2 KiB
Python
191 lines
7.2 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
mo_mpc.py -- "was there a known minor planet in the frame?"
|
|
|
|
Purpose
|
|
-------
|
|
The NGC 5128 (Centaurus A) LRGB session of 2026-07-21 was searched for moving
|
|
objects (see _mo_dets.npz / _mo_tracklets.npy). Before any detection can be
|
|
called a *new* object, we must rule out that it is simply a catalogued minor
|
|
planet that happened to drift through the field. This script asks the
|
|
authoritative services what known small bodies were inside the field of view at
|
|
the epoch of the exposures.
|
|
|
|
Field / epoch
|
|
-------------
|
|
Field centre : RA 13 25 27.37, Dec -43 01 10.9 (J2000)
|
|
= 201.36404 deg, -43.01969 deg
|
|
Field size : 42.9' x 28.6' (half-diagonal ~25.8')
|
|
Search radius: 30 arcmin (0.5 deg) -- comfortably encloses the whole frame
|
|
Epoch : 2026-07-21 09:22 UTC (= 2026 07 21.39), mid-luminance-sequence
|
|
(luminance ran 08:56-10:00 UTC, session ended ~11:16 UTC)
|
|
Observatory : Q62, iTelescope / Siding Spring Observatory
|
|
|
|
Services queried
|
|
----------------
|
|
1. JPL Small-Body Identification API
|
|
https://ssd-api.jpl.nasa.gov/sb_ident.api
|
|
Primary source. Well documented, accepts arbitrary (including future) epochs
|
|
because it propagates orbits rather than serving a precomputed ephemeris.
|
|
IMPORTANT UNITS GOTCHA: despite some documentation wording, fov-ra-hwidth and
|
|
fov-dec-hwidth are interpreted in DEGREES, not arcminutes. Passing "30"
|
|
yields a 30-degree half-width box (the API echoes the box it used in
|
|
["observer"]["fov_offset"], which is worth checking every run).
|
|
Queried twice: sb-kind=a (asteroids) and sb-kind=c (comets).
|
|
|
|
2. MPC "MPChecker" / minor planet checker
|
|
https://minorplanetcenter.net/cgi-bin/mpcheck.cgi (classic HTTP POST form)
|
|
https://www.minorplanetcenter.net/cgi-bin/checkmp.cgi
|
|
Cross-check. Kept in the script even if it fails, so the log records whether
|
|
the MPC endpoint was reachable and what it said.
|
|
|
|
Output
|
|
------
|
|
Raw responses (JSON + HTML) and a human-readable summary are appended to
|
|
intermediates/_mpc_query.txt
|
|
Nothing is fabricated: if a service errors or refuses the epoch, the error text
|
|
itself is written to the log.
|
|
"""
|
|
|
|
import json
|
|
import math
|
|
import sys
|
|
import datetime
|
|
|
|
import requests
|
|
|
|
import layout
|
|
|
|
# ---------------------------------------------------------------- field/epoch
|
|
RA_SEX = "13-25-27.37"
|
|
DEC_SEX = "M43-01-10.9" # JPL uses a leading "M" for a negative Dec
|
|
RA_DEG = 201.36404
|
|
DEC_DEG = -43.01969
|
|
OBS_TIME = "2026-07-21_09:22:00"
|
|
MPC_CODE = "Q62" # iTelescope, Siding Spring
|
|
RADIUS_ARCMIN = 30.0
|
|
HWIDTH_DEG = RADIUS_ARCMIN / 60.0 # = 0.5 deg; API wants DEGREES here
|
|
VMAG_LIM = 22.0
|
|
|
|
OUT = layout.path("_mpc_query.txt")
|
|
|
|
log_lines = []
|
|
|
|
|
|
def log(s=""):
|
|
print(s)
|
|
log_lines.append(str(s))
|
|
|
|
|
|
# ------------------------------------------------------------------ JPL query
|
|
def jpl(sb_kind):
|
|
"""Query ssd-api.jpl.nasa.gov/sb_ident.api for one small-body class."""
|
|
params = {
|
|
"sb-kind": sb_kind, # 'a' = asteroid, 'c' = comet
|
|
"mpc-code": MPC_CODE,
|
|
"obs-time": OBS_TIME,
|
|
"fov-ra-center": RA_SEX,
|
|
"fov-dec-center": DEC_SEX,
|
|
"fov-ra-hwidth": f"{HWIDTH_DEG}",
|
|
"fov-dec-hwidth": f"{HWIDTH_DEG}",
|
|
"mag-required": "true",
|
|
"two-pass": "true",
|
|
"vmag-lim": f"{VMAG_LIM}",
|
|
}
|
|
r = requests.get("https://ssd-api.jpl.nasa.gov/sb_ident.api",
|
|
params=params, timeout=180)
|
|
log(f"--- JPL sb_ident sb-kind={sb_kind} -> HTTP {r.status_code}")
|
|
log(f" URL: {r.url}")
|
|
if r.status_code != 200:
|
|
log(" RAW: " + r.text[:2000])
|
|
return None
|
|
d = r.json()
|
|
log(" observer : " + json.dumps(d.get("observer", {})))
|
|
log(" n_first_pass : " + str(d.get("n_first_pass")))
|
|
log(" n_second_pass: " + str(d.get("n_second_pass")))
|
|
return d
|
|
|
|
|
|
def show(d, label):
|
|
"""Print the refined (second-pass) hits, falling back to first pass."""
|
|
if d is None:
|
|
return
|
|
rows = d.get("data_second_pass") or []
|
|
fields = d.get("fields_second") or []
|
|
if not rows:
|
|
rows = d.get("data_first_pass") or []
|
|
fields = d.get("fields_first") or []
|
|
log(f" [{label}] no second-pass data; showing first pass")
|
|
log(f" fields: {fields}")
|
|
if not rows:
|
|
log(f" [{label}] NO OBJECTS in field.")
|
|
return
|
|
for row in rows:
|
|
log(" " + " | ".join(str(x) for x in row))
|
|
|
|
|
|
# ------------------------------------------------------------------ MPC query
|
|
def mpchecker():
|
|
"""Classic MPChecker POST form. Recorded even if it fails."""
|
|
data = {
|
|
"year": "2026", "month": "07", "day": "21.39",
|
|
"which": "pos",
|
|
"ra": "13 25 27.37", "decl": "-43 01 10.9",
|
|
"TextArea": "",
|
|
"radius": str(int(RADIUS_ARCMIN)),
|
|
"limit": f"{VMAG_LIM}",
|
|
"oc": MPC_CODE,
|
|
"sort": "d", "mot": "h", "tmot": "s",
|
|
"pdes": "u", "needed": "f", "ps": "n", "type": "p",
|
|
}
|
|
for url in ("https://minorplanetcenter.net/cgi-bin/mpcheck.cgi",
|
|
"https://www.minorplanetcenter.net/cgi-bin/checkmp.cgi"):
|
|
try:
|
|
r = requests.post(url, data=data, timeout=120,
|
|
headers={"User-Agent": "Mozilla/5.0 (mo_mpc.py)"})
|
|
log(f"--- MPC POST {url} -> HTTP {r.status_code}, {len(r.text)} bytes")
|
|
log(r.text[:8000])
|
|
except Exception as e: # noqa: BLE001
|
|
log(f"--- MPC POST {url} -> EXCEPTION {type(e).__name__}: {e}")
|
|
|
|
|
|
# ----------------------------------------------------------------------- main
|
|
if __name__ == "__main__":
|
|
log("MPC / minor-planet field check")
|
|
log("run at " + datetime.datetime.utcnow().isoformat() + "Z")
|
|
log(f"field centre {RA_SEX} {DEC_SEX} (J2000) = {RA_DEG}, {DEC_DEG}")
|
|
log(f"epoch {OBS_TIME} UTC, obs code {MPC_CODE}, "
|
|
f"radius {RADIUS_ARCMIN}' , V<{VMAG_LIM}")
|
|
log()
|
|
|
|
for kind, label in (("a", "asteroids"), ("c", "comets")):
|
|
try:
|
|
show(jpl(kind), label)
|
|
except Exception as e: # noqa: BLE001
|
|
log(f" JPL {label} EXCEPTION {type(e).__name__}: {e}")
|
|
log()
|
|
|
|
mpchecker()
|
|
|
|
# --- derived summary: total sky motion and position angle for JPL hits ---
|
|
log()
|
|
log("=== derived summary (from JPL second pass, sb-kind=a) ===")
|
|
try:
|
|
d = jpl("a")
|
|
for row in (d.get("data_second_pass") or []):
|
|
name, ra, dec = row[0], row[1], row[2]
|
|
norm = float(row[5])
|
|
vmag = row[6]
|
|
dra, ddec = float(row[7]), float(row[8]) # "/h, RA rate has cos(dec)
|
|
tot = math.hypot(dra, ddec)
|
|
pa = math.degrees(math.atan2(dra, ddec)) % 360.0 # N through E
|
|
log(f"{name}: RA {ra} Dec {dec} V={vmag} "
|
|
f"{norm:.0f}\" ({norm/60:.1f}') from centre "
|
|
f"motion {tot:.1f}\"/hr at PA {pa:.1f} deg "
|
|
f"(dRA*cos(dec)={dra:.1f}, dDec={ddec:.1f} \"/hr)")
|
|
except Exception as e: # noqa: BLE001
|
|
log(f"summary EXCEPTION {type(e).__name__}: {e}")
|
|
|
|
with open(OUT, "w", encoding="utf-8") as fh:
|
|
fh.write("\n".join(log_lines) + "\n")
|
|
print("\nwrote " + OUT)
|