TANGO Tutorial: Nuclear Pore Complexes#

This Tutorial was designed to demonstrate the usage of cryoCAT and its module tango.py for the affiliation computation of subunits (SUs) of the cytoplasmic ring (CR) of the nuclear pore complex (NPC).

Note on GUI:#

To access the TANGO graphical user interface (GUI), you have two options (make sure you are in the environment where cryocat is installed):

  1. Navigate to the /cryocat/app/ folder and run:

    python app.py
    
  2. Alternatively, simply type the following in the command line:

    tango_app
    

    Note: If you installed cryocat in editable mode (recommended) before tango_app was added, you will need to reinstall it to have the tango_app command properly registered.

Important: Neither of these commands will open the GUI directly. Instead, they will start a local server and display an address in the terminal, such as:

Running on http://127.0.0.1:8050

Copy this address into your browser to access the GUI.

For an introduction on how to use the GUI, consider watching this video.

Preparation of Notebook#

autoreload reloads modules automatically before entering the execution of code typed at the IPython prompt.

[10]:
%load_ext autoreload
%autoreload 2
The autoreload extension is already loaded. To reload it, use:
  %reload_ext autoreload

Besides the cryoCAT modules to handle motive lists (cryomotl) and the module dedicated to twist-aware neighborhoods for geometric organization (TANGO), several other common python libraries are imported for this demonstration.

[11]:
import numpy as np

from scipy.spatial import cKDTree

import matplotlib.pyplot as plt

from cryocat.core import cryomotl
from cryocat.analysis import tango

# for color palette
from monet_palette import monet_colors

Input#

The NPC motive list is loaded. npc_input was preprocessed by cleaning using a mask and by shifting all CR SUs by the CR radius.

vis_motl is used to visualize the affiliation results. Its particles had not been shifted in x-direction.

[12]:
npc_input = "./inputs/cr_mask_cleaned_shifted.em"

npc_motl = cryomotl.EmMotl(npc_input)

visualization_input = "./inputs/cr_mask_cleaned.em"

vis_motl = cryomotl.EmMotl(visualization_input)

Parameter Analysis#

Nearest neighbors (NNs) and their distances are gained using cKDTree.

The search radius for the initial TwistDescriptor depends on NN-statistics.

It is chosen so as to have non-empty supports for most subunits (SUs), while not being too large, either. The purpose of choosing a smaller spherical support stems from wanting to compute affiliations.

Large supports may contain many false positives, which exist in large, dense quantities in this data. This can slow down computation.

[13]:
positions = npc_motl.get_coordinates()

tree = cKDTree(positions)

dd, _ = tree.query(positions, k=2)

print(f"The median NN distance is {np.median(dd[:,1])} voxels.")
The median NN distance is 10.411326530299032 voxels.

Computation of Twist Features#

[ ]:
npc_twist_desc = tango.TwistDescriptor(input_motl= npc_motl, nn_radius= 30)

display(npc_twist_desc.df)

Among the support options offered by TANGO, the cylindrical support is a well-suited support to crop the initial one to.

In the context of a given CR, a subunit’s intrinsic z-axis serves as an normal to the nuclear envelope.

Thus, a shallow cylinder extending from a SU should ideally contain mostly particles close to or embedded into the nuclear envelope.

[ ]:
height = 10 # try cylindrical support with symmetric = True.

# cylinder statistics also require the choice for an axis of rotation; here, a query particle's intrinsic z-axis is chosen.
# This choice is inspired by the NPC subunits having z-normals pointing in approximately the same direction.
axis = np.array([0, 0, 1])

cylinder_supp = tango.Cylinder(npc_twist_desc, radius=30, height=height, axis = axis, symmetric= True)

display(cylinder_supp.support.df)

Using a filter, one can further zoom in on the cytoplasmic rings by reducting the data to those particles, for which the rotation transporting their orientation to that of a neighboring one is close to being a rotation around the intrinsic z-axis.

Furthermore, a focus on the eight-fold symmetry of the NPC is implemented in the form of restrictions on the geodesic distance in radians. It is restricted to what is expected for the relative orientation between neighboring SUs i, i+1 (2pi/8), with some room for noise.

[ ]:
max_angle = np.degrees(0.5) # tolerance

z_axis_filtered = tango.AxisRot(twist_desc= cylinder_supp.support, max_angle= max_angle)

# focus on required eight-fold symmetry of the CR.

df = z_axis_filtered.filter.df.copy()

# By removing the comment in the line of code below, SUs which are included which have a relative orientation close to 4pi/8, which is expected for SUs i, i+2.
df = df[((df['geodesic_distance_rad'] > 0.7) & (df['geodesic_distance_rad'] < 0.9))] # | ((df['geodesic_distance_rad'] > 1.4) & (df['geodesic_distance_rad'] < 1.7))]

# update the descriptor's data frame in order to use the built-in methods more easily.

z_axis_filtered.filter.df = df

Intersecting supports can be deduced from a data frame by treating subtomogram ids as nodes in a graph and connecting them whenever they form a ‘qp_id’–‘nn_id’–pair in a given row of that data frame.

The resulting graph decomposes into connected components which are computed from a twist descriptor using the proximity clustering method.

Here, it is applied to the most recent cleaning results. The parameter size_connected_components represents a lower bound for the amount of particles (nodes) per connected component.

This is chosen as 3, meaning that data is grouped into a CR if there are at least 3 SU in the same connected component.

[ ]:
S = z_axis_filtered.filter.proximity_clustering(size_connected_components= 3)

Each connected component is a networkx Graph object, the nodes of which are subtomogram ids, which can be used to get subsets of the input motivelist in order to label that sublist, before concatenating them into the output motive list.

[ ]:
out_motl = cryomotl.Motl()

for i, G in enumerate(S):

    subtomo_indices = list(set(G.nodes()))

    sub_motl = vis_motl.get_motl_subset(subtomo_indices, feature_id= 'subtomo_id')

    sub_motl.df['geom1'] = i * np.ones(sub_motl.df['geom1'].shape[0])

    out_motl = out_motl + sub_motl

out_motl.write_out('cr_components_tutorial.em')

The results can be visualized and inspected. Labels separating NPCs are included.