From e9f1342fc8e233a4841b7eb3c1363324e90ecda9 Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Sat, 29 Aug 2026 18:43:05 -0500 Subject: figure: show SDIL transfer across digital learners --- experiments/plot_digital_additive_transfer.py | 325 ++++++++ results/digital_additive_transfer.csv | 19 + results/digital_additive_transfer.json | 194 +++++ results/figs/figure_digital_additive_transfer.pdf | Bin 0 -> 17919 bytes results/figs/figure_digital_additive_transfer.png | Bin 0 -> 157768 bytes results/figs/figure_digital_additive_transfer.svg | 964 ++++++++++++++++++++++ visual-composer/digital-additive-transfer.md | 33 + visual-composer/qa-ledger.md | 14 + 8 files changed, 1549 insertions(+) create mode 100644 experiments/plot_digital_additive_transfer.py create mode 100644 results/digital_additive_transfer.csv create mode 100644 results/digital_additive_transfer.json create mode 100644 results/figs/figure_digital_additive_transfer.pdf create mode 100644 results/figs/figure_digital_additive_transfer.png create mode 100644 results/figs/figure_digital_additive_transfer.svg create mode 100644 visual-composer/digital-additive-transfer.md diff --git a/experiments/plot_digital_additive_transfer.py b/experiments/plot_digital_additive_transfer.py new file mode 100644 index 0000000..5e12a11 --- /dev/null +++ b/experiments/plot_digital_additive_transfer.py @@ -0,0 +1,325 @@ +#!/usr/bin/env python3 +"""Plot SDIL as an additive correction across four digital learners.""" + +from __future__ import annotations + +import argparse +import csv +import json +from pathlib import Path + +import matplotlib as mpl +import matplotlib.pyplot as plt +import numpy as np + + +COLORS = { + "clean": "#222222", + "noise": "#CC79A7", + "raw": "#D55E00", + "constant": "#777777", + "sdil": "#0072B2", + "oracle": "#009E73", +} + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--dualprop", type=Path, + default=Path("results/contrastive_bias/c1_gate.json")) + parser.add_argument( + "--ep", type=Path, + default=Path("results/ep_bias/c1_gate.json")) + parser.add_argument( + "--clln", type=Path, + default=Path("results/coupled_ladder/p2_confirm_side4.json")) + parser.add_argument( + "--overclamp", type=Path, + default=Path("results/coupled_ladder/p3_overclamp_side4.json")) + parser.add_argument( + "--output-analysis", type=Path, + default=Path("results/digital_additive_transfer.json")) + parser.add_argument( + "--output-csv", type=Path, + default=Path("results/digital_additive_transfer.csv")) + parser.add_argument( + "--output-figure", type=Path, + default=Path("results/figs/figure_digital_additive_transfer")) + parser.add_argument("--bootstrap-replicates", type=int, default=20000) + parser.add_argument("--bootstrap-seed", type=int, default=20260829) + return parser.parse_args() + + +def summarize_values( + values: np.ndarray, + rng: np.random.Generator, + replicates: int, +) -> dict: + values = np.asarray(values, dtype=float) + samples = rng.integers(0, len(values), size=(replicates, len(values))) + means = np.mean(values[samples], axis=1) + return { + "mean_accuracy_percent": float(np.mean(values)), + "bootstrap_95ci_percent": [ + float(value) for value in np.percentile(means, (2.5, 97.5)) + ], + "independent_units": len(values), + } + + +def task_cluster_accuracies(report: dict, method: str) -> np.ndarray: + tasks = sorted({record["task_index"] for record in report["records"]}) + return np.asarray([ + 100.0 * np.mean([ + 1.0 - record["methods"][method]["classification_error"] + for record in report["records"] + if record["task_index"] == task + ]) + for task in tasks + ]) + + +def gap_recovered(methods: dict) -> float: + clean = methods["clean"]["mean_accuracy_percent"] + raw = methods["raw"]["mean_accuracy_percent"] + sdil = methods["sdil"]["mean_accuracy_percent"] + return float(100.0 * (sdil - raw) / (clean - raw)) + + +def build_analysis( + dualprop: dict, + ep: dict, + clln: dict, + overclamp: dict, + *, + replicates: int, + seed: int, +) -> dict: + rng = np.random.default_rng(seed) + dp_rows = dualprop["rows"] + ep_rows = ep["rows"] + panels = { + "dualprop": { + "title": "Dual Propagation · CIFAR-10", + "unit": "seed", + "methods": { + "clean": summarize_values(np.asarray([ + row["same_path_clean"] for row in dp_rows]), rng, replicates), + "raw": summarize_values(np.asarray([ + row["raw"] for row in dp_rows]), rng, replicates), + "sdil": summarize_values(np.asarray([ + row["innovation"] for row in dp_rows]), rng, replicates), + "oracle": summarize_values(np.asarray([ + row["oracle"] for row in dp_rows]), rng, replicates), + }, + }, + "ep": { + "title": "Equilibrium Propagation · FashionMNIST", + "unit": "seed", + "methods": { + "clean": summarize_values(100.0 * np.asarray([ + row["clean"] for row in ep_rows]), rng, replicates), + "noise": summarize_values(100.0 * np.asarray([ + row["same_rms_noise"] for row in ep_rows]), rng, replicates), + "raw": summarize_values(100.0 * np.asarray([ + row["raw"] for row in ep_rows]), rng, replicates), + "constant": summarize_values(100.0 * np.asarray([ + row["constant"] for row in ep_rows]), rng, replicates), + "sdil": summarize_values(100.0 * np.asarray([ + row["innovation"] for row in ep_rows]), rng, replicates), + "oracle": summarize_values(100.0 * np.asarray([ + row["oracle"] for row in ep_rows]), rng, replicates), + }, + }, + "clln": { + "title": "Coupled learning · 32 edges", + "unit": "task; device draws averaged within task", + "methods": { + method: summarize_values( + task_cluster_accuracies(clln, source), rng, replicates) + for method, source in ( + ("clean", "clean"), + ("noise", "matched_noise"), + ("raw", "raw"), + ("constant", "constant"), + ("sdil", "sdil"), + ) + }, + }, + "overclamp": { + "title": "Overclamped coupled learning · 32 edges", + "unit": "task; device draws averaged within task", + "methods": { + method: summarize_values( + task_cluster_accuracies(overclamp, source), rng, replicates) + for method, source in ( + ("clean", "overclamp_clean"), + ("raw", "overclamp"), + ("sdil", "overclamp_sdil"), + ) + }, + }, + } + for panel in panels.values(): + panel["raw_to_clean_gap_recovered_percent"] = gap_recovered( + panel["methods"]) + return { + "analysis": "digital_additive_sdil_transfer", + "confirmatory_sources": { + "dualprop": dualprop["gate"] == "pass", + "ep": ep["gate"] == "pass", + "clln": bool(clln["confirmatory"]), + "overclamp": bool(overclamp["confirmatory"]), + }, + "bootstrap": { + "replicates": replicates, + "seed": seed, + "interval": "percentile 95%", + }, + "panels": panels, + } + + +def write_csv(path: Path, analysis: dict) -> None: + path.parent.mkdir(parents=True, exist_ok=True) + with path.open("w", newline="") as stream: + writer = csv.writer(stream) + writer.writerow(( + "panel", "method", "mean_accuracy_percent", "ci_low", "ci_high", + "independent_unit", "independent_units", "gap_recovered_percent", + )) + for panel_name, panel in analysis["panels"].items(): + for method, summary in panel["methods"].items(): + writer.writerow(( + panel_name, + method, + summary["mean_accuracy_percent"], + *summary["bootstrap_95ci_percent"], + panel["unit"], + summary["independent_units"], + panel["raw_to_clean_gap_recovered_percent"], + )) + + +def plot(path: Path, analysis: dict) -> None: + mpl.rcParams.update({ + "font.family": "DejaVu Sans", + "font.size": 8.5, + "axes.labelsize": 9, + "axes.titlesize": 9.5, + "xtick.labelsize": 7.8, + "ytick.labelsize": 8, + "axes.spines.top": False, + "axes.spines.right": False, + "svg.fonttype": "none", + "pdf.fonttype": 42, + "figure.facecolor": "white", + "axes.facecolor": "white", + }) + order = ("dualprop", "ep", "clln", "overclamp") + figure, axes = plt.subplots(2, 2, figsize=(8.0, 5.8), sharey=True) + for index, (axis, panel_name) in enumerate(zip(axes.ravel(), order)): + panel = analysis["panels"][panel_name] + methods = list(panel["methods"]) + summaries = [panel["methods"][method] for method in methods] + means = np.asarray([ + summary["mean_accuracy_percent"] for summary in summaries]) + intervals = np.asarray([ + summary["bootstrap_95ci_percent"] for summary in summaries]) + positions = np.arange(len(methods)) + axis.bar( + positions, + means, + color=[COLORS[method] for method in methods], + width=0.72, + edgecolor="white", + linewidth=0.4, + ) + axis.errorbar( + positions, + means, + yerr=np.vstack(( + means - intervals[:, 0], intervals[:, 1] - means, + )), + fmt="none", + ecolor="#222222", + elinewidth=0.8, + capsize=2.2, + ) + labels = { + "clean": "Clean", "noise": "Same-RMS\nnoise", "raw": "Raw", + "constant": "Static\ncalibration", "sdil": "SDIL", + "oracle": "Oracle", + } + axis.set_xticks(positions, [labels[method] for method in methods]) + axis.set_ylim(0.0, 106.0) + axis.grid(axis="y", color="#D9D9D9", linewidth=0.55, alpha=0.8) + axis.tick_params(length=3) + letter = chr(ord("a") + index) + recovery = panel["raw_to_clean_gap_recovered_percent"] + axis.set_title( + f"({letter}) {panel['title']}\n{recovery:.0f}% of clean gap recovered") + if index % 2 == 0: + axis.set_ylabel("Task accuracy (%)") + figure.text( + 0.995, + 0.005, + "Dual Prop/EP: 5 seeds; coupled learners: 40 tasks × 3 device draws", + ha="right", + va="bottom", + fontsize=7, + color="#666666", + ) + figure.tight_layout(rect=(0.0, 0.035, 1.0, 1.0), h_pad=2.1, w_pad=1.8) + path.parent.mkdir(parents=True, exist_ok=True) + figure.savefig(path.with_suffix(".svg"), bbox_inches="tight") + figure.savefig(path.with_suffix(".pdf"), bbox_inches="tight") + figure.savefig(path.with_suffix(".png"), dpi=240, bbox_inches="tight") + plt.close(figure) + + +def main() -> None: + args = parse_args() + reports = { + name: json.loads(path.read_text()) + for name, path in ( + ("dualprop", args.dualprop), + ("ep", args.ep), + ("clln", args.clln), + ("overclamp", args.overclamp), + ) + } + analysis = build_analysis( + **reports, + replicates=args.bootstrap_replicates, + seed=args.bootstrap_seed, + ) + analysis["sources"] = { + "dualprop": str(args.dualprop), + "ep": str(args.ep), + "clln": str(args.clln), + "overclamp": str(args.overclamp), + } + args.output_analysis.parent.mkdir(parents=True, exist_ok=True) + args.output_analysis.write_text(json.dumps(analysis, indent=2) + "\n") + write_csv(args.output_csv, analysis) + plot(args.output_figure, analysis) + print(json.dumps({ + name: { + "gap_recovered_percent": panel[ + "raw_to_clean_gap_recovered_percent"], + "methods": { + method: summary["mean_accuracy_percent"] + for method, summary in panel["methods"].items() + }, + } + for name, panel in analysis["panels"].items() + }, indent=2)) + print(f"wrote {args.output_analysis}") + print(f"wrote {args.output_csv}") + print(f"wrote {args.output_figure}.svg/.pdf/.png") + + +if __name__ == "__main__": + main() diff --git a/results/digital_additive_transfer.csv b/results/digital_additive_transfer.csv new file mode 100644 index 0000000..92b019c --- /dev/null +++ b/results/digital_additive_transfer.csv @@ -0,0 +1,19 @@ +panel,method,mean_accuracy_percent,ci_low,ci_high,independent_unit,independent_units,gap_recovered_percent +dualprop,clean,82.85999908447266,82.56399993896484,83.07999725341797,seed,5,100.08168001179783 +dualprop,raw,9.399999618530273,9.399999618530273,9.399999618530273,seed,5,100.08168001179783 +dualprop,sdil,82.92000122070313,82.6280014038086,83.23600158691406,seed,5,100.08168001179783 +dualprop,oracle,82.96000061035156,82.52799682617187,83.34400329589843,seed,5,100.08168001179783 +ep,clean,76.26,73.55,78.53,seed,5,96.12299465240643 +ep,noise,75.44,71.01,78.76,seed,5,96.12299465240643 +ep,raw,31.379999999999995,18.059999999999995,46.08,seed,5,96.12299465240643 +ep,constant,67.9,60.42,74.46674999999942,seed,5,96.12299465240643 +ep,sdil,74.52000000000001,72.51,76.25999999999999,seed,5,96.12299465240643 +ep,oracle,75.55999999999999,70.6,78.66999999999999,seed,5,96.12299465240643 +clln,clean,100.0,100.0,100.0,task; device draws averaged within task,40,100.0 +clln,noise,99.27083333333334,98.4375,99.89583333333334,task; device draws averaged within task,40,100.0 +clln,raw,65.3125,61.14583333333333,69.375,task; device draws averaged within task,40,100.0 +clln,constant,93.85416666666666,90.41666666666667,96.875,task; device draws averaged within task,40,100.0 +clln,sdil,100.0,100.0,100.0,task; device draws averaged within task,40,100.0 +overclamp,clean,91.875,87.8125,95.3125,task; device draws averaged within task,40,100.41666666666663 +overclamp,raw,66.875,61.25,72.70833333333333,task; device draws averaged within task,40,100.41666666666663 +overclamp,sdil,91.97916666666666,88.125,95.3125,task; device draws averaged within task,40,100.41666666666663 diff --git a/results/digital_additive_transfer.json b/results/digital_additive_transfer.json new file mode 100644 index 0000000..ccb06f8 --- /dev/null +++ b/results/digital_additive_transfer.json @@ -0,0 +1,194 @@ +{ + "analysis": "digital_additive_sdil_transfer", + "confirmatory_sources": { + "dualprop": true, + "ep": false, + "clln": true, + "overclamp": true + }, + "bootstrap": { + "replicates": 20000, + "seed": 20260829, + "interval": "percentile 95%" + }, + "panels": { + "dualprop": { + "title": "Dual Propagation \u00b7 CIFAR-10", + "unit": "seed", + "methods": { + "clean": { + "mean_accuracy_percent": 82.85999908447266, + "bootstrap_95ci_percent": [ + 82.56399993896484, + 83.07999725341797 + ], + "independent_units": 5 + }, + "raw": { + "mean_accuracy_percent": 9.399999618530273, + "bootstrap_95ci_percent": [ + 9.399999618530273, + 9.399999618530273 + ], + "independent_units": 5 + }, + "sdil": { + "mean_accuracy_percent": 82.92000122070313, + "bootstrap_95ci_percent": [ + 82.6280014038086, + 83.23600158691406 + ], + "independent_units": 5 + }, + "oracle": { + "mean_accuracy_percent": 82.96000061035156, + "bootstrap_95ci_percent": [ + 82.52799682617187, + 83.34400329589843 + ], + "independent_units": 5 + } + }, + "raw_to_clean_gap_recovered_percent": 100.08168001179783 + }, + "ep": { + "title": "Equilibrium Propagation \u00b7 FashionMNIST", + "unit": "seed", + "methods": { + "clean": { + "mean_accuracy_percent": 76.26, + "bootstrap_95ci_percent": [ + 73.55, + 78.53 + ], + "independent_units": 5 + }, + "noise": { + "mean_accuracy_percent": 75.44, + "bootstrap_95ci_percent": [ + 71.01, + 78.76 + ], + "independent_units": 5 + }, + "raw": { + "mean_accuracy_percent": 31.379999999999995, + "bootstrap_95ci_percent": [ + 18.059999999999995, + 46.08 + ], + "independent_units": 5 + }, + "constant": { + "mean_accuracy_percent": 67.9, + "bootstrap_95ci_percent": [ + 60.42, + 74.46674999999942 + ], + "independent_units": 5 + }, + "sdil": { + "mean_accuracy_percent": 74.52000000000001, + "bootstrap_95ci_percent": [ + 72.51, + 76.25999999999999 + ], + "independent_units": 5 + }, + "oracle": { + "mean_accuracy_percent": 75.55999999999999, + "bootstrap_95ci_percent": [ + 70.6, + 78.66999999999999 + ], + "independent_units": 5 + } + }, + "raw_to_clean_gap_recovered_percent": 96.12299465240643 + }, + "clln": { + "title": "Coupled learning \u00b7 32 edges", + "unit": "task; device draws averaged within task", + "methods": { + "clean": { + "mean_accuracy_percent": 100.0, + "bootstrap_95ci_percent": [ + 100.0, + 100.0 + ], + "independent_units": 40 + }, + "noise": { + "mean_accuracy_percent": 99.27083333333334, + "bootstrap_95ci_percent": [ + 98.4375, + 99.89583333333334 + ], + "independent_units": 40 + }, + "raw": { + "mean_accuracy_percent": 65.3125, + "bootstrap_95ci_percent": [ + 61.14583333333333, + 69.375 + ], + "independent_units": 40 + }, + "constant": { + "mean_accuracy_percent": 93.85416666666666, + "bootstrap_95ci_percent": [ + 90.41666666666667, + 96.875 + ], + "independent_units": 40 + }, + "sdil": { + "mean_accuracy_percent": 100.0, + "bootstrap_95ci_percent": [ + 100.0, + 100.0 + ], + "independent_units": 40 + } + }, + "raw_to_clean_gap_recovered_percent": 100.0 + }, + "overclamp": { + "title": "Overclamped coupled learning \u00b7 32 edges", + "unit": "task; device draws averaged within task", + "methods": { + "clean": { + "mean_accuracy_percent": 91.875, + "bootstrap_95ci_percent": [ + 87.8125, + 95.3125 + ], + "independent_units": 40 + }, + "raw": { + "mean_accuracy_percent": 66.875, + "bootstrap_95ci_percent": [ + 61.25, + 72.70833333333333 + ], + "independent_units": 40 + }, + "sdil": { + "mean_accuracy_percent": 91.97916666666666, + "bootstrap_95ci_percent": [ + 88.125, + 95.3125 + ], + "independent_units": 40 + } + }, + "raw_to_clean_gap_recovered_percent": 100.41666666666663 + } + }, + "sources": { + "dualprop": "results/contrastive_bias/c1_gate.json", + "ep": "results/ep_bias/c1_gate.json", + "clln": "results/coupled_ladder/p2_confirm_side4.json", + "overclamp": "results/coupled_ladder/p3_overclamp_side4.json" + } +} diff --git a/results/figs/figure_digital_additive_transfer.pdf b/results/figs/figure_digital_additive_transfer.pdf new file mode 100644 index 0000000..4070813 Binary files /dev/null and b/results/figs/figure_digital_additive_transfer.pdf differ diff --git a/results/figs/figure_digital_additive_transfer.png b/results/figs/figure_digital_additive_transfer.png new file mode 100644 index 0000000..78046ca Binary files /dev/null and b/results/figs/figure_digital_additive_transfer.png differ diff --git a/results/figs/figure_digital_additive_transfer.svg b/results/figs/figure_digital_additive_transfer.svg new file mode 100644 index 0000000..014aeb9 --- /dev/null +++ b/results/figs/figure_digital_additive_transfer.svg @@ -0,0 +1,964 @@ + + + + + + + + 2026-08-29T18:42:31.692423 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Clean + + + + + + + + + + Raw + + + + + + + + + + SDIL + + + + + + + + + + Oracle + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 20 + + + + + + + + + + + + + 40 + + + + + + + + + + + + + 60 + + + + + + + + + + + + + 80 + + + + + + + + + + + + + 100 + + + + Task accuracy (%) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (a) Dual Propagation · CIFAR-10 + 100% of clean gap recovered + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Clean + + + + + + + + + + Same-RMS + noise + + + + + + + + + + Raw + + + + + + + + + + Static + calibration + + + + + + + + + + SDIL + + + + + + + + + + Oracle + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (b) Equilibrium Propagation · FashionMNIST + 96% of clean gap recovered + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Clean + + + + + + + + + + Same-RMS + noise + + + + + + + + + + Raw + + + + + + + + + + Static + calibration + + + + + + + + + + SDIL + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 20 + + + + + + + + + + + + + 40 + + + + + + + + + + + + + 60 + + + + + + + + + + + + + 80 + + + + + + + + + + + + + 100 + + + + Task accuracy (%) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (c) Coupled learning · 32 edges + 100% of clean gap recovered + + + + + + + + + + + + + + + + + + + + + + + + Clean + + + + + + + + + + Raw + + + + + + + + + + SDIL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (d) Overclamped coupled learning · 32 edges + 100% of clean gap recovered + + + + Dual Prop/EP: 5 seeds; coupled learners: 40 tasks × 3 device draws + + + + + + + + + + + + + + + + + diff --git a/visual-composer/digital-additive-transfer.md b/visual-composer/digital-additive-transfer.md new file mode 100644 index 0000000..74867f8 --- /dev/null +++ b/visual-composer/digital-additive-transfer.md @@ -0,0 +1,33 @@ +# Visual contract: additive digital transfer + +- **Artifact:** four-panel quantitative result figure. +- **Target venue / format:** ICLR two-column paper, full-width figure. +- **Core claim:** the same local instruction-off subtraction improves four + digital local-learning backbones under matched teaching-channel + imperfection. +- **Reviewer question:** is SDIL a correction that transfers across learning + rules, or does it work only in one custom network? +- **Evidence layer:** main digital transfer result. +- **Source data:** + `results/contrastive_bias/c1_gate.json`, + `results/ep_bias/c1_gate.json`, + `results/coupled_ladder/p2_confirm_side4.json`, and + `results/coupled_ladder/p3_overclamp_side4.json`. +- **Statistics / uncertainty:** Dual Propagation and EP resample five seeds; + CLLN panels average three device draws within each of 40 tasks and resample + tasks. Error bars are percentile 95% bootstrap intervals. +- **Figure prototype:** coordinated 2-by-2 bar-chart small multiples. +- **Panel map:** author-code Dual Propagation, author-code EP, digital coupled + learning, and digital overclamped coupled learning. +- **Exact label inventory:** clean, same-RMS noise, raw, static calibration, + SDIL, oracle, task accuracy, clean gap recovered. +- **Caption role:** define raw-to-clean gap recovery within each panel and + state that datasets and absolute clean levels differ across panels. +- **Manuscript placement:** Part 1, after the algorithm definition. +- **Output formats:** editable SVG, vector PDF, PNG preview, analysis JSON, and + source CSV. +- **Traceability:** every bar is regenerated by + `experiments/plot_digital_additive_transfer.py` from the four JSON sources. +- **Constraint:** the EP five-seed result beats raw in every seed and recovers + 96% of the mean clean gap, while its stricter preregistered gate failed. The + manuscript and caption must retain that distinction. diff --git a/visual-composer/qa-ledger.md b/visual-composer/qa-ledger.md index c9e66e5..f601f93 100644 --- a/visual-composer/qa-ledger.md +++ b/visual-composer/qa-ledger.md @@ -26,3 +26,17 @@ changing the plot grammar. Rendered inspection: no clipped labels, tick-label collision, legend overlap, or panel overlap at 2,560-by-765 PNG resolution. SVG text remains editable and the PDF uses embedded TrueType fonts. + +## Additive digital transfer figure + +| Issue | Artifact | Severity | Fix | Status | +|:--|:--|:--|:--|:--| +| Different datasets invite invalid absolute comparisons | All panels | High | Used separate titled panels and defined recovery relative to each panel's own raw and clean endpoints | Resolved | +| CLLN device draws are not independent tasks | Panels (c) and (d) | High | Averaged device draws within task before task-level bootstrap | Resolved | +| EP result could be mistaken for a passed strict gate | Panel (b) | High | Stored gate status in analysis and required the caption/manifest to report the partial-gate boundary | Resolved | +| Long calibration labels could collide | Panels (b) and (c) | Medium | Used two-line labels and inspected the full-resolution render | Resolved | +| Color-only distinctions could fail in print | All panels | Low | Every bar has a direct method label | Resolved | + +Rendered inspection: no clipped labels, title collision, error-bar clipping, +or panel overlap at 1,920-by-1,392 PNG resolution. SVG text remains editable +and the PDF uses embedded TrueType fonts. -- cgit v1.2.3