Refactor A1 evaluation protocols to focus exclusively on Protocol C; update documentation and scripts accordingly.
Browse files- README.md +13 -18
- code/run_a1_bootstrap.py +96 -70
- code/run_a1_end_to_end.py +2 -2
- code/run_a1_fit.py +6 -267
- hf_jobs.py +1 -1
README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
-
## Plan: A1 SI Baseline with
|
| 2 |
|
| 3 |
-
Implement A1 as a frozen SI baseline with
|
| 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:
|
| 42 |
-
- Protocol
|
| 43 |
-
-
|
| 44 |
-
-
|
| 45 |
-
-
|
| 46 |
-
-
|
| 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
|
| 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
|
| 87 |
-
-
|
| 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 |
-
-
|
| 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
|
| 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
|
| 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
|
| 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 |
-
"--
|
| 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=
|
| 151 |
-
help="Protocol
|
| 152 |
)
|
| 153 |
parser.add_argument(
|
| 154 |
-
"--
|
| 155 |
type=int,
|
| 156 |
-
default=
|
| 157 |
-
help="
|
| 158 |
)
|
| 159 |
parser.add_argument(
|
| 160 |
-
"--
|
| 161 |
type=int,
|
| 162 |
-
default=
|
| 163 |
-
help="
|
| 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 |
-
|
| 497 |
-
|
| 498 |
-
|
| 499 |
-
|
| 500 |
-
|
| 501 |
-
|
| 502 |
-
|
| 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 =
|
| 525 |
-
manifest_df
|
| 526 |
-
|
| 527 |
-
|
| 528 |
-
|
| 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 |
-
"
|
| 649 |
-
"
|
| 650 |
-
"
|
| 651 |
-
"
|
| 652 |
-
"
|
| 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
|
| 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="
|
| 116 |
-
help="Fit protocols to execute:
|
| 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 = {"
|
| 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="
|
| 141 |
-
help="Protocols to run:
|
| 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
|
| 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 =
|
| 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="
|
| 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")
|