- open all maps in chimerax
- set origin of all maps to
sidelength / 2 - reorient any misaligned classes (shift and rotate)
- save the output of
view matrixin ChiemraX into a text file - run the above python script, pointing it at the file containing the chimerax output and the particle star file
Last active
December 3, 2022 08:30
-
-
Save alisterburt/130812c220100f581bc2e4517680d572 to your computer and use it in GitHub Desktop.
transform classes in a rln3.1 STAR file based on manipulations in ChimeraX
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| from typing import Tuple | |
| import starfile | |
| import numpy as np | |
| import einops | |
| from scipy.spatial.transform import Rotation as R | |
| CHIMERAX_VIEW_MATRIX_OUTPUT_FILE = 'chimerax/view_matrix_output_origin_at_rotation_center.txt' | |
| PARTICLE_STAR_FILE = 'class3d_dir/run_it025_data.star' | |
| OUTPUT_STAR_FILE = 'output_realigned_data.star' | |
| VOLUME_VOXEL_SPACING = 3.3 | |
| MODEL_TO_CLASS = { | |
| 2: 1, | |
| 3: 2, | |
| 4: 3, | |
| 5: 4, | |
| 6: 5, | |
| 7: 6, | |
| } | |
| def read_chimerax_matrices(output_text_file): | |
| """read 4x4 matrices from cx 'view matrix' output.""" | |
| with open(output_text_file) as f: | |
| [matrix_line] = [ | |
| line.strip() | |
| for line in f.readlines() | |
| if line.startswith('model positions:') | |
| ] | |
| matrix_line = matrix_line.strip('model positions: ') | |
| line_per_model = matrix_line.split('#') | |
| # submodels always identity transform so skip | |
| # assumes less than 9 models present | |
| model_to_matrix = {} | |
| for entry in line_per_model: | |
| if entry == '' or entry[1] == '.': | |
| continue | |
| model_number = int(entry[0]) | |
| matrix_data = entry[2:].split(',') | |
| matrix_data = np.array([float(char) for char in matrix_data if char != '']) | |
| model_to_matrix[model_number] = matrix_data.reshape((3, 4)) | |
| return model_to_matrix | |
| def Rx(theta: np.ndarray) -> np.ndarray: | |
| """Generate 4x4 matrices which rotate vectors around the X-axis by the angle `theta`. | |
| The rotation angle `theta` is expected to be in degrees. | |
| Application of these matrices is performed by left-multiplying xyzw column vectors. | |
| Parameters | |
| ---------- | |
| theta: np.ndarray | |
| An `(n, )` array of rotation angles in degrees. | |
| Returns | |
| ------- | |
| matrices: np.ndarray | |
| An `(n, 4, 4)` array of matrices which left-multiply xyzw column vectors. | |
| """ | |
| theta = np.asarray(theta).reshape(-1) | |
| c = np.cos(np.deg2rad(theta)) | |
| s = np.sin(np.deg2rad(theta)) | |
| matrices = einops.repeat( | |
| np.eye(4), 'i j -> n i j', n=len(theta) | |
| ) | |
| matrices[:, 1, 1] = c | |
| matrices[:, 1, 2] = -s | |
| matrices[:, 2, 1] = s | |
| matrices[:, 2, 2] = c | |
| return matrices | |
| def Ry(theta: np.ndarray) -> np.ndarray: | |
| """Generate 4x4 matrices which rotate vectors around the Y-axis by the angle `theta`. | |
| The rotation angle `theta` is expected to be in degrees. | |
| Application of these matrices is performed by left-multiplying xyzw column vectors. | |
| Parameters | |
| ---------- | |
| theta: np.ndarray | |
| An `(n, )` array of rotation angles in degrees. | |
| Returns | |
| ------- | |
| matrices: np.ndarray | |
| An `(n, 4, 4)` array of matrices which left-multiply xyzw column vectors.""" | |
| theta = np.asarray(theta).reshape(-1) | |
| c = np.cos(np.deg2rad(theta)) | |
| s = np.sin(np.deg2rad(theta)) | |
| matrices = einops.repeat(np.eye(4), 'i j -> n i j', n=len(theta)) | |
| matrices[:, 0, 0] = c | |
| matrices[:, 0, 2] = s | |
| matrices[:, 2, 0] = -s | |
| matrices[:, 2, 2] = c | |
| return matrices | |
| def Rz(theta: float) -> np.ndarray: | |
| """Generate 4x4 matrices which rotate vectors around the Z-axis by the angle `theta`. | |
| The rotation angle `theta` is expected to be in degrees. | |
| Application of these matrices is performed by left-multiplying xyzw column vectors. | |
| Parameters | |
| ---------- | |
| theta: np.ndarray | |
| An `(n, )` array of rotation angles in degrees. | |
| Returns | |
| ------- | |
| matrices: np.ndarray | |
| An `(n, 4, 4)` array of matrices which left-multiply xyzw column vectors.""" | |
| theta = np.asarray(theta).reshape(-1) | |
| c = np.cos(np.deg2rad(theta)) | |
| s = np.sin(np.deg2rad(theta)) | |
| matrices = einops.repeat( | |
| np.eye(4), 'i j -> n i j', n=len(theta) | |
| ) | |
| matrices[:, 0, 0] = c | |
| matrices[:, 0, 1] = -s | |
| matrices[:, 1, 0] = s | |
| matrices[:, 1, 1] = c | |
| return np.squeeze(matrices) | |
| def S(shifts: np.ndarray) -> np.ndarray: | |
| """Generate 4x4 matrices for 2D (xy) or 3D (xyz) shifts. | |
| Application of these matrices is performed by left-multiplying xyzw column vectors. | |
| Parameters | |
| ---------- | |
| shifts: np.ndarray | |
| An `(n, 2)` or `(n, 3)` array of xy(z) shifts. | |
| Returns | |
| ------- | |
| matrices: np.ndarray | |
| An `(n, 4, 4)` array of | |
| """ | |
| shifts = np.asarray(shifts, dtype=float) | |
| if shifts.shape[-1] == 2: | |
| shifts = _promote_2d_to_3d(shifts) | |
| shifts = np.array(shifts).reshape((-1, 3)) | |
| matrices = einops.repeat(np.eye(4), 'i j -> n i j', n=shifts.shape[0]) | |
| matrices[:, 0:3, 3] = shifts | |
| return np.squeeze(matrices) | |
| def _promote_2d_to_3d(shifts: np.ndarray) -> np.ndarray: | |
| """Promote 2D vectors to 3D with zeros in the last dimension.""" | |
| shifts = np.asarray(shifts).reshape(-1, 2) | |
| shifts = np.c_[shifts, np.zeros(len(shifts))] | |
| return np.squeeze(shifts) | |
| def decompose_chimerax_matrix_for_relion(view_matrix): | |
| """Convert a chimerax matrix to a set of shifts and euler angles for particles.""" | |
| # we need to decompose the chimerax matrix in a way that we can then use to construct RELION | |
| # transformations (shift, then rot around new origin, origin is at particle center) | |
| # get chimerax matrix and embed in 4x4 | |
| CX = np.eye(4) | |
| CX[:3, :] = view_matrix # (3, 4) | |
| # T[:3, 3] = [0, 0, 0] # TEST KNOWN SHIFTS | |
| # okay, now construct the thing you need to do to each particle in RELION... | |
| T = np.linalg.pinv(CX) | |
| # now decompose transformation into matrices for the rotation, then the shift | |
| TR = np.eye(4) | |
| TR[:3, :3] = T[:3, :3] | |
| TS = S(T[:3, 3]) | |
| assert np.allclose(T, TS @ TR) | |
| # in relion, we need to first apply shifts, then rotations | |
| TS = np.linalg.pinv(TR) @ TS | |
| TS[:3, :3] = np.eye(3) | |
| assert np.allclose(T, TR @ TS) | |
| particle_shifts_ang = TS[:3, 3] # (3, ) | |
| particle_rotation = TR[:3, :3] # (3, 3) | |
| assert particle_shifts_ang.ndim == 1 | |
| particle_shifts_ang = einops.rearrange(particle_shifts_ang, 'xyz -> xyz 1') | |
| particle_shifts_ang = particle_rotation @ particle_shifts_ang | |
| particle_shifts_ang = einops.rearrange(particle_shifts_ang, 'xyz 1 -> xyz') | |
| return particle_shifts_ang, particle_rotation | |
| def shift_then_rotate_particles( | |
| particle_positions, # (n, 3) | |
| particle_orientations, # (n, 3, 3) | |
| shift, # (3, ) | |
| rotation, # (3, 3) | |
| ) -> Tuple[np.ndarray, np.ndarray]: # positions, orientations | |
| # goal: apply transformations in the local coordinate | |
| # system of each particle | |
| # transform the shifts into the local particle reference frame | |
| shift = einops.rearrange(shift, 'xyz -> xyz 1') | |
| local_shifts = particle_orientations @ shift | |
| local_shifts = einops.rearrange(local_shifts, 'b xyz 1 -> b xyz') | |
| # apply the shifts | |
| updated_particle_positions = particle_positions + local_shifts | |
| # transform the reference rotation to find the new particle orientation | |
| particle_orientations = particle_orientations @ rotation | |
| return updated_particle_positions, particle_orientations | |
| def get_extraction_xyz(df, idx): | |
| headings = ( | |
| 'rlnCoordinateX', | |
| 'rlnCoordinateY', | |
| 'rlnCoordinateZ' | |
| ) | |
| return df.loc[idx, headings].to_numpy() | |
| def get_relion_shifts(df, idx): | |
| headings = ( | |
| 'rlnOriginXAngst', | |
| 'rlnOriginYAngst', | |
| 'rlnOriginZAngst' | |
| ) | |
| return df.loc[idx, headings].to_numpy() | |
| def get_eulers(df, idx): | |
| headings = ( | |
| 'rlnAngleRot', | |
| 'rlnAngleTilt', | |
| 'rlnAnglePsi' | |
| ) | |
| return df.loc[idx, headings].to_numpy() | |
| def get_active_rotation_matrices(df, idx): | |
| zyz_intrinsic_euler_angles = get_eulers(df, idx) | |
| rotations = R.from_euler( | |
| seq='ZYZ', | |
| angles=zyz_intrinsic_euler_angles, | |
| degrees=True | |
| ).inv() # passive -> active | |
| return rotations.as_matrix() | |
| def get_particle_pose(df, idx): | |
| particle_extraction_positions_px = get_extraction_xyz(df, idx) | |
| particle_shifts_ang = get_relion_shifts(df, idx) | |
| particle_shifts_px = particle_shifts_ang / VOLUME_VOXEL_SPACING | |
| particle_positions_px = particle_extraction_positions_px - particle_shifts_px | |
| particle_rotation_matrices = get_active_rotation_matrices(df, idx) | |
| return particle_positions_px, particle_rotation_matrices | |
| def active_rotation_matrix_to_relion_euler_angles(rotation_matrix): | |
| rotation = R.from_matrix(rotation_matrix).inv() | |
| return rotation.as_euler(seq='ZYZ', degrees=True) | |
| if __name__ == '__main__': | |
| star = starfile.read(PARTICLE_STAR_FILE) | |
| df = star['particles'] | |
| chimerax_view_matrices = read_chimerax_matrices(CHIMERAX_VIEW_MATRIX_OUTPUT_FILE) | |
| for model_number, chimerax_view_matrix in chimerax_view_matrices.items(): | |
| class_number = MODEL_TO_CLASS[model_number] | |
| idx = df['rlnClassNumber'] == class_number | |
| # get particle info for class | |
| particle_extraction_positions_px = get_extraction_xyz(df, idx) | |
| particle_positions_px, particle_rotation_matrices = get_particle_pose(df, idx) | |
| # decompose chimerax matrix and apply shift and rotation | |
| class_shift_ang, class_rotation = decompose_chimerax_matrix_for_relion(chimerax_view_matrix) | |
| class_shift_px = class_shift_ang / VOLUME_VOXEL_SPACING | |
| class_positions_px, class_rotation_matrices = shift_then_rotate_particles( | |
| particle_positions=particle_positions_px, | |
| particle_orientations=particle_rotation_matrices, | |
| shift=class_shift_px, | |
| rotation=class_rotation | |
| ) | |
| class_euler_angles = active_rotation_matrix_to_relion_euler_angles(class_rotation_matrices) | |
| # we don't want to reextract particles | |
| # instead, find the shifts relative to the original extraction position | |
| class_shifts_px = class_positions_px - particle_extraction_positions_px | |
| class_shifts_ang = class_shifts_px * VOLUME_VOXEL_SPACING | |
| # update dataframe | |
| df.loc[idx, ('rlnOriginXAngst', 'rlnOriginYAngst', 'rlnOriginZAngst')] = -class_shifts_ang | |
| df.loc[idx, ('rlnAngleRot', 'rlnAngleTilt', 'rlnAnglePsi')] = class_euler_angles | |
| starfile.write({'optics': star['optics'], 'particles': df.loc[idx, :]}, | |
| f'output_class{class_number}_data.star', overwrite=True) | |
| starfile.write({'optics': star['optics'], 'particles': df}, OUTPUT_STAR_FILE, overwrite=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment