How are sym_expand/idx IDs assigned during symmetry expansion?

Hello,

Can someone please explain to me how sym_expand/idx IDs are assigned to symmetry expanded particles?

I am working with a protein with D6 symmetry, I understand that after symmetry expansion I will have 12 idx IDs (0-11) associated with each sym_expand/src_uid.

My question is how are the idx IDs assigned across the protein?
Or more specifically, are the idx IDs assigned to all 6 subunits in one mirror plane 0-5 before moving to other side of the protein?

Thanks!

Hi @JJtheCryoEMguy!

For your particular (D6) case, here are the symmetry rotations applied to a coordinate axis. In each case, we’re looking down along the Z-axis of the un-rotated object (index 0). In order to support arbitrary point group symmetries, we generate symmetry-related rotations incrementally, which is why they do not appear in a simple order.

As you can see, indices 0, 1, 5, 7, 10, and 11 form the “top” plane, while the others are in the rotated “bottom” plane.

If you don’t mind, could I ask for what type of analysis you need to know which rotation corresponds to which symmetry expansion index?

Hi @rposert, thank you for the explanation and the diagram. It was very helpful.

If you don’t mind, could I ask for what type of analysis you need to know which rotation corresponds to which symmetry expansion index?

I am trying to understand how conformations are distributed across my particle to see if there is cooperation between subunits. To approach this, I had done 3D classification on symmetry expanded and signal subtracted particles. I am using the source ptcl IDs and the symm expansion index numbers from the meta data with respect to each class to see if there is any preference to where each class occurs in the particle (i.e. on the same side of the particle or randomly across it). I figured using the symmetry expansion ID would be an easy way to process the data with custom scripts, provided I knew how the index IDs were being assigned.

Does that make sense? Do you think this is a correct approach?

It’s certainly not a bad idea for a first analysis! It’s important to remember, though, that the symmetry index has a consistent relationship only to the pose of the original particle.

For instance, consider a C4 pseudosymmetric particle. It can be in four symmetry-related poses: A, B, C, or D. Symmetry index 0 will correspond to whichever of those four poses the initial (un-expanded) particle is in, and the others will be related to that pose by some rotation (for the sake of example, let’s say all symmetry indices are related by a 90° rotation).

This means that a particle with some symmetry-breaking feature in the expanded particles with indices 0 and 1 may be the same as a particle with symmetry breaking features in 0 and 3, if the first particle was in pose A and the second in pose D.

As long as you keep that in mind, I think your idea is sound!

Hi,

I am trying to do a similar analysis for a D7 symmetry molecule. I was wondering how would the “idx”s correspond to the rotations/inversions in that case? It would be great if you could give a similar image for D7 symmetry expansion.

Also, out of curiosity, is this order arbitrary? Or is there a logical way of getting them?

Thank you
Adwaith

Hi @Adwaith99!

At the bottom of this reply is a script you can use (you’ll need cryosparc tools, matplotlib, and scipy) to generate BILD files like this one to inspect the symmetry operators. If you hover over the axes ChimeraX will display which index corresponds to that rotation.

This image is the result of setting symmetry_order to D7.

I’m still not sure I personally quite understand the usefulness of this workflow, and want to make sure you saw the caveat I mentioned above – not all particles in the same symmetry expanded index are in identical pose with regards to symmetry-breaking features.

Now for your question about how the index order is generated. The short answer is that the indices are the same every time, but unfortunately there’s not currently a straightforward way to know which is which without looking manually.

Script

from cryosparc.tools import CryoSPARC
import json
import numpy as np
from pathlib import Path
from scipy.spatial.transform import Rotation as R
import matplotlib.pyplot as plt

with open(Path('~/instance-info.json').expanduser(), 'r') as f:
    instance_info = json.load(f)

cs = CryoSPARC(**instance_info)
assert cs.test_connection()

project_number = "P337"
workspace_number = "W7"
symmetry_order = "D7"
lane_name = "cryoem9"
# any job with a particles output will do - we're just using this
# dataset to avoid having to create all the fields manually
arbitrary_job_with_particles = "J68"

project = cs.find_project(project_number)
particles = project.find_job(arbitrary_job_with_particles).load_output("particles")

particles = particles.take([0])
particles["alignments3D/pose"][0] = [0., 0., 0.]
particles["alignments3D/shift"][0] = [0., 0.]
fake_particles_uid = project.save_external_result(
    workspace_uid = workspace_number,
    dataset = particles,
    type = "particle",
)

symexp_job = project.create_job(
    workspace_uid = workspace_number,
    type = "sym_expand",
    connections = {
        "particles": (fake_particles_uid, "particle")
    },
    params = {
        "sym_symmetry": symmetry_order
    }
)
symexp_job.queue(lane_name)
symexp_job.wait_for_done()
exp_particle = symexp_job.load_output("particles")

colors = plt.get_cmap("viridis")(np.linspace(0.1, 0.9, len(exp_particle)))

with open(f"{symmetry_order}.bild", "w") as f:
    arrows = np.array([
        [1, 0, 0],
        [0, 1, 0],
        [0, 0, 1]
    ])

    axes_per_bild_row = int(np.round(np.sqrt(len(exp_particle.rows()))))
    for row in exp_particle.rows():
        idx = row["sym_expand/idx"]
        x_translation = 2 * (idx % axes_per_bild_row)
        y_translation = -2 * (idx // axes_per_bild_row)

        f.write(".pop\n")
        f.write(f".translate {x_translation} {y_translation} 0\n")
        f.write(f".color {' '.join(str(x) for x in colors[idx][:-1])}\n")
        rotation = R.from_rotvec(row["alignments3D/pose"])
        pointing_directions = rotation.apply(arrows)
        f.write(f".note idx{str(idx)}\n")
        for axis in pointing_directions:
            f.write(f".cylinder 0 0 0 {' '.join(str(x) for x in axis)} 0.02\n")
        arrow_base = pointing_directions[2]
        arrow_point = arrow_base * 1.2
        f.write(f".cone {' '.join(str(x) for x in arrow_base)} {' '.join(str(x) for x in arrow_point)} 0.05\n")

Hi @rposert,

Thank you so much for the reply and explanation and the script.

Regarding the workflow, I wanted to do a similar analysis as mentioned before in the thread. I am not sure I completely understand the caveat you mentioned though. But, I can explain what I am trying to do. I see different conformations in the asymmetric unit after symmetry expansion. I want to see how these conformations are distributed across a single particle - like if adjacent asymmetric units have similar or different conformations. Do you think this approach makes sense for that? Or am I missing something?

Thanks again,
Adwaith

Hi @Adwaith99, thanks for explaining the workflow you want to use this for!

To clarify my notes about this technique, there are two main things I want to point out:

First, note that adjacent indices are not necessarily adjacent particles (it seems you know this already, but just want to highlight it). Thus, you’d have to manually create a list of which indices are adjacent to which.

Second, but related, is that there are several cases of pose and symmetry index which can represent the exact same image. Taking this D7 case and looking at the above image, a particle image with asymmetric conformations at indices 0 and 2 is exactly the same as a particle with asymmetric conformations at indices 3 and 13 with a different pose. Note that this is less important if all you care about is adjacency.

Hi @rposert ,

Thank you for the detailed explanation. I will keep them in mind. And yes, I am mostly just interested in the relative information (adjacency).