Source code for spectralbrain.viz.hipp

"""Hippocampal surface visualisation — 3D multi-view + unfolded flatmaps.

Combines two rendering engines:
- **hippunfold_plot** (nilearn/matplotlib backend) — for 2D unfolded
  flatmap views and standard 3D views via ``plot_hipp_surf()``.
- **hippomaps** (BrainSpace/VTK backend) — for high-quality 3D
  folded renders via ``surfplot_sub_foldunfold()``.

HippUnfold version compatibility
---------------------------------
All functions accept both **v1** (``"0p5mm"``, ``"1mm"``, ``"2mm"``)
and **v2** (``"2k"``, ``"8k"``, ``"18k"``) density labels.  The
default changed from ``"0p5mm"`` to **``"8k"``** to match HippUnfold
v2's vertex-count-based naming convention.

- v1 labels still work but emit a ``DeprecationWarning``.
- ``"8k"`` ≈ v1 ``"0p5mm"`` (~8,000 combined hipp + dentate vertices).
- v2 merges ``label-hipp`` + ``label-dentate`` → ``label-hippdentate``;
  all label variants are accepted.

See :data:`DENSITIES` for the full mapping and :data:`HIPP_LABELS`
for recognised structure labels.

Each panel row shows 3D views of the hippocampus in the main columns
and an unfolded flatmap in the first or last column, enabling
simultaneous inspection of spatial localisation (3D) and subfield
identity (unfolded).

Figure types
------------
1. Single metric on one hippocampus (3D views + flatmap)
2. Bilateral panel (L + R side by side)
3. Group comparison (control vs patient vs difference)
4. Spectral descriptor gallery (HKS, WKS, BKS, … stacked)
5. Multi-subject normative deviation panel
"""

from __future__ import annotations

from pathlib import Path
from typing import Any, Literal

import matplotlib.pyplot as plt
import numpy as np
from matplotlib.axes import Axes
from matplotlib.figure import Figure

from spectralbrain.runtime import PathLike, get_logger

logger = get_logger(__name__)

DPI: int = 600

# 3D views for hippocampus (hippunfold_plot view tokens).
HIPP_VIEWS_3D: list[str] = [
    "lateral",
    "medial",
    "dorsal",
    "ventral",
    "anterior",
]
"""Standard 3D views for hippocampal surface."""

HIPP_VIEWS_FULL: list[str] = [*HIPP_VIEWS_3D, "flatmap"]
"""3D views + unfolded flatmap."""

# Density mapping for HippUnfold versions.
# v2 (vertex-count-based) is the new canonical naming.
# v1 (millimetre-based) names are kept as backward-compatible aliases.
DENSITIES: dict[str, str] = {
    # HippUnfold v2 canonical names (preferred)
    "2k": "2k",  # ~2,000 combined vertices
    "8k": "8k",  # ~8,000 combined vertices (default)
    "18k": "18k",  # ~18,000 combined vertices
    # HippUnfold v1 backward-compatible aliases
    "0p5mm": "0p5mm",  # v1 default — 7,262 hipp + 1,788 dentate
    "1mm": "1mm",  # v1 — 2,004 hipp + 449 dentate
    "2mm": "2mm",  # v1 — 419 hipp + 64 dentate
}
"""Recognised HippUnfold density labels (v1 and v2)."""

# v1 → v2 approximate equivalence (for docs/migration only).
_DENSITY_V1_TO_V2: dict[str, str] = {
    "0p5mm": "8k",
    "1mm": "2k",
    "2mm": "2k",
}

# HippUnfold v2 merges hipp + dentate into a single surface.
# v1 outputs: label-hipp, label-dentate  (separate surfaces)
# v2 outputs: label-hippdentate           (combined surface)
# SpectralBrain accepts all three.
HIPP_LABELS: list[str] = ["hipp", "dentate", "hippdentate"]
"""Recognised HippUnfold structure labels."""


def _resolve_density(density: str) -> str:
    """Validate and return the density string.

    Accepts both HippUnfold v1 (``"8k"``, ``"1mm"``, ``"2mm"``) and
    v2 (``"2k"``, ``"8k"``, ``"18k"``) density labels.  Emits a
    deprecation warning for v1 labels and returns the string unchanged
    so that downstream libraries (hippunfold_plot, hippomaps) can
    handle it in their own way.

    Parameters
    ----------
    density : str
        Density label to validate.

    Returns
    -------
    str
        The validated density string (unchanged).

    Raises
    ------
    ValueError
        If *density* is not a recognised label.
    """
    import warnings

    if density not in DENSITIES:
        raise ValueError(
            f"Unknown HippUnfold density: {density!r}. Use one of {list(DENSITIES.keys())}."
        )
    if density in _DENSITY_V1_TO_V2:
        v2_equiv = _DENSITY_V1_TO_V2[density]
        warnings.warn(
            f"HippUnfold v1 density '{density}' is deprecated. "
            f"Consider switching to v2 equivalent '{v2_equiv}'. "
            f"See HippUnfold v2.0.0 release notes.",
            DeprecationWarning,
            stacklevel=3,
        )
    return density


# Default descriptor visual specs (hippocampal-specific).
HIPP_DESCRIPTOR_STYLES: dict[str, dict[str, Any]] = {
    "thickness": {"cmap": "inferno", "vmin": 1.0, "vmax": 4.0, "label": "Thickness"},
    "curvature": {"cmap": "RdBu_r", "vmin": -0.3, "vmax": 0.3, "label": "Curvature"},
    "gyrification": {"cmap": "magma", "vmin": 0, "vmax": 2.0, "label": "Gyrification"},
    "subfields": {"cmap": "tab10", "vmin": None, "vmax": None, "label": "Subfields"},
    "hks": {"cmap": "inferno", "vmin": None, "vmax": None, "label": "HKS"},
    "wks": {"cmap": "cividis", "vmin": None, "vmax": None, "label": "WKS"},
    "bks": {"cmap": "magma", "vmin": None, "vmax": None, "label": "BKS"},
    "z_score": {"cmap": "RdBu_r", "vmin": -3, "vmax": 3, "label": "Z-score"},
    "effect_d": {"cmap": "RdBu_r", "vmin": -1.5, "vmax": 1.5, "label": "Cohen's d"},
    "shape_idx": {"cmap": "RdBu_r", "vmin": -1, "vmax": 1, "label": "Shape Index"},
    "casorati": {"cmap": "magma", "vmin": None, "vmax": None, "label": "Casorati"},
}


# ======================================================================
# §0  LAZY IMPORTS
# ======================================================================


def _require_hippunfold_plot():
    """Lazy-import hippunfold_plot for hippocampal rendering."""
    try:
        from hippunfold_plot.plotting import plot_hipp_surf

        return plot_hipp_surf
    except ImportError as exc:
        raise ImportError(
            "hippunfold_plot is required for hippocampal flatmaps.\n  pip install hippunfold_plot"
        ) from exc


def _require_hippomaps():
    """Lazy-import hippomaps for normative hippocampal context."""
    try:
        import hippomaps

        return hippomaps
    except ImportError:
        logger.debug("hippomaps not installed — using hippunfold_plot only.")
        return None


def _apply_style():
    """Apply scienceplots if available."""
    try:
        import scienceplots  # noqa: F401

        plt.style.use(["science", "no-latex"])
        plt.rcParams["mathtext.fontset"] = "cm"
    except ImportError:
        pass
    plt.rcParams["savefig.dpi"] = DPI
    plt.rcParams["figure.dpi"] = DPI


def _save_figure(fig, path, formats=None):
    """Save the figure to disk if a path is provided."""
    from spectralbrain.viz.graphics import savefig

    return savefig(fig, path, formats=formats, dpi=DPI)


# ======================================================================
# §1  CORE RENDERING
# ======================================================================


def _render_hipp_3d(
    surf_map: Any,
    *,
    view: str = "dorsal",
    hemi: str = "left",
    density: str = "8k",
    cmap: str = "inferno",
    vmin: float | None = None,
    vmax: float | None = None,
    nan_color: Any = (0.85, 0.85, 0.85),
    bg_on_data: bool = True,
    alpha: float = 0.1,
    darkness: float = 2,
    dpi: int = DPI,
) -> Figure:
    """Render one 3D hippocampal view via hippunfold_plot.

    Parameters
    ----------
    surf_map : str or ndarray
        GIfTI path or vertex array.
    view : str
        ``"dorsal"``, ``"ventral"``, ``"lateral"``, ``"medial"``,
        ``"anterior"``, ``"posterior"``.
    hemi : str
        ``"left"`` or ``"right"``.
    density : str
        HippUnfold density.

    Returns
    -------
    matplotlib.Figure
    """
    plot_hipp_surf = _require_hippunfold_plot()
    density = _resolve_density(density)

    kwargs = dict(
        surf_map=surf_map,
        density=density,
        hemi=hemi,
        view=view,
        cmap=cmap,
        vmin=vmin,
        vmax=vmax,
        bg_on_data=bg_on_data,
        alpha=alpha,
        darkness=darkness,
        dpi=dpi,
        figsize=(4, 3),
        colorbar=False,
    )

    fig = plot_hipp_surf(**kwargs)
    return fig


def _render_hipp_flatmap(
    surf_map: Any,
    *,
    hemi: str = "left",
    density: str = "8k",
    cmap: str = "inferno",
    vmin: float | None = None,
    vmax: float | None = None,
    dpi: int = DPI,
) -> Figure:
    """Render an unfolded (flatmap) view via hippunfold_plot.

    Uses ``space='unfold'`` to get the 2D representation.
    """
    plot_hipp_surf = _require_hippunfold_plot()
    density = _resolve_density(density)

    fig = plot_hipp_surf(
        surf_map=surf_map,
        density=density,
        hemi=hemi,
        space="unfold",
        view="dorsal",  # top-down on unfold
        cmap=cmap,
        vmin=vmin,
        vmax=vmax,
        bg_on_data=True,
        alpha=0.1,
        darkness=2,
        dpi=dpi,
        figsize=(3, 4),
        colorbar=False,
    )
    return fig


def _fig_to_image(fig: Figure) -> np.ndarray:
    """Convert matplotlib Figure to RGB array."""
    fig.canvas.draw()
    buf = fig.canvas.buffer_rgba()
    img = np.asarray(buf)
    plt.close(fig)
    return img


# ======================================================================
# §2  SINGLE HIPPOCAMPUS — 3D + FLATMAP
# ======================================================================


[docs] def plot_hippocampus( surf_map: Any, *, hemi: str = "left", density: str = "8k", views: list[str] | None = None, show_flatmap: bool = True, flatmap_position: Literal["first", "last"] = "last", cmap: str = "inferno", vmin: float | None = None, vmax: float | None = None, nan_color: Any = (0.85, 0.85, 0.85), style: str = "default", display_type: str = "static", title: str = "", save: PathLike | None = None, formats: str | list[str] | None = None, ) -> tuple[Figure, list[Axes]]: """Single hippocampus: 3D multi-view + unfolded flatmap. Parameters ---------- surf_map : str or ndarray GIfTI path or per-vertex array. hemi : str ``"left"`` or ``"right"``. density : str HippUnfold density (``"8k"``, ``"2k"``, ``"8k"``, ``"18k"``). views : list of str, optional 3D view names. Default: lateral, medial, dorsal, ventral, anterior. show_flatmap : bool Include unfolded flatmap column. flatmap_position : str ``"first"`` or ``"last"`` column. cmap : str vmin, vmax : float nan_color, style, display_type : styling. title : str save : PathLike, optional Returns ------- fig, axes Examples -------- >>> plot_hippocampus( ... "sub-01_hemi-L_thickness.shape.gii", ... hemi="left", density="8k", ... cmap="inferno", vmin=1.0, vmax=4.0, ... save="hippo_thickness.png", ... ) """ if views is None: views = HIPP_VIEWS_3D _apply_style() # Render each 3D view. view_imgs = [] for v in views: fig_v = _render_hipp_3d( surf_map, view=v, hemi=hemi, density=density, cmap=cmap, vmin=vmin, vmax=vmax, nan_color=nan_color, ) view_imgs.append(_fig_to_image(fig_v)) # Render flatmap. flatmap_img = None if show_flatmap: fig_flat = _render_hipp_flatmap( surf_map, hemi=hemi, density=density, cmap=cmap, vmin=vmin, vmax=vmax, ) flatmap_img = _fig_to_image(fig_flat) # Compose into panel. n_cols = len(views) + (1 if show_flatmap else 0) fig, axes_row = plt.subplots( 1, n_cols, figsize=(2.5 * n_cols, 3), dpi=DPI, ) if n_cols == 1: axes_row = [axes_row] col_labels = list(views) col_images = list(view_imgs) if show_flatmap: if flatmap_position == "first": col_images.insert(0, flatmap_img) col_labels.insert(0, "unfolded") else: col_images.append(flatmap_img) col_labels.append("unfolded") import seaborn as sns for ax, img, label in zip(axes_row, col_images, col_labels): ax.imshow(img, aspect="auto", interpolation="lanczos") ax.set_xticks([]) ax.set_yticks([]) ax.set_xlabel(label, fontsize=7) sns.despine(ax=ax, left=True, bottom=True, top=True, right=True) if title: fig.suptitle(title, fontsize=10, fontweight="bold", y=1.02) fig.tight_layout() if save: _save_figure(fig, save, formats=formats) return fig, list(axes_row)
# ====================================================================== # §3 BILATERAL PANEL # ======================================================================
[docs] def plot_hippocampus_bilateral( surf_map_left: Any, surf_map_right: Any, *, density: str = "8k", views: list[str] | None = None, show_flatmap: bool = True, cmap: str = "inferno", vmin: float | None = None, vmax: float | None = None, nan_color: Any = (0.85, 0.85, 0.85), style: str = "default", display_type: str = "static", title: str = "Bilateral Hippocampus", save: PathLike | None = None, formats: str | list[str] | None = None, ) -> tuple[Figure, np.ndarray]: """Two-row bilateral panel: L (top) + R (bottom). Parameters ---------- surf_map_left, surf_map_right : str or ndarray """ if views is None: views = ["lateral", "medial", "dorsal", "anterior"] _apply_style() all_rows = [] row_labels = ["Left", "Right"] for hemi, smap in [("left", surf_map_left), ("right", surf_map_right)]: row_imgs = [] for v in views: fig_v = _render_hipp_3d( smap, view=v, hemi=hemi, density=density, cmap=cmap, vmin=vmin, vmax=vmax, nan_color=nan_color, ) row_imgs.append(_fig_to_image(fig_v)) if show_flatmap: fig_flat = _render_hipp_flatmap( smap, hemi=hemi, density=density, cmap=cmap, vmin=vmin, vmax=vmax, ) row_imgs.append(_fig_to_image(fig_flat)) all_rows.append(row_imgs) n_cols = len(views) + (1 if show_flatmap else 0) n_rows = 2 fig, axes = plt.subplots( n_rows, n_cols, figsize=(2.5 * n_cols, 3 * n_rows), dpi=DPI, ) import seaborn as sns col_labels = list(views) + (["unfolded"] if show_flatmap else []) for i in range(n_rows): for j in range(n_cols): ax = axes[i, j] ax.imshow(all_rows[i][j], aspect="auto", interpolation="lanczos") ax.set_xticks([]) ax.set_yticks([]) sns.despine(ax=ax, left=True, bottom=True, top=True, right=True) if i == n_rows - 1: ax.set_xlabel(col_labels[j], fontsize=7) if j == 0: ax.set_ylabel( row_labels[i], fontsize=8, fontweight="bold", rotation=0, ha="right", va="center", labelpad=10, ) if title: fig.suptitle(title, fontsize=10, fontweight="bold", y=1.02) fig.tight_layout() if save: _save_figure(fig, save, formats=formats) return fig, axes
# ====================================================================== # §4 GROUP COMPARISON # ======================================================================
[docs] def plot_hippocampus_comparison( group_a_map: Any, group_b_map: Any, diff_map: Any | None = None, *, hemi: str = "left", density: str = "8k", views: list[str] | None = None, show_flatmap: bool = True, cmap_groups: str = "inferno", cmap_diff: str = "RdBu_r", vmin: float | None = None, vmax: float | None = None, vmin_diff: float = -3.0, vmax_diff: float = 3.0, nan_color: Any = (0.85, 0.85, 0.85), style: str = "default", display_type: str = "static", row_labels: list[str] | None = None, title: str = "Group Comparison", save: PathLike | None = None, formats: str | list[str] | None = None, ) -> tuple[Figure, np.ndarray]: """2–3 row group comparison: A, B, [A−B]. Parameters ---------- group_a_map, group_b_map : str or ndarray Mean descriptor map per group. diff_map : str or ndarray, optional A − B difference (or t-map / z-map). """ if views is None: views = ["lateral", "medial", "dorsal", "anterior"] if row_labels is None: row_labels = ["Control", "Patient"] if diff_map is not None: row_labels.append("Difference") _apply_style() specs = [ (group_a_map, cmap_groups, vmin, vmax), (group_b_map, cmap_groups, vmin, vmax), ] if diff_map is not None: specs.append((diff_map, cmap_diff, vmin_diff, vmax_diff)) all_rows = [] for smap, cm, vmn, vmx in specs: row_imgs = [] for v in views: fig_v = _render_hipp_3d( smap, view=v, hemi=hemi, density=density, cmap=cm, vmin=vmn, vmax=vmx, nan_color=nan_color, ) row_imgs.append(_fig_to_image(fig_v)) if show_flatmap: fig_flat = _render_hipp_flatmap( smap, hemi=hemi, density=density, cmap=cm, vmin=vmn, vmax=vmx, ) row_imgs.append(_fig_to_image(fig_flat)) all_rows.append(row_imgs) n_cols = len(views) + (1 if show_flatmap else 0) n_rows = len(specs) fig, axes = plt.subplots( n_rows, n_cols, figsize=(2.5 * n_cols, 2.5 * n_rows), dpi=DPI, ) if n_rows == 1: axes = axes[np.newaxis, :] import seaborn as sns col_labels = list(views) + (["unfolded"] if show_flatmap else []) for i in range(n_rows): for j in range(n_cols): ax = axes[i, j] ax.imshow(all_rows[i][j], aspect="auto", interpolation="lanczos") ax.set_xticks([]) ax.set_yticks([]) sns.despine(ax=ax, left=True, bottom=True, top=True, right=True) if i == n_rows - 1: ax.set_xlabel(col_labels[j], fontsize=7) if j == 0: ax.set_ylabel( row_labels[i], fontsize=8, fontweight="bold", rotation=0, ha="right", va="center", labelpad=10, ) if title: fig.suptitle(title, fontsize=10, fontweight="bold", y=1.02) fig.tight_layout() if save: _save_figure(fig, save, formats=formats) return fig, axes
# ====================================================================== # §5 DESCRIPTOR GALLERY # ====================================================================== # ====================================================================== # §6 NORMATIVE DEVIATION PANEL # ======================================================================
[docs] def plot_hippocampus_normative( z_map: Any, *, hemi: str = "left", density: str = "8k", views: list[str] | None = None, show_flatmap: bool = True, threshold: float = 2.0, cmap: str = "RdBu_r", vmin: float = -3.0, vmax: float = 3.0, nan_color: Any = (0.85, 0.85, 0.85), style: str = "default", display_type: str = "static", title: str = "Hippocampal Normative Deviation", save: PathLike | None = None, formats: str | list[str] | None = None, ) -> tuple[Figure, np.ndarray]: """Normative z-score map with thresholded view. Two rows: full z-map (top), thresholded |Z| > threshold (bottom). Parameters ---------- z_map : str or ndarray Per-vertex z-scores. threshold : float Threshold for the second row. """ descriptors = {"Z-score": z_map} # Build thresholded version. if isinstance(z_map, np.ndarray): thr_map = z_map.copy() thr_map[np.abs(thr_map) <= threshold] = np.nan descriptors[f"|Z| > {threshold}"] = thr_map # Override styles for this specific plot. HIPP_DESCRIPTOR_STYLES["Z-score"] = { "cmap": cmap, "vmin": vmin, "vmax": vmax, "label": "Z-score", } HIPP_DESCRIPTOR_STYLES[f"|Z| > {threshold}"] = { "cmap": cmap, "vmin": vmin, "vmax": vmax, "label": f"|Z| > {threshold}", } return plot_hippocampus_gallery( descriptors, hemi=hemi, density=density, views=views, show_flatmap=show_flatmap, nan_color=nan_color, title=title, save=save, formats=formats, )
# ====================================================================== # §6 SPATIO-TEMPORAL DESCRIPTOR FIELD ON HIPPOCAMPAL UNFOLDED SHEET # ======================================================================
[docs] def plot_hippocampus_spatiotemporal( H: np.ndarray, *, hemi: Literal["left", "right"] = "left", density: str = "8k", t_values: np.ndarray | None = None, n_panels: int = 8, t_indices: list[int] | None = None, cmap: str = "magma", log_norm: bool = True, show_subfields: bool = True, descriptor_name: str = "HKS", unfolded_surf_path: str | Path | None = None, subfield_label_path: str | Path | None = None, figsize: tuple[float, float] | None = None, save: PathLike | None = None, formats: tuple[str, ...] = ("png", "pdf"), ) -> tuple[Figure, np.ndarray]: """Spatio-temporal descriptor field on the HippUnfold unfolded sheet. Wraps :func:`spectralbrain.viz.clusters.plot_spatiotemporal_field` with HippUnfold-aware defaults: loads the canonical unfolded mid-thickness surface and subfield labels automatically when paths are provided, applies the AP × PD coordinate convention, and uses neuroimaging-standard axis labels. This is the hippocampal-specific version of the general spatio-temporal small-multiples visualization. Each panel shows how HKS/WKS varies across the unfolded hippocampal sheet at a different diffusion time, revealing the local → global progression and how it relates to subfield boundaries. Parameters ---------- H : (V, T) array Per-vertex spectral descriptor matrix. V must match the number of vertices in the unfolded surface. hemi : str ``"left"`` or ``"right"``. density : str HippUnfold density label (``"8k"``, ``"2k"``, ``"18k"``; v1: ``"0p5mm"``, ``"1mm"``, ``"2mm"``). t_values : (T,) array or None Scale parameter values. n_panels : int t_indices : list of int or None cmap : str log_norm : bool show_subfields : bool Overlay subfield boundary lines. descriptor_name : str unfolded_surf_path : str or Path or None Path to the unfolded ``midthickness.surf.gii``. If None, attempts to load from hippunfold_plot's bundled data. subfield_label_path : str or Path or None Path to ``subfields.label.gii``. If None and show_subfields is True, attempts to load from hippunfold_plot's bundled data. figsize : tuple or None save : PathLike or None formats : tuple of str Export formats. Returns ------- (Figure, ndarray of Axes) Notes ----- The unfolded surface from HippUnfold uses Laplace-solved coordinates (AP × PD), not physical millimetres. Axes are labelled accordingly as "AP coordinate" and "PD coordinate" to avoid misleading the reader. Metric distortion between folded and unfolded spaces does not affect the plotted descriptor values (HKS/WKS are isometry- invariant), but it does affect area-based statistics — use folded- surface vertex areas for any integration. """ try: import nibabel as nib except ImportError: raise ImportError("nibabel required: pip install nibabel") H = np.asarray(H, dtype=np.float64) density = _resolve_density(density) # --- load unfolded surface --- if unfolded_surf_path is not None: surf_gii = nib.load(str(unfolded_surf_path)) coords = surf_gii.agg_data("pointset") # (V, 3) faces = surf_gii.agg_data("triangle") # (F, 3) # project 3D → 2D (drop the flat z ≈ 0) unfolded_2d = coords[:, :2] else: # attempt to get from hippunfold_plot bundled data try: from hippunfold_plot.utils import get_surf_coords coords, faces = get_surf_coords( density=density, hemi=hemi, space="unfold", ) unfolded_2d = coords[:, :2] except (ImportError, Exception) as e: raise ValueError( f"Could not load unfolded surface. Provide " f"unfolded_surf_path explicitly. Error: {e}" ) # --- load subfield labels --- subfield_labels = None if show_subfields: if subfield_label_path is not None: lab_gii = nib.load(str(subfield_label_path)) subfield_labels = lab_gii.agg_data().astype(np.int64) else: try: from hippunfold_plot.utils import get_label_data subfield_labels = get_label_data( density=density, hemi=hemi, ).astype(np.int64) except (ImportError, Exception): logger.warning( "Could not load subfield labels. " "Set show_subfields=False or provide subfield_label_path." ) # --- validate shapes --- if H.shape[0] != unfolded_2d.shape[0]: raise ValueError( f"H has {H.shape[0]} vertices but the unfolded surface " f"has {unfolded_2d.shape[0]}. They must match." ) # --- delegate to the generic spatio-temporal field plotter --- from spectralbrain.viz.clusters import plot_spatiotemporal_field fig, axes = plot_spatiotemporal_field( unfolded_coords=unfolded_2d, faces=faces, H=H, t_values=t_values, n_panels=n_panels, t_indices=t_indices, cmap=cmap, log_norm=log_norm, subfield_labels=subfield_labels, descriptor_name=descriptor_name, xlabel="AP coordinate (Laplace)", ylabel="PD coordinate (Laplace)", figsize=figsize, save=save, ) return fig, axes
[docs] def plot_hippocampus_hovmoller( H: np.ndarray, *, hemi: Literal["left", "right"] = "left", density: str = "8k", t_values: np.ndarray | None = None, axis: Literal["AP", "PD"] = "AP", unfolded_surf_path: str | Path | None = None, cmap: str = "viridis", descriptor_name: str = "HKS", figsize: tuple[float, float] = (8, 4), save: PathLike | None = None, ) -> tuple[Figure, Axes]: """Hovmöller diagram of descriptor along hippocampal axis × scale. Wraps :func:`spectralbrain.viz.clusters.plot_hovmoller` with HippUnfold-aware coordinate loading. Parameters ---------- H : (V, T) array hemi, density : str t_values : (T,) or None axis : str ``"AP"`` or ``"PD"``. unfolded_surf_path : str or None cmap, descriptor_name, figsize, save Returns ------- (Figure, Axes) """ try: import nibabel as nib except ImportError: raise ImportError("nibabel required: pip install nibabel") H = np.asarray(H, dtype=np.float64) density = _resolve_density(density) if unfolded_surf_path is not None: surf_gii = nib.load(str(unfolded_surf_path)) coords = surf_gii.agg_data("pointset") unfolded_2d = coords[:, :2] else: try: from hippunfold_plot.utils import get_surf_coords coords, _ = get_surf_coords( density=density, hemi=hemi, space="unfold", ) unfolded_2d = coords[:, :2] except (ImportError, Exception) as e: raise ValueError(f"Could not load unfolded coordinates: {e}") from spectralbrain.viz.clusters import plot_hovmoller return plot_hovmoller( unfolded_coords=unfolded_2d, H=H, t_values=t_values, axis=axis, cmap=cmap, descriptor_name=descriptor_name, title=f"Hovmöller — {descriptor_name} along {axis} ({hemi})", figsize=figsize, save=save, )
# ====================================================================== __all__ = [ "DENSITIES", "HIPP_DESCRIPTOR_STYLES", "HIPP_LABELS", # Constants "HIPP_VIEWS_3D", "HIPP_VIEWS_FULL", # Single hippocampus "plot_hippocampus", # Bilateral "plot_hippocampus_bilateral", # Group comparison "plot_hippocampus_comparison", # Descriptor gallery "plot_hippocampus_gallery", "plot_hippocampus_hovmoller", # Normative "plot_hippocampus_normative", # Spatio-temporal "plot_hippocampus_spatiotemporal", ]