Source code for thesis.workflows.mrtrix3.nodes.tractography

"""tckgen, tcksift2, tckmap, and waytotal writer nodes."""

from pathlib import Path

from nipype import Node
from nipype.interfaces.mrtrix3 import ComputeTDI
from nipype.interfaces.utility import Function

from ..config.params import MRtrix3Params
from ..operations import (
    adapt_tckgen_inputs_task,
    make_per_target_maps_task,
    rename_density_map_task,
    run_tckgen_task,
    run_tcksift2_task,
    write_mrtrix3_params_task,
    write_waytotal_task,
)


[docs] def prepare_tckgen_node( params: MRtrix3Params, out_dir: Path, name: str = "tckgen", ) -> Node: """Configure ``tckgen`` as a Function node wrapping the binary directly. Nipype's ``Tractography`` interface declares ``roi_incl`` / ``roi_excl`` as single-value traits, which cannot represent the multiple ``-include`` and ``-exclude`` flags MRtrix3 supports. This builder uses a Function node that constructs the command line itself so any number of include and exclude masks can be wired through. ACT-specific flags (``-backtrack``, ``-crop_at_gmwmi``) are set from the ``params`` dict; the 5TT input is always passed as ``act_file`` and the workflow controls the wiring. Args: params: MRtrix3 parameter dictionary. out_dir: Output directory; ``tracks.tck`` is written here. name: Nipype node name. Returns: Configured Nipype Function Node with output field ``out_file``. """ out_dir.mkdir(parents=True, exist_ok=True) node = Node( Function( input_names=[ "fod_file", "act_file", "seed_image", "seed_gmwmi", "include_masks", "exclude_masks", "mask_stop", "output_dir", "algorithm", "select", "seeds", "min_length", "max_length", "backtrack", "crop_at_gmwmi", "cutoff", ], output_names=["out_file"], function=run_tckgen_task, ), name=name, ) node.inputs.output_dir = str(out_dir) # Seed/filter inputs default to empty so an unconnected input (whole-brain # gmwmi seeding wires only seed_gmwmi; ROI seeding wires only seed_image) is # passed deterministically rather than as traits.Undefined. The workflow's # connections override whichever apply. node.inputs.seed_image = "" node.inputs.seed_gmwmi = "" node.inputs.include_masks = [] node.inputs.exclude_masks = [] node.inputs.mask_stop = "" node.inputs.algorithm = params["tckgen_algorithm"] node.inputs.select = int(params["tckgen_select"]) node.inputs.seeds = int(params["tckgen_seeds"]) if params["tckgen_seeds"] else 0 node.inputs.min_length = float(params["tckgen_minlength"]) node.inputs.max_length = float(params["tckgen_maxlength"]) node.inputs.backtrack = bool(params["tckgen_backtrack"]) node.inputs.crop_at_gmwmi = bool(params["tckgen_crop_at_gmwmi"]) node.inputs.cutoff = params.get("tckgen_cutoff") return node
[docs] def prepare_tcksift2_node( out_dir: Path, name: str = "tcksift2", ) -> Node: """Configure ``tcksift2`` (per-streamline weighting + mu coefficient). No Nipype interface exists for tcksift2, so this is a Function node wrapping a subprocess call. The streamline file, FOD, and 5TT inputs are wired by the workflow. Args: out_dir: Output directory; ``sift2_weights.txt`` and ``sift2_mu.txt`` are written here. name: Nipype node name. Returns: Configured Nipype Node with outputs ``weights_file`` and ``mu_file``. """ out_dir.mkdir(parents=True, exist_ok=True) node = Node( Function( input_names=["tracks_file", "fod_file", "fivett_file", "output_dir"], output_names=["weights_file", "mu_file"], function=run_tcksift2_task, ), name=name, ) node.inputs.output_dir = str(out_dir) return node
[docs] def prepare_tckmap_node( template_image: Path, out_dir: Path, name: str = "tckmap", ) -> Node: """Configure ``tckmap`` (track-density imaging). SIFT2 weighting is controlled entirely by the workflow, which decides whether to wire the SIFT2 weights file into ``tck_weights``. Args: template_image: Reference image whose grid the density map adopts (typically the WM FOD or another DWI-grid image). out_dir: Output directory; the raw ComputeTDI output lands here and is later renamed to ``fdt_paths.nii.gz``. name: Nipype node name. Returns: Configured Nipype Node. """ out_dir.mkdir(parents=True, exist_ok=True) node = Node(ComputeTDI(), name=name) # ``ComputeTDI.reference`` carries an ``exists=True`` trait; skip the # static set when the template doesn't exist at build time so a parent # meta-workflow (e.g. full_pipeline with the mrtrix3 backend) can wire it at runtime. if Path(template_image).exists(): node.inputs.reference = str(template_image) node.inputs.out_file = str(out_dir / "tdi_raw.nii.gz") node.inputs.args = "-force" return node
[docs] def prepare_density_map_renamer( out_dir: Path, name: str = "fdt_paths_writer", ) -> Node: """Copy the tckmap output to ``fdt_paths.nii.gz`` for downstream tools.""" node = Node( Function( input_names=["in_file", "output_dir"], output_names=["out_file"], function=rename_density_map_task, ), name=name, ) node.inputs.output_dir = str(out_dir) return node
[docs] def prepare_waytotal_writer( out_dir: Path, name: str = "waytotal_writer", ) -> Node: """Write a single-line ``waytotal`` file for atlas / tract_similarity. Declares an optional ``weights_file`` input so callers can wire the SIFT2 weights file when present; the underlying task sums those weights instead of relying on ``tckinfo -count``. """ node = Node( Function( input_names=["tracks_file", "output_dir", "weights_file"], output_names=["waytotal_file"], function=write_waytotal_task, ), name=name, ) node.inputs.output_dir = str(out_dir) node.inputs.weights_file = None return node
[docs] def prepare_per_target_maps_node( out_dir: Path, name: str = "per_target_maps", ) -> Node: """Configure the per-target ``seeds_to_<target>.nii.gz`` writer. Wraps ``make_per_target_maps_task`` (tckedit + tckmap) so MRtrix3 matches the ProbTrackX2 per-target filename layout that ``qc.checks.collect_connectivity_map_stats`` already consumes. Args: out_dir: Per-hemisphere tractography output directory. name: Nipype node name. Returns: Configured Function node with output ``seeds_files`` (list of absolute paths). """ out_dir.mkdir(parents=True, exist_ok=True) node = Node( Function( input_names=[ "tracks_file", "reference_image", "output_dir", "target_mask", "target_name", "weights_file", ], output_names=["seeds_files"], function=make_per_target_maps_task, ), name=name, ) node.inputs.output_dir = str(out_dir) node.inputs.target_name = "target_dwi" node.inputs.weights_file = None return node
[docs] def prepare_mrtrix3_params_writer( out_dir: Path, select: int, name: str = "mrtrix3_params_writer", ) -> Node: """Write ``tractography_params.json`` (backend, select, mu) for stats. Args: out_dir: Per-hemisphere tractography output directory. select: The ``-select`` count passed to ``tckgen``. name: Nipype node name. Returns: Configured Function node with output ``params_file``. """ out_dir.mkdir(parents=True, exist_ok=True) node = Node( Function( input_names=["output_dir", "select", "mu_file"], output_names=["params_file"], function=write_mrtrix3_params_task, ), name=name, ) node.inputs.output_dir = str(out_dir) node.inputs.select = int(select) node.inputs.mu_file = None return node
[docs] def prepare_tckgen_input_adapter(name: str = "tckgen_inputs") -> Node: """Create the adapter node that maps canonical ROI fields to tckgen inputs. Inputs (connected by the workflow): ``seed``, ``waypoints_file``, ``target_mask``, ``avoid_mask``, ``stop_mask``. Outputs: ``seed_image`` (str), ``include_masks`` (list[str]), ``exclude_masks`` (list[str]), ``mask_stop`` (str). """ return Node( Function( input_names=["seed", "waypoints_file", "target_mask", "avoid_mask", "stop_mask"], output_names=["seed_image", "include_masks", "exclude_masks", "mask_stop"], function=adapt_tckgen_inputs_task, ), name=name, )