Source code for thesis.workflows.tract_synthseg.validation

"""Validation sub-workflow for tract_synthseg.

Wraps ROI mask validation as a two-node Nipype workflow:
  roi_collector → validate_rois

Only used when ``config.validation.check_rois`` is True. Backend-agnostic:
both the ProbTrackX2 and MRtrix3 tractography sub-workflows re-publish their
final ROI sources under the same outputnode contract, so this validation
workflow is shared.
"""

from nipype import Node, Workflow
from nipype.interfaces.utility import Function

from thesis.core.config import PipelineConfig
from thesis.core.context import ProcessingContext
from thesis.core.logging import get_logger
from thesis.workflows.hcp.operations.validation import validate_warped_rois_task

logger = get_logger(__name__)


def _collect_roi_paths(seed: str, stop_mask: str, avoid_mask: str, target_mask: str) -> list[str]:
    """Collect individual ROI path strings into a flat list, skipping empty entries.

    Args:
        seed: Path to seed mask NIfTI, or empty string if absent.
        stop_mask: Path to stop mask NIfTI, or empty string if absent.
        avoid_mask: Path to avoid mask NIfTI, or empty string if absent.
        target_mask: Path to target mask NIfTI, or empty string if absent.

    Returns:
        List of non-empty path strings.
    """
    return [p for p in [seed, stop_mask, avoid_mask, target_mask] if p]


[docs] def build_validation_workflow( config: PipelineConfig, context: ProcessingContext, brain_mask_path: str, ) -> Workflow: """Build the ROI validation sub-workflow. Contains two nodes: * ``roi_collector`` — gathers individual ROI path fields into a ``List[str]``. * ``validate_rois`` — wraps :func:`validate_warped_rois_task`. External inputs (connected from the parent meta-workflow): * ``roi_collector.seed`` * ``roi_collector.stop_mask`` * ``roi_collector.avoid_mask`` * ``roi_collector.target_mask`` Args: config: PipelineConfig. Must have a ``validation`` section with ``min_voxels``. context: ProcessingContext with ``patient_id``. brain_mask_path: Absolute path to the subject-space brain mask used for centroid plausibility check inside ``validate_warped_rois_task``. Returns: Nipype Workflow named ``validation_{patient_id}``. """ pid = context.patient_id wf = Workflow(name=f"validation_{pid}") # --- roi_collector: gather individual paths into a list --- roi_collector = Node( Function( input_names=["seed", "stop_mask", "avoid_mask", "target_mask"], output_names=["roi_paths"], function=_collect_roi_paths, ), name="roi_collector", ) # Default all inputs to empty string so absent masks are silently skipped. roi_collector.inputs.seed = "" roi_collector.inputs.stop_mask = "" roi_collector.inputs.avoid_mask = "" roi_collector.inputs.target_mask = "" # --- validate_rois: check voxel count, centroid, volume --- validate_rois = Node( Function( input_names=[ "roi_paths", "reference_image", "min_voxels", "singularity_threshold", "volume_ratio_min", "volume_ratio_max", ], output_names=["roi_paths"], function=validate_warped_rois_task, ), name="validate_rois", ) validate_rois.inputs.reference_image = brain_mask_path validate_rois.inputs.min_voxels = config.validation.min_voxels validate_rois.inputs.singularity_threshold = config.validation.singularity_threshold validate_rois.inputs.volume_ratio_min = config.validation.volume_ratio_min validate_rois.inputs.volume_ratio_max = config.validation.volume_ratio_max wf.connect(roi_collector, "roi_paths", validate_rois, "roi_paths") logger.info( "Built validation workflow | patient={} | brain_mask={} | min_voxels={}", pid, brain_mask_path, config.validation.min_voxels, ) return wf