# ── Bootstrap: SST / Spinal Cord (same as a1 in zoom figure) ────────────────
bs_sorted = np.sort(bootstraps[('SST', 'Spinal Cord')])
_chop = int(np.ceil(len(bs_sorted) * 0.025))
bs_c = bs_sorted[_chop:-_chop]
# ── fv for each variant ───────────────────────────────────────────────────────
def make_fv_v1(bs_c, N=N_PIX):
ranks = np.linspace(0, len(bs_c), N, dtype=int); ranks[0] = 1
fv = np.array([bs_c[r-1] for r in ranks])
if sum(v > 0 for v in bs_c) < len(bs_c) / 2:
fv = fv[::-1]
return fv
def make_fv_v2(bs_c, N=N_PIX):
ranks = np.linspace(0, len(bs_c), N, dtype=int); ranks[0] = 1
all_q = np.array([bs_c[r-1] for r in ranks])
mid = N // 2
interleaved = [all_q[mid]]
lo, hi = mid, mid
for _ in range(mid):
lo -= 1; interleaved.append(all_q[lo])
hi += 1; interleaved.append(all_q[hi])
return np.array(interleaved)
def make_fv_v3(bs_c, N=N_PIX):
mean = np.mean(bs_c)
dev_order = np.argsort(np.abs(bs_c - mean))
sorted_s = bs_c[dev_order]
indices = np.linspace(0, len(sorted_s)-1, N, dtype=int)
return sorted_s[indices]
FV_LIST = [make_fv_v1(bs_c), make_fv_v2(bs_c), make_fv_v3(bs_c)]
PATHS_LIST = [PATH_OUT_IN, PATH_IN_OUT, PATH_IN_OUT]
DNORM = NORM # same colormap/norm as spiral_variants_zoom.png
# ── Helpers ───────────────────────────────────────────────────────────────────
def spiralize_grid(fv, path, n=7):
g = np.full((n, n), np.nan)
for i, (r, c) in enumerate(path):
g[r, c] = fv[i]
return g
def draw_ring_arrows(ax, ring_k, sz, avg_val, clockwise=True):
"""clockwise=True → V1 (outside-in); False → V2/V3 (inside-out)."""
mid = ring_k + sz // 2
bg = CMAP(DNORM(avg_val))
col = "w" if (0.299*bg[0]+0.587*bg[1]+0.114*bg[2]) < 0.52 else "k"
ap = dict(arrowstyle="->", color=col, lw=1.0, mutation_scale=9)
d, e = 0.25, 0.6
if clockwise:
ax.annotate("", xy=(mid+e, ring_k-d), xytext=(mid-e, ring_k-d), arrowprops=ap, zorder=8, annotation_clip=False)
ax.annotate("", xy=(ring_k+sz-1+d, mid+e), xytext=(ring_k+sz-1+d, mid-e), arrowprops=ap, zorder=8, annotation_clip=False)
ax.annotate("", xy=(mid-e, ring_k+sz-1+d), xytext=(mid+e, ring_k+sz-1+d), arrowprops=ap, zorder=8, annotation_clip=False)
ax.annotate("", xy=(ring_k-d, mid-e), xytext=(ring_k-d, mid+e), arrowprops=ap, zorder=8, annotation_clip=False)
else:
ax.annotate("", xy=(mid-e, ring_k-d), xytext=(mid+e, ring_k-d), arrowprops=ap, zorder=8, annotation_clip=False)
ax.annotate("", xy=(ring_k+sz-1+d, mid-e), xytext=(ring_k+sz-1+d, mid+e), arrowprops=ap, zorder=8, annotation_clip=False)
ax.annotate("", xy=(mid+e, ring_k+sz-1+d), xytext=(mid-e, ring_k+sz-1+d), arrowprops=ap, zorder=8, annotation_clip=False)
ax.annotate("", xy=(ring_k-d, mid+e), xytext=(ring_k-d, mid-e), arrowprops=ap, zorder=8, annotation_clip=False)
def draw_algo_cell(ax, fv, path, title, clockwise=True):
n = 7
fv_s = np.sort(fv)
grid = spiralize_grid(fv, path, n)
ax.imshow(grid, cmap=CMAP, norm=DNORM, origin="upper",
interpolation="nearest", aspect="equal")
# value rank of each pixel (q0 = smallest value, q48 = largest)
val_rank = np.argsort(np.argsort(fv))
# ring arrows
start = 0
for k in range(n // 2 + 1):
sz = n - 2*k
if sz <= 0: break
end = start + (4*(sz-1) if sz > 1 else 1)
if sz > 1:
avg = float(np.mean(fv_s[start:min(end, len(fv_s))]))
draw_ring_arrows(ax, k, sz, avg, clockwise=clockwise)
start = end
# labels: q{value_rank} at each pixel
for k, (r, c) in enumerate(path):
vr = val_rank[k]
val = float(fv[k])
bg = CMAP(DNORM(val))
lum = 0.299*bg[0]+0.587*bg[1]+0.114*bg[2]
tc = "w" if lum < 0.52 else "k"
ax.text(c, r, f"q{vr}\n{val:.1f}",
ha="center", va="center", fontsize=4.5, color=tc,
zorder=10, linespacing=1.1, multialignment="center")
ax.set_xlim(-0.5, n-0.5); ax.set_ylim(n-0.5, -0.5)
ax.axis("off")
ax.set_title(title, fontsize=8.5, pad=4)
def draw_shared_hist(ax, bs_c, fv):
"""Single histogram shared by all variants.
q-labels are value rank: q0 = leftmost (smallest), q48 = rightmost (largest)."""
N = len(fv)
fv_s = np.sort(fv)
span = fv_s[-1] - fv_s[0]
xl0 = fv_s[0] - span*0.05
xl1 = fv_s[-1] + span*0.05
BW = (xl1 - xl0) / 36
bins = np.arange(xl0, xl1+BW, BW)
counts, _ = np.histogram(np.clip(bs_c, xl0, xl1), bins=bins, density=True)
# segment boundaries = midpoints between consecutive sorted fv values
seg_bounds = np.empty(N+1)
seg_bounds[0] = xl0; seg_bounds[-1] = xl1
for k in range(1, N):
seg_bounds[k] = (fv_s[k-1]+fv_s[k]) / 2.0
seg_cols = [CMAP(DNORM(float(v))) for v in fv_s]
best_piece = {}
for lo, hi, ht in zip(bins[:-1], bins[1:], counts):
if ht == 0: continue
y_bot = 0.0
for k in range(N):
ov = max(0., min(hi, seg_bounds[k+1]) - max(lo, seg_bounds[k]))
if ov <= 0: continue
h = ht * ov / (hi-lo)
ax.bar(lo, h, width=hi-lo, bottom=y_bot, align="edge",
color=seg_cols[k], edgecolor="none")
if k not in best_piece or h > best_piece[k][2]:
best_piece[k] = (lo+(hi-lo)/2, y_bot, h)
y_bot += h
# q-labels by value rank (q0 = leftmost bar)
for k, (xc, yb, h) in best_piece.items():
if h < counts.max()*0.005: continue
col = seg_cols[k]; lum = 0.299*col[0]+0.587*col[1]+0.114*col[2]
tc = "w" if lum < 0.52 else "k"
ax.text(xc, yb+h/2, f"q{k}\n{fv_s[k]:.1f}",
ha="center", va="center", fontsize=3.5, color=tc,
zorder=11, linespacing=1.0, multialignment="center")
ax.set_xlim(fv_s[0]-span*0.06, fv_s[-1]+span*0.06)
ax.set_ylim(0, counts.max()*1.08)
for sp in ["left","right","top"]: ax.spines[sp].set_visible(False)
ax.set_yticks([]); ax.tick_params(labelsize=8)
ax.set_xlabel("Bootstrap mean difference (SST / Spinal Cord)", fontsize=8)
ax.set_title("Bootstrap distribution segmented by quantile rank\n"
"(q0 = smallest value, q48 = largest; bar colours match pixel colours below)",
fontsize=8.5)
# ── Figure: histogram on top, three cells below ───────────────────────────────
from matplotlib.gridspec import GridSpec
fig = plt.figure(figsize=(13, 8))
gs = GridSpec(2, 3, figure=fig,
height_ratios=[1, 1.6],
hspace=0.38, wspace=0.12)
ax_hist = fig.add_subplot(gs[0, :]) # full-width top row
draw_shared_hist(ax_hist, bs_c, FV_LIST[0]) # V1 fv = cleanest q ordering
TITLES_CELL = [
"V1 — outside-in\n(q0 = outer ring, q48 = centre)",
"V2 — inside-out symmetric\n(q24 = centre, q0/q48 = outer ring)",
"V3 — inside-out deviation\n(q~24 = centre, q0/q48 = outer ring)",
]
CLOCKWISE = [True, False, False]
for col, (fv, path, tc, cw) in enumerate(
zip(FV_LIST, PATHS_LIST, TITLES_CELL, CLOCKWISE)):
ax = fig.add_subplot(gs[1, col])
draw_algo_cell(ax, fv, path, tc, clockwise=cw)
plt.savefig("algorithm_explanation.png", dpi=600, bbox_inches="tight")
plt.close()