Skip to content

Commit d42c957

Browse files
committed
test script for brooke
1 parent 928f38f commit d42c957

1 file changed

Lines changed: 183 additions & 0 deletions

File tree

Lines changed: 183 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,183 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Compute number of TRs occupied by each modeled condition and by implicit baseline.
4+
5+
Best source:
6+
- events.tsv for modeled events
7+
- actual BOLD image + JSON for run length and TR
8+
9+
Outputs a TSV table with one row per run and columns for:
10+
- durations in seconds
11+
- durations in TRs
12+
"""
13+
14+
from pathlib import Path
15+
import json
16+
17+
import nibabel as nib
18+
import numpy as np
19+
import pandas as pd
20+
21+
22+
# ---------------------------------------------------------------------
23+
# EDIT THESE PATHS
24+
# ---------------------------------------------------------------------
25+
26+
bids_root = Path("/cbica/projects/grmpy/data/bids_datalad")
27+
deriv_root = Path(
28+
"/cbica/projects/grmpy/data/derivatives/fmriprep_func_full/fmriprep_func"
29+
)
30+
31+
task_label = "fracback"
32+
space_label = "MNI152NLin6Asym"
33+
34+
# Optional: restrict to one subject, e.g. "20238"
35+
subject_filter = 102041 # or None
36+
37+
output_tsv = Path(
38+
"/cbica/projects/grmpy/code/analysis/task_glm/fracback_condition_tr_counts.tsv"
39+
)
40+
41+
42+
# ---------------------------------------------------------------------
43+
# HELPERS
44+
# ---------------------------------------------------------------------
45+
46+
47+
def extract_bids_prefix(events_path: Path) -> str:
48+
name = events_path.name
49+
if not name.endswith("_events.tsv"):
50+
raise ValueError(f"Unexpected events filename: {name}")
51+
return name[:-11] # remove "_events.tsv"
52+
53+
54+
def normalize_trial_type(x: str) -> str:
55+
if pd.isna(x):
56+
return "unknown"
57+
x = str(x).strip()
58+
mapping = {
59+
"0BACK": "zero_back",
60+
"1BACK": "one_back",
61+
"2BACK": "two_back",
62+
"INSTRUCTION": "instruction",
63+
"zero_back": "zero_back",
64+
"one_back": "one_back",
65+
"two_back": "two_back",
66+
"instruction": "instruction",
67+
}
68+
return mapping.get(x, x)
69+
70+
71+
def seconds_to_trs(seconds: float, tr: float) -> float:
72+
return seconds / tr
73+
74+
75+
# ---------------------------------------------------------------------
76+
# MAIN
77+
# ---------------------------------------------------------------------
78+
79+
rows = []
80+
81+
events_paths = sorted(
82+
bids_root.glob(f"sub-*/ses-*/func/*task-{task_label}*_events.tsv")
83+
)
84+
85+
if subject_filter is not None:
86+
events_paths = [p for p in events_paths if f"sub-{subject_filter}" in str(p)]
87+
88+
for events_path in events_paths:
89+
prefix = extract_bids_prefix(events_path)
90+
91+
# Find matching BOLD json in raw BIDS
92+
bold_json = events_path.parent / f"{prefix}_bold.json"
93+
94+
# --- FIXED: allow extra entities like _res-2 ---
95+
deriv_pattern = (
96+
f"{events_path.parts[-4]}/{events_path.parts[-3]}/func/"
97+
f"{prefix}_space-{space_label}*_desc-preproc_bold.nii.gz"
98+
)
99+
100+
bold_candidates = sorted(deriv_root.glob(deriv_pattern))
101+
102+
print(f"\nProcessing: {events_path}")
103+
print("Looking for:", deriv_root / deriv_pattern)
104+
print("Found:", bold_candidates)
105+
106+
if not bold_json.exists():
107+
print(f"Skipping {events_path}: missing bold JSON {bold_json}")
108+
continue
109+
110+
if len(bold_candidates) == 0:
111+
print(f"Skipping {events_path}: no matching preproc bold image found")
112+
continue
113+
114+
bold_img_path = bold_candidates[0]
115+
116+
# Load events
117+
events = pd.read_csv(events_path, sep="\t")
118+
if (
119+
"trial_type" not in events.columns
120+
or "onset" not in events.columns
121+
or "duration" not in events.columns
122+
):
123+
print(f"Skipping {events_path}: missing required columns")
124+
continue
125+
126+
events["trial_type"] = events["trial_type"].map(normalize_trial_type)
127+
events["onset"] = pd.to_numeric(events["onset"], errors="coerce")
128+
events["duration"] = pd.to_numeric(events["duration"], errors="coerce")
129+
events = events.dropna(subset=["onset", "duration"])
130+
131+
# Load TR
132+
with open(bold_json, "r") as f:
133+
meta = json.load(f)
134+
tr = float(meta["RepetitionTime"])
135+
136+
# Load n_scans from NIfTI
137+
img = nib.load(str(bold_img_path))
138+
n_scans = img.shape[3]
139+
total_run_sec = n_scans * tr
140+
141+
# Sum modeled durations by condition
142+
dur_zero = events.loc[events["trial_type"] == "zero_back", "duration"].sum()
143+
dur_two = events.loc[events["trial_type"] == "two_back", "duration"].sum()
144+
dur_instr = events.loc[events["trial_type"] == "instruction", "duration"].sum()
145+
146+
modeled_sec = dur_zero + dur_two + dur_instr
147+
implicit_baseline_sec = total_run_sec - modeled_sec
148+
149+
# Guard against tiny negative values from rounding
150+
if implicit_baseline_sec < 0 and abs(implicit_baseline_sec) < 1e-6:
151+
implicit_baseline_sec = 0.0
152+
153+
row = {
154+
"subject": events_path.parts[-4],
155+
"session": events_path.parts[-3],
156+
"run_prefix": prefix,
157+
"tr_sec": tr,
158+
"n_scans": n_scans,
159+
"total_run_sec": total_run_sec,
160+
"zero_back_sec": dur_zero,
161+
"two_back_sec": dur_two,
162+
"instruction_sec": dur_instr,
163+
"implicit_baseline_sec": implicit_baseline_sec,
164+
"zero_back_trs": seconds_to_trs(dur_zero, tr),
165+
"two_back_trs": seconds_to_trs(dur_two, tr),
166+
"instruction_trs": seconds_to_trs(dur_instr, tr),
167+
"implicit_baseline_trs": seconds_to_trs(implicit_baseline_sec, tr),
168+
}
169+
rows.append(row)
170+
171+
summary_df = pd.DataFrame(rows)
172+
173+
if len(summary_df) == 0:
174+
raise RuntimeError("No runs processed. Check your paths and filenames.")
175+
176+
# Optional prettier rounding
177+
for col in summary_df.columns:
178+
if col.endswith("_sec") or col.endswith("_trs") or col == "tr_sec":
179+
summary_df[col] = summary_df[col].round(3)
180+
181+
summary_df.to_csv(output_tsv, sep="\t", index=False)
182+
print(f"\nWrote {output_tsv}")
183+
print(summary_df.head())

0 commit comments

Comments
 (0)