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.
156 lines
6.8 KiB
Python
156 lines
6.8 KiB
Python
"""
|
|
gc-validate.py -- step 5. External validation against published catalogues.
|
|
|
|
Two independent truth sets fall inside the field:
|
|
* SIMBAD, object type GlC -- confirmed/catalogued Cen A clusters
|
|
* SCABS (Taylor et al. 2017, MNRAS 469, 3444) -- deep DECam GC candidates,
|
|
with V magnitudes, so it can be binned in brightness
|
|
|
|
Neither is complete, especially in the inner few arcmin where the galaxy swamps
|
|
even professional data, so a candidate that matches nothing is UNCONFIRMED, not
|
|
false. Produces NGC5128-gc-recovery.png and prints the numbers used in the notes.
|
|
"""
|
|
import os, numpy as np, warnings
|
|
import matplotlib
|
|
matplotlib.use("Agg")
|
|
import matplotlib.pyplot as plt
|
|
from astropy.io import fits
|
|
from astropy.wcs import WCS
|
|
from astropy.coordinates import SkyCoord
|
|
|
|
import layout
|
|
warnings.filterwarnings("ignore")
|
|
|
|
S = layout.SESSION
|
|
PIXSCALE = 0.5376
|
|
NUC_RA, NUC_DEC = 201.365063, -43.019113
|
|
C_CAND, C_EXT, C_KNOWN, C_ACC = "#2563eb", "#e8710a", "#127a5a", "#8b5cf6"
|
|
C_INK, C_MUTED, C_GRID = "#1a1a1a", "#5c5c5c", "#d8d8d4"
|
|
SURF = "#fcfcfb"
|
|
plt.rcParams.update({
|
|
"figure.facecolor": SURF, "axes.facecolor": SURF, "axes.edgecolor": C_MUTED,
|
|
"text.color": C_INK, "xtick.color": C_MUTED, "ytick.color": C_MUTED,
|
|
"axes.grid": True, "grid.color": C_GRID, "grid.linewidth": 0.6,
|
|
"axes.axisbelow": True, "font.size": 10, "axes.spines.top": False,
|
|
"axes.spines.right": False, "legend.frameon": False, "axes.labelcolor": C_INK})
|
|
|
|
|
|
def main():
|
|
d = dict(np.load(layout.path("_gc_cat.npz"), allow_pickle=True))
|
|
hdr = fits.getheader(layout.path("master-Luminance.fit"))
|
|
w = WCS(hdr)
|
|
cx, cy = [float(v) for v in w.all_world2pix(NUC_RA, NUC_DEC, 0)]
|
|
cat = SkyCoord(d["ra"], d["dec"], unit="deg")
|
|
nuc = SkyCoord(NUC_RA, NUC_DEC, unit="deg")
|
|
|
|
# ---- reference sets restricted to the frame -----------------------------
|
|
def infield(ra, dec):
|
|
x, y = w.all_world2pix(ra, dec, 0)
|
|
return (x > 30) & (x < 4758) & (y > 30) & (y < 3164), x, y
|
|
|
|
z = np.load(layout.path("_simbad.npz"), allow_pickle=True)
|
|
m = z["otype"].astype(str) == "GlC"
|
|
sra, sdec = z["ra"][m].astype(float), z["dec"][m].astype(float)
|
|
inf, _, _ = infield(sra, sdec)
|
|
sim = SkyCoord(sra[inf], sdec[inf], unit="deg")
|
|
|
|
r = np.load(layout.path("_cena_gc_ref.npz"))
|
|
infr, _, _ = infield(r["ra"], r["dec"])
|
|
good_p = infr & (r["prob"] >= 0.9) & np.isfinite(r["vmag"])
|
|
sca = SkyCoord(r["ra"][good_p], r["dec"][good_p], unit="deg")
|
|
scav = r["vmag"][good_p]
|
|
print("in-field reference objects: SIMBAD GlC %d ; SCABS p>=0.9 with V %d"
|
|
% (len(sim), len(sca)))
|
|
|
|
cand = d["cand"]
|
|
ccand = cat[cand]
|
|
call = cat[d["base"]]
|
|
|
|
def recov(ref):
|
|
i, s, _ = ref.match_to_catalog_sky(ccand)
|
|
j, s2, _ = ref.match_to_catalog_sky(call)
|
|
return s.arcsec < 2.0, s2.arcsec < 2.0
|
|
|
|
hit_sim, det_sim = recov(sim)
|
|
hit_sca, det_sca = recov(sca)
|
|
print("SIMBAD GlC: detected at all %d/%d (%.0f%%), kept as candidate %d/%d (%.0f%%)"
|
|
% (det_sim.sum(), len(sim), 100 * det_sim.mean(),
|
|
hit_sim.sum(), len(sim), 100 * hit_sim.mean()))
|
|
print("SCABS p>=0.9: detected %d/%d (%.0f%%), kept %d/%d (%.0f%%)"
|
|
% (det_sca.sum(), len(sca), 100 * det_sca.mean(),
|
|
hit_sca.sum(), len(sca), 100 * hit_sca.mean()))
|
|
|
|
# ---- purity -------------------------------------------------------------
|
|
ci, cs, _ = ccand.match_to_catalog_sky(sim)
|
|
matched_sim = cs.arcsec < 2.0
|
|
ci2, cs2, _ = ccand.match_to_catalog_sky(
|
|
SkyCoord(r["ra"][infr], r["dec"][infr], unit="deg"))
|
|
matched_sca = cs2.arcsec < 2.0
|
|
any_match = matched_sim | matched_sca
|
|
print("\nPURITY: %d candidates; %d (%.0f%%) match SIMBAD GlC or SCABS; "
|
|
"%d (%.0f%%) unconfirmed"
|
|
% (cand.sum(), any_match.sum(), 100 * any_match.mean(),
|
|
(~any_match).sum(), 100 * (~any_match).mean()))
|
|
st = d["simbad_type"][cand]
|
|
bad = np.isin(st, ["*", "PM*", "V*", "RR*", "EB*", "LP*"])
|
|
print(" candidates matching a SIMBAD STAR-type object: %d (%.1f%%)"
|
|
% (bad.sum(), 100 * bad.mean()))
|
|
print(" matching a SIMBAD galaxy: %d ; Cepheid: %d ; X-ray/LXB: %d"
|
|
% ((st == "G").sum(), (st == "Ce*").sum(), np.isin(st, ["X", "LXB"]).sum()))
|
|
rcand = d["r_arcmin"][cand]
|
|
print(" unconfirmed fraction inside 8': %.0f%% ; outside 8': %.0f%%"
|
|
% (100 * (~any_match)[rcand < 8].mean(), 100 * (~any_match)[rcand >= 8].mean()))
|
|
|
|
# ---- figure -------------------------------------------------------------
|
|
fig, ax = plt.subplots(1, 2, figsize=(12.6, 5.0))
|
|
a = ax[0]
|
|
vb = np.arange(17.0, 22.6, 0.5)
|
|
vc = 0.5 * (vb[1:] + vb[:-1])
|
|
fd, fk, nn = [], [], []
|
|
for j in range(len(vb) - 1):
|
|
s = (scav >= vb[j]) & (scav < vb[j + 1])
|
|
nn.append(s.sum())
|
|
fd.append(det_sca[s].mean() if s.sum() > 4 else np.nan)
|
|
fk.append(hit_sca[s].mean() if s.sum() > 4 else np.nan)
|
|
a.plot(vc, fd, lw=2.2, marker="o", ms=7, color=C_KNOWN,
|
|
label="detected by our pipeline")
|
|
a.plot(vc, fk, lw=2.2, marker="s", ms=7, color=C_CAND,
|
|
label="detected AND kept as a candidate")
|
|
for x, y, n in zip(vc, fd, nn):
|
|
if np.isfinite(y) and n > 4:
|
|
a.annotate("%d" % n, (x, y), xytext=(0, 8), textcoords="offset points",
|
|
ha="center", fontsize=7.5, color=C_MUTED)
|
|
a.set_xlabel("SCABS V magnitude"); a.set_ylabel("fraction recovered")
|
|
a.set_ylim(0, 1.05)
|
|
a.set_title("a) recovery of published clusters vs brightness", fontsize=10.5, loc="left")
|
|
a.legend(fontsize=9, labelcolor=C_INK)
|
|
a.text(0.98, 0.9, "numbers = reference objects per bin", transform=a.transAxes,
|
|
ha="right", fontsize=8, color=C_MUTED)
|
|
|
|
b = ax[1]
|
|
rsca = sca.separation(nuc).arcmin
|
|
rb = np.array([0, 2, 4, 6, 9, 12, 16, 21, 27])
|
|
rc = 0.5 * (rb[1:] + rb[:-1])
|
|
for arr, c, lab, mk in ((det_sca, C_KNOWN, "detected", "o"),
|
|
(hit_sca, C_CAND, "kept as candidate", "s")):
|
|
f, nnr = [], []
|
|
for j in range(len(rb) - 1):
|
|
s = (rsca >= rb[j]) & (rsca < rb[j + 1])
|
|
nnr.append(s.sum())
|
|
f.append(arr[s].mean() if s.sum() > 4 else np.nan)
|
|
b.plot(rc, f, lw=2.2, marker=mk, ms=7, color=c, label=lab)
|
|
b.set_xlabel("projected radius (arcmin)"); b.set_ylabel("fraction recovered")
|
|
b.set_ylim(0, 1.05)
|
|
b.set_title("b) the same, vs radius (SCABS p$\\geq$0.9)", fontsize=10.5, loc="left")
|
|
b.legend(fontsize=9, labelcolor=C_INK)
|
|
fig.suptitle("External validation against SCABS (Taylor et al. 2017) and SIMBAD",
|
|
fontsize=12.5, x=0.008, ha="left")
|
|
fig.tight_layout()
|
|
fig.savefig(layout.path("NGC5128-gc-recovery.png"), dpi=115, bbox_inches="tight",
|
|
facecolor=SURF)
|
|
plt.close(fig)
|
|
print("wrote NGC5128-gc-recovery.png")
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|