AlphaFold3 / flax_model /alphafold3 /structure /bioassemblies.py
OneScience's picture
Upload folder using huggingface_hub
35cdf53 verified
Raw
History Blame Contribute Delete
12.7 kB
"""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 # shape: (3,)
rot: np.ndarray # shape: (3, 3)
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."""
# The sequence of operations that form the transform. These will be applied
# right-to-left (last-to-first).
operations: Sequence[Operation]
# The chain IDs that this transform should be applied to. These are
# label_asym_ids in the mmCIF spec.
chain_ids: Sequence[str]
# A mapping from chain IDs (of chains that participate in this transform)
# to their new values in the bioassembly.
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."""
# Not all of these columns are required for internal operations, but all
# should be present whenever bioassemblies are defined in an mmCIF to stay
# consistent with external mmCIFs.
_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 is unlike the other two tables because it can
# have multiple rows share the same assembly ID. This can happen when an
# assembly is constructed by applying different sets of transforms to
# different sets of chain IDs. Each of these would have its own row.
# Here we group rows by their assembly_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,
})
# We provide these separately to keep track of the original order that they
# appear in the mmCIF.
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."""
# The first assembly is usually (though not always) the best choice.
# If we find a better heuristic for picking bioassemblies then this
# method should be updated.
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))
# We start assigning new chain IDs by finding the largest chain ID in
# the original structure that is involved in this bioassembly, and then
# starting from the next one.
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:
# The first time we see a label_asym_id we don't need to rename it.
# This isn't strictly necessary since we don't provide any
# guarantees about chain naming after bioassembly extraction but
# can make it a bit easier to inspect and compare structures
# pre and post bioassembly extraction.
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) # pytype: disable=unsupported-operands
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),
)