Skeletons#

Often in working with neurons, you want to measure things along a linear dimension of a neuron. Length of axon, for example. Or number of branches. However, the EM segmentation is a complex 3d shape that makes this non-trivial. There are methods for reducing the shape of a segmented neuron down to a linear tree-like structure usually referred to as a skeleton. We have precalculated skeletons for a large number of cells in the MICrONS dataset, including the proofread cells with the most complete axons.

There are several formats of EM skeleton that you may encounter in this workshop and tutorial material. See the subsections below for more about them. As a summary:

  • swc skeleton : files end in .swc

  • meshwork skeleton : objects saved in h5 file format

  • precomputed skeleton : a binary file for each skeleton, within a directory with metadata ‘info’ files on how to render them

All of these formats are composed of a set of vertices and edges which form the graph of the neuron’s branching structure. As well as information about the compartment type of the vertex, indicating if it is axon, dendrite, soma etc. And the radius of the branch, measured as half the cable thickness in micrometers.

However, the different formats may have different metadata attached to each vertex. Meshwork skeleton objects may include annotations about the input and output synapses, and provide functions to operate on the graph of the skeleton. Precomputed format may include arbitrary vertex properties that summarize connectivity information (for EM neurons) and CCF brain area (for exaSPIM neurons; see the section on Single Cell Morphology for more)

Note

Any format of skeleton can be converted into a Meshwork skeleton object to make use of the graph functions.

How EM skeletons are generated#

../../_images/skeleton-cartoon.png

Fig. 25 Cartoon illustration of “level 2” graph and skeletons.#

In order to understand how these skeletons have been made, you have to understand how large scale EM data is represented. Portions of 3d space are broken up into chunks, such as the grid in the image above. Neurons, such as the cartoon green cell above, span many chunks. Components of the segmentation that live within a single chunk are called level 2 ids, this is because in fact the chunks get iteratively combined into larger chunks, until the chunks span the whole volume. We call this the PyChunkedGraph or PCG, after the library which we use to store and interact with this representation. Level 0 is the voxels, level 1 refers to the grouping of voxels within the chunk (also known as supervoxels) and level 2 are the groups of supervoxels within a chunk. A segmentation result can be thought of as a graph at any of these levels, where the voxels, supervoxels, or level 2 ids that are part of the same object are connected. In the above diagram, the PCG level 2 graph is represented as the light red lines.

Such graphs are useful in that they track all the parts of the neuron that are connected to one another, but they aren’t skeletons, because the graph is not directed, and isn’t a simple branching structure.

In order to turn them into a skeleton we have to run an algorithm, that finds a tree like structure that covers the graph and gets close to every node. This is called the TEASAR algorithm and you can read more about how to skeletonize graphs in the MeshParty documentation.

The result of the algorithm, when applied to a level 2 graph, is a linear tree (like the dotted purple one shown above), where a subset of the level 2 chunks are vertices in that tree, but all the unused level 2 nodes “near” a vertex (red unfilled circles) on the graph are mapped to one of the skeleton vertices (black arrows). This means there are two sets of indices, mesh indices, and skeleton indices, which have a mapping between them (see diagram above).

The MeshParty library allows us to easily store these representations and helps us relate them to each other. A Meshwork object is a data structure that is designed to have three main components that are kept in sync with mesh and skeleton indices:

  • mesh: the graph of PCG level2 ID nodes that are skeletonized, stored as a standard meshparty mesh. Note that this is not the high resolution mesh that you see in neuroglancer. See EM Meshes for more

  • skeleton: a meshwork skeleton, derived from the simplification of the mesh

  • anno : is a class that holds dataframes and adds some extra info to keep track of indexing.

Working with Meshwork Objects#

Skeletons are “tree-like”, where every vertex (except the root vertex) has a single parent that is closer to the root than it, and any number of child vertices. Because of this, for a skeleton there are well-defined directions “away from root” and “towards root” and few types of vertices have special names:

  • Branch point: vertices with two or more children, where a neuronal process splits.

  • End point: vertices with no children, where a neuronal process ends.

  • Root point: The one vertex with no parent node. By convention, we typically set the root vertex at the cell body, so these are equivalent to “away from soma” and “towards soma”.

  • Segment: A collection of vertices along an unbranched region, between one branch point and the next end point or branch point downstream.

Skeleton Properties and Methods#

To plot this skeleton in a more sophisticated way, you have to start thinking of it as a graph, and the meshwork object has a bunch of tools and properties to help you utilize the skeleton graph.

Let’s list some of the most useful ones below You access each of these with nrn.skeleton.* Use the ? to read more details about each one

Properties

  • branch_points: a list of skeleton vertices which are branches

  • root: the skeleton vertex which is the soma

  • distance_to_root: an array the length of vertices which tells you how far away from the root each vertex is

  • root_position: the position of the root node in nanometers

  • end_points: the tips of the neuron

  • cover_paths: a list of arrays containing vertex indices that describe individual paths that in total cover the neuron without repeating a vertex. Each path starts at an end point and continues toward root, stopping once it gets to a vertex already listed in a previously defined path. Paths are ordered to start with the end points farthest from root first. Each skeleton vertex appears in exactly one cover path.

  • csgraph: a scipy.sparse.csr.csr_matrix containing a graph representation of the skeleton. Useful to do more advanced graph operations and algorithms. https://docs.scipy.org/doc/scipy/reference/sparse.csgraph.html

  • kdtree: a scipy.spatial.ckdtree.cKDTree containing the vertices of skeleton as a kdtree. Useful for quickly finding points that are nearby. https://docs.scipy.org/doc/scipy/reference/generated/scipy.spatial.cKDTree.html

Methods

  • path_length(paths=None): the path length of the whole neuron if no arguments, or pass a list of paths to get the path length of that. A path is just a list of vertices which are connected by edges.

  • path_to_root(vertex_index): returns the path to the root from the passed vertex

  • path_between(source_index, target_index): the shortest path between the source vertex index and the target vertex index

  • child_nodes(vertex_indices): a list of arrays listing the children of the vertex indices passed in

  • parent_nodes(vertex_indices): an array listing the parent of the vertex indices passed in

Example Data Access#

Load precomputed skeleton (v1078)#

A precomputed skeleton is a simplified representation of a neuron where its shape is captured by a tree-like structure that passes through the center of the neuron. More specificy, a precomputed skeleton stores a neuron as a graph with vertices and edges in addition to a collection of vertex-attached attributes that capture morphological and anatomical information about the neuron.

The most recent MICrONS skeletons are those from data release version 1078, which includes skeletonization of all neurons with proofread axons. The precomputed format is flexible and scalable, and also can be rendered in Neuroglancer.

Note

SWDB 2024 relies on this format when working with the EM and exaSPIM skeletons. The EM skeletons will be available as part of the course’s resources, but also in the public repository below.

# cloudpath to the precomputed EM files
cv_skel_path = "precomputed://gs://allen_neuroglancer_ccf/em_minnie65_v1078"
import cloudvolume
import numpy as np

# Initialize cloud volume
cv_obj = cloudvolume.CloudVolume(cv_skel_path, use_https = True) 

# Example skeleton (as of v1078)
skeleton_id = 864691135591041291

# load precomputed neuron skeleton
cv_sk = cv_obj.skeleton.get(skeleton_id)
print("This is a precomputed skeleton of an EM neuron...\n")
print(cv_sk)
This is a precomputed skeleton of an EM neuron...

Skeleton(segid=864691135591041291, vertices=(shape=9154, float32), edges=(shape=9153, uint32), radius=(9154, float32), compartment=(9154, float32), presyn_counts=(9154, float32), postsyn_counts=(9154, float32), presyn_size=(9154, float32), postsyn_size=(9154, float32), space='physical' transform=[[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0]])

We will use MeshParty’s skeleton object to help plot and analyze the precomputed skeletons. To do this, convert the vertices, edges, and radius of the precomputed format with the skeleton module.

from meshparty import skeleton

sk = skeleton.Skeleton(cv_sk.vertices, 
                       cv_sk.edges, 
                       vertex_properties={'radius': cv_sk.radius,
                                          'compartment': cv_sk.compartment},  
                       root = len(cv_sk.edges), # set root as final entry
                       remove_zero_length_edges = False)

Plot Skeleton in 2D#

The package Skeleton-plot provides some handy utilities for plotting meshwork skeletons, including:

  • specifying the 2D orientation

  • annotating somas

  • labeling compartments by color

import skeleton_plot as skelplot
import matplotlib.pyplot as plt
%matplotlib inline

f, ax = plt.subplots(figsize=(7, 10))
skelplot.plot_tools.plot_skel(
    sk,
    title="Neuron with radius and compartments",
    line_width=1,
    plot_soma=True,
    soma_size = 150,
    pull_radius=True,
    invert_y=True,
    pull_compartment_colors=True,
    x="x",
    y="y",
    skel_color_map = { 1: "olive", 2: "black", 3: "firebrick",4: "salmon", },
    ax=ax,
)

ax.spines['right'].set_visible(False) 
ax.spines['left'].set_visible(False) 
ax.spines['top'].set_visible(False) 
ax.spines['bottom'].set_visible(False)
ax.axis('off')
(7726840.5, 8438740.0, 1497648.0, 675351.375)
../../_images/6b6520e2eaccee4cdbeec5490543002e2d53569b40baf9b0c6da3ad5b3bafd4b.png

Vertices and Neighborhoods#

# Vertices
print(f"There are {len(sk.vertices)} vertices in this neuron", "\n")

# Branch points
print("Branch points are at the following vertices: \n",sk.branch_points, "\n")

# End points
print("End points are at the following vertices: \n",sk.end_points, "\n")

# Root - the point associated with the root node, which is the soma
print("Root is vertex: ", sk.root.item(), "\n")

# Select downstream nodes from one branch point
downstream_nodes = sk.downstream_nodes(sk.branch_points[3])
print("Nodes downstream of selected brach are: \n", downstream_nodes, "\n")
There are 9154 vertices in this neuron 

Branch points are at the following vertices: 
 [ 522  738 1115 1256 1263 1428 1493 1523 1535 1540 1632 1866 1878 1902
 1935 1951 1996 2030 2043 2074 2104 2170 2190 2243 2262 2271 2294 2304
 2384 2425 2445 2449 2484 2611 2616 2653 2687 2736 2755 2760 2775 2848
 2867 2972 3049 3057 3110 3176 3226 3302 3340 3386 3413 3423 3453 3513
 3516 3550 3558 3622 3638 3665 3688 3703 3721 3823 3972 4016 4024 4065
 4068 4096 4113 4161 4225 4285 4302 4366 4390 4466 4543 4718 4921 4959
 4985 5161 5176 5509 5518 5727 5746 5925 6800 7798 7858 7920 8463 8492
 8614 8632 8645 8755 8903 8933 9026 9057 9082 9153] 

End points are at the following vertices: 
 [   5   29   31   68   86  170  172  177  218  245  286  291  323  335
  366  392  419  470  516  564  638  647  881  934  950 1286 1347 1351
 1544 1562 1661 1662 1672 1682 1743 1778 1781 1811 2008 2042 2072 2096
 2168 2259 2367 2370 2373 2470 2562 2622 2623 2638 2682 2756 2757 2842
 2882 2937 3030 3073 3130 3150 3193 3296 3395 3519 3720 3864 4051 4394
 4477 4502 4611 4679 4740 4771 4776 4813 4887 5131 5198 5547 5551 5591
 5597 5618 5663 5685 5823 5870 5891 6128 6146 6171 6181 6268 6314 6352
 6400 6426 6445 6546 6709 6780 6848 6874 6967 7028 7070 7093 7289 7531
 7822 8064 8252 8266 8323 8490 8497 8745 8861 8872 8931 9012 9014 9042
 9116 9152] 

Root is vertex:  9153 

Nodes downstream of selected brach are: 
 [ 470  471  505  506  507  516  518  549  551  552  559  560  561  605
  606  607  614  615  643  645  646  648  652  694  695  696  701  749
  750  752  758  811  813  814  815  816  818  820  823  824  866  868
  908  909  910  912  948  951  952  953 1012 1014 1015 1076 1078 1079
 1080 1154 1156 1157 1158 1159 1160 1161 1162 1163 1164 1240 1241 1242
 1244 1245 1247 1249 1250 1251 1252 1253 1254 1255 1256 1325 1326 1327
 1328 1329 1331 1333 1334 1335 1336 1395 1396 1397 1399 1402 1460 1461
 1463 1465 1467 1468 1469 1471 1472 1473 1476 1536 1539 1596 1597] 

Paths and Segments#

# Segments - path of nodes between two vertices i and j which are assumed to be a root, branch point, or endpoint.
print("The number of branch segments are: ",len(sk.segments), "\n")

# Path between - path_between() function finds the vertices on the path between two points on the skeleton graph.
nodes_between = sk.path_between(0,150) 
print("Path between the given nodes: ", nodes_between, "\n")

# Path-length - path_length() function calculates the physical distance along the path for a neuron
full_pathlength = sk.path_length() / 1000 # Convert to microns
print("Path-length of the entire neuron: ", full_pathlength, ' um', "\n")

# Path-length for arbitrary segment
example_segment = 11
segment_pathlength = sk.path_length(sk.segments[example_segment]) / 1000 # Convert to microns 
print("Path-length of one segment: ", segment_pathlength, ' um', "\n")
The number of branch segments are:  236 

Path between the given nodes:  [   0    2    7    1    3    8    9   10   11   12   13   14   15   17
   16   18   19   20   21   22   23   24   25   26   27   28   33   34
   38   39   42   45   46   49   52   55   56   60   63   69   75   76
   82   83   93   92  105  114  115  121  126  133  134  139  140  149
  162  163  181  192  205  227  228  247  261  277  295  316  317  346
  369  387  409  410  437  436  438  461  462  496  497  538  539  589
  590  591  592  593  632  675  676  728  798  847  890  930  991 1050
 1126 1214 1298 1373 1441 1509 1576 1622 1674 1675 1673 1755 1754 1844
 1916 2003 2083 2156 2239 2346 2464 2463 2576 2704 2812 2925 2926 3042
 3151 3271 3270 3382 3487 3588 3589 3673 3672 3746 3826 3825 3940 4037
 4038 4138 4230 4229 4312 4390 4391 4313 4314 4231 4139 4140 4039 4040
 3941 3827 3828 3830 3748 3750 3675 3676 3677 3592 3489 3490 3385 3273
 3274 3153 3044 3045 2931 2821 2822 2711 2712 2582 2466 2467 2349 2350
 2241 2159 2085 2086 2006 2007 1918 1847 1757 1758 1677 1678 1625 1579
 1514 1516 1442 1374 1375 1299 1300 1215 1127 1051  992  931  891  848
  799  729  730  677  678  633  594  540  498  463  439  440  413  411
  412  388  371  370  347  348  318  296  279  280  278  262  248  229
  230  206  193  182  165  164  150] 

Path-length of the entire neuron:  20129.62  um 

Path-length of one segment:  26.120412109375  um 

Masking#

One of the most useful methods of the skeleton meshwork’s is the ability to mask or select only parts of the skeleton to work with at a time. The function apply_mask() acts on the meshwork skeleton, and will apply in place if apply_mask(in_place=True).

Warning: be aware when setting a mask in place–mask operations are additive. To reset the mask completely, use reset_mask(in_place=True).

# Let's select a set of nodes from example segment 11
example_segment = 11
selected_nodes = sk.segments[example_segment]
print("Path of the selected nodes is: \n", selected_nodes, "\n")

# reset to make sure you have the full skeleton
sk.reset_mask(in_place=True)

# Apply a mask to these nodes
sk_masked = sk.apply_mask(selected_nodes)

# Check that this masked skeleton matches the pathlength calculated above
print("Path-length of masked segment: ", sk_masked.path_length() / 1000, ' um')
Path of the selected nodes is: 
 [291 290 308 306 304 331 332 358 379 380 401 428 452 484] 

Path-length of masked segment:  26.120412109375  um

Critically, apply_mask() allows us to mask a neuron according to its compartment label: axon, dendrite, soma, etc.

Compartment label conventions (from standardized swc files www.neuromorpho.org )

  • 0 - undefined

  • 1 - soma

  • 2 - axon

  • 3 - (basal) dendrite

  • 4 - apical dendrite

  • 5+ - custom

print("The unique compartment labels in this skeleton are:", np.unique(sk.vertex_properties['compartment']), "\n")

# Select the indices associated with the dendrites
dendrite_inds = (sk.vertex_properties['compartment']==3) | (sk.vertex_properties['compartment']==4)| (sk.vertex_properties['compartment']==1) #soma is included here to connect the dendrite graphs 

# create new skeleton that masks (selects) only the axon
sk_dendrite = sk.apply_mask(dendrite_inds)
print("Dendrite pathlength of all branches is : ", sk_dendrite.path_length() / 1000, ' um', "\n")

# Select the indices associated with the axon, find length
axon_inds = sk.vertex_properties['compartment']==2
sk_axon = sk.apply_mask(axon_inds)
print("Axon pathlength is : ", sk_axon.path_length() / 1000, ' um', "\n")
The unique compartment labels in this skeleton are: [1. 2. 3.] 

Dendrite pathlength of all branches is :  5832.823  um 

Axon pathlength is :  14280.873  um 
# Render all the segments in the dendrite graph

fig, ax = plt.subplots(figsize=(4,3), dpi=150)

for ss in range(len(sk_dendrite.segments)):
    seg = sk_dendrite.segments[ss]

    ax.plot(sk_dendrite.vertices[seg][:,0], 
            sk_dendrite.vertices[seg][:,1], linewidth=0.5)

# add soma marker
ax.plot(sk.vertices[sk.root][0], 
        sk.vertices[sk.root][1], 'oc', markersize=5)
    
ax.invert_yaxis()
ax.axis('off')
(7915166.75, 8225504.25, 986368.134375, 660541.053125)
../../_images/7c464f7366406777ab0801327d2173b30a905868fc0f9e67ecf139e63431b30f.png

Load meshwork h5 file (v661)#

The skeletons of the meshes are calculated at specific timepoints. The most recent collection of all neurons in the dataset was at materialization version 661 (June, 2023).

Both the h5. meshwork neuron object and the .swc skeletons are available in the BossDB repository

# path to the skeleton and meshwork .h5 files
mesh_path = "s3://bossdb-open-data/iarpa_microns/minnie/minnie65/skeletons/v661/meshworks/"
import skeleton_plot.skel_io as skel_io

# Example skeleton (as of v661)
nucleus_id = 230650
segment_id = 864691134940133219

# load a pregenerated neuron skeleton
nrn = skel_io.load_mw(mesh_path, f"{segment_id}_{nucleus_id}.h5")

Annotations#

nrn.anno has a set of annotation tables containing some additional information for analysis. Each annotation table has both a Pandas DataFrame object storing data and additional information that allow the rows of the DataFrame to be mapped to mesh and skeleton vertices. For the neurons that have been pre-computed, there is a consistent set of annotation tables:

  • post_syn: Postsynaptic sites (inputs) for the cell

  • pre_syn: Presynaptic sites (outputs) for the cell

  • is_axon: List of vertices that have been labeled as part of the axon

  • lvl2_ids: Gives the PCG level 2 id for each mesh vertex, largely for book-keeping reasons.

  • segment_properties: For each vertex, information about the approximate radius, surface area, volume, and length of the segment it is on.

  • vol_prop: For every vertex, information about the volume and surface area of the level 2 id it is associated with.

To access one of the DataFrames, use the name of the table as an attribute of the anno object and then get the .df property. For example, to get the postsynaptic sites, we can do:

nrn.anno.post_syn.df.head(3)
ctr_pt_position id post_pt_level2_id post_pt_mesh_ind post_pt_position post_pt_root_id post_pt_supervoxel_id pre_pt_position pre_pt_root_id pre_pt_supervoxel_id size post_pt_mesh_ind_filt
0 [167494, 178677, 22973] 147824610 159893593257935774 6887 [167518, 178672, 22972] 864691134940133219 87835999220528052 [167440, 178598, 22980] 864691135947747425 87765630476372999 3400 6887
1 [138824, 196821, 23152] 97189471 155955348832780498 2332 [138796, 196792, 23151] 864691134940133219 83897754795009976 [138892, 196914, 23159] 864691136785011310 83897823514489547 2012 2332
2 [166258, 197684, 22697] 140170915 159685029646041267 6778 [166246, 197648, 22691] 864691134940133219 87627435608336600 [166200, 197720, 22703] 864691135444655596 87627435608350744 4648 6778

However, in addition to these rows, you can also easily get the mesh vertex index or skeleton vertex index that a row corresponds to with nrn.anno.post_syn.mesh_index or nrn.anno.post_syn.skel_index respectively. This seems small, but it allows you to integrate skeleton-like measurements with annotations trivially.

For example, to get the distance from the cell body for each postsynaptic site, we can do:

nrn.distance_to_root(nrn.anno.post_syn.mesh_index)
array([107741.13371277,  50763.05639648,  89650.54669189, ...,
        53957.32983398,  67804.22151184,  52812.87652588])

Important

Note that the nrn.distance_to_root, like all basic meshwork object functions, expects a mesh vertex index rather than a skeleton vertex index.

A common pattern is to copy a synapse dataframe and then add columns to it. For example, to add the distance to root to the postsynaptic sites, we can do:

syn_df = nrn.anno.pre_syn.df.copy()
syn_df['dist_to_root'] = nrn.distance_to_root(nrn.anno.pre_syn.mesh_index)

Filtering Annotations by Skeleton Arbor#

Each annotation table has a ‘filter_query’ method that takes a boolean mesh mask and returns only those rows of the dataframe associated with those particular locations on the mesh. Let’s use what we learned above in two examples: first, getting all input synapses within 50 microns of the root and second, getting all input synapses on one particular branch off of the soma.

dtr = nrn.distance_to_root() / 1_000   # Convert from nanometers to microns
nrn.anno.post_syn.filter_query( dtr < 50).df
ctr_pt_position id post_pt_level2_id post_pt_mesh_ind post_pt_position post_pt_root_id post_pt_supervoxel_id pre_pt_position pre_pt_root_id pre_pt_supervoxel_id size post_pt_mesh_ind_filt
5 [157726, 193046, 22788] 127341851 158488142519730768 5752 [157706, 193094, 22795] 864691134940133219 86430548482153326 [157682, 192972, 22780] 864691136824031086 86430548482140967 11020 5752
8 [151982, 191342, 23145] 120502078 157713880242454752 4892 [152038, 191390, 23149] 864691134940133219 85656286204696158 [151996, 191298, 23140] 864691135489252804 85656286204685856 6928 4892
22 [150364, 191008, 23443] 112691936 157502705290445181 4655 [150330, 190992, 23435] 864691134940133219 85445111253025608 [150438, 190954, 23442] 864691136827445870 85445111253035274 5252 4655
25 [154448, 194194, 23180] 120218955 158066067560726725 5285 [154378, 194200, 23178] 864691134940133219 86008473522980712 [154520, 194204, 23181] 864691136757444974 86008473522981601 312 5285
29 [154090, 190584, 22953] 120378123 157995217713103485 5202 [154072, 190652, 22947] 864691134940133219 85937623675692998 [154078, 190578, 22960] 864691136713068398 85937623675711336 9116 5202
... ... ... ... ... ... ... ... ... ... ... ... ...
3431 [156296, 195908, 23183] 127450278 158347817415344328 5610 [156264, 195978, 23183] 864691134940133219 86290223377588084 [156252, 195838, 23182] 864691135210513856 86290223377588010 7036 5610
3434 [147054, 185786, 23994] 110381182 157079805697721156 3949 [147026, 185772, 23992] 864691134940133219 85022211660301024 [147124, 185818, 24002] 864691135658301314 85022211660316873 9124 3949
3436 [154928, 196122, 23050] 116358701 158136711182811181 5356 [154940, 196164, 23050] 864691134940133219 86079117144942192 [154934, 196072, 23055] 864691136032031675 86079117144945472 3968 5356
3437 [150724, 186076, 23779] 112536188 157572386906964425 4735 [150760, 185998, 23771] 864691134940133219 85514792869314896 [150720, 186090, 23783] 864691136195284556 85514861588802988 508 4735
3438 [149372, 190040, 23289] 112691418 157361830363136488 4400 [149382, 190088, 23280] 864691134940133219 85304236325525055 [149360, 190010, 23283] 864691136195284556 85304236325524912 2124 4400

473 rows × 12 columns

We can also use one set of annotations as an input to filter query from another set of annotations. For example, due to errors in segmentation or mistakes in synapse detection, there can be synaptic outputs on the dendrite. However, if we have an is_axon annotation that simply contains a collection of vertices that correspond to the cell’s axon. We can use this annotation to create a mask and filter out all of the synapses that are not on the axon.

axon_mask = nrn.anno.is_axon.mesh_mask
nrn.anno.pre_syn.filter_query(~axon_mask).df # The "~" is a logical not operation that flips True and False
ctr_pt_position id post_pt_position post_pt_root_id post_pt_supervoxel_id pre_pt_level2_id pre_pt_mesh_ind pre_pt_position pre_pt_root_id pre_pt_supervoxel_id size pre_pt_mesh_ind_filt
65 [162422, 198658, 23978] 134156279 [162390, 198688, 23981] 864691136785500782 87134991972525815 159192586009969622 6439 [162464, 198636, 23985] 864691134940133219 87134991972537317 972 6439
166 [157086, 198862, 23150] 127450957 [157010, 198908, 23151] 864691135952299043 86361004438641166 158418529756905664 5697 [157164, 198808, 23156] 864691134940133219 86360935719148064 668 5697
444 [174372, 191708, 23157] 155609310 [174306, 191714, 23158] 864691136210173628 88752579668012861 160810104986271912 7366 [174362, 191656, 23156] 864691134940133219 88752510948535116 2888 7366
456 [147822, 191848, 24173] 110894360 [147772, 191806, 24171] 864691135393532914 85093405104959624 157150999142727859 4073 [147868, 191768, 24170] 864691134940133219 85093405104962373 5068 4073
486 [134460, 194490, 22866] 84031640 [134458, 194454, 22867] 864691135528294084 83264161152691689 155321755190165812 1665 [134424, 194532, 22865] 864691134940133219 83264161152685771 4112 1665
490 [175468, 203358, 22627] 158325494 [175478, 203300, 22634] 864691134988925306 88894828917712054 160952422955484181 7444 [175454, 203396, 22626] 864691134940133219 88894828917704472 1308 7444
517 [135018, 192856, 23553] 86645797 [135062, 192872, 23550] 864691136210178236 83334323805717938 155391917910130691 1731 [135004, 192878, 23557] 864691134940133219 83334323872216719 292 1731
604 [148492, 207248, 23239] 112357047 [148486, 207244, 23240] 864691135853648185 85165835299321100 157223429336990100 4211 [148462, 207276, 23241] 864691134940133219 85165835299314732 2144 4211
1017 [156432, 189900, 23033] 125728175 [156470, 189928, 23033] 864691133262391923 86289398677197625 158346992714515502 5599 [156402, 189886, 23034] 864691134940133219 86289398677193502 2652 5599
1067 [128170, 199156, 23519] 71166059 [128102, 199184, 23512] 864691136012791778 82420354765068733 154477948802433940 902 [128160, 199106, 23519] 864691134940133219 82420354765076207 1584 902
1293 [156188, 191520, 22873] 127340911 [156258, 191546, 22880] 864691136210178236 86289604835470780 158276830128767512 5520 [156152, 191638, 22877] 864691134940133219 86219236091245799 1632 5520

As a sanity check, we can use nglui to see if these synapses we have labeled as being on the axon are all where we expect.

from caveclient import CAVEclient
from nglui.statebuilder.helpers import make_synapse_neuroglancer_link

client = CAVEclient('minnie65_public')
client.materialize.version = 661 # version at which skeletons were calculated

make_synapse_neuroglancer_link(
    nrn.anno.pre_syn.filter_query(axon_mask).df,
    client,
    return_as="html"
)

Neuroglancer link

(Click one of the synapse annotations to load the neuron mesh).

Another common example might be to pick one of the child nodes of the soma and get all of the synapses on that branch. We can do this by using the nrn.skeleton.get_child_nodes function to get the skeleton vertex indices of the child nodes and then use that to filter the synapses.

branch_index = 0 # Let's just use the first child vertex of the root node, which is at the soma by default.
branch_inds = nrn.downstream_of(nrn.child_index(nrn.root)[branch_index])
branch_mask = branch_inds.to_mesh_mask

make_synapse_neuroglancer_link(
    nrn.anno.post_syn.filter_query(branch_mask).df,
    client,
    return_as="html"
)

Neuroglancer link

Load swc skeleton (v661)#

The skeletons of the meshes are calculated at specific timepoints. The most recent collection of all neurons in the dataset was at materialization version 661 (June, 2023).

Both the h5. meshwork neuron object and the .swc skeletons are available in the BossDB repository

# path to the skeleton .swc files
skel_path = "s3://bossdb-open-data/iarpa_microns/minnie/minnie65/skeletons/v661/skeletons/"

import skeleton_plot.skel_io as skel_io

# Example skeleton (as of v661)
nucleus_id = 230650
segment_id = 864691134940133219

# load the .swc skeleton
sk = skel_io.read_skeleton(skel_path, f"{segment_id}_{nucleus_id}.swc")

To get a more accurate understanding of the neuron’s morphology, you can pull in information about the radius and compartment labels into your visualization.

Here the axon is colored black, basal dendrites ‘firebrick’ red, apical dendrites ‘salmon’ orange, and the soma a green ‘olive’.

Compartment label conventions (from from standardized swc files www.neuromorpho.org )

  • 0 - undefined

  • 1 - soma

  • 2 - axon

  • 3 - (basal) dendrite

  • 4 - apical dendrite

  • 5+ - custom

f, ax = plt.subplots(figsize=(7, 10))
skelplot.plot_tools.plot_skel(
    sk,
    title=nucleus_id,
    line_width=1,
    plot_soma=True,
    invert_y=True,
    pull_compartment_colors=True,
    x="z",
    y="y",
    skel_color_map = { 3: "firebrick",4: "salmon",2: "black",1: "olive" },
)

ax.spines['right'].set_visible(False) 
ax.spines['left'].set_visible(False) 
ax.spines['top'].set_visible(False) 
ax.spines['bottom'].set_visible(False)