Note

Click here to download the full example code

# Neuron Types#

This tutorial will show you the different neuron types and how to work with them.

Depending your data/workflows, you will use different representations of neurons. If, for example, you work with light-level data you might end up extracting point clouds or neuron skeletons from image stacks. If, on the other hand, you work with segmented EM data, you will typically work with meshes.

To cater for these different representations, neurons in NAVis come in four flavours:

Neuron type | Description | Core data |
---|---|---|

`navis.TreeNeuron` | A hierarchical skeleton consisting of nodes and edges. | - `.nodes` : the SWC node table |

`navis.MeshNeuron` | A mesh with faces and vertices. | - `.vertices` : `(N, 3)` array of x/y/z vertex coordinates- `.faces` : `(M, 3)` array of faces |

`navis.VoxelNeuron` | An image represented by either a 2d array of voxels or a 3d voxel grid. | - `.voxels` : `(N, 3)` array of voxels- `.values` : `(N, )` array of values (i.e. intensity)- `.grid` : `(N, M, K)` 3D voxelgrid |

`navis.Dotprops` | A cloud of points, each with an associated local vector. | - `.points` : `(N, 3)` array of point coordinates- `.vect` : `(N, 3)` array of normalized vectors |

Note that functions in NAVis may only work on a subset of neuron types: check out this table in the API reference for details. If necessary, NAVis can help you convert between the different neuron types (see further below)!

Important

In this guide we introduce the different neuron types using data bundled with NAVis. To learn how to load your own neurons into NAVis please see the tutorials on Import/Export.

## TreeNeurons#

`TreeNeurons`

represent a neuron as a tree-like "skeleton" - effectively a directed acyclic graph, i.e. they consist of nodes and each node connects to at most one parent. This format is commonly used to describe a neuron's topology and often shared using SWC files.

A `navis.TreeNeuron`

is typically loaded from an SWC file via `navis.read_swc`

but you can also constructed one yourself from e.g. `pandas.DataFrame`

or a `networkx.DiGraph`

. See the skeleton I/O tutorial for details.

NAVis ships with a couple example *Drosophila* neurons from the Janelia hemibrain project published in Scheffer et al. (2020) and available at https://neuprint.janelia.org (see also the neuPrint tutorial):

```
import navis
# Load one of the example neurons
sk = navis.example_neurons(n=1, kind="skeleton")
# Inspect the neuron
sk
```

`navis.TreeNeuron`

stores nodes and other data as attached `pandas.DataFrames`

:

```
sk.nodes.head()
```

## MeshNeurons#

`MeshNeurons`

consist of vertices and faces, and are a typical output of e.g. image segmentation.

A `navis.MeshNeuron`

can be constructed from any object that has `.vertices`

and `.faces`

properties, a dictionary of `vertices`

and `faces`

or a file that can be parsed by `trimesh.load`

. See the mesh I/O tutorial for details.

Each of the example neurons in NAVis also comes as mesh representation:

```
m = navis.example_neurons(n=1, kind="mesh")
m
```

`navis.MeshNeuron`

stores vertices and faces as attached numpy arrays:

```
m.vertices, m.faces
```

Out:

```
(TrackedArray([[16384. , 34792.03125 , 24951.88085938],
[16384. , 36872.0625 , 25847.89453125],
[16384. , 36872.0625 , 25863.89453125],
...,
[ 5328.08105469, 21400.07617188, 16039.99414062],
[ 6872.10498047, 19560.04882812, 13903.96191406],
[ 6872.10498047, 19488.046875 , 13927.96191406]]), TrackedArray([[3888, 3890, 3887],
[3890, 1508, 3887],
[1106, 1104, 1105],
...,
[5394, 5426, 5548],
[5852, 5926, 6017],
[ 207, 217, 211]]))
```

## Dotprops#

`Dotprops`

represent neurons as point clouds where each point is associated with a vector describing the local orientation. This simple representation often comes from e.g. light-level data or as direvative of skeletons/meshes (see `navis.make_dotprops`

).

Dotprops are used e.g. for NBLAST. See the dotprops I/O tutorial for details.

`navis.Dotprops`

consist of `.points`

and associated `.vect`

(vectors). They are typically created from other types of neurons using `navis.make_dotprops`

:

Turn our above skeleton into dotprops

```
dp = navis.make_dotprops(sk, k=5)
dp
```

```
dp.points, dp.vect
```

Out:

```
(array([[15784., 37250., 28062.],
[15764., 37230., 28082.],
[15744., 37190., 28122.],
...,
[14544., 36430., 28422.],
[14944., 36510., 28282.],
[15264., 36870., 28282.]], dtype=float32), array([[-0.3002053 , -0.39364937, 0.8688596 ],
[-0.10845336, -0.2113751 , 0.9713694 ],
[-0.0435693 , -0.45593134, 0.8889479 ],
...,
[-0.38446087, 0.44485292, -0.80888546],
[-0.9457323 , -0.1827982 , -0.26865458],
[-0.79947734, -0.5164282 , -0.30681902]], dtype=float32))
```

Check out the NBLAST tutorial for further details on dotprops!

## VoxelNeurons#

`VoxelNeurons`

represent neurons as either 3d image or x/y/z voxel coordinates typically obtained from e.g. light-level microscopy.

`navis.VoxelNeuron`

consist of either a dense 3d `(N, M, K)`

array (a "grid") or a sparse 2d `(N, 3)`

array of voxel coordinates (COO format). You will probably find yourself loading these data from image files (e.g. `.nrrd`

via `navis.read_nrrd()`

). That said we can also "voxelize" other neuron types to produce `VoxelNeurons`

:

```
# Load an example mesh
m = navis.example_neurons(n=1, kind="mesh")
# Voxelize:
# - with a 0.5 micron voxel size
# - some Gaussian smoothing
# - use number of vertices (counts) for voxel values
vx = navis.voxelize(m, pitch="0.5 microns", smooth=2, counts=True)
vx
```

This is the grid representation of the neuron:

```
vx.grid.shape
```

Out:

```
(298, 392, 286)
```

And this is the `(N, 3)`

voxel coordinates + `(N, )`

values sparse representation of the neuron:

```
vx.voxels.shape, vx.values.shape
```

Out:

```
((643611, 3), (643611,))
```

Note

You may have noticed that all neurons share some properties irrespective of their type, for example `.id`

, `.name`

or `.units`

. These properties are optional and can be set when you first create the neuron, or at a later point.

In particular the `.id`

property is important because many functions in NAVis will return results that are indexed by the neurons' IDs. If `.id`

is not set explicitly, it will default to some rather cryptic random UUID - you have been warned!

## Neuron meta data#

### Connectors#

NAVis was designed with connectivity data in mind! Therefore, each neuron - regardless of type - *can* have a `.connectors`

table. Connectors are meant to bundle all kinds of connections: pre- & postsynapses, electrical synapses, gap junctions and so on.

A connector table must minimally contain an `x/y/z`

coordinate and a `type`

for each connector. Here is an example of a connector table:

```
n = navis.example_neurons(1)
n.connectors.head()
```

Connector tables aren't just passive meta data: certain functions in NAVis use or even require them. The most obvious example is probably for plotting:

```
# Plot neuron including its connectors
fig, ax = navis.plot2d(
n, # the neuron
connectors=True, # plot the neurons' connectors
color="k", # make the neuron black
cn_size=3, # slightly increase connector size
view=("x", "-z"), # set frontal view
method="2d" # connectors are better visible in 2d
)
```

In above plot, red dots are presynapses (outputs) and cyan dots are postsynapses (inputs).

### Somas#

Unless a neuron is truncated, it should have a soma somewhere. Knowing where the soma is can be very useful, e.g. as point of reference for distance calculations or for plotting. Therefore, {{ soma }} neurons have a `.soma`

property:

```
n = navis.example_neurons(1)
n.soma
```

Out:

```
4177
```

In case of this exemplary `navis.TreeNeuron`

, the `.soma`

points to an ID in the node table. We can also get the position:

```
n.soma_pos
```

Out:

```
array([[14957.1, 36540.7, 28432.4]], dtype=float32)
```

Other neuron types also support soma annotations but they may look slightly different. For a `navis.MeshNeuron`

, annotating a node position makes little sense. Instead, we track the x/y/z position directly:

```
m = navis.example_neurons(1, kind="mesh")
m.soma_pos
```

Out:

```
array([14957.1, 36540.7, 28432.4])
```

For the record: `.soma`

/ `.soma_pos`

can be set manually like any other property (there are some checks and balances to avoid issues) and can also be `None`

:

```
# Set the skeleton's soma on node with ID 1
n.soma = 1
n.soma
```

Out:

```
1
```

Drop soma from MeshNeuron

```
m.soma_pos = None
```

### Units#

NAVis supports assigning units to neurons. The neurons shipping with NAVis, for example, are in 8x8x8nm voxel space^{1}:

```
m = navis.example_neurons(1, kind="mesh")
print(m.units)
```

Out:

```
8 nanometer
```

To set the neuron's units simply use a descriptive string:

```
m.units = "10 micrometers"
print(m.units)
```

Out:

```
10 micrometer
```

Note

Setting the units as we did above does not actually change the neuron's coordinates. It merely sets a property that can be used by other functions to interpret the neuron's coordinate space. See below on how to convert the units of a neuron.

Tracking units is good practice in general but is also very useful in a variety of scenarios:

First, certain NAVis functions let you pass quantities as unit strings:

```
# Load example neuron which is in 8x8x8nm space
n = navis.example_neurons(1, kind="skeleton")
# Resample to 1 micrometer
rs = navis.resample_skeleton(n, resample_to="1 um")
```

Second, NAVis optionally uses the neuron's units to make certain properties more interpretable. By default, properties like cable length or volume are returned in the neuron's units, i.e. in 8x8x8nm voxel space in our case:

```
print(n.cable_length)
```

Out:

```
266476.88
```

You can tell NAVis to use the neuron's `.units`

to make these properties more readable:

```
navis.config.add_units = True
print(n.cable_length)
navis.config.add_units = False # reset to default
```

Out:

```
2.1318150000000005 millimeter
```

Note

Note that `n.cable_length`

is now a `pint.Quantity`

object. This may make certain operations a bit more cumbersome which is why this feature is optional. You can to a float by calling `.magnitude`

:

```
n.cable_length.magnitude
```

Check out Pint's documentation to learn more.

To actually convert the neuron's coordinate space, you have two options:

You can multiply or divide any neuron or `NeuronList`

by a number to change the units:

```
# Example neuron are in 8x8x8nm voxel space
n = navis.example_neurons(1)
# Multiply by 8 to get to nanometer space
n_nm = n * 8
# Divide by 1000 to get micrometers
n_um = n_nm / 1000
```

For non-isometric conversions you can pass a vector of scaling factors:

```
neuron * [4, 4, 40]
```

`TreeNeurons`

, this is expected to be scaling factors for `(x, y, z, radius)`

. If your neuron has known units, you can let NAVis do the conversion for you:

```
n = navis.example_neurons(1)
# Convert to micrometers
n_um = n.convert_units("micrometers")
```

Addition & Subtraction

Multiplication and division will scale the neuro as you've seen above. Similarly, adding or subtracting to/from neurons will offset the neuron's coordinates:

```
n = navis.example_neurons(1)
# Convert to microns
n_um = n.convert_units("micrometers")
# Add 100 micrometers along all axes to the neuron
n_offset = n + 100
# Subtract 100 micrometers along just one axis
n_offset = n - [0, 0, 100]#
```

## Operating on neurons#

Above we've already seen examples of passing neurons to functions - for example `navis.plot2d(n)`

.

For some NAVis functions, neurons offer have shortcut "methods":

```
import navis
sk = navis.example_neurons(1, kind='skeleton')
sk.reroot(sk.soma, inplace=True) # reroot the neuron to its soma
lh = navis.example_volume('LH')
sk.prune_by_volume(lh, inplace=True) # prune the neuron to a volume#
sk.plot3d(color='red') # plot the neuron in 3d
```

```
import navis
sk = navis.example_neurons(1, kind='skeleton')
navis.reroot_skeleton(sk, sk.soma, inplace=True) # reroot the neuron to its soma
lh = navis.example_volume('LH')
navis.in_volume(sk, lh, inplace=True) # prune the neuron to a volume
navis.plot3d(sk, color='red') # plot the neuron in 3d
```

Note

In some cases the shorthand methods might offer only a subset of the full function's functionality.

### The `inplace`

parameter#

The `inplace`

parameter is part of many NAVis functions and works like e.g. in the `pandas`

library:

- if
`inplace=True`

operations are performed directly on the input neuron(s) - if
`inplace=False`

(default) a modified copy of the input is returned and the input is left unchanged

If you know you don't need the original, you can use `inplace=True`

to save memory (and a bit of time):

```
# Load a neuron
n = navis.example_neurons(1)
# Load an example neuropil
lh = navis.example_volume("LH")
# Prune neuron to neuropil but leave original intact
n_lh = n.prune_by_volume(lh, inplace=False)
print(f"{n.n_nodes} nodes before and {n_lh.n_nodes} nodes after pruning")
```

Out:

```
4465 nodes before and 344 nodes after pruning
```

## All neurons are equal...#

... but some are more equal than others.

In Python the `==`

operator compares two objects:

```
1 == 1
```

Out:

```
True
```

```
2 == 1
```

Out:

```
False
```

For NAVis neurons this is comparison done by looking at the neurons' attribues: morphologies (soma & root nodes, cable length, etc) and meta data (name).

```
n1, n2 = navis.example_neurons(n=2)
n1 == n1
```

Out:

```
True
```

```
n1 == n2
```

Out:

```
False
```

To find out which attributes are compared, check out the neuron's `.EQ_ATTRIBUTES`

property:

```
navis.TreeNeuron.EQ_ATTRIBUTES
```

Out:

```
['n_nodes', 'n_connectors', 'soma', 'root', 'n_branches', 'n_leafs', 'cable_length', 'name']
```

Edit this list to establish your own criteria for equality.

## Making custom changes#

Under the hood NAVis calculates certain properties when you load a neuron: e.g. it produces a graph representation (`.graph`

or `.igraph`

) and a list of linear segments (`.segments`

) for `TreeNeurons`

. These data are attached to a neuron and are crucial for many functions. Therefore NAVis makes sure that any changes to a neuron automatically propagate into these derived properties. See this example:

```
n = navis.example_neurons(1, kind="skeleton")
print(f"Nodes in node table: {n.nodes.shape[0]}")
print(f"Nodes in graph: {len(n.graph.nodes)}")
```

Out:

```
Nodes in node table: 4465
Nodes in graph: 4465
```

Making changes will cause the graph representation to be regenerated:

```
n.prune_by_strahler(1, inplace=True)
print(f"Nodes in node table: {n.nodes.shape[0]}")
print(f"Nodes in graph: {len(n.graph.nodes)}")
```

Out:

```
Nodes in node table: 1761
Nodes in graph: 1761
```

If, however, you make changes to the neurons that do not use built-in functions there is a chance that NAVis won't realize that things have changed and properties need to be regenerated!

```
n = navis.example_neurons(1)
print(f"Nodes in node table before: {n.nodes.shape[0]}")
print(f"Nodes in graph before: {len(n.graph.nodes)}")
# Truncate the node table by 55 nodes
n.nodes = n.nodes.iloc[:-55].copy()
print(f"\nNodes in node table after: {n.nodes.shape[0]}")
print(f"Nodes in graph after: {len(n.graph.nodes)}")
```

Out:

```
Nodes in node table before: 4465
Nodes in graph before: 4465
Nodes in node table after: 4410
Nodes in graph after: 4410
```

Here, the changes to the node table automatically triggered a regeneration of the graph. This works because NAVis checks hash values of neurons and in this instance it detected that the node node table - which represents the core data for `TreeNeurons`

- had changed. It would not work the other way around: changing the graph does not trigger changes in the node table.

Again: as long as you are using built-in functions, you don't have to worry about this. If you do run some custom manipulation of neurons be aware that you might want to make sure that the data structure remains intact. If you ever need to manually trigger a regeneration you can do so like this:

Clear temporary attributes of the neuron

```
n._clear_temp_attr()
```

## Converting neuron types#

NAVis provides a couple functions to move between neuron types:

`navis.make_dotprops`

: Convert any neuron to dotprops`navis.skeletonize`

: Convert any neuron to a skeleton`navis.mesh`

: Convert any neuron to a mesh`navis.voxelize`

: Convert any neuron to a voxel grid

In particular meshing and skeletonizing are non-trivial and you might have to play around with the parameters to optimize results with your data! Let's demonstrate on some example:

```
# Start with a mesh neuron
m = navis.example_neurons(1, kind="mesh")
# Skeletonize the mesh
s = navis.skeletonize(m)
# Make dotprops (this works from any other neuron type
dp = navis.make_dotprops(s, k=5)
# Voxelize the mesh
vx = navis.voxelize(m, pitch="2 microns", smooth=1, counts=True)
# Mesh the voxels
mm = navis.mesh(vx.threshold(0.5))
```

Out:

```
/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/skeletor/skeletonize/wave.py:198: DeprecationWarning:
Graph.clusters() is deprecated; use Graph.connected_components() instead
/opt/hostedtoolcache/Python/3.10.15/x64/lib/python3.10/site-packages/skeletor/skeletonize/wave.py:228: DeprecationWarning:
Graph.shortest_paths() is deprecated; use Graph.distances() instead
```

Inspect the results:

```
# Co-visualize the mesh and the skeleton
navis.plot3d(
[m, s],
color=[(0.7, 0.7, 0.7, 0.2), "r"], # transparent mesh, skeleton in red
radius=False, # False so that skeleton is drawn as a line
)
```

Co-visualize the mesh and the dotprops

```
navis.plot3d(
[m, dp],
color=[(0.7, 0.7, 0.7, 0.2), "r"], # transparent mesh, dotprops in red
)
```