astrophotography/session-scripts/verify_core.py
laurence 5286a2e81b 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.
2026-07-21 15:29:49 +01:00

74 lines
3 KiB
Python

"""Is the nucleus of NGC 5128 actually saturated, or was that a foreground star?
The earlier claim - that the core clips at 65313 ADU in a single 300 s sub -
came from taking the maximum inside a 300x300 px box centred on the frame. That
box is wide enough to contain a bright foreground star, so the measurement
proves only that SOMETHING in the middle of the frame is bright. This checks
where the bright pixels actually are, and what the galaxy itself peaks at once
stars are filtered out.
"""
import numpy as np
from astropy import units as u
from astropy.coordinates import SkyCoord
from astropy.io import fits
from astropy.wcs import WCS
from scipy.ndimage import median_filter
import layout
SUB = layout.path("calibrated-T32-qisback-NGC5128-20260721-190133"
"-Luminance-BIN2-W-300-002.fit")
MASTER = layout.path("master-Luminance.fit")
# NGC 5128's nucleus, from SIMBAD, not from "the middle of the frame".
NUCLEUS = SkyCoord("13h25m27.6s", "-43d01m08.8s")
with fits.open(MASTER) as hd:
wcs = WCS(hd[0].header, naxis=2)
nx_c, ny_c = wcs.world_to_pixel(NUCLEUS)
print(f"nucleus lands at master pixel ({nx_c:.1f}, {ny_c:.1f})")
data = fits.getdata(SUB).astype(np.float32)
ny, nx = data.shape
print(f"single sub {nx} x {ny}, global max {data.max():.0f} ADU")
# Where are the saturated-ish pixels?
ys, xs = np.where(data > 60000)
print(f"{len(xs)} pixels above 60000 ADU")
if len(xs):
# Cluster them crudely by proximity to see how many distinct objects.
print(f" x range {xs.min()}-{xs.max()}, y range {ys.min()}-{ys.max()}")
cx, cy = nx / 2.0, ny / 2.0
d = np.hypot(xs - cx, ys - cy)
print(f" distance from frame centre: min {d.min():.0f} px, "
f"median {np.median(d):.0f} px, max {d.max():.0f} px")
# The brightest pixel specifically
iy, ix = np.unravel_index(np.argmax(data), data.shape)
print(f" brightest pixel at ({ix}, {iy}), "
f"{np.hypot(ix - cx, iy - cy):.0f} px from frame centre")
# The galaxy's own peak: median filter removes stars, which are small, while
# leaving the smooth galaxy light essentially untouched.
h = 400
y0, y1 = int(ny / 2) - h, int(ny / 2) + h
x0, x1 = int(nx / 2) - h, int(nx / 2) + h
core = data[y0:y1, x0:x1]
smooth = median_filter(core, size=15)
print(f"\ninner {2*h}x{2*h} px box:")
print(f" raw max {core.max():9.1f} ADU")
print(f" median-filtered max {smooth.max():9.1f} ADU <- galaxy light")
iy, ix = np.unravel_index(np.argmax(smooth), smooth.shape)
print(f" galaxy peak at frame pixel ({x0+ix}, {y0+iy})")
# How many pixels of the median-filtered (star-free) galaxy are near clipping?
for lvl in (30000, 50000, 60000):
print(f" star-free pixels above {lvl}: {(smooth > lvl).sum()}")
# And in the master stack.
mdata = fits.getdata(MASTER).astype(np.float32)
mcore = mdata[y0:y1, x0:x1]
msmooth = median_filter(mcore, size=15)
print(f"\nmaster stack inner box: raw max {mcore.max():.1f}, "
f"star-free max {msmooth.max():.1f} ADU")
print(f" master 99.995th percentile (the stretch white point) "
f"{np.percentile(mdata, 99.995):.1f} ADU")