""" 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()