"""How deep did the luminance master actually go? Calibrates instrumental magnitudes against Gaia G, then reports the faintest star still detected at 5 sigma. That number decides which follow-up analyses are worth attempting on this data and which are wishful thinking. """ import os import numpy as np import sep from astropy import units as u from astropy.coordinates import SkyCoord from astropy.io import fits from astropy.wcs import WCS import layout OUT = layout.SESSION CACHE = layout.path("_gaia_deep.npz") with fits.open(layout.path("master-Luminance.fit")) as hd: img = hd[0].data.astype(np.float32) hdr = hd[0].header wcs = WCS(hdr, naxis=2) ny, nx = img.shape bkg = sep.Background(img, bw=64, bh=64, fw=3, fh=3) sub = img - bkg.back() objs = sep.extract(sub, 3.0, err=bkg.globalrms, minarea=6, deblend_cont=0.005) objs = objs[(objs["flag"] == 0) & (objs["npix"] > 6)] flux, fluxerr, _ = sep.sum_circle(sub, objs["x"], objs["y"], 5.0, err=bkg.globalrms, subpix=5) snr = flux / np.maximum(fluxerr, 1e-9) keep = (flux > 0) & (snr > 3) objs, flux, snr = objs[keep], flux[keep], snr[keep] print(f"{len(objs)} sources detected at SNR > 3 (rms {bkg.globalrms:.2f} ADU)") sky = wcs.pixel_to_world(objs["x"], objs["y"]) centre = wcs.pixel_to_world(nx / 2, ny / 2) if os.path.exists(CACHE): z = np.load(CACHE) gra, gdec, gmag = z["ra"], z["dec"], z["g"] else: from astroquery.gaia import Gaia Gaia.ROW_LIMIT = 60000 job = Gaia.launch_job_async(f""" SELECT ra, dec, phot_g_mean_mag FROM gaiadr3.gaia_source WHERE 1 = CONTAINS(POINT('ICRS', ra, dec), CIRCLE('ICRS', {centre.ra.deg}, {centre.dec.deg}, 0.42)) AND phot_g_mean_mag IS NOT NULL AND phot_g_mean_mag < 20.5 """) t = job.get_results() gra = np.asarray(t["ra"], float) gdec = np.asarray(t["dec"], float) gmag = np.asarray(t["phot_g_mean_mag"], float) np.savez_compressed(CACHE, ra=gra, dec=gdec, g=gmag) print(f"{len(gmag)} Gaia sources in the field, G down to {gmag.max():.2f}") gcoord = SkyCoord(gra * u.deg, gdec * u.deg) idx, sep2d, _ = sky.match_to_catalog_sky(gcoord) matched = sep2d.arcsec < 1.5 print(f"{matched.sum()} detections matched to Gaia within 1.5\"") inst = -2.5 * np.log10(flux[matched]) gm = gmag[idx[matched]] # Fit the zero point on well exposed, unsaturated stars only. fit = (gm > 12) & (gm < 17) & (snr[matched] > 20) zp = float(np.median(gm[fit] - inst[fit])) scatter = float(np.std(gm[fit] - inst[fit] - 0.0)) print(f"zero point {zp:.3f} (G = inst + zp) from {fit.sum()} stars, " f"scatter {scatter:.3f} mag") mag_all = -2.5 * np.log10(flux) + zp # SNR falls monotonically with magnitude, so read the SNR=5 crossing off a # running median rather than requiring sources to land in a narrow SNR bin. order = np.argsort(mag_all) ms, ss = mag_all[order], snr[order] win = max(11, len(ms) // 60) run_m = np.array([np.median(ms[i:i + win]) for i in range(0, len(ms) - win, win // 2)]) run_s = np.array([np.median(ss[i:i + win]) for i in range(0, len(ss) - win, win // 2)]) below = np.where(run_s < 5.0)[0] lim5 = float(run_m[below[0]]) if len(below) else float(run_m[-1]) print(f"limiting magnitude at SNR 5: G ~ {lim5:.2f} " f"(SNR range {snr.min():.1f}-{snr.max():.0f})") print(f"faintest detection: G ~ {mag_all.max():.2f} (SNR " f"{snr[np.argmax(mag_all)]:.1f})") unmatched = ~matched print(f"{unmatched.sum()} detections with NO Gaia counterpart " f"({100 * unmatched.mean():.1f}% of sources)") r_gal = np.hypot(objs["x"] - nx / 2, objs["y"] - ny / 2) * 0.5376 / 60.0 near = unmatched & (r_gal < 12.0) & (mag_all > 18.0) & (mag_all < 22.0) print(f" of those, {near.sum()} lie within 12' of the galaxy at " f"G 18-22: the magnitude and radius range of Centaurus A's " f"globular cluster system") # Surface brightness of the sky, a fair summary of how much the moon cost. pixarea = 0.5376 ** 2 sky_adu = float(np.median(bkg.back())) print(f"sky background {sky_adu:.1f} ADU/px -> " f"{zp - 2.5 * np.log10(max(sky_adu, 1e-6) / pixarea):.2f} mag/arcsec^2")