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:
laurence 2026-07-21 15:29:49 +01:00
commit 5286a2e81b
53 changed files with 8820 additions and 0 deletions

View file

@ -0,0 +1,70 @@
"""Pass 1: measure every calibrated frame and cache its star list.
Frames are 4788x3194 float32 (61 MB each) and the machine has little free RAM,
so each frame is opened, measured and released one at a time. The star lists are
cached to an npz because both the registration pass and the plate solve need
them, and re-detecting costs more than re-reading a small array.
Recorded per frame: sky background and its rms, the number of detections, and a
median FWHM derived from sep's half-flux radius. The FWHM is the seeing metric
used later to pick the registration reference and to weight the stack.
"""
import glob
import os
import re
import numpy as np
import sep
from astropy.io import fits
import layout
SRC = layout.SESSION
CACHE = layout.path("_stars.npz")
os.makedirs(os.path.dirname(CACHE), exist_ok=True)
NAME_RE = re.compile(r"-(Luminance|Red|Green|Blue)-BIN2-W-300-(\d+)\.fit$")
def frame_list():
out = []
for path in sorted(glob.glob(layout.path("calibrated-*.fit"))):
m = NAME_RE.search(path)
if m:
out.append((path, m.group(1), int(m.group(2))))
return out
if __name__ == "__main__":
frames = frame_list()
print(f"{len(frames)} calibrated frames")
store, meta = {}, []
for path, filt, idx in frames:
with fits.open(path, memmap=False) as hd:
data = hd[0].data.astype(np.float32)
bkg = sep.Background(data, bw=64, bh=64, fw=3, fh=3)
back_med = float(np.median(bkg.back()))
rms = float(bkg.globalrms)
sub = data - bkg.back()
objs = sep.extract(sub, 5.0, err=rms, minarea=9, deblend_cont=0.005)
objs = objs[(objs["flag"] == 0) & (objs["npix"] > 12) &
(objs["npix"] < 2000)]
objs = objs[np.argsort(objs["flux"])[::-1][:400]]
rad, _ = sep.flux_radius(sub, objs["x"], objs["y"], 6.0 * objs["a"],
0.5, normflux=objs["flux"])
fwhm = float(np.median(rad) * 2.0)
key = f"{filt}_{idx:03d}"
store[key + "_xy"] = np.column_stack([objs["x"], objs["y"]])
store[key + "_flux"] = objs["flux"]
meta.append((key, os.path.basename(path), filt, idx, len(objs), fwhm,
back_med, rms))
print(f"{key:16s} stars={len(objs):4d} fwhm={fwhm:5.2f}px "
f"bg={back_med:8.1f} rms={rms:6.1f}")
del data, sub, bkg, objs
np.savez_compressed(
CACHE,
meta=np.array(meta, dtype=object),
**store,
)
print("cached ->", CACHE)