English
OneScience
protein structure generation
La-Proteina / models /nn /feature_factory.py
anzhi2710gmailcom's picture
Upload folder using huggingface_hub
8e04e6f verified
Raw
History Blame Contribute Delete
78.5 kB
import gzip
import math
import os
import random
from typing import Dict, List, Literal
import einops
import torch
from jaxtyping import Float
from loguru import logger
from torch.nn import functional as F
from torch_scatter import scatter_mean
from onescience.datapipes.openfold import data_transforms
from onescience.utils.openfold.np.residue_constants import atom_types
from models.utils.angle_utils import bond_angles, signed_dihedral_angle
from models.utils.fold_utils import extract_cath_code_by_level
from torch.nn.utils.rnn import pad_sequence
################################
# # Some auxiliary functions # #
################################
# From frameflow code
def get_index_embedding(indices, edim, max_len=2056):
"""Creates sine / cosine positional embeddings from a prespecified indices.
Args:
indices: offsets of type integer, shape either [n] or [b, n].
edim: dimension of the embeddings to create.
max_len: maximum length.
Returns:
positional embedding of shape either [n, edim] or [b, n, edim]
"""
# indices [n] of [b, n]
K = torch.arange(edim // 2, device=indices.device) # [edim / 2]
if len(indices.shape) == 1: # [n]
K = K[None, ...]
elif len(indices.shape) == 2: # [b, n]
K = K[None, None, ...]
pos_embedding_sin = torch.sin(
indices[..., None] * math.pi / (max_len ** (2 * K / edim))
).to(indices.device)
# [n, 1] / [1, edim/2] -> [n, edim/2] or [b, n, 1] / [1, 1, edim/2] -> [b, n, edim/2]
pos_embedding_cos = torch.cos(
indices[..., None] * math.pi / (max_len ** (2 * K / edim))
).to(indices.device)
pos_embedding = torch.cat(
[pos_embedding_sin, pos_embedding_cos], axis=-1
) # [n, edim]
return pos_embedding
def get_time_embedding(
t: Float[torch.Tensor, "b"], edim: int, max_positions: int = 2000
) -> Float[torch.Tensor, "b embdim"]:
"""
Code from Frameflow, which got it from
https://github.com/hojonathanho/diffusion/blob/master/diffusion_tf/nn.py
Creates embedding for a given vector of times t.
Args:
t: vector of times (float) of shape [b].
edim: dimension of the embeddings.
max_positions: ...
Returns:
Embedding for the vector t of shape [b, edim]
"""
assert len(t.shape) == 1
t = t * max_positions
half_dim = edim // 2
emb = math.log(max_positions) / (half_dim - 1)
emb = torch.exp(torch.arange(half_dim, dtype=torch.float32, device=t.device) * -emb)
emb = t.float()[:, None] * emb[None, :]
emb = torch.cat([torch.sin(emb), torch.cos(emb)], dim=1)
if edim % 2 == 1: # zero pad
emb = F.pad(emb, (0, 1), mode="constant")
assert emb.shape == (t.shape[0], edim)
return emb
def bin_pairwise_distances(x, min_dist, max_dist, dim):
"""
Takes coordinates and bins the pairwise distances.
Args:
x: Coordinates of shape [b, n, 3]
min_dist: Right limit of first bin
max_dist: Left limit of last bin
dim: Dimension of the final one hot vectors
Returns:
Tensor of shape [b, n, n, dim] consisting of one-hot vectors
"""
pair_dists_nm = torch.norm(x[:, :, None, :] - x[:, None, :, :], dim=-1) # [b, n, n]
bin_limits = torch.linspace(
min_dist, max_dist, dim - 1, device=x.device
) # Open left and right
return bin_and_one_hot(pair_dists_nm, bin_limits) # [b, n, n, pair_dist_dim]
def bin_and_one_hot(tensor, bin_limits):
"""
Converts a tensor of shape [*] to a tensor of shape [*, d] using the given bin limits.
Args:
tensor (Tensor): Input tensor of shape [*]
bin_limits (Tensor): bin limits [l1, l2, ..., l_{d-1}]. d-1 limits define
d-2 bins, and the first one is <l1, the last one is >l_{d-1}, giving a total of d bins.
Returns:
torch.Tensor: Output tensor of shape [*, d] where d = len(bin_limits) + 1
"""
bin_indices = torch.bucketize(tensor, bin_limits)
return torch.nn.functional.one_hot(bin_indices, len(bin_limits) + 1) * 1.0
def indices_force_start_w_one(pdb_idx, mask):
"""
Takes a tensor with pdb indices for a batch and forces them all to start with the index 1.
Masked elements are still assigned the index -1.
Args:
pdb_idx: tensor of increasing integers (except masked ones fixed to -1), shape [b, n]
mask: binary tensor, shape [b, n]
Returns:
pdb_idx but now all rows start at 1, masked elements are still set to -1.
"""
first_val = pdb_idx[:, 0][:, None] # min val is the first one
pdb_idx = pdb_idx - first_val + 1
pdb_idx = torch.masked_fill(pdb_idx, ~mask, -1) # set masked elements to -1
return pdb_idx
################################
# # Classes for each feature # #
################################
class Feature(torch.nn.Module):
"""Base class for features."""
def __init__(self, dim: int):
super().__init__()
self.dim = dim
def get_dim(self):
return self.dim
def forward(self, batch: Dict):
pass # Implemented by each class
def extract_bs_and_n(self, batch: Dict):
"""
Extracts batch size and n from input batch.
Once we add more modalities just try modaliites until you hit one.
Other option is to pass arguments to nn which modalities are used.
"""
if "x_t" in batch:
if "bb_ca" in batch["x_t"]:
v = batch["x_t"]["bb_ca"] # [b, n, 3]
elif "coords" in batch:
v = batch["coords"] # [b, n, 37, 3]
elif "z_latent" in batch:
v = batch["z_latent"] # [b, n, latent_dim]
else:
raise IOError("Don't know how to extract batch size and n from batch...")
bs, n = v.shape[0], v.shape[1]
return bs, n
def extract_device(self, batch: Dict):
"""Extracts device from input batch."""
if "x_t" in batch:
if "bb_ca" in batch["x_t"]:
v = batch["x_t"]["bb_ca"] # [b, n, 3]
elif "coords" in batch:
v = batch["coords"] # [b, n, 37, 3]
elif "z_latent" in batch:
v = batch["z_latent"] # [b, n, latent_dim]
else:
raise IOError("Don't know how to extract device from batch...")
return v.device
def assert_defaults_allowed(self, batch: Dict, ftype: str):
"""Raises error if default features should not be used to fill-up missing features in the current batch."""
if "strict_feats" in batch:
if batch["strict_feats"]:
raise IOError(
f"{ftype} feature requested but no appropriate feature provided. "
"Make sure to include the relevant transform in the data config."
)
class ZeroFeat(Feature):
"""Computes empty feature (zero) of shape [b, n, dim] or [b, n, n, dim],
depending on sequence or pair features."""
def __init__(
self,
dim_feats_out=128,
mode: Literal["seq", "pair"] = "seq",
name=None,
**kwargs,
):
super().__init__(dim=128)
self.mode = mode
def forward(self, batch):
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if self.mode == "seq":
return torch.zeros((b, n, self.dim), device=device)
elif self.mode == "pair":
torch.zeros((b, n, n, self.dim_feats_out), device=device)
else:
raise IOError(f"Mode {self.mode} wrong for zero feature")
class CroppedFlagSeqFeat(Feature):
"""Computes feature of shape [b, n, 1] indicating if protein is cropped
(1s if it is, 0s if it isn't)."""
def __init__(self):
super().__init__(dim=1)
def forward(self, batch):
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if "cropped" in batch:
ones = torch.ones((b, n, self.dim), device=device)
cropped = batch["cropped"] # boolean [b]
return ones * cropped[..., None, None] # [b, n, dim(=1)]
else:
return torch.zeros((b, n, self.dim), device=device)
class FoldEmbeddingSeqFeat(Feature):
"""Computes fold class embedding and returns as sequence feature of shape [b, n, fold_emb_dim * 3]."""
def __init__(
self,
fold_emb_dim,
cath_code_dir,
multilabel_mode="sample",
fold_nhead=4,
fold_nlayer=2,
**kwargs,
):
"""
multilabel_mode (["sample", "average", "transformer"]): Schemes to handle multiple fold labels
"sample": randomly sample one label
"average": average fold embeddings over all labels
"transformer": pad labels together and feed into a transformer, take the average over the output
"""
super().__init__(dim=fold_emb_dim * 3)
self.create_mapping(cath_code_dir)
self.embedding_C = torch.nn.Embedding(
self.num_classes_C + 1, fold_emb_dim
) # The last class is left as null embedding
self.embedding_fA = torch.nn.Embedding(self.num_classes_A + 1, fold_emb_dim)
self.embedding_T = torch.nn.Embedding(self.num_classes_T + 1, fold_emb_dim)
self.register_buffer("_device_param", torch.tensor(0), persistent=False)
assert multilabel_mode in ["sample", "average", "transformer"]
self.multilabel_mode = multilabel_mode
if multilabel_mode == "transformer":
encoder_layer = torch.nn.TransformerEncoderLayer(
fold_emb_dim * 3,
nhead=fold_nhead,
dim_feedforward=fold_emb_dim * 3,
batch_first=True,
)
self.transformer = torch.nn.TransformerEncoder(encoder_layer, fold_nlayer)
@property
def device(self):
return next(self.buffers()).device
def create_mapping(self, cath_code_dir):
"""Create cath label vocabulary for C, A, T levels."""
mapping_file = os.path.join(cath_code_dir, "cath_label_mapping.pt")
if os.path.exists(mapping_file):
class_mapping = torch.load(mapping_file)
else:
cath_code_file = os.path.join(cath_code_dir, "cath-b-newest-all.gz")
cath_code_set = {"C": set(), "A": set(), "T": set()}
with gzip.open(cath_code_file, "rt") as f:
for line in f:
cath_id, cath_version, cath_code, cath_segment_and_chain = (
line.strip().split()
)
cath_code_set["C"].add(extract_cath_code_by_level(cath_code, "C"))
cath_code_set["A"].add(extract_cath_code_by_level(cath_code, "A"))
cath_code_set["T"].add(extract_cath_code_by_level(cath_code, "T"))
class_mapping = {
"C": {k: i for i, k in enumerate(sorted(list(cath_code_set["C"])))},
"A": {k: i for i, k in enumerate(sorted(list(cath_code_set["A"])))},
"T": {k: i for i, k in enumerate(sorted(list(cath_code_set["T"])))},
}
torch.save(class_mapping, mapping_file)
self.class_mapping_C = class_mapping["C"]
self.class_mapping_A = class_mapping["A"]
self.class_mapping_T = class_mapping["T"]
self.num_classes_C = len(self.class_mapping_C)
self.num_classes_A = len(self.class_mapping_A)
self.num_classes_T = len(self.class_mapping_T)
def parse_label(self, cath_code_list):
"""Parse cath_code into corresponding indices at C, A, T levels
Args:
cath_code_list (List[List[str]]): List of cath codes for each protein. Each protein can have no, one or multiple labels.
Return:
results: for each label of each protein, return its C, A, T label indices
"""
results = []
for cath_codes in cath_code_list:
result = []
for cath_code in cath_codes:
result.append(
[
self.class_mapping_C.get(
extract_cath_code_by_level(cath_code, "C"),
self.num_classes_C,
), # If unknown or masked, set as null
self.class_mapping_A.get(
extract_cath_code_by_level(cath_code, "A"),
self.num_classes_A,
),
self.class_mapping_T.get(
extract_cath_code_by_level(cath_code, "T"),
self.num_classes_T,
),
]
)
if len(cath_codes) == 0:
result = [
[self.num_classes_C, self.num_classes_A, self.num_classes_T]
] # If no cath code is provided, return null
results.append(result)
return results # [b, num_label, 3]
def sample(self, cath_code_list):
"""Randomly sample one cath code"""
results = []
for cath_codes in cath_code_list:
idx = random.randint(0, len(cath_codes) - 1)
results.append(cath_codes[idx])
return results
def flatten(self, cath_code_list):
"""Flatten variable lengths of cath codes into a long cath code tensor"""
results = []
batch_id = []
for i, cath_codes in enumerate(cath_code_list):
results += cath_codes
batch_id += [i] * len(cath_codes)
results = torch.as_tensor(results, device=self.device)
batch_id = torch.as_tensor(batch_id, device=self.device)
return results, batch_id
def pad(self, cath_code_list):
"""Pad variable lengths of cath codes into a batched cath code tensor"""
results = []
max_num_label = 0
for cath_codes in cath_code_list:
results.append(cath_codes)
max_num_label = max(max_num_label, len(cath_codes))
mask = []
for i in range(len(results)):
mask_i = [False] * len(results[i])
if len(results[i]) < max_num_label:
mask_i += [True] * (max_num_label - len(results[i]))
results[i] += [
[self.num_classes_C, self.num_classes_A, self.num_classes_T]
] * (max_num_label - len(results[i]))
mask.append(mask_i)
results = torch.as_tensor(results, device=self.device)
mask = torch.as_tensor(mask, device=self.device)
return results, mask
def forward(self, batch):
bs, n = self.extract_bs_and_n(batch)
if "cath_code" not in batch:
cath_code = [
["x.x.x.x"]
] * bs # If no cath code provided, return null embeddings
else:
cath_code = batch["cath_code"]
cath_code_list = self.parse_label(cath_code)
if self.multilabel_mode == "sample":
cath_code_list = self.sample(
cath_code_list
) # Random sample one label for each protein
cath_code = torch.as_tensor(cath_code_list, device=self.device) # [b, 3]
fold_emb = torch.cat(
[
self.embedding_C(cath_code[:, 0]),
self.embedding_A(cath_code[:, 1]),
self.embedding_T(cath_code[:, 2]),
],
dim=-1,
) # [b, fold_emb_dim * 3]
elif self.multilabel_mode == "average":
cath_code, batch_id = self.flatten(cath_code_list)
fold_emb = torch.cat(
[
self.embedding_C(cath_code[:, 0]),
self.embedding_A(cath_code[:, 1]),
self.embedding_T(cath_code[:, 2]),
],
dim=-1,
) # [num_code, fold_emb_dim * 3]
fold_emb = scatter_mean(fold_emb, batch_id, dim=0, dim_size=bs)
elif self.multilabel_mode == "transformer":
cath_code, mask = self.pad(cath_code_list)
fold_emb = torch.cat(
[
self.embedding_C(cath_code[:, :, 0]),
self.embedding_A(cath_code[:, :, 1]),
self.embedding_T(cath_code[:, :, 2]),
],
dim=-1,
) # [b, max_num_label, fold_emb_dim * 3]
fold_emb = self.transformer(
fold_emb, src_key_padding_mask=mask
) # [b, max_num_label, fold_emb_dim * 3]
fold_emb = (fold_emb * (~mask[:, :, None]).float()).sum(dim=1) / (
(~mask[:, :, None]).float().sum(dim=1) + 1e-10
) # [b, fold_emb_dim * 3]
fold_emb = fold_emb[:, None, :] # [b, 1, fold_emb_dim * 3]
return fold_emb.expand(
(fold_emb.shape[0], n, fold_emb.shape[2])
) # [b, n, fold_emb_dim * 3]
class TimeEmbeddingSeqFeat(Feature):
"""Computes time embedding and returns as sequence feature of shape [b, n, t_emb_dim]."""
def __init__(self, data_mode_use, t_emb_dim, **kwargs):
super().__init__(dim=t_emb_dim)
self.data_mode_use = data_mode_use
def forward(self, batch):
t = batch["t"][self.data_mode_use] # [b]
_, n = self.extract_bs_and_n(batch)
t_emb = get_time_embedding(t, edim=self.dim) # [b, t_emb_dim]
t_emb = t_emb[:, None, :] # [b, 1, t_emb_dim]
return t_emb.expand((t_emb.shape[0], n, t_emb.shape[2])) # [b, n, t_emb_dim]
class TimeEmbeddingPairFeat(Feature):
"""Computes time embedding and returns as pair feature of shape [b, n, n, t_emb_dim]."""
def __init__(self, data_mode_use, t_emb_dim, **kwargs):
super().__init__(dim=t_emb_dim)
self.data_mode_use = data_mode_use
def forward(self, batch):
t = batch["t"][self.data_mode_use] # [b]
_, n = self.extract_bs_and_n(batch)
t_emb = get_time_embedding(t, edim=self.dim) # [b, t_emb_dim]
t_emb = t_emb[:, None, None, :] # [b, 1, 1, t_emb_dim]
return t_emb.expand((t_emb.shape[0], n, n, t_emb.shape[3])) # [b, n, t_emb_dim]
class IdxEmbeddingSeqFeat(Feature):
"""Computes index embedding and returns sequence feature of shape [b, n, idx_emb]."""
def __init__(self, idx_emb_dim, **kwargs):
super().__init__(dim=idx_emb_dim)
def forward(self, batch):
if "residue_pdb_idx" in batch:
inds = batch["residue_pdb_idx"] # [b, n]
inds = indices_force_start_w_one(inds, batch["mask"])
else:
self.assert_defaults_allowed(batch, "Residue index sequence")
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
inds = torch.Tensor([[i + 1 for i in range(n)] for _ in range(b)]).to(
device
) # [b, n]
return get_index_embedding(inds, edim=self.dim) # [b, n, idx_embed_dim]
class ChainBreakPerResidueSeqFeat(Feature):
"""Computes a 1D sequence feature indicating if a residue is followed by a chain break, shape [b, n, 1]."""
def __init__(self, **kwargs):
super().__init__(dim=1)
def forward(self, batch):
if "chain_breaks_per_residue" in batch:
chain_breaks = batch["chain_breaks_per_residue"] * 1.0 # [b, n]
else:
self.assert_defaults_allowed(batch, "Chain break sequence")
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
chain_breaks = torch.zeros((b, n), device=device) * 1.0 # [b, n]
return chain_breaks[..., None] # [b, n, 1]
class XscBBCASeqFeat(Feature):
"""Computes feature from backbone CA self conditining coordinates, seq feature of shape [b, n, 3]."""
def __init__(self, mode_key="x_sc", **kwargs):
super().__init__(dim=3)
self.mode_key = mode_key
self._has_logged = False
def forward(self, batch):
if self.mode_key in batch:
data_modes_avail = [k for k in batch[self.mode_key]]
assert (
"bb_ca" in data_modes_avail
), f"`bb_ca` sc/recycle seq feature requested but key not available in data modes {data_modes_avail}"
return batch[self.mode_key]["bb_ca"] # [b, n, 3]
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
f"No {self.mode_key} in batch, returning zeros for XscBBCASeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, 3, device=device)
class XscLocalLatentsSeqFeat(Feature):
"""Computes feature from local latents self conditining, seq feature of shape [b, n, dim]."""
def __init__(self, latent_dim, mode_key="x_sc", **kwargs):
super().__init__(dim=latent_dim)
self.mode_key = mode_key
self._has_logged = False
def forward(self, batch):
if self.mode_key in batch:
data_modes_avail = [k for k in batch[self.mode_key]]
assert (
"local_latents" in data_modes_avail
), f"`local_latents` sc/recycle seq feature requested but key not available in data modes {data_modes_avail}"
return batch[self.mode_key]["local_latents"] # [b, n, latent_dim]
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
f"No {self.mode_key} in batch, returning zeros for XscLocalLatentsSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class XtBBCASeqFeat(Feature):
"""Computes feature from backbone CA x_t coordinates, seq feature of shape [b, n, 3]."""
def __init__(self, **kwargs):
super().__init__(dim=3)
def forward(self, batch):
data_modes_avail = [k for k in batch["x_t"]]
assert (
"bb_ca" in data_modes_avail
), f"`bb_ca` seq feat feature requested but key not available in data modes {data_modes_avail}"
return batch["x_t"]["bb_ca"] # [b, n, 3]
class XtLocalLatentsSeqFeat(Feature):
"""Computes feature from backbone CA x_t coordinates, seq feature of shape [b, n, 3]."""
def __init__(self, latent_dim, **kwargs):
super().__init__(dim=latent_dim)
def forward(self, batch):
data_modes_avail = [k for k in batch["x_t"]]
assert (
"local_latents" in data_modes_avail
), f"`local_latents` seq feat feature requested but key not available in data modes {data_modes_avail}"
return batch["x_t"]["local_latents"] # [b, n, latent_dim]
class CaCoorsNanometersSeqFeat(Feature):
"""Computes feature from ca coordinates, seq feature of shape [b, n, 3]."""
def __init__(self, **kwargs):
super().__init__(dim=3)
def forward(self, batch):
assert (
"ca_coors_nm" in batch or "coords_nm" in batch
), "`ca_coors_nm` nor `coords_nm` in batch, cannot compute CaCoorsNanometersSeqFeat"
if "ca_coors_nm" in batch:
return batch["ca_coors_nm"] # [b, n, 3]
else:
return batch["coords_nm"][:, :, 1, :] # [b, n, 3]
class TryCaCoorsNanometersSeqFeat(CaCoorsNanometersSeqFeat):
"""
If `ca_coors_nm` in batch, returns sequence feature with CA coordinates (in nm) of shape [b, n, 3].
If `ca_coors_nm` not in batch return zero feature.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._has_logged = False
def forward(self, batch):
if "ca_coors_nm" in batch or "coords_nm" in batch:
return super().forward(batch)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No ca_coors_nm or coords_nm in batch, returning zeros for TryCaCoorsNanometersSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class OptionalCaCoorsNanometersSeqFeat(CaCoorsNanometersSeqFeat):
"""
If `use_ca_coors_nm_feature` in batch and true, returns sequence feature with CA coordinates (in nm) of shape [b, n, 3].
If `use_ca_coors_nm_feature` not in batch, defaults to False.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._has_logged = False
def forward(self, batch):
if batch.get("use_ca_coors_nm_feature", False): # defaults to False
return super().forward(batch)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"use_ca_coors_nm_feature disabled or not in batch, returning zeros for OptionalCaCoorsNanometersSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class ResidueTypeSeqFeat(Feature):
"""
Computes feature from residue type, feature of shape [b, n, 20].
Residue type is an integer in {0, 1, ..., 19}, coorsponding to the 20 aa types.
Feature is a one-hot vector of dimension 20.
Note that in residue type the padding is done with a -1, but this function
multiplies with the mask.
"""
def __init__(self, **kwargs):
super().__init__(dim=20)
def forward(self, batch):
assert (
"residue_type" in batch
), "`residue_type` not in batch, cannot compute ResidueTypeSeqFeat"
rtype = batch["residue_type"] # [b, n]
rpadmask = batch["mask_dict"]["residue_type"] # [b, n] binary
rtype = rtype * rpadmask # [b, n], the -1 padding becomes 0
rtype_onehot = F.one_hot(rtype, num_classes=20) # [b, n, 20]
rtype_onehot = (
rtype_onehot * rpadmask[..., None]
)
return rtype_onehot * 1.0
class OptionalResidueTypeSeqFeat(ResidueTypeSeqFeat):
"""
If `use_residue_type_feature` in batch and true, adds residue type feature of shape [b, n, 20].
If `use_residue_type_feature` not in batch, defaults to False.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._has_logged = False
def forward(self, batch):
if batch.get("use_residue_type_feature", False):
return super().forward(batch)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"use_residue_type_feature disabled or not in batch, returning zeros for OptionalResidueTypeSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, 20, device=device)
class Atom37NanometersCoorsSeqFeat(Feature):
"""
Computes feature from the atom37 representation (in Å), feature of shape [b, n, 37 * 4].
Atom37 has shape [b, n, 37, 3], and the appropriate mask (for the residue type) has shape
[b, n, 37]. This feature concatenates the flattened mask (shape [b, n, 37]) with the flattened coordinates (of shape
[b, n, 37 * 3])
Note that in residue type the padding is done with a -1, but this function
multiplies with the mask.
"""
def __init__(self, rel=False, **kwargs):
super().__init__(dim=int(37 * 4))
# 37 * 4, 37 * 3 for the coordinates, + 37 for the mask
self.rel = rel # whether to get features relative to CA or absolute
def forward(self, batch):
assert (
"coords_nm" in batch
), "`coords_nm` not in batch, cannot compute Atom37NanometersCoorsSeqFeat"
assert (
"coord_mask" in batch
), "`coord_mask` not in batch, cannot compute Atom37NanometersCoorsSeqFeat"
coors = batch["coords_nm"] # [b, n, 37, 3]
coors_mask = batch[
"coord_mask"
] # [b, n, 37]
coors = (
coors * coors_mask[..., None]
) # Zero-out non-atoms
if self.rel:
# If relative remove CA coordinates
ca_coors = coors[:, :, 1, :] # [b, n, 3]
coors = coors - ca_coors[:, :, None, :] # [b, n, 37, 3]
coors = coors * coors_mask[..., None]
coors_flat = einops.rearrange(
coors, "b n a t -> b n (a t)"
) # [b, n, 37, 3] -> [b, n, 37 * 3]
feat = torch.cat([coors_flat, coors_mask], dim=-1) # [b, n, 37 * 4]
return feat
class BackboneTorsionAnglesSeqFeat(Feature):
"""
Computes torsion angle and featurizes it, with binning and 1-hot.
TODO: Add mask?
"""
def __init__(self, **kwargs):
super().__init__(dim=int(3 * 21))
def forward(self, batch):
bb_torsion = self._get_bb_torsion_angles(batch) # [b, n, 3]
bb_torsion_feats = bin_and_one_hot(
bb_torsion,
torch.linspace(-torch.pi, torch.pi, 20, device=bb_torsion.device),
) # [b, n, 3, nbins], nbins in 20+1
bb_torsion_feats = einops.rearrange(
bb_torsion_feats, "b n t d -> b n (t d)"
) # [b, n, 3 * nbins]
return bb_torsion_feats
def _get_bb_torsion_angles(self, batch):
a37 = batch["coords"] # [b, n, 37, 3]
if "residue_pdb_idx" in batch and batch["residue_pdb_idx"] is not None:
idx = batch["residue_pdb_idx"] # [b, n]
else:
self.assert_defaults_allowed(batch, "Relative sequence separation pair")
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
idx = torch.Tensor([[i + 1 for i in range(n)] for _ in range(b)]).to(
device
) # [b, n]
N = a37[:, :, 0, :] # [b, n, 3]
CA = a37[:, :, 1, :] # [b, n, 3]
C = a37[:, :, 2, :] # [b, n, 3]
psi = signed_dihedral_angle(
N[:, :-1, :], CA[:, :-1, :], C[:, :-1, :], N[:, 1:, :]
) # [b, n-1]
omega = signed_dihedral_angle(
CA[:, :-1, :], C[:, :-1, :], N[:, 1:, :], CA[:, 1:, :]
) # [b, n-1]
phi = signed_dihedral_angle(
C[:, :-1, :], N[:, 1:, :], CA[:, 1:, :], C[:, 1:, :]
) # [b, n-1]
bb_angles = torch.stack([psi, omega, phi], dim=-1) # [b, n-1, 3]
good_pair = idx[:, 1:] - idx[:, :-1] == 1 # boolean [b, n-1]
bb_angles = bb_angles * good_pair[..., None] # [b, n-1, 3]
zero_pad = torch.zeros((a37.shape[0], 1, 3), device=bb_angles.device)
bb_angles = torch.cat([bb_angles, zero_pad], dim=1) # [b, n, 3]
return bb_angles
class BackboneBondAnglesSeqFeat(Feature):
"""
Computes bond angle and featurizes it, with binning and 1-hot.
TODO: Add mask?
"""
def __init__(self, **kwargs):
super().__init__(dim=int(3 * 21))
def forward(self, batch):
bb_bond_angle = self._get_bb_bond_angles(batch) # [b, n, 3]
bb_bond_angle_feats = bin_and_one_hot(
bb_bond_angle,
torch.linspace(-torch.pi, torch.pi, 20, device=bb_bond_angle.device),
) # [b, n, 3, nbins]
bb_bond_angle_feats = einops.rearrange(
bb_bond_angle_feats, "b n t d -> b n (t d)"
) # [b, n, 3 * nbins]
return bb_bond_angle_feats
def _get_bb_bond_angles(self, batch):
a37 = batch["coords"] # [b, n, 37, 3]
mask = batch["mask_dict"]["coords"][..., 0, 0] # [b, n]
if "residue_pdb_idx" in batch and batch["residue_pdb_idx"] is not None:
idx = batch["residue_pdb_idx"] # [b, n]
else:
self.assert_defaults_allowed(batch, "Relative sequence separation pair")
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
idx = torch.Tensor([[i + 1 for i in range(n)] for _ in range(b)]).to(
device
) # [b, n]
b = a37.shape[0]
N = a37[:, :, 0, :] # [b, n, 3]
CA = a37[:, :, 1, :] # [b, n, 3]
C = a37[:, :, 2, :] # [b, n, 3]
theta_1 = bond_angles(N[:, :, :], CA[:, :, :], C[:, :, :]) # [b, n]
theta_2 = bond_angles(CA[:, :-1, :], C[:, :-1, :], N[:, 1:, :]) # [b, n-1]
theta_3 = bond_angles(C[:, :-1, :], N[:, 1:, :], CA[:, 1:, :]) # [b, n-1]
# Account for chain breaks in theta_2 and theta_3
good_pair = idx[:, 1:] - idx[:, :-1] == 1 # boolean [b, n-1]
theta_2 = theta_2 * good_pair # [b, n-1]
theta_3 = theta_3 * good_pair # [b, n-1]
# Add a zero at the end of theta_2 and theta_3 to get shape [b, n]
zero_pad = torch.zeros((b, 1), device=theta_2.device) # [b, 1]
theta_2 = torch.cat([theta_2, zero_pad], dim=-1) # [b, n]
theta_3 = torch.cat([theta_3, zero_pad], dim=-1) # [b, n]
bb_angles = torch.stack([theta_1, theta_2, theta_3], dim=-1) # [b, n, 3]
return bb_angles
class OpenfoldSideChainAnglesSeqFeat(Feature):
"""Computes sequence features from side chain angles."""
def __init__(self, **kwargs):
super().__init__(dim=int(4 * 21 + 4)) # 88
def forward(self, batch):
_, angles, torsion_angles_mask = self._get_sidechain_angles(batch)
# _, [b, n, 4] and [b, n, 4]
angles_feat = bin_and_one_hot(
angles, torch.linspace(-torch.pi, torch.pi, 20, device=angles.device)
) # [b, n, 4, nbins]
angles_feat = angles_feat * torsion_angles_mask[..., None]
angles_feat = einops.rearrange(
angles_feat, "b n s d -> b n (s d)"
) # [b, n, 4 * nbins]
feat = torch.cat(
[angles_feat, torsion_angles_mask], dim=-1
) # [b, n, 4 * nbins + 4]
return feat
def _get_sidechain_angles(self, batch):
orig_dtype = batch["coords"].dtype
aatype = batch["residue_type"] # [b, n]
coords = batch["coords"].double() # [b, n, 37, 3]
atom_mask = batch["coord_mask"].double() # [b, n, 37]
p = {
"aatype": aatype,
"all_atom_positions": coords,
"all_atom_mask": atom_mask,
}
# Next function defined with curry1 decorator
p = data_transforms.atom37_to_torsion_angles(prefix="")(p)
torsion_angles_sin_cos = p["torsion_angles_sin_cos"] # [b, n, 7, 2]
alt_torsion_angles_sin_cos = p["alt_torsion_angles_sin_cos"] # [b, n, 7, 2]
# Normalize, all these vectors should have norm 1
torsion_angles_sin_cos = torsion_angles_sin_cos / (
torch.linalg.norm(torsion_angles_sin_cos, dim=-1, keepdim=True) + 1e-10
) # [b, n, 7, 2]
alt_torsion_angles_sin_cos = alt_torsion_angles_sin_cos / (
torch.linalg.norm(alt_torsion_angles_sin_cos, dim=-1, keepdim=True) + 1e-10
) # [b, n, 7, 2]
torsion_angles_mask = p["torsion_angles_mask"] # [b, n, 7]
torsion_angles_sin_cos = torsion_angles_sin_cos * torsion_angles_mask[..., None]
alt_torsion_angles_sin_cos = (
alt_torsion_angles_sin_cos * torsion_angles_mask[..., None]
)
angles = torch.atan2(
torsion_angles_sin_cos[..., 0], torsion_angles_sin_cos[..., 1]
) # [b, n, 7]
angles = angles * torsion_angles_mask
# Keep only sidechain
torsion_angles_sin_cos = torsion_angles_sin_cos[..., -4:, :] # [b, n, 4, 2]
alt_torsion_angles_sin_cos = alt_torsion_angles_sin_cos[
..., -4:, :
] # [b, n, 4, 2]
angles = angles[..., -4:] # [b, n, 4]
torsion_angles_mask = torsion_angles_mask[..., -4:] # [b, n, 4]
return (
torsion_angles_sin_cos.to(dtype=orig_dtype),
angles.to(dtype=orig_dtype),
torsion_angles_mask.bool(),
) # [b, n, 4, 2], [b, n, 4] and [b, n, 4]
class LatentVariableSeqFeat(Feature):
"""Returns sequence feature from latent variable."""
def __init__(self, latent_z_dim, **kwargs):
super().__init__(dim=latent_z_dim)
def forward(self, batch):
assert (
"z_latent" in batch
), "`z_latent` not in batch, cannot compute LatentVariableSeqFeat"
return batch["z_latent"] # [b, n, latent_dim]
class MotifAbsoluteCoordsSeqFeat(Feature):
"""Computes absolute coordinates feature from motif coordinates."""
def __init__(self, **kwargs):
super().__init__(dim=148) # 37 * 4 for absolute coords
self._has_logged = False
def forward(self, batch):
if "x_motif" in batch and "motif_mask" in batch:
batch_coors = {
"coords_nm": batch["x_motif"],
"coord_mask": batch["motif_mask"],
}
return Atom37NanometersCoorsSeqFeat(rel=False)(batch_coors)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No x_motif or motif_mask in batch, returning zeros for MotifAbsoluteCoordsSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class MotifRelativeCoordsSeqFeat(Feature):
"""Computes relative coordinates feature from motif coordinates."""
def __init__(self, **kwargs):
super().__init__(dim=148) # 37 * 4 for relative coords
self._has_logged = False
def forward(self, batch):
if "x_motif" in batch and "motif_mask" in batch and "seq_motif_mask" in batch:
required_atoms = torch.tensor(
[atom_types.index("CA")], device=batch["motif_mask"].device
) # CA
has_required_atoms = torch.all(
batch["motif_mask"][:, :, required_atoms], dim=-1
) # [batch, seq_len]
relevant_has_required_atoms = torch.where(
batch["seq_motif_mask"],
has_required_atoms,
torch.ones_like(has_required_atoms, dtype=torch.bool),
)
if not torch.all(relevant_has_required_atoms):
if not self._has_logged:
logger.warning(
"Missing required CA atoms in motif region, returning zeros for MotifRelativeCoordsSeqFeat"
)
self._has_logged = True
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
return torch.zeros(b, n, self.dim, device=device)
batch_coors = {
"coords_nm": batch["x_motif"],
"coord_mask": batch["motif_mask"],
}
return Atom37NanometersCoorsSeqFeat(rel=True)(batch_coors)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No x_motif or motif_mask in batch, returning zeros for MotifRelativeCoordsSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class MotifSequenceSeqFeat(Feature):
"""Computes sequence feature from motif."""
def __init__(self, **kwargs):
super().__init__(dim=20) # 20 for one-hot encoded residues
self._has_logged = False
def forward(self, batch):
if "seq_motif" in batch and "seq_motif_mask" in batch:
batch_seq = {
"residue_type": batch["seq_motif"],
"mask_dict": {
"residue_type": batch["seq_motif_mask"],
},
}
return ResidueTypeSeqFeat()(batch_seq)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No seq_motif or seq_motif_mask in batch, returning zeros for MotifSequenceSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class MotifSideChainAnglesSeqFeat(Feature):
"""Computes side chain angles feature from motif."""
def __init__(self, **kwargs):
super().__init__(dim=88) # 4 * 21 + 4 for side chain angles
self._has_logged = False
def forward(self, batch):
if "x_motif" in batch and "motif_mask" in batch and "seq_motif" in batch:
batch_sc_angles = {
"residue_type": batch["seq_motif"],
"coords": batch["x_motif"],
"coord_mask": batch["motif_mask"],
}
return OpenfoldSideChainAnglesSeqFeat()(batch_sc_angles)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"Missing required motif data in batch, returning zeros for MotifSideChainAnglesSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class MotifTorsionAnglesSeqFeat(Feature):
"""Computes torsion angles feature from motif."""
def __init__(self, **kwargs):
super().__init__(dim=63) # 3 * 21 for torsion angles
self._has_logged = False
def forward(self, batch):
if "x_motif" in batch and "motif_mask" in batch and "seq_motif_mask" in batch:
backbone_atoms = torch.tensor(
[
atom_types.index("N"),
atom_types.index("CA"),
atom_types.index("C"),
atom_types.index("O"),
],
device=batch["motif_mask"].device,
)
motif_mask_per_residue_backbone = torch.any(
batch["motif_mask"][:, :, backbone_atoms], dim=-1
) # [batch, seq_len]
relevant_motif_mask = torch.where(
batch["seq_motif_mask"],
motif_mask_per_residue_backbone,
torch.ones_like(motif_mask_per_residue_backbone, dtype=torch.bool),
)
if not torch.all(relevant_motif_mask):
if not self._has_logged:
logger.warning(
"Missing backbone atoms in motif region, returning zeros"
)
self._has_logged = True
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
return torch.zeros(b, n, self.dim, device=device)
batch_torsion_angles = {
"coords": batch["x_motif"],
"residue_pdb_idx": batch.get("residue_pdb_idx", None),
}
return BackboneTorsionAnglesSeqFeat()(batch_torsion_angles)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No x_motif or motif_mask in batch, returning zeros for MotifTorsionAnglesSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class XmotifBulkTipSeqFeat(Feature):
"""Computes feature from x_motif coordinates, seq feature of shape [b, n, 3] or [b, na, 3]."""
def __init__(self, **kwargs):
super().__init__(dim=None) # dim will be fixed
self.const_coors_abs = Atom37NanometersCoorsSeqFeat(rel=False)
self.const_seq = ResidueTypeSeqFeat()
dim = self.const_coors_abs.dim + self.const_seq.dim + 37
self.dim = dim # fix dim
def forward(self, batch):
if "x_motif" in batch:
# Coordinates features
batch_coors = {
"coords_nm": batch["x_motif"], # [b, n, 37, 3]
"coord_mask": batch["motif_mask"], # [b, n, 37]
}
feat_coors_abs = self.const_coors_abs(batch_coors) # [b, n, some #]
# Sequence features
seq_mask = batch["motif_mask"].sum(-1).bool() # [b, n]
batch_seq = {
"residue_type": batch["seq_motif"], # [b, n]
"mask_dict": {
"residue_type": seq_mask,
},
}
feat_seq = self.const_seq(batch_seq) # [b, n, some #]
# motif mask
motif_mask = batch["motif_mask"] * 1.0 # [b, n, 37]
# concatenate all features
feat = torch.cat([feat_coors_abs, feat_seq, motif_mask], dim=-1) # [b, n, some # added up]
feat = feat * seq_mask[..., None] # [b, n, some # added up]
return feat
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
return torch.zeros(b, n, self.dim, device=device)
class MotifMaskSeqFeat(Feature):
"""Computes motif mask feature."""
def __init__(self, **kwargs):
super().__init__(dim=37) # 37 for atom mask
self._has_logged = False
def forward(self, batch):
if "motif_mask" in batch:
return batch["motif_mask"] * 1.0 # [b, n, 37]
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No motif_mask in batch, returning zeros for MotifMaskSeqFeat"
)
self._has_logged = True
return torch.zeros(b, n, self.dim, device=device)
class XmotifSeqFeatUnindexed(Feature):
"""Computes feature from x_motif coordinates, seq feature of shape [b, n, 3] or [b, na, 3]."""
def __init__(self, **kwargs):
super().__init__(dim=None) # dim will be fixed
self.const_coors_abs = Atom37NanometersCoorsSeqFeat(rel=False)
self.const_seq = ResidueTypeSeqFeat()
dim = self.const_coors_abs.dim + self.const_seq.dim + 37
self.dim = dim
def forward(self, batch):
if "x_motif" in batch:
# Coordinates features
batch_coors = {
"coords_nm": batch["x_motif"], # [b, n, 37, 3]
"coord_mask": batch["motif_mask"], # [b, n, 37]
}
feat_coors_abs = self.const_coors_abs(batch_coors) # [b, n, some #]
# Sequence features
seq_mask = batch["motif_mask"].sum(-1).bool() # [b, n]
batch_seq = {
"residue_type": batch["seq_motif"], # [b, n]
"mask_dict": {
"residue_type": seq_mask,
},
}
feat_seq = self.const_seq(batch_seq) # [b, n, some #]
# motif mask
motif_mask = batch["motif_mask"] * 1.0 # [b, n, 37]
motif_mask_residue = motif_mask.sum(-1).bool() # [b, n]
# concatenate all features
feat = torch.cat([feat_coors_abs, feat_seq, motif_mask], dim=-1) # [b, n, some # added up]
feat = feat * seq_mask[..., None] # [b, n, some # added up]
feats_ind = []
masks_ind = []
for b in range(feat.shape[0]):
feat_local = feat[b, ...] # [n, some # added up]
mask_local = motif_mask_residue[b, ...] # [n]
feat_local = feat_local[mask_local, ...] # [# motif residues for b element, some # added up]
mask_local = mask_local[mask_local] # [# motif residues for b element], should be all True
feat_local = feat_local * mask_local[..., None] # [# motif residues for b element, some # added up]
feats_ind.append(feat_local)
masks_ind.append(mask_local)
assert torch.all(mask_local), "Mask local wrong"
masks_motif_uidx = pad_sequence(masks_ind, batch_first=True, padding_value=False) # [b, n]
feats_motif_uidx = pad_sequence(feats_ind, batch_first=True, padding_value=0.0) # [b, n, some # added up]
return feats_motif_uidx, masks_motif_uidx
else:
raise IOError("No x_motif in batch")
# b, n = self.extract_bs_and_n(batch)
# device = self.extract_device(batch)
# return torch.zeros(b, n, self.dim, device=device)
class BulkAllAtomXmotifSeqFeat(Feature):
"""Computes feature from x_motif coordinates, seq feature of shape [b, n, 3] or [b, na, 3]."""
def __init__(self, **kwargs):
super().__init__(dim=None) # dim will be fixed
self.const_coors_abs = Atom37NanometersCoorsSeqFeat(rel=False)
self.const_coors_rel = Atom37NanometersCoorsSeqFeat(rel=True)
self.const_seq = ResidueTypeSeqFeat()
self.const_sc_angles = OpenfoldSideChainAnglesSeqFeat()
self.const_torsion_angles = BackboneTorsionAnglesSeqFeat()
dim = self.const_coors_abs.dim + self.const_coors_rel.dim + self.const_seq.dim + self.const_sc_angles.dim + self.const_torsion_angles.dim + 37
self.dim = dim
def forward(self, batch):
if "x_motif" in batch:
# Coordinates features
batch_coors = {
"coords_nm": batch["x_motif"], # [b, n, 37, 3]
"coord_mask": batch["motif_mask"], # [b, n, 37]
}
feat_coors_abs = self.const_coors_abs(batch_coors) # [b, n, some #]
feat_coors_rel = self.const_coors_rel(batch_coors) # [b, n, some #]
# Sequence features
seq_mask = batch["motif_mask"].sum(-1).bool() # [b, n]
batch_seq = {
"residue_type": batch["seq_motif"], # [b, n]
"mask_dict": {
"residue_type": seq_mask,
},
}
feat_seq = self.const_seq(batch_seq) # [b, n, some #]
# Side chain angles features
batch_sc_angles = {
"residue_type": batch["seq_motif"], # [b, n]
"coords": batch["x_motif"], # [b, n, 37, 3]
"coord_mask": batch["motif_mask"], # [b, n, 37]
}
feat_sc_angles = self.const_sc_angles(batch_sc_angles) # [b, n, some #]
if "residue_pdb_idx" in batch:
idx = batch["residue_pdb_idx"] # [b, n]
else:
self.assert_defaults_allowed(batch, "Relative sequence separation pair")
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
idx = torch.Tensor([[i + 1 for i in range(n)] for _ in range(b)]).to(
device
) # [b, n]
# Torsion angle features
batch_torsion_angles = {
"coords": batch["x_motif"], # [b, n, 37, 3]
"residue_pdb_idx": idx, # [b, n]
}
feat_torsion_angles = self.const_torsion_angles(batch_torsion_angles) # [b, n, some #]
# motif mask
motif_mask = batch["motif_mask"] * 1.0 # [b, n, 37]
# concatenate all features
feat = torch.cat([feat_coors_abs, feat_coors_rel, feat_seq, feat_sc_angles, feat_torsion_angles, motif_mask], dim=-1) # [b, n, some # added up]
feat = feat * seq_mask[..., None] # [b, n, some # added up]
return feat
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
return torch.zeros(b, n, self.dim, device=device)
class ChainIdxSeqFeat(Feature):
"""Gets chain idx feature (-1 for padding) and returns feature of shape [b, n, 1]."""
def __init__(self, **kwargs):
super().__init__(dim=1)
def forward(self, batch):
if "chains" in batch:
mask = batch["chains"].unsqueeze(-1) # [b, n, 1]
else:
raise ValueError("chains")
return mask
class SequenceSeparationPairFeat(Feature):
"""Computes sequence separation and returns feature of shape [b, n, n, seq_sep_dim]."""
def __init__(self, seq_sep_dim, **kwargs):
super().__init__(dim=seq_sep_dim)
def forward(self, batch):
if "residue_pdb_idx" in batch:
# no need to force 1 since taking difference
inds = batch["residue_pdb_idx"] # [b, n]
else:
self.assert_defaults_allowed(batch, "Relative sequence separation pair")
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
inds = torch.Tensor([[i + 1 for i in range(n)] for _ in range(b)]).to(
device
) # [b, n]
seq_sep = inds[:, :, None] - inds[:, None, :] # [b, n, n]
# Dimension should be odd, bins limits [-(dim/2-1), ..., -1.5, -0.5, 0.5, 1.5, ..., dim/2-1]
# gives dim-2 bins, and the first and last for values beyond the bin limits
assert (
self.dim % 2 == 1
), "Relative seq separation feature dimension must be odd and > 3"
# Create bins limits [..., -3.5, -2.5, -1.5, -0.5, 0.5, 1.5, 2.3, 3.5, ...]
# Equivalent to binning relative sequence separation
low = -(self.dim / 2.0 - 1)
high = self.dim / 2.0 - 1
bin_limits = torch.linspace(low, high, self.dim - 1, device=inds.device)
return bin_and_one_hot(seq_sep, bin_limits) # [b, n, n, seq_sep_dim]
class XtBBCAPairwiseDistancesPairFeat(Feature):
"""Computes pairwise distances for CA backbone atoms and returns feature of shape [b, n, n, dim_pair_dist]."""
def __init__(self, xt_pair_dist_dim, xt_pair_dist_min, xt_pair_dist_max, **kwargs):
super().__init__(dim=xt_pair_dist_dim)
self.min_dist = xt_pair_dist_min
self.max_dist = xt_pair_dist_max
def forward(self, batch):
data_modes_avail = [k for k in batch["x_t"]]
assert (
"bb_ca" in data_modes_avail
), f"`bb_ca` pair dist feature requested but key not available in data modes {data_modes_avail}"
return bin_pairwise_distances(
x=batch["x_t"]["bb_ca"],
min_dist=self.min_dist,
max_dist=self.max_dist,
dim=self.dim,
) # [b, n, n, pair_dist_dim]
class CaCoorsNanometersPairwiseDistancesPairFeat(Feature):
"""Computes pairwise distances for CA backbone atoms and returns feature of shape [b, n, n, dim_pair_dist]."""
def __init__(self, **kwargs):
super().__init__(dim=30)
self.min_dist = 0.1
self.max_dist = 3.0
def forward(self, batch):
assert (
"ca_coors_nm" in batch or "coords_nm" in batch
), f"`ca_coors_nm` pair dist feature requested but key `ca_coors_nm` nor `coords_nm` not available"
if "ca_coors_nm" in batch:
ca_coors = batch["ca_coors_nm"]
else:
ca_coors = batch["coords_nm"][:, :, 1, :]
return bin_pairwise_distances(
x=ca_coors,
min_dist=self.min_dist,
max_dist=self.max_dist,
dim=self.dim,
) # [b, n, n, pair_dist_dim]
class OptionalCaCoorsNanometersPairwiseDistancesPairFeat(
CaCoorsNanometersPairwiseDistancesPairFeat
):
"""
If `use_ca_coors_nm_feature` in batch and true, returns pair feature with CA pairwise distances binned, shape [b, n, n, nbins].
If `use_ca_coors_nm_feature` not in batch, defaults to False.
"""
def __init__(self, **kwargs):
super().__init__(**kwargs)
self._has_logged = False
def forward(self, batch):
if batch.get("use_ca_coors_nm_feature", False): # defaults to False
return super().forward(batch)
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"use_ca_coors_nm_feature disabled or not in batch, returning zeros for OptionalCaCoorsNanometersPairwiseDistancesPairFeat"
)
self._has_logged = True
return torch.zeros(b, n, n, self.dim, device=device)
class XscBBCAPairwiseDistancesPairFeat(Feature):
"""Computes pairwise distances for CA backbone atoms and returns feature of shape [b, n, n, dim_pair_dist]."""
def __init__(
self,
x_sc_pair_dist_dim,
x_sc_pair_dist_min,
x_sc_pair_dist_max,
mode_key="x_sc",
**kwargs,
):
super().__init__(dim=x_sc_pair_dist_dim)
self.min_dist = x_sc_pair_dist_min
self.max_dist = x_sc_pair_dist_max
self.mode_key = mode_key
self._has_logged = False
def forward(self, batch):
if self.mode_key in batch:
data_modes_avail = [k for k in batch[self.mode_key]]
assert (
"bb_ca" in data_modes_avail
), f"`bb_ca` sc/recycle pair dist feature requested but key not available in data modes {data_modes_avail}"
return bin_pairwise_distances(
x=batch[self.mode_key]["bb_ca"],
min_dist=self.min_dist,
max_dist=self.max_dist,
dim=self.dim,
) # [b, n, n, pair_dist_dim]
else:
# If we do not provide self-conditioning as input to the nn
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
f"No {self.mode_key} in batch, returning zeros for XscBBCAPairwiseDistancesPairFeat"
)
self._has_logged = True
return torch.zeros(b, n, n, self.dim, device=device)
class RelativeResidueOrientationPairFeat(Feature):
"""Computes pair feature with pairwise residue orientations.
See paper "Improved protein structure prediction using
predicted inter-residue orientations".
TODO: Impute beta carbon for Glycine
TODO: 20 as argument
"""
def __init__(self, **kwargs):
super().__init__(dim=int(5 * 21)) # 105
def forward(self, batch):
aatype = batch["residue_type"] # [b, n]
coords = batch["coords"] # [b, n, 37, 3]
atom_mask = batch["coord_mask"] # [b, n, 37]
mask = atom_mask[:, :, 1] # [b, n]
has_cb = atom_mask[:, :, 3] # [b, n] boolean, indicates if corresponding
pair_mask = mask[:, :, None] * mask[:, None, :] # [b, n, n] boolean
beta_carbon_pair_mask = (
has_cb[:, :, None] * has_cb[:, :, None]
) # [b, n, n] boolean
pair_mask = pair_mask * beta_carbon_pair_mask # [b, n, n]
N = coords[:, :, 0, :] # [b, n, 3]
CA = coords[:, :, 1, :] # [b, n, 3]
CB = coords[:, :, 3, :] # [b, n, 3]
N_p1, CA_p1, CB_p1 = map(
lambda v: v[:, :, None, :], (N, CA, CB)
) # Each [b, n, 1, 3]
N_p2, CA_p2, CB_p2 = map(
lambda v: v[:, None, :, :], (N, CA, CB)
) # Each [b, 1, n, 3]
theta_12 = signed_dihedral_angle(N_p1, CA_p1, CB_p1, CB_p2) # [b, n, n]
theta_21 = signed_dihedral_angle(N_p2, CA_p2, CB_p2, CB_p1) # [b, n, n]
phi_12 = bond_angles(CA_p1, CB_p1, CB_p2) # [b, n, n]
phi_21 = bond_angles(CA_p2, CB_p2, CB_p1) # [b, n, n]
w = signed_dihedral_angle(CA_p1, CB_p1, CB_p2, CA_p2) # [b, n, n]
angles = torch.stack(
[theta_12, theta_21, phi_12, phi_21, w], dim=-1
) # [b, n, n, 5]
angles_feat = bin_and_one_hot(
angles, torch.linspace(-torch.pi, torch.pi, 20, device=angles.device)
) # [b, n, n, 5, nbins]
angles_feat = einops.rearrange(
angles_feat, "b n m f d -> b n m (f d)"
) # [b, n, n, 5 * nbins]
angles_feat = angles_feat * pair_mask[..., None] # Mask padding
return angles_feat
class BackbonePairDistancesNanometerPairFeat(Feature):
"""
Computes pairwise distances between backbone atoms.
Position (i, j) encodes the distance between CA_i and
{N_j, CA_j, C_j, CB_j}.
"""
def __init__(self, **kwargs):
super().__init__(dim=int(4 * 21)) # 84
def forward(self, batch):
assert (
"coords_nm" in batch
), "`coords_nm` not in batch, cannot comptue BackbonePairDistancesNanometerPairFeat"
coords = batch["coords_nm"]
atom_mask = batch["coord_mask"] # [b, n, 37]
mask = atom_mask[:, :, 1] # [b, n]
pair_mask = mask[:, None, :] * mask[:, :, None] # [b, n, n]
has_cb = atom_mask[:, :, 3] # [b, n] boolean, indicates if corresponding
N = coords[:, :, 0, :] # [b, n, 3]
CA = coords[:, :, 1, :] # [b, n, 3]
C = coords[:, :, 2, :] # [b, n, 3]
CB = coords[:, :, 3, :] # [b, n, 3]
CA_i = CA[:, :, None, :] # [b, n, 1, 3]
N_j, CA_j, C_j, CB_j = map(
lambda v: v[:, None, :, :], (N, CA, C, CB)
) # Each [b, 1, n, 3]
CA_N, CA_CA, CA_C, CA_CB = map(
lambda v: torch.linalg.norm(v[0] - v[1], dim=-1),
((CA_i, N_j), (CA_i, CA_j), (CA_i, C_j), (CA_i, CB_j)),
) # Each shape [b, n, n]
# CA_X[..., i, j] has distance (nm) between CA[..., i] and X[..., j]
# Accomodate residues without CB
CA_CB = CA_CB * has_cb[:, None, :] # [b, n, n]
# Fix for mask
CA_N, CA_CA, CA_C, CA_CB = map(
lambda v: v * pair_mask,
(CA_N, CA_CA, CA_C, CA_CB),
) # Each shape [b, n, n]
bin_limits = torch.linspace(0.1, 2, 20, device=coords.device)
CA_N_feat, CA_CA_feat, CA_C_feat, CA_CB_feat = map(
lambda v: bin_and_one_hot(v, bin_limits=bin_limits),
(CA_N, CA_CA, CA_C, CA_CB),
) # Each [b, n, n, 21]
feat = torch.cat(
[CA_N_feat, CA_CA_feat, CA_C_feat, CA_CB_feat], dim=-1
) # [b, n, n, 4 * 21]
feat = feat * pair_mask[..., None]
return feat
class XmotifPairwiseDistancesPairFeat(Feature):
"""Computes pairwise distances for CA backbone motif atoms and returns feature of shape [b, n, n, dim_pair_dist]."""
def __init__(self, **kwargs):
super().__init__(dim=None)
self.const = BackbonePairDistancesNanometerPairFeat()
self.dim = self.const.dim # Fix dim, cannot put init here
self._has_logged = False
def forward(self, batch):
if "x_motif" in batch:
batch_bbpd = {
"coords_nm": batch["x_motif"], # [b, n, 37, 3]
"coord_mask": batch["motif_mask"], # [b, n, 37]
}
feat = self.const(batch_bbpd) # [b, n, n, some #]
return feat
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No x_motif in batch, returning zeros for XmotifPairwiseDistancesPairFeat"
)
self._has_logged = True
return torch.zeros(b, n, n, self.dim, device=device)
class ChainIdxPairFeat(Feature):
"""Gets chain idx feature (-1 for padding) and returns feature of shape [b, n, n, 1]."""
def __init__(self, **kwargs):
super().__init__(dim=1)
def forward(self, batch):
if "chains" in batch:
seq_mask = batch["chains"] # [b, n]
mask = torch.einsum("bi,bj->bij", seq_mask, seq_mask).unsqueeze(
-1
) # [b, n, n, 1]
else:
raise ValueError("chains")
return mask
class StochasticTranslationSeqFeat(Feature):
"""Gets stochastic translation from centering transform and returns feature of shape [b, n, 3]."""
def __init__(self, **kwargs):
super().__init__(dim=3)
self._has_logged = False
def forward(self, batch):
if "stochastic_translation" in batch:
b, n = self.extract_bs_and_n(batch)
translation = batch["stochastic_translation"] # [b, 3]
# Broadcast translation to all residues
mask = translation[:, None, :].expand(b, n, -1) # [b, n, 3]
else:
b, n = self.extract_bs_and_n(batch)
device = self.extract_device(batch)
if not self._has_logged:
logger.warning(
"No stochastic_translation in batch, returning zeros for StochasticTranslationSeqFeat"
)
self._has_logged = True
mask = torch.zeros((b, n, 3), device=device)
return mask
####################################
# # Class that produces features # #
####################################
class FeatureFactory(torch.nn.Module):
def __init__(
self,
feats: List[str],
dim_feats_out: int,
use_ln_out: bool,
mode: Literal["seq", "pair"],
**kwargs,
):
"""
Feature factory for creating sequence and pair features.
Sequence features include:
Time embeddings:
- "time_emb_bb_ca": Time embedding for backbone CA atoms
- "time_emb_local_latents": Time embedding for local latents
Position and structure:
- "res_seq_pdb_idx": Residue sequence position (requires ResidueSequencePositionPdbTransform)
- "chain_break_per_res": Chain break per residue (requires ChainBreakPerResidueTransform)
- "fold_emb": Fold embedding
Coordinates and angles:
- "x_sc_bb_ca": Self-conditioning backbone CA coordinates
- "x_recycle_bb_ca": Recycled backbone CA coordinates
- "x_sc_local_latents": Self-conditioning local latents
- "x_recycle_local_latents": Recycled local latents
- "xt_bb_ca": Target backbone CA coordinates
- "xt_local_latents": Target local latents
- "ca_coors_nm": CA coordinates in nanometers
- "ca_coors_nm_try": Try CA coordinates in nanometers
- "optional_ca_coors_nm_seq_feat": Optional CA coordinates in nanometers
- "x1_bb_angles": Backbone torsion angles
- "x1_bond_angles": Backbone bond angles
- "x1_sidechain_angles": Sidechain angles
Residue information:
- "x1_aatype": Residue type
- "optional_res_type_seq_feat": Optional residue type
- "x1_a37coors_nm": Atom37 coordinates in nanometers
- "x1_a37coors_nm_rel": Relative atom37 coordinates in nanometers
Motif and target:
- "x_motif": Motif coordinates and features
- "z_latent_seq": Latent variable sequence
Pair features include:
Distance features:
- "xt_bb_ca_pair_dists": Target backbone CA pairwise distances
- "x_sc_bb_ca_pair_dists": Self-conditioning backbone CA pairwise distances
- "x_recycle_bb_ca_pair_dists": Recycled backbone CA pairwise distances
- "ca_coors_nm_pair_dists": CA coordinates pairwise distances in nanometers
- "x1_bb_pair_dists_nm": Backbone pairwise distances in nanometers
- "optional_ca_pair_dist": Optional CA pairwise distances
- "x_motif_pair_dists": Motif pairwise distances
Sequence and time:
- "rel_seq_sep": Relative sequence separation
- "time_emb_bb_ca": Time embedding for backbone CA atoms
- "time_emb_local_latents": Time embedding for local latents
Structure and orientation:
- "x1_bb_pair_orientation": Relative residue orientation
- "chain_idx_pair": Chain index pairwise feature
"""
super().__init__()
self.mode = mode
self.ret_zero = True if (feats is None or len(feats) == 0) else False
if self.ret_zero:
logger.info("No features requested")
self.zero_creator = ZeroFeat(dim_feats_out=dim_feats_out, mode=mode)
return
self.feat_creators = torch.nn.ModuleList(
[self.get_creator(f, **kwargs) for f in feats]
)
self.ln_out = (
torch.nn.LayerNorm(dim_feats_out) if use_ln_out else torch.nn.Identity()
)
self.linear_out = torch.nn.Linear(
sum([c.get_dim() for c in self.feat_creators]), dim_feats_out, bias=False
)
def get_creator(self, f, **kwargs):
"""Returns the right class for the requested feature f (a string)."""
if self.mode == "seq":
# Time embeddings
if f == "time_emb_bb_ca":
return TimeEmbeddingSeqFeat(data_mode_use="bb_ca", **kwargs)
elif f == "time_emb_local_latents":
return TimeEmbeddingSeqFeat(data_mode_use="local_latents", **kwargs)
# Position and indexing
elif f == "res_seq_pdb_idx":
return IdxEmbeddingSeqFeat(**kwargs)
elif f == "chain_break_per_res":
return ChainBreakPerResidueSeqFeat(**kwargs)
elif f == "chain_idx_seq":
return ChainIdxSeqFeat(**kwargs)
elif f == "fold_emb":
return FoldEmbeddingSeqFeat(**kwargs)
elif f == "cropped_flag_seq":
return CroppedFlagSeqFeat()
# Basic residue information
elif f == "x1_aatype":
return ResidueTypeSeqFeat(**kwargs)
elif f == "optional_res_type_seq_feat":
return OptionalResidueTypeSeqFeat(**kwargs)
# Raw coordinate features
elif f == "ca_coors_nm":
return CaCoorsNanometersSeqFeat(**kwargs)
elif f == "ca_coors_nm_try":
return TryCaCoorsNanometersSeqFeat(**kwargs)
elif f == "optional_ca_coors_nm_seq_feat":
return OptionalCaCoorsNanometersSeqFeat(**kwargs)
elif f == "x1_a37coors_nm":
return Atom37NanometersCoorsSeqFeat(**kwargs)
elif f == "x1_a37coors_nm_rel":
return Atom37NanometersCoorsSeqFeat(rel=True, **kwargs)
# Diffusion/sampling coordinates
elif f == "xt_bb_ca":
return XtBBCASeqFeat(**kwargs)
elif f == "xt_local_latents":
return XtLocalLatentsSeqFeat(**kwargs)
elif f == "x_sc_bb_ca":
return XscBBCASeqFeat(**kwargs)
elif f == "x_recycle_bb_ca":
return XscBBCASeqFeat(mode_key="x_recycle", **kwargs)
elif f == "x_sc_local_latents":
return XscLocalLatentsSeqFeat(**kwargs)
elif f == "x_recycle_local_latents":
return XscLocalLatentsSeqFeat(mode_key="x_recycle", **kwargs)
# Structural features (angles)
elif f == "x1_bb_angles":
return BackboneTorsionAnglesSeqFeat(**kwargs)
elif f == "x1_bond_angles":
return BackboneBondAnglesSeqFeat(**kwargs)
elif f == "x1_sidechain_angles":
return OpenfoldSideChainAnglesSeqFeat(**kwargs)
# Latent variables
elif f == "z_latent_seq":
return LatentVariableSeqFeat(**kwargs)
# Motif features
elif f == "motif_abs_coords":
return MotifAbsoluteCoordsSeqFeat(**kwargs)
elif f == "motif_rel_coords":
return MotifRelativeCoordsSeqFeat(**kwargs)
elif f == "motif_seq":
return MotifSequenceSeqFeat(**kwargs)
elif f == "motif_sc_angles":
return MotifSideChainAnglesSeqFeat(**kwargs)
elif f == "motif_torsion_angles":
return MotifTorsionAnglesSeqFeat(**kwargs)
elif f == "motif_mask":
return MotifMaskSeqFeat(**kwargs)
#######################################################
# Some manual changes needed here for the motif task
#######################################################
# # Uncomment this for indexed + tip atom
# elif f == "x_motif":
# return XmotifBulkTipSeqFeat(**kwargs)
# Uncomment this for any other motif task
elif f == "bulk_all_atom_xmotif" or f == "x_motif":
return BulkAllAtomXmotifSeqFeat(**kwargs)
#######################################################
#######################################################
#######################################################
elif f == "bulk_all_atom_xmotif":
return BulkAllAtomXmotifSeqFeat(**kwargs)
# Centering
elif f == "stochastic_translation":
return StochasticTranslationSeqFeat(**kwargs)
# Special/utility features
elif f == "zero_feat_seq":
return ZeroFeat(**kwargs)
else:
raise IOError(f"Sequence feature {f} not implemented.")
elif self.mode == "pair":
# Time embeddings
if f == "time_emb_bb_ca":
return TimeEmbeddingPairFeat(data_mode_use="bb_ca", **kwargs)
elif f == "time_emb_local_latents":
return TimeEmbeddingPairFeat(data_mode_use="local_latents", **kwargs)
# Sequence separation
elif f == "rel_seq_sep":
return SequenceSeparationPairFeat(**kwargs)
# Distance features
elif f == "xt_bb_ca_pair_dists":
return XtBBCAPairwiseDistancesPairFeat(**kwargs)
elif f == "x_sc_bb_ca_pair_dists":
return XscBBCAPairwiseDistancesPairFeat(**kwargs)
elif f == "x_recycle_bb_ca_pair_dists":
return XscBBCAPairwiseDistancesPairFeat(mode_key="x_recycle", **kwargs)
elif f == "ca_coors_nm_pair_dists":
return CaCoorsNanometersPairwiseDistancesPairFeat(**kwargs)
elif f == "optional_ca_pair_dist":
return OptionalCaCoorsNanometersPairwiseDistancesPairFeat(**kwargs)
elif f == "x1_bb_pair_dists_nm":
return BackbonePairDistancesNanometerPairFeat(**kwargs)
elif f == "x_motif_pair_dists":
return XmotifPairwiseDistancesPairFeat(**kwargs)
# Structural and orientation features
elif f == "x1_bb_pair_orientation":
return RelativeResidueOrientationPairFeat(**kwargs)
# Chain and indexing features
elif f == "chain_idx_pair":
return ChainIdxPairFeat(**kwargs)
else:
raise IOError(f"Pair feature {f} not implemented.")
else:
raise IOError(
f"Wrong feature mode (creator): {self.mode}. Should be 'seq' or 'pair'."
)
def apply_padding_mask(self, feature_tensor, mask):
"""
Applies mask to features.
Args:
feature_tensor: tensor with requested features, shape [b, n, d] of [b, n, n, d] depending on self.mode ('seq' or 'pair')
mask: Binary mask, shape [b, n]
Returns:
Masked features, same shape as input tensor.
"""
if self.mode == "seq":
return feature_tensor * mask[..., None] # [b, n, d]
elif self.mode == "pair":
mask_pair = mask[:, None, :] * mask[:, :, None] # [b, n, n]
return feature_tensor * mask_pair[..., None] # [b, n, n, d]
else:
raise IOError(
f"Wrong feature mode (pad mask): {self.mode}. Should be 'seq' or 'pair'."
)
def forward(self, batch):
"""Returns masked features, shape depends on mode, either 'seq' or 'pair'."""
# If no features requested just return the zero tensor of appropriate dimensions
if self.ret_zero:
return self.zero_creator(batch)
# Compute requested features
feature_tensors = []
for fcreator in self.feat_creators:
feature_tensors.append(
fcreator(batch)
) # [b, n, dim_f] or [b, n, n, dim_f] if seq or pair mode
# Concatenate features and mask
features = torch.cat(
feature_tensors, dim=-1
) # [b, n, dim_f] or [b, n, n, dim_f]
features = self.apply_padding_mask(
features, batch["mask"]
) # [b, n, dim_f] or [b, n, n, dim_f]
# Linear layer and mask
features_proc = self.ln_out(
self.linear_out(features)
) # [b, n, dim_f] or [b, n, n, dim_f]
return self.apply_padding_mask(
features_proc, batch["mask"]
) # [b, n, dim_f] or [b, n, n, dim_f]
class FeatureFactoryUidxMotif(torch.nn.Module):
def __init__(
self,
# feats: List[str],
dim_feats_out: int,
use_ln_out: bool,
**kwargs,
):
"""
Sequence features include:
- "x_motix_uidx"
Pair features include:
"""
super().__init__()
self.feat_creator = XmotifSeqFeatUnindexed(**kwargs)
self.ln_out = (
torch.nn.LayerNorm(dim_feats_out) if use_ln_out else torch.nn.Identity()
)
self.linear_out = torch.nn.Linear(self.feat_creator.get_dim(), dim_feats_out, bias=False)
def forward(self, batch):
"""Returns feature and mask, shapes [b, n_motif, dim_f] and [b, n_motif]"""
feat, feat_mask = self.feat_creator(batch) # [b, n_motif, dim_f], [b, n_motif]
feat_proc = self.ln_out(self.linear_out(feat)) # [b, n_motif, dim_f]
return feat_proc * feat_mask[..., None], feat_mask