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