Documentation

NBLAST

NBLAST is a method to quantify morphological similarity.

27 Dec 2021

Overview

NBLAST (Costa et al., 2016) is a method to quantify morphological similarity. It works on “dotprops” which represent neurons as tangent vectors. For each tangent vector in the query neuron, NBLAST finds the closest tangent vector in the target neuron and calculates a score from the distance between and the dotproduct of the two vectors. The final NBLAST score is the sum over all query-target vector pairs. Typically, this score is normalized to a self-self comparison (i.e. a perfect match would be 1).

nblast

Finding Matching Neurons

VFB computes NBLAST scores for all neurons in its database. So if all you want is a list of similar neurons, it’s fastest (and easiest) to get those directly from VFB.

# Import libs and initialise API objects
from vfb_connect.cross_server_tools import VfbConnect
import navis.interfaces.neuprint as neu

import pandas as pd
import navis

navis.set_pbars(jupyter=False)

vc = VfbConnect()
client = neu.Client('https://neuprint.janelia.org', dataset='hemibrain:v1.1')

First let’s write a function to grab VFB neurons and turn them into navis neurons. This is modified from vc.neo_query_wrapper.get_images.

import requests
from tqdm import tqdm

def get_vfb_neurons(vfb_ids, template='JRC2018Unisex'):
    """Load neurons from VFB as navis.TreeNeurons."""
    vfb_ids = list(navis.utils.make_iterable(vfb_ids))
    inds = vc.neo_query_wrapper.get_anatomical_individual_TermInfo(short_forms=vfb_ids)
    nl = []
    # Note: queries should be parallelized in the future 
    # -> for pymaid I use request-futures which works really well
    for i in tqdm(inds, desc='Loading VFB neurons', leave=False):        
        if not ('has_image' in i['term']['core']['types']):
            continue
        label = i['term']['core']['label']
        image_matches = [x['image'] for x in i['channel_image']]
        if not image_matches:
            continue
        for imv in image_matches:
            if imv['template_anatomy']['label'] == template:
                r = requests.get(imv['image_folder'] + '/volume.swc')
                ### Slightly dodgy warning - could mask network errors
                if not r.ok:
                    warnings.warn("No '%s' file found for '%s'." % (image_type, label))
                    continue

                # `id` should ideally be unique but that's not enforced
                n = navis.TreeNeuron(r.text, name=label, id=i['term']['core']['short_form'])   

                # This registers attributes that you want to show in the summary
                # alternatively we could have a generic attribute like so:
                # n.template = template
                n._register_attr("template", template, temporary=False)

                # I assume all available templates are in microns?
                # @Robbie -do we have this metadata on templates?  If not, we should add ASAP.
                n.units = '1 micron'

                nl.append(n)
    return navis.NeuronList(nl)
# Search for similar neurons using the VFB ID of an ellipsoid body neuron in FAFB
# We got that ID from a search on the VFB website
similar_to_EPG6L1 = vc.get_similar_neurons('VFB_001012ay')
similar_to_EPG6L1 

idNBLAST_scorelabeltypessource_idaccession_in_source
0VFB_jrchjtk60.525EPG(PB08)_L6 - 541870397[EB-PB 1 glomerulus-D/Vgall neuron]neuprint_JRC_Hemibrain_1point1541870397
1VFB_jrchjtk80.524EPG(PB08)_L6 - 912601268[EB-PB 1 glomerulus-D/Vgall neuron]neuprint_JRC_Hemibrain_1point1912601268
2VFB_jrchjtkb0.504EPG(PB08)_L6 - 788794171[EB-PB 1 glomerulus-D/Vgall neuron]neuprint_JRC_Hemibrain_1point1788794171
3VFB_jrchjtji0.481EL(EQ5)_L - 1036753721[EBw.AMP.s-Dga-s.b neuron]neuprint_JRC_Hemibrain_1point11036753721
# Read those neurons from VFB 
query = get_vfb_neurons('VFB_001012ay')
matches = get_vfb_neurons(similar_to_EPG6L1.id.values)
matches

typenameidn_nodesn_connectorsn_branchesn_leafscable_lengthsomaunitstemplate
0navis.TreeNeuronEPG(PB08)_L6 - 912601268VFB_jrchjtk89819None100010152211.292188None1 micronJRC2018Unisex
1navis.TreeNeuronEL(EQ5)_L - 1036753721VFB_jrchjtji13864None194219743403.311508[1]1 micronJRC2018Unisex
2navis.TreeNeuronEPG(PB08)_L6 - 788794171VFB_jrchjtkb9651None9629872125.364857None1 micronJRC2018Unisex
3navis.TreeNeuronEPG(PB08)_L6 - 541870397VFB_jrchjtk611042None109911272432.101351[1]1 micronJRC2018Unisex
# Define colors such that query is black 
import seaborn as sns 
colors = ['k'] + sns.color_palette('muted', len(matches))

navis.plot3d([query, matches], color=colors)

These like a good match!

Clustering Groups of Neurons

Now imagine you have a set of neurons and you want to group them into morphologically similar “types”. To demonstrate this, we will take some ellipsoid body neurons from VFB and cluster them using NBLAST.

# Find instances of EB-PB-gall neurons
eb_pb_ids = pd.DataFrame.from_records(vc.get_instances("adult ellipsoid body-protocerebral bridge-gall neuron"))
eb_pb_ids.head()

labelsymbolidtagsparents_labelparents_iddata_sourceaccessiontemplatesdatasetlicense
0EPGt(PB09)_L9 - 1168664447VFB_jrchjtlcEntity|has_image|Adult|Anatomy|has_neuron_conn...EB slice 8-GA-PB slice 9 neuronFBbt_00111423neuprint_JRC_Hemibrain_1point11168664447JRC_FlyEM_Hemibrain|JRC2018UnisexXu2020NeuronsV1point1https://creativecommons.org/licenses/by/4.0/le...
1EPGt(PB09)_R9 - 1219069439VFB_jrchjtl9Entity|has_image|Adult|Anatomy|has_neuron_conn...EB slice 8-GA-PB slice 9 neuronFBbt_00111423neuprint_JRC_Hemibrain_1point11219069439JRC2018Unisex|JRC_FlyEM_HemibrainXu2020NeuronsV1point1https://creativecommons.org/licenses/by/4.0/le...
2EPG(PB08)_L2 - 697001770VFB_jrchjtkwEntity|has_image|Adult|Anatomy|has_neuron_conn...EB-PB 1 glomerulus-D/Vgall neuronFBbt_00047030neuprint_JRC_Hemibrain_1point1697001770JRC2018Unisex|JRC_FlyEM_HemibrainXu2020NeuronsV1point1https://creativecommons.org/licenses/by/4.0/le...
3EPG(PB08)_R8 - 1125964814VFB_jrchjtl5Entity|has_image|Adult|Anatomy|has_neuron_conn...EB-PB 1 glomerulus-D/Vgall neuronFBbt_00047030neuprint_JRC_Hemibrain_1point11125964814JRC2018Unisex|JRC_FlyEM_HemibrainXu2020NeuronsV1point1https://creativecommons.org/licenses/by/4.0/le...
4EPG(PB08)_L6 - 912601268VFB_jrchjtk8Entity|has_image|Adult|Anatomy|has_neuron_conn...EB-PB 1 glomerulus-D/Vgall neuronFBbt_00047030neuprint_JRC_Hemibrain_1point1912601268JRC2018Unisex|JRC_FlyEM_HemibrainXu2020NeuronsV1point1https://creativecommons.org/licenses/by/4.0/le...
# Get the neurons from VFB
eb_pb = get_vfb_neurons(eb_pb_ids.id.values)
eb_pb
Loading VFB neurons: 100%|██████████| 62/62 [01:04<00:00,  1.04s/it]

typenameidn_nodesn_connectorsn_branchesn_leafscable_lengthsomaunits
0navis.TreeNeuronEPG(PB08)_R5 - 725951521VFB_jrchjtk511097None122312512221.279929[1]1 dimensionless
1navis.TreeNeuronEPG(PB08)_R2 - 632544268VFB_jrchjtkt12861None147515072664.323311[1]1 dimensionless
.................................
60navis.TreeNeuronEPG(PB08)_R5 - 694920753VFB_jrchjtk711358None134813762335.616245[1]1 dimensionless
61navis.TreeNeuronEPG(PB08)_R6 - 910438331VFB_jrchjtkc10983None108611072340.485198[1]1 dimensionless

Now that we have the skeletons, we need to turn them into dotprops. Keep in mind that:

  1. NBLAST is optimized for data in microns. Since our neurons are in the JRC2018Unisex template, they already are.
  2. Neurons should have the same sampling rate (i.e. nodes per micron). To ensure this we will resample the neurons to 1 point per micron.
  3. We won’t be doing this this time around but we typically simplify the neurons slightly by e.g. pruning small twigs (navis.prune_twigs()) to reduce noise.
eb_dps = navis.make_dotprops(eb_pb, resample=1, k=5)

# Note this NeuronList /contains dotprops now
eb_dps

typenameidkunitsn_points
0navis.DotpropsEPG(PB08)_R5 - 725951521VFB_jrchjtk551 dimensionless2683
1navis.DotpropsEPG(PB08)_R2 - 632544268VFB_jrchjtkt51 dimensionless3152
.....................
60navis.DotpropsEPG(PB08)_R5 - 694920753VFB_jrchjtk751 dimensionless2924
61navis.DotpropsEPG(PB08)_R6 - 910438331VFB_jrchjtkc51 dimensionless2486

To illustrate what dotprops are, lets visualize one side-by-side with its skeleton.

sk = eb_pb[0].copy()
dp = eb_dps[0].copy()

# Slightly offset the dotprops by a micron 
dp.points[:, 0] += 1

navis.plot3d([sk, dp], color=['k', 'r'])

Now we can run the NBLAST. Note that in this case, we run an all-by-all NBLAST which means that we don’t need to worry about the directionality - i.e. whether we NBLAST A->B or B->A.

scores = navis.nblast_allbyall(eb_dps)
scores.head()

VFB_jrchjtk5VFB_jrchjtktVFB_001012baVFB_jrchjtkjVFB_jrchjtkpVFB_jrchjtkeVFB_jrchjtkrVFB_jrchjtkbVFB_00004319VFB_00009714...VFB_jrchjtk8VFB_001012bpVFB_jrchjtk9VFB_jrchjtkiVFB_jrchjtl3VFB_jrchjtkvVFB_001012b9VFB_001012bbVFB_jrchjtk7VFB_jrchjtkc
VFB_jrchjtk51.000000-0.0839600.4276590.0582100.0219040.4842100.1682350.1789540.0654570.355732...0.1307880.5283140.569923-0.029195-0.130578-0.0721850.3816680.1399700.8164550.504740
VFB_jrchjtkt-0.0624651.000000-0.030980-0.1243260.507521-0.122518-0.1432980.091243-0.124431-0.142560...0.172337-0.0592010.078412-0.1160400.129102-0.034670-0.0533820.103465-0.063922-0.073508
VFB_001012ba0.129771-0.1158791.0000000.374231-0.0915310.5495190.450368-0.1302000.1615740.112378...-0.1279900.7860010.0139100.330330-0.0008750.2129140.817302-0.1100840.2004970.569953
VFB_jrchjtkj-0.027448-0.2196020.4833411.000000-0.0311110.4436430.461981-0.0256820.0618030.434210...-0.0348950.389407-0.0911650.8197160.2177210.5529820.447278-0.0530550.0457220.485596
VFB_jrchjtkp-0.1586020.493915-0.161524-0.1030611.000000-0.212700-0.2213680.405601-0.163415-0.130491...0.515069-0.1708310.024440-0.095100-0.032720-0.082523-0.1823860.412998-0.158171-0.197676

5 rows × 62 columns

Next, we need to take those forward scores (A->B) and symmetrize by calculating mean scores (A<->B).

# Symmetrize scores
scores_mean = (scores + scores.T) / 2

scores_mean.head()

VFB_jrchjtk5VFB_jrchjtktVFB_001012baVFB_jrchjtkjVFB_jrchjtkpVFB_jrchjtkeVFB_jrchjtkrVFB_jrchjtkbVFB_00004319VFB_00009714...VFB_jrchjtk8VFB_001012bpVFB_jrchjtk9VFB_jrchjtkiVFB_jrchjtl3VFB_jrchjtkvVFB_001012b9VFB_001012bbVFB_jrchjtk7VFB_jrchjtkc
VFB_jrchjtk51.000000-0.0732120.2787150.015381-0.0683490.4112570.0987610.1422770.0320640.187264...0.0708280.4373370.572156-0.043306-0.140155-0.0920710.2717650.1319250.8204550.398975
VFB_jrchjtkt-0.0732121.000000-0.073429-0.1719640.500718-0.142672-0.1649920.062182-0.115399-0.162311...0.176281-0.0990060.074666-0.1620020.097262-0.082222-0.0895340.120931-0.075555-0.114645
VFB_001012ba0.278715-0.0734291.0000000.428786-0.1265270.6679800.527915-0.1285990.2206820.068423...-0.1430150.7922160.0706750.3895900.0180900.1764920.826461-0.1021220.3321480.684591
VFB_jrchjtkj0.015381-0.1719640.4287861.000000-0.0670860.4369140.494708-0.031511-0.0305390.489038...-0.0452530.396773-0.0690740.8246810.1680680.5085380.395092-0.0627990.1000870.509110
VFB_jrchjtkp-0.0683490.500718-0.126527-0.0670861.000000-0.164867-0.2035830.413305-0.171488-0.124160...0.537125-0.1093460.020020-0.074199-0.011358-0.093459-0.1334730.435029-0.062292-0.161714

5 rows × 62 columns

From here on out, we use cookie cutter scipy to produce a hierarchical clustering. First we need to convert those similarity scores to distances because that’s what scipy operates on:

dist_mean = (scores_mean - 1) * -1
dist_mean.head()

VFB_jrchjtk5VFB_jrchjtktVFB_001012baVFB_jrchjtkjVFB_jrchjtkpVFB_jrchjtkeVFB_jrchjtkrVFB_jrchjtkbVFB_00004319VFB_00009714...VFB_jrchjtk8VFB_001012bpVFB_jrchjtk9VFB_jrchjtkiVFB_jrchjtl3VFB_jrchjtkvVFB_001012b9VFB_001012bbVFB_jrchjtk7VFB_jrchjtkc
VFB_jrchjtk5-0.0000001.0732120.7212850.9846191.0683490.5887430.9012390.8577230.9679360.812736...0.9291720.5626630.4278441.0433061.1401551.0920710.7282350.8680750.1795450.601025
VFB_jrchjtkt1.073212-0.0000001.0734291.1719640.4992821.1426721.1649920.9378181.1153991.162311...0.8237191.0990060.9253341.1620020.9027381.0822221.0895340.8790691.0755551.114645
VFB_001012ba0.7212851.073429-0.0000000.5712141.1265270.3320200.4720851.1285990.7793180.931577...1.1430150.2077840.9293250.6104100.9819100.8235080.1735391.1021220.6678520.315409
VFB_jrchjtkj0.9846191.1719640.571214-0.0000001.0670860.5630860.5052921.0315111.0305390.510962...1.0452530.6032271.0690740.1753190.8319320.4914620.6049081.0627990.8999130.490890
VFB_jrchjtkp1.0683490.4992821.1265271.067086-0.0000001.1648671.2035830.5866951.1714881.124160...0.4628751.1093460.9799801.0741991.0113581.0934591.1334730.5649711.0622921.161714

5 rows × 62 columns

from scipy.spatial.distance import squareform
from scipy.cluster.hierarchy import linkage, dendrogram, fcluster

# This turns the square distance matrix into 1-d vector form
dist_sq = squareform(dist_mean)

# This does the actual hierarchical clustering
Z = linkage(dist_sq, method='ward', optimal_ordering=True)
# Plot a quick dendrogram
import matplotlib.pyplot as plt 
import seaborn as sns 

# Generate the canvas
fig, ax = plt.subplots(figsize=(12, 5))

# Make some more meaningful labels 
vfb2name = dict(zip(eb_pb.id, eb_pb.name))
labels = dist_mean.index.map(lambda x: vfb2name[x].split(' - ')[0])

# Plot the dendrogram
dn = dendrogram(Z, ax=ax, labels=labels, color_threshold=.65, above_threshold_color='lightgrey')

# Make xticks biggger
ax.set_xticklabels(ax.get_xticklabels(), size=11)

# Clean axes
sns.despine(bottom=True)

png

# Make cluster matching the color threshold in the dendrogram
clusters = fcluster(Z, t=.65, criterion='distance')
clusters
array([16, 14,  4,  5, 11,  4,  3, 12,  2, 19, 13,  4,  8,  3, 19,  2,  5,
       13,  3, 11, 17, 14, 16,  6, 12, 15, 18, 11, 12, 15, 15, 18,  5, 10,
        9,  2, 19,  8,  1,  9, 19,  3,  1, 10,  2,  9,  7, 18,  9,  2, 17,
       13, 12,  4, 17,  5,  7,  6,  4, 12, 16,  4], dtype=int32)
# We have to downsample the neurons for visualization 
# because DeepNote enforces a limit on the size of plots
eb_pb_ds = eb_pb.downsample(10)
# Make a color per cluster 
palette = sns.color_palette('tab20', max(clusters))
cmap = dict(zip(range(1, max(clusters) + 1), palette))

colors = [cmap[i] for i in clusters]

navis.plot3d(eb_pb_ds, color=colors)