Mohith202 commited on
Commit
e5d02d5
·
1 Parent(s): 17ccdd5

Refactor A1 evaluation protocols to focus exclusively on Protocol C; update documentation and scripts accordingly.

Browse files
README.md CHANGED
@@ -1,6 +1,6 @@
1
- ## Plan: A1 SI Baseline with Dual Evaluation and Expanded Parcels
2
 
3
- Implement A1 as a frozen SI baseline with two required evaluation protocols: cross-run generalization and within-run blocked split. Keep both 7 language ROIs and expanded Harvard-Oxford symmetric parcels, and report results separately so speaker-shift robustness and same-condition fit are not conflated.
4
 
5
  **Steps**
6
  1. Phase 0: Paper-first gate before coding
@@ -38,21 +38,17 @@ Implement A1 as a frozen SI baseline with two required evaluation protocols: cro
38
  - Apply per-run z-score to regressors and BOLD targets.
39
  - Enforce exact regressor-target TR length match after trimming policy.
40
 
41
- 8. Phase 7: Dual evaluation implementation (both required)
42
- - Protocol A: cross-run split
43
- - Leave-one-run-out per subject to test speaker and condition transfer.
44
- - Nested alpha selection on training-only partitions.
45
- - Protocol B: within-run blocked split
46
- - Use contiguous temporal blocks inside each run for train and test, not random word sampling.
47
- - Add temporal gap buffer between train and test blocks to reduce HRF bleed leakage.
48
- - Tune alpha using training-only temporal partitions.
49
- - Compute metrics for both target sets in both protocols: 7 ROIs and expanded parcels.
50
- - Keep outputs clearly labeled by protocol to avoid mixing interpretations.
51
 
52
  9. Phase 8: Outputs and reporting
53
  - Save per-model, per-layer, per-subject, per-run, per-target, per-protocol tables.
54
  - Save left-right asymmetry summaries and significance tables for parcel and ROI levels.
55
- - Save protocol comparison report: cross-run versus within-run blocked.
56
 
57
  10. Phase 9: Optional extension after baseline
58
  - Add fine-tuning branch as a separate experiment only after frozen baseline is complete.
@@ -83,8 +79,8 @@ Implement A1 as a frozen SI baseline with two required evaluation protocols: cro
83
 
84
  4. Modeling checks
85
  - Frozen baseline confirmed with no weight updates.
86
- - Protocol A and Protocol B each have independent train and validation and test metadata.
87
- - Within-run blocked split includes temporal buffer and zero overlap after buffering.
88
 
89
  5. Reporting checks
90
  - 7 ROI and expanded parcel outputs exist for both protocols.
@@ -92,7 +88,7 @@ Implement A1 as a frozen SI baseline with two required evaluation protocols: cro
92
 
93
  **Decisions**
94
  - Baseline SI uses frozen inference plus ridge only.
95
- - Two evaluation protocols are both required in baseline: cross-run and within-run blocked.
96
  - Random word-level 500/100 split is excluded from primary SI evaluation due temporal leakage risk after HRF convolution.
97
  - Expanded ROI set uses Harvard-Oxford cortical maxprob-thr0-2mm with symmetric split.
98
  - Fine-tuning remains a separate post-baseline experiment branch.
@@ -148,7 +144,7 @@ python mohith/TEAM-9/hf_jobs.py \
148
  --results-path hf_jobs/run_001 \
149
  --flavor a10g-small \
150
  --model-profile current \
151
- --protocols B,C \
152
  --secret-hf-token \
153
  --launch
154
  ```
@@ -158,5 +154,4 @@ python mohith/TEAM-9/hf_jobs.py \
158
  - `--secret-hf-token` is recommended when the dataset repos are private.
159
  - The job keeps the CUDA-enabled PyTorch already present in the container image and installs the rest of `project/requirements.txt` separately.
160
  - By default bootstrap artifacts go to `outputs/a1_bootstrap_hf` inside the cloned repo.
161
- - You can forward the normal end-to-end controls such as `--model-profile`, `--model-slug`, `--reuse-caches`, `--skip-bootstrap`, `--skip-fit`, and `--skip-visualize`.
162
  - You can forward the normal end-to-end controls such as `--model-profile`, `--model-slug`, `--protocols`, `--reuse-caches`, `--skip-bootstrap`, `--skip-fit`, and `--skip-visualize`.
 
1
+ ## Plan: A1 SI Baseline with Protocol C Evaluation and Expanded Parcels
2
 
3
+ Implement A1 as a frozen SI baseline with Protocol C cross-subject shared-space evaluation. Keep both 7 language ROIs and expanded Harvard-Oxford symmetric parcels, and report results by fold and run condition.
4
 
5
  **Steps**
6
  1. Phase 0: Paper-first gate before coding
 
38
  - Apply per-run z-score to regressors and BOLD targets.
39
  - Enforce exact regressor-target TR length match after trimming policy.
40
 
41
+ 8. Phase 7: Evaluation implementation (required)
42
+ - Protocol C: cross-subject shared-space split
43
+ - Use 21:3 subject split with 2-fold cross-validation over accepted subjects.
44
+ - Keep fold construction deterministic via a fixed seed.
45
+ - Compute metrics for both target sets: 7 ROIs and expanded parcels.
46
+ - Keep outputs clearly labeled by fold and run condition.
 
 
 
 
47
 
48
  9. Phase 8: Outputs and reporting
49
  - Save per-model, per-layer, per-subject, per-run, per-target, per-protocol tables.
50
  - Save left-right asymmetry summaries and significance tables for parcel and ROI levels.
51
+ - Save Protocol C summary report across folds and run conditions.
52
 
53
  10. Phase 9: Optional extension after baseline
54
  - Add fine-tuning branch as a separate experiment only after frozen baseline is complete.
 
79
 
80
  4. Modeling checks
81
  - Frozen baseline confirmed with no weight updates.
82
+ - Protocol C fold table uses 21 train and 3 test subjects per fold with 2 folds.
83
+ - Each canonical run condition is evaluated per fold.
84
 
85
  5. Reporting checks
86
  - 7 ROI and expanded parcel outputs exist for both protocols.
 
88
 
89
  **Decisions**
90
  - Baseline SI uses frozen inference plus ridge only.
91
+ - Protocol C cross-subject shared-space is the required baseline evaluation protocol.
92
  - Random word-level 500/100 split is excluded from primary SI evaluation due temporal leakage risk after HRF convolution.
93
  - Expanded ROI set uses Harvard-Oxford cortical maxprob-thr0-2mm with symmetric split.
94
  - Fine-tuning remains a separate post-baseline experiment branch.
 
144
  --results-path hf_jobs/run_001 \
145
  --flavor a10g-small \
146
  --model-profile current \
147
+ --protocols C \
148
  --secret-hf-token \
149
  --launch
150
  ```
 
154
  - `--secret-hf-token` is recommended when the dataset repos are private.
155
  - The job keeps the CUDA-enabled PyTorch already present in the container image and installs the rest of `project/requirements.txt` separately.
156
  - By default bootstrap artifacts go to `outputs/a1_bootstrap_hf` inside the cloned repo.
 
157
  - You can forward the normal end-to-end controls such as `--model-profile`, `--model-slug`, `--protocols`, `--reuse-caches`, `--skip-bootstrap`, `--skip-fit`, and `--skip-visualize`.
code/run_a1_bootstrap.py CHANGED
@@ -1,5 +1,5 @@
1
  #!/usr/bin/env python
2
- """Bootstrap A1 baseline with manifest QC and dual evaluation split artifacts.
3
 
4
  This command currently implements:
5
  - Phase 1: strict derivatives manifest and integrity checks
@@ -8,13 +8,14 @@ This command currently implements:
8
  - Phase 4: annotation harmonization to unified run/condition/speaker event tables
9
  - Phase 5: optional frozen feature extraction wrappers and caching
10
  - Phase 6: optional TR-level HRF regressor + z-score alignment input caching
11
- - Phase 7 prep: Protocol A (cross-run) and Protocol B (within-run blocked) split tables
12
  """
13
 
14
  from __future__ import annotations
15
 
16
  import argparse
17
  import json
 
18
  from pathlib import Path
19
  from typing import Any
20
 
@@ -29,11 +30,6 @@ from a1_pipeline.io_utils import ensure_directory, write_json
29
  from a1_pipeline.manifest import build_derivatives_manifest
30
  from a1_pipeline.model_config import load_model_ids_from_config, list_model_profiles
31
  from a1_pipeline.spatial import build_symmetric_analysis_mask
32
- from a1_pipeline.splits import (
33
- build_cross_run_folds,
34
- build_within_run_blocked_splits,
35
- summarize_split_counts,
36
- )
37
  from a1_pipeline.targets import evaluate_core_roi_preservation, load_core_roi_masks
38
 
39
 
@@ -74,6 +70,69 @@ def _parse_layer_indices(raw: str) -> list[int] | None:
74
  return indices
75
 
76
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
77
  def _build_arg_parser() -> argparse.ArgumentParser:
78
  parser = argparse.ArgumentParser(description="Bootstrap A1 manifest and split artifacts")
79
  parser.add_argument(
@@ -139,28 +198,22 @@ def _build_arg_parser() -> argparse.ArgumentParser:
139
  help="Disable mixed-run condition fallback to generic 'mixed' label",
140
  )
141
  parser.add_argument(
142
- "--within-test-fraction",
143
- type=float,
144
- default=0.2,
145
- help="Protocol B test fraction per run",
146
- )
147
- parser.add_argument(
148
- "--within-gap-tr",
149
  type=int,
150
- default=8,
151
- help="Protocol B temporal gap (in TR) between train and test blocks",
152
  )
153
  parser.add_argument(
154
- "--within-min-train-volumes",
155
  type=int,
156
- default=40,
157
- help="Minimum train TR count for Protocol B",
158
  )
159
  parser.add_argument(
160
- "--within-min-test-volumes",
161
  type=int,
162
- default=20,
163
- help="Minimum test TR count for Protocol B",
164
  )
165
  parser.add_argument(
166
  "--run-mask-method",
@@ -456,20 +509,8 @@ def main() -> None:
456
  enable_mixed_fallback=mixed_fallback_enabled,
457
  )
458
 
459
- cross_run_df = build_cross_run_folds(manifest_df)
460
- within_run_df, within_run_skipped_df = build_within_run_blocked_splits(
461
- manifest_df=manifest_df,
462
- test_fraction=float(args.within_test_fraction),
463
- gap_tr=int(args.within_gap_tr),
464
- min_train_volumes=int(args.within_min_train_volumes),
465
- min_test_volumes=int(args.within_min_test_volumes),
466
- )
467
-
468
  accepted_manifest_path = output_dir / "accepted_manifest.csv"
469
  rejected_manifest_path = output_dir / "rejected_manifest.csv"
470
- protocol_a_path = output_dir / "protocol_a_cross_run_folds.csv"
471
- protocol_b_path = output_dir / "protocol_b_within_run_splits.csv"
472
- protocol_b_skipped_path = output_dir / "protocol_b_within_run_skipped.csv"
473
  protocol_c_path = output_dir / "protocol_c_cross_subject_folds.csv"
474
  analysis_mask_path = output_dir / "analysis_mask.nii.gz"
475
  run_mask_qc_path = output_dir / "run_mask_qc.csv"
@@ -493,25 +534,13 @@ def main() -> None:
493
 
494
  manifest_df.to_csv(accepted_manifest_path, index=False)
495
  rejected_df.to_csv(rejected_manifest_path, index=False)
496
- cross_run_df.to_csv(protocol_a_path, index=False)
497
- within_run_df.to_csv(protocol_b_path, index=False)
498
- within_run_skipped_df.to_csv(protocol_b_skipped_path, index=False)
499
-
500
- # Protocol C: leave-one-subject-out folds across all 4 canonical runs.
501
- _CANONICAL_RUN_LABELS = {1: "single_female", 2: "single_male", 3: "mixed_female", 4: "mixed_male"}
502
- protocol_c_subjects = sorted(manifest_df["subject"].unique().tolist())
503
- protocol_c_rows = []
504
- for _cr, _cl in _CANONICAL_RUN_LABELS.items():
505
- for _test_sub in protocol_c_subjects:
506
- _train_subs = ",".join(s for s in protocol_c_subjects if s != _test_sub)
507
- protocol_c_rows.append({
508
- "fold_id": f"loso_run{_cr}_{_test_sub}",
509
- "canonical_run": _cr,
510
- "condition_label": _cl,
511
- "train_subjects": _train_subs,
512
- "test_subjects": _test_sub,
513
- })
514
- pd.DataFrame(protocol_c_rows).to_csv(protocol_c_path, index=False)
515
  run_mask_qc_df.to_csv(run_mask_qc_path, index=False)
516
  core_roi_coverage_df.to_csv(core_roi_coverage_path, index=False)
517
  core_roi_failures_df.to_csv(core_roi_failures_path, index=False)
@@ -521,12 +550,14 @@ def main() -> None:
521
 
522
  nib.save(analysis_mask_img, str(analysis_mask_path))
523
 
524
- split_summary = summarize_split_counts(
525
- manifest_df=manifest_df,
526
- cross_run_df=cross_run_df,
527
- within_run_df=within_run_df,
528
- within_run_skipped_df=within_run_skipped_df,
529
- )
 
 
530
 
531
  write_json(manifest_qc_path, manifest_qc)
532
  write_json(analysis_mask_qc_path, mask_qc)
@@ -645,11 +676,11 @@ def main() -> None:
645
  "min_voxels_required": int(args.min_core_roi_voxels),
646
  "allow_core_roi_failures": bool(args.allow_core_roi_failures),
647
  },
648
- "within_protocol": {
649
- "test_fraction": float(args.within_test_fraction),
650
- "gap_tr": int(args.within_gap_tr),
651
- "min_train_volumes": int(args.within_min_train_volumes),
652
- "min_test_volumes": int(args.within_min_test_volumes),
653
  },
654
  "manifest_qc": manifest_qc,
655
  "analysis_mask_qc": mask_qc,
@@ -684,9 +715,6 @@ def main() -> None:
684
  if alignment_regressor_summary_path.exists()
685
  else None,
686
  "alignment_qc": str(alignment_qc_path) if alignment_qc_path.exists() else None,
687
- "protocol_a_cross_run": str(protocol_a_path),
688
- "protocol_b_within_run": str(protocol_b_path),
689
- "protocol_b_within_run_skipped": str(protocol_b_skipped_path),
690
  "protocol_c_cross_subject": str(protocol_c_path),
691
  },
692
  }
@@ -696,9 +724,7 @@ def main() -> None:
696
  print("A1 bootstrap complete")
697
  print(f"Accepted manifest rows: {len(manifest_df)}")
698
  print(f"Rejected candidate rows: {len(rejected_df)}")
699
- print(f"Protocol A folds: {len(cross_run_df)}")
700
- print(f"Protocol B splits: {len(within_run_df)}")
701
- print(f"Protocol B skipped: {len(within_run_skipped_df)}")
702
  print(f"Core ROI fail rows: {len(core_roi_failures_df)}")
703
  print(f"Core ROI gate passed: {core_roi_gate_passed}")
704
  print(f"Run-level event rows: {len(run_events_df)}")
 
1
  #!/usr/bin/env python
2
+ """Bootstrap A1 baseline with manifest QC and Protocol C split artifacts.
3
 
4
  This command currently implements:
5
  - Phase 1: strict derivatives manifest and integrity checks
 
8
  - Phase 4: annotation harmonization to unified run/condition/speaker event tables
9
  - Phase 5: optional frozen feature extraction wrappers and caching
10
  - Phase 6: optional TR-level HRF regressor + z-score alignment input caching
11
+ - Phase 7 prep: Protocol C (cross-subject) split table
12
  """
13
 
14
  from __future__ import annotations
15
 
16
  import argparse
17
  import json
18
+ import random
19
  from pathlib import Path
20
  from typing import Any
21
 
 
30
  from a1_pipeline.manifest import build_derivatives_manifest
31
  from a1_pipeline.model_config import load_model_ids_from_config, list_model_profiles
32
  from a1_pipeline.spatial import build_symmetric_analysis_mask
 
 
 
 
 
33
  from a1_pipeline.targets import evaluate_core_roi_preservation, load_core_roi_masks
34
 
35
 
 
70
  return indices
71
 
72
 
73
+ def _build_protocol_c_folds(
74
+ manifest_df: pd.DataFrame,
75
+ n_test_subjects: int,
76
+ n_folds: int,
77
+ seed: int,
78
+ ) -> pd.DataFrame:
79
+ """Build Protocol C folds using repeated 21:3 subject holdout splits."""
80
+ canonical_labels = {
81
+ 1: "single_female",
82
+ 2: "single_male",
83
+ 3: "mixed_female",
84
+ 4: "mixed_male",
85
+ }
86
+
87
+ subjects = sorted(manifest_df["subject"].astype(str).unique().tolist())
88
+ n_subjects = len(subjects)
89
+
90
+ if n_subjects != 24:
91
+ raise ValueError(
92
+ "Protocol C 21:3 split requires exactly 24 accepted subjects. "
93
+ f"Found {n_subjects}."
94
+ )
95
+ if n_test_subjects != 3:
96
+ raise ValueError(
97
+ "Protocol C is fixed to 21:3, so --protocol-c-test-subjects must be 3"
98
+ )
99
+ if n_folds <= 0:
100
+ raise ValueError("--protocol-c-num-folds must be positive")
101
+
102
+ rng = random.Random(int(seed))
103
+ shuffled_subjects = list(subjects)
104
+ rng.shuffle(shuffled_subjects)
105
+
106
+ rows: list[dict[str, Any]] = []
107
+ for fold_idx in range(int(n_folds)):
108
+ start = (fold_idx * n_test_subjects) % n_subjects
109
+ test_subjects = [
110
+ shuffled_subjects[(start + offset) % n_subjects]
111
+ for offset in range(n_test_subjects)
112
+ ]
113
+ test_subject_set = set(test_subjects)
114
+ train_subjects = [subject for subject in subjects if subject not in test_subject_set]
115
+
116
+ if len(train_subjects) != 21:
117
+ raise ValueError(
118
+ "Protocol C expected 21 train subjects per fold, got "
119
+ f"{len(train_subjects)}"
120
+ )
121
+
122
+ for canonical_run, condition_label in canonical_labels.items():
123
+ rows.append(
124
+ {
125
+ "fold_id": f"cv2_run{canonical_run}_fold{fold_idx + 1}",
126
+ "canonical_run": int(canonical_run),
127
+ "condition_label": str(condition_label),
128
+ "train_subjects": ",".join(train_subjects),
129
+ "test_subjects": ",".join(test_subjects),
130
+ }
131
+ )
132
+
133
+ return pd.DataFrame(rows).sort_values(["canonical_run", "fold_id"]).reset_index(drop=True)
134
+
135
+
136
  def _build_arg_parser() -> argparse.ArgumentParser:
137
  parser = argparse.ArgumentParser(description="Bootstrap A1 manifest and split artifacts")
138
  parser.add_argument(
 
198
  help="Disable mixed-run condition fallback to generic 'mixed' label",
199
  )
200
  parser.add_argument(
201
+ "--protocol-c-test-subjects",
 
 
 
 
 
 
202
  type=int,
203
+ default=3,
204
+ help="Protocol C test subject count per fold (fixed to 3 for 21:3)",
205
  )
206
  parser.add_argument(
207
+ "--protocol-c-num-folds",
208
  type=int,
209
+ default=2,
210
+ help="Protocol C repeated holdout fold count",
211
  )
212
  parser.add_argument(
213
+ "--protocol-c-seed",
214
  type=int,
215
+ default=0,
216
+ help="Random seed for Protocol C fold construction",
217
  )
218
  parser.add_argument(
219
  "--run-mask-method",
 
509
  enable_mixed_fallback=mixed_fallback_enabled,
510
  )
511
 
 
 
 
 
 
 
 
 
 
512
  accepted_manifest_path = output_dir / "accepted_manifest.csv"
513
  rejected_manifest_path = output_dir / "rejected_manifest.csv"
 
 
 
514
  protocol_c_path = output_dir / "protocol_c_cross_subject_folds.csv"
515
  analysis_mask_path = output_dir / "analysis_mask.nii.gz"
516
  run_mask_qc_path = output_dir / "run_mask_qc.csv"
 
534
 
535
  manifest_df.to_csv(accepted_manifest_path, index=False)
536
  rejected_df.to_csv(rejected_manifest_path, index=False)
537
+ protocol_c_df = _build_protocol_c_folds(
538
+ manifest_df=manifest_df,
539
+ n_test_subjects=int(args.protocol_c_test_subjects),
540
+ n_folds=int(args.protocol_c_num_folds),
541
+ seed=int(args.protocol_c_seed),
542
+ )
543
+ protocol_c_df.to_csv(protocol_c_path, index=False)
 
 
 
 
 
 
 
 
 
 
 
 
544
  run_mask_qc_df.to_csv(run_mask_qc_path, index=False)
545
  core_roi_coverage_df.to_csv(core_roi_coverage_path, index=False)
546
  core_roi_failures_df.to_csv(core_roi_failures_path, index=False)
 
550
 
551
  nib.save(analysis_mask_img, str(analysis_mask_path))
552
 
553
+ split_summary = {
554
+ "n_manifest_rows": int(len(manifest_df)),
555
+ "n_subjects_manifest": int(manifest_df["subject"].nunique()) if not manifest_df.empty else 0,
556
+ "n_protocol_a_folds": 0,
557
+ "n_protocol_b_splits": 0,
558
+ "n_protocol_b_skipped": 0,
559
+ "n_protocol_c_folds": int(len(protocol_c_df)),
560
+ }
561
 
562
  write_json(manifest_qc_path, manifest_qc)
563
  write_json(analysis_mask_qc_path, mask_qc)
 
676
  "min_voxels_required": int(args.min_core_roi_voxels),
677
  "allow_core_roi_failures": bool(args.allow_core_roi_failures),
678
  },
679
+ "protocol_c_config": {
680
+ "n_test_subjects": int(args.protocol_c_test_subjects),
681
+ "n_train_subjects": 21,
682
+ "n_folds": int(args.protocol_c_num_folds),
683
+ "seed": int(args.protocol_c_seed),
684
  },
685
  "manifest_qc": manifest_qc,
686
  "analysis_mask_qc": mask_qc,
 
715
  if alignment_regressor_summary_path.exists()
716
  else None,
717
  "alignment_qc": str(alignment_qc_path) if alignment_qc_path.exists() else None,
 
 
 
718
  "protocol_c_cross_subject": str(protocol_c_path),
719
  },
720
  }
 
724
  print("A1 bootstrap complete")
725
  print(f"Accepted manifest rows: {len(manifest_df)}")
726
  print(f"Rejected candidate rows: {len(rejected_df)}")
727
+ print(f"Protocol C folds: {len(protocol_c_df)}")
 
 
728
  print(f"Core ROI fail rows: {len(core_roi_failures_df)}")
729
  print(f"Core ROI gate passed: {core_roi_gate_passed}")
730
  print(f"Run-level event rows: {len(run_events_df)}")
code/run_a1_end_to_end.py CHANGED
@@ -112,8 +112,8 @@ def _build_parser() -> argparse.ArgumentParser:
112
  parser.add_argument(
113
  "--protocols",
114
  type=str,
115
- default="B,C",
116
- help="Fit protocols to execute: B, C, or a comma-separated subset (bootstrap generates protocol_c_cross_subject_folds.csv automatically).",
117
  )
118
  parser.add_argument(
119
  "--metric",
 
112
  parser.add_argument(
113
  "--protocols",
114
  type=str,
115
+ default="C",
116
+ help="Fit protocols to execute: C (bootstrap generates protocol_c_cross_subject_folds.csv automatically).",
117
  )
118
  parser.add_argument(
119
  "--metric",
code/run_a1_fit.py CHANGED
@@ -3,7 +3,6 @@
3
 
4
  This script consumes Phase 6 artifacts from `run_a1_bootstrap.py` and computes
5
  ridge-regression performance for:
6
- - Protocol B: within-run blocked split
7
  - Protocol C: cross-subject subject-holdout on a shared canonical stimulus
8
 
9
  It is optimized for ROI-level evaluation using the 7 core language ROIs.
@@ -81,7 +80,7 @@ def _parse_protocols(raw: str) -> set[str]:
81
  if not values:
82
  raise ValueError("At least one protocol is required")
83
 
84
- allowed = {"B", "C"}
85
  unknown = values.difference(allowed)
86
  if unknown:
87
  raise ValueError(f"Unsupported protocol(s): {sorted(unknown)}")
@@ -137,8 +136,8 @@ def _build_parser() -> argparse.ArgumentParser:
137
  parser.add_argument(
138
  "--protocols",
139
  type=str,
140
- default="B,C",
141
- help="Protocols to run: B, C, or a comma-separated subset (default: B,C).",
142
  )
143
  parser.add_argument(
144
  "--roi-mask-dir",
@@ -166,7 +165,7 @@ def _build_parser() -> argparse.ArgumentParser:
166
  type=str,
167
  default="auto",
168
  help=(
169
- "Number of CPU workers to evaluate layers in parallel for protocols B and C. "
170
  "'auto' uses min(n_layers, cpu_count // 2). Use 1 to force serial."
171
  ),
172
  )
@@ -187,7 +186,6 @@ def _resolve_core_paths(bootstrap_output_dir: Path) -> dict[str, Path]:
187
  "analysis_mask": bootstrap_output_dir / "analysis_mask.nii.gz",
188
  "bold_summary": _pick_csv("alignment_bold_summary.csv"),
189
  "regressor_summary": _pick_csv("alignment_regressor_summary.csv"),
190
- "protocol_b": _pick_csv("protocol_b_within_run_splits.csv"),
191
  "protocol_c": _pick_csv("protocol_c_cross_subject_folds.csv"),
192
  }
193
 
@@ -199,8 +197,6 @@ def _check_required_files(path_map: dict[str, Path], protocols: set[str]) -> Non
199
  "bold_summary",
200
  "regressor_summary",
201
  }
202
- if "B" in protocols:
203
- required_names.add("protocol_b")
204
  if "C" in protocols:
205
  required_names.add("protocol_c")
206
 
@@ -443,18 +439,6 @@ def _score_matrix(y_true: np.ndarray, y_pred: np.ndarray) -> dict[str, float]:
443
  }
444
 
445
 
446
- def _stack_protocol_b_train(
447
- values: np.ndarray,
448
- left_start: int,
449
- left_end: int,
450
- right_start: int,
451
- right_end: int,
452
- ) -> np.ndarray:
453
- left = values[left_start:left_end]
454
- right = values[right_start:right_end]
455
- return np.concatenate([left, right], axis=0)
456
-
457
-
458
  def _collect_protocol_c_subjects(cross_subject_df: pd.DataFrame) -> list[str]:
459
  if cross_subject_df.empty:
460
  return []
@@ -467,224 +451,6 @@ def _collect_protocol_c_subjects(cross_subject_df: pd.DataFrame) -> list[str]:
467
  return sorted(subjects)
468
 
469
 
470
- def _evaluate_protocol_b_layer(
471
- layer_idx: int,
472
- subjects: list[str],
473
- alpha: float,
474
- within_run_df: pd.DataFrame,
475
- regressor_path_map: dict[tuple[int, int], Path],
476
- canonical_runs_required: list[int],
477
- subject_roi_cache: dict[str, dict[int, dict[str, np.ndarray]]],
478
- participant_run_map: dict[str, dict[int, int]],
479
- model_slug: str,
480
- model_id: str,
481
- blas_threads: int,
482
- ) -> list[dict[str, Any]]:
483
- """Evaluate Protocol B for a single layer. Returns list of result rows."""
484
- from threadpoolctl import threadpool_limits
485
-
486
- rows: list[dict[str, Any]] = []
487
-
488
- with threadpool_limits(limits=int(blas_threads)):
489
- x_by_canonical_run: dict[int, np.ndarray] = {}
490
- for canonical_run in canonical_runs_required:
491
- key = (canonical_run, layer_idx)
492
- if key not in regressor_path_map:
493
- raise KeyError(
494
- "Missing regressor cache for "
495
- f"canonical_run={canonical_run}, layer={layer_idx}, model={model_slug}"
496
- )
497
- x_by_canonical_run[canonical_run] = np.asarray(np.load(regressor_path_map[key]), dtype=np.float32)
498
-
499
- group_columns = [
500
- "run",
501
- "test_start_tr",
502
- "test_end_tr_exclusive",
503
- "train_left_start_tr",
504
- "train_left_end_tr_exclusive",
505
- "train_right_start_tr",
506
- "train_right_end_tr_exclusive",
507
- "split_id",
508
- ]
509
-
510
- for _, group_df in within_run_df.groupby(group_columns):
511
- example = group_df.iloc[0]
512
-
513
- run = int(example["run"])
514
- split_id = str(example["split_id"])
515
- test_start = int(example["test_start_tr"])
516
- test_end = int(example["test_end_tr_exclusive"])
517
- left_start = int(example["train_left_start_tr"])
518
- left_end = int(example["train_left_end_tr_exclusive"])
519
- right_start = int(example["train_right_start_tr"])
520
- right_end = int(example["train_right_end_tr_exclusive"])
521
-
522
- per_canonical_cache: dict[int, tuple[np.ndarray, np.ndarray, int]] = {}
523
-
524
- for row in group_df.itertuples(index=False):
525
- subject = str(getattr(row, "subject"))
526
- canonical_run = resolve_subject_canonical_run(
527
- participant_run_map=participant_run_map,
528
- subject=subject,
529
- run=run,
530
- )
531
-
532
- if canonical_run not in per_canonical_cache:
533
- x_full = x_by_canonical_run[canonical_run]
534
- x_train = _stack_protocol_b_train(
535
- values=x_full,
536
- left_start=left_start,
537
- left_end=left_end,
538
- right_start=right_start,
539
- right_end=right_end,
540
- )
541
- x_test = x_full[test_start:test_end]
542
- projector = _ridge_projection_matrix(x_train=x_train, alpha=alpha)
543
- per_canonical_cache[canonical_run] = (projector, x_test, int(x_train.shape[0]))
544
-
545
- projector, x_test, n_train_tr = per_canonical_cache[canonical_run]
546
- y_subject = subject_roi_cache[subject]
547
-
548
- for roi_name in CORE_ROI_NAMES:
549
- y_full = y_subject[run][roi_name]
550
- if y_full.shape[0] != x_by_canonical_run[canonical_run].shape[0]:
551
- raise ValueError(
552
- "Protocol B full-run TR mismatch between regressors and BOLD for "
553
- f"subject={subject}, run={run}, canonical_run={canonical_run}, "
554
- f"layer={layer_idx}, roi={roi_name}. "
555
- f"x_full_tr={x_by_canonical_run[canonical_run].shape[0]}, y_full_tr={y_full.shape[0]}"
556
- )
557
-
558
- y_train = _stack_protocol_b_train(
559
- values=y_full,
560
- left_start=left_start,
561
- left_end=left_end,
562
- right_start=right_start,
563
- right_end=right_end,
564
- )
565
- y_test = y_full[test_start:test_end]
566
-
567
- if y_train.shape[0] != n_train_tr:
568
- raise ValueError(
569
- "Protocol B train TR mismatch between regressors and BOLD for "
570
- f"subject={subject}, run={run}, canonical_run={canonical_run}, "
571
- f"layer={layer_idx}, roi={roi_name}. "
572
- f"x_train_tr={n_train_tr}, y_train_tr={y_train.shape[0]}"
573
- )
574
- if y_test.shape[0] != x_test.shape[0]:
575
- raise ValueError(
576
- "Protocol B test TR mismatch between regressors and BOLD for "
577
- f"subject={subject}, run={run}, canonical_run={canonical_run}, "
578
- f"layer={layer_idx}, roi={roi_name}. "
579
- f"x_test_tr={x_test.shape[0]}, y_test_tr={y_test.shape[0]}"
580
- )
581
-
582
- weights = projector @ np.asarray(y_train, dtype=np.float64)
583
- y_pred = np.asarray(x_test, dtype=np.float64) @ weights
584
-
585
- scores = _score_matrix(y_true=y_test, y_pred=y_pred)
586
-
587
- rows.append(
588
- {
589
- "protocol": "B_within_run_blocked",
590
- "model_id": model_id,
591
- "model_slug": model_slug,
592
- "subject": subject,
593
- "run": int(run),
594
- "canonical_run": int(canonical_run),
595
- "split_id": split_id,
596
- "layer_idx": int(layer_idx),
597
- "roi_name": roi_name,
598
- "alpha": float(alpha),
599
- "n_train_tr": int(n_train_tr),
600
- "n_test_tr": int(x_test.shape[0]),
601
- "n_voxels_roi": int(y_test.shape[1]),
602
- **scores,
603
- }
604
- )
605
-
606
- return rows
607
-
608
-
609
- def _evaluate_protocol_b(
610
- subjects: list[str],
611
- layers: list[int],
612
- alpha: float,
613
- within_run_df: pd.DataFrame,
614
- bold_path_map: dict[tuple[str, int], Path],
615
- regressor_path_map: dict[tuple[int, int], Path],
616
- roi_index_map: dict[str, np.ndarray],
617
- participant_run_map: dict[str, dict[int, int]],
618
- model_slug: str,
619
- model_id: str,
620
- n_workers: int = 1,
621
- blas_threads_per_worker: int = 0,
622
- ) -> pd.DataFrame:
623
- runs = sorted({int(value) for value in within_run_df["run"].tolist()})
624
-
625
- subject_roi_cache: dict[str, dict[int, dict[str, np.ndarray]]] = {}
626
- for subject in subjects:
627
- subject_roi_cache[subject] = _load_subject_roi_runs(
628
- subject=subject,
629
- runs=runs,
630
- bold_path_map=bold_path_map,
631
- roi_index_map=roi_index_map,
632
- )
633
-
634
- canonical_runs_required = sorted(
635
- {
636
- resolve_subject_canonical_run(
637
- participant_run_map=participant_run_map,
638
- subject=subject,
639
- run=run,
640
- )
641
- for subject in subjects
642
- for run in runs
643
- }
644
- )
645
-
646
- if blas_threads_per_worker <= 0:
647
- blas_threads_per_worker = max(1, (os.cpu_count() or 4) // max(1, n_workers))
648
-
649
- def _layer_task(layer_idx: int) -> list[dict[str, Any]]:
650
- return _evaluate_protocol_b_layer(
651
- layer_idx=int(layer_idx),
652
- subjects=subjects,
653
- alpha=alpha,
654
- within_run_df=within_run_df,
655
- regressor_path_map=regressor_path_map,
656
- canonical_runs_required=canonical_runs_required,
657
- subject_roi_cache=subject_roi_cache,
658
- participant_run_map=participant_run_map,
659
- model_slug=model_slug,
660
- model_id=model_id,
661
- blas_threads=blas_threads_per_worker,
662
- )
663
-
664
- if n_workers > 1 and len(layers) > 1:
665
- from joblib import Parallel, delayed
666
-
667
- print(
668
- f"[fit] Protocol B: parallel layers across {n_workers} threads "
669
- f"(layers={len(layers)}, blas_threads/worker={blas_threads_per_worker})",
670
- flush=True,
671
- )
672
- results = Parallel(n_jobs=int(n_workers), prefer="threads")(
673
- delayed(_layer_task)(layer_idx) for layer_idx in layers
674
- )
675
- else:
676
- results = [_layer_task(layer_idx) for layer_idx in layers]
677
-
678
- rows: list[dict[str, Any]] = []
679
- for layer_rows in results:
680
- rows.extend(layer_rows)
681
-
682
- df = pd.DataFrame(rows)
683
- if not df.empty:
684
- df = df.sort_values(["layer_idx", "subject", "run", "roi_name"]).reset_index(drop=True)
685
- return df
686
-
687
-
688
  def _evaluate_protocol_c(
689
  layers: list[int],
690
  alpha: float,
@@ -946,7 +712,6 @@ def main() -> None:
946
 
947
  bold_summary_df = pd.read_csv(path_map["bold_summary"])
948
  regressor_summary_df = pd.read_csv(path_map["regressor_summary"])
949
- within_run_df = pd.read_csv(path_map["protocol_b"]) if path_map["protocol_b"].exists() else pd.DataFrame()
950
  cross_subject_df = (
951
  pd.read_csv(path_map["protocol_c"]) if path_map["protocol_c"].exists() else pd.DataFrame()
952
  )
@@ -958,11 +723,8 @@ def main() -> None:
958
 
959
  if requested_subjects is not None:
960
  subject_set = set(requested_subjects)
961
- within_run_df = within_run_df[within_run_df["subject"].isin(subject_set)].copy()
962
  bold_summary_df = bold_summary_df[bold_summary_df["subject"].isin(subject_set)].copy()
963
 
964
- if within_run_df.empty and "B" in protocols:
965
- raise ValueError("Protocol B was requested but no within-run rows remained after filtering")
966
  if cross_subject_df.empty and "C" in protocols:
967
  raise ValueError("Protocol C was requested but no cross-subject rows were available")
968
 
@@ -998,10 +760,7 @@ def main() -> None:
998
  )
999
 
1000
  protocol_c_subjects = _collect_protocol_c_subjects(cross_subject_df) if "C" in protocols else []
1001
- subjects = sorted(
1002
- set(within_run_df["subject"].tolist())
1003
- | set(protocol_c_subjects)
1004
- )
1005
  if not subjects:
1006
  raise ValueError("No subjects available after filtering")
1007
 
@@ -1023,7 +782,6 @@ def main() -> None:
1023
  }
1024
  )
1025
 
1026
- protocol_b_df = pd.DataFrame()
1027
  protocol_c_df = pd.DataFrame()
1028
 
1029
  n_fit_workers, blas_threads_per_worker = _resolve_num_fit_workers(
@@ -1038,23 +796,6 @@ def main() -> None:
1038
  flush=True,
1039
  )
1040
 
1041
- if "B" in protocols:
1042
- protocol_b_df = _evaluate_protocol_b(
1043
- subjects=subjects,
1044
- layers=layers,
1045
- alpha=float(args.alpha),
1046
- within_run_df=within_run_df,
1047
- bold_path_map=bold_path_map,
1048
- regressor_path_map=regressor_path_map,
1049
- roi_index_map=roi_index_map,
1050
- participant_run_map=participant_run_map,
1051
- model_slug=str(args.model_slug),
1052
- model_id=model_id,
1053
- n_workers=n_fit_workers,
1054
- blas_threads_per_worker=blas_threads_per_worker,
1055
- )
1056
- protocol_b_df.to_csv(output_dir / "protocol_b_core_roi_scores.csv", index=False)
1057
-
1058
  if "C" in protocols:
1059
  protocol_c_df = _evaluate_protocol_c(
1060
  layers=layers,
@@ -1071,7 +812,7 @@ def main() -> None:
1071
  )
1072
  protocol_c_df.to_csv(output_dir / "protocol_c_core_roi_scores.csv", index=False)
1073
 
1074
- combined_df = pd.concat([protocol_b_df, protocol_c_df], ignore_index=True)
1075
  combined_df.to_csv(output_dir / "core_roi_scores_all.csv", index=False)
1076
 
1077
  layer_summary_df, best_layer_df = _summarize_layers(scores_df=combined_df)
@@ -1093,7 +834,6 @@ def main() -> None:
1093
  "participant_run_info_path": str(participant_run_info_path),
1094
  "required_runs": [int(value) for value in required_runs],
1095
  "canonical_runs_used": [int(value) for value in canonical_runs_used],
1096
- "n_protocol_b_rows": int(len(protocol_b_df)),
1097
  "n_protocol_c_rows": int(len(protocol_c_df)),
1098
  "n_total_rows": int(len(combined_df)),
1099
  }
@@ -1106,7 +846,6 @@ def main() -> None:
1106
  print(f"Subjects: {len(subjects)}")
1107
  print(f"Layers: {len(layers)}")
1108
  print(f"Alpha: {float(args.alpha)}")
1109
- print(f"Protocol B rows: {len(protocol_b_df)}")
1110
  print(f"Protocol C rows: {len(protocol_c_df)}")
1111
  print(f"Output directory: {output_dir}")
1112
  print("=" * 72)
 
3
 
4
  This script consumes Phase 6 artifacts from `run_a1_bootstrap.py` and computes
5
  ridge-regression performance for:
 
6
  - Protocol C: cross-subject subject-holdout on a shared canonical stimulus
7
 
8
  It is optimized for ROI-level evaluation using the 7 core language ROIs.
 
80
  if not values:
81
  raise ValueError("At least one protocol is required")
82
 
83
+ allowed = {"C"}
84
  unknown = values.difference(allowed)
85
  if unknown:
86
  raise ValueError(f"Unsupported protocol(s): {sorted(unknown)}")
 
136
  parser.add_argument(
137
  "--protocols",
138
  type=str,
139
+ default="C",
140
+ help="Protocols to run: C (cross-subject shared-space).",
141
  )
142
  parser.add_argument(
143
  "--roi-mask-dir",
 
165
  type=str,
166
  default="auto",
167
  help=(
168
+ "Number of CPU workers to evaluate layers in parallel for protocol C. "
169
  "'auto' uses min(n_layers, cpu_count // 2). Use 1 to force serial."
170
  ),
171
  )
 
186
  "analysis_mask": bootstrap_output_dir / "analysis_mask.nii.gz",
187
  "bold_summary": _pick_csv("alignment_bold_summary.csv"),
188
  "regressor_summary": _pick_csv("alignment_regressor_summary.csv"),
 
189
  "protocol_c": _pick_csv("protocol_c_cross_subject_folds.csv"),
190
  }
191
 
 
197
  "bold_summary",
198
  "regressor_summary",
199
  }
 
 
200
  if "C" in protocols:
201
  required_names.add("protocol_c")
202
 
 
439
  }
440
 
441
 
 
 
 
 
 
 
 
 
 
 
 
 
442
  def _collect_protocol_c_subjects(cross_subject_df: pd.DataFrame) -> list[str]:
443
  if cross_subject_df.empty:
444
  return []
 
451
  return sorted(subjects)
452
 
453
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  def _evaluate_protocol_c(
455
  layers: list[int],
456
  alpha: float,
 
712
 
713
  bold_summary_df = pd.read_csv(path_map["bold_summary"])
714
  regressor_summary_df = pd.read_csv(path_map["regressor_summary"])
 
715
  cross_subject_df = (
716
  pd.read_csv(path_map["protocol_c"]) if path_map["protocol_c"].exists() else pd.DataFrame()
717
  )
 
723
 
724
  if requested_subjects is not None:
725
  subject_set = set(requested_subjects)
 
726
  bold_summary_df = bold_summary_df[bold_summary_df["subject"].isin(subject_set)].copy()
727
 
 
 
728
  if cross_subject_df.empty and "C" in protocols:
729
  raise ValueError("Protocol C was requested but no cross-subject rows were available")
730
 
 
760
  )
761
 
762
  protocol_c_subjects = _collect_protocol_c_subjects(cross_subject_df) if "C" in protocols else []
763
+ subjects = sorted(set(protocol_c_subjects))
 
 
 
764
  if not subjects:
765
  raise ValueError("No subjects available after filtering")
766
 
 
782
  }
783
  )
784
 
 
785
  protocol_c_df = pd.DataFrame()
786
 
787
  n_fit_workers, blas_threads_per_worker = _resolve_num_fit_workers(
 
796
  flush=True,
797
  )
798
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
799
  if "C" in protocols:
800
  protocol_c_df = _evaluate_protocol_c(
801
  layers=layers,
 
812
  )
813
  protocol_c_df.to_csv(output_dir / "protocol_c_core_roi_scores.csv", index=False)
814
 
815
+ combined_df = protocol_c_df.copy()
816
  combined_df.to_csv(output_dir / "core_roi_scores_all.csv", index=False)
817
 
818
  layer_summary_df, best_layer_df = _summarize_layers(scores_df=combined_df)
 
834
  "participant_run_info_path": str(participant_run_info_path),
835
  "required_runs": [int(value) for value in required_runs],
836
  "canonical_runs_used": [int(value) for value in canonical_runs_used],
 
837
  "n_protocol_c_rows": int(len(protocol_c_df)),
838
  "n_total_rows": int(len(combined_df)),
839
  }
 
846
  print(f"Subjects: {len(subjects)}")
847
  print(f"Layers: {len(layers)}")
848
  print(f"Alpha: {float(args.alpha)}")
 
849
  print(f"Protocol C rows: {len(protocol_c_df)}")
850
  print(f"Output directory: {output_dir}")
851
  print("=" * 72)
hf_jobs.py CHANGED
@@ -297,7 +297,7 @@ def build_parser() -> argparse.ArgumentParser:
297
  parser.add_argument("--model-profile", default="current")
298
  parser.add_argument("--model-slug", default="auto")
299
  parser.add_argument("--alpha", type=float, default=300.0)
300
- parser.add_argument("--protocols", default="B,C")
301
  parser.add_argument("--metric", default="mean_corr")
302
  parser.add_argument("--allowed-runs", default="1,2,3,4")
303
  parser.add_argument("--exclude-subjects", default="sub-03,sub-18")
 
297
  parser.add_argument("--model-profile", default="current")
298
  parser.add_argument("--model-slug", default="auto")
299
  parser.add_argument("--alpha", type=float, default=300.0)
300
+ parser.add_argument("--protocols", default="C")
301
  parser.add_argument("--metric", default="mean_corr")
302
  parser.add_argument("--allowed-runs", default="1,2,3,4")
303
  parser.add_argument("--exclude-subjects", default="sub-03,sub-18")