Skip to content

Commit f654632

Browse files
committed
WIP/feat(tat2): -fmriprep option
1 parent fb44964 commit f654632

3 files changed

Lines changed: 209 additions & 14 deletions

File tree

t/tat2-fd.bats

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
setup(){
2+
source tat2
3+
export FD_THRES=0.3
4+
export tsvfile="$BATS_TEST_TMPDIR/sub-11924_ses-1_task-rest_run-1_desc-confounds_timeseries.tsv"
5+
printf "a\tb\tframewise_displacement\n0\t0\t99\n0\t0\t0.01\n0\t0\t0.5\n0\t0\t0.2\n" > $tsvfile
6+
}
7+
8+
test_fd_from_fmriprep { # @test
9+
run fmriprep_to_fd_censor $tsvfile
10+
[[ ${output} =~ 0.1.0.1 ]]
11+
12+
export FD_THRES=100
13+
run fmriprep_to_fd_censor $tsvfile
14+
[[ ${output} =~ 1.1.1.1 ]]
15+
16+
sed -i s/framewise/xxxx/ $tsvfile
17+
run fmriprep_to_fd_censor $tsvfile
18+
[ $status -eq 2 ]
19+
[[ $output =~ "no 'framewise" ]]
20+
}
21+
22+
test_update_with_censor { # @test
23+
input=$tsvfile.nii.gz
24+
25+
# need as many timepoints as fd lines
26+
3dUndump -dimen 2 2 2 -overwrite -ijk -prefix ${tsvfile}_1.nii.gz -fval 10 -dval 1
27+
3dcalc -a ${tsvfile}_1.nii.gz -b '1D: 4@1' -expr 'a*b' -prefix $tsvfile.nii.gz
28+
censor_file=n/a
29+
censor_rel='s/.nii.gz//'
30+
update_with_censor $BATS_TEST_TMPDIR
31+
echo "input: $input; censor_file: $censor_file" >&2
32+
[[ $censor_file == $BATS_TEST_TMPDIR/fd-0.3.1D ]]
33+
test -r $censor_file
34+
[[ $(where1csv $censor_file) == 1,3 ]]
35+
[[ $input =~ \[1,3]$ ]]
36+
37+
}

t/tat2-fmriprep.bats

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
setup(){
2+
export PATH="$PWD:$PATH"
3+
cd $BATS_TEST_TMPDIR
4+
5+
3dUndump -dimen 2 2 2 -overwrite -ijk -prefix 3d.nii.gz -fval 10 -dval 1
6+
3dcalc -a 3d.nii.gz -b '1D: 4@1' -expr 'gran(0,1)' -prefix 4d.nii.gz
7+
printf "a\tb\tframewise_displacement\n0\t0\tn/a\n0\t0\t0.01\n0\t0\t0.5\n0\t0\t0.2\n" > confound.tsv
8+
for subj in 1 2; do
9+
funcdir=deriv/sub-$subj/ses-1/func
10+
mkdir -p $funcdir
11+
for prefix in $funcdir/sub-${subj}_ses-1_task-{rest_run-{1,2},nback}; do
12+
sed "s:0.5:0.$subj:" confound.tsv > ${prefix}_desc-confounds_timeseries.tsv
13+
ln -s $PWD/4d.nii.gz ${prefix}_space-MNI152NLin2009cAsym_desc-preproc_bold.nii.gz
14+
ln -fs $PWD/3d.nii.gz ${prefix}_space-MNI152NLin2009cAsym_desc-brain_mask.nii.gz
15+
done
16+
done
17+
18+
}
19+
20+
test_find_all { # @test
21+
run tat2 -verbose -fmriprep $BATS_TEST_TMPDIR/deriv
22+
tree deriv >&2
23+
test -r deriv/sub-1/ses-1/func/sub-1_ses-1_desc-preproc_tat2star.nii.gz
24+
test -r deriv/sub-2/ses-1/func/sub-2_ses-1_desc-preproc_tat2star.nii.gz
25+
jq . deriv/sub-1/ses-1/func/sub-1_ses-1_desc-preproc_tat2star.log.json >&2
26+
[[ "$(jq '.censor_files[2]' deriv/sub-1/ses-1/func/sub-1_ses-1_desc-preproc_tat2star.log.json)" =~ fd-0.3 ]]
27+
28+
# rest 1 & 2 + nback == 3
29+
[[ "$(jq '.nt|length' deriv/sub-2/ses-1/func/sub-2_ses-1_desc-preproc_tat2star.log.json)" == 3 ]]
30+
}

tat2

Lines changed: 142 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,31 @@ censor_rel="" # 3dTcat: truncate input
1818
MAXVOLSTOTAL=-1 # " truncate how many volumes are ultimately averaged.
1919
IDX_SAMPLE_METHOD="first" # how to sample indexes (first, last, random) when MAXVOLS*
2020
tnorm_opt="-nzmean" # 3dTstat: final output (time normalized)
21+
FD_THRES=${FD_THRES:-0.3} # framewise displacement applied to confounds_timeseries.tsv
22+
23+
# GNU Parallel default options for per func/ files (added for -fmriprep, 20260102)
24+
PARALEL_OPTS=${PARALEL_OPTS:-"--eta --progress --total-jobs -P50%"}
2125

2226
OUTPUT=tat2star.nii.gz
2327
TMPLOCATION=/tmp
2428
CLEAN=1
2529
TMPD="" # will be folder specific to single invocation of tat2. removed at the end
2630
WRITE_JSON_LOG=1
2731

32+
# run through fmriprep. -fmriprep option added 20260102
33+
FMRIPREP_DERIV_ROOT=""
34+
FMRIPREP_TASK_PATTERN=${FMRIPREP_TASK_PATTERN:-"*_task-*desc-preproc_bold.nii.gz"}
35+
2836
# all the inputs (possibly globs) we want to work on
2937
declare -a GLOBFILES
3038

3139
filedesc_whichvol=""
3240

33-
TAT2_VER="20240719-calc_ln+BUGFIX-no_vol"
41+
TAT2_VER="1.0.0.20260102-fmriprep"
42+
# 1.0.0.20260102
43+
# add -fmriprep option
44+
# 20240719-calc_ln+BUGFIX-no_vol
45+
# novolume was 'x*1', fixed to be '(x/m)*m'
3446
# 20201116
3547
# back to mean default
3648
# add
@@ -67,6 +79,20 @@ SYNOPSIS
6779
final '$OUTPUT' will be in the cwd
6880
6981
OPTIONS (in order of relevance)
82+
-fmriprep /path/to/fmriprep/
83+
Specify path to fmriprep root. Will run for all func directories within provided path
84+
85+
Will overwrite -censor_rel and -mask_rel options to new defaults.
86+
87+
To change new defaults, specify modifications after -fmriprep option
88+
NB. without -fmriprep, tat2 creates a single file from all inputs
89+
with -fmriprep, tat2start.nii.gz file PER func/ directory
90+
91+
GNU Parallel will be used when avalaible to parallelize.
92+
Parameters set with environment variable
93+
export PARALEL_OPTS='$PARALEL_OPTS'
94+
95+
7096
-censor_rel FILE
7197
specify the motion censor file. single column no header file. nLines = nVols. 0=exclude, 1=keep.
7298
either a filename or regexp
@@ -197,7 +223,8 @@ collapse_seq_idx(){
197223

198224
parse_args(){
199225
# keep args around for 3dNotes history
200-
allargs="$*"
226+
allargs="$*" # single long string, into json/log
227+
ALLARGS_=("$@") # used to forward tat2 when -fmriprep
201228

202229

203230
# read in any arguments/paramaters
@@ -223,6 +250,15 @@ parse_args(){
223250
-inverse) t2_inv=1; shift;;
224251
-no_voxscale)vox_scale=0; shift;;
225252
-noclean) CLEAN=0; shift;;
253+
-fmriprep)
254+
FMRIPREP_DERIV_ROOT="${2:?expect fmriprep deriviate directory}"; shift 2
255+
[[ "$MASK_REL" != "subject_mask.nii.gz" ]] &&
256+
echo "WARNING: -mask_rel '$MASK_REL' defined before -fmriprep, overwriting" >&2
257+
MASK_REL='s/_desc-preproc_bold/_desc-brain_mask/'
258+
[ -n "$censor_rel" ] &&
259+
echo "WARNING: -censor_rel '$censor_rel' defined before -fmriprep, overwriting" >&2
260+
censor_rel='s/(task-[^_]+(_run-[^_]+)?)_.*/\1_desc-confounds_timeseries.tsv/'
261+
;;
226262
-verbose) set -x; shift;;
227263
-h*) usage;;
228264
-*) echo "unknown option '$1'"; usage;;
@@ -251,30 +287,65 @@ args_are_sane(){
251287
err "must use -no_voxscale with -calc_zscore or -calc_ln"
252288
fi
253289

254-
echo "#files/globs: ${#GLOBFILES[@]}"
255-
# need to have at least one file to average
256-
[ -z "${GLOBFILES[*]}" ] && usage
290+
# only check globs/inputs if not running as fmriprep
291+
if [ -z "$FMRIPREP_DERIV_ROOT" ]; then
292+
echo "#files/globs: ${#GLOBFILES[@]}"
293+
# need to have at least one file to average
294+
[ -z "${GLOBFILES[*]}" ] && usage
295+
296+
# how many files do we have
297+
nfiles=$(find -L ${GLOBFILES[@]} -maxdepth 0 | wc -l)
298+
[ $nfiles -eq 0 ] && err "no files match input GLOBFILESs: ${GLOBFILES[@]}"
299+
[ $nfiles -eq 1 ] && echo "WARNING: only one file matches '${GLOBFILES[@]}'. expected all (>1) runs"
300+
[ $nfiles -gt 10 ] && echo "WARNING: running on $nfiles epi files! Are you sure you don't want to run one visit at a time?"
301+
302+
[ -r "$OUTPUT" ] && echo "# have $OUTPUT; rm $OUTPUT # to redo" && exit 0
303+
fi
304+
257305

258-
# how many files do we have
259-
nfiles=$(find -L ${GLOBFILES[@]} -maxdepth 0 | wc -l)
260-
[ $nfiles -eq 0 ] && err "no files match input GLOBFILESs: ${GLOBFILES[@]}"
261-
[ $nfiles -eq 1 ] && echo "WARNING: only one file matches '${GLOBFILES[@]}'. expected all (>1) runs"
262-
[ $nfiles -gt 10 ] && echo "WARNING: running on $nfiles epi files! Are you sure you don't want to run one visit at a time?"
263306

264-
[ -r "$OUTPUT" ] && echo "# have $OUTPUT; rm $OUTPUT # to redo" && exit 0
265307
[ "$MAXVOLS" == "1" ] && echo "-maxvols cannot be 1. 3dcalc doesn't like applying a single value 1D file to a 4D dataset" && exit 1
266308
return 0
267309
}
268310

311+
fmriprep_to_fd_censor(){
312+
local tsvfile=${1:?expect framewise displacement file}
313+
! test -r $tsvfile &&
314+
echo "ERROR: could not find expected file '$tsvfile'" >&2 &&
315+
return 1
316+
317+
! perl -slane 'BEGIN{
318+
$i=-1;
319+
@c=split/\t/,<>;
320+
foreach (@c){
321+
++$i;
322+
last if /framewise_displacement/;
323+
exit 100 if $i >= $#c;
324+
}}
325+
print $F[$i]>$FD_THRES?0:1;' -- -FD_THRES="$FD_THRES" < $tsvfile &&
326+
echo "ERROR: no 'framewise_displacement' in $tsvfile" >&2 &&
327+
return 2
328+
return 0
329+
}
330+
269331
update_with_censor(){
270332
declare -g input filedesc_whichvol censor_file
333+
local tmpd=${1:-$TMPLOCATION} # only used if fmrirep/rewriting
271334
filedesc_whichvol=""
272335
readonly censor_rel
273336
local idxs nkeep tat2inputfile
274337
# update 'input' and 'runoutput'
275338
# censor input
276339
if [ -n "$censor_rel" ]; then
277340
censor_file=$(find_rel_file "$input" "$censor_rel")
341+
# SPECIAL CASE (20260102): fmriprep input is not
342+
if [[ $censor_file =~ desc-confounds_timeseries.tsv$ ]]; then
343+
new_censor="$tmpd/$(basename $censor_file _timeseries.tsv)_fd-$FD_THRES.1D"
344+
msg "apply fd<$FD_THRES from '$censor_file' to '$new_censor'"
345+
test -r "$new_censor" && echo "WARNING: overwriting an existing censor file $new_censor" >&2
346+
fmriprep_to_fd_censor "$censor_file" > $new_censor
347+
censor_file=$new_censor
348+
fi
278349
# if MAXVOLS is default "-1", firstn_cvs does nothing
279350
idxs=$(where1csv $censor_file| idx_shuffle $IDX_SAMPLE_METHOD | firstn_csv $MAXVOLS)
280351
nkeep=$(echo $idxs| tr ',' '\n' |wc -l)
@@ -364,7 +435,7 @@ one_ta(){
364435

365436
# change $input and $filedesc_whichvol if $cesnor_rel exist
366437
censor_file="" # updated version of '$censor_rel' if used
367-
update_with_censor
438+
update_with_censor "$tmpd"
368439

369440
# must match *_tat2.nii.gz to be picked up by 3dTstat at the end
370441
runoutput="$tmpd/${cnt}${filedesc_whichvol}${filedesc_volnorm}_tat2.nii.gz"
@@ -431,6 +502,11 @@ json_list(){
431502
}
432503

433504
_tat2(){
505+
# would see this message on arg check unless running w/-fmriprep
506+
test -r "$OUTPUT" &&
507+
msg "# have $OUTPUT. rm '$OUTPUT' # to redo" &&
508+
return 0
509+
434510
TMPD=$(mktemp -d $TMPLOCATION/tat2star_XXXX)
435511
cnt=0
436512
volcount=0
@@ -528,7 +604,7 @@ _tat2(){
528604
[ "${WRITE_JSON_LOG:-1}" -eq 1 ] &&
529605
cat <<- HEREDOC > "${OUTPUT/.nii.gz/.log.json}"
530606
{
531-
"cmd": "$0 $allargs",
607+
"cmd": "$0 $(sed 's:\\:\\\\:g' <<< "$allargs")",
532608
"roistats_cmds": "${all_roistats}",
533609
"volume_norm_cmds": "${all_calc_cmd}",
534610
"collapse_cmd": "${Tstat_cmd}",
@@ -561,11 +637,63 @@ _tat2(){
561637
return 0
562638
}
563639

640+
_fmriprep_loop(){
641+
# run all of fmripre func/ files
642+
# 'deriv/sub-x/ses-y/func' or 'deriv/sub-x/func/'
643+
mapfile -t bold_dirs < <(find "$FMRIPREP_DERIV_ROOT" -name 'func' -type d)
644+
msg "found ${#bold_dirs[@]} 'func' folders (fmriprep sessions)"
645+
[ ${#bold_dirs[@]} -eq 0 ] &&
646+
err "No $FMRIPREP_DERIV_ROOT/**/func folders to work with!"
647+
648+
# ALLARGS_ includes two we want to remove: -fmriprep /path/to/deriv
649+
noprepargs=(); i=0
650+
while [ $i -lt ${#ALLARGS_[@]} ]; do
651+
arg=${ALLARGS_[$i]}
652+
[[ $arg == -fmriprep ]] && let i=i+2 && continue
653+
noprepargs+=("$arg")
654+
let ++i
655+
done
656+
657+
if command -v env_parallel >/dev/null && [ -n "$PARALEL_OPTS" ]; then
658+
export FMRIPREP_TASK_PATTERN FMRIPREP_DERIV_ROOT
659+
export -f _tat2_fmriprep
660+
parallel $PARALEL_OPTS -N1 _tat2_fmriprep {1} -censor_rel "'$censor_rel'" -mask_rel "'$MASK_REL'" "${noprepargs[@]}" ::: "${bold_dirs[@]}"
661+
else
662+
echo "WARNING: not parallelizing. install GNU Parallel to use mutliple cores." >&2
663+
for session_func in "${bold_dirs[@]}"; do
664+
msg "starting $session_func"
665+
# continue across failures -- don't want one bad session to bring it all down
666+
_tat2_fmriprep "$session_func" "${noprepargs[@]}" || :
667+
done
668+
fi
669+
}
670+
_tat2_fmriprep(){
671+
# expect to be called by _fmriprep_loop after parse_args sets all bug GLOBFILES
672+
local session_func=${1:?expect fmriprep func/ directory}; shift;
673+
mapfile -t GLOBFILES < <(find $session_func -iname "$FMRIPREP_TASK_PATTERN" -not -iname '*echo-*')
674+
[ ${#GLOBFILES[@]} -eq 0 ] &&
675+
echo "WARNING: no non-echo task files in $session_func" >&2 &&
676+
return 1
677+
678+
! [[ ${GLOBFILES[0]} =~ sub-[^_/]+(_ses-[^_/]+)?_ ]] &&
679+
echo "ERROR: no 'sub-*' in first input of $session_func: '${GLOBFILES[0]}'" &&
680+
return 2
681+
682+
# TODO: option to save outside of fmrirep func dir
683+
tat2 -output "$session_func/${BASH_REMATCH}desc-preproc_tat2star.nii.gz" "$@" ${GLOBFILES[@]}
684+
}
685+
564686
# run if not sourced
565687
if ! [[ "$(caller)" != "0 "* ]]; then
566688
set -eou pipefail
567689
# 20210218WF - hygenic file usage -- tmp files after crash clog fileystem
568690
trap '[ -n "$TMPD" -a -d "$TMPD" -a $CLEAN -eq 1 ] && [[ "$TMPD" =~ $TMPLOCATION ]] && rm -r "$TMPD"' EXIT
569691
args_are_sane "$@"
570-
_tat2
692+
693+
# running on many at once!
694+
if [ -n "$FMRIPREP_DERIV_ROOT" ]; then
695+
_fmriprep_loop
696+
else
697+
_tat2
698+
fi
571699
fi

0 commit comments

Comments
 (0)