summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorYurenHao0426 <Blackhao0426@gmail.com>2026-07-23 08:17:56 -0500
committerYurenHao0426 <Blackhao0426@gmail.com>2026-07-23 08:17:56 -0500
commit75a64888cb3e117ee6d232adc7b48974647fbc1e (patch)
tree4a79340ec2f2f41b267152180f14a5e696b5fca6
parent9a8c0570f8d96cc3bca200ed872323e44e60f55f (diff)
figures: add audited oral-B-v2 confirmation panel
-rw-r--r--experiments/plot_bci_v2_confirmation.py533
-rw-r--r--results/figs/figure5_bci_v2.pdfbin0 -> 30918 bytes
-rw-r--r--results/figs/figure5_bci_v2.pngbin0 -> 572434 bytes
-rw-r--r--results/figs/figure5_bci_v2_caption.md20
-rw-r--r--results/figs/figure5_bci_v2_manifest.json78
5 files changed, 631 insertions, 0 deletions
diff --git a/experiments/plot_bci_v2_confirmation.py b/experiments/plot_bci_v2_confirmation.py
new file mode 100644
index 0000000..6f08ea7
--- /dev/null
+++ b/experiments/plot_bci_v2_confirmation.py
@@ -0,0 +1,533 @@
+#!/usr/bin/env python3
+"""Render the audited oral-B-v2 confirmation figure from all 30 records."""
+import argparse
+import hashlib
+import json
+import math
+import os
+import statistics
+
+import matplotlib
+
+matplotlib.use("Agg")
+import matplotlib.pyplot as plt
+import numpy as np
+
+
+TASK_SEEDS = tuple(range(30, 36))
+MODEL_SEEDS = tuple(range(5))
+PROTOCOL = "oral_b_v2_calibrated_recovery_confirmation_v1"
+BLUE = "#0072B2"
+LIGHT_BLUE = "#56B4E9"
+GREEN = "#009E73"
+ORANGE = "#E69F00"
+RED = "#D55E00"
+GRAY = "#5A5A5A"
+LIGHT_GRAY = "#B7B7B7"
+PDF_METADATA = {
+ "Creator": "SDIL audited oral-B-v2 figure pipeline",
+ "Producer": "Matplotlib",
+ "CreationDate": None,
+ "ModDate": None,
+}
+
+
+def load_json(path):
+ with open(path) as handle:
+ return json.load(handle)
+
+
+def sha256(path):
+ digest = hashlib.sha256()
+ with open(path, "rb") as handle:
+ for chunk in iter(lambda: handle.read(1024 * 1024), b""):
+ digest.update(chunk)
+ return digest.hexdigest()
+
+
+def require(condition, message):
+ if not condition:
+ raise RuntimeError(message)
+
+
+def finite_leaves(value):
+ if isinstance(value, bool) or value is None:
+ return True
+ if isinstance(value, (int, float)):
+ return math.isfinite(float(value))
+ if isinstance(value, dict):
+ return all(finite_leaves(child) for child in value.values())
+ if isinstance(value, (list, tuple)):
+ return all(finite_leaves(child) for child in value)
+ return True
+
+
+def all_true(value):
+ if isinstance(value, dict):
+ return all(all_true(child) for child in value.values())
+ return value is True
+
+
+def load_panel(input_dir, gate_path):
+ gate = load_json(gate_path)
+ require(gate.get("protocol") == PROTOCOL, "oral-B-v2 protocol drift")
+ require(gate.get("status") == "passed", "oral-B-v2 gate is not passed")
+ require(gate.get("review_score_after") == 8, "oral-B-v2 score drift")
+ require(all_true(gate.get("checks", {})), "oral-B-v2 has false checks")
+ records = {}
+ paths = []
+ for task_seed in TASK_SEEDS:
+ for model_seed in MODEL_SEEDS:
+ name = (
+ f"bci_v2_calibrated_confirm_t{task_seed}"
+ f"_m{model_seed}.json"
+ )
+ path = os.path.join(input_dir, name)
+ require(os.path.isfile(path), f"missing oral-B-v2 record: {path}")
+ row = load_json(path)
+ paths.append(path)
+ require(row.get("schema_version") == 4, f"{path}: schema")
+ require(
+ row.get("protocol", {}).get("name") == PROTOCOL
+ and row["protocol"].get("split")
+ == "untouched_confirmation"
+ and row.get("split") == "untouched_confirmation",
+ f"{path}: split/protocol",
+ )
+ require(
+ row.get("args", {}).get("task_seed") == task_seed
+ and row["args"].get("model_seed") == model_seed,
+ f"{path}: seed",
+ )
+ require(
+ row.get("provenance", {}).get("git_tracked_dirty") is False
+ and row["provenance"].get("git_commit")
+ == gate.get("source_commit"),
+ f"{path}: provenance",
+ )
+ require(
+ row.get("finite") is True and finite_leaves(row),
+ f"{path}: finite",
+ )
+ records[(task_seed, model_seed)] = row
+ observed = sorted(
+ name for name in os.listdir(input_dir) if name.endswith(".json")
+ )
+ expected = sorted(
+ (
+ f"bci_v2_calibrated_confirm_t{task_seed}"
+ f"_m{model_seed}.json"
+ )
+ for task_seed in TASK_SEEDS
+ for model_seed in MODEL_SEEDS
+ )
+ require(observed == expected, "oral-B-v2 record-set drift")
+ return gate, records, paths
+
+
+def task_cluster_array(records, getter):
+ return np.asarray([
+ np.mean([
+ getter(records[(task_seed, model_seed)])
+ for model_seed in MODEL_SEEDS
+ ], axis=0)
+ for task_seed in TASK_SEEDS
+ ], dtype=float)
+
+
+def style_axis(axis):
+ axis.spines["top"].set_visible(False)
+ axis.spines["right"].set_visible(False)
+ axis.tick_params(width=1.0, length=4)
+ axis.grid(axis="y", color="#E7E7E7", linewidth=0.8, zorder=0)
+
+
+def panel_label(axis, label):
+ axis.text(
+ -0.14,
+ 1.08,
+ label,
+ transform=axis.transAxes,
+ fontsize=15,
+ fontweight="bold",
+ va="top",
+ )
+
+
+def plot_learning(axis, records):
+ conditions = (
+ ("intact", "SDIL + TD critic", BLUE),
+ ("critic_training_lesion", "critic lesion", ORANGE),
+ ("outcome_training_lesion", "outcome lesion", GREEN),
+ ("fixed_role", "fixed role", LIGHT_GRAY),
+ )
+ days = np.arange(1, 15)
+ for condition, label, color in conditions:
+ clustered = task_cluster_array(
+ records,
+ lambda row: np.asarray(
+ row["conditions"][condition]["daily_success"]
+ ) * 100,
+ )
+ mean = clustered.mean(0)
+ interval = 1.96 * clustered.std(0, ddof=1) / np.sqrt(len(TASK_SEEDS))
+ axis.plot(days, mean, color=color, linewidth=2.0, label=label)
+ axis.fill_between(
+ days, mean - interval, mean + interval,
+ color=color, alpha=0.12, linewidth=0,
+ )
+ axis.set_xlim(1, 14)
+ axis.set_ylim(-3, 104)
+ axis.set_xticks([1, 4, 7, 10, 14])
+ axis.set_xlabel("Training day")
+ axis.set_ylabel("Successful episodes (%)")
+ axis.set_title(
+ "Local actor–critic learns",
+ loc="left",
+ fontsize=11.5,
+ fontweight="bold",
+ pad=10,
+ )
+ axis.legend(frameon=False, fontsize=8, loc="upper left")
+ style_axis(axis)
+ panel_label(axis, "a")
+
+
+def plot_residualization(axis, records):
+ raw = task_cluster_array(
+ records,
+ lambda row: row["signatures"]["mean_abs_raw_soma_corr"],
+ )
+ residual = task_cluster_array(
+ records,
+ lambda row: row["signatures"]["mean_abs_residual_soma_corr"],
+ )
+ for index in range(len(TASK_SEEDS)):
+ axis.plot(
+ [0, 1],
+ [raw[index], residual[index]],
+ color=LIGHT_GRAY,
+ linewidth=1.3,
+ zorder=1,
+ )
+ axis.scatter(
+ np.zeros_like(raw), raw, s=32, facecolor="white",
+ edgecolor=GRAY, zorder=2,
+ )
+ axis.scatter(
+ np.ones_like(residual), residual, s=34, facecolor=BLUE,
+ edgecolor="white", zorder=3,
+ )
+ axis.scatter(
+ [0, 1], [raw.mean(), residual.mean()],
+ marker="D", s=65, c=[GRAY, BLUE], edgecolor="#111111",
+ linewidth=0.7, zorder=4,
+ )
+ axis.axhline(0.10, color=RED, linestyle="--", linewidth=1.0)
+ axis.text(
+ 0.98, 0.115, "frozen residual ceiling",
+ ha="right", va="bottom", color=RED, fontsize=8,
+ )
+ axis.set_xlim(-0.35, 1.35)
+ axis.set_ylim(0, 1.06)
+ axis.set_xticks([0, 1], ["Raw apical", "Innovation"])
+ axis.set_ylabel("Mean |soma correlation|")
+ axis.set_title(
+ "Prediction removes ordinary traffic",
+ loc="left",
+ fontsize=11.5,
+ fontweight="bold",
+ pad=10,
+ )
+ axis.text(
+ 0.05,
+ 0.48,
+ f"raw $-$ residual = {raw.mean() - residual.mean():.3f}\n"
+ f"surrounding decoder = "
+ f"{100 * task_cluster_array(records, lambda row: row['signatures']['surrounding_event_decoder_balanced_acc']).mean():.2f}%",
+ transform=axis.transAxes,
+ fontsize=8.5,
+ bbox={
+ "boxstyle": "round,pad=0.35",
+ "facecolor": "white",
+ "edgecolor": "#D6D6D6",
+ },
+ )
+ style_axis(axis)
+ panel_label(axis, "b")
+
+
+def plot_psychometric(axis, records):
+ quantiles = np.asarray([0.20, 0.35, 0.50, 0.65, 0.80])
+
+ def success_curve(row):
+ targets = row["assays"]["challenge"]["targets"]
+ values = row["signatures"]["target_success_fraction"]
+ return np.asarray([values[str(target)] for target in targets]) * 100
+
+ clustered = task_cluster_array(records, success_curve)
+ mean = clustered.mean(0)
+ interval = 1.96 * clustered.std(0, ddof=1) / np.sqrt(len(TASK_SEEDS))
+ expected = (1.0 - quantiles) * 100
+ axis.plot(
+ quantiles, expected, color=LIGHT_GRAY, linestyle="--",
+ linewidth=1.5, label="calibration expectation",
+ )
+ axis.plot(
+ quantiles, mean, color=GREEN, marker="o", linewidth=2.0,
+ label="independent assay",
+ )
+ axis.fill_between(
+ quantiles, mean - interval, mean + interval,
+ color=GREEN, alpha=0.15, linewidth=0,
+ )
+ axis.axhline(50, color="#DDDDDD", linewidth=0.9)
+ axis.set_xlim(0.16, 0.84)
+ axis.set_ylim(10, 90)
+ axis.set_xticks(quantiles)
+ axis.set_xlabel("Calibration quantile used as target")
+ axis.set_ylabel("Rewarded trials (%)")
+ axis.set_title(
+ "Label-free calibration generalizes",
+ loc="left",
+ fontsize=11.5,
+ fontweight="bold",
+ pad=10,
+ )
+ axis.legend(frameon=False, fontsize=8, loc="upper right")
+ axis.text(
+ 0.04,
+ 0.05,
+ f"pooled success = "
+ f"{100 * task_cluster_array(records, lambda row: row['signatures']['challenge_success_fraction']).mean():.2f}%",
+ transform=axis.transAxes,
+ fontsize=8.6,
+ )
+ style_axis(axis)
+ panel_label(axis, "c")
+
+
+def plot_outcome(axis, records, gate):
+ keys = (
+ (
+ "terminal_previous_soma_outcome_balanced_acc",
+ "Pre-outcome\nsoma",
+ LIGHT_GRAY,
+ ),
+ (
+ "acute_outcome_lesion_outcome_balanced_acc",
+ "Innovation\noutcome lesion",
+ ORANGE,
+ ),
+ (
+ "terminal_residual_outcome_balanced_acc",
+ "Innovation\nintact",
+ BLUE,
+ ),
+ )
+ values = [
+ task_cluster_array(
+ records, lambda row, key=key: row["signatures"][key]
+ ) * 100
+ for key, _, _ in keys
+ ]
+ for index, ((_, _, color), clustered) in enumerate(zip(keys, values)):
+ axis.bar(
+ index, clustered.mean(), color=color, width=0.66,
+ edgecolor="white", zorder=2,
+ )
+ jitter = np.linspace(-0.13, 0.13, len(clustered))
+ axis.scatter(
+ index + jitter, clustered, s=22, facecolor="white",
+ edgecolor="#333333", linewidth=0.7, zorder=3,
+ )
+ axis.axhline(50, color=RED, linestyle="--", linewidth=1.0)
+ axis.set_ylim(45, 103)
+ axis.set_xticks(range(len(keys)), [item[1] for item in keys])
+ axis.set_ylabel("Outcome balanced accuracy (%)")
+ axis.set_title(
+ "Residual carries outcome surprise",
+ loc="left",
+ fontsize=11.5,
+ fontweight="bold",
+ pad=10,
+ )
+ drop = gate["metrics"]["outcome_lesion_drop"]
+ critic = gate["metrics"]["critic_expectedness"]
+ axis.text(
+ 0.04,
+ 0.95,
+ "role separation lesion\n"
+ f"$\\Delta$ = {drop['mean']:.3f} "
+ f"(LCB {drop['one_sided_95pct_lower']:.3f})\n"
+ "critic expectedness\n"
+ f"{critic['mean']:.3f} "
+ f"(LCB {critic['one_sided_95pct_lower']:.3f})",
+ transform=axis.transAxes,
+ va="top",
+ fontsize=8.3,
+ bbox={
+ "boxstyle": "round,pad=0.35",
+ "facecolor": "white",
+ "edgecolor": "#D6D6D6",
+ "alpha": 0.96,
+ },
+ )
+ style_axis(axis)
+ panel_label(axis, "d")
+
+
+def write_caption(path):
+ caption = """# Oral-B-v2 confirmation figure caption
+
+**Figure 5 | Role-vectorized somato-dendritic innovation carries
+temporal-difference outcome surprise.** All summaries use the complete
+untouched six-task by five-model confirmation; uncertainty is computed after
+averaging models within each task seed. **a,** Mean episode success over
+training. The learned causal-role vector is necessary, the forward-plasticity
+lesion (omitted for visual overlap with zero) does not learn, and critic or
+terminal-outcome training lesions are reported rather than hidden. Bands are
+95% normal intervals over six task clusters. **b,** Ordinary apical traffic is
+almost perfectly soma-correlated before subtraction; the innovation is below
+the frozen correlation ceiling in every task cluster. **c,** Five targets are
+chosen from fixed quantiles of 512 outcome-free cursor-max calibration trials.
+The displayed rewarded fractions come from independent assay trajectories;
+no evaluation label selects or reweights a target. **d,** Terminal innovation
+decodes rewarded versus timeout outcomes, while the acute terminal-outcome
+lesion removes role-aligned separation. The critic contribution tracks the
+stored value prediction and attenuates expected outcomes. Exact role maps,
+outcome labels, and gradients are diagnostic-only and never used by the
+learner's updates.
+"""
+ with open(path, "w") as handle:
+ handle.write(caption)
+
+
+def main():
+ parser = argparse.ArgumentParser()
+ parser.add_argument(
+ "--input-dir",
+ default="results/bci_v2_calibrated_confirmation",
+ )
+ parser.add_argument(
+ "--gate",
+ default="results/bci_v2_calibrated_confirmation_gate.json",
+ )
+ parser.add_argument("--outdir", default="results/figs")
+ args = parser.parse_args()
+ gate, records, paths = load_panel(args.input_dir, args.gate)
+ os.makedirs(args.outdir, exist_ok=True)
+ plt.rcParams.update({
+ "font.family": "DejaVu Sans",
+ "font.size": 9.5,
+ "axes.linewidth": 1.0,
+ "xtick.major.width": 1.0,
+ "ytick.major.width": 1.0,
+ "pdf.fonttype": 42,
+ "ps.fonttype": 42,
+ })
+ figure, axes = plt.subplots(2, 2, figsize=(10.5, 7.8))
+ plot_learning(axes[0, 0], records)
+ plot_residualization(axes[0, 1], records)
+ plot_psychometric(axes[1, 0], records)
+ plot_outcome(axes[1, 1], records, gate)
+ figure.subplots_adjust(
+ left=0.09, right=0.98, bottom=0.10, top=0.93,
+ hspace=0.42, wspace=0.30,
+ )
+ pdf_path = os.path.join(args.outdir, "figure5_bci_v2.pdf")
+ png_path = os.path.join(args.outdir, "figure5_bci_v2.png")
+ caption_path = os.path.join(
+ args.outdir, "figure5_bci_v2_caption.md"
+ )
+ manifest_path = os.path.join(
+ args.outdir, "figure5_bci_v2_manifest.json"
+ )
+ figure.savefig(pdf_path, metadata=PDF_METADATA)
+ figure.savefig(png_path, dpi=320)
+ plt.close(figure)
+ write_caption(caption_path)
+
+ def mean_metric(key):
+ return float(task_cluster_array(
+ records, lambda row: row["signatures"][key]
+ ).mean())
+
+ terminal_previous_soma = mean_metric(
+ "terminal_previous_soma_outcome_balanced_acc"
+ )
+ terminal_outcome_lesion = mean_metric(
+ "acute_outcome_lesion_outcome_balanced_acc"
+ )
+ manifest = {
+ "strict": True,
+ "protocol": PROTOCOL,
+ "gate_status": gate["status"],
+ "task_seeds": list(TASK_SEEDS),
+ "model_seeds": list(MODEL_SEEDS),
+ "record_count": len(records),
+ "statistics": {
+ "intact_final_mean": gate["metrics"]["intact_final"]["mean"],
+ "learning_gain_mean": gate["metrics"]["intact_gain"]["mean"],
+ "fixed_role_gap_mean": gate["metrics"]["fixed_role_gap"]["mean"],
+ "role_cosine_mean": gate["metrics"]["role_cosine"]["mean"],
+ "residual_soma_corr_mean":
+ gate["metrics"]["residual_soma_corr"]["mean"],
+ "raw_residual_corr_gap_mean":
+ gate["metrics"]["raw_residual_corr_gap"]["mean"],
+ "surrounding_accuracy_mean":
+ gate["metrics"]["surrounding_accuracy"]["mean"],
+ "surrounding_accuracy_lower":
+ gate["metrics"]["surrounding_accuracy"][
+ "one_sided_95pct_lower"
+ ],
+ "decoder_corr_mean": gate["metrics"]["decoder_corr"]["mean"],
+ "sign_inversion_mean":
+ gate["metrics"]["sign_inversion"]["mean"],
+ "positive_sign_count": gate["positive_sign_count"],
+ "velocity_advantage_mean":
+ gate["metrics"]["velocity_advantage"]["mean"],
+ "challenge_fraction_mean":
+ gate["metrics"]["challenge_fraction"]["mean"],
+ "terminal_accuracy_mean":
+ gate["metrics"]["terminal_accuracy"]["mean"],
+ "terminal_accuracy_lower":
+ gate["metrics"]["terminal_accuracy"][
+ "one_sided_95pct_lower"
+ ],
+ "terminal_previous_soma_accuracy_mean":
+ terminal_previous_soma,
+ "terminal_outcome_lesion_accuracy_mean":
+ terminal_outcome_lesion,
+ "terminal_separation_mean":
+ gate["metrics"]["terminal_separation"]["mean"],
+ "outcome_lesion_drop_mean":
+ gate["metrics"]["outcome_lesion_drop"]["mean"],
+ "outcome_lesion_drop_lower":
+ gate["metrics"]["outcome_lesion_drop"][
+ "one_sided_95pct_lower"
+ ],
+ "critic_expectedness_mean":
+ gate["metrics"]["critic_expectedness"]["mean"],
+ "critic_expectedness_lower":
+ gate["metrics"]["critic_expectedness"][
+ "one_sided_95pct_lower"
+ ],
+ },
+ "source_sha256": {
+ os.path.relpath(path): sha256(path)
+ for path in sorted(paths + [args.gate])
+ },
+ }
+ with open(manifest_path, "w") as handle:
+ json.dump(manifest, handle, indent=2, sort_keys=True)
+ handle.write("\n")
+ print(json.dumps({
+ "manifest": manifest_path,
+ "png": png_path,
+ "status": "passed",
+ }, indent=2))
+
+
+if __name__ == "__main__":
+ main()
diff --git a/results/figs/figure5_bci_v2.pdf b/results/figs/figure5_bci_v2.pdf
new file mode 100644
index 0000000..deff043
--- /dev/null
+++ b/results/figs/figure5_bci_v2.pdf
Binary files differ
diff --git a/results/figs/figure5_bci_v2.png b/results/figs/figure5_bci_v2.png
new file mode 100644
index 0000000..f1eb506
--- /dev/null
+++ b/results/figs/figure5_bci_v2.png
Binary files differ
diff --git a/results/figs/figure5_bci_v2_caption.md b/results/figs/figure5_bci_v2_caption.md
new file mode 100644
index 0000000..41e0dbd
--- /dev/null
+++ b/results/figs/figure5_bci_v2_caption.md
@@ -0,0 +1,20 @@
+# Oral-B-v2 confirmation figure caption
+
+**Figure 5 | Role-vectorized somato-dendritic innovation carries
+temporal-difference outcome surprise.** All summaries use the complete
+untouched six-task by five-model confirmation; uncertainty is computed after
+averaging models within each task seed. **a,** Mean episode success over
+training. The learned causal-role vector is necessary, the forward-plasticity
+lesion (omitted for visual overlap with zero) does not learn, and critic or
+terminal-outcome training lesions are reported rather than hidden. Bands are
+95% normal intervals over six task clusters. **b,** Ordinary apical traffic is
+almost perfectly soma-correlated before subtraction; the innovation is below
+the frozen correlation ceiling in every task cluster. **c,** Five targets are
+chosen from fixed quantiles of 512 outcome-free cursor-max calibration trials.
+The displayed rewarded fractions come from independent assay trajectories;
+no evaluation label selects or reweights a target. **d,** Terminal innovation
+decodes rewarded versus timeout outcomes, while the acute terminal-outcome
+lesion removes role-aligned separation. The critic contribution tracks the
+stored value prediction and attenuates expected outcomes. Exact role maps,
+outcome labels, and gradients are diagnostic-only and never used by the
+learner's updates.
diff --git a/results/figs/figure5_bci_v2_manifest.json b/results/figs/figure5_bci_v2_manifest.json
new file mode 100644
index 0000000..ae5a6ea
--- /dev/null
+++ b/results/figs/figure5_bci_v2_manifest.json
@@ -0,0 +1,78 @@
+{
+ "gate_status": "passed",
+ "model_seeds": [
+ 0,
+ 1,
+ 2,
+ 3,
+ 4
+ ],
+ "protocol": "oral_b_v2_calibrated_recovery_confirmation_v1",
+ "record_count": 30,
+ "source_sha256": {
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t30_m0.json": "2909c54021e9d451ef54ae5de6b521478119b9bffaca06f58f711b24431c4d06",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t30_m1.json": "775b835a8d48f6d8dea2cb0078446552fb7cfab2d3e437aa9631c311cd6f40e2",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t30_m2.json": "0f95bdeb1a156ef4a97a59748453dfa1f872b490e68e13df926e37057a42eb36",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t30_m3.json": "fec845763b8402c8abf021000041d3b0790b5b50979ef381c1e2b016a9c5fee1",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t30_m4.json": "110fa43b9b0fcbcef89ed8a2332e871777c116690024ed2fa4bedea155fbb246",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t31_m0.json": "1f1c1fa49dd56743081d6420193b56ecba5f11cb7b2a40300f26e666bc4fb821",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t31_m1.json": "4efde69c884057c73d68a712625f5d940fb152e0ad7f5e301ffbecf9c9b8379a",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t31_m2.json": "10e16869c8b23007b840bec177378abe18c26f3fbabaa9dc09d112d4655b140e",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t31_m3.json": "a8be20d34915f95f7c278c4a4a3c2eaaf775d280b42614dfad8256cd877fe789",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t31_m4.json": "38e58907ad43ab06361a2946935855ea9d059a0ffd51d3cbd0ad596e1b923271",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t32_m0.json": "0254fc9d1945d11c5f178b9267ecbf8fe7f7ac2af8099b84095b4e931e1bb719",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t32_m1.json": "359c7a46d1f3bdde2e9394b5f7dceea01511b4a1714688976be3d69b4312a370",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t32_m2.json": "b2e1228d511fffd9753aacfd2b8f534652612d348900d44584f3c8f7e9ce7c05",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t32_m3.json": "e34d2a5e285184c6e60d088b67d8658d6dc78c62b2fc42c06acdb78f55bd6803",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t32_m4.json": "033dd12ed1e2efacc07ae14297bb0cc8fc74dc9114cd57431de9794bee7c7695",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t33_m0.json": "fba80b70c40169efcb60144e8315b5837184463601c86f3e3bda21361b3318ed",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t33_m1.json": "be952f8068e07612ef469dab0c495788d5b5b9dbc1c198b84ef5f9a17be3f62e",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t33_m2.json": "1d144e27aff463aacbc2e418bb5567016690477ba6f1e9e0e84f241cc8f24105",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t33_m3.json": "f81a7942503b1768c5819aff76e4525c1f0425c70b5a9f1ba397a30b0d5b31f2",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t33_m4.json": "42cc8ca59a3239c351bd3436bd8662e08c706f0861fce07266958e8e55d6dcd4",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t34_m0.json": "b798f59f61ff25c1f6d8e06b35727492a901ee87d8798700d2d7ff111cff0ecb",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t34_m1.json": "f5d4d84beeefd5c2e46187ccb61ef4432b0193c4c7f053c40252740a939ee651",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t34_m2.json": "7b8a5e144f71c5c6476be92612b8924dcdf1ba1d8f3083ce80618099b756adc1",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t34_m3.json": "c75f0c42a28ee1b764a7181e5fb7f449fecf84a3a63f69bf0663ed29f4dd1846",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t34_m4.json": "eb3de9a7edb8d3283a8762c1202310775d634af9a8ea5a25f90f314412f8f615",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t35_m0.json": "c026bcf06003c4129fe2d444f30331187b872ea2d28d55e41e5752cf2e3acb15",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t35_m1.json": "42452e471e10d9b17916f906b6b205632aa1f1de8335b1ffe962e855c53ee4e1",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t35_m2.json": "37ecff67bc1f7899b95f81c5587eb826602c1053b6777d76076bb891742161df",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t35_m3.json": "e53a422eff2bb2961ea396f071ea040a71d921e348a35bcb2d7e54d0952b3c23",
+ "results/bci_v2_calibrated_confirmation/bci_v2_calibrated_confirm_t35_m4.json": "4d9d35cb6d9bc30d240c85a0a5887c7d983e81acdb4a94b613bc633a11ea3178",
+ "results/bci_v2_calibrated_confirmation_gate.json": "3b161c32727a2a7c4b50ca696dafff20a023d23e8cd008a0b8dddd662516b084"
+ },
+ "statistics": {
+ "challenge_fraction_mean": 0.5009375,
+ "critic_expectedness_lower": 0.2813635909438583,
+ "critic_expectedness_mean": 0.3187244219033087,
+ "decoder_corr_mean": 0.1208991158963185,
+ "fixed_role_gap_mean": 0.9997395833333333,
+ "intact_final_mean": 1.0,
+ "learning_gain_mean": 0.9880208333333333,
+ "outcome_lesion_drop_lower": 0.3886365638440749,
+ "outcome_lesion_drop_mean": 0.40001482185266496,
+ "positive_sign_count": 30,
+ "raw_residual_corr_gap_mean": 0.935791327739437,
+ "residual_soma_corr_mean": 0.06308767535907349,
+ "role_cosine_mean": 0.9761252363522848,
+ "sign_inversion_mean": 0.04271187342053094,
+ "surrounding_accuracy_lower": 0.5425667337998379,
+ "surrounding_accuracy_mean": 0.5436850460987417,
+ "terminal_accuracy_lower": 0.9968020433207192,
+ "terminal_accuracy_mean": 0.9983265604220595,
+ "terminal_outcome_lesion_accuracy_mean": 0.583741356708507,
+ "terminal_previous_soma_accuracy_mean": 0.7350079225783795,
+ "terminal_separation_mean": 0.4116997088030089,
+ "velocity_advantage_mean": 0.6383611285557892
+ },
+ "strict": true,
+ "task_seeds": [
+ 30,
+ 31,
+ 32,
+ 33,
+ 34,
+ 35
+ ]
+}