bootstrap_MRIQC/bootstrap_MRIQC_TRR-TEMPLATE_JSC.sh

519 lines
17 KiB
Shell

set -e -u
# project name space
PROJECT="mriqc"
# define SAMPLE to be processed
SAMPLE="q01-*"
# define the output store to push all results to
output_store="https://hub.trr379.de/f.hoffstaedter"
# define and input ria-store only to clone from
inputstore="/p/project1/qaprep/ria-trr379_inputstore"
# add spec to name for debugging runs
spec=""
# define clonable location for BIDS inputs
raw_store="https://hub.trr379.de/${SAMPLE}/q01-bids-dataset"
raw_ds=""
# define temporal working directory for job execution
temporary_store="/dev/shm"
# define location for annex cache to store container
annex_cache="/p/scratch/icei-hbp-00000000004/STAGE/annex-cache"
# how much to parallelize your single subject functional jobs
parallelize=2
RAM=6000
### parallelization of subjects on compute nodes for node wise job submission
nodesubs=128
# mriqc container from the Repronim container datalad dataset
container_store="https://github.com/ReproNim/containers.git"
container_ds=""
container='bids-mriqc'
version='24.0.2'
##### don't change anything below if you are not sure what you want to do #####
# define ID for git commits (take from local user configuration)
git_name="$(git config user.name)"
git_email="$(git config user.email)"
# define and create the input ria-store only to clone from
input_store="ria+file://${inputstore}"
mkdir -p ${inputstore}
# all results a tracked in a single output dataset
# create a fresh one
# job submission will take place from a checkout of this dataset, but no
# results will be pushed into it
# Create a source dataset with all analysis components as an analysis access
# point.
datalad create -c yoda ${SAMPLE}-${PROJECT}${version}${spec}
cd ${SAMPLE}-${PROJECT}${version}${spec}
# add html in unlocked mode and keep tsvs and datalset_description in git
git annex config --set annex.addunlocked 'include=*.html'
echo ".bidsignore annex.largefiles=nothing
dataset_description.json annex.largefiles=nothing
group*.tsv annex.largefiles=nothing
mclf_data-unseen_pred.csv annex.largefiles=nothing" >> .gitattributes
datalad save -m "keep .bidsignore, dataset_description.json & group results in git"
# the actual compute job specification
cat > README.md << EOT
# ${SAMPLE} - MRIQC derivatives
This dataset is a BIDS Derivatives dataset resulting from running MRIQC ${version} on [${SAMPLE}](https://openneuro.org/datasets/${SAMPLE}).
## Methods
MRIQC documentation can be found on https://mriqc.readthedocs.io/.
This dataset was created using [BOOTSTRAP - a high-throughput container workflow for MRIQC](https://cerebra.fz-juelich.de/f.hoffstaedter/bootstrap_MRIQC) implementing the [FAIRly Big Workflow](https://doi.org/10.1038/s41597-022-01163-2) using [Datalad](https://www.datalad.org/) and [Singularity containers](https://apptainer.org/) for full reproducibility and provenance capture including resource consumption via [con-duct](https://github.com/con/duct).
## Acknowledgements
The authors from the Institute of Neuroscience and Medicine (INM-7) at the Forschungszentrum Jülich, Germany gratefully acknowledge computing time on the supercomputer JURECA[1] at Forschungszentrum Jülich under grant no. 65913.
[1] Jülich Supercomputing Centre. (2021). JURECA: Data Centric and Booster Modules implementing the Modular Supercomputing Architecture at Jülich Supercomputing Centre Journal of large-scale research facilities, 7, A182. http://dx.doi.org/10.17815/jlsrf-7-182
EOT
datalad save -m "add README with Acknowledgements"
# clone the container-dataset as a subdataset.
datalad clone -d . "${container_store}" code/containers
# configure a custom container call to satisfy the needs of this analysis
datalad run -m "Freeze mriqc container version" \
code/containers/scripts/freeze_versions --save-dataset=. ${container}=${version}
datalad run -m "Setup elderly mriqc version with classification support" \
code/containers/scripts/freeze_versions --save-dataset=. ${container}:${container}-clf=0.15.1
datalad run -m "Remove datalad-get option and do singularity exec instead of run" \
"sed -i -e 's, --no-datalad-get,,g' .datalad/config
sed -i -e 's, run, exec,g' .datalad/config"
# create dedicated input and output locations. Results will be pushed into the
# output sibling, and the analysis will start with a clone from the input
# sibling.
datalad create-sibling-ria -s ${PROJECT}_in "${input_store}" \
--alias ${SAMPLE}-${PROJECT}${version}${spec} --new-store-ok --storage-sibling off
git remote add ${PROJECT}_out ${output_store}
git push ${PROJECT}_out
# register the input dataset, a superdataset comprising all participants
datalad clone -d . "${raw_store}${raw_ds}" sourcedata/raw
# amend the previous commit with a nicer commit message
git commit --amend -m "Register ${SAMPLE} BIDS dataset as sourcedata"
# the actual compute job specification
cat > code/participant_job << EOT
#!/bin/bash
# the job assumes that it is a good idea to run everything in PWD
# the job manager should make sure that is true
# fail whenever something is fishy, use -x to get verbose logfiles
set -e -u -x
dssource="\$1"
pushgitremote="\$2"
subid="\$3"
export DUCT_OUTPUT_PREFIX="duct-logs/\${subid}_{datetime_filesafe}-{pid}."
# get the analysis dataset, which includes the inputs as well
# importantly, we do not clone from the lcoation that we want to push the
# results too, in order to avoid too many jobs blocking access to
# the same location and creating a throughput bottleneck
datalad clone "\${dssource}" ds
# all following actions are performed in the context of the superdataset
cd ds
# in order to avoid accumulation temporary git-annex availability information
# and to avoid a syncronization bottleneck by having to consolidate the
# git-annex branch across jobs, we will only push the main tracking branch
# back to the output store (plus the actual file content). Final availability
# information can be establish via an eventual "git-annex fsck -f ${PROJECT}_out-storage".
# this remote is never fetched, it accumulates a larger number of branches
# and we want to avoid progressive slowdown. Instead we only ever push
# a unique branch per each job (subject AND process specific name)
git remote add outputstore "\$pushgitremote"
# all results of this job will be put into a dedicated branch
git checkout -b "job_\${JOBID}"
# we pull down the input subject manually in order to discover relevant
# files. We do this outside the recorded call, because on a potential
# re-run we want to be able to do fine-grained recomputing of individual
# outputs. The recorded calls will have specific paths that will enable
# recomputation outside the scope of the original Condor setup
datalad get -n "sourcedata/raw/"
# setup annex cache for container dataset
datalad get -n "code/containers"
git -C code/containers remote add cache ${annex_cache}
git -C code/containers config remote.cache.annex-speculate-present true
git -C code/containers config remote.cache.annex-cost 10
git -C code/containers config remote.cache.annex-pull false
git -C code/containers config remote.cache.annex-push false
git -C code/containers config remote.cache.fetch do-not-fetch-from-this-remote:
# the meat of the matter
# look for T1w files in the input data for the given participant
# it is critical for reproducibility that the command given to
# "containers-run" does not rely on any property of the immediate
# computational environment (env vars, services, etc)
# do T1w first
datalad containers-run \\
-m "Compute MRIQC for T1w of \${subid}" \\
-n ${container} \\
-i sourcedata/raw/\${subid} \\
-i sourcedata/raw/dataset_description.json \\
mriqc sourcedata/raw mriqc participant \\
--participant-label \$subid \\
--modalities T1w \\
--no-datalad-get \\
--no-sub \\
--verbose \\
--nprocs ${nthreads} \\
--omp-nthreads ${nthreads} \\
--mem $mb_RAM \\
--work-dir tmp \\
--float32 \\
--verbose-reports || true
# do bold next
datalad containers-run \\
-m "Compute MRIQC for BOLD of \${subid}" \\
-n ${container} \\
-i sourcedata/raw/\${subid} \\
-i sourcedata/raw/dataset_description.json \\
mriqc sourcedata/raw mriqc participant \\
--participant-label \$subid \\
--modalities bold \\
--no-datalad-get \\
--no-sub \\
--verbose \\
--nprocs ${nthreads} \\
--omp-nthreads ${nthreads} \\
--mem $mb_RAM \\
--work-dir tmp \\
--float32 \\
--min-bold-length 10 \\
--verbose-reports || true
# then run dwi seperately and continue after error
datalad containers-run \\
-m "Compute MRIQC for DWI of \${subid}" \\
-n ${container} \\
-i sourcedata/raw/\${subid} \\
-i sourcedata/raw/dataset_description.json \\
mriqc sourcedata/raw mriqc participant \\
--participant-label \$subid \\
--modalities dwi \\
--no-datalad-get \\
--no-sub \\
--verbose \\
--nprocs ${nthreads} \\
--omp-nthreads ${nthreads} \\
--mem $mb_RAM \\
--work-dir tmp \\
--float32 \\
--min-dwi-length 6 \\
--verbose-reports || true
# file content first -- does not need a lock, no interaction with Git
git annex copy -t outputstore
# and the output branch
flock --verbose \$DSLOCKFILE git push outputstore
echo "SUBJECT JOB COMPLETED"
# job handler should clean up workspace
EOT
chmod +x code/participant_job
datalad save -m "Participant compute job implementation" code/participant_job
cat > code/process.sub << EOT
#!/bin/bash
subid="\$1"
executable=\$(pwd)/code/participant_job
# the job expects these environment variables for labeling and synchronization
# - JOBID: subject AND process specific ID to make a branch name from
# (must be unique across all (even multiple) submissions)
# including the cluster ID will enable sorting multiple computing attempts
# - DSLOCKFILE: lock (must be accessible from all compute jobs) to synchronize
# write access to the output dataset
# - DATALAD_GET_SUBDATASET__SOURCE__CANDIDATE__...:
# (additional) locations for datalad to locate relevant subdatasets, in case
# a configured URL is outdated
# - GIT_AUTHOR_...: Identity information used to save dataset changes in compute
# jobs
export JOBID=\${subid} \\
DSLOCKFILE=\$(pwd)/.condor_datalad_lock \\
GIT_AUTHOR_NAME='${git_name}' \\
GIT_AUTHOR_EMAIL='${git_email}' \\
REPRONIM_USE_DUCT=1
# essential args for "participant_job"
# 1: where to clone the analysis dataset
# 2: location to push the result git branch to. The "ria+" prefix is stripped.
# 3: ID of the subject to process
arguments="${input_store}#$(datalad -f '{infos[dataset][id]}' wtf -S dataset) \\
$(git remote get-url --push ${PROJECT}_out) \\
\${subid} \\
"
mkdir -p ${temporary_store}/tmp_\${subid:4}
cd ${temporary_store}/tmp_\${subid:4}
\${executable} \${arguments} \\
> $(pwd)/logs/\${subid}.out \\
2> $(pwd)/logs/\${subid}.err
chmod +w -R ${temporary_store}/tmp_\${subid:4} && \
rm -rf ${temporary_store}/tmp_\${subid:4}
EOT
chmod +x code/process.sub
datalad save -m "individual job submission" code/process.sub
cat > code/results.merger << EOT
#!/bin/bash
# fail whenever something is fishy, use -x to get verbose logfiles
set -e -x
# finalize FAIRly Big Workflow dataset
dssource="\$1"
if [[ ! -z \${dssource} ]]; then
datalad clone "\${dssource}" ds
cd ds
else
datalad update -s ${PROJECT}_out
fi
# octopus merge job branches and move data to root
git merge -m "Merge results" \$(git branch -al | grep 'job_' | tr -d ' ')
git mv mriqc/.bidsignore mriqc/* .
datalad save -m "move data to dataset root"
# clean git annex branch
if [[ ! -z \${dssource} ]]; then
git annex fsck --fast
else
git annex fsck -f ${PROJECT}_out --fast
fi
# declare local data clone as dead
git annex dead here
# datalad push merged results
if [[ ! -z \${dssource} ]]; then
datalad push --data nothing
else
datalad push --data nothing --to ${PROJECT}_out
fi
export DATALAD_GET_SUBDATASET__SOURCE__CANDIDATE__102container="ria+file:///p/data1/inm7/containerstores#{id}" \
REPRONIM_USE_DUCT=1 \
DUCT_OUTPUT_PREFIX="logs/duct/mriqc-group-stats_{datetime_filesafe}-{pid}_"
# get mri input for MRIQC group stats
datalad get -n sourcedata/raw
datalad get sourcedata/raw/dataset_description.json \\
\$(find sourcedata/raw -maxdepth 3 -name anat) \\
\$(find sourcedata/raw -maxdepth 3 -name func) \\
\$(find sourcedata/raw -maxdepth 3 -name dwi)
# run mriqc group stats
datalad containers-run \\
-m "Compute MRIQC group stats" \\
-n bids-mriqc \\
--explicit \\
-o 'group*' \\
-o .bidsignore \\
mriqc sourcedata/raw . group \\
--no-datalad-get \\
--notrack \\
--verbose \\
--work-dir tmp
datalad drop --what datasets --reckless kill -r -d sourcedata/raw
# run mriqc quality classifier
datalad containers-run \\
-m "run MRIQC classifier" \\
-n bids-mriqc-clf \\
-i group_T1w.tsv \\
"mriqc_clf --load-classifier -X group_T1w.tsv; \\
mv mclf_run*data-unseen_pred.csv mclf_data-unseen_pred.csv"
if [[ ! -z \${dssource} ]]; then
datalad push --data nothing
else
datalad push --data nothing --to ${PROJECT}_out
fi
echo SUCCESS
EOT
chmod +x code/results.merger
datalad save -m "finalize dataset by merging results branches into master" code/results.merger
# create clean up script
cat > code/killall.sh << EOT
#!/bin/bash
#
# if setup went wrong, delete everything
TOKEN=\$1
[[ "\$(read -e -p 'Are you sure you want to delete everything? [yes_sure!/NO]> '; echo \$REPLY)" == yes_sure! ]] && echo KILLALLNOW || exit
# delete wrong input & output RIA stores + aliases
rm -rf $(git config --get remote.${PROJECT}_in.url)
rm -f ${inputstore}/alias/${SAMPLE}-${PROJECT}${version}
curl -X 'DELETE' \
"https://hub.trr379.de/api/v1/repos/f.hoffstaedter/${SAMPLE}-${PROJECT}${version}${spec}" \
-H \'accept: application/json\' \
-H \"Authorization: token \${TOKEN}\"
# remove faulty dataset
cd ..
datalad drop --what datasets --reckless kill -r -d ${SAMPLE}-${PROJECT}${spec}${version}
EOT
chmod +x code/killall.sh
datalad save -m "add clean up script" code/killall.sh
###############################################################################
# SLURM SETUP START - FIXME remove or adjust this according to your needs.
###############################################################################
# *!*!*!* GNUPARALLEL is necessary for node wise job allocation *!*!*!*
# makes sure that the jobs per node don't exceed RAM and wall clock time !!
# SLURM compute environment for the whole dataset
cat > code/submit.sbatch << EOT
#!/bin/bash -x
#SBATCH --account=qaprep
#SBATCH --mail-user=f.hoffstaedter@fz-juelich.de
#SBATCH --mail-type=END
#SBATCH --job-name=${PROJECT}_${spec}${SAMPLE}
#SBATCH --output=logs/${SAMPLE}_${PROJECT}-out.%j
#SBATCH --error=logs/${SAMPLE}_${PROJECT}-err.%j
#SBATCH --time=24:00:00
#SBATCH --partition=dc-cpu-bigmem
#SBATCH --nodes=1
parallel --delay 0.2 -a \$1 -j $nodesubs
wait
exit
EOT
# create job.call-file for all commands to call
# each subject is processed on RAMDISK in an own dataset
cat > code/slurm.submit << EOT
#!/bin/bash -x
#
# redundant input per subject
subid=\$1
# define DSLOCKFILE, DATALAD & GIT ENV for participant_job
export DSLOCKFILE=$(pwd)/.SLURM_datalad_lock \
DATALAD_GET_SUBDATASET__SOURCE__CANDIDATE__102container="ria+file:///p/data1/inm7/containerstores#{id}" \
GIT_AUTHOR_NAME=\$(git config user.name) \
GIT_AUTHOR_EMAIL=\$(git config user.email) \
JOBID=${SAMPLE}_\${subid:4}.\${SLURM_JOB_ID} \
REPRONIM_USE_DUCT=1
# use subject specific folder
mkdir ${temporary_store}/\${JOBID}
cd ${temporary_store}/\${JOBID}
# run things
$(pwd)/code/participant_job \
${input_store}#$(datalad -f '{infos[dataset][id]}' wtf -S dataset) \
$(git remote get-url --push ${PROJECT}_out) \
\${subid} \
>$(pwd)/logs/\${JOBID}.out \
2>$(pwd)/logs/\${JOBID}.err
cd ${temporary_store}/
chmod 777 -R ${temporary_store}/\${JOBID}
rm -fr ${temporary_store}/\${JOBID}
EOT
cat > code/slurm.submit_post << EOT
#!/bin/bash -x
export JOBID=${SAMPLE}_postpro.\${SLURM_JOB_ID} \
REPRONIM_USE_DUCT=1
# use subject specific folder
mkdir ${temporary_store}/\${JOBID}
cd ${temporary_store}/\${JOBID}
# run things
$(pwd)/code/results.merger \
$(git remote get-url --push ${PROJECT}_out) \
>$(pwd)/logs/\${JOBID}.out \
2>$(pwd)/logs/\${JOBID}.err
cd ${temporary_store}/
chmod 777 -R ${temporary_store}/\${JOBID}
rm -fr ${temporary_store}/\${JOBID}
EOT
chmod +x code/slurm.submit_post
for s in $(find sourcedata/raw -maxdepth 1 -name 'sub-*' -printf '%f\n'); do
printf "code/slurm.submit $s\n" >> code/${SAMPLE}_mriqc.jobs
done
mkdir logs
echo logs >> .gitignore
echo tmp >> .gitignore
echo .bids_db >> .gitignore
echo .SLURM_datalad_lock >> .gitignore
# create run scripts for slurm batches
cat > code/run-mriqc_JURECA.sh << EOT
sbatch code/submit.sbatch code/${SAMPLE}_mriqc.jobs
EOT
chmod +x code/run-mriqc_JURECA.sh
datalad save -m "SLURM submission setup" code/ .gitignore
################################################################################
# SLURM SETUP END
################################################################################
# cleanup - we have generated the job definitions, we do not need to keep a
# massive input dataset around. Having it around wastes resources and makes many
# git operations needlessly slow
datalad drop --what datasets --reckless kill -r -d sourcedata/raw
datalad drop --what datasets --reckless kill -r -d code/containers
# make sure the fully configured input & output datasets are available
# from the designated ria stores
datalad push --to ${PROJECT}_in
datalad push --to ${PROJECT}_out
# submit condor dag to run the sample
sbatch code/submit.sbatch code/${SAMPLE}_mriqc.jobs
# if we get here, we are happy
echo SUCCESS ${SAMPLE} is submitted to SLURM