66
77from __future__ import annotations
88
9+ import logging
910from dataclasses import dataclass
1011from typing import TYPE_CHECKING , Literal
1112
13+ import nibabel as nib
1214import polars as pl
1315from tqdm import tqdm
1416
1517from rbc .cli import _DEFAULT_ENV_VARS , _FUNC_GROUP_ENTITIES , _SUB_SES_QUERY
1618from rbc .cli .base import BaseArgs , _validate_atlas , _validate_positive , _validate_task
17- from rbc .context import PipelineContext
19+ from rbc .context import RunContext
1820from rbc .core .bids import Datatype , Suffix , TemplateSpace , extract_entities
1921from rbc .core .bids2table import load_table
2022from rbc .core .niwrap import setup_runner
2325if TYPE_CHECKING :
2426 import argparse
2527 from collections .abc import Sequence
28+ from pathlib import Path
2629
2730 from rbc_resources import AtlasName
2831
@@ -35,6 +38,7 @@ class MetricsArgs(BaseArgs):
3538 fwhm : float
3639 task : str | None
3740 regressor : Sequence [Literal ["36-parameter" , "aCompCor" ]]
41+ tr : float | None
3842
3943 @classmethod
4044 def validate_namespace (cls , ns : argparse .Namespace ) -> MetricsArgs :
@@ -43,21 +47,43 @@ def validate_namespace(cls, ns: argparse.Namespace) -> MetricsArgs:
4347 _validate_atlas (atlas )
4448 _validate_task (ns .task )
4549 _validate_positive (ns .fwhm , "FWHM" )
50+ _validate_positive (ns .tr , "TR" )
4651 return cls (
4752 ** BaseArgs .validate_namespace (ns ).__dict__ ,
4853 atlas = ns .atlas ,
4954 fwhm = ns .fwhm ,
5055 task = ns .task ,
5156 regressor = ns .regressor ,
57+ tr = ns .tr ,
5258 )
5359
5460
61+ _logger = logging .getLogger (__name__ )
62+
63+
64+ def _read_header_tr (nifti_path : Path ) -> float :
65+ """Read TR from a NIfTI header, raising on missing/zero values."""
66+ hdr = nib .nifti1 .load (nifti_path ).header
67+ tr = float (hdr ["pixdim" ][4 ]) # type: ignore[index]
68+ if tr <= 0 :
69+ msg = (
70+ f"NIfTI header TR is { tr } for { nifti_path } . Pass --tr to specify manually."
71+ )
72+ raise ValueError (msg )
73+ _logger .info ("TR: %.4f s (from NIfTI header)" , tr )
74+ return tr
75+
76+
5577def main (args : MetricsArgs ) -> int :
5678 """Main entrypoint of metrics workflow."""
5779 ctx = setup_runner (runner = args .runner , verbose = args .verbose , tmp_dir = args .tmp_dir )
5880 ctx .runner .environ = _DEFAULT_ENV_VARS
5981
6082 ctx .logger .info ("Preparing to run RBC metrics workflow" )
83+ if args .tr is not None :
84+ ctx .logger .info ("Using CLI-provided TR: %.4f s" , args .tr )
85+ else :
86+ ctx .logger .info ("TR will be read from NIfTI headers" )
6187 df = load_table (
6288 dataset_dir = args .output_dir ,
6389 index_fpath = None ,
@@ -82,7 +108,7 @@ def main(args: MetricsArgs) -> int:
82108 for _ , group in tqdm (df .group_by (_SUB_SES_QUERY ), disable = not ctx .verbose ):
83109 sub : str = group ["sub" ][0 ]
84110 ses : str | None = group ["ses" ][0 ] or None
85- pipe_ctx = PipelineContext (sub = sub , ses = ses , output_dir = args .output_dir )
111+ pipe_ctx = RunContext (sub = sub , ses = ses , output_dir = args .output_dir )
86112
87113 deriv_df = load_table (
88114 dataset_dir = args .output_dir , index_fpath = None , max_workers = 0 , verbose = False
@@ -115,10 +141,13 @@ def main(args: MetricsArgs) -> int:
115141 extra = {"reg" : regressor },
116142 )
117143
144+ tr = args .tr if args .tr is not None else _read_header_tr (regressed_bold )
145+
118146 outputs = single_session_metrics (
119147 regressed_bold = regressed_bold ,
120148 cleaned_bold = cleaned_bold ,
121149 template_brain_mask = template_brain_mask ,
150+ tr = tr ,
122151 atlas = args .atlas ,
123152 fwhm = args .fwhm ,
124153 )
@@ -199,5 +228,11 @@ def register_command(
199228 "functional preprocessing."
200229 ),
201230 )
231+ parser .add_argument (
232+ "--tr" ,
233+ type = float ,
234+ default = None ,
235+ help = "Repetition time in seconds. Overrides NIfTI header value for ALFF." ,
236+ )
202237
203238 parser .set_defaults (func = lambda args : main (MetricsArgs .validate_namespace (args )))
0 commit comments