Preparing to merge this repository into a combined astrophotography repo. session-scripts/ becomes pipeline/ because the scripts import layout.py from their own directory and must stay together, and because 'pipeline' says what it is rather than how it came about. observing/ stays at the top level: observing plans are not processing code.
77 lines
2.9 KiB
Python
77 lines
2.9 KiB
Python
"""Step 4: smooth elliptical model, model-subtracted residual, and renders.
|
|
|
|
The model is the Pass B isophote table (stars and dust lane masked) turned into
|
|
a 2-D image with sb_model.build, with a Sersic extrapolation inside a = 62 px
|
|
where no dust-free azimuth exists. Subtracting it leaves everything that is
|
|
not a smooth ellipse: the dust lane, foreground stars, and any shell, tidal
|
|
feature or halo asymmetry.
|
|
|
|
Outputs
|
|
sb-model.fits the smooth model
|
|
sb-residual.fits luminance minus model
|
|
NGC5128-sb-model-residual.png 4-panel: data / model / residual / binned deep residual
|
|
NGC5128-sb-residual-deep.png heavily binned residual alone, for shell hunting
|
|
"""
|
|
import numpy as np
|
|
from astropy.io import fits
|
|
import matplotlib
|
|
matplotlib.use('Agg')
|
|
import matplotlib.pyplot as plt
|
|
from scipy.ndimage import gaussian_filter
|
|
from sb_common import *
|
|
import sb_model
|
|
|
|
XC, YC = np.load(path('_geom.npy'))
|
|
P = np.load(path('_profile.npz'))
|
|
tB = dict(np.load(path('_isoB.npz')))
|
|
star = fits.getdata(path('sb-mask-stars.fits')).astype(bool)
|
|
dust = fits.getdata(path('sb-mask-dust.fits')).astype(bool)
|
|
A_IN, A_OUT = float(P['a_in']), float(P['a_out'])
|
|
mue, re_as, nser = P['sersic']
|
|
|
|
|
|
def sersic_mu(a_as):
|
|
bn = 2 * nser - 1 / 3. + 0.009876 / nser
|
|
return mue + 2.5 * bn / np.log(10) * ((a_as / re_as) ** (1. / nser) - 1.)
|
|
|
|
|
|
# ---- build a profile that is defined at every radius -----------------------
|
|
ac = P['a']
|
|
Lp = P['Luminance'].copy()
|
|
inner = ac < A_IN
|
|
Lp[inner] = 10 ** ((MU0 - sersic_mu(ac[inner] * PIXSCALE)) / 2.5)
|
|
okp = np.isfinite(Lp) & (ac < 1500)
|
|
tab = dict(sma=ac[okp], intens=Lp[okp],
|
|
eps=np.interp(ac[okp], tB['sma'], np.where(np.isfinite(tB['eps']),
|
|
tB['eps'], 0.15)),
|
|
pa=np.interp(ac[okp], tB['sma'], np.where(np.isfinite(tB['pa']),
|
|
tB['pa'], 150.)))
|
|
model, a_map = sb_model.build((3194, 4788), XC, YC, tab, block=2)
|
|
fits.PrimaryHDU(model).writeto(path('sb-model.fits'), overwrite=True)
|
|
|
|
PEDL = float(P['ped'])
|
|
L = load('Luminance')
|
|
res = (L - model).astype(np.float32)
|
|
fits.PrimaryHDU(res).writeto(path('sb-residual.fits'), overwrite=True)
|
|
print('model built; residual rms inside a<600 px: %.2f ADU/px'
|
|
% res[(a_map < 600) & ~star & ~dust].std())
|
|
|
|
|
|
def binned(img, mask, B):
|
|
"""Masked block mean, returning NaN where a block is mostly masked."""
|
|
H, W = img.shape
|
|
h, w = H // B, W // B
|
|
a = img[:h * B, :w * B].reshape(h, B, w, B)
|
|
m = (~mask)[:h * B, :w * B].reshape(h, B, w, B)
|
|
n = m.sum(axis=(1, 3))
|
|
s = np.where(m, a, 0).sum(axis=(1, 3))
|
|
return np.where(n > 0.35 * B * B, s / np.maximum(n, 1), np.nan)
|
|
|
|
|
|
|
|
|
|
# ------------------------------------------------------------------ renders
|
|
import sb_render_tail
|
|
sb_render_tail.run(dict(res=res, model=model, a_map=a_map, L=L, star=star,
|
|
dust=dust, XC=XC, YC=YC, PEDL=PEDL, A_OUT=A_OUT,
|
|
tab=tab, binned=binned))
|