From 3562fd83ef8fc7f1ebede7571301224f2b5368c7 Mon Sep 17 00:00:00 2001 From: YurenHao0426 Date: Sat, 29 Aug 2026 18:34:49 -0500 Subject: figure: consolidate hardware-realistic CLLN evidence --- experiments/plot_physical_hardware_evidence.py | 423 ++++++++ results/figs/figure_physical_hardware_evidence.pdf | Bin 0 -> 19174 bytes results/figs/figure_physical_hardware_evidence.png | Bin 0 -> 152686 bytes results/figs/figure_physical_hardware_evidence.svg | 1082 ++++++++++++++++++++ .../p10_hardware_evidence_analysis.json | 308 ++++++ .../physical_bias/p10_hardware_evidence_source.csv | 25 + visual-composer/physical-hardware-evidence.md | 40 + visual-composer/qa-ledger.md | 13 + 8 files changed, 1891 insertions(+) create mode 100644 experiments/plot_physical_hardware_evidence.py create mode 100644 results/figs/figure_physical_hardware_evidence.pdf create mode 100644 results/figs/figure_physical_hardware_evidence.png create mode 100644 results/figs/figure_physical_hardware_evidence.svg create mode 100644 results/physical_bias/p10_hardware_evidence_analysis.json create mode 100644 results/physical_bias/p10_hardware_evidence_source.csv create mode 100644 visual-composer/physical-hardware-evidence.md diff --git a/experiments/plot_physical_hardware_evidence.py b/experiments/plot_physical_hardware_evidence.py new file mode 100644 index 0000000..6a38b60 --- /dev/null +++ b/experiments/plot_physical_hardware_evidence.py @@ -0,0 +1,423 @@ +#!/usr/bin/env python3 +"""Build the hardware-realistic CLLN evidence figure from frozen results.""" + +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 + + +METHODS = ( + ("clean", "Clean", "#222222"), + ("raw", "Raw", "#D55E00"), + ("constant", "Static\ncalibration", "#777777"), + ("overclamp", "Overclamp", "#E69F00"), + ("sdil", "SDIL", "#0072B2"), + ("overclamp_sdil", "Overclamp\n+ SDIL", "#56B4E9"), +) + +COMBINED_CONDITIONS = ( + ("ideal_cds", "Ideal\nCDS"), + ("common_pedestal_10", "Common\npedestal"), + ("sample_noise_1", "Sample\nnoise"), + ("refresh_every_4", "Refresh\nevery 4"), + ("combined_mild_refresh4", "Mild\ncombined"), + ("combined_strong", "Strong\ncombined"), + ("overclamp_plus_combined_mild", "Overclamp\n+ mild"), +) + + +def parse_args() -> argparse.Namespace: + parser = argparse.ArgumentParser() + parser.add_argument( + "--physical", + type=Path, + default=Path( + "results/physical_bias/p5_full_grid_bias_crossover.json"), + ) + parser.add_argument( + "--sampler", + type=Path, + default=Path( + "results/physical_bias/p9_grid_correlated_autozero.json"), + ) + parser.add_argument( + "--spice", + type=Path, + default=Path( + "results/physical_bias/p8_spice_autozero_primitive.json"), + ) + parser.add_argument( + "--output-analysis", + type=Path, + default=Path( + "results/physical_bias/p10_hardware_evidence_analysis.json"), + ) + parser.add_argument( + "--output-csv", + type=Path, + default=Path( + "results/physical_bias/p10_hardware_evidence_source.csv"), + ) + parser.add_argument( + "--output-figure", + type=Path, + default=Path("results/figs/figure_physical_hardware_evidence"), + ) + parser.add_argument("--bootstrap-replicates", type=int, default=20000) + parser.add_argument("--bootstrap-seed", type=int, default=20260829) + return parser.parse_args() + + +def clustered_summary( + records: list[dict], + value, + *, + rng: np.random.Generator, + replicates: int, +) -> dict: + tasks = sorted({record["task_index"] for record in records}) + task_means = np.asarray([ + np.mean([value(record) for record in records + if record["task_index"] == task]) + for task in tasks + ]) + samples = rng.integers( + 0, len(task_means), size=(replicates, len(task_means))) + bootstrap = np.mean(task_means[samples], axis=1) + return { + "mean": float(np.mean(task_means)), + "task_bootstrap_95ci": [ + float(bound) for bound in np.percentile(bootstrap, (2.5, 97.5)) + ], + "task_clusters": len(tasks), + "trials": len(records), + } + + +def physical_method_summaries( + report: dict, rng: np.random.Generator, replicates: int +) -> dict: + output = {} + for method, _, _ in METHODS: + output[method] = clustered_summary( + report["records"], + lambda record, name=method: record["methods"][name][ + "classification_error"], + rng=rng, + replicates=replicates, + ) + return output + + +def sampler_condition_summary( + report: dict, + condition: str, + rng: np.random.Generator, + replicates: int, +) -> dict: + selected = [ + record for record in report["records"] + if record["condition"] == condition + ] + summary = clustered_summary( + selected, + lambda record: record["classification_error"], + rng=rng, + replicates=replicates, + ) + summary["zero_error_fraction"] = float(np.mean([ + record["classification_error"] == 0.0 for record in selected + ])) + summary["solver_failure_fraction"] = float(np.mean([ + record["status"] != "completed" for record in selected + ])) + finite_rmse = [ + record["applied_rate_rmse_v_per_s"] for record in selected + if record["applied_rate_rmse_v_per_s"] is not None + ] + summary["median_applied_rate_rmse_v_per_s"] = float(np.median([ + value for value in finite_rmse + ])) + return summary + + +def build_analysis( + physical: dict, + sampler: dict, + spice: dict, + *, + replicates: int, + seed: int, +) -> dict: + rng = np.random.default_rng(seed) + sampler_conditions = { + condition["name"] for condition in sampler["protocol"]["conditions"] + } + required_conditions = { + name for name, _ in COMBINED_CONDITIONS + } | { + f"pedestal_mismatch_{value:g}" + for value in (0.01, 0.025, 0.05, 0.1, 0.25, 0.5) + } | { + f"gain_mismatch_{value:g}" + for value in (0.001, 0.005, 0.01, 0.025, 0.05) + } + missing = required_conditions - sampler_conditions + if missing: + raise ValueError(f"sampler report is missing {sorted(missing)}") + condition_summaries = { + condition: sampler_condition_summary( + sampler, condition, rng, replicates) + for condition in sorted(required_conditions) + } + return { + "analysis": "physical_clln_hardware_evidence_figure", + "confirmatory": False, + "bootstrap": { + "unit": "task; four device draws averaged within task", + "task_clusters": 40, + "replicates": replicates, + "seed": seed, + "interval": "percentile 95%", + }, + "physical_methods": physical_method_summaries( + physical, rng, replicates), + "sampler_conditions": condition_summaries, + "spice_primitive": { + "publication_evidence": spice["publication_evidence"], + "scope": spice["scope"], + "configuration_count": spice["configuration_count"], + "fraction_below_1_percent_error_at_start": spice[ + "fraction_below_1_percent_error_at_start"], + "fraction_below_1_percent_error_at_end": spice[ + "fraction_below_1_percent_error_at_end"], + "reference_configuration": spice["reference_configuration"], + }, + } + + +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", "series", "condition", "x", "x_unit", "mean_error", + "ci_low", "ci_high", "zero_error_fraction", + )) + for method, label, _ in METHODS: + summary = analysis["physical_methods"][method] + writer.writerow(( + "a", "physical_method", label.replace("\n", " "), "", "", + summary["mean"], *summary["task_bootstrap_95ci"], "", + )) + for family, values in ( + ("pedestal mismatch", (0.01, 0.025, 0.05, 0.1, 0.25, 0.5)), + ("gain mismatch", (0.001, 0.005, 0.01, 0.025, 0.05)), + ): + prefix = family.replace(" ", "_") + for value in values: + summary = analysis["sampler_conditions"][f"{prefix}_{value:g}"] + writer.writerow(( + "b", family, f"{prefix}_{value:g}", + summary["median_applied_rate_rmse_v_per_s"], "V/s", + summary["mean"], *summary["task_bootstrap_95ci"], + summary["zero_error_fraction"], + )) + for condition, label in COMBINED_CONDITIONS: + summary = analysis["sampler_conditions"][condition] + writer.writerow(( + "c", "sampling condition", label.replace("\n", " "), "", "", + summary["mean"], *summary["task_bootstrap_95ci"], + summary["zero_error_fraction"], + )) + + +def errorbar(axis, x, summaries, **kwargs) -> None: + means = np.asarray([summary["mean"] for summary in summaries]) * 100.0 + intervals = np.asarray([ + summary["task_bootstrap_95ci"] for summary in summaries + ]) * 100.0 + axis.errorbar( + x, + means, + yerr=np.vstack((means - intervals[:, 0], intervals[:, 1] - means)), + capsize=2.2, + **kwargs, + ) + + +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, + "legend.fontsize": 7.5, + "xtick.labelsize": 7.7, + "ytick.labelsize": 8, + "axes.spines.top": False, + "axes.spines.right": False, + "svg.fonttype": "none", + "pdf.fonttype": 42, + "figure.facecolor": "white", + "axes.facecolor": "white", + }) + figure, axes = plt.subplots(1, 3, figsize=(10.7, 3.15)) + + method_summaries = [ + analysis["physical_methods"][method] for method, _, _ in METHODS + ] + method_means = np.asarray([ + summary["mean"] for summary in method_summaries]) * 100.0 + method_intervals = np.asarray([ + summary["task_bootstrap_95ci"] for summary in method_summaries + ]) * 100.0 + positions = np.arange(len(METHODS))[::-1] + axes[0].barh( + positions, + method_means, + color=[color for _, _, color in METHODS], + height=0.70, + edgecolor="white", + linewidth=0.4, + ) + axes[0].errorbar( + method_means, + positions, + xerr=np.vstack(( + method_means - method_intervals[:, 0], + method_intervals[:, 1] - method_means, + )), + fmt="none", + ecolor="#222222", + elinewidth=0.8, + capsize=2.2, + ) + axes[0].set_yticks( + positions, [label.replace("\n", " ") for _, label, _ in METHODS]) + axes[0].set_xlabel("Final classification error (%)") + axes[0].set_title("(a) Nonlinear CLLN with component errors") + + sweep_specs = ( + ("pedestal_mismatch", (0.01, 0.025, 0.05, 0.1, 0.25, 0.5), + "Pedestal mismatch", "#D55E00", "o"), + ("gain_mismatch", (0.001, 0.005, 0.01, 0.025, 0.05), + "Gain mismatch", "#0072B2", "D"), + ) + for prefix, values, label, color, marker in sweep_specs: + summaries = [ + analysis["sampler_conditions"][f"{prefix}_{value:g}"] + for value in values + ] + x = np.asarray([ + summary["median_applied_rate_rmse_v_per_s"] + for summary in summaries + ]) + errorbar( + axes[1], x, summaries, color=color, marker=marker, + markersize=4.5, linewidth=1.35, label=label, + ) + axes[1].set_xscale("log") + axes[1].set_xlabel("Residual sampling error (V/s, RMSE)") + axes[1].set_ylabel("Final classification error (%)") + axes[1].set_title("(b) Local sample-path mismatch") + axes[1].legend(frameon=False, loc="upper left") + + combined = [ + analysis["sampler_conditions"][condition] + for condition, _ in COMBINED_CONDITIONS + ] + combined_means = np.asarray([ + summary["mean"] for summary in combined]) * 100.0 + combined_intervals = np.asarray([ + summary["task_bootstrap_95ci"] for summary in combined + ]) * 100.0 + positions = np.arange(len(COMBINED_CONDITIONS))[::-1] + axes[2].barh( + positions, + combined_means, + color="#0072B2", + height=0.70, + edgecolor="white", + linewidth=0.4, + ) + axes[2].errorbar( + combined_means, + positions, + xerr=np.vstack(( + combined_means - combined_intervals[:, 0], + combined_intervals[:, 1] - combined_means, + )), + fmt="none", + ecolor="#222222", + elinewidth=0.8, + capsize=2.2, + ) + axes[2].set_yticks( + positions, + [label.replace("\n", " ") for _, label in COMBINED_CONDITIONS], + ) + axes[2].set_xlabel("Final classification error (%)") + axes[2].set_title("(c) Nonideal local sampling") + + axes[0].set_xlim(-0.8, 32.0) + axes[1].set_ylim(-0.8, 32.0) + axes[2].set_xlim(-0.8, 32.0) + for index, axis in enumerate(axes): + axis.grid( + axis="y" if index == 1 else "x", + color="#D9D9D9", linewidth=0.55, alpha=0.8) + axis.tick_params(length=3) + figure.text( + 0.995, + 0.005, + "40 tasks × 4 device draws; error bars are task-bootstrap 95% intervals", + ha="right", + va="bottom", + fontsize=7, + color="#666666", + ) + figure.tight_layout(rect=(0.0, 0.065, 1.0, 1.0), w_pad=2.0) + 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() + physical = json.loads(args.physical.read_text()) + sampler = json.loads(args.sampler.read_text()) + spice = json.loads(args.spice.read_text()) + analysis = build_analysis( + physical, + sampler, + spice, + replicates=args.bootstrap_replicates, + seed=args.bootstrap_seed, + ) + analysis["sources"] = [ + str(args.physical), str(args.sampler), str(args.spice)] + 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({ + "physical_methods": analysis["physical_methods"], + "combined_strong": analysis["sampler_conditions"]["combined_strong"], + }, 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/figs/figure_physical_hardware_evidence.pdf b/results/figs/figure_physical_hardware_evidence.pdf new file mode 100644 index 0000000..6e3aeac Binary files /dev/null and b/results/figs/figure_physical_hardware_evidence.pdf differ diff --git a/results/figs/figure_physical_hardware_evidence.png b/results/figs/figure_physical_hardware_evidence.png new file mode 100644 index 0000000..d78f8ea Binary files /dev/null and b/results/figs/figure_physical_hardware_evidence.png differ diff --git a/results/figs/figure_physical_hardware_evidence.svg b/results/figs/figure_physical_hardware_evidence.svg new file mode 100644 index 0000000..9efa4a8 --- /dev/null +++ b/results/figs/figure_physical_hardware_evidence.svg @@ -0,0 +1,1082 @@ + + + + + + + + 2026-08-29T18:34:08.451813 + image/svg+xml + + + Matplotlib v3.10.8, https://matplotlib.org/ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 10 + + + + + + + + + + + + + 20 + + + + + + + + + + + + + 30 + + + + Final classification error (%) + + + + + + + + + + + + + + Clean + + + + + + + + + + Raw + + + + + + + + + + Static calibration + + + + + + + + + + Overclamp + + + + + + + + + + SDIL + + + + + + + + + + Overclamp + SDIL + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (a) Nonlinear CLLN with component errors + + + + + + + + + + + + + + + + + + 1 + 0 + + 2 + + + + + + + + + + + + + + + 1 + 0 + + 1 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Residual sampling error (V/s, RMSE) + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 5 + + + + + + + + + + + + + 10 + + + + + + + + + + + + + 15 + + + + + + + + + + + + + 20 + + + + + + + + + + + + + 25 + + + + + + + + + + + + + 30 + + + + Final classification error (%) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (b) Local sample-path mismatch + + + + + + + + + + + + + + + + + + + + + + + + + Pedestal mismatch + + + + + + + + + + + + + + + + + + + + + + + + Gain mismatch + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 0 + + + + + + + + + + + + + 10 + + + + + + + + + + + + + 20 + + + + + + + + + + + + + 30 + + + + Final classification error (%) + + + + + + + + + + + Ideal CDS + + + + + + + + + + Common pedestal + + + + + + + + + + Sample noise + + + + + + + + + + Refresh every 4 + + + + + + + + + + Mild combined + + + + + + + + + + Strong combined + + + + + + + + + + Overclamp + mild + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + (c) Nonideal local sampling + + + + 40 tasks × 4 device draws; error bars are task-bootstrap 95% intervals + + + + + + + + + + + + + + diff --git a/results/physical_bias/p10_hardware_evidence_analysis.json b/results/physical_bias/p10_hardware_evidence_analysis.json new file mode 100644 index 0000000..9811f5c --- /dev/null +++ b/results/physical_bias/p10_hardware_evidence_analysis.json @@ -0,0 +1,308 @@ +{ + "analysis": "physical_clln_hardware_evidence_figure", + "confirmatory": false, + "bootstrap": { + "unit": "task; four device draws averaged within task", + "task_clusters": 40, + "replicates": 20000, + "seed": 20260829, + "interval": "percentile 95%" + }, + "physical_methods": { + "clean": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160 + }, + "raw": { + "mean": 0.25859375, + "task_bootstrap_95ci": [ + 0.22265625, + 0.29609375 + ], + "task_clusters": 40, + "trials": 160 + }, + "constant": { + "mean": 0.0375, + "task_bootstrap_95ci": [ + 0.01875, + 0.05859375 + ], + "task_clusters": 40, + "trials": 160 + }, + "overclamp": { + "mean": 0.04765625, + "task_bootstrap_95ci": [ + 0.01875, + 0.0828125 + ], + "task_clusters": 40, + "trials": 160 + }, + "sdil": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160 + }, + "overclamp_sdil": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160 + } + }, + "sampler_conditions": { + "combined_mild_refresh4": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.46660538253621064 + }, + "combined_strong": { + "mean": 0.00546875, + "task_bootstrap_95ci": [ + 0.00078125, + 0.01171875 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 0.96875, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.36811707519239995 + }, + "common_pedestal_10": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 7.092845963015098e-16 + }, + "gain_mismatch_0.001": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.0021327524546310593 + }, + "gain_mismatch_0.005": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.010662411888727154 + }, + "gain_mismatch_0.01": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.021335094097404632 + }, + "gain_mismatch_0.025": { + "mean": 0.0015625, + "task_bootstrap_95ci": [ + 0.0, + 0.00390625 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 0.9875, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.05337234986008901 + }, + "gain_mismatch_0.05": { + "mean": 0.00625, + "task_bootstrap_95ci": [ + 0.00078125, + 0.01328125 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 0.9625, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.10745724630761008 + }, + "ideal_cds": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.0 + }, + "overclamp_plus_combined_mild": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.14562285807356745 + }, + "pedestal_mismatch_0.01": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.010003519663837685 + }, + "pedestal_mismatch_0.025": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.02500879915959358 + }, + "pedestal_mismatch_0.05": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.05001759831918712 + }, + "pedestal_mismatch_0.1": { + "mean": 0.00625, + "task_bootstrap_95ci": [ + 0.0015625, + 0.0125 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 0.95625, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.10003519663837424 + }, + "pedestal_mismatch_0.25": { + "mean": 0.04453125, + "task_bootstrap_95ci": [ + 0.0203125, + 0.07421875 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 0.825, + "solver_failure_fraction": 0.00625, + "median_applied_rate_rmse_v_per_s": 0.25008799159594736 + }, + "pedestal_mismatch_0.5": { + "mean": 0.096875, + "task_bootstrap_95ci": [ + 0.059375, + 0.13828125 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 0.70625, + "solver_failure_fraction": 0.0125, + "median_applied_rate_rmse_v_per_s": 0.5001759831918947 + }, + "refresh_every_4": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 0.4399296286060528 + }, + "sample_noise_1": { + "mean": 0.0, + "task_bootstrap_95ci": [ + 0.0, + 0.0 + ], + "task_clusters": 40, + "trials": 160, + "zero_error_fraction": 1.0, + "solver_failure_fraction": 0.0, + "median_applied_rate_rmse_v_per_s": 1.4156351463196133 + } + }, + "spice_primitive": { + "publication_evidence": false, + "scope": "Generic switch-capacitor acquisition and droop sanity check; this is not the complete CLLN edge circuit.", + "configuration_count": 48, + "fraction_below_1_percent_error_at_start": 0.4166666666666667, + "fraction_below_1_percent_error_at_end": 0.4166666666666667, + "reference_configuration": { + "acquisition_seconds": 1e-06, + "switch_resistance_ohm": 200.0, + "hold_capacitance_f": 1e-09, + "leakage_resistance_ohm": 1000000000.0, + "held_at_switch_open_v": 0.004966396, + "residual_at_learning_start_v": 0.001033604, + "residual_at_learning_end_v": 0.001034101, + "start_relative_error": 0.03360400000000001, + "end_relative_error": 0.03410099999999992 + } + }, + "sources": [ + "results/physical_bias/p5_full_grid_bias_crossover.json", + "results/physical_bias/p9_grid_correlated_autozero.json", + "results/physical_bias/p8_spice_autozero_primitive.json" + ] +} diff --git a/results/physical_bias/p10_hardware_evidence_source.csv b/results/physical_bias/p10_hardware_evidence_source.csv new file mode 100644 index 0000000..a2aec46 --- /dev/null +++ b/results/physical_bias/p10_hardware_evidence_source.csv @@ -0,0 +1,25 @@ +panel,series,condition,x,x_unit,mean_error,ci_low,ci_high,zero_error_fraction +a,physical_method,Clean,,,0.0,0.0,0.0, +a,physical_method,Raw,,,0.25859375,0.22265625,0.29609375, +a,physical_method,Static calibration,,,0.0375,0.01875,0.05859375, +a,physical_method,Overclamp,,,0.04765625,0.01875,0.0828125, +a,physical_method,SDIL,,,0.0,0.0,0.0, +a,physical_method,Overclamp + SDIL,,,0.0,0.0,0.0, +b,pedestal mismatch,pedestal_mismatch_0.01,0.010003519663837685,V/s,0.0,0.0,0.0,1.0 +b,pedestal mismatch,pedestal_mismatch_0.025,0.02500879915959358,V/s,0.0,0.0,0.0,1.0 +b,pedestal mismatch,pedestal_mismatch_0.05,0.05001759831918712,V/s,0.0,0.0,0.0,1.0 +b,pedestal mismatch,pedestal_mismatch_0.1,0.10003519663837424,V/s,0.00625,0.0015625,0.0125,0.95625 +b,pedestal mismatch,pedestal_mismatch_0.25,0.25008799159594736,V/s,0.04453125,0.0203125,0.07421875,0.825 +b,pedestal mismatch,pedestal_mismatch_0.5,0.5001759831918947,V/s,0.096875,0.059375,0.13828125,0.70625 +b,gain mismatch,gain_mismatch_0.001,0.0021327524546310593,V/s,0.0,0.0,0.0,1.0 +b,gain mismatch,gain_mismatch_0.005,0.010662411888727154,V/s,0.0,0.0,0.0,1.0 +b,gain mismatch,gain_mismatch_0.01,0.021335094097404632,V/s,0.0,0.0,0.0,1.0 +b,gain mismatch,gain_mismatch_0.025,0.05337234986008901,V/s,0.0015625,0.0,0.00390625,0.9875 +b,gain mismatch,gain_mismatch_0.05,0.10745724630761008,V/s,0.00625,0.00078125,0.01328125,0.9625 +c,sampling condition,Ideal CDS,,,0.0,0.0,0.0,1.0 +c,sampling condition,Common pedestal,,,0.0,0.0,0.0,1.0 +c,sampling condition,Sample noise,,,0.0,0.0,0.0,1.0 +c,sampling condition,Refresh every 4,,,0.0,0.0,0.0,1.0 +c,sampling condition,Mild combined,,,0.0,0.0,0.0,1.0 +c,sampling condition,Strong combined,,,0.00546875,0.00078125,0.01171875,0.96875 +c,sampling condition,Overclamp + mild,,,0.0,0.0,0.0,1.0 diff --git a/visual-composer/physical-hardware-evidence.md b/visual-composer/physical-hardware-evidence.md new file mode 100644 index 0000000..b271127 --- /dev/null +++ b/visual-composer/physical-hardware-evidence.md @@ -0,0 +1,40 @@ +# Visual contract: hardware-realistic CLLN evidence + +- **Artifact:** three-panel quantitative result figure. +- **Target venue / format:** ICLR two-column paper, full-width figure. +- **Core claim:** local innovation subtraction restores nonlinear CLLN + learning under published component imperfections and remains effective with + nonideal local sampling. +- **Reviewer question:** does the method survive component mismatch, sampler + mismatch, noise, stale refresh, and their combination at the task endpoint? +- **Evidence layer:** hardware-realistic simulation; no fabricated-chip claim. +- **Source data:** + `results/physical_bias/p5_full_grid_bias_crossover.json`, + `results/physical_bias/p9_grid_correlated_autozero.json`, and + `results/physical_bias/p8_spice_autozero_primitive.json`. +- **Statistics / uncertainty:** four device draws are averaged within each of + 40 tasks; tasks are resampled for percentile 95% bootstrap intervals. +- **Figure prototype:** horizontal method comparison, mismatch response curves, + and horizontal nonideality comparison. +- **Panel map:** + - (a) nonlinear CLLN final error across clean, raw, calibration, + overclamping, SDIL, and their combination; + - (b) final error against measured local sampling-error RMSE for pedestal + and gain mismatch sweeps; + - (c) final error under common pedestal, sampling noise, stale neutral + refresh, and combined nonidealities. +- **Exact label inventory:** nonlinear CLLN, component errors, sample-path + mismatch, residual sampling error, nonideal local sampling, final + classification error. +- **Caption role:** identify the published component-error scale, the local + correlated-double-sampling operation, task/device counts, and the boundary + between simulation and fabricated hardware. +- **Manuscript placement:** Part 3, after the digital scaling result. +- **Output formats:** editable SVG, vector PDF, PNG preview, analysis JSON, and + source CSV. +- **Traceability:** every plotted aggregate is regenerated by + `experiments/plot_physical_hardware_evidence.py`; the SPICE primitive is + carried in the analysis manifest and reserved for supplementary evidence. +- **Constraint:** circuit-solver failures remain classification failures. + Sampler RMSE excludes the missing diagnostic value on failed solves, while + the task endpoint includes every trial. diff --git a/visual-composer/qa-ledger.md b/visual-composer/qa-ledger.md index 319c1f2..c9e66e5 100644 --- a/visual-composer/qa-ledger.md +++ b/visual-composer/qa-ledger.md @@ -13,3 +13,16 @@ detached source note at 2,556-by-778 PNG resolution. The next QA pass replaces the pilot source with the frozen multi-task, multi-device confirmation without changing the plot grammar. +## Hardware-realistic CLLN figure + +| Issue | Artifact | Severity | Fix | Status | +|:--|:--|:--|:--|:--| +| Method and condition labels collided | Panels (a) and (c) | High | Replaced vertical bars with horizontal bars and single-line labels | Resolved | +| Different mismatch units were not directly comparable | Panel (b) | High | Used measured residual local-rate RMSE as the common x-axis and retained mismatch family as series | Resolved | +| Zero-error conditions were visually ambiguous | Panels (a) and (c) | Low | Kept a small x-axis margin below zero so zero-valued error bars remain visible | Resolved | +| Generic SPICE primitive could be read as a full-circuit simulation | Figure scope | High | Excluded it from the main plotted panels and recorded its limited scope in the analysis and visual contract | Resolved | +| Color-only distinctions could fail in print | Panel (b) | Medium | Added distinct circle and diamond markers | Resolved | + +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. -- cgit v1.2.3