Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft pre-init ingression #102

Merged
merged 19 commits into from
Sep 23, 2024
Merged
2 changes: 1 addition & 1 deletion .circleci/config.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ orbs:

.dockersetup: &dockersetup
docker:
- image: pennlinc/qsirecon_build:24.8.3
- image: pennlinc/qsirecon_build:24.9.0
working_directory: /src/qsirecon

runinstall: &runinstall
Expand Down
2 changes: 1 addition & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ RUN pip install build
RUN apt-get update && \
apt-get install -y --no-install-recommends git

FROM pennlinc/qsirecon_build:24.8.3
FROM pennlinc/qsirecon_build:24.9.0

# Install qsirecon
COPY . /src/qsirecon
Expand Down
5 changes: 4 additions & 1 deletion docs/input_data.rst
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,10 @@ HCP Young Adult Preprocessed Data
=================================

To use minimally preprocessed dMRI data from HCP-YA specify ``--input-type hcpya``.
The included FNIRT transforms are usable directly. NOTE: this does not work yet.
Note that the transforms to/from MNI space are not able to be used at this time. Please note that if you have the
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great idea to add this to the docs

HCPYA dataset from datalad (https://github.com/datalad-datasets/human-connectome-project-openaccess)
then you should ``datalad get`` relevant subject data before running QSIRecon,
and be mindful about how you mount the directory in Docker/Apptainer.

.. _anat_reqs:

Expand Down
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,11 +23,12 @@ dependencies = [
"filelock",
"fury",
"indexed_gzip <= 1.8.7",
"Ingress2QSIRecon == 0.2.1",
"jinja2 < 3.1",
"matplotlib",
"networkx ~= 2.8.8",
"nibabel <= 5.2.0",
"nilearn == 0.10.1",
"nibabel <= 6.0.0",
"nilearn",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is there a need to unpin this one?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ah this was a holdout from some dependency differences between qsirecon and i2q, we should be able to pin it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done in last commit

"nipype == 1.8.6",
"nireports ~= 24.0.2",
"niworkflows >=1.9,<= 1.10",
Expand Down
82 changes: 66 additions & 16 deletions qsirecon/cli/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -116,11 +116,16 @@ def _bids_filter(value, parser):
# required, positional arguments
# IMPORTANT: they must go directly with the parser object
parser.add_argument(
"bids_dir",
"input_dir",
action="store",
metavar="input_dir",
type=PathExists,
help="The root folder of a BIDS valid dataset (sub-XXXXX folders should "
"be found at the top level in this folder).",
help=(
"The root folder of the input dataset "
"(subject-level folders should be found at the top level in this folder). "
"If the dataset is not BIDS-valid, "
"then a BIDS-compliant version will be created based on the --input-type value."
),
)
parser.add_argument(
"output_dir",
Expand Down Expand Up @@ -294,7 +299,7 @@ def _bids_filter(value, parser):
default="qsiprep",
choices=["qsiprep", "ukb", "hcpya"],
help=(
"Specify which pipeline was used to create the data specified as the bids_dir."
"Specify which pipeline was used to create the data specified as the input_dir."
"Not necessary to specify if the data was processed by QSIPrep. "
"Other options include "
'"ukb" for data processed with the UK BioBank minimal preprocessing pipeline and '
Expand Down Expand Up @@ -442,39 +447,84 @@ def parse_args(args=None, namespace=None):
f"total threads (--nthreads/--n_cpus={config.nipype.nprocs})"
)

bids_dir = config.execution.bids_dir
input_dir = config.execution.input_dir
output_dir = config.execution.output_dir
work_dir = config.execution.work_dir
version = config.environment.version

# Update the config with an empty dict to trigger initialization of all config
# sections (we used `init=False` above).
# This must be done after cleaning the work directory, or we could delete an
# open SQLite database
config.from_dict({})

# Ensure input and output folders are not the same
if output_dir == bids_dir:
if output_dir == input_dir:
parser.error(
"The selected output folder is the same as the input BIDS folder. "
"Please modify the output path (suggestion: %s)."
% bids_dir
% input_dir
/ "derivatives"
/ ("qsirecon-%s" % version.split("+")[0])
)

if bids_dir in work_dir.parents:
if input_dir in work_dir.parents:
parser.error(
"The selected working directory is a subdirectory of the input BIDS folder. "
"Please modify the output path."
)

# Setup directories
config.execution.log_dir = config.execution.output_dir / "logs"
log_dir = output_dir / "logs"
# Check and create output and working directories
config.execution.log_dir.mkdir(exist_ok=True, parents=True)
log_dir.mkdir(exist_ok=True, parents=True)
work_dir.mkdir(exist_ok=True, parents=True)

# Run ingression if necessary
if config.workflow.input_type in ("hcpya", "ukb"):
import os.path as op
import shutil

from ingress2qsirecon.data import load_resource
from ingress2qsirecon.utils.functions import create_layout
from ingress2qsirecon.utils.workflows import create_ingress2qsirecon_wf

# Fake BIDS directory to be created
config.execution.bids_dir = work_dir / "bids"

# Make fake BIDS files
bids_scaffold = str(load_resource("bids_scaffold"))
if not op.exists(op.join(config.execution.bids_dir, "dataset_description.json")):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

could you switch this to use pathlib instead of op? I think we're going to try to consistently use it to be in line with nipreps

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Think I changed this, untested so far:

if config.workflow.input_type in ("hcpya", "ukb"):
        import shutil

        from ingress2qsirecon.data import load_resource
        from ingress2qsirecon.utils.functions import create_layout
        from ingress2qsirecon.utils.workflows import create_ingress2qsirecon_wf

        # Fake BIDS directory to be created
        config.execution.bids_dir = work_dir / "bids"

        # Make fake BIDS files
        bids_scaffold = load_resource("bids_scaffold")
        if not (config.execution.bids_dir / "dataset_description.json").exists():
            shutil.copytree(
                bids_scaffold,
                config.execution.bids_dir,
                dirs_exist_ok=True,
            )

shutil.copytree(
bids_scaffold,
config.execution.bids_dir,
dirs_exist_ok=True,
)

if config.execution.participant_label is None:
participants_ingression = []
else:
participants_ingression = list(config.execution.participant_label)
layouts = create_layout(
config.execution.input_dir,
config.execution.bids_dir,
config.workflow.input_type,
participants_ingression,
)

# Create the ingression workflow
wf = create_ingress2qsirecon_wf(
layouts,
base_dir=work_dir,
)

# Configure the nipype workflow
wf.config["execution"]["crashdump_dir"] = str(log_dir)
wf.run()
else:
config.execution.bids_dir = config.execution.input_dir

# Update the config with an empty dict to trigger initialization of all config
# sections (we used `init=False` above).
# This must be done after cleaning the work directory, or we could delete an
# open SQLite database
config.from_dict({})
config.execution.log_dir = log_dir

# Force initialization of the BIDSLayout
config.execution.init()
all_subjects = config.execution.layout.get_subjects()
Expand Down
3 changes: 3 additions & 0 deletions qsirecon/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -383,6 +383,9 @@ class execution(_Config):

bids_dir = None
"""An existing path to the dataset, which must be BIDS-compliant."""
input_dir = None
"""An existing path to the input data, which may not be BIDS-compliant
(in which case a BIDS-compliant version will be created and stored as bids_dir)."""
derivatives = {}
"""Path(s) to search for pre-computed derivatives"""
bids_database_dir = None
Expand Down
33 changes: 0 additions & 33 deletions qsirecon/interfaces/anatomical.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,6 @@

import os.path as op
from glob import glob
from pathlib import Path

import nibabel as nb
import numpy as np
Expand All @@ -26,9 +25,6 @@
)
from nipype.utils.filemanip import fname_presuffix

from ..utils.ingress import ukb_dirname_to_bids
from .images import to_lps

LOGGER = logging.getLogger("nipype.interface")


Expand Down Expand Up @@ -151,35 +147,6 @@ def _get_if_exists(self, name, pattern, excludes=None):
self._results[name] = files[0]


class UKBAnatomicalIngressInputSpec(QSIPrepAnatomicalIngressInputSpec):
recon_input_dir = traits.Directory(
exists=True, mandatory=True, help="directory containing a single subject's results"
)


class UKBAnatomicalIngress(QSIPrepAnatomicalIngress):
input_spec = UKBAnatomicalIngressInputSpec

def _run_interface(self, runtime):
# Load the Bias-corrected brain and brain mask
input_path = Path(self.inputs.recon_input_dir)
bids_name = ukb_dirname_to_bids(self.inputs.recon_input_dir)

ukb_brain = input_path / "T1" / "T1_unbiased_brain.nii.gz"
ukb_brain_mask = input_path / "T1" / "T1_brain_mask.nii.gz"

conformed_t1w_file = str(Path(runtime.cwd) / (bids_name + "_desc-preproc_T1w.nii.gz"))
conformed_mask_file = str(Path(runtime.cwd) / (bids_name + "_desc-brain_mask.nii.gz"))

to_lps(nb.load(ukb_brain)).to_filename(conformed_t1w_file)
to_lps(nb.load(ukb_brain_mask)).to_filename(conformed_mask_file)

self._results["t1_preproc"] = conformed_t1w_file
self._results["t1_brain_mask"] = conformed_mask_file

return runtime


"""

The spherical harmonic coefficients are stored as follows. First, since the
Expand Down
64 changes: 0 additions & 64 deletions qsirecon/interfaces/ingress.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,8 @@
"""

import os.path as op
import shutil
from glob import glob
from pathlib import Path

import nibabel as nb
from nipype import logging
from nipype.interfaces.base import (
BaseInterfaceInputSpec,
Expand All @@ -36,9 +33,6 @@
from nipype.utils.filemanip import split_filename

from .bids import get_bids_params
from .dsi_studio import btable_from_bvals_bvecs
from .images import ConformDwi, to_lps
from .mrtrix import _convert_fsl_to_mrtrix

LOGGER = logging.getLogger("nipype.interface")

Expand Down Expand Up @@ -119,61 +113,3 @@ def _get_qc_filename(self, out_root, params, desc, suffix):
used_keys = ["subject_id", "session_id", "acq_id", "dir_id", "run_id"]
fname = "_".join([params[key] for key in used_keys if params[key]])
return out_root + "/" + fname + "_desc-%s_dwi.%s" % (desc, suffix)


class _UKBioBankDWIIngressInputSpec(QSIPrepDWIIngressInputSpec):
dwi_file = File(exists=False, help="The name of what a BIDS dwi file may have been")
data_dir = traits.Directory(
exists=True, help="The UKB data directory for a subject. Must contain DTI/ and T1/"
)


class UKBioBankDWIIngress(SimpleInterface):
input_spec = _UKBioBankDWIIngressInputSpec
output_spec = QSIPrepDWIIngressOutputSpec

def _run_interface(self, runtime):
runpath = Path(runtime.cwd)

# The UKB input files
in_dir = Path(self.inputs.data_dir)
dwi_dir = in_dir / "DTI" / "dMRI" / "dMRI"
ukb_bval_file = dwi_dir / "bvals"
ukb_bvec_file = dwi_dir / "bvecs" # These are the same as eddy rotated
ukb_dwi_file = dwi_dir / "data_ud.nii.gz"
ukb_dwiref_file = dwi_dir / "dti_FA.nii.gz"

# The bids_name is what the images will be renamed to
bids_name = Path(self.inputs.dwi_file).name.replace(".nii.gz", "")
dwi_file = str(runpath / (bids_name + ".nii.gz"))
bval_file = str(runpath / (bids_name + ".bval"))
bvec_file = str(runpath / (bids_name + ".bvec"))
b_file = str(runpath / (bids_name + ".b"))
btable_file = str(runpath / (bids_name + "btable.txt"))
dwiref_file = str(runpath / (bids_name.replace("_dwi", "_dwiref") + ".nii.gz"))

dwi_conform = ConformDwi(
dwi_file=str(ukb_dwi_file), bval_file=str(ukb_bval_file), bvec_file=str(ukb_bvec_file)
)

result = dwi_conform.run()
Path(result.outputs.dwi_file).rename(dwi_file)
Path(result.outputs.bvec_file).rename(bvec_file)
shutil.copyfile(result.outputs.bval_file, bval_file)
# Reorient the dwi file to LPS+
self._results["dwi_file"] = dwi_file
self._results["bvec_file"] = bvec_file
self._results["bval_file"] = bval_file

# Create a btable_txt file for DSI Studio
btable_from_bvals_bvecs(bval_file, bvec_file, btable_file)
self._results["btable_file"] = btable_file

# Create a mrtrix .b file
_convert_fsl_to_mrtrix(bval_file, bvec_file, b_file)
self._results["b_file"] = b_file

# Create a dwi ref file
to_lps(nb.load(ukb_dwiref_file)).to_filename(dwiref_file)
self._results["dwi_ref"] = dwiref_file
return runtime
Loading