Hierarchical & Surface Networks
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:
- To build the coarse structural network from tractography, see Build a Connectome from Tractography.
- To wrap connectivity matrices you already have, or load a built-in connectome, see Defining Networks.
- To give regions a detailed spiking or reservoir patch, see Model Across Scales.
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 = unmappedCreate 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.