astrophotography/session-scripts/mo_finalise.py
laurence 5286a2e81b Processing and analysis code for remote-telescope imaging sessions
The scripts that processed the NGC 5128 session of 2026-07-21 previously
lived inside the data directory and addressed it with absolute paths.
Code and data are now separated: the code lives here, and a session is
located at runtime through the ASTRO_SESSION environment variable.

layout.py is what makes that work. It maps a FILENAME to the
subdirectory that file belongs in, using the same rules the session
directories are organised with, so a script can go on asking for
'master-Red.fit' or '_stars.npz' without any call site knowing the
directory structure. Anything unrecognised resolves to the session root,
which is visible and correctable rather than silently wrong.

restructure.py reorganises a flat session directory into that layout. It
is idempotent and dry-run by default.

The 50 session scripts are kept as they were run rather than tidied into
a library. They were written in sequence as the work went along, several
of them by parallel agents, and they show it - but they are the honest
provenance of a published set of results, and the productionised pipeline
should be able to reproduce those results exactly.

Verified before committing: all 51 files compile without warnings, and
verify_core.py, closeup.py and triptych.py were run end to end against
the reorganised session, correctly finding inputs across calibrated/,
stacks/masters/ and final/ and writing outputs back to the right places.
2026-07-21 15:29:49 +01:00

146 lines
6.6 KiB
Python

"""Write the final flagged-object CSVs, with an identification and verdict
attached to every row.
Two files are produced.
mo-flagged-objects.csv is the short list: the six sources that survived every
cut of the transient search, plus the (zero) moving-object candidates, each
with the identification established from published catalogues and an explicit
assessment. This is the file to read.
mo-transient-candidates.csv (written by mo_transient.py) is the long list of
all 567 catalogue non-matches, kept for completeness. Almost all of them are
faint objects below the depth at which Gaia DR3 and SkyMapper DR2 are complete
against this galaxy, and are not individually assessed.
"""
import csv
import os
import numpy as np
import mo_common as C
# Identifications from cross-matching against published Centaurus A cluster
# catalogues (Woodley+2007 J/AJ/134/494 and others), SIMBAD, NED and a blanket
# VizieR cone search. Separations quoted are to the matched catalogue entry.
IDENT = {
"201.672625": dict(
name="WHH-31", otype="GlC (SIMBAD)", sep=0.10,
refs="Woodley+2007; Hughes+2021 #86; SCABS GC prob=1.0",
cat_phot="V=19.50 B=20.44 R=18.94",
assessment="Known Centaurus A globular cluster. Not a transient."),
"201.500583": dict(
name="pff_gc-081", otype="GlC (SIMBAD)", sep=0.32,
refs="Woodley+2007; Hughes+2021 ID 248669; SCABS GC prob=1.0",
cat_phot="V=19.34 B=20.17 R=18.80",
assessment="Known Centaurus A globular cluster. Not a transient."),
"201.441625": dict(
name="WHH-24", otype="GlC (SIMBAD)", sep=0.19,
refs="Woodley+2007; SCABS GC prob=1.0",
cat_phot="V=19.63 B=20.43 R=19.12",
assessment="Known Centaurus A globular cluster. Not a transient."),
"201.407000": dict(
name="resolved object at the position of CAVS 210 / "
"Gaia DR3 6088701636326983168",
otype="resolved (1.34x PSF, elongation 1.40); not a point source",
sep=1.32,
refs="de Jong+2008 J/A+A/478/755 (CAVS 210); DENIS 1996-2001; "
"Swift/UVOT SSC (12 ObsIDs); XMM-OM ~2020; VHS DR4; KS4 DR1",
cat_phot="Gaia DR3 G=21.03 at 1.32\" (point source); "
"DENIS I=18.23 J=16.44; UVOT U=18.2-18.6",
assessment="NOT a transient and NOT a brightening; the 2.8 mag "
"difference from Gaia is a measurement artefact. The "
"source is resolved (r50 = 2.96 px vs PSF 2.21 px, "
"elongation 1.40), so an integrated aperture magnitude is "
"not comparable with Gaia's point-source G; Gaia has no "
"entry at all for the extended object and nothing within "
"25\" brighter than G=19.8. It is steady over the session "
"(rms 0.066 mag vs 0.039 for comparison stars) and is "
"present on 1990s DSS2 red plates at the same brightness "
"relative to its neighbours (implied change +0.26 mag). "
"Even the PSF-scaled 2 px core gives G=18.63. See "
"notes-transient-search.md section 4.3 and NGC5128-mo-cavs210.png."),
"201.303542": dict(
name="WHH-10", otype="GlC (SIMBAD)", sep=0.23,
refs="Woodley+2007; SCABS GC prob=1.0",
cat_phot="V=19.57 B=20.36 R=19.07",
assessment="Known Centaurus A globular cluster. Not a transient."),
"201.256708": dict(
name="pff_gc-029 / GC0103", otype="GlC (SIMBAD)", sep=0.23,
refs="Woodley+2007; Peng+2004; Spitler+2008; Hughes+2021; Dumont+2022",
cat_phot="V=19.37 B=19.75 R=19.06 Ks=14.69",
assessment="Known Centaurus A globular cluster, well studied. "
"Not a transient."),
}
def ident_for(ra):
for k, v in IDENT.items():
if abs(float(k) - ra) < 1e-4:
return v
return dict(name="", otype="", sep=np.nan, refs="", cat_phot="",
assessment="UNIDENTIFIED - needs follow-up")
FIELDS = ["search", "ra_hms", "dec_dms", "ra_deg", "dec_deg", "x_ref",
"y_ref", "g_mag_this_image", "snr", "r50_over_psf", "n_subs",
"snr_R", "snr_G", "snr_B", "dist_from_cenA_arcmin",
"identification", "object_type", "match_sep_arcsec",
"catalogue_photometry", "references", "assessment"]
def main():
rows = []
vet = list(np.load(os.path.join(C.OUT, "_mo_vetted.npy"),
allow_pickle=True))
vet.sort(key=lambda r: r["dist_cenA_arcmin"])
for r in vet:
d = ident_for(r["ra_deg"])
rows.append({
"search": "transient",
"ra_hms": r["ra_hms"], "dec_dms": r["dec_dms"],
"ra_deg": r["ra_deg"], "dec_deg": r["dec_deg"],
"x_ref": r["x"], "y_ref": r["y"],
"g_mag_this_image": r["g_mag"], "snr": r["snr"],
"r50_over_psf": r["r50_over_psf"], "n_subs": r["n_subs"],
"snr_R": r["snr_R"], "snr_G": r["snr_G"], "snr_B": r["snr_B"],
"dist_from_cenA_arcmin": r["dist_cenA_arcmin"],
"identification": d["name"], "object_type": d["otype"],
"match_sep_arcsec": d["sep"],
"catalogue_photometry": d["cat_phot"],
"references": d["refs"], "assessment": d["assessment"]})
# Moving-object search: zero candidates, but the file should say so
# explicitly rather than being empty.
mo = os.path.join(C.OUT, "mo-moving-candidates.csv")
nmov = 0
if os.path.exists(mo):
with open(mo) as fh:
nmov = max(0, sum(1 for _ in fh) - 1)
if nmov == 0:
rows.append({f: "" for f in FIELDS} | {
"search": "moving-object",
"identification": "(no candidates)",
"assessment": "Blind tracklet search over the 12 luminance subs "
"returned 0 candidates. Sensitivity measured by "
"synthetic injection: 50% recovery at G=18.9 for "
"8-40 arcsec/hr; no sensitivity below ~4 arcsec/hr. "
"The only catalogued minor planet within 30' of the "
"pointing, (427494) 2002 BK26 at V=21.7, fell 2.4' "
"outside the frame."})
out = os.path.join(C.OUT, "mo-flagged-objects.csv")
with open(out, "w", newline="", encoding="utf-8") as fh:
w = csv.DictWriter(fh, fieldnames=FIELDS)
w.writeheader()
w.writerows(rows)
print(f"wrote {out} ({len(rows)} rows)")
for r in rows:
print(f" {r['search']:14s} {r['identification'][:44]:44s} "
f"{str(r['g_mag_this_image']):>6s} "
f"{r['assessment'][:60]}")
if __name__ == "__main__":
main()