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,85 @@
"""Diagnostic: how good is the coordinate registration, and how many
detections survive removal of the static sky?
astroalign only reports a few dozen matched stars, which is enough to define a
similarity transform but says nothing about how well it holds across a
4788 x 3194 field. This refines each frame's transform by nearest-neighbour
matching every detection against the reference frame's detections and refitting
a full affine, then quotes the residual. It also counts, per frame, how many
detections are left once everything coincident with a master-stack source is
removed - that residual population is the input to the tracklet search.
"""
import os
import numpy as np
from astropy.io import fits
from scipy.spatial import cKDTree
from skimage.transform import AffineTransform
import layout
SRC = layout.SESSION
OUT = layout.SESSION
CACHE = layout.path("_mo_dets.npz")
def load():
z = np.load(CACHE, allow_pickle=True)
meta = {r[0]: r for r in z["meta"]}
dets = {k: z[k] for k in meta}
return meta, dets
def refine(dets, ref_key="Luminance_002", tol=3.0):
"""Refit each frame's transform on all cross-matched detections."""
ref = dets[ref_key]
tree = cKDTree(ref[:, :2])
out = {}
for key, d in dets.items():
src = d[:, 2:4].astype(float) # native coords
cur = d[:, :2].astype(float) # astroalign-transformed
for t in (tol, 1.5):
dist, idx = tree.query(cur, distance_upper_bound=t)
ok = np.isfinite(dist)
if ok.sum() < 50:
break
tf = AffineTransform()
tf.estimate(src[ok], ref[idx[ok], :2].astype(float))
cur = tf(src)
dist, idx = tree.query(cur, distance_upper_bound=1.5)
ok = np.isfinite(dist)
out[key] = (cur, np.median(dist[ok]), np.percentile(dist[ok], 90),
ok.sum())
return out
def main():
meta, dets = load()
lum = [k for k in dets if k.startswith("Luminance")]
ref = refine(dets)
print("registration residual against reference frame (px):")
for k in sorted(dets):
_, med, p90, n = ref[k]
print(f" {k:16s} n={n:5d} median={med:.3f} p90={p90:.3f}")
# Static sky = every source in the deep luminance master.
with fits.open(layout.path("master-Luminance.fit")) as hd:
master = hd[0].data.astype(np.float32)
import sep
bkg = sep.Background(master, bw=64, bh=64, fw=3, fh=3)
m = sep.extract(master - bkg.back(), 2.5, err=bkg.globalrms, minarea=4,
deblend_cont=0.005)
print(f"\nmaster detections (static sky): {len(m)}")
mtree = cKDTree(np.column_stack([m["x"], m["y"]]))
print("\nresiduals after removing anything within 4 px of a master source:")
for k in sorted(lum):
cur = ref[k][0]
d, _ = mtree.query(cur, distance_upper_bound=4.0)
left = ~np.isfinite(d)
print(f" {k:16s} {left.sum():5d} / {len(cur)}")
if __name__ == "__main__":
main()