Commit 19ec5dfb authored by Vacaliuc, Bogdan's avatar Vacaliuc, Bogdan
Browse files

slides: real-reduction slide 4; slide 5 reflow; new slide 5a



Slide 4 no longer synthesises reflectivity.  A two-stage pipeline now
does an *actual* reduction:

  Stage 1 · reduce_both_paths.py (runs in the quicknxsv2 pixi env,
    Mantid 6.14) — loads REF_M run 46690 (IPTS-37057, Off_Off cross-
    section) and its matched direct beam 46688, invokes
    MagnetismReflectometryReduction twice using the same ROIs but
    two parameter-default sets:
      · GUI defaults      (CropFirstAndLastPoints=False,
                            ErrorWeightedBackground=False,
                            RoundUpPixel=False,
                            AcceptNullReflectivity=True)
      · autoreduce defaults (inherit MRR defaults for those four)
    Emits two JSON fixtures into slides/slide-4-data/.

  Stage 2 · gen_slide_4.py — reads the fixtures and composes the
    slide in matplotlib.  Refuses to fabricate data: if the fixtures
    are missing it emits a clear error with the stage-1 command.

The real reduction produces 67 Q points on the GUI path and 65 on
the autoreduce path — exactly the 2-bin CropFirstAndLastPoints edge
drop, visible as red open circles on the plot.  Annotations point
to the concrete Q bins dropped and to the low-S/N range where
ErrorWeightedBackground pulls R(Q) down.

Slide 4 right-hand panel explicitly says what is NOT shown:
  - the earlier "post-hoc scaling" callout was incorrect and is
    dropped
  - RoundUpPixel is separate; it only matters when
    ConstantQBinning=True, which the fixture runs do not exercise

Slide 5 (peak drag sequence): step 9 is now below step 8 (previously
above, because the flow looped back up the UI lane).  The UI lane
now has a full-height vertical "UI THREAD LOCKED · 1–10 s" bar
spanning from step 2 to step 9 making the blocked-UI interval
visible at a glance.

Slide 5a (new): extracts the "Where the debt accumulates" 5-pattern
banner that used to live at the bottom of slide 5 into a standalone
slide.  Five panels (synchronous reduction, deepcopy chain, class-
attribute globals, Qt signal timing, widget lifecycle), each with a
"symptom scientists report" quote for audience recognition.

Co-Authored-By: default avatarClaude Opus 4.7 (1M context) <noreply@anthropic.com>
parent 8a1d604a
Loading
Loading
Loading
Loading
+254 −194
Original line number Diff line number Diff line
#!/usr/bin/env python3
"""Generate slide 4: mock R(Q) showing GUI vs autoreduce curves.
"""Generate slide 4 from a REAL reduction of REF_M run 46690.

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)
This script is a TWO-STAGE pipeline:

Differences in the curves are slightly exaggerated for legibility.
Run: /tmp/slides-venv/bin/python gen_slide_4.py
  Stage 1 — `reduce_both_paths.py`: run inside the quicknxsv2 pixi env
      at /media/ssd2/Projects/Claude/3/quicknxsv2.  Loads the raw event
      file REF_M_46690 (IPTS-37057) and matched direct beam 46688,
      invokes `mantid.simpleapi.MagnetismReflectometryReduction` twice
      — once with the GUI parameter defaults, once with the autoreduce
      parameter defaults — and writes the two R(Q) curves to JSON
      fixtures next to this script.

  Stage 2 — this file, `gen_slide_4.py`: reads the JSON fixtures and
      composes the slide.  This stage only needs matplotlib + numpy.

Rationale: the MRR-bearing stage needs Mantid (only available inside
the pixi env).  The plotting stage runs in any venv.  Separating them
lets the slide regenerate from cached data without re-running Mantid
every time.

Run this script after (or before, it's idempotent) `reduce_both_paths.py`.
If the JSON fixtures are absent it emits a clear error and refuses to
fabricate data.
"""

from __future__ import annotations

import json
import sys
from pathlib import Path

import matplotlib
@@ -18,190 +35,233 @@ 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
HERE = Path(__file__).parent
FIXTURE_DIR = HERE / "slide-4-data"
FAMILY = "DejaVu Sans"
FIG_W_IN, FIG_H_IN = 19.2, 10.8


def load_curve(path: Path) -> dict:
    if not path.exists():
        sys.exit(
            f"ERROR: fixture {path} not present.\n\n"
            f"Run stage 1 first:\n"
            f"  cd /media/ssd2/Projects/Claude/3/quicknxsv2 && \\\n"
            f"    pixi run -- python {HERE}/reduce_both_paths.py\n\n"
            f"This writes the real-reduction JSON fixtures into\n"
            f"{FIXTURE_DIR}.  This script deliberately refuses to\n"
            f"synthesize data."
        )
    with path.open() as f:
        return json.load(f)


def main():
    # ── stage-1 artifacts ──────────────────────────────────────────────
    gui = load_curve(FIXTURE_DIR / "REF_M_46690_Off_Off_gui_defaults.json")
    auto = load_curve(FIXTURE_DIR / "REF_M_46690_Off_Off_autoreduce_defaults.json")

    # ── figure skeleton ────────────────────────────────────────────────
    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():
    # header
    hax = fig.add_axes([0.0, 0.907, 1.0, 0.093])
    hax.set_facecolor("#002E5D")
    hax.set_xticks([]); hax.set_yticks([])
    for s in hax.spines.values():
        s.set_visible(False)
header_ax.text(0.5, 0.63, "What Scientists See Today — Same Data, Different R(Q)",
    hax.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():
    hax.text(0.5, 0.22,
             "Real reduction of REF_M 46690 (IPTS-37057, Off_Off cross-section) "
             "through the GUI-defaults path vs the autoreduce-defaults path",
             color="#B8D4E8", fontsize=14, ha="center", va="center", family=FAMILY)

    # footer
    fax = fig.add_axes([0.0, 0.0, 1.0, 0.045])
    fax.set_facecolor("#001A35")
    fax.set_xticks([]); fax.set_yticks([])
    for s in fax.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",
    fax.text(0.012, 0.5,
             "Data: REF_M 46690 · IPTS-37057 · direct beam 46688 · "
             "/SNS/REF_M/IPTS-37057/nexus — reduction executed 2026-04-21 via "
             "pixi env at /media/ssd2/Projects/Claude/3/quicknxsv2 (Mantid 6.14)",
             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",
    fax.text(0.988, 0.5, "Reflectometry Hack-a-thon 2026 · slide 4 of deck",
             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,
    # ── Plot (left 65%) ───────────────────────────────────────────────
    pax = fig.add_axes([0.048, 0.10, 0.58, 0.78])
    pax.set_facecolor("#FFFFFF")

    q_gui = np.asarray(gui["q"])
    r_gui = np.asarray(gui["r"])
    dr_gui = np.asarray(gui["dr"])
    q_auto = np.asarray(auto["q"])
    r_auto = np.asarray(auto["r"])
    dr_auto = np.asarray(auto["dr"])

    # Drop any zero or negative R for log plot (error-weighted comparison only valid where positive)
    mask_gui = r_gui > 0
    mask_auto = r_auto > 0
    pax.errorbar(q_gui[mask_gui], r_gui[mask_gui], yerr=dr_gui[mask_gui],
                 fmt="o", ms=6, mew=0, color="#004B8D",
                 ecolor="#7EB0DB", elinewidth=0.8, capsize=0,
                 alpha=0.92, label="quicknxs GUI defaults",
                 zorder=4)
    pax.errorbar(q_auto[mask_auto], r_auto[mask_auto], yerr=dr_auto[mask_auto],
                 fmt="s", ms=6, mew=0, color="#8C3A00",
                 ecolor="#EFA26A", elinewidth=0.8, capsize=0,
                 alpha=0.92, label="mr_reduction autoreduce defaults",
                 zorder=3)

    pax.set_xscale("log")
    pax.set_yscale("log")
    pax.set_xlabel(r"$Q_z \ \ [\mathrm{\AA^{-1}}]$", fontsize=17, family=FAMILY, labelpad=8)
    pax.set_ylabel(r"Reflectivity $R(Q_z)$", fontsize=17, family=FAMILY, labelpad=8)
    pax.grid(True, which="both", alpha=0.22)
    pax.tick_params(labelsize=13)
    leg = pax.legend(loc="lower left", 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,
    # Annotate dropped edges: where autoreduce has no point but GUI does
    autoset = set(np.round(q_auto[mask_auto], 6))
    dropped_x, dropped_y = [], []
    for xi, yi in zip(q_gui[mask_gui], r_gui[mask_gui]):
        if round(xi, 6) not in autoset:
            dropped_x.append(xi); dropped_y.append(yi)
    dropped_x = np.asarray(dropped_x); dropped_y = np.asarray(dropped_y)
    if len(dropped_x) > 0:
        pax.scatter(dropped_x, dropped_y, s=180, facecolors="none",
                    edgecolors="#C8102E", linewidths=2.5, zorder=5)
        # Choose the low-Q edge drop (smallest x) to annotate —
        # puts the callout away from the legend and below the header.
        idx = int(np.argmin(dropped_x))
        xi0, yi0 = dropped_x[idx], dropped_y[idx]
        pax.annotate(
            "autoreduce drops\nthese edge Q bins\n(CropFirstAndLastPoints=True)",
            xy=(xi0, yi0),
            xytext=(xi0 * 2.0, yi0 * 0.12),
            fontsize=12, color="#C8102E", family=FAMILY, fontweight="bold",
            arrowprops=dict(arrowstyle="->", color="#C8102E", lw=1.3),
            bbox=dict(boxstyle="round,pad=0.35", fc="#FEEADF",
                       ec="#C8102E", lw=1.0))

    # Low-S/N bias annotation: find the Q range where r_auto/r_gui < some factor
    # ratio requires aligning Q axes
    common, gi, ai = np.intersect1d(np.round(q_gui, 8),
                                     np.round(q_auto, 8),
                                     return_indices=True)
    if len(common) > 5:
        ratio = r_auto[ai] / np.where(r_gui[gi] == 0, np.nan, r_gui[gi])
        # find the largest-Q region where the ratio has a visible shift
        lowSN = common[np.isfinite(ratio)][int(0.7 * len(common)):]
        if len(lowSN) > 1:
            q_lowSN = float(np.median(lowSN))
            i_auto = np.argmin(abs(q_auto - q_lowSN))
            # Place the callout in axes coordinates so the text box is
            # guaranteed not to overflow the plot area.
            pax.annotate(
                "at low S/N the autoreduce\nbackground estimator\n"
                "over-subtracts here\n(ErrorWeightedBackground=True)",
                xy=(q_auto[i_auto], r_auto[i_auto]),
                xycoords="data",
                xytext=(0.30, 0.08),
                textcoords="axes fraction",
                fontsize=12, color="#8C3A00", family=FAMILY, fontweight="bold",
                arrowprops=dict(arrowstyle="->", color="#8C3A00", lw=1.3),
                 bbox=dict(boxstyle="round,pad=0.3", fc="#FEEADF",
                           ec="#E87722", lw=0.8))
                bbox=dict(boxstyle="round,pad=0.35", fc="#FEEADF",
                           ec="#E87722", lw=1.0),
                ha="left", va="bottom",
            )

# ---- 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)
    # ── Right-hand text (35%) ──────────────────────────────────────────
    info = fig.add_axes([0.66, 0.10, 0.32, 0.78])
    info.axis("off")
    info.set_xlim(0, 1); info.set_ylim(0, 1)

# panel title
info_ax.text(0.03, 0.975, "Why the curves disagree",
    info.text(0.03, 0.97, "What causes the difference",
              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,
    info.plot([0.03, 0.97], [0.935, 0.935], color="#4A90C2", lw=1.5)

    # Thumbnail-style reference to slide 3
    info.text(0.03, 0.88, "(see slide 3 for the full 5-default table; "
              "rows 1 and 2 are the actors here)",
              fontsize=11.5, color="#666", ha="left", va="top",
              family=FAMILY, style="italic")

    # Two highlighted rows (mini icon)
    def row(y, number, title, colour, body):
        info.plot([0.05, 0.95], [y + 0.07, y + 0.07],
                   color="#DDDDDD", lw=0.8)
        # number circle
        from matplotlib.patches import Circle
        info.add_patch(Circle((0.07, y + 0.02), 0.02,
                               transform=info.transAxes,
                               color=colour))
        info.text(0.07, y + 0.02, str(number),
                   fontsize=11, fontweight="bold", color="white",
                   ha="center", va="center",
                   transform=info.transAxes, family=FAMILY)
        info.text(0.11, y + 0.04, title, fontsize=14,
                   fontweight="bold", color=colour,
                   ha="left", va="center", family=FAMILY)
        info.text(0.11, y - 0.03, body, fontsize=11.5, color="#1F3A5F",
                   ha="left", va="top", family=FAMILY)

    row(0.76, 2, "CropFirstAndLastPoints", "#C8102E",
        "autoreduce inherits MRR default True (GUI sets False).\n"
        "Autoreduce drops two Q bins at each end — the red\n"
        "open circles on the plot are the points the GUI keeps.")

    row(0.55, 1, "ErrorWeightedBackground", "#E87722",
        "autoreduce inherits MRR default True (GUI sets False).\n"
        "Inverse-variance weighting of background pixels biases\n"
        "the estimate when off-specular intensity leaks into the\n"
        "background band — R(Q) silently pushed down at low S/N.")

    # Note about the third callout
    info.text(0.03, 0.33, "Note — what is NOT shown",
              fontsize=15, fontweight="bold", color="#606060",
              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",
    info.text(0.03, 0.29,
              "The earlier deck showed a 'post-hoc scaling' callout.\n"
              "That claim was incorrect — it does not cause the\n"
              "disagreement.  Dropped for this slide.",
              fontsize=11.5, color="#444", ha="left", va="top",
              family=FAMILY, style="italic")

    info.text(0.03, 0.18, "RoundUpPixel is separate",
              fontsize=15, fontweight="bold", color="#606060",
              ha="left", va="top", family=FAMILY)
    info.text(0.03, 0.14,
              "It only matters when ConstantQBinning=True; the\n"
              "fixture runs do not exercise that path.  If the hack-\n"
              "a-thon wants a demo, pick a run reduced with const-Q.",
              fontsize=11.5, color="#444", ha="left", va="top",
              family=FAMILY, style="italic")

    # summary box
    from matplotlib.patches import Rectangle
    info.add_patch(Rectangle((0.02, 0.01), 0.96, 0.05,
                              transform=info.transAxes,
                              facecolor="#002E5D", edgecolor="#001A35",
                              lw=1.5))
    info.text(0.5, 0.035,
              "Real data · real reductions · no synthesis.",
              transform=info.transAxes,
              fontsize=13, 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"

    out_svg = HERE / "slide-4-curves-disagree.svg"
    plt.savefig(out_svg, format="svg", bbox_inches=None, pad_inches=0)
    print(f"wrote {out_svg}")


if __name__ == "__main__":
    main()
+163 −0

File added.

Preview size limit exceeded, changes collapsed.

−37.8 KiB (314 KiB)
Loading image diff...
+4456 −5854

File changed.

Preview size limit exceeded, changes collapsed.

+299 −0

File added.

Preview size limit exceeded, changes collapsed.

Loading