Source code for thesis.workflows.atlas_to_patient.workflow

"""Atlas-to-patient-space transform workflow.

Transforms cohort-level atlas maps (mean, std, probability, etc.) into
individual patient space using the template-to-patient transforms defined
under ``transforms.template_to_patient``.

The images to transform are specified explicitly via ``transforms.jobs`` in
configuration.  The workflow delegates the actual transform execution to the
generic :mod:`thesis.workflows.transforms.workflow` module.

Register as ``thesis run -w atlas_to_patient -p <patient_id> -c <config>``.
"""

from __future__ import annotations

import shutil
from typing import Callable, List, cast

import nipype.pipeline.engine as pe

import thesis.workflows.transforms.workflow  # noqa: F401 — populate registry
from thesis.core.config import PipelineConfig
from thesis.core.context import ProcessingContext
from thesis.core.decorators import verify, workflow
from thesis.core.logging import get_logger
from thesis.core.registry import WORKFLOW_REGISTRY
from thesis.workflows.transforms.workflow import (
    verify_requirements as _verify_transform_requirements,
)

_WorkflowFactory = Callable[[PipelineConfig, ProcessingContext], pe.Workflow]
_build_transform_workflow = cast(_WorkflowFactory, WORKFLOW_REGISTRY.get("transform").factory)

logger = get_logger(__name__)

__all__ = ["build_workflow", "verify_requirements"]


[docs] def verify_requirements(config: PipelineConfig, context: ProcessingContext) -> List[str]: """Preflight checks: antsApplyTransforms on PATH and template_to_patient transforms configured (plus everything from the base transform verifier).""" errors: List[str] = [] if shutil.which("antsApplyTransforms") is None: errors.append("ANTs 'antsApplyTransforms' not found on PATH.") if not config.transforms.jobs: errors.append( "No transform jobs defined. Add entries under 'transforms.jobs' " "listing the atlas maps to transform." ) return errors tp_jobs = [j for j in config.transforms.jobs if j.direction == "template_to_patient"] if tp_jobs and not config.transforms.template_to_patient: errors.append( "One or more jobs use direction='template_to_patient' but " "'transforms.template_to_patient' is not configured." ) if tp_jobs and not config.transforms.reference_image: errors.append( "One or more jobs use direction='template_to_patient' but " "'transforms.reference_image' is not configured." ) seen = set(errors) for e in _verify_transform_requirements(config, context): if e not in seen and "antsApplyTransforms" not in e: errors.append(e) seen.add(e) return errors
# End-of-module decoration: verify_requirements is defined after build_workflow.
[docs] @workflow( name="atlas_to_patient", description=( "Transform cohort atlas maps into patient space using " "pre-computed template-to-patient ANTs transforms. " "Images and output locations are defined under 'transforms.jobs'." ), ) @verify(verify_requirements) def build_workflow(*, config: PipelineConfig, context: ProcessingContext) -> pe.Workflow: """Build the atlas-to-patient-space transform workflow. Delegates to the generic transform workflow after validating that the required transforms are configured. Raises ``ValueError`` when no jobs are configured. """ if not config.transforms.jobs: raise ValueError( "No transform jobs defined for the atlas_to_patient workflow. " "Add entries under 'transforms.jobs' listing the atlas maps to transform." ) wf = _build_transform_workflow(config, context) wf.name = f"atlas_to_patient_{context.patient_id}" logger.info( "Built atlas_to_patient workflow for {} | {} job(s)", context.patient_id, len(config.transforms.jobs), ) return wf