ESMFold2 / fastplms /models /esmfold2 /esmfold2_aligner.py
lhallee's picture
Update FastPLMs runtime files
74b8b2a verified
Raw
History Blame Contribute Delete
2.7 kB
"""Rigid alignment for structure dataclasses."""
from __future__ import annotations
from dataclasses import Field, replace
from typing import Any, ClassVar, Protocol, TypeVar
import numpy as np
import torch
from torch import Tensor
from .esmfold2_protein_structure import compute_affine_and_rmsd
class Alignable(Protocol):
"""Minimum structure interface accepted by :class:`Aligner`."""
__dataclass_fields__: ClassVar[dict[str, Field[Any]]]
@property
def atom37_positions(self) -> np.ndarray: ...
@property
def atom37_mask(self) -> np.ndarray: ...
def __len__(self) -> int: ...
AlignableT = TypeVar("AlignableT", bound=Alignable)
def _coordinate_batch(structure: Alignable) -> Tensor:
return torch.as_tensor(structure.atom37_positions, dtype=torch.double).unsqueeze(0)
def _shared_atom_mask(mobile: Alignable, target: Alignable, backbone_only: bool) -> Tensor:
shared = np.asarray(mobile.atom37_mask, dtype=bool) & np.asarray(
target.atom37_mask,
dtype=bool,
)
if backbone_only:
shared = shared.copy()
shared[:, 3:] = False
return torch.from_numpy(shared).unsqueeze(0)
class Aligner:
"""Fit a mobile structure onto a target with masked Kabsch alignment."""
def __init__(
self,
mobile: Alignable,
target: Alignable,
only_use_backbone: bool = False,
use_reflection: bool = False,
) -> None:
if len(mobile) != len(target):
raise AssertionError("mobile and target must contain the same residue count")
mobile_coordinates = _coordinate_batch(mobile)
target_coordinates = _coordinate_batch(target)
if use_reflection:
target_coordinates = -target_coordinates
atom_mask = _shared_atom_mask(mobile, target, only_use_backbone)
self._affine3D, rmsd = compute_affine_and_rmsd(
mobile_coordinates,
target_coordinates,
atom_exists_mask=atom_mask,
)
self._rmsd = rmsd.item()
@property
def rmsd(self) -> float:
return self._rmsd
def apply(self, mobile: AlignableT) -> AlignableT:
"""Return a dataclass copy with all present atom coordinates aligned."""
present = np.asarray(mobile.atom37_mask, dtype=bool)
packed = torch.as_tensor(
mobile.atom37_positions[present],
dtype=torch.float32,
).unsqueeze(0)
aligned = self._affine3D.apply(packed).squeeze(0).cpu().numpy()
atom37_positions = np.full_like(mobile.atom37_positions, np.nan)
atom37_positions[present] = aligned
return replace(mobile, atom37_positions=atom37_positions)