Skip to content

qsi

tit.pre.qsi

QSI (QSIPrep / QSIRecon) integration for TI-Toolbox.

Provides Docker-out-of-Docker (DooD) integration for running QSIPrep and QSIRecon as sibling containers from within the SimNIBS container. The primary use case is preprocessing DWI data to extract DTI tensors for SimNIBS anisotropic conductivity simulations.

Public API

run_qsiprep Run the QSIPrep DWI preprocessing pipeline. run_qsirecon Run the QSIRecon DWI reconstruction pipeline. extract_dti_tensor Extract a DTI tensor and register it to the SimNIBS T1 grid. check_dti_tensor_exists Check whether an extracted DTI tensor already exists.

Configuration Classes

QSIPrepConfig Configuration dataclass for QSIPrep runs. QSIReconConfig Configuration dataclass for QSIRecon runs. ResourceConfig Resource allocation (CPU, memory, OMP threads) for QSI containers. ReconSpec Enum of available QSIRecon reconstruction specifications. QSIAtlas Enum of available atlases for connectivity analysis.

See Also

tit.pre : Parent preprocessing package. tit.pre.structural.run_pipeline : Full pipeline that can invoke QSI steps.

QSIPrepConfig dataclass

QSIPrepConfig(subject_id: str, output_resolution: float = QSI_DEFAULT_OUTPUT_RESOLUTION, resources: ResourceConfig = ResourceConfig(), image_tag: str = QSI_QSIPREP_IMAGE_TAG, skip_bids_validation: bool = True, denoise_method: str = 'dwidenoise', unringing_method: str = 'mrdegibbs', distortion_group_merge: str = 'none')

Configuration for a QSIPrep run.

Attributes

subject_id : str Subject identifier (without 'sub-' prefix). output_resolution : float Target output resolution in mm (default: 2.0). resources : ResourceConfig Resource allocation settings. image_tag : str Docker image tag for QSIPrep. skip_bids_validation : bool Whether to skip BIDS validation (useful for non-BIDS datasets). denoise_method : str Denoising method: 'dwidenoise', 'patch2self', or 'none'. unringing_method : str Unringing method: 'mrdegibbs', 'rpg', or 'none'. distortion_group_merge : str Method for merging distortion groups: 'concatenate', 'average', or 'none'. The legacy/internal value 'concatenate' is translated to QSIPrep's current CLI value 'concat' when building the Docker command.

QSIReconConfig dataclass

QSIReconConfig(subject_id: str, recon_specs: list[str] = (lambda: [QSI_DEFAULT_RECON_SPEC])(), atlases: list[str] | None = None, use_gpu: bool = False, resources: ResourceConfig = ResourceConfig(), image_tag: str = QSI_QSIRECON_IMAGE_TAG, skip_odf_reports: bool = True)

Configuration for a QSIRecon run.

Attributes

subject_id : str Subject identifier (without 'sub-' prefix). recon_specs : list[str] List of reconstruction specs to run. Defaults to ['dsi_studio_gqi']. atlases : list[str] | None List of atlases for connectivity analysis. None (default) = no connectivity. Set to e.g. ['4S156Parcels', 'AAL116'] if needed. use_gpu : bool Whether to enable GPU acceleration (requires NVIDIA Docker runtime). resources : ResourceConfig Resource allocation settings. image_tag : str Docker image tag for QSIRecon. skip_odf_reports : bool Whether to skip ODF report generation (saves time).

ReconSpec

Bases: StrEnum

Available QSIRecon reconstruction specifications.

Each spec defines a complete reconstruction pipeline with specific algorithms and output formats.

from_string classmethod

from_string(value: str) -> Self

Convert string to ReconSpec enum.

Source code in tit/pre/qsi/config.py
@classmethod
def from_string(cls, value: str) -> Self:
    """Convert string to ReconSpec enum."""
    for spec in cls:
        if spec.value == value:
            return spec
    raise ValueError(f"Unknown recon spec: {value}")

list_all classmethod

list_all() -> list[str]

Return list of all spec values.

Source code in tit/pre/qsi/config.py
@classmethod
def list_all(cls) -> list[str]:
    """Return list of all spec values."""
    return [spec.value for spec in cls]

QSIAtlas

Bases: StrEnum

Available atlases for QSIRecon connectivity analysis.

These atlases can be used for structural connectivity matrix generation. The 4S series combines Schaefer cortical parcels with 56 subcortical ROIs.

from_string classmethod

from_string(value: str) -> Self

Convert string to QSIAtlas enum.

Source code in tit/pre/qsi/config.py
@classmethod
def from_string(cls, value: str) -> Self:
    """Convert string to QSIAtlas enum."""
    for atlas in cls:
        if atlas.value == value:
            return atlas
    raise ValueError(f"Unknown atlas: {value}")

list_all classmethod

list_all() -> list[str]

Return list of all atlas values.

Source code in tit/pre/qsi/config.py
@classmethod
def list_all(cls) -> list[str]:
    """Return list of all atlas values."""
    return [atlas.value for atlas in cls]

ResourceConfig dataclass

ResourceConfig(cpus: int | None = None, memory_gb: int | None = None, omp_threads: int = QSI_DEFAULT_OMP_THREADS)

Resource allocation configuration for QSI containers.

Attributes

cpus : int Number of CPUs to allocate to the container. memory_gb : int Memory limit in gigabytes. omp_threads : int Number of OpenMP threads (affects ANTS, MRtrix, etc.).

run_qsiprep

run_qsiprep(project_dir: str, subject_id: str, *, logger: Logger, output_resolution: float = QSI_DEFAULT_OUTPUT_RESOLUTION, cpus: int | None = None, memory_gb: int | None = None, omp_threads: int = QSI_DEFAULT_OMP_THREADS, image_tag: str = QSI_QSIPREP_IMAGE_TAG, skip_bids_validation: bool = True, denoise_method: str = 'dwidenoise', unringing_method: str = 'mrdegibbs', runner: CommandRunner | None = None) -> None

Run QSIPrep preprocessing for a subject's DWI data.

This function spawns a QSIPrep Docker container as a sibling to the current SimNIBS container using Docker-out-of-Docker (DooD).

Parameters

project_dir : str Path to the BIDS project root directory. subject_id : str Subject identifier (without 'sub-' prefix). logger : logging.Logger Logger for status messages. output_resolution : float, optional Target output resolution in mm. Default: 2.0. cpus : int, optional Number of CPUs to allocate. Default: 8. memory_gb : int, optional Memory limit in GB. Default: 32. omp_threads : int, optional Number of OpenMP threads. Default: 1. image_tag : str, optional QSIPrep Docker image tag. Default from constants.QSI_QSIPREP_IMAGE_TAG. skip_bids_validation : bool, optional Skip BIDS validation. Default: True. denoise_method : str, optional Denoising method. Default: 'dwidenoise'. unringing_method : str, optional Unringing method. Default: 'mrdegibbs'. runner : CommandRunner | None, optional Command runner for subprocess execution.

Raises

PreprocessError If QSIPrep fails or prerequisites are not met.

Source code in tit/pre/qsi/qsiprep.py
def run_qsiprep(
    project_dir: str,
    subject_id: str,
    *,
    logger: logging.Logger,
    output_resolution: float = const.QSI_DEFAULT_OUTPUT_RESOLUTION,
    cpus: int | None = None,
    memory_gb: int | None = None,
    omp_threads: int = const.QSI_DEFAULT_OMP_THREADS,
    image_tag: str = const.QSI_QSIPREP_IMAGE_TAG,
    skip_bids_validation: bool = True,
    denoise_method: str = "dwidenoise",
    unringing_method: str = "mrdegibbs",
    runner: CommandRunner | None = None,
) -> None:
    """
    Run QSIPrep preprocessing for a subject's DWI data.

    This function spawns a QSIPrep Docker container as a sibling to the
    current SimNIBS container using Docker-out-of-Docker (DooD).

    Parameters
    ----------
    project_dir : str
        Path to the BIDS project root directory.
    subject_id : str
        Subject identifier (without 'sub-' prefix).
    logger : logging.Logger
        Logger for status messages.
    output_resolution : float, optional
        Target output resolution in mm. Default: 2.0.
    cpus : int, optional
        Number of CPUs to allocate. Default: 8.
    memory_gb : int, optional
        Memory limit in GB. Default: 32.
    omp_threads : int, optional
        Number of OpenMP threads. Default: 1.
    image_tag : str, optional
        QSIPrep Docker image tag. Default from ``constants.QSI_QSIPREP_IMAGE_TAG``.
    skip_bids_validation : bool, optional
        Skip BIDS validation. Default: True.
    denoise_method : str, optional
        Denoising method. Default: 'dwidenoise'.
    unringing_method : str, optional
        Unringing method. Default: 'mrdegibbs'.
    runner : CommandRunner | None, optional
        Command runner for subprocess execution.

    Raises
    ------
    PreprocessError
        If QSIPrep fails or prerequisites are not met.
    """
    from tit.telemetry import track_operation
    from tit import constants as _const

    with track_operation(_const.TELEMETRY_OP_PRE_QSIPREP):
        logger.info(f"Starting QSIPrep for subject {subject_id}")

        # Validate DWI data exists
        is_valid, error_msg = validate_bids_dwi(project_dir, subject_id, logger)
        if not is_valid:
            raise PreprocessError(f"DWI validation failed: {error_msg}")

        # Check for existing output
        output_dir = Path(project_dir) / "derivatives" / "qsiprep" / f"sub-{subject_id}"

        if output_dir.exists():
            existing_valid, _ = validate_qsiprep_output(project_dir, subject_id)
            if existing_valid:
                raise PreprocessError(
                    f"QSIPrep output already exists at {output_dir}. "
                    "Remove the directory manually before rerunning."
                )

        # Create output directories
        output_dir.parent.mkdir(parents=True, exist_ok=True)
        work_dir = Path(project_dir) / "derivatives" / ".qsiprep_work"
        work_dir.mkdir(parents=True, exist_ok=True)

        # Build configuration
        config = QSIPrepConfig(
            subject_id=subject_id,
            output_resolution=output_resolution,
            resources=ResourceConfig(
                cpus=cpus,
                memory_gb=memory_gb,
                omp_threads=omp_threads,
            ),
            image_tag=image_tag,
            skip_bids_validation=skip_bids_validation,
            denoise_method=denoise_method,
            unringing_method=unringing_method,
        )

        try:
            # Build Docker command
            builder = DockerCommandBuilder(project_dir)
            cmd = builder.build_qsiprep_cmd(config)
        except DockerBuildError as e:
            raise PreprocessError(f"Failed to build QSIPrep command: {e}")

        # Ensure image is available
        if not pull_image_if_needed(const.QSI_QSIPREP_IMAGE, image_tag, logger):
            raise PreprocessError(
                f"Failed to pull QSIPrep image: {const.QSI_QSIPREP_IMAGE}:{image_tag}"
            )

        # Log the command for debugging
        logger.debug(f"QSIPrep command: {' '.join(cmd)}")

        # Run the container
        if runner is None:
            runner = CommandRunner()

        logger.info(f"Running QSIPrep for subject {subject_id}...")
        returncode = runner.run(cmd, logger=logger)

        if returncode != 0:
            raise PreprocessError(f"QSIPrep failed with exit code {returncode}")

        # Validate output
        is_valid, error_msg = validate_qsiprep_output(project_dir, subject_id)
        if not is_valid:
            raise PreprocessError(f"QSIPrep output validation failed: {error_msg}")

    logger.info(f"QSIPrep completed successfully for subject {subject_id}")

run_qsirecon

run_qsirecon(project_dir: str, subject_id: str, *, logger: Logger, recon_specs: list[str] | None = None, atlases: list[str] | None = None, use_gpu: bool = False, cpus: int | None = None, memory_gb: int | None = None, omp_threads: int = QSI_DEFAULT_OMP_THREADS, image_tag: str = QSI_QSIRECON_IMAGE_TAG, skip_odf_reports: bool = True, runner: CommandRunner | None = None) -> None

Run QSIRecon reconstruction for a subject's preprocessed DWI data.

This function spawns QSIRecon Docker containers as siblings to the current SimNIBS container using Docker-out-of-Docker (DooD).

QSIRecon requires QSIPrep output as input. Multiple reconstruction specs can be run sequentially.

Parameters

project_dir : str Path to the BIDS project root directory. subject_id : str Subject identifier (without 'sub-' prefix). logger : logging.Logger Logger for status messages. recon_specs : list[str] | None, optional List of reconstruction specifications to run. Default: ['dsi_studio_gqi']. This default produces DTI tensors for SimNIBS anisotropic modeling. Other specs (mrtrix_, dipy_, amico_noddi, pyafq_*, etc.) remain available. atlases : list[str] | None, optional List of atlases for connectivity analysis. Default: None (no connectivity). Not needed for DTI extraction. Set to e.g. ['4S156Parcels', 'AAL116'] if connectivity matrices are desired. use_gpu : bool, optional Enable GPU acceleration. Default: False. cpus : int | None, optional Number of CPUs to allocate. None = inherit from current container. memory_gb : int | None, optional Memory limit in GB. None = inherit from current container. omp_threads : int, optional Number of OpenMP threads. Default: 1. image_tag : str, optional QSIRecon Docker image tag. Default from constants.QSI_QSIRECON_IMAGE_TAG. skip_odf_reports : bool, optional Skip ODF report generation. Default: True. runner : CommandRunner | None, optional Command runner for subprocess execution.

Raises

PreprocessError If QSIRecon fails or prerequisites are not met.

Source code in tit/pre/qsi/qsirecon.py
def run_qsirecon(
    project_dir: str,
    subject_id: str,
    *,
    logger: logging.Logger,
    recon_specs: list[str] | None = None,
    atlases: list[str] | None = None,
    use_gpu: bool = False,
    cpus: int | None = None,
    memory_gb: int | None = None,
    omp_threads: int = const.QSI_DEFAULT_OMP_THREADS,
    image_tag: str = const.QSI_QSIRECON_IMAGE_TAG,
    skip_odf_reports: bool = True,
    runner: CommandRunner | None = None,
) -> None:
    """
    Run QSIRecon reconstruction for a subject's preprocessed DWI data.

    This function spawns QSIRecon Docker containers as siblings to the
    current SimNIBS container using Docker-out-of-Docker (DooD).

    QSIRecon requires QSIPrep output as input. Multiple reconstruction
    specs can be run sequentially.

    Parameters
    ----------
    project_dir : str
        Path to the BIDS project root directory.
    subject_id : str
        Subject identifier (without 'sub-' prefix).
    logger : logging.Logger
        Logger for status messages.
    recon_specs : list[str] | None, optional
        List of reconstruction specifications to run. Default: ['dsi_studio_gqi'].
        This default produces DTI tensors for SimNIBS anisotropic modeling.
        Other specs (mrtrix_*, dipy_*, amico_noddi, pyafq_*, etc.) remain available.
    atlases : list[str] | None, optional
        List of atlases for connectivity analysis. Default: None (no connectivity).
        Not needed for DTI extraction. Set to e.g. ['4S156Parcels', 'AAL116']
        if connectivity matrices are desired.
    use_gpu : bool, optional
        Enable GPU acceleration. Default: False.
    cpus : int | None, optional
        Number of CPUs to allocate. None = inherit from current container.
    memory_gb : int | None, optional
        Memory limit in GB. None = inherit from current container.
    omp_threads : int, optional
        Number of OpenMP threads. Default: 1.
    image_tag : str, optional
        QSIRecon Docker image tag. Default from ``constants.QSI_QSIRECON_IMAGE_TAG``.
    skip_odf_reports : bool, optional
        Skip ODF report generation. Default: True.
    runner : CommandRunner | None, optional
        Command runner for subprocess execution.

    Raises
    ------
    PreprocessError
        If QSIRecon fails or prerequisites are not met.
    """
    # Default to dsi_studio_gqi for SimNIBS DTI extraction
    if recon_specs is None:
        recon_specs = [const.QSI_DEFAULT_RECON_SPEC]

    # Atlases are optional — not needed for DTI extraction
    # Pass through None/empty to skip connectivity workflows

    from tit.telemetry import track_operation
    from tit import constants as _const

    with track_operation(_const.TELEMETRY_OP_PRE_QSIRECON):
        logger.info(
            f"Starting QSIRecon for subject {subject_id} with specs: {recon_specs}, atlases: {atlases}"
        )

        # Validate QSIPrep output exists
        is_valid, error_msg = validate_qsiprep_output(project_dir, subject_id)
        if not is_valid:
            raise PreprocessError(
                f"QSIPrep output validation failed: {error_msg}. "
                "Run QSIPrep first before running QSIRecon."
            )

        # No mkdir here — Docker's `-v` creates host directories automatically.
        # Creating them from SimNIBS fails on Docker Desktop due to phantom
        # bind-mount entries left by previous sibling containers.
        output_base = Path(project_dir) / "derivatives" / "qsirecon"

        # Build configuration
        config = QSIReconConfig(
            subject_id=subject_id,
            recon_specs=recon_specs,
            atlases=atlases,
            use_gpu=use_gpu,
            resources=ResourceConfig(
                cpus=cpus,
                memory_gb=memory_gb,
                omp_threads=omp_threads,
            ),
            image_tag=image_tag,
            skip_odf_reports=skip_odf_reports,
        )

        try:
            # Build Docker command builder
            builder = DockerCommandBuilder(project_dir)
        except DockerBuildError as e:
            raise PreprocessError(f"Failed to initialize Docker: {e}")

        # Ensure image is available
        if not pull_image_if_needed(const.QSI_QSIRECON_IMAGE, image_tag, logger):
            raise PreprocessError(
                f"Failed to pull QSIRecon image: {const.QSI_QSIRECON_IMAGE}:{image_tag}"
            )

        # Create runner if not provided
        if runner is None:
            runner = CommandRunner()

        # Check for existing output before starting any specs
        subject_output_dir = output_base / f"sub-{subject_id}"
        if subject_output_dir.exists():
            raise PreprocessError(
                f"QSIRecon output already exists at {subject_output_dir}. "
                "Remove the directory manually before rerunning."
            )

        # Run each recon spec
        for spec in recon_specs:
            logger.info(f"Running QSIRecon spec: {spec}")

            try:
                cmd = builder.build_qsirecon_cmd(config, spec)
            except DockerBuildError as e:
                raise PreprocessError(f"Failed to build QSIRecon command: {e}")

            # Log the command for debugging
            logger.debug(f"QSIRecon command: {' '.join(cmd)}")

            # Run the container
            logger.info(f"Running QSIRecon {spec} for subject {subject_id}...")
            returncode = runner.run(cmd, logger=logger)

            if returncode != 0:
                raise PreprocessError(f"QSIRecon {spec} failed with exit code {returncode}")

            logger.info(f"QSIRecon {spec} completed for subject {subject_id}")

        logger.info(f"QSIRecon completed successfully for subject {subject_id}")

extract_dti_tensor

extract_dti_tensor(project_dir: str, subject_id: str, *, logger: Logger, skip_registration: bool = False) -> Path

Extract and register a DTI tensor from QSIRecon DSI Studio output.

Loads the six tensor components produced by DSI Studio GQI, validates them, registers the tensor to the SimNIBS T1 grid (with FSL-convention pre-compensation), saves DTI_coregT1_tensor.nii.gz into the m2m directory, and generates a QC report.

Parameters

project_dir : str BIDS project root directory. subject_id : str Subject identifier (e.g. '070'). logger : logging.Logger Logger instance for progress and diagnostic messages. skip_registration : bool, optional When True, copy the tensor as-is without resampling or reorientation. Default is False.

Returns

pathlib.Path Path to the saved DTI_coregT1_tensor.nii.gz.

Raises

tit.pre.utils.PreprocessError If required inputs are missing, the tensor already exists, or the tensor data is invalid.

Source code in tit/pre/qsi/dti_extractor.py
def extract_dti_tensor(
    project_dir: str,
    subject_id: str,
    *,
    logger: logging.Logger,
    skip_registration: bool = False,
) -> Path:
    """Extract and register a DTI tensor from QSIRecon DSI Studio output.

    Loads the six tensor components produced by DSI Studio GQI,
    validates them, registers the tensor to the SimNIBS T1 grid
    (with FSL-convention pre-compensation), saves
    ``DTI_coregT1_tensor.nii.gz`` into the m2m directory, and
    generates a QC report.

    Parameters
    ----------
    project_dir : str
        BIDS project root directory.
    subject_id : str
        Subject identifier (e.g. ``'070'``).
    logger : logging.Logger
        Logger instance for progress and diagnostic messages.
    skip_registration : bool, optional
        When *True*, copy the tensor as-is without resampling or
        reorientation.  Default is *False*.

    Returns
    -------
    pathlib.Path
        Path to the saved ``DTI_coregT1_tensor.nii.gz``.

    Raises
    ------
    tit.pre.utils.PreprocessError
        If required inputs are missing, the tensor already exists, or
        the tensor data is invalid.
    """
    project = Path(project_dir)
    logger.info(f"Extracting DTI tensor for subject {subject_id}")

    pm = get_path_manager(project_dir)
    m2m_dir = Path(pm.m2m(subject_id))
    if not m2m_dir.is_dir():
        raise PreprocessError(f"m2m directory not found: {m2m_dir}. Run charm first.")

    output_path = m2m_dir / const.FILE_DTI_TENSOR
    if output_path.exists():
        raise PreprocessError(
            f"DTI tensor already exists at {output_path}. "
            "Remove the file before rerunning."
        )

    simnibs_t1 = m2m_dir / const.FILE_T1
    if not simnibs_t1.exists():
        raise PreprocessError(f"SimNIBS T1 not found: {simnibs_t1}. Run charm first.")

    dwi_dir = _dsistudio_dwi_dir(project, subject_id)
    if not dwi_dir.is_dir():
        raise PreprocessError(
            f"DSI Studio output not found: {dwi_dir}. "
            "Run QSIRecon with dsi_studio_gqi first."
        )

    # Load and validate
    tensor_data, affine = _load_tensor(dwi_dir, subject_id, logger)
    _validate_tensor(tensor_data, logger)

    # Save intermediate in ACPC space
    intermediate = m2m_dir / "DTI_ACPC_tensor.nii.gz"
    _save_nifti_gz(tensor_data, affine, intermediate, logger)
    logger.info(f"Intermediate tensor: {intermediate}")

    # Register to SimNIBS T1 space
    if skip_registration:
        shutil.copy2(intermediate, output_path)
        logger.info("Copied tensor as-is (skip_registration=True)")
    else:
        acpc_t1 = _qsiprep_t1(project, subject_id)
        _register_tensor(tensor_data, affine, simnibs_t1, acpc_t1, output_path, logger)

    logger.info(f"DTI tensor saved to: {output_path}")

    # QC report
    from tit.reporting.generators.dti_qc import create_dti_qc_report

    qc_path = create_dti_qc_report(
        project_dir=project_dir,
        subject_id=subject_id,
        tensor_file=str(output_path),
        t1_file=str(simnibs_t1),
    )
    logger.info(f"DTI QC report: {qc_path}")

    return output_path

check_dti_tensor_exists

check_dti_tensor_exists(project_dir: str, subject_id: str) -> bool

Check whether a registered DTI tensor already exists for subject_id.

Parameters

project_dir : str BIDS project root directory. subject_id : str Subject identifier.

Returns

bool True if DTI_coregT1_tensor.nii.gz is present in the m2m directory.

Source code in tit/pre/qsi/dti_extractor.py
def check_dti_tensor_exists(project_dir: str, subject_id: str) -> bool:
    """Check whether a registered DTI tensor already exists for *subject_id*.

    Parameters
    ----------
    project_dir : str
        BIDS project root directory.
    subject_id : str
        Subject identifier.

    Returns
    -------
    bool
        *True* if ``DTI_coregT1_tensor.nii.gz`` is present in the m2m
        directory.
    """
    pm = get_path_manager(project_dir)
    m2m_dir = pm.m2m(subject_id)
    if not os.path.isdir(m2m_dir):
        return False
    return (Path(m2m_dir) / const.FILE_DTI_TENSOR).exists()