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")