Processing and analysis code for remote-telescope imaging sessions
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.
This commit is contained in:
commit
5286a2e81b
53 changed files with 8820 additions and 0 deletions
102
session-scripts/depth.py
Normal file
102
session-scripts/depth.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
"""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")
|
||||
Loading…
Add table
Add a link
Reference in a new issue