Move the processing code under pipeline/
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.
This commit is contained in:
parent
653ca103cd
commit
c6299f41ab
53 changed files with 0 additions and 0 deletions
|
|
@ -1,423 +0,0 @@
|
|||
"""Cross-match six unidentified point-like detections in an amateur NGC 5128
|
||||
(Centaurus A) image against published catalogues, to establish whether each is an
|
||||
already-catalogued object (globular cluster of Cen A, background galaxy, foreground
|
||||
star, X-ray/UV source, planetary nebula, ...) or genuinely uncatalogued.
|
||||
|
||||
Context: the six sources have already been shown NOT to match Gaia DR3 (G<20.5)
|
||||
or SkyMapper DR2. The purpose here is to rule out a supernova / transient, so a
|
||||
firm identification with a static catalogued source is the desired outcome and a
|
||||
"no match" is a result that must be reported honestly rather than manufactured.
|
||||
|
||||
Three independent searches are run for every position:
|
||||
|
||||
1. TARGETED -- a hand-picked list of VizieR catalogues covering Cen A globular
|
||||
clusters and compact stellar systems (Woodley+, Harris+, Taylor+ SCABS,
|
||||
Hughes+, Dumont+, Voggel+, Mieske+ CCOS), plus Cen A X-ray, UV, planetary
|
||||
nebula and variable-star catalogues, at a 5 arcsec cone radius. The exact
|
||||
VizieR identifiers are resolved at runtime with Vizier.find_catalogs /
|
||||
get_catalog_metadata rather than being assumed correct.
|
||||
|
||||
2. BLANKET -- Vizier.query_region(..., catalog=None), i.e. every table in
|
||||
VizieR, at a 5 arcsec cone radius. Every table returning a row is reported
|
||||
with its designation, separation and any magnitude-like columns.
|
||||
|
||||
3. SIMBAD -- astroquery.simbad cone search at 10 arcsec, reporting main
|
||||
identifier, object type and separation.
|
||||
|
||||
Astrometry of the input positions is good to well under 1 arcsec; 3-5 arcsec is
|
||||
used because older ground-based Cen A cluster catalogues can be off by 1-2 arcsec.
|
||||
|
||||
Output: a plain-text report at
|
||||
C:\\Users\\lhorrocks-barlow\\Downloads\\NGC5128\\20260721\\stacked\\_gc_crossmatch.txt
|
||||
Run: python mo_gccheck.py
|
||||
"""
|
||||
|
||||
import io
|
||||
import sys
|
||||
import time
|
||||
import warnings
|
||||
import contextlib
|
||||
|
||||
import numpy as np
|
||||
import astropy.units as u
|
||||
from astropy.coordinates import SkyCoord
|
||||
from astroquery.vizier import Vizier
|
||||
|
||||
import layout
|
||||
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
OUT = layout.path("_gc_crossmatch.txt")
|
||||
|
||||
# id, RA deg, Dec deg, luminance G, r50/psf, note
|
||||
SOURCES = [
|
||||
(1, 201.672625, -43.190250, 19.18, 1.08, ""),
|
||||
(2, 201.500583, -42.816944, 19.04, 1.04, ""),
|
||||
(3, 201.441625, -42.948111, 19.42, 1.02, ""),
|
||||
(4, 201.407000, -43.041639, 18.20, 1.33, "brightest, 2.3' from nucleus"),
|
||||
(5, 201.303542, -42.950000, 19.24, 1.05, ""),
|
||||
(6, 201.256708, -42.911417, 19.06, 1.03, ""),
|
||||
]
|
||||
|
||||
# Targeted catalogues. (VizieR id, short human label)
|
||||
TARGETS = [
|
||||
("J/AJ/134/494", "Woodley+ 2007, Cen A GC catalogue (kinematics/dyn.)"),
|
||||
("J/AJ/139/1871", "Woodley+ 2010, Cen A GC ages/metallicities"),
|
||||
("J/AJ/143/84", "Harris+ 2012, new candidate GCs in NGC 5128"),
|
||||
("J/ApJ/682/199", "Woodley+ 2008, Cen A globulars with X-ray sources"),
|
||||
("J/MNRAS/469/3444", "Taylor+ 2017, SCABS II GC candidates"),
|
||||
("J/ApJ/914/16", "Hughes+ 2021, NGC 5128 GCs (PISCeS/Gaia DR2/NSC)"),
|
||||
("J/ApJ/929/147", "Dumont+ 2022, luminous GCs in NGC 5128"),
|
||||
("J/ApJ/899/140", "Voggel+ 2020, Gaia UCD candidates"),
|
||||
("J/A+A/472/111", "Mieske+ 2007, Centaurus Compact Object Survey"),
|
||||
("J/MNRAS/389/1150", "Spitler+ 2008, Spitzer photometry of globulars"),
|
||||
("J/A+A/447/71", "Voss+ 2006, Chandra X-ray point sources in Cen A"),
|
||||
("J/ApJ/560/675", "Kraft+ 2001, Chandra X-ray point sources in Cen A"),
|
||||
("J/ApJ/766/88", "Burke+ 2013, Chandra X-ray binaries in Cen A"),
|
||||
("J/MNRAS/516/2300", "Joseph+ 2022, UV sources in the Cen A nuclear region"),
|
||||
("J/A+A/574/A109", "Walsh+ 2015, planetary nebulae in NGC 5128"),
|
||||
("J/A+A/478/755", "de Jong+ 2008, variable stars in Cen A"),
|
||||
("J/A+A/657/A41", "Rejkuba+ 2022, stellar halo of NGC 5128"),
|
||||
("J/A+A/448/983", "Rejkuba+ 2006, VIJsKs in AM1339-445 / AM1343-452"),
|
||||
("J/A+A/705/A112", "Faucher+ 2026, distances of Cen A / M83 galaxies"),
|
||||
("J/AJ/133/504", "Karachentsev+ 2007, galaxies around CenA/M83"),
|
||||
]
|
||||
|
||||
RAD_TARGET = 5.0 * u.arcsec
|
||||
RAD_BLANKET = 5.0 * u.arcsec
|
||||
RAD_SIMBAD = 10.0 * u.arcsec
|
||||
|
||||
MAGISH = ("mag", "MAG", "Vmag", "Bmag", "Rmag", "Imag", "gmag", "rmag", "imag",
|
||||
"umag", "zmag", "Kmag", "Jmag", "Hmag", "Gmag", "FUV", "NUV", "flux",
|
||||
"Flux", "F(")
|
||||
|
||||
|
||||
class Tee:
|
||||
def __init__(self, *streams):
|
||||
self.streams = streams
|
||||
|
||||
def write(self, s):
|
||||
for st in self.streams:
|
||||
st.write(s)
|
||||
|
||||
def flush(self):
|
||||
for st in self.streams:
|
||||
st.flush()
|
||||
|
||||
|
||||
def sky(rec):
|
||||
return SkyCoord(rec[1] * u.deg, rec[2] * u.deg, frame="icrs")
|
||||
|
||||
|
||||
def row_coord(tab, row):
|
||||
"""Best-effort ICRS coordinate for a VizieR result row."""
|
||||
cn = tab.colnames
|
||||
for rk, dk in (("_RAJ2000", "_DEJ2000"), ("RAJ2000", "DEJ2000"),
|
||||
("RA_ICRS", "DE_ICRS"), ("_RA", "_DE"), ("RAB1950", "DEB1950")):
|
||||
if rk in cn and dk in cn:
|
||||
rv, dv = row[rk], row[dk]
|
||||
if rv is None or dv is None:
|
||||
continue
|
||||
try:
|
||||
if isinstance(rv, str) and (":" in rv or " " in rv.strip()):
|
||||
return SkyCoord(rv, dv, unit=(u.hourangle, u.deg), frame="icrs")
|
||||
fr, fd = float(rv), float(dv)
|
||||
if not (np.isfinite(fr) and np.isfinite(fd)):
|
||||
continue
|
||||
fr_ = "fk4" if rk == "RAB1950" else "icrs"
|
||||
return SkyCoord(fr * u.deg, fd * u.deg,
|
||||
frame=fr_).icrs if fr_ == "fk4" else \
|
||||
SkyCoord(fr * u.deg, fd * u.deg, frame="icrs")
|
||||
except Exception:
|
||||
continue
|
||||
return None
|
||||
|
||||
|
||||
def designation(tab, row):
|
||||
cn = tab.colnames
|
||||
prefer = ["Name", "ID", "GC", "Cluster", "Seq", "Source", "SimbadName",
|
||||
"OName", "Object", "Obj", "CXO", "PN", "UCD", "GCID", "recno",
|
||||
"HGHH", "WHH", "AAT", "f_Name", "No", "Nr"]
|
||||
parts = []
|
||||
for k in prefer:
|
||||
if k in cn and row[k] is not None:
|
||||
v = str(row[k]).strip()
|
||||
if v and v not in ("--", "nan", "None"):
|
||||
parts.append(f"{k}={v}")
|
||||
if len(parts) >= 2:
|
||||
break
|
||||
return " ".join(parts) if parts else "(no name column)"
|
||||
|
||||
|
||||
def mags(tab, row, limit=8):
|
||||
out = []
|
||||
for c in tab.colnames:
|
||||
if c.startswith("_"):
|
||||
continue
|
||||
if any(m in c for m in MAGISH):
|
||||
v = row[c]
|
||||
if v is None:
|
||||
continue
|
||||
try:
|
||||
fv = float(v)
|
||||
if not np.isfinite(fv):
|
||||
continue
|
||||
out.append(f"{c}={fv:.3f}")
|
||||
except (TypeError, ValueError):
|
||||
s = str(v).strip()
|
||||
if s and s not in ("--", "nan"):
|
||||
out.append(f"{c}={s}")
|
||||
if len(out) >= limit:
|
||||
break
|
||||
return ", ".join(out) if out else "(no magnitudes)"
|
||||
|
||||
|
||||
def classification(tab, row):
|
||||
out = []
|
||||
for c in ("Class", "Type", "otype", "OType", "Cl", "Note", "Notes", "Flag",
|
||||
"Prob", "p", "Member", "n_Name", "Com", "Comm", "Sp", "SpType"):
|
||||
if c in tab.colnames and row[c] is not None:
|
||||
v = str(row[c]).strip()
|
||||
if v and v not in ("--", "nan", "None"):
|
||||
out.append(f"{c}={v}")
|
||||
return "; ".join(out) if out else ""
|
||||
|
||||
|
||||
def verify_catalogues():
|
||||
"""Confirm which of TARGETS actually exist on VizieR."""
|
||||
print("\n" + "=" * 78)
|
||||
print("STEP 0 -- verifying targeted VizieR catalogue identifiers")
|
||||
print("=" * 78)
|
||||
live, dead = [], []
|
||||
for cid, label in TARGETS:
|
||||
try:
|
||||
with contextlib.redirect_stdout(io.StringIO()):
|
||||
meta = Vizier.get_catalog_metadata(catalog=cid)
|
||||
desc = ""
|
||||
try:
|
||||
desc = str(meta["description"][0])
|
||||
except Exception:
|
||||
desc = "(metadata returned, no description field)"
|
||||
print(f" OK {cid:20s} {desc[:78]}")
|
||||
live.append((cid, label))
|
||||
except Exception as e:
|
||||
print(f" ABSENT/FAILED {cid:20s} ({label})")
|
||||
print(f" -> {type(e).__name__}: {str(e)[:140]}")
|
||||
dead.append((cid, label, f"{type(e).__name__}: {e}"))
|
||||
return live, dead
|
||||
|
||||
|
||||
def query_targeted(live, coords):
|
||||
"""Return {src_id: [ (cid,label,tabname,sep,desig,mags,cls) ]}"""
|
||||
res = {s[0]: [] for s in SOURCES}
|
||||
print("\n" + "=" * 78)
|
||||
print(f"STEP 1 -- targeted catalogue cone search, r = {RAD_TARGET}")
|
||||
print("=" * 78)
|
||||
v = Vizier(columns=["**", "+_r"], row_limit=200)
|
||||
for cid, label in live:
|
||||
print(f"\n [{cid}] {label}")
|
||||
for rec in SOURCES:
|
||||
sid = rec[0]
|
||||
c = coords[sid]
|
||||
try:
|
||||
tl = v.query_region(c, radius=RAD_TARGET, catalog=cid)
|
||||
except Exception as e:
|
||||
print(f" src {sid}: QUERY FAILED {type(e).__name__}: {str(e)[:110]}")
|
||||
continue
|
||||
n = 0
|
||||
for tab in tl:
|
||||
tname = tab.meta.get("name", cid)
|
||||
for row in tab:
|
||||
rc = row_coord(tab, row)
|
||||
if rc is not None:
|
||||
sep = c.separation(rc).arcsec
|
||||
elif "_r" in tab.colnames and row["_r"] is not None:
|
||||
try:
|
||||
sep = float(row["_r"]) * 60.0
|
||||
except Exception:
|
||||
sep = float("nan")
|
||||
else:
|
||||
sep = float("nan")
|
||||
if np.isfinite(sep) and sep > RAD_TARGET.to_value(u.arcsec) + 0.5:
|
||||
continue
|
||||
d = designation(tab, row)
|
||||
m = mags(tab, row)
|
||||
cl = classification(tab, row)
|
||||
res[sid].append((cid, label, tname, sep, d, m, cl))
|
||||
n += 1
|
||||
print(f" src {sid}: MATCH {tname} sep={sep:.2f}\" {d}")
|
||||
print(f" mags: {m}")
|
||||
if cl:
|
||||
print(f" class: {cl}")
|
||||
if n == 0:
|
||||
print(f" src {sid}: no match")
|
||||
time.sleep(0.15)
|
||||
return res
|
||||
|
||||
|
||||
def query_blanket(coords):
|
||||
res = {s[0]: [] for s in SOURCES}
|
||||
print("\n" + "=" * 78)
|
||||
print(f"STEP 2 -- BLANKET all-VizieR cone search, r = {RAD_BLANKET}")
|
||||
print("=" * 78)
|
||||
v = Vizier(columns=["**", "+_r"], row_limit=50, timeout=600)
|
||||
for rec in SOURCES:
|
||||
sid = rec[0]
|
||||
c = coords[sid]
|
||||
print(f"\n --- source {sid} {c.to_string('hmsdms', sep=':', precision=2)} ---")
|
||||
try:
|
||||
tl = v.query_region(c, radius=RAD_BLANKET, catalog=None)
|
||||
except Exception as e:
|
||||
print(f" BLANKET QUERY FAILED: {type(e).__name__}: {str(e)[:200]}")
|
||||
continue
|
||||
if tl is None or len(tl) == 0:
|
||||
print(" no tables returned")
|
||||
continue
|
||||
print(f" {len(tl)} table(s) with entries")
|
||||
for tab in tl:
|
||||
tname = tab.meta.get("name", "?")
|
||||
best = None
|
||||
for row in tab:
|
||||
rc = row_coord(tab, row)
|
||||
sep = c.separation(rc).arcsec if rc is not None else float("nan")
|
||||
if not np.isfinite(sep) and "_r" in tab.colnames:
|
||||
try:
|
||||
sep = float(row["_r"]) * 60.0
|
||||
except Exception:
|
||||
pass
|
||||
item = (tname, sep, designation(tab, row), mags(tab, row),
|
||||
classification(tab, row))
|
||||
if best is None or (np.isfinite(sep) and
|
||||
(not np.isfinite(best[1]) or sep < best[1])):
|
||||
best = item
|
||||
res[sid].append(item)
|
||||
if best:
|
||||
print(f" {best[0]:28s} sep={best[1]:6.2f}\" {best[2]}")
|
||||
print(f" {best[3]}")
|
||||
if best[4]:
|
||||
print(f" {best[4]}")
|
||||
time.sleep(0.3)
|
||||
return res
|
||||
|
||||
|
||||
def query_simbad(coords):
|
||||
res = {s[0]: [] for s in SOURCES}
|
||||
print("\n" + "=" * 78)
|
||||
print(f"STEP 3 -- SIMBAD cone search, r = {RAD_SIMBAD}")
|
||||
print("=" * 78)
|
||||
try:
|
||||
from astroquery.simbad import Simbad
|
||||
except Exception as e:
|
||||
print(f" SIMBAD IMPORT FAILED: {e}")
|
||||
return res
|
||||
sb = Simbad()
|
||||
sb.TIMEOUT = 180
|
||||
for extra in ("otype", "otypes", "V", "B", "R", "G", "sp_type", "rvz_redshift",
|
||||
"distance"):
|
||||
try:
|
||||
sb.add_votable_fields(extra)
|
||||
except Exception:
|
||||
pass
|
||||
for rec in SOURCES:
|
||||
sid = rec[0]
|
||||
c = coords[sid]
|
||||
print(f"\n --- source {sid} ---")
|
||||
try:
|
||||
t = sb.query_region(c, radius=RAD_SIMBAD)
|
||||
except Exception as e:
|
||||
print(f" SIMBAD QUERY FAILED: {type(e).__name__}: {str(e)[:160]}")
|
||||
continue
|
||||
if t is None or len(t) == 0:
|
||||
print(" no SIMBAD object within 10\"")
|
||||
continue
|
||||
rk = "ra" if "ra" in t.colnames else "RA"
|
||||
dk = "dec" if "dec" in t.colnames else "DEC"
|
||||
for row in t:
|
||||
try:
|
||||
rc = SkyCoord(float(row[rk]) * u.deg, float(row[dk]) * u.deg)
|
||||
sep = c.separation(rc).arcsec
|
||||
except Exception:
|
||||
sep = float("nan")
|
||||
mid = str(row["main_id"] if "main_id" in t.colnames else row["MAIN_ID"])
|
||||
ot = str(row["otype"]) if "otype" in t.colnames else "?"
|
||||
ots = str(row["otypes"]) if "otypes" in t.colnames else ""
|
||||
mg = []
|
||||
for k in ("V", "B", "R", "G"):
|
||||
if k in t.colnames and row[k] is not None:
|
||||
try:
|
||||
fv = float(row[k])
|
||||
if np.isfinite(fv):
|
||||
mg.append(f"{k}={fv:.2f}")
|
||||
except Exception:
|
||||
pass
|
||||
res[sid].append((mid, ot, ots, sep, ", ".join(mg)))
|
||||
print(f" {mid:30s} otype={ot:12s} sep={sep:6.2f}\" {', '.join(mg)}")
|
||||
if ots:
|
||||
print(f" otypes: {ots}")
|
||||
time.sleep(0.3)
|
||||
return res
|
||||
|
||||
|
||||
def summarise(targ, blank, simb, dead):
|
||||
print("\n\n" + "#" * 78)
|
||||
print("# PER-SOURCE SUMMARY")
|
||||
print("#" * 78)
|
||||
for rec in SOURCES:
|
||||
sid, ra, dec, g, r50, note = rec
|
||||
c = SkyCoord(ra * u.deg, dec * u.deg)
|
||||
print("\n" + "-" * 78)
|
||||
print(f"SOURCE {sid} RA={ra:.6f} Dec={dec:+.6f} "
|
||||
f"({c.to_string('hmsdms', sep=':', precision=2)})")
|
||||
print(f" G={g:.2f} r50/psf={r50:.2f} {note}")
|
||||
t, b, s = targ[sid], blank[sid], simb[sid]
|
||||
print(f" targeted catalogue matches : {len(t)}")
|
||||
for cid, label, tname, sep, d, m, cl in t:
|
||||
print(f" * {tname} sep={sep:.2f}\"")
|
||||
print(f" {label}")
|
||||
print(f" {d}")
|
||||
print(f" {m}")
|
||||
if cl:
|
||||
print(f" {cl}")
|
||||
print(f" blanket VizieR matches : {len(b)}")
|
||||
for tname, sep, d, m, cl in b:
|
||||
print(f" * {tname} sep={sep:.2f}\" {d}")
|
||||
print(f" {m}" + (f" | {cl}" if cl else ""))
|
||||
print(f" SIMBAD objects <10\" : {len(s)}")
|
||||
for mid, ot, ots, sep, mg in s:
|
||||
print(f" * {mid} otype={ot} sep={sep:.2f}\" {mg}")
|
||||
if dead:
|
||||
print("\n" + "-" * 78)
|
||||
print("CATALOGUES THAT COULD NOT BE QUERIED (absent from VizieR or errored):")
|
||||
for cid, label, err in dead:
|
||||
print(f" {cid} ({label})")
|
||||
print(f" {err[:200]}")
|
||||
|
||||
|
||||
def main():
|
||||
coords = {rec[0]: sky(rec) for rec in SOURCES}
|
||||
buf = io.StringIO()
|
||||
real = sys.stdout
|
||||
sys.stdout = Tee(real, buf)
|
||||
try:
|
||||
print("Cen A (NGC 5128) cross-match of six unidentified detections")
|
||||
print("Generated by mo_gccheck.py at " +
|
||||
time.strftime("%Y-%m-%d %H:%M:%S"))
|
||||
print("Input positions J2000/ICRS; no Gaia DR3 (G<20.5) or SkyMapper DR2 match.")
|
||||
for rec in SOURCES:
|
||||
sid, ra, dec, g, r50, note = rec
|
||||
print(f" {sid}: {ra:.6f} {dec:+.6f} G={g:.2f} r50/psf={r50:.2f} {note}")
|
||||
live, dead = verify_catalogues()
|
||||
targ = query_targeted(live, coords)
|
||||
blank = query_blanket(coords)
|
||||
simb = query_simbad(coords)
|
||||
summarise(targ, blank, simb, dead)
|
||||
finally:
|
||||
sys.stdout = real
|
||||
with open(OUT, "w", encoding="utf-8") as fh:
|
||||
fh.write(buf.getvalue())
|
||||
print(f"\nWROTE {OUT}")
|
||||
return 0
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
raise SystemExit(main())
|
||||
Loading…
Add table
Add a link
Reference in a new issue