Hierarchical & Surface Networks

Build layered TVBO networks where a fine mesh references a coarse parent network

TVBO supports hierarchical multi-layer networks where a fine-grained network (e.g. surface vertices) references a coarser parent network (e.g. brain regions or lobes). This page covers that layered construction.

Two related topics live elsewhere:

Multi-Layer Networks: Surface Mapping

A hierarchical network avoids redundant storage: the surface network stores only the mesh geometry and a vertex→region mapping, while the parent network holds the connectivity matrices.

Architecture

Surface Network (desc-surf)          Parent Network (desc-SC)
┌──────────────────────────┐        ┌──────────────────────┐
│ 64,984 vertices          │        │ 17 lobe nodes        │
│ mesh: vertices, triangles│  ───►  │ edges: weights,      │
│ region_mapping: int[N]   │        │        lengths        │
│ parent_network: *.yaml   │        │ centroids, metadata   │
└──────────────────────────┘        └──────────────────────┘

The parent_network field in the surface YAML points to the SC network file, and node_mapping gives the HDF5 dataset path for the vertex→region index array.

Creating a Surface Network

Load a cortical surface mesh (e.g. fsLR 32k from TemplateFlow) and map each vertex to its parent region by sampling the parcellation atlas. This reuses the lobar_atlas.nii.gz and the saved SC Network produced in Build a Connectome from Tractography:

import nibabel as nib
import numpy as np
from pathlib import Path
from templateflow.conf import TF_HOME

# Load fsLR 32k midthickness surfaces (L + R hemispheres)
fslr_dir = Path(TF_HOME) / "tpl-fsLR"
vertices, triangles = [], []
offset = 0
for hemi in ("L", "R"):
    gii = nib.load(fslr_dir / f"tpl-fsLR_den-32k_hemi-{hemi}_midthickness.surf.gii")
    v = gii.darrays[0].data.astype(np.float32)
    t = gii.darrays[1].data.astype(np.int32)
    vertices.append(v)
    triangles.append(t + offset)
    offset += len(v)

vertices = np.concatenate(vertices)   # (64984, 3)
triangles = np.concatenate(triangles) # (129960, 3)

Map each vertex to a lobe by sampling the atlas in voxel space:

atlas_img = nib.load("lobar_atlas.nii.gz")
atlas_data = np.asarray(atlas_img.dataobj, dtype=np.int32)
inv_affine = np.linalg.inv(atlas_img.affine)

# MNI → voxel → nearest-neighbor sample
ones = np.ones((len(vertices), 1), dtype=np.float32)
vox = (inv_affine @ np.hstack([vertices, ones]).T).T[:, :3]
vi = np.round(vox).astype(np.int32)
for ax in range(3):
    np.clip(vi[:, ax], 0, atlas_data.shape[ax] - 1, out=vi[:, ax])

labels = atlas_data[vi[:, 0], vi[:, 1], vi[:, 2]]
region_mapping = labels.astype(np.int32) - 1  # 0-based, -1 = unmapped

Create the surface Network and link it to the parent:

from tvbo import Network
from tvbo.datamodel import tvbo_datamodel

surface_net = Network(nodes=[], edges=[], number_of_nodes=0)
surface_net.number_of_nodes = len(vertices)
surface_net.label = "Lobar surface (fsLR 32k)"
surface_net.descriptor = "surf"
surface_net.distance_unit = "mm"

# Store mesh geometry
mesh = tvbo_datamodel.Mesh(
    label="CorticalSurface",
    element_type="triangle",
    number_of_vertices=len(vertices),
    number_of_elements=len(triangles),
)
object.__setattr__(surface_net, '_mesh', mesh)
object.__setattr__(surface_net, '_mesh_vertices', vertices)
object.__setattr__(surface_net, '_mesh_elements', triangles)

# Link to parent SC network
surface_net.set_node_mapping(
    region_mapping,
    parent_network=sc_network,  # the saved lobar SC Network
    dataset_path="/mesh/region_mapping",
)

surface_net.save("my_surface.yaml")

Output Structure

The surface network produces:

File Size Contents
*_desc-surf_relmat.yaml ~600 B Metadata + parent reference
*_desc-surf_relmat.h5 ~2 MB Mesh (vertices, triangles, normals) + region mapping
*_desc-SC_relmat.yaml ~2 KB SC metadata (17 nodes, positions)
*_desc-SC_relmat.h5 ~3 KB Weight + length matrices (17×17)

No connectivity matrices are duplicated, because the surface network references the parent SC network for all edge data.

Tips

  • Surface coverage: Cortical surfaces (fsLR, fsaverage) cover only cortical regions, so subcortical and cerebellar vertices are not mapped. Unmapped vertices get index -1 in the region mapping.
  • Schema validation: network.save() validates the YAML against the TVBO LinkML schema automatically.