"""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")