Commit 00a81b9e authored by Vacaliuc, Bogdan's avatar Vacaliuc, Bogdan
Browse files

slides: add slide 4 — mock R(Q) showing GUI vs autoreduce curves



Visceral visual companion to slide 3: two offset reflectivity curves
on log-log axes for the same synthetic REF_M run, annotated with
the three mechanisms that make them disagree — edge cropping
(2 Q-bins at each end), ErrorWeighted background over-subtracting
at high Q, and the ~4% multiplicative QuickNXS post-hoc scale.

Generated by gen_slide_4.py (matplotlib). Uses DejaVu Sans so there
are no font-missing warnings at render time. Effect sizes are slightly
exaggerated for legibility (stated in the footer).

The right panel explains each mechanism in plain prose, and the
bottom summary box carries the key insight: "None of the differences
is an outright bug. Each is a defensible choice. The debt is that
the two paths silently chose different defaults, with no decision
record."

Co-Authored-By: default avatarClaude Opus 4.7 (1M context) <noreply@anthropic.com>
parent 808a4b20
Loading
Loading
Loading
Loading
+207 −0
Original line number Diff line number Diff line
#!/usr/bin/env python3
"""Generate slide 4: mock R(Q) showing GUI vs autoreduce curves.

Produces a 1920x1080 SVG with reflectivity curves that differ in the
ways documented in 10-mantid-algorithms-deep-dive.md:
  * a ~2% multiplicative offset (QuickNXS post-hoc scaling difference)
  * a low-S/N shift from the ErrorWeightedBackground tension
  * 2 Q-bins dropped at each end (CropFirstAndLastPoints tension)

Differences in the curves are slightly exaggerated for legibility.
Run: /tmp/slides-venv/bin/python gen_slide_4.py
"""

from pathlib import Path

import matplotlib
matplotlib.use("Agg")
import matplotlib.pyplot as plt
import numpy as np

FAMILY = "DejaVu Sans"  # matplotlib's default; ships everywhere
FIG_W_IN, FIG_H_IN = 19.2, 10.8  # 1920x1080 at 100 dpi

fig = plt.figure(figsize=(FIG_W_IN, FIG_H_IN), dpi=100)
fig.patch.set_facecolor("#FAFAFA")

# ---- header band ---------------------------------------------------------
header_ax = fig.add_axes([0.0, 0.907, 1.0, 0.093])
header_ax.set_facecolor("#002E5D")
header_ax.set_xticks([]); header_ax.set_yticks([])
for s in header_ax.spines.values():
    s.set_visible(False)
header_ax.text(0.5, 0.63, "What Scientists See Today — Same Data, Different R(Q)",
               color="white", fontsize=26, fontweight="bold",
               ha="center", va="center", family=FAMILY)
header_ax.text(0.5, 0.22,
               "The GUI and the autoreduce pipeline produce numerically different reflectivity curves for the same run",
               color="#B8D4E8", fontsize=15, ha="center", va="center", family=FAMILY)

# ---- footer band ---------------------------------------------------------
footer_ax = fig.add_axes([0.0, 0.0, 1.0, 0.045])
footer_ax.set_facecolor("#001A35")
footer_ax.set_xticks([]); footer_ax.set_yticks([])
for s in footer_ax.spines.values():
    s.set_visible(False)
footer_ax.text(0.012, 0.5,
               "Illustrative curves (not real data); effect size slightly exaggerated for legibility — all three mechanisms documented in 10-mantid-algorithms-deep-dive.md §11",
               color="#6A90B0", fontsize=10.5, ha="left", va="center", family=FAMILY)
footer_ax.text(0.988, 0.5, "Reflectometry Hack-a-thon 2026 · slide 4 of 8",
               color="#6A90B0", fontsize=11, ha="right", va="center", family=FAMILY)

# ---- synthesize a reflectivity curve -------------------------------------
Q = np.geomspace(0.005, 0.14, 220)
Q_c = 0.0085
Rf = np.where(Q <= Q_c, 1.0,
              (Q_c / Q) ** 4 *
              (1 + 0.30 * np.cos(8 * np.pi * Q / Q_c + 0.5) *
               np.exp(-60 * (Q - Q_c))))
edge = 0.5 * (1 - np.tanh((Q - Q_c) / 0.0006))
R_true = edge * 1.0 + (1 - edge) * Rf
R_true = np.clip(R_true, 1e-7, 1.5)

N_counts = np.maximum(R_true * 2e6 * Q ** 0.5, 2.0)
R_err = R_true / np.sqrt(N_counts) + 0.02 * R_true

rng = np.random.default_rng(42)
noise = rng.normal(0, 1, size=Q.shape) * R_err
R_base = np.clip(R_true + noise, 1e-8, 2.0)

# --- GUI path ---
R_gui = R_base.copy()

# --- autoreduce path: ---
# 1) 4% multiplicative shift (QuickNXS-compat scale factor difference)
# 2) low-S/N bias growing with Q (ErrorWeightedBackground over-subtracts)
# 3) first and last 2 Q points dropped (CropFirstAndLastPoints=True)
scale_factor_shift = 1.04
lowSN_bias = 1.0 - 0.12 * np.clip((Q - 0.045) / 0.07, 0, 1) ** 2
R_auto = R_base * scale_factor_shift * lowSN_bias
R_auto[:2] = np.nan
R_auto[-2:] = np.nan

# ---- Plot ----------------------------------------------------------------
plot_ax = fig.add_axes([0.048, 0.095, 0.58, 0.77])
plot_ax.set_facecolor("#FFFFFF")

plot_ax.errorbar(Q, R_gui, yerr=R_err, fmt="o", ms=5.5, mew=0,
                 color="#004B8D", ecolor="#7EB0DB", elinewidth=0.8,
                 capsize=0, alpha=0.9, label="quicknxsv2 (GUI path)", zorder=4)
plot_ax.errorbar(Q, R_auto, yerr=R_err * 0.98, fmt="s", ms=5.5, mew=0,
                 color="#8C3A00", ecolor="#EFA26A", elinewidth=0.8,
                 capsize=0, alpha=0.9, label="mr_reduction (autoreduce path)", zorder=3)
plot_ax.plot(Q, R_true, color="#909090", linestyle="--", linewidth=1.4,
             alpha=0.6, label="underlying truth", zorder=2)

plot_ax.set_xscale("log")
plot_ax.set_yscale("log")
plot_ax.set_xlim(0.005, 0.15)
plot_ax.set_ylim(4e-6, 2.0)
plot_ax.set_xlabel(r"$Q_z \ \ [\mathrm{\AA^{-1}}]$", fontsize=17, family=FAMILY, labelpad=8)
plot_ax.set_ylabel(r"Reflectivity $R(Q_z)$", fontsize=17, family=FAMILY, labelpad=8)
plot_ax.grid(True, which="both", alpha=0.22)
plot_ax.tick_params(labelsize=13)
leg = plot_ax.legend(loc="upper right", fontsize=13.5, frameon=True,
                     framealpha=0.97, edgecolor="#BBBBBB")
leg.get_frame().set_boxstyle("round,pad=0.3")

# Highlight the high-Q disagreement region
plot_ax.axvspan(0.055, 0.15, facecolor="#FFEFE3", alpha=0.45, zorder=0)
plot_ax.text(0.085, 1.2, "high-Q: where the curves visibly diverge",
             fontsize=11.5, color="#8C3A00", ha="center", va="top",
             style="italic", family=FAMILY)

# ---- Annotation arrows ---------------------------------------------------
# 1) ~4% multiplicative scale gap
i85 = np.argmin(abs(Q - 0.085))
plot_ax.annotate("", xy=(0.085, R_gui[i85]), xytext=(0.085, R_auto[i85]),
                 arrowprops=dict(arrowstyle="<->", color="#002E5D", lw=1.6))
plot_ax.annotate("~4% multiplicative\nQuickNXS post-hoc scale",
                 xy=(0.105, np.sqrt(R_gui[i85] * R_auto[i85])),
                 fontsize=11.5, color="#002E5D", ha="left", va="center",
                 fontweight="bold", family=FAMILY,
                 bbox=dict(boxstyle="round,pad=0.3", fc="#EEF7FB",
                           ec="#4A90C2", lw=0.8))

# 2) Edge-cropping (first two points)
plot_ax.annotate("autoreduce drops first\nand last 2 Q bins",
                 xy=(0.0053, R_gui[0]), xytext=(0.008, 1.3e-3),
                 fontsize=11.5, color="#004B8D", family=FAMILY,
                 arrowprops=dict(arrowstyle="->", color="#004B8D", lw=1.3),
                 bbox=dict(boxstyle="round,pad=0.3", fc="#EEF7FB",
                           ec="#4A90C2", lw=0.8))

# 3) Low-S/N divergence
i60 = np.argmin(abs(Q - 0.06))
plot_ax.annotate("ErrorWeighted background\nover-subtracts at low S/N\n→ autoreduce pushed down",
                 xy=(0.06, R_auto[i60]), xytext=(0.008, 5e-5),
                 fontsize=11.5, color="#8C3A00", family=FAMILY,
                 arrowprops=dict(arrowstyle="->", color="#8C3A00", lw=1.3),
                 bbox=dict(boxstyle="round,pad=0.3", fc="#FEEADF",
                           ec="#E87722", lw=0.8))

# ---- Right-hand explanation panel ----------------------------------------
info_ax = fig.add_axes([0.655, 0.095, 0.335, 0.77])
info_ax.axis("off")
info_ax.set_xlim(0, 1); info_ax.set_ylim(0, 1)

# panel title
info_ax.text(0.03, 0.975, "Why the curves disagree",
             fontsize=22, fontweight="bold", color="#002E5D",
             ha="left", va="top", family=FAMILY)
info_ax.plot([0.03, 0.97], [0.935, 0.935], color="#4A90C2", lw=1.5)

blocks = [
    ("1.  Edge cropping", "#E87722",
     "Autoreduce inherits MRR's default\n"
     "CropFirstAndLastPoints=True; the GUI\n"
     "NexusData path sets False.\n"
     "Two Q-bins at each end disappear from the\n"
     "autoreduce curve; stitching across runs then\n"
     "picks up a mismatched overlap region."),
    ("2.  Background estimator", "#E87722",
     "Autoreduce inherits ErrorWeightedBackground=True\n"
     "— inverse-variance mean of the background pixels.\n"
     "The GUI hardcodes False (uniform mean).\n"
     "For Poisson noise, the weighted mean biases\n"
     "toward low-count bins, over-subtracting at high Q."),
    ("3.  Post-hoc scaling", "#E87722",
     "The GUI always applies the QuickNXS v1\n"
     "compatibility scale factor;\n"
     "autoreduce applies it only when emitting\n"
     "a 'quicknxs-mode' reproducibility script.\n"
     "Net: a few-percent multiplicative shift."),
]

y_block = 0.89
for title, colour, body in blocks:
    info_ax.text(0.04, y_block, title,
                 fontsize=15.5, fontweight="bold", color=colour,
                 ha="left", va="top", family=FAMILY)
    info_ax.text(0.06, y_block - 0.048, body,
                 fontsize=11.5, color="#1F3A5F", ha="left", va="top",
                 family=FAMILY)
    y_block -= 0.245

# bottom summary box
summary = plt.Rectangle((0.02, 0.005), 0.96, 0.155,
                         transform=info_ax.transAxes,
                         facecolor="#002E5D", edgecolor="#001A35", lw=1.5)
info_ax.add_patch(summary)
info_ax.text(0.5, 0.12,
             "None of the differences is an outright bug.",
             transform=info_ax.transAxes,
             fontsize=14, fontweight="bold", color="#F5C13A",
             ha="center", va="center", family=FAMILY)
info_ax.text(0.5, 0.075,
             "Each is a defensible choice.  The debt is that the two paths",
             transform=info_ax.transAxes,
             fontsize=12, color="#E0ECF5", ha="center", va="center", family=FAMILY)
info_ax.text(0.5, 0.04,
             "silently chose different defaults, with no decision record.",
             transform=info_ax.transAxes,
             fontsize=12, color="#E0ECF5", ha="center", va="center", family=FAMILY)

out_svg = Path(__file__).parent / "slide-4-curves-disagree.svg"
plt.savefig(out_svg, format="svg", bbox_inches=None, pad_inches=0)
print(f"wrote {out_svg}")
+352 KiB
Loading image diff...
+7515 −0

File added.

Preview size limit exceeded, changes collapsed.