Commit b73f56c8 authored by Vacaliuc, Bogdan's avatar Vacaliuc, Bogdan
Browse files

slides: add slide 7 — technical-debt triage (effort vs payoff)



Scatter plot of all 18 debt items from 11-technical-debt.md §A–§O.
X-axis = effort (30 min → 1 month, log scale), y-axis = scientist-
visible payoff (0-10).

Four quadrants:
  - DO FIRST (cheap + high payoff) — 8 items totaling ~1 day, all
    green. Includes the _as_ints fix, QuickNXS scale consolidation,
    dead-code deletes, typo fixes, BinningType drift.
  - PLAN DURING HACKATHON (costly + high payoff) — 4 items, blue.
    Unified build_mrr_kwargs, equivalence harness, DataInfo
    consolidation, reduction off Qt thread.
  - FOLLOW-ON (Tier 3) — 3 items, orange. Frozen Configuration,
    reactive bus, upstream MRR defaults.
  - DEFER (low payoff) — 3 items, red. Mantid pin alignment,
    staging environment, docstring rewrites.

Right panel summarizes each tier with rationale and rough total-cost
estimate; bottom insight box reads "Tier 1 pays for itself on Day 1.
Tier 2 is the hack-a-thon backbone. Tiers 3–4 flow onto Q3/Q4
roadmap."

Generated by gen_slide_7.py. Label positions chosen to avoid overlap
with quadrant dividers; each label has a semi-transparent white
bbox so it reads cleanly across shaded regions.

Co-Authored-By: default avatarClaude Opus 4.7 (1M context) <noreply@anthropic.com>
parent 9ec1f817
Loading
Loading
Loading
Loading
+228 −0
Original line number Diff line number Diff line
#!/usr/bin/env python3
"""Generate slide 7: debt triage matrix (cost vs scientist-visible payoff).

Each dot is a technical-debt item from 11-technical-debt.md §Q.
X-axis = effort required to resolve (hours to weeks, log-ish).
Y-axis = scientist-visible payoff (how much it improves scientists' daily work).

Quadrants:
  Top-left  (cheap + high payoff) — DO FIRST
  Top-right (costly + high payoff) — PLAN DURING HACK-A-THON
  Bot-left  (cheap + low payoff)  — DO OPPORTUNISTICALLY
  Bot-right (costly + low payoff) — DEFER

Run: /tmp/slides-venv/bin/python gen_slide_7.py
"""

from pathlib import Path

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

FAMILY = "DejaVu Sans"
FIG_W_IN, FIG_H_IN = 19.2, 10.8

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, "Technical-Debt Triage — Effort vs. Scientist Payoff",
               color="white", fontsize=26, fontweight="bold",
               ha="center", va="center", family=FAMILY)
header_ax.text(0.5, 0.22,
               "Where to start: the upper-left quadrant is the ~1-day surgery that removes most silent numerical disagreement",
               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,
               "All 17 items traceable to 11-technical-debt.md §Q — effort estimates are code-reading judgments, not commitments",
               color="#6A90B0", fontsize=11, ha="left", va="center", family=FAMILY)
footer_ax.text(0.988, 0.5, "Reflectometry Hack-a-thon 2026 · slide 7 of 8",
               color="#6A90B0", fontsize=11, ha="right", va="center", family=FAMILY)

# ---- main axes (the scatter matrix) --------------------------------------
ax = fig.add_axes([0.055, 0.09, 0.65, 0.785])
ax.set_facecolor("#FFFFFF")

# Effort axis on log scale: 0.5 = 30 min, 1 = 1 h, 8 = 1 day,
#   40 = 1 week, 200 = 1 month
ax.set_xscale("log")
ax.set_xlim(0.3, 300)
ax.set_ylim(0, 10)

# Axis labels
ax.set_xlabel("Effort to resolve  (hours on the x-axis; anchors shown)",
              fontsize=15, family=FAMILY, labelpad=10)
ax.set_ylabel("Scientist-visible payoff  (0 = internal only, 10 = fixes wrong numbers)",
              fontsize=15, family=FAMILY, labelpad=10)
ax.tick_params(labelsize=12)

# Anchor tick positions
ax.set_xticks([0.5, 1, 2, 4, 8, 40, 160])
ax.set_xticklabels(["30 min", "1 h", "2 h", "½ day", "1 day", "1 week", "1 month"])

# Quadrant shading
ax.axvspan(0.3, 8, facecolor="#E8F3E3", alpha=0.5, zorder=0)
ax.axvspan(8, 300, facecolor="#FFF1E1", alpha=0.5, zorder=0)
ax.axhspan(5, 10, facecolor="#EEF7FB", alpha=0.35, zorder=-1)

ax.axvline(8, color="#888", linestyle="--", linewidth=1.1, alpha=0.6)
ax.axhline(5, color="#888", linestyle="--", linewidth=1.1, alpha=0.6)

# Quadrant labels
ax.text(2.2, 9.6, "DO FIRST\n(cheap + high payoff)",
        fontsize=15, fontweight="bold", color="#1F5B1F",
        ha="center", va="top", family=FAMILY,
        bbox=dict(boxstyle="round,pad=0.35", fc="#F0F7EE", ec="#53A548", lw=1.2))
ax.text(60, 9.6, "PLAN DURING HACKATHON\n(costly + high payoff)",
        fontsize=15, fontweight="bold", color="#002E5D",
        ha="center", va="top", family=FAMILY,
        bbox=dict(boxstyle="round,pad=0.35", fc="#EEF7FB", ec="#4A90C2", lw=1.2))
ax.text(2.2, 0.45, "DO OPPORTUNISTICALLY\n(cheap + low payoff)",
        fontsize=13, color="#3A2C00", ha="center", va="bottom",
        family=FAMILY, style="italic")
ax.text(60, 0.45, "DEFER\n(costly + low payoff)",
        fontsize=13, color="#8C3A00", ha="center", va="bottom",
        family=FAMILY, style="italic")

# ---- data: (x_hours, y_payoff, label, category) --------------------------
#   categories: A=duplicated, B=dead, C=hardcoded, D=defaults, F=type-drift,
#               G=globals, H=qt, J=docs, L=tests
items = [
    # (effort_hours, payoff, label, color, size)
    (0.5,  6.5, "1.  Unify two _as_ints",            "#53A548", 280),
    (2.0,  7.0, "2.  Consolidate QuickNXS scale ×3", "#53A548", 300),
    (2.0,  4.5, "3.  Delete 2 dup SingleReadoutDTC", "#53A548", 240),
    (1.0,  2.5, "4.  Delete dead peak_finding copy", "#53A548", 220),
    (1.0,  2.0, "5.  Delete dead dead_time copy",    "#53A548", 200),
    (2.0,  3.5, "6.  Fix BinningType int/enum drift","#53A548", 220),
    (2.0,  2.5, "7.  Rename use_dangle→use_sangle",  "#53A548", 200),
    (2.0,  1.5, "8.  Fix typos (\"No valida\"…)",    "#53A548", 180),

    (12,   9.5, "9.  Unified build_mrr_kwargs() fn", "#4A90C2", 420),
    (24,   8.5, "10. Equivalence test harness\n    (GUI ↔ autoreduce)", "#4A90C2", 400),
    (40,   7.0, "11. MRInspectData adopts DataInfo", "#4A90C2", 340),
    (60,   8.0, "12. Reduction off Qt thread\n    (QThread / futures)", "#4A90C2", 380),

    (80,  8.5, "13. Frozen Configuration dataclass\n    (no class-attr globals)", "#E87722", 360),
    (120, 7.5, "14. Reactive configuration bus",     "#E87722", 300),
    (40,  6.0, "15. Upstream MRR defaults\n    to Mantid", "#E87722", 280),

    # Low payoff items
    (16,  3.0, "16. Align Mantid pin 6.14 → 6.15",   "#C8102E", 220),
    (24,  2.5, "17. Autoreduce staging\n    environment", "#C8102E", 220),
    (8,   1.5, "18. Rewrite Configuration docstrings","#E87722", 180),
]

# Plot points
for (x, y, label, colour, size) in items:
    ax.scatter([x], [y], s=size, c=colour, alpha=0.88,
               edgecolors="#002E5D", linewidths=1.4, zorder=3)

# Labels — placed with small offsets
label_offsets = {
    "1.  Unify two _as_ints":                         (+0.14, +0.32),
    "2.  Consolidate QuickNXS scale ×3":              (+0.14, -0.22),
    "3.  Delete 2 dup SingleReadoutDTC":              (+0.14, -0.05),
    "4.  Delete dead peak_finding copy":              (+0.14, +0.20),
    "5.  Delete dead dead_time copy":                 (+0.14, -0.22),
    "6.  Fix BinningType int/enum drift":             (+0.14, +0.22),
    "7.  Rename use_dangle→use_sangle":               (+0.14, +0.22),
    "8.  Fix typos (\"No valida\"…)":                 (+0.14, -0.22),
    "9.  Unified build_mrr_kwargs() fn":              (+0.18, -0.55),
    "10. Equivalence test harness\n    (GUI ↔ autoreduce)": (+0.18, +0.15),
    "11. MRInspectData adopts DataInfo":              (+0.18, -0.15),
    "12. Reduction off Qt thread\n    (QThread / futures)": (+0.18, -0.15),
    "13. Frozen Configuration dataclass\n    (no class-attr globals)": (+0.18, +0.15),
    "14. Reactive configuration bus":                 (+0.18, -0.18),
    "15. Upstream MRR defaults\n    to Mantid":       (+0.18, -0.20),
    "16. Align Mantid pin 6.14 → 6.15":               (+0.18, -0.15),
    "17. Autoreduce staging\n    environment":       (+0.18, +0.18),
    "18. Rewrite Configuration docstrings":           (+0.18, +0.15),
}
for (x, y, label, colour, size) in items:
    dx, dy = label_offsets[label]
    xa = x * (10 ** dx)
    ha = "left" if dx > 0 else "right"
    ax.text(xa, y + dy, label, fontsize=11, ha=ha, va="center",
            family=FAMILY, color="#1F3A5F",
            bbox=dict(boxstyle="round,pad=0.18", fc="white", ec="none",
                       alpha=0.75))

# Title inside the plot
ax.set_title("All 17 debt items from 11-technical-debt.md §A–§O",
             fontsize=14, family=FAMILY, pad=8, color="#444")

# ---- Right panel: tier summary ------------------------------------------
panel = fig.add_axes([0.72, 0.09, 0.27, 0.785])
panel.axis("off")
panel.set_xlim(0, 1); panel.set_ylim(0, 1)

panel.text(0.02, 0.97, "Reading the matrix",
           fontsize=22, fontweight="bold", color="#002E5D",
           ha="left", va="top", family=FAMILY)
panel.plot([0.02, 0.98], [0.935, 0.935], color="#4A90C2", lw=1.5)

tiers = [
    ("Tier 1 — DO FIRST", "#53A548",
     "8 items, ~1 total day of work.\n"
     "Removes observable numerical drift between\n"
     "GUI and autoreduce.  Items 1, 2, 3 alone\n"
     "answer most of what scientists complain about."),
    ("Tier 2 — DURING HACK-A-THON", "#4A90C2",
     "4 items, 1-week each.\n"
     "Unified reduce() contract (slide 6), equivalence\n"
     "test harness (slide 8), DataInfo consolidation,\n"
     "reduction off the Qt thread."),
    ("Tier 3 — FOLLOW-ON", "#E87722",
     "3 items, 1-3 weeks each.\n"
     "Frozen Configuration, reactive bus,\n"
     "upstreaming MRR defaults to Mantid.\n"
     "High payoff but high blast radius."),
    ("Tier 4 — DEFER", "#C8102E",
     "3 items, schedule when quiet.\n"
     "Version pin alignment, staging environment,\n"
     "exhaustive docstring rewrites."),
]
y0 = 0.9
for title, colour, body in tiers:
    panel.text(0.03, y0, title, fontsize=14.5, fontweight="bold", color=colour,
               ha="left", va="top", family=FAMILY)
    panel.text(0.05, y0 - 0.04, body, fontsize=11.5, color="#1F3A5F",
               ha="left", va="top", family=FAMILY)
    y0 -= 0.22

# Bottom insight box
insight = plt.Rectangle((0.02, 0.015), 0.96, 0.11,
                        transform=panel.transAxes,
                        facecolor="#002E5D", edgecolor="#001A35", lw=1.5)
panel.add_patch(insight)
panel.text(0.5, 0.089, "Tier 1 pays for itself on Day 1.",
           transform=panel.transAxes,
           fontsize=13, fontweight="bold", color="#F5C13A",
           ha="center", va="center", family=FAMILY)
panel.text(0.5, 0.05, "Tier 2 is the hack-a-thon backbone.",
           transform=panel.transAxes,
           fontsize=12, color="#E0ECF5",
           ha="center", va="center", family=FAMILY)
panel.text(0.5, 0.022, "Tiers 3–4 flow onto Q3 / Q4 roadmap.",
           transform=panel.transAxes,
           fontsize=12, color="#E0ECF5",
           ha="center", va="center", family=FAMILY)

out_svg = Path(__file__).parent / "slide-7-debt-triage.svg"
plt.savefig(out_svg, format="svg", bbox_inches=None, pad_inches=0)
print(f"wrote {out_svg}")
+370 KiB
Loading image diff...
+6362 −0

File added.

Preview size limit exceeded, changes collapsed.