Source code for thesis.core.nipype.interfaces.fsl

"""
Custom FSL Nipype interfaces.

Provides patched or extended versions of FSL Nipype interfaces to work around
upstream bugs or limitations, and adds GPU-accelerated variants where FSL
ships separate GPU binaries.

CUDA / GPU validation is performed **once at CLI startup** by
:func:`thesis.core.gpu.check_gpu`, not here.  By the time a workflow is
built, ``config.hardware.gpu_enabled`` already reflects whether a compatible
GPU setup was found.  This module's only responsibility is to map that flag
to the correct binary name.

Classes:
    ProbTrackX2:    CPU ProbTrackX2 with corrected stderr-based error detection.
    ProbTrackX2GPU: GPU-accelerated ProbTrackX2.

Functions:
    get_probtrackx2_interface: Return the best available ProbTrackX2 class.
"""

from typing import Optional, Type

from nipype.interfaces.base import traits
from nipype.interfaces.fsl import ProbTrackX2 as _UpstreamProbTrackX2

from thesis.core.gpu import present_gpu_binaries
from thesis.core.logging import get_logger

logger = get_logger(__name__)

__all__ = ["ProbTrackX2", "ProbTrackX2GPU", "get_probtrackx2_interface"]

# ---------------------------------------------------------------------------
# Binary name resolution (presence only — CUDA validation is the CLI's job)
# ---------------------------------------------------------------------------

# The candidate list and the presence-search live once in ``thesis.core.gpu``;
# this module reuses :func:`present_gpu_binaries` so the two never drift.


def _find_gpu_binary() -> Optional[str]:
    """
    Return the name of the first GPU probtrackx2 binary found on the system.

    Searches ``$FSLDIR/bin/`` first, then ``$PATH`` (via
    :func:`thesis.core.gpu.present_gpu_binaries`).  This is a **presence check
    only** — CUDA compatibility is validated separately by
    :func:`thesis.core.gpu.check_gpu` at CLI startup.

    Returns:
        Binary name string (first present candidate in preference order), or
        ``None`` if no GPU binary is installed.
    """
    present = present_gpu_binaries()
    return present[0] if present else None


# Resolved once at module-import time.
_GPU_BINARY: Optional[str] = _find_gpu_binary()


# ---------------------------------------------------------------------------
# CPU interface (stderr fix)
# ---------------------------------------------------------------------------


[docs] class ProbTrackX2(_UpstreamProbTrackX2): """ ProbTrackX2 with a corrected ``_run_interface`` stderr check. The upstream nipype implementation raises ``RuntimeError`` whenever the command writes *anything* to stderr — including harmless OS warnings:: bash: /path/to/libtinfo.so.6: no version information available These appear on some HPC clusters due to conda / system library mismatches and are entirely benign. The upstream check therefore causes spurious workflow failures even when ``probtrackx2`` exits with return code 0. This subclass re-raises only for genuine command failures (non-zero return code). All other behaviour — sample symlinking, ``targets.txt`` writing, output listing — is inherited unchanged from the upstream class. """ def _run_interface(self, runtime): try: return super()._run_interface(runtime) except RuntimeError: # Upstream raises on any stderr content regardless of return code. # Re-raise only when the command actually failed. if getattr(runtime, "returncode", None) != 0: raise return runtime
# --------------------------------------------------------------------------- # GPU interface # --------------------------------------------------------------------------- class ProbTrackX2GPUInputSpec(_UpstreamProbTrackX2.input_spec): """Input specification for ProbTrackX2GPU.""" use_gpu = traits.Bool( True, usedefault=True, desc=( "Marks this node as requiring a GPU slot. " "GPU serialization is handled at the scheduler level via " "``n_gpu_procs=1`` in ``plugin_args``." ), )
[docs] class ProbTrackX2GPU(ProbTrackX2): """ GPU-accelerated ProbTrackX2 (``probtrackx2_gpu11.0`` / ``probtrackx2_gpu``). Accepts the same inputs and produces the same outputs as the CPU :class:`ProbTrackX2`. The binary is selected at module-import time by :func:`_find_gpu_binary` (presence check only). CUDA compatibility is validated at CLI startup via :func:`thesis.core.gpu.check_gpu` — if a compatible GPU is not available, ``config.hardware.gpu_enabled`` is set to ``False`` before any workflow is built, so this class will never be instantiated on an incompatible node. GPU serialization is handled at the scheduler level via ``n_gpu_procs=1`` in ``plugin_args`` — no process-level lock is needed, so this class adds no ``_run_interface`` override and runs ``probtrackx2_gpu`` directly via the inherited :class:`ProbTrackX2` implementation. Use :func:`get_probtrackx2_interface` to select between CPU and GPU transparently based on the config flag. """ _cmd: str = _GPU_BINARY or "probtrackx2_gpu" input_spec = ProbTrackX2GPUInputSpec
# --------------------------------------------------------------------------- # Factory # ---------------------------------------------------------------------------
[docs] def get_probtrackx2_interface(use_gpu: bool = True) -> Type[ProbTrackX2]: """ Return the appropriate ``ProbTrackX2`` interface class. When ``use_gpu=True`` and a GPU binary was found at module-import time, returns :class:`ProbTrackX2GPU`. Otherwise returns CPU :class:`ProbTrackX2`. CUDA availability is **not** re-checked here — it was already validated (and ``use_gpu`` corrected if necessary) at CLI startup by :func:`thesis.core.gpu.check_gpu`. Args: use_gpu: Use the GPU variant when ``True`` and the binary is present. Returns: Uninstantiated class; pass directly to ``nipype.Node``. """ if use_gpu and _GPU_BINARY is not None: logger.debug("Using GPU probtrackx2 binary: {}", _GPU_BINARY) return ProbTrackX2GPU return ProbTrackX2