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