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