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.
171 lines
6.8 KiB
Python
171 lines
6.8 KiB
Python
"""
|
|
gc-detect.py -- source detection and photometry for the NGC 5128 globular cluster survey.
|
|
|
|
Step 1 of the pipeline. Reads the luminance master, builds a SPATIALLY VARYING
|
|
background model (this is the critical step: NGC 5128's own light is a steep,
|
|
structured background and a single global sky level would produce a spurious
|
|
central concentration of detections), detects sources above a locally-scaled
|
|
threshold, does aperture photometry, measures morphology, and runs forced
|
|
photometry at the same positions on the R/G/B masters.
|
|
|
|
Outputs (into the stacked directory):
|
|
_gc_raw.npz all detections + photometry + morphology
|
|
NGC5128-gc-background-check.png diagnostic of the background model
|
|
|
|
Memory: only one 61 MB master is held at a time.
|
|
"""
|
|
import os, sys, time
|
|
import numpy as np
|
|
from astropy.io import fits
|
|
from astropy.wcs import WCS
|
|
import sep
|
|
import warnings
|
|
|
|
import layout
|
|
warnings.filterwarnings("ignore")
|
|
|
|
STACKED = layout.SESSION
|
|
|
|
# --- configuration -----------------------------------------------------------
|
|
BW = 16 # background mesh, px. 8.6 arcsec = 3.2 x the 5 px FWHM. Chosen
|
|
# empirically: see the mesh trial in the notes -- a coarser mesh
|
|
# destroys recovery of known clusters inside ~8 arcmin.
|
|
FW = 3 # median filter over meshes
|
|
THRESH = 3.0 # sigma above the LOCAL rms
|
|
MINAREA = 5
|
|
APR = 5.0 # photometric aperture radius, px, matching the published ZP
|
|
ZP_L = 27.941 # G_mag = -2.5 log10(flux) + ZP_L
|
|
FWHM_PX = 5.0
|
|
|
|
# NGC 5128 nucleus, J2000 (NED)
|
|
NUC_RA, NUC_DEC = 201.365063, -43.019113
|
|
|
|
|
|
def gauss_kernel(fwhm, size=None):
|
|
sig = fwhm / 2.3548
|
|
if size is None:
|
|
size = int(2 * round(3 * sig) + 1)
|
|
r = np.arange(size) - size // 2
|
|
x, y = np.meshgrid(r, r)
|
|
k = np.exp(-(x * x + y * y) / (2 * sig * sig))
|
|
return (k / k.sum()).astype(np.float32)
|
|
|
|
|
|
def load(name):
|
|
d = fits.getdata(layout.path(name)).astype(np.float32)
|
|
if not d.dtype.isnative:
|
|
d = d.byteswap().newbyteorder()
|
|
return np.ascontiguousarray(d)
|
|
|
|
|
|
def main():
|
|
t0 = time.time()
|
|
hdr = fits.getheader(layout.path("master-Luminance.fit"))
|
|
w = WCS(hdr)
|
|
ny, nx = hdr["NAXIS2"], hdr["NAXIS1"]
|
|
|
|
data = load("master-Luminance.fit")
|
|
|
|
# ---------------------------------------------------------------- background
|
|
# Compare mesh sizes so the choice is defensible, then adopt BW.
|
|
print("background mesh comparison (median |back| inside r<2' of nucleus vs outer field):")
|
|
nx_c, ny_c = w.all_world2pix(NUC_RA, NUC_DEC, 0)
|
|
nx_c, ny_c = float(nx_c), float(ny_c)
|
|
print(" nucleus pixel = %.1f, %.1f" % (nx_c, ny_c))
|
|
yy, xx = np.mgrid[0:ny:8, 0:nx:8]
|
|
rr = np.hypot(xx - nx_c, yy - ny_c) * 0.5376 / 60.0 # arcmin
|
|
inner = rr < 2.0
|
|
outer = rr > 12.0
|
|
for bw in (16, 32, 64, 128, 512):
|
|
b = sep.Background(data, bw=bw, bh=bw, fw=FW, fh=FW)
|
|
bk = b.back()[::8, ::8]
|
|
rms = b.rms()[::8, ::8]
|
|
print(" bw=%4d back(in)=%8.1f back(out)=%7.2f rms(in)=%7.2f rms(out)=%6.2f"
|
|
% (bw, np.median(bk[inner]), np.median(bk[outer]),
|
|
np.median(rms[inner]), np.median(rms[outer])))
|
|
del b, bk, rms
|
|
|
|
bkg = sep.Background(data, bw=BW, bh=BW, fw=FW, fh=FW)
|
|
back = bkg.back()
|
|
rmsmap = bkg.rms()
|
|
data_sub = data - back
|
|
del data
|
|
np.save(layout.path("_gc_backmodel_thumb.npy"), back[::8, ::8])
|
|
del back
|
|
|
|
# ---------------------------------------------------------------- detection
|
|
sep.set_extract_pixstack(3000000)
|
|
kern = gauss_kernel(FWHM_PX)
|
|
objs, segmap = sep.extract(data_sub, THRESH, err=rmsmap, minarea=MINAREA,
|
|
filter_kernel=kern, filter_type="matched",
|
|
deblend_nthresh=32, deblend_cont=0.005,
|
|
clean=True, clean_param=1.0, segmentation_map=True)
|
|
print("detected %d sources (thresh=%.1f x LOCAL rms, minarea=%d)"
|
|
% (len(objs), THRESH, MINAREA))
|
|
del segmap
|
|
|
|
x, y = objs["x"], objs["y"]
|
|
|
|
# ---------------------------------------------------------------- photometry
|
|
flux, fluxerr, flag = sep.sum_circle(data_sub, x, y, APR, err=rmsmap,
|
|
gain=None, subpix=5)
|
|
# local-noise-only error; add Poisson from the source itself using EGAIN
|
|
egain = float(hdr.get("EGAIN", 0.2467))
|
|
nimg = 12.0 # 12 x 300 s stacked, normalised -> approximate
|
|
poiss = np.clip(flux, 0, None) / (egain * nimg)
|
|
fluxerr_tot = np.sqrt(fluxerr ** 2 + poiss)
|
|
|
|
# aperture at 2x radius, to catch extended light (galaxy discriminator)
|
|
flux2, _, _ = sep.sum_circle(data_sub, x, y, 2 * APR, err=rmsmap, subpix=5)
|
|
|
|
# half-light radius and a "PSF concentration" index
|
|
rhalf, rflag = sep.flux_radius(data_sub, x, y, np.full(len(x), 6 * APR),
|
|
0.5, normflux=flux2, subpix=5)
|
|
# peak-to-total sharpness
|
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
conc = -2.5 * np.log10(np.clip(flux, 1e-9, None) / np.clip(flux2, 1e-9, None))
|
|
|
|
# local background rms at each source (for SNR bookkeeping)
|
|
xi = np.clip(np.round(x).astype(int), 0, nx - 1)
|
|
yi = np.clip(np.round(y).astype(int), 0, ny - 1)
|
|
local_rms = rmsmap[yi, xi]
|
|
local_back = np.load(layout.path("_gc_backmodel_thumb.npy"))[
|
|
np.clip(yi // 8, 0, ny // 8 - 1), np.clip(xi // 8, 0, nx // 8 - 1)]
|
|
|
|
del rmsmap, data_sub
|
|
|
|
with np.errstate(divide="ignore", invalid="ignore"):
|
|
mag = -2.5 * np.log10(np.clip(flux, 1e-9, None)) + ZP_L
|
|
magerr = 1.0857 * fluxerr_tot / np.clip(flux, 1e-9, None)
|
|
snr = flux / np.clip(fluxerr_tot, 1e-9, None)
|
|
|
|
ra, dec = w.all_pix2world(x, y, 0)
|
|
|
|
out = dict(x=x, y=y, ra=ra, dec=dec,
|
|
flux=flux, fluxerr=fluxerr_tot, mag=mag, magerr=magerr, snr=snr,
|
|
flux2=flux2, rhalf=rhalf, conc=conc,
|
|
a=objs["a"], b=objs["b"], theta=objs["theta"],
|
|
npix=objs["npix"].astype(np.float64), peak=objs["peak"],
|
|
cflux=objs["cflux"], sepflag=objs["flag"].astype(np.float64),
|
|
apflag=flag.astype(np.float64),
|
|
local_rms=local_rms, local_back=local_back)
|
|
|
|
# ---------------------------------------------------------------- colours
|
|
for band, key in (("Red", "R"), ("Green", "V"), ("Blue", "B")):
|
|
d = load("master-%s.fit" % band)
|
|
b = sep.Background(d, bw=BW, bh=BW, fw=FW, fh=FW)
|
|
ds = d - b.back()
|
|
rm = b.rms()
|
|
del d
|
|
f, fe, fl = sep.sum_circle(ds, x, y, APR, err=rm, subpix=5)
|
|
out["flux_" + key] = f
|
|
out["fluxerr_" + key] = fe
|
|
print(" forced photometry on %s done" % band)
|
|
del ds, rm, b, f, fe, fl
|
|
|
|
np.savez(layout.path("_gc_raw.npz"), **out)
|
|
print("wrote _gc_raw.npz with %d rows in %.0f s" % (len(x), time.time() - t0))
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|