#!/usr/bin/env python3
"""
plot_widening_ladder.py — the "L0 reactive-widening ladder".

Diagnostic for PPDF-16 (the DIS-only degeneracy verdict). During the full-DIS
L0 closure gate, UltraNest's reactive strategy kept judging the effective
sample size insufficient and doubling the live-point population — 300 → 600 →
1200 → 2400 — burning iterations at each level without ever converging. That
runaway escalation IS the confirmation of a structurally under-constrained
(degenerate) model: on a bowl-shaped posterior the sampler settles; on a
posterior riddled with flat directions it never does.

Data (hardcoded from the L0 log; see context/RESULTS_LEDGER.md, PPDF-16):
    live points N  vs  cumulative iterations at each widening round
        N=300  -> 15100
        N=600  -> 18500
        N=1200 -> 43000
        N=2400 -> 56300

Output: site/assets/img/ppdf16_widening_ladder.png
"""

import os
import matplotlib

matplotlib.use("Agg")
import matplotlib.pyplot as plt
from matplotlib.ticker import FuncFormatter

# ---- site palette (assets/site.css :root) -------------------------------
TEAL = "#0e7c7b"
TEAL_DARK = "#0e5453"
ORANGE = "#d96c2c"
INK = "#16222e"
BODY = "#37424e"
MUTED = "#4a5866"
FAINT = "#8a94a0"
BG = "#fdfcfa"
PANEL = "#f2efe9"
LINE = "#d5cfc2"

# ---- data (widening ladder) ---------------------------------------------
live_points = [300, 600, 1200, 2400]      # N at each reactive-widening round
cum_iters = [15100, 18500, 43000, 56300]  # cumulative UltraNest iterations
# iterations spent *at* each level = successive differences of the cumulative
iters_at_level = [cum_iters[0]] + [
    cum_iters[i] - cum_iters[i - 1] for i in range(1, len(cum_iters))
]

# ---- figure -------------------------------------------------------------
plt.rcParams.update(
    {
        "font.family": "DejaVu Sans",
        "font.size": 12,
        "axes.edgecolor": LINE,
        "axes.linewidth": 1.0,
        "figure.facecolor": BG,
        "axes.facecolor": BG,
    }
)

fig, ax = plt.subplots(figsize=(9.6, 5.6))

x = list(range(len(live_points)))
labels = [f"N = {n}" for n in live_points]

# bar per widening round, height = cumulative iterations reached at that round.
# colour deepens toward orange as the escalation runs away (never converges).
bar_colors = [TEAL, TEAL_DARK, "#a15a1f", ORANGE]
bars = ax.bar(
    x,
    cum_iters,
    width=0.62,
    color=bar_colors,
    edgecolor="white",
    linewidth=1.5,
    zorder=3,
)

# stepped "escalation" line tracing the top of the ladder (post step: the
# population doubling happens between rounds).
ax.step(
    [xi + 0.5 for xi in [-1] + x],
    [cum_iters[0]] + cum_iters,
    where="pre",
    color=INK,
    linewidth=1.6,
    linestyle="--",
    alpha=0.55,
    zorder=4,
)

# annotate each bar: live-point count (bottom) + iterations spent at that level
for xi, n, cum, spent, col in zip(x, live_points, cum_iters, iters_at_level, bar_colors):
    # cumulative total at top of the bar
    ax.text(
        xi,
        cum + 1400,
        f"{cum:,}\niterations",
        ha="center",
        va="bottom",
        fontsize=11.5,
        color=INK,
        fontweight="bold",
        linespacing=1.15,
    )
    # "+spent this round" inside the bar, near the top
    if xi > 0:
        ax.text(
            xi,
            cum - 3200,
            f"+{spent:,}\nthis round",
            ha="center",
            va="top",
            fontsize=10,
            color="white",
            linespacing=1.1,
        )

# the doubling arrows between rounds — the "reactive widening" itself.
# label sits just below the arrow midpoint to stay clear of the bar-top totals.
for i in range(len(x) - 1):
    mid_y = (cum_iters[i] + cum_iters[i + 1]) / 2
    ax.annotate(
        "live points\ndoubled",
        xy=(x[i] + 0.5, mid_y - 4200),
        ha="center",
        va="top",
        fontsize=9,
        color=ORANGE,
        fontweight="bold",
        linespacing=1.05,
    )
    ax.annotate(
        "",
        xy=(x[i + 1] - 0.30, cum_iters[i + 1] * 0.985),
        xytext=(x[i] + 0.30, cum_iters[i] * 1.02),
        arrowprops=dict(arrowstyle="-|>", color=ORANGE, lw=1.6, alpha=0.9),
        zorder=5,
    )

# "killed by decision — never converged" flag at the final round.
# offset left so the arrow doesn't cross the bar-top total.
ax.annotate(
    "killed by decision\n(never converged)",
    xy=(x[-1] - 0.28, cum_iters[-1] * 0.995),
    xytext=(x[-1] - 0.62, cum_iters[-1] + 11500),
    ha="center",
    va="bottom",
    fontsize=10.5,
    color=ORANGE,
    fontweight="bold",
    linespacing=1.15,
    arrowprops=dict(arrowstyle="-|>", color=ORANGE, lw=1.8),
)

# axes cosmetics
ax.set_xticks(x)
ax.set_xticklabels(labels, fontsize=13, color=INK, fontweight="bold")
ax.set_xlim(-0.75, len(x) - 0.25)
ax.set_ylim(0, cum_iters[-1] * 1.40)
ax.set_ylabel("cumulative UltraNest iterations", fontsize=12.5, color=MUTED)
ax.set_xlabel("reactive-widening round  →  live-point population", fontsize=12.5, color=MUTED)

ax.yaxis.set_major_formatter(FuncFormatter(lambda v, _: f"{int(v/1000)}k"))
ax.tick_params(axis="y", colors=MUTED, labelsize=11)
ax.tick_params(axis="x", length=0)

for spine in ("top", "right"):
    ax.spines[spine].set_visible(False)
ax.grid(axis="y", color=LINE, linewidth=0.7, alpha=0.55, zorder=0)
ax.set_axisbelow(True)

ax.set_title(
    "L0 discriminator: reactive widening never converged (zero-noise closure)",
    fontsize=14.5,
    color=INK,
    fontweight="bold",
    pad=16,
)

# subtitle / reading aid
fig.text(
    0.5,
    0.005,
    "Full-DIS 52-parameter MSHT fit · escalating live points is the tell of a degenerate, flat-direction posterior — not a sampler setting to tune.",
    ha="center",
    va="bottom",
    fontsize=9.5,
    color=FAINT,
)

fig.tight_layout(rect=(0, 0.03, 1, 1))

# ---- save ---------------------------------------------------------------
out_dir = os.path.join(
    os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
    "site",
    "assets",
    "img",
)
os.makedirs(out_dir, exist_ok=True)
out_path = os.path.join(out_dir, "ppdf16_widening_ladder.png")
fig.savefig(out_path, dpi=150, facecolor=BG, bbox_inches="tight")
print(f"wrote {out_path}")
