Point Tree Visualization#

This notebook focuses on the runtime point graph used by multi-compartment cells.

A NodeTree is built after Cell.init_state() and exposes the execution-oriented node-edge topology used by the runtime.

In this notebook we will:

  1. build a Cell and inspect its NodeTree

  2. render node / CV / branch topology views from Cell

  3. compare layouts, coverage modes, and runtime value colouring

  4. summarize the level capabilities and most useful parameters

import os
from collections import deque

os.environ.setdefault("JAX_PLATFORMS", "cpu")

import brainunit as u
import braincell.mech as mech
import matplotlib.pyplot as plt
import numpy as np

from braincell import Cell, Morphology, MaxCVLen
from braincell.filter import BranchInFilter, BranchSlice, RootLocation, Terminals

1. Build a NodeTree#

We start from a morphology, choose a CV policy that creates a reasonably dense discretization, initialize the cell, and retrieve the runtime NodeTree.

morpho = Morphology.from_asc("../../data/morphology/Cerebellum_morph/PC.asc")
# morpho.vis3d(jupyter_backend="html")
cell = Cell(
    morpho,
    cv_policy=MaxCVLen(25.0 * u.um),
)
cell.init_state()

node_tree = cell.node_tree

print(cell)
print(node_tree)
print("n_nodes:", len(node_tree.nodes))
print("n_edges:", len(node_tree.edges))
print("root_node_id:", node_tree.root_node_id)
Warning: no DISPLAY environment variable.
--No graphics will be displayed.
/home/swl/braincell/braincell/io/asc/reader.py:584: UserWarning: from_points() produced 1 zero-length segment(s) from coincident consecutive points at index pair(s) [6]. These degenerate segments are kept but contribute zero volume.
  return branch_class_for_type(segment.branch_type).from_points(
/home/swl/braincell/braincell/io/asc/reader.py:584: UserWarning: from_points() produced 1 zero-length segment(s) from coincident consecutive points at index pair(s) [8]. These degenerate segments are kept but contribute zero volume.
  return branch_class_for_type(segment.branch_type).from_points(
/home/swl/braincell/braincell/io/asc/reader.py:584: UserWarning: from_points() produced 1 zero-length segment(s) from coincident consecutive points at index pair(s) [4]. These degenerate segments are kept but contribute zero volume.
  return branch_class_for_type(segment.branch_type).from_points(
/home/swl/braincell/braincell/io/asc/reader.py:584: UserWarning: from_points() produced 1 zero-length segment(s) from coincident consecutive points at index pair(s) [5]. These degenerate segments are kept but contribute zero volume.
  return branch_class_for_type(segment.branch_type).from_points(
Cell(root='soma', n_cv=483, n_point=945, initialized=True)
-----------------------------------
n_nodes        | 945
n_edges        | 944
root_node_id   | 0
-----------------------------------

n_nodes: 945
n_edges: 944
root_node_id: 0

2. Default node-level topology view#

The main user-facing entry point is now the cell-level API:

  • cell.vis_node(...)

  • or the thin dispatcher cell.vis_topology(level="node", ...)

The default preset is "dendrotweaks", which uses a topology-only layout and ignores physical length and radius.

fig, ax = plt.subplots(figsize=(7, 7))
cell.vis_node(ax=ax, show=False)
ax.set_title("Node topology (default preset)")
plt.show()
../_images/37f64f97c5042c6ae4a5a178c113d6a66798e9a65816718429c21b8540768a29.png

3. Compare presets and layouts#

Available presets in this build:

  • dendrotweaks

  • mono

  • depth

Available layouts in this build:

  • twopi

  • dot

  • neato

  • kamada_kawai

fig, axes = plt.subplots(2, 3, figsize=(14, 10))

cell.vis_topology(level="node", preset="dendrotweaks", ax=axes[0, 0], show=False)
axes[0, 0].set_title("preset='dendrotweaks'")

cell.vis_topology(level="node", preset="mono", ax=axes[0, 1], show=False)
axes[0, 1].set_title("preset='mono'")

cell.vis_topology(level="node", preset="depth", ax=axes[0, 2], show=False)
axes[0, 2].set_title("preset='depth'")

cell.vis_topology(level="node", layout="twopi", ax=axes[1, 0], show=False)
axes[1, 0].set_title("layout='twopi'")

cell.vis_topology(level="node", layout="dot", ax=axes[1, 1], show=False)
axes[1, 1].set_title("layout='dot'")

cell.vis_topology(level="node", layout="kamada_kawai", ax=axes[1, 2], show=False)
axes[1, 2].set_title("layout='kamada_kawai'")

plt.tight_layout()
plt.show()
../_images/ed249342405f7cb6d69fd379887bb8e373f8e30c3f7fa66311aad8233daa666f.png

Global spacing with layout_scale#

layout_scale changes the overall point spacing for the resolved layout.

  • smaller than 1.0: tighter graph

  • larger than 1.0: more spread out graph

fig, axes = plt.subplots(1, 3, figsize=(15, 5))

cell.vis_node(layout="kamada_kawai", layout_scale=0.6, ax=axes[0], show=False)
axes[0].set_title("layout_scale=0.6")

cell.vis_node(layout="kamada_kawai", layout_scale=1.0, ax=axes[1], show=False)
axes[1].set_title("layout_scale=1.0")

cell.vis_node(preset="depth", layout="kamada_kawai", layout_scale=3.0, ax=axes[2], show=False)
axes[2].set_title("layout_scale=3.0")

plt.tight_layout()
plt.show()
../_images/7c35477cc9be8e580173600bd2b900763c4e78871fa1ddd2d798321a7352b65e.png
fig, ax = plt.subplots(1, 1, figsize=(10, 10), dpi=300)  # dpi=300 for high resolution

cell.vis_node(preset="depth", layout="kamada_kawai", layout_scale=2.0, ax=ax, show=False)
ax.set_title("layout_scale=2.0")  

plt.tight_layout()
plt.show()
../_images/57e1b3b8f2d6e171655b99c14e3a8362383b0ae62fda80598a2661c393c68d97.png

4. Color by custom point values#

For low-level value mode, pass one scalar per point.

Below we compute a simple point-depth array from the root and use it as the colour source.

def node_depths(node_tree):
    depths = np.full(len(node_tree.nodes), np.nan, dtype=float)
    depths[node_tree.root_node_id] = 0.0
    children_by_node_id = {node.id: [] for node in node_tree.nodes}
    for edge in node_tree.edges:
        children_by_node_id[edge.parent_node_id].append(edge.child_node_id)
    queue = deque([node_tree.root_node_id])
    while queue:
        node_id = queue.popleft()
        for child_id in children_by_node_id[node_id]:
            depths[child_id] = depths[node_id] + 1.0
            queue.append(child_id)
    return depths


depth_values = node_depths(node_tree)
print("depth_values shape:", depth_values.shape)
print("max depth:", int(np.nanmax(depth_values)))

fig, axes = plt.subplots(1, 2, figsize=(12, 5))

cell.vis_node(
    value=depth_values,
    cmap="magma",
    ax=axes[0],
    show=False,
)
axes[0].set_title("Custom values: node depth")

cell.vis_node(
    layout="kamada_kawai",
    layout_scale=3.0,
    node_color="#2563eb",
    edge_color="#94a3b8",
    root_color="#ef4444",
    ax=axes[1],
    show=False,
)
axes[1].set_title("Manual low-level colours")

plt.tight_layout()
plt.show()
depth_values shape: (945,)
max depth: 51
../_images/84d81253d0ef5bbf6ae46ac8ded93425fdf77010c3e23513daf2151fe8350327.png

5. Cell-aware highlighting and runtime values#

Node-level topology is the most detailed cell-facing view. It can:

  • highlight points selected by a region

  • highlight points selected by a locset

  • colour by runtime voltage (value="V")

  • colour by mechanism fields such as ("channel", "IL", "g_max")

Two important v1 rules:

  • highlight mode and value mode are mutually exclusive

  • region / locset are mapped to CV midpoint points only

cell.reset()
cell.paint(
    cell.paint_rules[0].region,
    mech.Channel("IL", g_max=0.1 * (u.mS / u.cm ** 2), E=-70.0 * u.mV),
)
cell.init_state()
fig, axes = plt.subplots(2, 2, figsize=(12, 10))

cell.vis_node(
    region=BranchInFilter("type", "basal_dendrite"),
    ax=axes[0, 0],
    show=False,
)
axes[0, 0].set_title("Highlight by region")

cell.vis_node(
    locset=Terminals(),
    ax=axes[0, 1],
    show=False,
)
axes[0, 1].set_title("Highlight by locset")

cell.vis_node(
    value="V",
    cmap="viridis",
    ax=axes[1, 0],
    show=False,
)
axes[1, 0].set_title("Runtime voltage")

cell.vis_node(
    value=("channel", "IL", "g_max"),
    cmap="magma",
    ax=axes[1, 1],
    show=False,
)
axes[1, 1].set_title("Channel g_max")

plt.tight_layout()
plt.show()
../_images/1d781f162394a3e8dce10c4cb6055a587da982cdef74ec5e28ab5e7c0dbc73be.png

6. Parameter quick reference#

cell.vis_node(...)#

  • highlight mode: region, locset, coverage_mode, highlight_color

  • value mode: value, cmap, vmin, vmax, norm, value_label, show_colorbar

  • style: node_color, edge_color, root_color

  • layout controls: preset, layout, layout_scale

cell.vis_cv(...) / cell.vis_branch(...)#

  • cell.vis_cv(...): same high-level API surface as cell.vis_node(...)

  • cell.vis_branch(...): region-only, no locset, no value colormap

  • region coverage: region, coverage_mode, highlight_color

  • style: node_color, edge_color, root_color

  • layout controls: preset, layout, layout_scale

Capability matrix#

Level

Region

Locset

Value

Coverage mode

node

yes

yes

yes

yes

cv

yes

yes

yes

yes

branch

yes

no

no

yes

Highlight colour examples#

Use highlight_color to change the overlay colour in highlight mode.

fig, axes = plt.subplots(1, 2, figsize=(10, 4))

cell.vis_node(
    region=BranchSlice(branch_index=1, prox=0.0, dist=1.0),
    highlight_color="#22c55e",
    ax=axes[0],
    show=False,
)
axes[0].set_title("highlight_color='#22c55e'")

cell.vis_node(
    locset=RootLocation(0.5),
    highlight_color="#f97316",
    ax=axes[1],
    show=False,
)
axes[1].set_title("highlight_color='#f97316'")

plt.tight_layout()
plt.show()
../_images/3abd2c83495915b00694cd8e739465b162cb606b265db44f6a137784d7330d16.png

Colormap and colorbar examples#

Use cmap, vmin, vmax, value_label, and show_colorbar in value mode.

fig, axes = plt.subplots(2, 2, figsize=(12, 10))

cell.vis_node(
    value="V",
    cmap="plasma",
    ax=axes[0, 0],
    show=False,
)
axes[0, 0].set_title("cmap='plasma'")

cell.vis_node(
    value="V",
    cmap="viridis",
    vmin=-80.0,
    vmax=-40.0,
    value_label="Voltage",
    ax=axes[0, 1],
    show=False,
)
axes[0, 1].set_title("vmin / vmax + value_label")

cell.vis_node(
    value=("channel", "IL", "g_max"),
    cmap="magma",
    value_label="IL g_max",
    ax=axes[1, 0],
    show=False,
)
axes[1, 0].set_title("mechanism field + colorbar")

cell.vis_node(
    value=("channel", "IL", "g_max"),
    cmap="magma",
    show_colorbar=False,
    ax=axes[1, 1],
    show=False,
)
axes[1, 1].set_title("show_colorbar=False")

plt.tight_layout()
plt.show()
../_images/57e6c65567c71c880606c4d3521004eb6013f630b84bd12037351a0a9572a1f8.png

Low-level node / edge / root colours#

These style controls are shared by the cell-level topology views.

fig, ax = plt.subplots(figsize=(7, 7))
cell.vis_node(
    layout="kamada_kawai",
    node_color="#0f172a",
    edge_color="#94a3b8",
    root_color="#dc2626",
    ax=ax,
    show=False,
)
ax.set_title("node_color / edge_color / root_color")
plt.show()
../_images/6ff76dccd4b3f0b074b8a95ebc991562250f1ffaf15136ab6b7911a3cee75f88.png

7. CV-level topology view#

You can also collapse the runtime graph upward and show one node per CV.

This view now shares the same high-level API surface as cell.vis_node(...), but it is often most useful as a pure topology + coverage view.

Coverage is still controlled with:

  • coverage_mode="fraction": blend by coverage fraction

  • coverage_mode="any": any overlap gives full highlight

  • coverage_mode="all": only full coverage highlights

fig, axes = plt.subplots(1, 3, figsize=(15, 4))

region = BranchInFilter("type", "basal_dendrite")

cell.vis_cv(region=region, coverage_mode="fraction", ax=axes[0], show=False)
axes[0].set_title("vis_cv: fraction")

cell.vis_cv(region=region, coverage_mode="any", ax=axes[1], show=False)
axes[1].set_title("vis_cv: any")

cell.vis_cv(region=region, coverage_mode="all", ax=axes[2], show=False)
axes[2].set_title("vis_cv: all")

plt.tight_layout()
plt.show()
../_images/2d812340f1e1133eb90b2ea5f592ca7f78a601b5bbc176d326e24dc8c3060bee.png

8. Branch-level topology view#

At a higher level, you can show one node per branch.

This remains topology-only and region-only.

fig, axes = plt.subplots(1, 2, figsize=(12, 4))

cell.vis_topology(
    level="branch",
    region=BranchSlice(branch_index=1, prox=0.25, dist=0.75),
    coverage_mode="fraction",
    ax=axes[0],
    show=False,
)
axes[0].set_title("vis_branch: fraction")

cell.vis_topology(
    level="branch",
    region=BranchSlice(branch_index=1, prox=0.0, dist=1.0),
    coverage_mode="all",
    highlight_color="#22c55e",
    ax=axes[1],
    show=False,
)
axes[1].set_title("vis_branch: all")

plt.tight_layout()
plt.show()
../_images/7c9611582833c135b4af76e02ab36ed6bf06a77acffb08272f3cd05cea95067b.png