Move the processing code under pipeline/
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.
This commit is contained in:
parent
653ca103cd
commit
c6299f41ab
53 changed files with 0 additions and 0 deletions
|
|
@ -1,191 +0,0 @@
|
|||
"""Step 5: dust-lane extinction map, with an independent colour-excess check.
|
||||
|
||||
Method
|
||||
1. The smooth isophote model of the luminance master gives the light the
|
||||
galaxy would show with no dust. A_L = -2.5 log10(observed / model).
|
||||
This is a foreground-screen approximation: the lane is a warped disk seen
|
||||
nearly edge-on across the near side of the bulge, so the screen assumption
|
||||
is good for the lane itself but underestimates the true optical depth
|
||||
wherever stars sit in front of the dust.
|
||||
2. The same is done for R, G and B on the SAME elliptical isophotes, using
|
||||
the sigma-clipped azimuthal median of the dust-free azimuths as each
|
||||
channel's unobscured model. E(B-R) = A_B - A_R is then a completely
|
||||
independent, model-ratio-based reddening measurement, and A_L / E(B-R)
|
||||
is a measured extinction-law ratio rather than an assumed one.
|
||||
|
||||
Outputs
|
||||
sb-extinction.fits A_L map (mag), NaN outside the measurable region
|
||||
NGC5128-sb-extinction-map.png 4-panel extinction / reddening figure
|
||||
NGC5128-sb-extinction-law.png A_L against E(B-R) with the fitted ratio
|
||||
"""
|
||||
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, median_filter
|
||||
from sb_common import *
|
||||
|
||||
XC, YC = np.load(path('_geom.npy'))
|
||||
P = np.load(path('_profile.npz'))
|
||||
star = fits.getdata(path('sb-mask-stars.fits')).astype(bool)
|
||||
dust = fits.getdata(path('sb-mask-dust.fits')).astype(bool)
|
||||
a_map = np.load(path('_amap.npy'))
|
||||
ac = P['a']
|
||||
|
||||
A = {}
|
||||
for ch in ['Luminance', 'Red', 'Green', 'Blue']:
|
||||
pr = P[ch]
|
||||
ok = np.isfinite(pr) & (pr > 0)
|
||||
mdl = np.interp(a_map, ac[ok], pr[ok], left=pr[ok][0], right=np.nan)
|
||||
img = gaussian_filter(load(ch), 2.0) # match the colour smoothing
|
||||
with np.errstate(all='ignore'):
|
||||
A[ch] = np.where((img > 0) & (mdl > 0), -2.5*np.log10(img/mdl),
|
||||
np.nan).astype(np.float32)
|
||||
del img, mdl
|
||||
print('%-10s A map built' % ch)
|
||||
|
||||
AL = A['Luminance']
|
||||
EBR = (A['Blue'] - A['Red']).astype(np.float32)
|
||||
|
||||
# region where both are trustworthy: inside the lane, bright enough, not a star
|
||||
Lmod = np.interp(a_map, ac[np.isfinite(P['Luminance'])],
|
||||
P['Luminance'][np.isfinite(P['Luminance'])])
|
||||
valid = (~star) & (a_map < 700) & (Lmod > 60) & np.isfinite(AL) & np.isfinite(EBR)
|
||||
lane = valid & dust
|
||||
print('valid extinction pixels: %d (%.1f arcmin^2); inside the lane: %d (%.1f arcmin^2)'
|
||||
% (valid.sum(), valid.sum()*PIXAREA/3600., lane.sum(), lane.sum()*PIXAREA/3600.))
|
||||
|
||||
x, y = EBR[lane].astype(float), AL[lane].astype(float)
|
||||
sel = (x > 0.1) & (x < 2.0) & (y > -0.3) & (y < 3.5)
|
||||
k = float(np.median(y[sel]/x[sel]))
|
||||
klsq = float(np.sum(x[sel]*y[sel])/np.sum(x[sel]**2))
|
||||
print('extinction-law ratio A_L / E(B-R) = %.2f (median of ratios), %.2f (lsq)'
|
||||
% (k, klsq))
|
||||
|
||||
ALs = median_filter(np.nan_to_num(AL, nan=0.0), 9)
|
||||
ALs[~valid] = np.nan
|
||||
pk = np.nanpercentile(ALs[lane], [50, 90, 99, 99.9])
|
||||
print('A_L inside the lane mask (9x9 median filtered): '
|
||||
'median %.2f, p90 %.2f, p99 %.2f, p99.9 %.2f mag' % tuple(pk))
|
||||
iy, ix = np.unravel_index(np.nanargmax(np.where(lane, ALs, np.nan)), ALs.shape)
|
||||
w = wcs()
|
||||
rr, dd = w.pixel_to_world_values(ix, iy)
|
||||
print('peak A_L = %.2f mag at pixel (%d, %d) = %.5f %+.5f deg, %.0f arcsec from '
|
||||
'the nucleus' % (ALs[iy, ix], ix, iy, rr, dd,
|
||||
np.hypot(ix-XC, iy-YC)*PIXSCALE))
|
||||
|
||||
hdu = fits.PrimaryHDU(np.where(valid, AL, np.nan).astype(np.float32),
|
||||
header=w.to_header())
|
||||
hdu.header['BUNIT'] = 'mag'
|
||||
hdu.header['COMMENT'] = 'A_L = -2.5 log10(observed / smooth isophote model)'
|
||||
hdu.writeto(path('sb-extinction.fits'), overwrite=True)
|
||||
|
||||
# obscured light: how much luminance flux the lane removes
|
||||
Lraw = load('Luminance')
|
||||
lost = float(np.nansum((Lmod - Lraw)[lane]))
|
||||
tot = float(np.nansum(Lmod[valid & (a_map < 700)]))
|
||||
print('the lane hides %.3e ADU, i.e. %.1f%% of the modelled light inside a=700 px'
|
||||
% (lost, 100*lost/tot))
|
||||
print(' that is %.2f mag of integrated light removed from the lane region'
|
||||
% (-2.5*np.log10(1 - lost/max(np.nansum(Lmod[lane]), 1))))
|
||||
|
||||
# ------------------------------------------------------------------- figure 1
|
||||
NV, EV = north_east_pixel()
|
||||
CUT = 620
|
||||
sl = (slice(int(YC)-CUT, int(YC)+CUT), slice(int(XC)-CUT, int(XC)+CUT))
|
||||
ext = [-CUT*PIXSCALE/60, CUT*PIXSCALE/60]*2
|
||||
|
||||
|
||||
def compass(ax, c='k', x=0.885, y=0.10, Ln=0.075):
|
||||
for v, lab in [(NV, 'N'), (EV, 'E')]:
|
||||
ax.annotate('', xy=(x+Ln*v[0], y+Ln*v[1]), xytext=(x, y),
|
||||
xycoords='axes fraction', textcoords='axes fraction',
|
||||
arrowprops=dict(arrowstyle='->', color=c, lw=1.4))
|
||||
ax.annotate(lab, xy=(x+1.45*Ln*v[0], y+1.45*Ln*v[1]), color=c,
|
||||
xycoords='axes fraction', ha='center', va='center', fontsize=10)
|
||||
|
||||
|
||||
fig, axs = plt.subplots(2, 2, figsize=(15, 14.4))
|
||||
axs = axs.ravel()
|
||||
axs[0].imshow(np.arcsinh(np.clip(Lraw[sl], 0, None)/30), origin='lower',
|
||||
cmap='gray', extent=ext)
|
||||
axs[0].set_title('(a) luminance master')
|
||||
compass(axs[0], 'w')
|
||||
|
||||
im = axs[1].imshow(np.where(valid, ALs, np.nan)[sl], origin='lower', cmap='magma_r',
|
||||
vmin=0, vmax=2.0, extent=ext)
|
||||
axs[1].set_title('(b) extinction $A_L$ from the smooth model [mag]')
|
||||
plt.colorbar(im, ax=axs[1], fraction=.046, label='mag')
|
||||
compass(axs[1])
|
||||
|
||||
im = axs[2].imshow(np.where(valid, gaussian_filter(np.nan_to_num(EBR), 2), np.nan)[sl],
|
||||
origin='lower', cmap='inferno_r', vmin=0, vmax=1.4, extent=ext)
|
||||
axs[2].set_title('(c) colour excess E(B-R) from the R and B models [mag]')
|
||||
plt.colorbar(im, ax=axs[2], fraction=.046, label='mag')
|
||||
compass(axs[2])
|
||||
|
||||
im = axs[3].imshow(np.where(valid, ALs - k*EBR, np.nan)[sl], origin='lower',
|
||||
cmap='RdBu_r', vmin=-0.6, vmax=0.6, extent=ext)
|
||||
axs[3].set_title('(d) $A_L$ - %.2f E(B-R): agreement of the two methods' % k)
|
||||
plt.colorbar(im, ax=axs[3], fraction=.046, label='mag')
|
||||
compass(axs[3])
|
||||
for a in axs:
|
||||
a.set_xlabel('arcmin')
|
||||
a.set_ylabel('arcmin')
|
||||
fig.suptitle('NGC 5128 dust lane: extinction and reddening '
|
||||
'(central %.1f x %.1f arcmin)' % (2*CUT*PIXSCALE/60, 2*CUT*PIXSCALE/60),
|
||||
fontsize=14)
|
||||
fig.tight_layout()
|
||||
fig.savefig(path('NGC5128-sb-extinction-map.png'), dpi=125)
|
||||
plt.close(fig)
|
||||
|
||||
# ------------------------------------------------------------------- figure 2
|
||||
fig, axs = plt.subplots(1, 2, figsize=(13.5, 5.6))
|
||||
h = axs[0].hist2d(x[sel], y[sel], bins=(140, 140), range=[[0, 1.6], [-0.3, 3.0]],
|
||||
cmap='viridis', norm=matplotlib.colors.LogNorm())
|
||||
plt.colorbar(h[3], ax=axs[0], label='pixels')
|
||||
xs = np.linspace(0, 1.6, 50)
|
||||
axs[0].plot(xs, k*xs, 'r-', lw=2, label='$A_L$ = %.2f E(B-R) (median ratio)' % k)
|
||||
axs[0].plot(xs, klsq*xs, 'w--', lw=1.6, label='least squares: %.2f' % klsq)
|
||||
axs[0].set_xlabel('E(B-R) [mag, instrumental]')
|
||||
axs[0].set_ylabel('$A_L$ [mag]')
|
||||
axs[0].legend(fontsize=9)
|
||||
axs[0].set_title('extinction law measured inside the lane (%d px)' % sel.sum())
|
||||
|
||||
bb = np.geomspace(20, 700, 26)
|
||||
ib = np.digitize(a_map, bb)
|
||||
med, p90 = [], []
|
||||
for kk in range(1, len(bb)):
|
||||
m = (ib == kk) & lane
|
||||
med.append(np.nanmedian(ALs[m]) if m.sum() > 200 else np.nan)
|
||||
p90.append(np.nanpercentile(ALs[m], 90) if m.sum() > 200 else np.nan)
|
||||
bc = np.sqrt(bb[1:]*bb[:-1])*PIXSCALE
|
||||
axs[1].plot(bc, med, 'o-', color='tab:purple', label='median $A_L$ in the lane')
|
||||
axs[1].plot(bc, p90, 's--', color='tab:orange', label='90th percentile')
|
||||
axs[1].set_xscale('log')
|
||||
axs[1].set_xlabel('semi-major axis a [arcsec]')
|
||||
axs[1].set_ylabel('$A_L$ [mag]')
|
||||
axs[1].grid(alpha=.3)
|
||||
axs[1].legend(fontsize=9)
|
||||
axs[1].set_title('extinction against radius along the lane')
|
||||
fig.tight_layout()
|
||||
fig.savefig(path('NGC5128-sb-extinction-law.png'), dpi=140)
|
||||
plt.close(fig)
|
||||
|
||||
with open(path('sb-derived-quantities.txt'), 'a') as f:
|
||||
wr = lambda t: (f.write(t + chr(10)), print(t))
|
||||
wr('')
|
||||
wr('Dust lane')
|
||||
wr(' lane mask area %.1f arcmin^2' % (dust.sum()*PIXAREA/3600.))
|
||||
wr(' measurable extinction area %.1f arcmin^2' % (lane.sum()*PIXAREA/3600.))
|
||||
wr(' A_L median / p90 / p99 / p99.9 %.2f / %.2f / %.2f / %.2f mag' % tuple(pk))
|
||||
wr(' peak A_L (9x9 median filtered) %.2f mag at RA %.4f Dec %+.4f'
|
||||
% (ALs[iy, ix], rr, dd))
|
||||
wr(' peak is %.0f arcsec from the nucleus' % (np.hypot(ix-XC, iy-YC)*PIXSCALE))
|
||||
wr(' extinction law A_L / E(B-R) %.2f (median of ratios), %.2f (lsq)'
|
||||
% (k, klsq))
|
||||
wr(' luminance flux hidden by the lane %.1f%% of the modelled light inside a=700 px'
|
||||
% (100*lost/tot))
|
||||
print('')
|
||||
print('wrote sb-extinction.fits, NGC5128-sb-extinction-map.png, NGC5128-sb-extinction-law.png')
|
||||
Loading…
Add table
Add a link
Reference in a new issue