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.
85 lines
3 KiB
Python
85 lines
3 KiB
Python
"""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()
|