|
|
|
|
| """Utilities for parsing and manipulating bioassembly data.""" |
|
|
| from collections.abc import Mapping, Sequence |
| import copy |
| import dataclasses |
| from typing import Self |
|
|
| from flax_model.alphafold3.structure import mmcif |
| import numpy as np |
|
|
|
|
| @dataclasses.dataclass(frozen=True) |
| class Operation: |
| """A rigid transformation operation.""" |
|
|
| trans: np.ndarray |
| rot: np.ndarray |
|
|
| def apply_to_coords(self, coords: np.ndarray) -> np.ndarray: |
| """Applies the rotation followed by the translation to `coords`.""" |
| return np.dot(coords, self.rot.T) + self.trans[np.newaxis, :] |
|
|
|
|
| @dataclasses.dataclass(frozen=True) |
| class Transform: |
| """A rigid transformation composed of a sequence of `Operation`s.""" |
|
|
| |
| |
| operations: Sequence[Operation] |
|
|
| |
| |
| chain_ids: Sequence[str] |
|
|
| |
| |
| chain_id_rename_map: Mapping[str, str] |
|
|
| def apply_to_coords(self, coords: np.ndarray) -> np.ndarray: |
| """Applies the `operations` in right-to-left order.""" |
| for operation in reversed(self.operations): |
| coords = operation.apply_to_coords(coords) |
| return coords |
|
|
|
|
| def _get_operation(oper_data: Mapping[str, str]) -> Operation: |
| """Parses an `Operation` from a mmCIF _pdbx_struct_oper_list row.""" |
| trans = np.zeros((3,), dtype=np.float32) |
| rot = np.zeros((3, 3), dtype=np.float32) |
| for i in range(3): |
| trans[i] = float(oper_data[f'_pdbx_struct_oper_list.vector[{i + 1}]']) |
| for i in range(3): |
| for j in range(3): |
| rot[i][j] = float( |
| oper_data[f'_pdbx_struct_oper_list.matrix[{i + 1}][{j + 1}]'] |
| ) |
| return Operation(trans=trans, rot=rot) |
|
|
|
|
| class MissingBioassemblyDataError(Exception): |
| """Raised when bioassembly data is missing from an mmCIF.""" |
|
|
|
|
| class BioassemblyData: |
| """Stores and processes bioassembly data from mmCIF tables.""" |
|
|
| |
| |
| |
| _REQUIRED_COLUMNS = ( |
| '_pdbx_struct_assembly.id', |
| '_pdbx_struct_assembly.details', |
| '_pdbx_struct_assembly.method_details', |
| '_pdbx_struct_assembly.oligomeric_details', |
| '_pdbx_struct_assembly.oligomeric_count', |
| '_pdbx_struct_assembly_gen.assembly_id', |
| '_pdbx_struct_assembly_gen.oper_expression', |
| '_pdbx_struct_assembly_gen.asym_id_list', |
| '_pdbx_struct_oper_list.id', |
| '_pdbx_struct_oper_list.type', |
| '_pdbx_struct_oper_list.name', |
| '_pdbx_struct_oper_list.symmetry_operation', |
| '_pdbx_struct_oper_list.matrix[1][1]', |
| '_pdbx_struct_oper_list.matrix[1][2]', |
| '_pdbx_struct_oper_list.matrix[1][3]', |
| '_pdbx_struct_oper_list.vector[1]', |
| '_pdbx_struct_oper_list.matrix[2][1]', |
| '_pdbx_struct_oper_list.matrix[2][2]', |
| '_pdbx_struct_oper_list.matrix[2][3]', |
| '_pdbx_struct_oper_list.vector[2]', |
| '_pdbx_struct_oper_list.matrix[3][1]', |
| '_pdbx_struct_oper_list.matrix[3][2]', |
| '_pdbx_struct_oper_list.matrix[3][3]', |
| '_pdbx_struct_oper_list.vector[3]', |
| ) |
|
|
| def __init__( |
| self, |
| *, |
| pdbx_struct_assembly: Mapping[str, Mapping[str, str]], |
| pdbx_struct_assembly_gen: Mapping[str, Sequence[Mapping[str, str]]], |
| pdbx_struct_oper_list: Mapping[str, Mapping[str, str]], |
| assembly_ids: Sequence[str], |
| oper_ids: Sequence[str], |
| ): |
| for assembly_id in assembly_ids: |
| for table, table_name in ( |
| (pdbx_struct_assembly, '_pdbx_struct_assembly'), |
| (pdbx_struct_assembly_gen, '_pdbx_struct_assembly_gen'), |
| ): |
| if assembly_id not in table: |
| raise ValueError( |
| f'Assembly ID "{assembly_id}" missing from {table_name} ' |
| f'with keys: {table.keys()}' |
| ) |
| for oper_id in oper_ids: |
| if oper_id not in pdbx_struct_oper_list: |
| raise ValueError( |
| f'Oper ID "{oper_id}" missing from _pdbx_struct_oper_list ' |
| f'with keys: {pdbx_struct_oper_list.keys()}' |
| ) |
|
|
| self._pdbx_struct_assembly = pdbx_struct_assembly |
| self._pdbx_struct_assembly_gen = pdbx_struct_assembly_gen |
| self._pdbx_struct_oper_list = pdbx_struct_oper_list |
| self._operations = { |
| oper_id: _get_operation(oper_data) |
| for oper_id, oper_data in self._pdbx_struct_oper_list.items() |
| } |
| self._assembly_ids = assembly_ids |
| self._oper_ids = oper_ids |
|
|
| @classmethod |
| def from_mmcif(cls, cif: mmcif.Mmcif) -> Self: |
| """Constructs an instance of `BioassemblyData` from an `Mmcif` object.""" |
| for col in cls._REQUIRED_COLUMNS: |
| if col not in cif: |
| raise MissingBioassemblyDataError(col) |
|
|
| pdbx_struct_assembly = cif.extract_loop_as_dict( |
| prefix='_pdbx_struct_assembly.', index='_pdbx_struct_assembly.id' |
| ) |
| pdbx_struct_oper_list = cif.extract_loop_as_dict( |
| prefix='_pdbx_struct_oper_list.', index='_pdbx_struct_oper_list.id' |
| ) |
|
|
| |
| |
| |
| |
| |
| pdbx_struct_assembly_gen = {} |
| for assembly_id, oper_expression, asym_id_list in zip( |
| cif['_pdbx_struct_assembly_gen.assembly_id'], |
| cif['_pdbx_struct_assembly_gen.oper_expression'], |
| cif['_pdbx_struct_assembly_gen.asym_id_list'], |
| ): |
| pdbx_struct_assembly_gen.setdefault(assembly_id, []).append({ |
| '_pdbx_struct_assembly_gen.assembly_id': assembly_id, |
| '_pdbx_struct_assembly_gen.oper_expression': oper_expression, |
| '_pdbx_struct_assembly_gen.asym_id_list': asym_id_list, |
| }) |
|
|
| |
| |
| assembly_ids = cif['_pdbx_struct_assembly.id'] |
| oper_ids = cif['_pdbx_struct_oper_list.id'] |
| return cls( |
| pdbx_struct_assembly=pdbx_struct_assembly, |
| pdbx_struct_assembly_gen=pdbx_struct_assembly_gen, |
| pdbx_struct_oper_list=pdbx_struct_oper_list, |
| assembly_ids=assembly_ids, |
| oper_ids=oper_ids, |
| ) |
|
|
| @property |
| def assembly_ids(self) -> Sequence[str]: |
| return self._assembly_ids |
|
|
| def asym_id_by_assembly_chain_id(self, assembly_id: str) -> Mapping[str, str]: |
| asym_id_by_assembly_chain_id = {} |
| for transform in self.get_transforms(assembly_id): |
| for asym_id, assembly_chain_id in transform.chain_id_rename_map.items(): |
| asym_id_by_assembly_chain_id[assembly_chain_id] = asym_id |
| return asym_id_by_assembly_chain_id |
|
|
| def assembly_chain_ids_by_asym_id( |
| self, assembly_id: str |
| ) -> Mapping[str, set[str]]: |
| assembly_chain_ids_by_asym_id = {} |
| for transform in self.get_transforms(assembly_id): |
| for asym_id, assembly_chain_id in transform.chain_id_rename_map.items(): |
| assembly_chain_ids_by_asym_id.setdefault(asym_id, set()).add( |
| assembly_chain_id |
| ) |
| return assembly_chain_ids_by_asym_id |
|
|
| def get_default_assembly_id(self) -> str: |
| """Gets a default assembly ID.""" |
| |
| |
| |
| return min(self._assembly_ids) |
|
|
| def get_assembly_info(self, assembly_id: str) -> Mapping[str, str]: |
| return { |
| k.replace('_pdbx_struct_assembly.', ''): v |
| for k, v in self._pdbx_struct_assembly[assembly_id].items() |
| } |
|
|
| def get_transforms(self, assembly_id: str) -> Sequence[Transform]: |
| """Returns the transforms required to generate the given assembly.""" |
| partial_transforms = [] |
| all_chain_ids = set() |
| for row in self._pdbx_struct_assembly_gen[assembly_id]: |
| oper_expression = row['_pdbx_struct_assembly_gen.oper_expression'] |
| parsed_oper_id_seqs = mmcif.parse_oper_expr(oper_expression) |
| label_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split(',') |
| all_chain_ids |= set(label_asym_ids) |
| for parsed_oper_id_seq in parsed_oper_id_seqs: |
| partial_transforms.append((parsed_oper_id_seq, label_asym_ids)) |
|
|
| |
| |
| |
| max_int_chain_id = max(mmcif.str_id_to_int_id(c) for c in all_chain_ids) |
| next_int_chain_id = max_int_chain_id + 1 |
|
|
| transforms = [] |
| has_been_renamed = set() |
| for parsed_oper_id_seq, label_asym_ids in partial_transforms: |
| chain_id_rename_map = {} |
| for label_asym_id in label_asym_ids: |
| if label_asym_id not in has_been_renamed: |
| |
| |
| |
| |
| |
| chain_id_rename_map[label_asym_id] = label_asym_id |
| has_been_renamed.add(label_asym_id) |
| else: |
| chain_id_rename_map[label_asym_id] = mmcif.int_id_to_str_id( |
| next_int_chain_id |
| ) |
| next_int_chain_id += 1 |
| transforms.append( |
| Transform( |
| operations=[ |
| self._operations[oper_id] for oper_id in parsed_oper_id_seq |
| ], |
| chain_ids=label_asym_ids, |
| chain_id_rename_map=chain_id_rename_map, |
| ) |
| ) |
| return transforms |
|
|
| def to_mmcif_dict(self) -> Mapping[str, Sequence[str]]: |
| """Returns the bioassembly data as a dict suitable for `mmcif.Mmcif`.""" |
| mmcif_dict = {} |
| for assembly_id in self._assembly_ids: |
| for column, val in self._pdbx_struct_assembly[assembly_id].items(): |
| mmcif_dict.setdefault(column, []).append(val) |
| for row in self._pdbx_struct_assembly_gen[assembly_id]: |
| for column, val in row.items(): |
| mmcif_dict.setdefault(column, []).append(val) |
| for oper_id in self._oper_ids: |
| for column, val in self._pdbx_struct_oper_list[oper_id].items(): |
| mmcif_dict.setdefault(column, []).append(val) |
| return mmcif_dict |
|
|
| def rename_label_asym_ids( |
| self, |
| mapping: Mapping[str, str], |
| present_chains: set[str], |
| ) -> Self: |
| """Returns a new BioassemblyData with renamed label_asym_ids. |
| |
| Args: |
| mapping: A mapping from original label_asym_ids to their new values. Any |
| label_asym_ids in this BioassemblyData that are not in this mapping will |
| remain unchanged. |
| present_chains: A set of label_asym_ids that are actually present in the |
| atom site list. All label_asym_ids that are in the BioassemblyData but |
| not in present_chains won't be included in the output BioassemblyData. |
| |
| Returns: |
| A new BioassemblyData with renamed label_asym_ids. |
| |
| Raises: |
| ValueError: If any two previously distinct chains do not have unique names |
| anymore after the rename. |
| """ |
| new_pdbx_struct_assembly_gen = copy.deepcopy(self._pdbx_struct_assembly_gen) |
| for rows in new_pdbx_struct_assembly_gen.values(): |
| for row in rows: |
| old_asym_ids = row['_pdbx_struct_assembly_gen.asym_id_list'].split(',') |
| new_asym_ids = [ |
| mapping.get(label_asym_id, label_asym_id) |
| for label_asym_id in old_asym_ids |
| if label_asym_id in present_chains |
| ] |
| if len(set(old_asym_ids) & present_chains) != len(set(new_asym_ids)): |
| raise ValueError( |
| 'Can not rename chains, the new names are not unique: ' |
| f'{sorted(new_asym_ids)}.' |
| ) |
| row['_pdbx_struct_assembly_gen.asym_id_list'] = ','.join(new_asym_ids) |
|
|
| return BioassemblyData( |
| pdbx_struct_assembly=copy.deepcopy(self._pdbx_struct_assembly), |
| pdbx_struct_assembly_gen=new_pdbx_struct_assembly_gen, |
| pdbx_struct_oper_list=copy.deepcopy(self._pdbx_struct_oper_list), |
| assembly_ids=copy.deepcopy(self._assembly_ids), |
| oper_ids=copy.deepcopy(self._oper_ids), |
| ) |
|
|