Skip to content

templates

lacuna.assets.templates

Template asset management for Lacuna.

This module provides template registry and loading with TemplateFlow integration.

TemplateMetadata dataclass

Bases: SpatialAssetMetadata

Metadata for a reference brain template.

Attributes:

Name Type Description
name str

Template identifier (e.g., "MNI152NLin2009cAsym")

space str

Coordinate space (same as name for templates)

resolution float

Voxel resolution in mm

description str

Human-readable description

modality str

Image modality (e.g., "T1w", "T2w", "FLAIR")

source str

Source of template (always "templateflow")

Source code in src/lacuna/assets/templates/registry.py
@dataclass(frozen=True)
class TemplateMetadata(SpatialAssetMetadata):
    """Metadata for a reference brain template.

    Attributes
    ----------
    name : str
        Template identifier (e.g., "MNI152NLin2009cAsym")
    space : str
        Coordinate space (same as name for templates)
    resolution : float
        Voxel resolution in mm
    description : str
        Human-readable description
    modality : str
        Image modality (e.g., "T1w", "T2w", "FLAIR")
    source : str
        Source of template (always "templateflow")
    """

    modality: str = "T1w"
    source: str = "templateflow"

is_template_cached(name)

Check if template is already cached locally.

Parameters:

Name Type Description Default
name str

Template name from registry

required

Returns:

Type Description
bool

True if template is cached, False otherwise

Examples:

>>> from lacuna.assets.templates import is_template_cached
>>> is_template_cached("MNI152NLin2009cAsym_res-1")
True
Source code in src/lacuna/assets/templates/loader.py
def is_template_cached(name: str) -> bool:
    """Check if template is already cached locally.

    Parameters
    ----------
    name : str
        Template name from registry

    Returns
    -------
    bool
        True if template is cached, False otherwise

    Examples
    --------
    >>> from lacuna.assets.templates import is_template_cached
    >>> is_template_cached("MNI152NLin2009cAsym_res-1")
    True
    """
    try:
        template_path = load_template(name)
        return template_path.exists()
    except (FileNotFoundError, KeyError):
        return False

list_templates(space=None, resolution=None, modality=None)

List available templates from TemplateFlow.

Parameters:

Name Type Description Default
space str

Filter by coordinate space

None
resolution float

Filter by resolution in mm

None
modality str

Filter by modality (e.g., "T1w", "T2w")

None

Returns:

Type Description
list[TemplateMetadata]

Matching templates

Examples:

>>> from lacuna.assets.templates import list_templates
>>>
>>> # List all available templates
>>> templates = list_templates()
>>>
>>> # Filter by space and resolution
>>> mni_1mm = list_templates(space="MNI152NLin2009cAsym", resolution=1.0)
Source code in src/lacuna/assets/templates/registry.py
def list_templates(
    space: str | None = None,
    resolution: float | None = None,
    modality: str | None = None,
) -> list[TemplateMetadata]:
    """List available templates from TemplateFlow.

    Parameters
    ----------
    space : str, optional
        Filter by coordinate space
    resolution : float, optional
        Filter by resolution in mm
    modality : str, optional
        Filter by modality (e.g., "T1w", "T2w")

    Returns
    -------
    list[TemplateMetadata]
        Matching templates

    Examples
    --------
    >>> from lacuna.assets.templates import list_templates
    >>>
    >>> # List all available templates
    >>> templates = list_templates()
    >>>
    >>> # Filter by space and resolution
    >>> mni_1mm = list_templates(space="MNI152NLin2009cAsym", resolution=1.0)
    """
    return TEMPLATE_REGISTRY.list(space=space, resolution=resolution, modality=modality)

load_template(name)

Load a reference brain template by name.

Downloads from TemplateFlow on first use and caches locally.

Supports space equivalence: anatomically identical spaces like MNI152NLin2009[abc]Asym are automatically normalized to their canonical form (cAsym).

Parameters:

Name Type Description Default
name str

Template name from registry (e.g., "MNI152NLin2009cAsym_res-1")

required

Returns:

Type Description
Path

Path to template NIfTI file

Raises:

Type Description
KeyError

If template not found in registry

FileNotFoundError

If template download fails

Examples:

>>> from lacuna.assets.templates import load_template
>>>
>>> # Load MNI template
>>> template_path = load_template("MNI152NLin2009cAsym_res-1")
>>> import nibabel as nib
>>> template = nib.load(template_path)
>>> print(template.shape)
(193, 229, 193)
Source code in src/lacuna/assets/templates/loader.py
def load_template(name: str) -> Path:
    """Load a reference brain template by name.

    Downloads from TemplateFlow on first use and caches locally.

    Supports space equivalence: anatomically identical spaces like
    MNI152NLin2009[abc]Asym are automatically normalized to their
    canonical form (cAsym).

    Parameters
    ----------
    name : str
        Template name from registry (e.g., "MNI152NLin2009cAsym_res-1")

    Returns
    -------
    Path
        Path to template NIfTI file

    Raises
    ------
    KeyError
        If template not found in registry
    FileNotFoundError
        If template download fails

    Examples
    --------
    >>> from lacuna.assets.templates import load_template
    >>>
    >>> # Load MNI template
    >>> template_path = load_template("MNI152NLin2009cAsym_res-1")
    >>> import nibabel as nib
    >>> template = nib.load(template_path)
    >>> print(template.shape)
    (193, 229, 193)
    """
    # Canonicalize space variant in template name before registry lookup
    # e.g., "MNI152NLin2009bAsym_res-2" -> "MNI152NLin2009cAsym_res-2"
    if "_res-" in name:
        space_part, res_part = name.rsplit("_res-", 1)
        canonical_space = _canonicalize_space_variant(space_part)
        canonical_name = f"{canonical_space}_res-{res_part}"
        if canonical_name != name:
            logger.info(
                f"Using space equivalence: {name}{canonical_name} "
                f"(anatomically identical spaces)"
            )
            name = canonical_name

    # Get metadata from registry
    metadata = TEMPLATE_REGISTRY.get(name)

    # Normalize space to handle equivalence
    space_normalized = _canonicalize_space_variant(metadata.space)

    # Log if normalization occurred
    if space_normalized != metadata.space:
        logger.info(
            f"Using space equivalence: {metadata.space}{space_normalized} "
            f"(anatomically identical spaces)"
        )

    try:
        import templateflow.api as tflow
    except ImportError as e:
        raise ImportError(
            "TemplateFlow is required for template loading. "
            "Install with: pip install templateflow"
        ) from e

    try:
        # Get template from TemplateFlow (using normalized space)
        template_path = tflow.get(
            space_normalized,
            resolution=metadata.resolution,
            desc=None,
            suffix=metadata.modality,
            extension=".nii.gz",
        )

        if template_path is None or (isinstance(template_path, list) and not template_path):
            raise ValueError(
                f"Template not found in TemplateFlow: {metadata.space} at {metadata.resolution}mm"
            )

        # TemplateFlow can return a list, take first item
        if isinstance(template_path, list):
            template_path = template_path[0]

        return Path(template_path)

    except Exception as e:
        raise FileNotFoundError(
            f"Failed to load template {name} "
            f"(space={metadata.space}, res={metadata.resolution}, modality={metadata.modality}): {e}"
        ) from e