Skip to content

The Virtual Brain data

quiltwright.tvb_data

TVB Dataset Loader

Downloads and parses demonstration data from The Virtual Brain <https://www.thevirtualbrain.org/>_ (TVB) so that real human, macaque and mouse brain geometry can be fed to the holographic backends -- Looking Glass light-field quilts (:mod:quiltwright.lfd) and Hololuminescent Display video (:mod:quiltwright.hld).

This is a scene source, the same role POV-Ray scenes and PyVista example datasets play elsewhere in Quiltwright: it produces geometry, and says nothing about how that geometry reaches a display.

Where the data comes from

The tvb-root <https://github.com/the-virtual-brain/tvb-root>_ source tree contains no data. All demonstration datasets ship separately as tvb-data, published on Zenodo as a single ~337 MB archive (:data:TVB_DATA_DOI). The old tvb-data GitHub repository is deprecated and the PyPI package carries a reduced file set, so Zenodo is the canonical source and the only one this module uses.

Nothing is vendored into Quiltwright. The archive is fetched on first use and cached on disk, exactly as :mod:pyvista.examples does for its own downloads -- which also keeps TVB's GPL-3.0 licensing out of this BSD-3 source tree. See :func:cache_dir for cache placement and overrides.

Dependencies

Loading needs only the standard library and NumPy. The PyVista bridge (:func:surface_polydata, :func:connectome_polydata) needs the viz extra. Nothing here touches ffmpeg: stills and quilts never encode video, and the GPL-3 imageio-ffmpeg build stays in its own optional video group, as it does for the rest of the package.

Licensing and citation

tvb-data is distributed under GPL-3.0. The data is downloaded at runtime and never redistributed as part of Quiltwright. TVB asks that scientific publications cite the platform; see :data:TVB_CITATION.

What the archive contains

Cortical surfaces are the useful part for holography, and they are stored in the friendliest possible format: nested zips of plain-text vertices.txt / triangles.txt / vertex_normals.txt. No tvb package, no HDF5 and no NIfTI reader is needed for any of the geometry this module exposes.

Surfaces (:func:load_surface) cortex_16384 (16 384 pts), cortex_80k (81 924 pts), cortex_2x120k (283 380 pts, split hemispheres), plus the inner_skull_4096 / outer_skull_4096 / outer_skin_4096 / scalp_1082 / face_8614 head shells and the macaque surface_147k.

Connectivity (:func:load_connectivity) Region-level connectomes at 66/68/76/80/96/192/998 nodes -- a weights matrix, a tract-length matrix and named 3-D region centres, which is everything needed to draw a connectome as nodes-and-tubes in space.

Region mappings (:func:load_region_mapping) Per-vertex integer parcellation labels that colour a surface by region.

Sensors (:func:load_sensors) EEG (63/65), MEG (248/276) and sEEG (588/960) electrode positions.

Quick start

::

from quiltwright.tvb_data import load_surface, load_connectivity

verts, tris, normals = load_surface("cortex_16384")   # downloads once
conn = load_connectivity("connectivity_76")
print(conn.weights.shape, conn.centres.shape, conn.labels[:3])

To build PyVista meshes and put one on a display (needs the viz extra)::

import pyvista as pv
from quiltwright import QUILT_PRESETS, render_quilt, save_quilt
from quiltwright.tvb_data import surface_polydata

cortex = surface_polydata("cortex_16384", region_mapping="regionMapping_16k_76")
p = pv.Plotter(off_screen=True)
p.add_mesh(cortex, scalars="region", cmap="turbo", show_scalar_bar=False)
spec = QUILT_PRESETS["portrait"]
save_quilt(render_quilt(p, spec), "cortex", spec)

For a ready-made CLI over these datasets, see the --tvb-demo mode of waverider-voxel-viz in WaveRider <https://github.com/Flux-Frontiers/waverider>_, which wires scene presets on top of this module.

Part of Quiltwright -- https://github.com/Flux-Frontiers/quiltwright Author: Eric G. Suchanek, PhD

Connectome

Bases: NamedTuple

A TVB region-level connectome.

Parameters:

Name Type Description Default
weights

(n, n) structural connection strengths.

required
tract_lengths

(n, n) fibre-tract lengths in mm.

required
centres

(n, 3) region centre coordinates in mm.

required
labels

(n,) region names, aligned with the matrices.

required

degree property

Weighted degree (row sum of weights) per region.

n_regions property

Number of regions in the connectome.

archive_path()

Return the cached archive's path, downloaded or not.

Returns:

Type Description
Path

Path to tvb_data.zip inside :func:cache_dir.

Source code in src/quiltwright/tvb_data.py
285
286
287
288
289
290
def archive_path() -> Path:
    """Return the cached archive's path, downloaded or not.

    :return: Path to ``tvb_data.zip`` inside :func:`cache_dir`.
    """
    return cache_dir() / _ARCHIVE_NAME

cache_dir()

Return the directory where the TVB archive is cached.

$QUILTWRIGHT_TVB_CACHE overrides the location entirely -- useful for shared or read-only installs, CI, putting a 337 MB archive on a different volume, and pointing several checkouts at one copy. Otherwise the platform's native cache root is used; see :func:quiltwright.cache.cache_root.

The directory is created if it does not exist.

Returns:

Type Description
Path

Path to the cache directory.

Source code in src/quiltwright/tvb_data.py
269
270
271
272
273
274
275
276
277
278
279
280
281
282
def cache_dir() -> Path:
    """Return the directory where the TVB archive is cached.

    ``$QUILTWRIGHT_TVB_CACHE`` overrides the location entirely -- useful for
    shared or read-only installs, CI, putting a 337 MB archive on a different
    volume, and pointing several checkouts at one copy.  Otherwise the
    platform's native cache root is used; see
    :func:`quiltwright.cache.cache_root`.

    The directory is created if it does not exist.

    :return: Path to the cache directory.
    """
    return dataset_cache_dir("tvb", env_var=_CACHE_ENV, create=True)

clear_cache()

Delete the cached TVB archive, if present.

Source code in src/quiltwright/tvb_data.py
390
391
392
393
394
395
def clear_cache() -> None:
    """Delete the cached TVB archive, if present."""
    target = archive_path()
    if target.exists():
        target.unlink()
        print(f"  Removed {target}")

connectome_polydata(name, *, percentile=90.0, tube_radius=1.1, node_radius=4.5, quiet=False)

Build node and edge meshes for a TVB connectome.

Edges below the percentile of non-zero connection weights are dropped -- a full connectome is far too dense to read as a hologram, and the strongest tracts are what carry the structure.

Parameters:

Name Type Description Default
name str

Key from :data:CONNECTIVITIES.

required
percentile float

Keep only edges at or above this percentile of the non-zero weights (0 keeps every non-zero edge).

90.0
tube_radius float

Edge tube radius in mm.

1.1
node_radius float

Radius in mm of the largest region sphere; spheres are scaled by weighted degree.

4.5
quiet bool

Suppress download progress output.

False

Returns:

Type Description

(nodes, edges) PyVista meshes. nodes carries a "degree" scalar, edges carries a "weight" scalar.

Source code in src/quiltwright/tvb_data.py
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
def connectome_polydata(
    name: str,
    *,
    percentile: float = 90.0,
    tube_radius: float = 1.1,
    node_radius: float = 4.5,
    quiet: bool = False,
):
    """Build node and edge meshes for a TVB connectome.

    Edges below the *percentile* of non-zero connection weights are dropped --
    a full connectome is far too dense to read as a hologram, and the
    strongest tracts are what carry the structure.

    :param name: Key from :data:`CONNECTIVITIES`.
    :param percentile: Keep only edges at or above this percentile of the
        non-zero weights (0 keeps every non-zero edge).
    :param tube_radius: Edge tube radius in mm.
    :param node_radius: Radius in mm of the largest region sphere; spheres
        are scaled by weighted degree.
    :param quiet: Suppress download progress output.
    :return: ``(nodes, edges)`` PyVista meshes.  *nodes* carries a
        ``"degree"`` scalar, *edges* carries a ``"weight"`` scalar.
    """
    require_pyvista("connectome_polydata")

    conn = load_connectivity(name, quiet=quiet)
    weights = conn.weights
    centres = conn.centres

    nonzero = weights[weights > 0]
    if nonzero.size == 0:
        raise ValueError(f"Connectome '{name}' has no non-zero weights.")
    threshold = np.percentile(nonzero, percentile) if percentile > 0 else nonzero.min()

    # Upper triangle only -- TVB weight matrices are effectively symmetric for
    # display purposes, and drawing both directions doubles the tube count.
    rows, cols = np.nonzero(np.triu(weights >= threshold, k=1))
    if len(rows) == 0:
        raise ValueError(f"No edges survived the {percentile}th-percentile threshold for '{name}'.")

    lines = np.hstack([np.column_stack([np.full(len(rows), 2), rows, cols])]).ravel()
    edge_net = pv.PolyData(centres, lines=lines)
    edge_net.cell_data["weight"] = weights[rows, cols]
    edges = edge_net.tube(radius=tube_radius)

    degree = conn.degree
    node_cloud = pv.PolyData(centres)
    node_cloud.point_data["degree"] = degree
    scale = node_radius / degree.max() if degree.max() > 0 else node_radius
    # orient=False: the cloud carries no vectors, only the degree scalar.
    nodes = node_cloud.glyph(geom=pv.Sphere(radius=1.0), scale="degree", factor=scale, orient=False)

    return nodes, edges

fetch_archive(*, force=False, verify=True, quiet=False)

Download the TVB data archive to the local cache, if not already there.

The download streams to a temporary file in the cache directory and is only moved into place once complete, so an interrupted transfer can never leave a truncated archive behind.

Parameters:

Name Type Description Default
force bool

Re-download even if a cached copy exists.

False
verify bool

Check the download against :data:TVB_DATA_MD5. A mismatch raises and removes the bad file.

True
quiet bool

Suppress progress output.

False

Returns:

Type Description
Path

Path to the cached tvb_data.zip.

Raises:

Type Description
RuntimeError

If the download fails or the checksum mismatches.

Source code in src/quiltwright/tvb_data.py
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
def fetch_archive(*, force: bool = False, verify: bool = True, quiet: bool = False) -> Path:
    """Download the TVB data archive to the local cache, if not already there.

    The download streams to a temporary file in the cache directory and is
    only moved into place once complete, so an interrupted transfer can
    never leave a truncated archive behind.

    :param force: Re-download even if a cached copy exists.
    :param verify: Check the download against :data:`TVB_DATA_MD5`.  A
        mismatch raises and removes the bad file.
    :param quiet: Suppress progress output.
    :return: Path to the cached ``tvb_data.zip``.
    :raises RuntimeError: If the download fails or the checksum mismatches.
    """
    target = archive_path()

    if target.exists() and not force:
        return target

    if not quiet:
        print(
            f"  Downloading tvb-data {TVB_DATA_VERSION} "
            f"(~{_TVB_DATA_BYTES / 1e6:.0f} MB, one time)\n"
            f"    from {TVB_DATA_URL}\n"
            f"    to   {target}",
            flush=True,
        )

    tmp_fd, tmp_name = tempfile.mkstemp(dir=str(target.parent), suffix=".part")
    os.close(tmp_fd)
    tmp_path = Path(tmp_name)

    try:
        with urllib.request.urlopen(TVB_DATA_URL) as response:  # noqa: S310  # fixed https URL
            declared = int(response.headers.get("Content-Length") or 0)
            total = declared or _TVB_DATA_BYTES
            seen = 0
            with open(tmp_path, "wb") as out:
                while True:
                    block = response.read(1 << 20)
                    if not block:
                        break
                    out.write(block)
                    seen += len(block)
                    if not quiet:
                        _report_progress(seen, total)
        if not quiet and sys.stderr.isatty():
            sys.stderr.write("\n")
            sys.stderr.flush()
    except Exception as exc:  # noqa: BLE001  # re-raised with actionable context
        tmp_path.unlink(missing_ok=True)
        raise RuntimeError(
            f"Failed to download tvb-data from {TVB_DATA_URL}: {exc}\n"
            f"You can download it manually and place it at {target}"
        ) from exc

    if verify:
        actual = _md5(tmp_path)
        if actual != TVB_DATA_MD5:
            tmp_path.unlink(missing_ok=True)
            raise RuntimeError(
                f"Checksum mismatch for tvb_data.zip: expected {TVB_DATA_MD5}, got {actual}.\n"
                "The download may be corrupt or the Zenodo record changed."
            )

    shutil.move(str(tmp_path), str(target))
    if not quiet:
        print(f"  Cached {target} ({target.stat().st_size / 1e6:.0f} MB)", flush=True)
    return target

load_connectivity(name, *, quiet=False)

Load a TVB region-level connectome.

Parameters:

Name Type Description Default
name str

Key from :data:CONNECTIVITIES, e.g. "connectivity_76".

required
quiet bool

Suppress download progress output.

False

Returns:

Type Description
Connectome

A :class:Connectome with weights, tract lengths, region centres and region labels.

Source code in src/quiltwright/tvb_data.py
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
def load_connectivity(name: str, *, quiet: bool = False) -> Connectome:
    """Load a TVB region-level connectome.

    :param name: Key from :data:`CONNECTIVITIES`, e.g. ``"connectivity_76"``.
    :param quiet: Suppress download progress output.
    :return: A :class:`Connectome` with weights, tract lengths, region
        centres and region labels.
    """
    member = _resolve(name, CONNECTIVITIES, "connectivity")
    raw = _read_member(member, quiet=quiet)

    with zipfile.ZipFile(io.BytesIO(raw)) as inner:
        read, by_base = _member_reader(inner)
        missing = {"weights.txt", "centres.txt"} - by_base
        if missing:
            raise ValueError(
                f"Connectome '{name}' is missing {sorted(missing)}; found {sorted(by_base)}"
            )
        weights = _loadtxt(read("weights.txt"))
        tract_lengths = (
            _loadtxt(read("tract_lengths.txt"))
            if "tract_lengths.txt" in by_base
            else np.zeros_like(weights)
        )
        centres_raw = read("centres.txt").decode("utf-8")

    labels: list[str] = []
    coords: list[list[float]] = []
    for line in centres_raw.splitlines():
        parts = line.split()
        if len(parts) < 4:
            continue
        labels.append(parts[0])
        coords.append([float(v) for v in parts[1:4]])

    return Connectome(
        weights=weights,
        tract_lengths=tract_lengths,
        centres=np.asarray(coords, dtype=float),
        labels=np.asarray(labels, dtype=object),
    )

load_region_mapping(name, *, quiet=False)

Load per-vertex parcellation labels for a surface.

Parameters:

Name Type Description Default
name str

Key from :data:REGION_MAPPINGS, e.g. "regionMapping_16k_76".

required
quiet bool

Suppress download progress output.

False

Returns:

Type Description
ndarray

(n_vertices,) integer region indices.

Source code in src/quiltwright/tvb_data.py
637
638
639
640
641
642
643
644
645
646
def load_region_mapping(name: str, *, quiet: bool = False) -> np.ndarray:
    """Load per-vertex parcellation labels for a surface.

    :param name: Key from :data:`REGION_MAPPINGS`, e.g.
        ``"regionMapping_16k_76"``.
    :param quiet: Suppress download progress output.
    :return: ``(n_vertices,)`` integer region indices.
    """
    member = _resolve(name, REGION_MAPPINGS, "region mapping")
    return _loadtxt(_read_member(member, quiet=quiet)).astype(np.int64)

load_sensors(name, *, quiet=False)

Load electrode / sensor positions.

Parameters:

Name Type Description Default
name str

Key from :data:SENSORS, e.g. "eeg_63".

required
quiet bool

Suppress download progress output.

False

Returns:

Type Description
tuple[ndarray, ndarray]

(labels, positions) with (n,) names and (n, 3) millimetre coordinates.

Source code in src/quiltwright/tvb_data.py
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
def load_sensors(name: str, *, quiet: bool = False) -> tuple[np.ndarray, np.ndarray]:
    """Load electrode / sensor positions.

    :param name: Key from :data:`SENSORS`, e.g. ``"eeg_63"``.
    :param quiet: Suppress download progress output.
    :return: ``(labels, positions)`` with ``(n,)`` names and ``(n, 3)``
        millimetre coordinates.
    """
    member = _resolve(name, SENSORS, "sensor set")
    text = _read_member(member, quiet=quiet).decode("utf-8")

    labels: list[str] = []
    coords: list[list[float]] = []
    for line in text.splitlines():
        parts = line.split()
        if len(parts) < 4:
            continue
        labels.append(parts[0])
        coords.append([float(v) for v in parts[1:4]])

    return np.asarray(labels, dtype=object), np.asarray(coords, dtype=float)

load_surface(name, *, quiet=False)

Load a triangulated TVB surface.

Parameters:

Name Type Description Default
name str

Key from :data:SURFACES, e.g. "cortex_16384".

required
quiet bool

Suppress download progress output.

False

Returns:

Type Description
tuple[ndarray, ndarray, ndarray | None]

(vertices, triangles, normals) where vertices is (n, 3) float mm coordinates, triangles is (m, 3) int vertex indices, and normals is (n, 3) or None.

Source code in src/quiltwright/tvb_data.py
578
579
580
581
582
583
584
585
586
587
588
589
590
591
def load_surface(
    name: str, *, quiet: bool = False
) -> tuple[np.ndarray, np.ndarray, np.ndarray | None]:
    """Load a triangulated TVB surface.

    :param name: Key from :data:`SURFACES`, e.g. ``"cortex_16384"``.
    :param quiet: Suppress download progress output.
    :return: ``(vertices, triangles, normals)`` where *vertices* is
        ``(n, 3)`` float mm coordinates, *triangles* is ``(m, 3)`` int
        vertex indices, and *normals* is ``(n, 3)`` or ``None``.
    """
    member = _resolve(name, SURFACES, "surface")
    surface = _parse_surface_zip(_read_member(member, quiet=quiet))
    return surface.vertices, surface.triangles, surface.normals

surface_polydata(name, *, region_mapping=None, smooth_iters=0, decimate=0.0, quiet=False)

Build a :class:pyvista.PolyData mesh from a TVB surface.

Parameters:

Name Type Description Default
name str

Key from :data:SURFACES.

required
region_mapping str | None

Optional key from :data:REGION_MAPPINGS. When given, the labels are attached as a "region" point scalar so the mesh can be coloured by parcellation. The vertex counts must match.

None
smooth_iters int

Laplacian smoothing iterations (0 disables). Applied after the region scalars are attached, so labels survive.

0
decimate float

Fraction of triangles to remove, 0.0-1.0. Useful for cortex_2x120k (566 752 triangles), which is heavy to sweep across 48 quilt views.

0.0
quiet bool

Suppress download progress output.

False

Returns:

Type Description

The surface as a PyVista mesh.

Raises:

Type Description
ValueError

If region_mapping length does not match the mesh.

Source code in src/quiltwright/tvb_data.py
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
def surface_polydata(
    name: str,
    *,
    region_mapping: str | None = None,
    smooth_iters: int = 0,
    decimate: float = 0.0,
    quiet: bool = False,
):
    """Build a :class:`pyvista.PolyData` mesh from a TVB surface.

    :param name: Key from :data:`SURFACES`.
    :param region_mapping: Optional key from :data:`REGION_MAPPINGS`.  When
        given, the labels are attached as a ``"region"`` point scalar so the
        mesh can be coloured by parcellation.  The vertex counts must match.
    :param smooth_iters: Laplacian smoothing iterations (0 disables).
        Applied *after* the region scalars are attached, so labels survive.
    :param decimate: Fraction of triangles to remove, 0.0-1.0.  Useful for
        ``cortex_2x120k`` (566 752 triangles), which is heavy to sweep
        across 48 quilt views.
    :param quiet: Suppress download progress output.
    :return: The surface as a PyVista mesh.
    :raises ValueError: If *region_mapping* length does not match the mesh.
    """
    require_pyvista("surface_polydata")

    vertices, triangles, normals = load_surface(name, quiet=quiet)
    mesh = pv.PolyData(vertices, _faces_array(triangles))

    if normals is not None and len(normals) == mesh.n_points:
        mesh.point_data["Normals"] = normals

    if region_mapping is not None:
        labels = load_region_mapping(region_mapping, quiet=quiet)
        if len(labels) != mesh.n_points:
            raise ValueError(
                f"Region mapping '{region_mapping}' has {len(labels)} labels but "
                f"surface '{name}' has {mesh.n_points} vertices -- they do not pair."
            )
        mesh.point_data["region"] = labels

    if decimate > 0.0:
        reduced = mesh.decimate(decimate)
        # Quadric decimation builds new vertices and discards point data, so
        # parcellation labels have to be carried across explicitly.  Nearest
        # neighbour is the right rule here: the labels are categorical, and
        # interpolating between region 3 and region 70 is meaningless.
        if region_mapping is not None:
            reduced.point_data["region"] = _nearest_labels(
                mesh.points, mesh.point_data["region"], reduced.points
            )
        mesh = reduced

    if smooth_iters > 0:
        mesh = mesh.smooth(n_iter=smooth_iters, relaxation_factor=0.1)

    return mesh