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:
laurence 2026-07-21 17:13:54 +01:00
parent 653ca103cd
commit c6299f41ab
53 changed files with 0 additions and 0 deletions

View file

@ -1,156 +0,0 @@
"""
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()