Skip to content

docker_builder

tit.pre.qsi.docker_builder

Docker command builder for QSI containers.

This module constructs Docker run commands for QSIPrep and QSIRecon, handling volume mounts, resource allocation, and pipeline arguments.

DockerBuildError

Bases: Exception

Raised when Docker command construction fails.

DockerPaths dataclass

DockerPaths(bids_dir: str = '/data', output_dir: str = '/out', work_dir: str = '/work', license_file: str = '/opt/freesurfer/license.txt')

Container-side mount points for QSI containers.

QSI containers expect BIDS data at /data and write outputs to /out.

Attributes

bids_dir : str Container path for BIDS input data. output_dir : str Container path for pipeline outputs. work_dir : str Container path for intermediate working files. license_file : str Container path for the FreeSurfer license.

DockerCommandBuilder

DockerCommandBuilder(project_dir: str, paths: DockerPaths | None = None)

Build Docker run commands for QSIPrep and QSIRecon containers.

Handles volume mounts (host ↔ container), resource limits (CPU / memory), FreeSurfer license staging, and custom pipeline YAML staging for Docker-outside-of-Docker (DooD) execution.

Parameters

project_dir : str Path to the BIDS project directory inside the current container (used to resolve relative paths). paths : DockerPaths or None, optional Container path configuration. Uses DockerPaths() defaults when None.

Attributes

project_dir : str Container-side BIDS project directory. paths : DockerPaths Resolved container path configuration.

Raises

DockerBuildError If Docker is not available or required paths cannot be resolved.

See Also

tit.pre.qsi.config : QSIPrepConfig and QSIReconConfig dataclasses.

Source code in tit/pre/qsi/docker_builder.py
def __init__(
    self,
    project_dir: str,
    paths: DockerPaths | None = None,
) -> None:
    self.project_dir = project_dir
    self.paths = paths or DockerPaths()

    self._host_project_dir = get_host_project_dir()
    self._host_license_path = self._stage_fs_license()

build_qsiprep_cmd

build_qsiprep_cmd(config: QSIPrepConfig) -> list[str]

Build Docker command for QSIPrep.

Parameters

config : QSIPrepConfig QSIPrep configuration.

Returns

list[str] Complete Docker command as a list of arguments.

Source code in tit/pre/qsi/docker_builder.py
def build_qsiprep_cmd(self, config: QSIPrepConfig) -> list[str]:
    """
    Build Docker command for QSIPrep.

    Parameters
    ----------
    config : QSIPrepConfig
        QSIPrep configuration.

    Returns
    -------
    list[str]
        Complete Docker command as a list of arguments.
    """
    image = f"{const.QSI_QSIPREP_IMAGE}:{config.image_tag}"
    inherited_cpus, inherited_mem_gb = get_inherited_dood_resources()
    effective_cpus = (
        config.resources.cpus
        if config.resources.cpus is not None
        else inherited_cpus
    )
    effective_mem_gb = (
        config.resources.memory_gb
        if config.resources.memory_gb is not None
        else inherited_mem_gb
    )

    # Build base docker run command
    cmd = [
        "docker",
        "run",
        "--rm",
        "--platform",
        "linux/amd64",
        "--name",
        f"qsiprep_{config.subject_id}_{uuid.uuid4().hex[:8]}",
    ]

    # Resource limits
    cmd.extend(
        [
            "--cpus",
            str(effective_cpus),
            "--memory",
            format_memory_limit(effective_mem_gb),
        ]
    )

    # Environment variables
    cmd.extend(["-e", f"OMP_NUM_THREADS={config.resources.omp_threads}"])
    if self._host_license_path:
        cmd.extend(["-e", f"FS_LICENSE={self.paths.license_file}"])

    # Volume mounts - mount host project directory
    # BIDS data is at project root
    cmd.extend(["-v", f"{self._host_project_dir}:{self.paths.bids_dir}:ro"])

    qsiprep_output = str(Path(self._host_project_dir) / "derivatives" / "qsiprep")
    cmd.extend(["-v", f"{qsiprep_output}:{self.paths.output_dir}"])

    work_dir = str(Path(self._host_project_dir) / "derivatives" / ".qsiprep_work")
    cmd.extend(["-v", f"{work_dir}:{self.paths.work_dir}"])

    if self._host_license_path:
        cmd.extend(
            ["-v", f"{self._host_license_path}:{self.paths.license_file}:ro"]
        )

    cmd.append(image)

    # QSIPrep arguments
    cmd.extend(
        [
            self.paths.bids_dir,
            self.paths.output_dir,
            "participant",
        ]
    )

    cmd.extend(["--participant-label", config.subject_id])
    cmd.extend(["--output-resolution", str(config.output_resolution)])
    cmd.extend(["-w", self.paths.work_dir])
    cmd.extend(["--nthreads", str(effective_cpus)])
    cmd.extend(["--omp-nthreads", str(config.resources.omp_threads)])
    cmd.extend(["--mem-mb", str(effective_mem_gb * 1024)])

    if self._host_license_path:
        cmd.extend(["--fs-license-file", self.paths.license_file])

    if config.skip_bids_validation:
        cmd.append("--skip-bids-validation")

    cmd.extend(["--denoise-method", config.denoise_method])
    cmd.extend(["--unringing-method", config.unringing_method])

    if config.distortion_group_merge != "none":
        merge = (
            "concat"
            if config.distortion_group_merge == "concatenate"
            else config.distortion_group_merge
        )
        cmd.extend(["--distortion-group-merge", merge])

    return cmd

build_qsirecon_cmd

build_qsirecon_cmd(config: QSIReconConfig, recon_spec: str) -> list[str]

Build Docker command for QSIRecon with a specific recon spec.

Parameters

config : QSIReconConfig QSIRecon configuration. recon_spec : str The reconstruction specification to use.

Returns

list[str] Complete Docker command as a list of arguments.

Source code in tit/pre/qsi/docker_builder.py
def build_qsirecon_cmd(self, config: QSIReconConfig, recon_spec: str) -> list[str]:
    """
    Build Docker command for QSIRecon with a specific recon spec.

    Parameters
    ----------
    config : QSIReconConfig
        QSIRecon configuration.
    recon_spec : str
        The reconstruction specification to use.

    Returns
    -------
    list[str]
        Complete Docker command as a list of arguments.
    """
    image = f"{const.QSI_QSIRECON_IMAGE}:{config.image_tag}"
    inherited_cpus, inherited_mem_gb = get_inherited_dood_resources()
    effective_cpus = (
        config.resources.cpus
        if config.resources.cpus is not None
        else inherited_cpus
    )
    effective_mem_gb = (
        config.resources.memory_gb
        if config.resources.memory_gb is not None
        else inherited_mem_gb
    )

    cmd = [
        "docker",
        "run",
        "--rm",
        "--platform",
        "linux/amd64",
        "--name",
        f"qsirecon_{config.subject_id}_{recon_spec.replace('-', '_')}_{uuid.uuid4().hex[:8]}",
    ]

    if config.use_gpu:
        cmd.extend(["--gpus", "all"])

    cmd.extend(
        [
            "--cpus",
            str(effective_cpus),
            "--memory",
            format_memory_limit(effective_mem_gb),
        ]
    )

    cmd.extend(["-e", f"OMP_NUM_THREADS={config.resources.omp_threads}"])
    if self._host_license_path:
        cmd.extend(["-e", f"FS_LICENSE={self.paths.license_file}"])

    qsiprep_output = str(Path(self._host_project_dir) / "derivatives" / "qsiprep")
    cmd.extend(["-v", f"{qsiprep_output}:{self.paths.bids_dir}:ro"])

    qsirecon_output = str(Path(self._host_project_dir) / "derivatives" / "qsirecon")
    cmd.extend(["-v", f"{qsirecon_output}:{self.paths.output_dir}"])

    work_dir = str(Path(self._host_project_dir) / "derivatives" / ".qsirecon_work")
    cmd.extend(["-v", f"{work_dir}:{self.paths.work_dir}"])

    if self._host_license_path:
        cmd.extend(
            ["-v", f"{self._host_license_path}:{self.paths.license_file}:ro"]
        )

    # Determine recon spec and stage custom YAML before appending image,
    # so we can add the file mount with the other -v flags.
    container_spec = recon_spec
    if not config.atlases and recon_spec in _CUSTOM_PIPELINE_MAP:
        container_spec, host_yaml = self._stage_custom_pipeline(
            _CUSTOM_PIPELINE_MAP[recon_spec]
        )
        cmd.extend(["-v", f"{host_yaml}:{container_spec}:ro"])

    cmd.append(image)

    cmd.extend(
        [
            self.paths.bids_dir,
            self.paths.output_dir,
            "participant",
        ]
    )

    cmd.extend(["--participant-label", config.subject_id])
    cmd.extend(["--recon-spec", container_spec])
    cmd.extend(["--input-type", "qsiprep"])
    cmd.extend(["-w", self.paths.work_dir])
    cmd.extend(["--nthreads", str(effective_cpus)])
    cmd.extend(["--omp-nthreads", str(config.resources.omp_threads)])
    cmd.extend(["--mem-mb", str(effective_mem_gb * 1024)])

    if self._host_license_path:
        cmd.extend(["--fs-license-file", self.paths.license_file])

    if config.atlases:
        cmd.append("--atlases")
        cmd.extend(config.atlases)

    if config.skip_odf_reports:
        cmd.append("--skip-odf-reports")

    return cmd

get_output_dir

get_output_dir(pipeline: str) -> Path

Get the output directory path for a pipeline.

Parameters

pipeline : str Pipeline name ('qsiprep' or 'qsirecon').

Returns

Path Path to the output directory.

Source code in tit/pre/qsi/docker_builder.py
def get_output_dir(self, pipeline: str) -> Path:
    """
    Get the output directory path for a pipeline.

    Parameters
    ----------
    pipeline : str
        Pipeline name ('qsiprep' or 'qsirecon').

    Returns
    -------
    Path
        Path to the output directory.
    """
    return Path(self._host_project_dir) / "derivatives" / pipeline