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,491 +0,0 @@
|
|||
"""
|
||||
gc-plots.py -- step 4. All figures for the NGC 5128 globular cluster survey.
|
||||
|
||||
NGC5128-gc-background-check.png how the galaxy light was removed
|
||||
NGC5128-gc-finder.png annotated finder chart with zoom insets
|
||||
NGC5128-gc-cmd.png colour-magnitude diagram
|
||||
NGC5128-gc-completeness.png artificial-star recovery
|
||||
NGC5128-gc-radial-profile.png THE key plot: surface density vs radius
|
||||
"""
|
||||
import os, numpy as np, warnings
|
||||
import matplotlib
|
||||
matplotlib.use("Agg")
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.patches import Circle, Rectangle
|
||||
from matplotlib.lines import Line2D
|
||||
from astropy.io import fits
|
||||
from astropy.wcs import WCS
|
||||
|
||||
import layout
|
||||
warnings.filterwarnings("ignore")
|
||||
|
||||
S = layout.SESSION
|
||||
PIXSCALE = 0.5376
|
||||
NUC_RA, NUC_DEC = 201.365063, -43.019113
|
||||
KPC_PER_ARCMIN = 3.8 * 1000.0 * (np.pi / 180.0) / 60.0
|
||||
|
||||
# validated categorical palette (see notes)
|
||||
C_CAND, C_EXT, C_KNOWN, C_ACC = "#2563eb", "#e8710a", "#127a5a", "#8b5cf6"
|
||||
C_INK, C_MUTED, C_GRID = "#1a1a1a", "#5c5c5c", "#d8d8d4"
|
||||
SURF = "#fcfcfb"
|
||||
|
||||
plt.rcParams.update({
|
||||
"figure.facecolor": SURF, "axes.facecolor": SURF,
|
||||
"axes.edgecolor": C_MUTED, "axes.labelcolor": C_INK,
|
||||
"text.color": C_INK, "xtick.color": C_MUTED, "ytick.color": C_MUTED,
|
||||
"axes.grid": True, "grid.color": C_GRID, "grid.linewidth": 0.6,
|
||||
"axes.axisbelow": True, "font.size": 10,
|
||||
"axes.spines.top": False, "axes.spines.right": False,
|
||||
"legend.frameon": False,
|
||||
})
|
||||
|
||||
|
||||
def asinh_stretch(a, lo=1.0, hi=99.7, soft=8.0):
|
||||
v0, v1 = np.percentile(a[np.isfinite(a)], [lo, hi])
|
||||
x = np.clip((a - v0) / max(v1 - v0, 1e-9), 0, 1)
|
||||
return np.arcsinh(soft * x) / np.arcsinh(soft)
|
||||
|
||||
|
||||
def load_cat():
|
||||
return dict(np.load(layout.path("_gc_cat.npz"), allow_pickle=True))
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
def fig_background(d):
|
||||
import sep
|
||||
img = np.ascontiguousarray(
|
||||
fits.getdata(layout.path("master-Luminance.fit")).astype(np.float32))
|
||||
b16 = sep.Background(img, bw=16, bh=16, fw=3, fh=3)
|
||||
b256 = sep.Background(img, bw=256, bh=256, fw=3, fh=3)
|
||||
raw = img[::4, ::4]
|
||||
back = b16.back()[::4, ::4]
|
||||
sub16 = raw - back
|
||||
sub256 = raw - b256.back()[::4, ::4]
|
||||
del img, b16, b256
|
||||
|
||||
# ONE stretch for the two image-scale panels and ONE for the two residuals,
|
||||
# so the panels are actually comparable to each other.
|
||||
v0, v1 = np.percentile(raw, [1.0, 99.7])
|
||||
r0, r1 = np.percentile(sub16, [1.0, 99.85])
|
||||
|
||||
def show(a, im, lo, hi, t, sub=""):
|
||||
x = np.clip((im - lo) / (hi - lo), 0, 1)
|
||||
a.imshow(np.arcsinh(8 * x) / np.arcsinh(8.0), origin="lower", cmap="gray",
|
||||
interpolation="nearest", vmin=0, vmax=1)
|
||||
a.set_title(t, fontsize=10.5, loc="left", pad=4)
|
||||
if sub:
|
||||
a.text(0.5, -0.045, sub, transform=a.transAxes, ha="center", va="top",
|
||||
fontsize=9, color=C_MUTED)
|
||||
a.set_xticks([]); a.set_yticks([]); a.grid(False)
|
||||
|
||||
fig, ax = plt.subplots(2, 2, figsize=(12.4, 8.0))
|
||||
show(ax[0, 0], raw, v0, v1, "a) luminance master")
|
||||
show(ax[0, 1], back, v0, v1, "b) background model, 16 px mesh (adopted)",
|
||||
"same stretch as (a): the model reproduces the galaxy and the dust lane")
|
||||
show(ax[1, 0], sub16, r0, r1, "c) residual after (b) - what detection runs on",
|
||||
"galaxy gone; point sources remain at every radius")
|
||||
show(ax[1, 1], sub256, r0, r1, "d) residual after a 256 px mesh - the failure mode",
|
||||
"the galaxy survives, swamping the inner field and hiding its clusters")
|
||||
fig.suptitle("Removing NGC 5128's own light before detection", fontsize=13,
|
||||
x=0.008, ha="left")
|
||||
fig.tight_layout(rect=[0, 0.02, 1, 0.97])
|
||||
fig.savefig(layout.path("NGC5128-gc-background-check.png"), dpi=110,
|
||||
bbox_inches="tight", facecolor=SURF)
|
||||
plt.close(fig)
|
||||
print("wrote NGC5128-gc-background-check.png")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
def fig_finder(d):
|
||||
hdr = fits.getheader(layout.path("master-Luminance.fit"))
|
||||
w = WCS(hdr)
|
||||
img = fits.getdata(layout.path("master-Luminance.fit")).astype(np.float32)
|
||||
B = 4
|
||||
small = img[::B, ::B]
|
||||
disp = asinh_stretch(small, 20, 99.5, 12)
|
||||
cx, cy = [float(v) for v in w.all_world2pix(NUC_RA, NUC_DEC, 0)]
|
||||
|
||||
cand = d["cand"]
|
||||
known = cand & ((d["simbad_type"] == "GlC") | d["scabs_hit"])
|
||||
newc = cand & ~known
|
||||
|
||||
ny_s, nx_s = small.shape
|
||||
# Main panel keeps the detector's 1.5:1 aspect; insets sit in a row beneath.
|
||||
fig = plt.figure(figsize=(14.0, 13.0))
|
||||
gs = fig.add_gridspec(2, 4, height_ratios=[nx_s / ny_s * 0.98, 1.0],
|
||||
hspace=0.09, wspace=0.06,
|
||||
left=0.012, right=0.988, top=0.945, bottom=0.015)
|
||||
ax = fig.add_subplot(gs[0, :])
|
||||
ax.imshow(disp, origin="lower", cmap="gray", interpolation="bilinear",
|
||||
aspect="equal")
|
||||
ax.set_xlim(0, nx_s); ax.set_ylim(0, ny_s)
|
||||
ax.grid(False); ax.set_xticks([]); ax.set_yticks([])
|
||||
|
||||
for m, col, lab in ((known, C_KNOWN, "matches a published catalogue (%d)" % known.sum()),
|
||||
(newc, C_CAND, "no published counterpart (%d)" % newc.sum())):
|
||||
ax.scatter(d["x"][m] / B, d["y"][m] / B, s=110, facecolors="none",
|
||||
edgecolors=col, linewidths=1.2, label=lab)
|
||||
# nucleus
|
||||
import matplotlib.patheffects as pe
|
||||
halo = [pe.withStroke(linewidth=3.0, foreground="black")]
|
||||
ax.plot(cx / B, cy / B, marker="+", ms=20, mew=2.2, color=C_ACC, zorder=6)
|
||||
ax.annotate("NGC 5128 nucleus", (cx / B, cy / B), xytext=(-95, -78),
|
||||
textcoords="offset points", color=C_ACC, fontsize=11,
|
||||
weight="bold", ha="center", zorder=7, path_effects=halo,
|
||||
arrowprops=dict(arrowstyle="-", color=C_ACC, lw=1.3,
|
||||
shrinkA=2, shrinkB=10))
|
||||
|
||||
# scale bar: 5 arcmin, bottom left, inside the frame
|
||||
L = 5 * 60 / PIXSCALE / B
|
||||
x0, y0 = nx_s * 0.035, ny_s * 0.062
|
||||
ax.plot([x0, x0 + L], [y0, y0], color="white", lw=3.5, solid_capstyle="butt")
|
||||
ax.text(x0 + L / 2, y0 + ny_s * 0.018,
|
||||
"5' = %.1f kpc at 3.8 Mpc" % (5 * KPC_PER_ARCMIN),
|
||||
ha="center", color="white", fontsize=10)
|
||||
# compass, derived from the WCS itself rather than from the quoted PA:
|
||||
# step 1 arcmin north and 1 arcmin east of the nucleus and see where it lands
|
||||
cxo, cyo = nx_s * 0.915, ny_s * 0.16
|
||||
for dra, ddec, lab in ((0.0, 1 / 60.0, "N"), (1 / 60.0, 0.0, "E")):
|
||||
px, py = w.all_world2pix(NUC_RA + dra / np.cos(np.radians(NUC_DEC)),
|
||||
NUC_DEC + ddec, 0)
|
||||
vx, vy = float(px) - cx, float(py) - cy
|
||||
n = np.hypot(vx, vy)
|
||||
dx, dy = vx / n * 48, vy / n * 48
|
||||
ax.annotate("", (cxo + dx, cyo + dy), (cxo, cyo),
|
||||
arrowprops=dict(arrowstyle="->", color="white", lw=1.8))
|
||||
ax.text(cxo + dx * 1.38, cyo + dy * 1.38, lab, color="white",
|
||||
ha="center", va="center", fontsize=11, weight="bold")
|
||||
|
||||
ax.legend(loc="upper left", fontsize=10.5, labelcolor=C_INK,
|
||||
handletextpad=0.4, borderpad=0.6, markerscale=1.1,
|
||||
facecolor="white", framealpha=0.82, frameon=True, edgecolor="none")
|
||||
ax.set_title("NGC 5128 globular cluster candidates: %d circled, G < 20 "
|
||||
"(42.9' x 28.6' luminance master, asinh stretch, 4x downsampled)"
|
||||
% cand.sum(), fontsize=12.5, loc="left", pad=9)
|
||||
|
||||
# ---- zoom insets, full resolution ----
|
||||
zooms = [(2.5, 55), (5.5, 340), (9.0, 130), (16.0, 205)]
|
||||
HW = 230 # half-width in full-res px -> 4.1 arcmin box
|
||||
for k, (rr, pa) in enumerate(zooms):
|
||||
az = fig.add_subplot(gs[1, k])
|
||||
ang = np.radians(pa)
|
||||
zx = cx + rr * 60 / PIXSCALE * np.cos(ang)
|
||||
zy = cy + rr * 60 / PIXSCALE * np.sin(ang)
|
||||
zx = float(np.clip(zx, HW, img.shape[1] - HW))
|
||||
zy = float(np.clip(zy, HW, img.shape[0] - HW))
|
||||
cut = img[int(zy - HW):int(zy + HW), int(zx - HW):int(zx + HW)]
|
||||
az.imshow(asinh_stretch(cut, 15, 99.7, 14), origin="lower", cmap="gray",
|
||||
interpolation="nearest", aspect="equal",
|
||||
extent=[zx - HW, zx + HW, zy - HW, zy + HW])
|
||||
for m, col in ((known, C_KNOWN), (newc, C_CAND)):
|
||||
sel = m & (np.abs(d["x"] - zx) < HW) & (np.abs(d["y"] - zy) < HW)
|
||||
az.scatter(d["x"][sel], d["y"][sel], s=230, facecolors="none",
|
||||
edgecolors=col, linewidths=1.6)
|
||||
az.set_xticks([]); az.set_yticks([]); az.grid(False)
|
||||
az.set_title("%.1f' from the nucleus" % rr, fontsize=10.5, loc="left", pad=5)
|
||||
ax.add_patch(Rectangle(((zx - HW) / B, (zy - HW) / B), 2 * HW / B, 2 * HW / B,
|
||||
fill=False, ec="white", lw=1.1, ls="-", alpha=0.8))
|
||||
ax.text((zx) / B, (zy + HW) / B + 6, "%.1f'" % rr, color="white",
|
||||
fontsize=9, ha="center")
|
||||
fig.text(0.012, 0.002,
|
||||
"Lower row: full-resolution %.1f' cut-outs at the marked positions, same "
|
||||
"circles. The innermost arcminute is empty of candidates: the nucleus and "
|
||||
"dust lane are impenetrable at this depth." % (2 * HW * PIXSCALE / 60),
|
||||
fontsize=9.5, color=C_MUTED)
|
||||
fig.savefig(layout.path("NGC5128-gc-finder.png"), dpi=100, facecolor=SURF)
|
||||
plt.close(fig)
|
||||
del img
|
||||
print("wrote NGC5128-gc-finder.png")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
def fig_cmd(d):
|
||||
fig, ax = plt.subplots(1, 2, figsize=(12.6, 5.6))
|
||||
stars = d["base"] & d["is_star"] & np.isfinite(d["BmR"]) & (d["mag"] > 15) & (d["mag"] < 21.5)
|
||||
cand = d["cand"] & np.isfinite(d["BmR"])
|
||||
known = cand & ((d["simbad_type"] == "GlC") | d["scabs_hit"])
|
||||
|
||||
a = ax[0]
|
||||
a.hexbin(d["BmR"][stars], d["mag"][stars], gridsize=60, cmap="Greys",
|
||||
mincnt=1, extent=(-1.0, 3.5, 15, 21.6), linewidths=0)
|
||||
a.scatter(d["BmR"][cand & ~known], d["mag"][cand & ~known], s=20, c=C_CAND,
|
||||
alpha=0.85, lw=0, label="candidate, no published counterpart")
|
||||
a.scatter(d["BmR"][known], d["mag"][known], s=22, c=C_KNOWN, alpha=0.9,
|
||||
lw=0, marker="D", label="candidate, published GC")
|
||||
a.set_xlim(-1.0, 3.5); a.set_ylim(21.6, 15)
|
||||
a.set_xlabel("B - R (instrumental, calibrated to Gaia BP - RP)")
|
||||
a.set_ylabel("G (luminance)")
|
||||
a.set_title("a) candidates against the foreground stellar field", fontsize=10.5, loc="left")
|
||||
a.legend(fontsize=9, loc="lower left", labelcolor=C_INK)
|
||||
a.text(0.98, 0.03, "grey = %d astrometric\nMilky Way stars" % stars.sum(),
|
||||
transform=a.transAxes, ha="right", va="bottom", fontsize=9, color=C_MUTED)
|
||||
|
||||
b = ax[1]
|
||||
bins = np.arange(-1.0, 3.51, 0.2)
|
||||
b.hist(d["BmR"][stars], bins=bins, density=True, color=C_MUTED, alpha=0.35,
|
||||
label="foreground stars (n=%d)" % stars.sum())
|
||||
b.hist(d["BmR"][cand], bins=bins, density=True, histtype="step", lw=2.0,
|
||||
color=C_CAND, label="all candidates (n=%d)" % cand.sum())
|
||||
b.hist(d["BmR"][known], bins=bins, density=True, histtype="step", lw=2.0,
|
||||
color=C_KNOWN, ls="--", label="published GCs (n=%d)" % known.sum())
|
||||
b.set_xlabel("B - R"); b.set_ylabel("normalised density")
|
||||
b.set_title("b) colour distributions", fontsize=10.5, loc="left")
|
||||
b.legend(fontsize=9, labelcolor=C_INK)
|
||||
med = np.nanmedian(d["BmR"][cand])
|
||||
b.axvline(med, color=C_CAND, lw=1, ls=":")
|
||||
b.text(med + 0.06, b.get_ylim()[1] * 0.93, "candidate median %.2f" % med,
|
||||
fontsize=9, color=C_CAND)
|
||||
|
||||
fig.suptitle("Colour-magnitude diagram, NGC 5128 cluster candidates",
|
||||
fontsize=12.5, x=0.008, ha="left")
|
||||
fig.tight_layout()
|
||||
fig.savefig(layout.path("NGC5128-gc-cmd.png"), dpi=115, bbox_inches="tight", facecolor=SURF)
|
||||
plt.close(fig)
|
||||
print("wrote NGC5128-gc-cmd.png")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
def completeness_grid():
|
||||
"""Prefer the merged uniform + inner-field artificial-star runs.
|
||||
|
||||
The uniform run alone puts only ~2% of its fakes inside 3 arcmin, which left
|
||||
the innermost completeness bins too noisy to correct with.
|
||||
"""
|
||||
for name in ("_gc_complete_all.npz", "_gc_complete.npz"):
|
||||
p = layout.path(name)
|
||||
if os.path.exists(p):
|
||||
print("completeness from %s" % name)
|
||||
return np.load(p)
|
||||
return None
|
||||
|
||||
|
||||
def fig_completeness(cp):
|
||||
fig, ax = plt.subplots(1, 2, figsize=(12.6, 5.0))
|
||||
mag, rad, ok = cp["mag"], cp["rad"], cp["ok"]
|
||||
mb = np.arange(17.5, 22.51, 0.25)
|
||||
mc = 0.5 * (mb[1:] + mb[:-1])
|
||||
rbins = [(0, 2), (2, 4), (4, 8), (8, 30)]
|
||||
cols = [C_ACC, C_EXT, C_KNOWN, C_CAND]
|
||||
a = ax[0]
|
||||
for (lo, hi), c in zip(rbins, cols):
|
||||
f = []
|
||||
for j in range(len(mb) - 1):
|
||||
s = (mag >= mb[j]) & (mag < mb[j + 1]) & (rad >= lo) & (rad < hi)
|
||||
f.append(ok[s].mean() if s.sum() > 15 else np.nan)
|
||||
a.plot(mc, f, lw=2.0, color=c, label="%d - %d arcmin" % (lo, hi))
|
||||
a.axhline(0.5, color=C_MUTED, lw=1, ls=":")
|
||||
a.text(17.6, 0.53, "50%", fontsize=9, color=C_MUTED)
|
||||
a.set_xlabel("injected G magnitude"); a.set_ylabel("recovery fraction")
|
||||
a.set_ylim(0, 1.05)
|
||||
a.set_title("a) artificial-star completeness by projected radius", fontsize=10.5, loc="left")
|
||||
a.legend(fontsize=9, title="projected radius", title_fontsize=9, labelcolor=C_INK)
|
||||
|
||||
b = ax[1]
|
||||
rb = np.array([0, 1, 2, 3, 4, 6, 8, 11, 15, 20, 30])
|
||||
rc = 0.5 * (rb[1:] + rb[:-1])
|
||||
for mlo, mhi, c, ls in ((17.5, 19.0, C_KNOWN, "-"), (19.0, 20.0, C_CAND, "-"),
|
||||
(20.0, 21.0, C_EXT, "-"), (21.0, 21.5, C_ACC, "--")):
|
||||
f = []
|
||||
for j in range(len(rb) - 1):
|
||||
s = (mag >= mlo) & (mag < mhi) & (rad >= rb[j]) & (rad < rb[j + 1])
|
||||
f.append(ok[s].mean() if s.sum() > 15 else np.nan)
|
||||
b.plot(rc, f, lw=2.0, color=c, ls=ls, label="G %.1f - %.1f" % (mlo, mhi))
|
||||
b.set_xlabel("projected radius (arcmin)"); b.set_ylabel("recovery fraction")
|
||||
b.set_ylim(0, 1.05); b.set_xscale("log")
|
||||
b.set_xticks([1, 2, 3, 5, 10, 20]); b.set_xticklabels(["1", "2", "3", "5", "10", "20"])
|
||||
b.set_title("b) the same, as a function of radius", fontsize=10.5, loc="left")
|
||||
b.legend(fontsize=9, labelcolor=C_INK)
|
||||
fig.suptitle("Completeness: how much harder is a cluster to find near the nucleus?",
|
||||
fontsize=12.5, x=0.008, ha="left")
|
||||
fig.tight_layout()
|
||||
fig.savefig(layout.path("NGC5128-gc-completeness.png"), dpi=115, bbox_inches="tight",
|
||||
facecolor=SURF)
|
||||
plt.close(fig)
|
||||
print("wrote NGC5128-gc-completeness.png")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
def annulus_area(rb, cx, cy, nx=4788, ny=3194, step=4):
|
||||
"""effective area of each annulus that actually lies on the detector"""
|
||||
yy, xx = np.mgrid[0:ny:step, 0:nx:step]
|
||||
r = np.hypot(xx - cx, yy - cy) * PIXSCALE / 60.0
|
||||
px_area = (step * PIXSCALE / 60.0) ** 2
|
||||
return np.array([((r >= rb[i]) & (r < rb[i + 1])).sum() * px_area
|
||||
for i in range(len(rb) - 1)]), r
|
||||
|
||||
|
||||
def completeness_of(cp, mag, rad):
|
||||
"""Completeness for each object, interpolated from the artificial-star grid.
|
||||
|
||||
A 2D lookup on (magnitude, radius). No luminosity function is assumed: each
|
||||
surviving candidate is weighted by 1/C, the standard inverse-completeness
|
||||
(Vmax-style) estimator.
|
||||
"""
|
||||
fm, fr, fok = cp["mag"], cp["rad"], cp["ok"]
|
||||
mb = np.arange(17.5, 20.01, 0.5)
|
||||
rbg = np.array([0, 1.5, 2.25, 3, 4, 5, 6, 8, 11, 15, 20, 30])
|
||||
grid = np.full((len(mb) - 1, len(rbg) - 1), np.nan)
|
||||
for i in range(len(mb) - 1):
|
||||
for j in range(len(rbg) - 1):
|
||||
s = ((fm >= mb[i]) & (fm < mb[i + 1]) & (fr >= rbg[j]) & (fr < rbg[j + 1]))
|
||||
if s.sum() >= 15:
|
||||
grid[i, j] = fok[s].mean()
|
||||
# completeness is a strong function of magnitude and a weak one of radius
|
||||
# outside the innermost annuli, so fill empty cells with the row median
|
||||
for i in range(grid.shape[0]):
|
||||
row = grid[i]
|
||||
if np.isfinite(row).any():
|
||||
row[~np.isfinite(row)] = np.nanmedian(row)
|
||||
mi = np.clip(np.digitize(mag, mb) - 1, 0, len(mb) - 2)
|
||||
ri = np.clip(np.digitize(rad, rbg) - 1, 0, len(rbg) - 2)
|
||||
return grid[mi, ri], grid
|
||||
|
||||
|
||||
def fig_radial(d, cp):
|
||||
hdr = fits.getheader(layout.path("master-Luminance.fit"))
|
||||
w = WCS(hdr)
|
||||
cx, cy = [float(v) for v in w.all_world2pix(NUC_RA, NUC_DEC, 0)]
|
||||
rb = np.array([0, 1.5, 3, 4.5, 6, 8, 11, 15, 20, 27])
|
||||
rc = 0.5 * (rb[1:] + rb[:-1])
|
||||
area, _ = annulus_area(rb, cx, cy)
|
||||
|
||||
r = d["r_arcmin"]
|
||||
mag = d["mag"]
|
||||
# Magnitude-limited sample: G < 19.5, where the measured completeness is
|
||||
# 25-70% and the 1/C weights are therefore stable.
|
||||
MLIM = 19.5
|
||||
lim = mag < MLIM
|
||||
cand = d["cand"] & lim
|
||||
ext = (d["cls"] == "extended") & lim
|
||||
star = (d["cls"] == "foreground-star") & lim
|
||||
|
||||
C, grid = completeness_of(cp, mag, r)
|
||||
usable = np.isfinite(C) & (C > 0.15)
|
||||
wt = np.where(usable, 1.0 / np.clip(C, 0.15, None), 0.0)
|
||||
print("median completeness of the candidate sample: %.2f (%d of %d usable)"
|
||||
% (np.nanmedian(C[cand]), (cand & usable).sum(), cand.sum()))
|
||||
|
||||
def prof(mask, weights=None):
|
||||
n, sw = [], []
|
||||
for i in range(len(rb) - 1):
|
||||
s = mask & (r >= rb[i]) & (r < rb[i + 1])
|
||||
n.append(s.sum())
|
||||
sw.append(np.sum(weights[s]) if weights is not None else s.sum())
|
||||
n = np.array(n, float); sw = np.array(sw, float)
|
||||
with np.errstate(divide="ignore", invalid="ignore"):
|
||||
mw = np.where(n > 0, sw / np.maximum(n, 1), 0.0) # mean weight
|
||||
return n, sw / area, mw * np.sqrt(n) / area # Poisson error
|
||||
|
||||
n_c, s_c, e_c = prof(cand)
|
||||
n_cc, s_cc, e_cc = prof(cand & usable, wt)
|
||||
n_e, s_e, e_e = prof(ext)
|
||||
n_s, s_s, e_s = prof(star)
|
||||
# The SAME correction applied to the foreground stars. Milky Way stars are
|
||||
# uniformly distributed on this scale, so their corrected profile MUST come
|
||||
# out flat. That is the check on the whole correction: if it does not
|
||||
# flatten them, the correction is wrong and so is the candidate profile.
|
||||
n_sc, s_sc, e_sc = prof(star & usable, wt)
|
||||
corr = np.where(s_c > 0, s_cc / np.maximum(s_c, 1e-12), np.nan)
|
||||
|
||||
# outer-field level from the two outermost annuli of the corrected profile
|
||||
denom = np.nansum(area[-2:])
|
||||
bg = np.nansum(s_cc[-2:] * area[-2:]) / denom
|
||||
bg_e = np.sqrt(np.nansum(e_cc[-2:] ** 2 * area[-2:] ** 2)) / denom
|
||||
|
||||
fig, ax = plt.subplots(1, 2, figsize=(13.2, 5.6))
|
||||
a = ax[0]
|
||||
a.errorbar(rc, s_c, yerr=e_c, color=C_MUTED, lw=1.4, marker="o", ms=6,
|
||||
capsize=3, label="candidates, raw counts", ls="--", zorder=3)
|
||||
a.errorbar(rc, s_cc, yerr=e_cc, color=C_CAND, lw=2.4, marker="o", ms=8,
|
||||
capsize=3, label="candidates, completeness-corrected", zorder=5)
|
||||
a.errorbar(rc, s_e, yerr=e_e, color=C_EXT, lw=1.8, marker="s", ms=6,
|
||||
capsize=3, label="extended sources, raw", zorder=4)
|
||||
a.errorbar(rc, s_sc / 20.0, yerr=e_sc / 20.0, color=C_KNOWN, lw=1.8, marker="^",
|
||||
ms=6, capsize=3, label="foreground stars / 20, corrected", zorder=2)
|
||||
a.errorbar(rc, s_s / 20.0, yerr=e_s / 20.0, color=C_KNOWN, lw=1.1, marker="^",
|
||||
ms=4, capsize=2, ls=":", alpha=0.55,
|
||||
label="foreground stars / 20, raw", zorder=1)
|
||||
a.axhline(bg, color=C_ACC, lw=1.4, ls="-.")
|
||||
a.fill_between([0.8, 30], bg - bg_e, bg + bg_e, color=C_ACC, alpha=0.15, lw=0)
|
||||
a.annotate("outer-field level", xy=(23, bg), xytext=(23, bg * 0.42),
|
||||
color=C_ACC, fontsize=8.5, ha="center",
|
||||
arrowprops=dict(arrowstyle="->", color=C_ACC, lw=1))
|
||||
a.set_xscale("log"); a.set_yscale("log")
|
||||
a.set_xlim(0.8, 30)
|
||||
a.set_xticks([1, 2, 3, 5, 10, 20]); a.set_xticklabels(["1", "2", "3", "5", "10", "20"])
|
||||
a.set_xlabel("projected radius from the nucleus (arcmin)")
|
||||
a.set_ylabel("surface density (objects per arcmin$^2$)")
|
||||
a.set_title("a) radial surface density, G < 19.5", fontsize=10.5, loc="left")
|
||||
a.legend(fontsize=8.6, loc="lower left", labelcolor=C_INK)
|
||||
sec = a.secondary_xaxis("top", functions=(lambda v: v * KPC_PER_ARCMIN,
|
||||
lambda v: v / KPC_PER_ARCMIN))
|
||||
sec.set_xlabel("projected radius (kpc at 3.8 Mpc)", fontsize=9.5)
|
||||
|
||||
# panel b: background-subtracted, with a power law fit
|
||||
b = ax[1]
|
||||
excess = s_cc - bg
|
||||
ee = np.sqrt(e_cc ** 2 + bg_e ** 2)
|
||||
ok = np.isfinite(excess) & (excess > 0) & (rc < 20)
|
||||
b.errorbar(rc[ok], excess[ok], yerr=ee[ok], color=C_CAND, lw=2.2, marker="o",
|
||||
ms=8, capsize=3, label="candidate excess over the outer field")
|
||||
slope = np.nan
|
||||
if ok.sum() >= 3:
|
||||
p = np.polyfit(np.log10(rc[ok]), np.log10(excess[ok]), 1)
|
||||
slope = p[0]
|
||||
xr = np.array([rc[ok].min(), rc[ok].max()])
|
||||
b.plot(xr, 10 ** np.polyval(p, np.log10(xr)), color=C_INK, lw=1.4, ls="--",
|
||||
label=r"power law, $\Sigma \propto R^{%.2f}$" % slope)
|
||||
b.set_xscale("log"); b.set_yscale("log")
|
||||
b.set_xlim(0.8, 30)
|
||||
b.set_xticks([1, 2, 3, 5, 10, 20]); b.set_xticklabels(["1", "2", "3", "5", "10", "20"])
|
||||
b.set_xlabel("projected radius from the nucleus (arcmin)")
|
||||
b.set_ylabel(r"excess surface density (arcmin$^{-2}$)")
|
||||
b.set_title("b) excess over the outer field", fontsize=10.5, loc="left")
|
||||
b.legend(fontsize=9, labelcolor=C_INK)
|
||||
|
||||
fig.suptitle("Are the candidates concentrated on NGC 5128?", fontsize=13,
|
||||
x=0.008, ha="left")
|
||||
fig.tight_layout()
|
||||
fig.savefig(layout.path("NGC5128-gc-radial-profile.png"), dpi=115,
|
||||
bbox_inches="tight", facecolor=SURF)
|
||||
plt.close(fig)
|
||||
print("wrote NGC5128-gc-radial-profile.png")
|
||||
|
||||
print("\nRADIAL PROFILE TABLE (G < 19.5)")
|
||||
print("%6s %6s %5s %8s %7s %10s %10s %10s"
|
||||
% ("r_in", "r_out", "N", "area", "medC", "sig_raw", "sig_corr", "sig_ext"))
|
||||
for i in range(len(rc)):
|
||||
s = cand & (r >= rb[i]) & (r < rb[i + 1])
|
||||
mc = np.nanmedian(C[s]) if s.sum() else np.nan
|
||||
print("%6.1f %6.1f %5d %8.2f %7.2f %10.4f %10.4f %10.4f"
|
||||
% (rb[i], rb[i + 1], n_c[i], area[i], mc, s_c[i], s_cc[i], s_e[i]))
|
||||
print("outer-field level %.4f +/- %.4f arcmin^-2 ; power-law slope %.2f"
|
||||
% (bg, bg_e, slope))
|
||||
print("corrected density at 1.5-3' / outer-field level = %.1f"
|
||||
% (s_cc[1] / bg if bg > 0 else np.nan))
|
||||
ei = np.nansum(n_e[:4]) / np.nansum(area[:4])
|
||||
eo = np.nansum(n_e[6:]) / np.nansum(area[6:])
|
||||
si = np.nansum(n_s[:4]) / np.nansum(area[:4])
|
||||
so = np.nansum(n_s[6:]) / np.nansum(area[6:])
|
||||
print("extended sources: inner(<6') %.4f outer(>11') %.4f ratio %.2f"
|
||||
% (ei, eo, ei / eo if eo else np.nan))
|
||||
print("foreground stars: inner(<6') %.4f outer(>11') %.4f ratio %.2f"
|
||||
% (si, so, si / so if so else np.nan))
|
||||
fin = np.nansum(s_sc[1:4] * area[1:4]) / np.nansum(area[1:4])
|
||||
fout = np.nansum(s_sc[6:] * area[6:]) / np.nansum(area[6:])
|
||||
print("foreground stars CORRECTED: inner(1.5-6') %.3f outer(>11') %.3f ratio %.2f"
|
||||
% (fin, fout, fin / fout if fout else np.nan))
|
||||
np.savez(layout.path("_gc_profile.npz"), rb=rb, rc=rc, area=area,
|
||||
n_c=n_c, s_c=s_c, e_c=e_c, corr=corr, s_cc=s_cc, e_cc=e_cc,
|
||||
s_e=s_e, s_s=s_s, s_sc=s_sc, bg=bg, bg_e=bg_e, slope=slope, grid=grid)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
d = load_cat()
|
||||
cp = completeness_grid()
|
||||
fig_background(d)
|
||||
fig_finder(d)
|
||||
fig_cmd(d)
|
||||
if cp is not None:
|
||||
fig_completeness(cp)
|
||||
fig_radial(d, cp)
|
||||
Loading…
Add table
Add a link
Reference in a new issue