Source code for spectralbrain.utils.atlas

"""Brain atlas label registries.

Maps atlas label IDs to human-readable region names, hemispheres,
network assignments, and canonical colours.  Covers the 19 atlases
in :class:`~spectralbrain.runtime.AtlasScheme`.

The two primary use cases are:

1. **Point-cloud extraction**: look up label IDs for a structure
   (``get_label_id("aseg", "Left-Hippocampus") → 17``).
2. **Geometric connectome**: map Schaefer parcels to Yeo networks
   for block-level aggregation.

Label tables for subcortical atlases (aseg, thalamic nuclei,
hippocampal subfields, amygdala nuclei) are embedded.  Cortical
atlases (Schaefer, DKT, Destrieux) load from FreeSurfer annotation
files when available.
"""

from __future__ import annotations

from typing import Literal

from spectralbrain.runtime import get_logger

logger = get_logger(__name__)


# ======================================================================
# §1  FREESURFER ASEG
# ======================================================================

ASEG_LABELS: dict[int, str] = {
    2: "Left-Cerebral-White-Matter",
    3: "Left-Cerebral-Cortex",
    4: "Left-Lateral-Ventricle",
    5: "Left-Inf-Lat-Vent",
    7: "Left-Cerebellum-White-Matter",
    8: "Left-Cerebellum-Cortex",
    10: "Left-Thalamus",
    11: "Left-Caudate",
    12: "Left-Putamen",
    13: "Left-Pallidum",
    14: "3rd-Ventricle",
    15: "4th-Ventricle",
    16: "Brain-Stem",
    17: "Left-Hippocampus",
    18: "Left-Amygdala",
    24: "CSF",
    26: "Left-Accumbens-area",
    28: "Left-VentralDC",
    30: "Left-vessel",
    31: "Left-choroid-plexus",
    41: "Right-Cerebral-White-Matter",
    42: "Right-Cerebral-Cortex",
    43: "Right-Lateral-Ventricle",
    44: "Right-Inf-Lat-Vent",
    46: "Right-Cerebellum-White-Matter",
    47: "Right-Cerebellum-Cortex",
    49: "Right-Thalamus",
    50: "Right-Caudate",
    51: "Right-Putamen",
    52: "Right-Pallidum",
    53: "Right-Hippocampus",
    54: "Right-Amygdala",
    58: "Right-Accumbens-area",
    60: "Right-VentralDC",
    62: "Right-vessel",
    63: "Right-choroid-plexus",
    77: "WM-hypointensities",
    85: "Optic-Chiasm",
    251: "CC_Posterior",
    252: "CC_Mid_Posterior",
    253: "CC_Central",
    254: "CC_Mid_Anterior",
    255: "CC_Anterior",
}


# ======================================================================
# §2  HIPPOCAMPAL SUBFIELDS (FreeSurfer v7.x, T1-based)
# ======================================================================

HIPPOCAMPAL_SUBFIELDS: dict[int, str] = {
    203: "parasubiculum",
    204: "presubiculum-head",
    205: "presubiculum-body",
    206: "subiculum-head",
    207: "subiculum-body",
    208: "CA1-head",
    209: "CA1-body",
    210: "CA2/3-head",
    211: "CA2/3-body",
    212: "CA4-head",
    213: "CA4-body",
    214: "GC-ML-DG-head",
    215: "GC-ML-DG-body",
    226: "molecular_layer_HP-head",
    227: "molecular_layer_HP-body",
    228: "hippocampal-fissure",
    229: "HATA",
    230: "fimbria",
    231: "hippocampal_tail",
    232: "whole_hippocampal_head",
    233: "whole_hippocampal_body",
}

# Right-hemisphere labels are + 1000.
HIPPOCAMPAL_SUBFIELDS_RIGHT: dict[int, str] = {
    k + 1000: v.replace("left", "right") for k, v in HIPPOCAMPAL_SUBFIELDS.items()
}


# ======================================================================
# §3  THALAMIC NUCLEI (FreeSurfer v7.x)
# ======================================================================

THALAMIC_NUCLEI: dict[int, str] = {
    8103: "Left-AV",
    8104: "Left-CeM",
    8105: "Left-CL",
    8106: "Left-CM",
    8108: "Left-LD",
    8109: "Left-LGN",
    8110: "Left-LP",
    8111: "Left-L-Sg",
    8112: "Left-MDl",
    8113: "Left-MDm",
    8115: "Left-MGN",
    8116: "Left-MV(Re)",
    8117: "Left-Pc",
    8118: "Left-Pf",
    8119: "Left-Pt",
    8120: "Left-PuA",
    8121: "Left-PuI",
    8122: "Left-PuL",
    8123: "Left-PuM",
    8126: "Left-VA",
    8127: "Left-VAmc",
    8128: "Left-VLa",
    8129: "Left-VLp",
    8130: "Left-VM",
    8131: "Left-VPL",
    8133: "Left-Whole_thalamus",
    # Right = Left + 100
}

THALAMIC_NUCLEI_RIGHT: dict[int, str] = {
    k + 100: v.replace("Left", "Right") for k, v in THALAMIC_NUCLEI.items()
}


# ======================================================================
# §4  AMYGDALA NUCLEI (FreeSurfer v7.x)
# ======================================================================

AMYGDALA_NUCLEI: dict[int, str] = {
    7001: "Left-Lateral-nucleus",
    7002: "Left-Basal-nucleus",
    7003: "Left-Accessory-Basal-nucleus",
    7004: "Left-Anterior-amygdaloid-area",
    7005: "Left-Central-nucleus",
    7006: "Left-Medial-nucleus",
    7007: "Left-Cortical-nucleus",
    7008: "Left-Corticoamygdaloid-transition",
    7009: "Left-Paralaminar-nucleus",
    7010: "Left-Whole-amygdala",
}

AMYGDALA_NUCLEI_RIGHT: dict[int, str] = {
    k + 1000: v.replace("Left", "Right") for k, v in AMYGDALA_NUCLEI.items()
}


# ======================================================================
# §5  YEO NETWORK ASSIGNMENTS
# ======================================================================

YEO_7_NETWORKS: dict[int, str] = {
    1: "Visual",
    2: "Somatomotor",
    3: "DorsalAttention",
    4: "VentralAttention",
    5: "Limbic",
    6: "Frontoparietal",
    7: "Default",
}

YEO_17_NETWORKS: dict[int, str] = {
    1: "VisCent",
    2: "VisPeri",
    3: "SomMotA",
    4: "SomMotB",
    5: "DorsAttnA",
    6: "DorsAttnB",
    7: "SalVentAttnA",
    8: "SalVentAttnB",
    9: "LimbicA",
    10: "LimbicB",
    11: "ContA",
    12: "ContB",
    13: "ContC",
    14: "DefaultA",
    15: "DefaultB",
    16: "DefaultC",
    17: "TempPar",
}


# ======================================================================
# §6  UNIFIED LOOKUP
# ======================================================================

_REGISTRIES: dict[str, dict[int, str]] = {
    "aseg": ASEG_LABELS,
    "hippocampal_subfields": {
        **HIPPOCAMPAL_SUBFIELDS,
        **HIPPOCAMPAL_SUBFIELDS_RIGHT,
    },
    "thalamic_nuclei": {
        **THALAMIC_NUCLEI,
        **THALAMIC_NUCLEI_RIGHT,
    },
    "amygdala_nuclei": {
        **AMYGDALA_NUCLEI,
        **AMYGDALA_NUCLEI_RIGHT,
    },
}


[docs] def get_label_name(atlas: str, label_id: int) -> str: """Look up the region name for a label ID. Parameters ---------- atlas : str Atlas name (e.g. ``"aseg"``, ``"thalamic_nuclei"``). label_id : int Returns ------- str Region name, or ``"Unknown-{label_id}"``. """ registry = _REGISTRIES.get(atlas, {}) return registry.get(label_id, f"Unknown-{label_id}")
[docs] def get_label_id(atlas: str, name: str) -> int | None: """Reverse lookup: region name → label ID. Parameters ---------- atlas : str name : str Region name (case-insensitive substring match). Returns ------- int or None """ registry = _REGISTRIES.get(atlas, {}) name_lower = name.lower() for lid, lname in registry.items(): if name_lower in lname.lower(): return lid return None
[docs] def list_labels(atlas: str) -> dict[int, str]: """Return all label ID → name mappings for an atlas. Parameters ---------- atlas : str Returns ------- dict """ return dict(_REGISTRIES.get(atlas, {}))
[docs] def get_structure_ids( atlas: str, hemisphere: Literal["left", "right", "both"] = "both", ) -> list[int]: """Get all label IDs for a hemisphere. Parameters ---------- atlas : str hemisphere : str Returns ------- list of int """ registry = _REGISTRIES.get(atlas, {}) if hemisphere == "both": return sorted(registry.keys()) ids = [] for lid, name in registry.items(): name_l = name.lower() if hemisphere == "left" and ("left" in name_l or "lh" in name_l): ids.append(lid) elif hemisphere == "right" and ("right" in name_l or "rh" in name_l): ids.append(lid) return sorted(ids)
[docs] def schaefer_to_yeo( parcel_id: int, n_parcels: int = 200, n_networks: int = 7, ) -> str: """Map a Schaefer parcel ID to its Yeo network name. Schaefer parcels encode the network in their naming convention: ``7Networks_LH_Vis_1`` → "Visual". Parameters ---------- parcel_id : int 1-indexed Schaefer parcel ID. n_parcels : int Total parcels (100, 200, 400, etc.). n_networks : int 7 or 17. Returns ------- str Network name. Notes ----- This is a heuristic based on the standard Schaefer ordering. For exact mapping, load the annotation file and parse names. """ networks = YEO_7_NETWORKS if n_networks == 7 else YEO_17_NETWORKS parcels_per_hemi = n_parcels // 2 parcels_per_net = parcels_per_hemi // n_networks # Determine which network this parcel belongs to. hemi_id = (parcel_id - 1) % parcels_per_hemi net_idx = min(hemi_id // parcels_per_net, n_networks - 1) + 1 return networks.get(net_idx, f"Network-{net_idx}")
__all__ = [ "AMYGDALA_NUCLEI", "ASEG_LABELS", "HIPPOCAMPAL_SUBFIELDS", "THALAMIC_NUCLEI", "YEO_7_NETWORKS", "YEO_17_NETWORKS", "get_label_id", "get_label_name", "get_structure_ids", "list_labels", "schaefer_to_yeo", ]